function ytdl(link, options) {
var stream = createStream(options);
getInfo(link, options, function(err, info) {
if (err) {
stream.emit('error', err);
return;
}
downloadFromInfoCallback(stream, info, options);
});
return stream;
}n/a
chooseFormat = function (formats, options) {
if (typeof options.format === 'object') {
return options.format;
}
if (options.filter) {
formats = exports.filterFormats(formats, options.filter);
if (formats.length === 0) {
return new Error('No formats found with custom filter');
}
}
var format;
var quality = options.quality || 'highest';
switch (quality) {
case 'highest':
format = formats[0];
break;
case 'lowest':
format = formats[formats.length - 1];
break;
default:
var getFormat = function(itag) {
for (var i = 0, len = formats.length; i < len; i++) {
if (formats[i].itag === '' + itag) {
return formats[i];
}
}
return null;
};
if (Array.isArray(quality)) {
for (var i = 0, len = quality.length; i < len; i++) {
format = getFormat(quality[i]);
if (format) { break; }
}
} else {
format = getFormat(quality);
}
}
if (!format) {
return new Error('No such format found: ' + quality);
} else if (format.rtmp) {
return new Error('rtmp protocol not supported');
}
return format;
}...
Use this if you only want to get metainfo from a video. If `callback` isn't given, returns a promise.
### ytdl.downloadFromInfo(info, options)
Once you have received metadata from a video with the `getInfo` function,
you may pass that `info`, along with other `options` to `downloadFromInfo`.
### ytdl.chooseFormat(formats, options)
Can be used if you'd like to choose a format yourself with the [options above](#ytdlurl-options).
### ytdl.filterFormats(formats, filter)
If you'd like to work with only some formats, you can use the [`filter` option above](#ytdlurl-options).
...downloadFromInfo = function (info, options) {
var stream = createStream(options);
setImmediate(function() {
downloadFromInfoCallback(stream, info, options);
});
return stream;
}...
Destroys the underlying connection.
### ytdl.getInfo(url, [options], [callback(err, info)])
Use this if you only want to get metainfo from a video. If `callback` isn't given, returns a promise.
### ytdl.downloadFromInfo(info, options)
Once you have received metadata from a video with the `getInfo` function,
you may pass that `info`, along with other `options` to `downloadFromInfo`.
### ytdl.chooseFormat(formats, options)
Can be used if you'd like to choose a format yourself with the [options above](#ytdlurl-options).
...filterFormats = function (formats, filter) {
var fn;
switch (filter) {
case 'video':
fn = function(format) { return format.bitrate; };
break;
case 'videoonly':
fn = function(format) { return format.bitrate && !format.audioBitrate; };
break;
case 'audio':
fn = function(format) { return format.audioBitrate; };
break;
case 'audioonly':
fn = function(format) { return !format.bitrate && format.audioBitrate; };
break;
default:
fn = filter;
}
return formats.filter(fn);
}...
Once you have received metadata from a video with the `getInfo` function,
you may pass that `info`, along with other `options` to `downloadFromInfo`.
### ytdl.chooseFormat(formats, options)
Can be used if you'd like to choose a format yourself with the [options above](#ytdlurl-options).
### ytdl.filterFormats(formats, filter)
If you'd like to work with only some formats, you can use the [`filter` option above](#ytdlurl-options).
## Limitations
ytdl cannot download videos that fall into the following
* Regionally restricted (requires a [proxy](example/proxy.js))
...function getInfo(link, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
if (!callback) {
return new Promise(function(resolve, reject) {
getInfo(link, options, function(err, info) {
if (err) return reject(err);
resolve(info);
});
});
}
if (options.request) {
console.warn('`options.request` is deprecated, please use `options.requestOptions.transform`');
}
var id = util.getVideoID(link);
if (id instanceof Error) return callback(id);
// Try getting config from the video page first.
var url = VIDEO_URL + id;
request(url, options.requestOptions, function(err, res, body) {
if (err) return callback(err);
// Check if there are any errors with this video page.
var unavailableMsg = util.between(body, '<div id="player-unavailable"', '>');
if (!/\bhid\b/.test(util.between(unavailableMsg, 'class="', '"'))) {
// Ignore error about age restriction.
if (body.indexOf('<div id="watch7-player-age-gate-content"') < 0) {
return callback(new Error(util.between(body,
'<h1 id="unavailable-message" class="message">', '</h1>').trim()));
}
}
// Parse out some additional informations since we already load that page.
var additional = {
// Get informations about the author/uploader.
author: util.getAuthor(body),
// Get the day the vid was published.
published: util.getPublished(body),
// Get description from #eow-description.
description: util.getVideoDescription(body),
// Get related videos.
related_videos: util.getRelatedVideos(body),
// Give the canonical link to the video.
video_url: url,
// Thumbnails.
iurlsd : THUMBNAIL_URL + id + '/sddefault.jpg',
iurlmq : THUMBNAIL_URL + id + '/mqdefault.jpg',
iurlhq : THUMBNAIL_URL + id + '/hqdefault.jpg',
iurlmaxres : THUMBNAIL_URL + id + '/maxresdefault.jpg',
};
var jsonStr = util.between(body, 'ytplayer.config = ', '</script>');
if (jsonStr) {
jsonStr = jsonStr.slice(0, jsonStr.lastIndexOf(';ytplayer.load'));
var config;
try {
config = JSON.parse(jsonStr);
} catch (err) {
return callback(new Error('Error parsing config: ' + err.message));
}
if (!config) {
return callback(new Error('Could not parse video page config'));
}
gotConfig(id, options, additional, config, callback);
} else {
// If the video page doesn't work, maybe because it has mature content.
// and requires an account logged into view, try the embed page.
url = EMBED_URL + id;
request(url, options.requestOptions, function(err, res, body) {
if (err) return callback(err);
config = util.between(body, 't.setConfig({\'PLAYER_CONFIG\': ', '},\'');
if (!config) {
return callback(new Error('Could not find `player config`'));
}
try {
config = JSON.parse(config + '}');
} catch (err) {
return callback(new Error('Error parsing config: ' + err.message));
}
gotConfig(id, options, additional, config, callback);
});
}
});
}...
Emitted whenever a new chunk is received. Passes values descriping the download progress and the parsed percentage.
### Stream#destroy()
Destroys the underlying connection.
### ytdl.getInfo(url, [options], [callback(err, info)])
Use this if you only want to get metainfo from a video. If `callback` isn't given, returns a promise.
### ytdl.downloadFromInfo(info, options)
Once you have received metadata from a video with the `getInfo` function,
you may pass that `info`, along with other `options` to `downloadFromInfo`.
...get = function (key) {
return exports.store[key];
}...
*/
exports.getTokens = function(html5playerfile, options, callback) {
var key, cachedTokens;
var rs = /(?:html5)?player-([a-zA-Z0-9\-_]+)(?:\.js|\/)/
.exec(html5playerfile);
if (rs) {
key = rs[1];
cachedTokens = cache.get(key);
} else {
console.warn('Could not extract html5player key:', html5playerfile);
}
if (cachedTokens) {
callback(null, cachedTokens);
} else {
request(html5playerfile, options.requestOptions, function(err, res, body) {
...reset = function () {
exports.store = {};
}n/a
set = function (key, value) {
exports.store[key] = value;
}...
var tokens = exports.extractActions(body);
if (key && (!tokens || !tokens.length)) {
callback(new Error('Could not extract signature deciphering actions'));
return;
}
cache.set(key, tokens);
callback(null, tokens);
});
}
};
/**
...decipher = function (tokens, sig) {
sig = sig.split('');
var pos;
for (var i = 0, len = tokens.length; i < len; i++) {
var token = tokens[i];
switch (token[0]) {
case 'r':
sig = sig.reverse();
break;
case 'w':
pos = ~~token.slice(1);
sig = swapHeadAndPosition(sig, pos);
break;
case 's':
pos = ~~token.slice(1);
sig = sig.slice(pos);
break;
case 'p':
pos = ~~token.slice(1);
sig.splice(0, pos);
break;
}
}
return sig.join('');
}...
/**
* @param {String} url
* @param {Array.<String>} tokens
*/
function decipherURL(url, tokens) {
return url.replace(/\/s\/([a-fA-F0-9\.]+)/, function(_, s) {
return '/signature/' + sig.decipher(tokens, s);
});
}
/**
* Merges formats from DASH or M3U8 with formats from video info page.
*
...decipherFormats = function (formats, tokens, debug) {
formats.forEach(function(format) {
var sig = tokens && format.s ? exports.decipher(tokens, format.s) : null;
exports.setDownloadURL(format, sig, debug);
});
}...
if (info.formats.some(function(f) { return !!f.s; }) ||
config.args.dashmpd || info.dashmpd || info.hlsvp) {
var html5playerfile = urllib.resolve(VIDEO_URL, config.assets.js);
sig.getTokens(html5playerfile, options, function(err, tokens) {
if (err) return callback(err);
sig.decipherFormats(info.formats, tokens, options.debug);
var funcs = [];
var dashmpd;
if (config.args.dashmpd) {
dashmpd = decipherURL(config.args.dashmpd, tokens);
funcs.push(getDashManifest.bind(null, dashmpd, options));
}
...extractActions = function (body) {
var objResult = actionsObjRegexp.exec(body);
var funcResult = actionsFuncRegexp.exec(body);
if (!objResult || !funcResult) { return null; }
var obj = objResult[1].replace(/\$/g, '\\$');
var objBody = objResult[2].replace(/\$/g, '\\$');
var funcbody = funcResult[1].replace(/\$/g, '\\$');
var result = reverseRegexp.exec(objBody);
var reverseKey = result && result[1].replace(/\$/g, '\\$');
result = sliceRegexp.exec(objBody);
var sliceKey = result && result[1].replace(/\$/g, '\\$');
result = spliceRegexp.exec(objBody);
var spliceKey = result && result[1].replace(/\$/g, '\\$');
result = swapRegexp.exec(objBody);
var swapKey = result && result[1].replace(/\$/g, '\\$');
var myreg = '(?:a=)?' + obj + '\\.(' +
[reverseKey, sliceKey, spliceKey, swapKey].join('|') + ')\\(a,(\\d+)\\)';
var tokenizeRegexp = new RegExp(myreg, 'g');
var tokens = [];
while ((result = tokenizeRegexp.exec(funcbody)) !== null) {
switch (result[1]) {
case swapKey:
tokens.push('w' + result[2]);
break;
case reverseKey:
tokens.push('r');
break;
case sliceKey:
tokens.push('s' + result[2]);
break;
case spliceKey:
tokens.push('p' + result[2]);
break;
}
}
return tokens;
}...
}
if (cachedTokens) {
callback(null, cachedTokens);
} else {
request(html5playerfile, options.requestOptions, function(err, res, body) {
if (err) return callback(err);
var tokens = exports.extractActions(body);
if (key && (!tokens || !tokens.length)) {
callback(new Error('Could not extract signature deciphering actions'));
return;
}
cache.set(key, tokens);
callback(null, tokens);
...getTokens = function (html5playerfile, options, callback) {
var key, cachedTokens;
var rs = /(?:html5)?player-([a-zA-Z0-9\-_]+)(?:\.js|\/)/
.exec(html5playerfile);
if (rs) {
key = rs[1];
cachedTokens = cache.get(key);
} else {
console.warn('Could not extract html5player key:', html5playerfile);
}
if (cachedTokens) {
callback(null, cachedTokens);
} else {
request(html5playerfile, options.requestOptions, function(err, res, body) {
if (err) return callback(err);
var tokens = exports.extractActions(body);
if (key && (!tokens || !tokens.length)) {
callback(new Error('Could not extract signature deciphering actions'));
return;
}
cache.set(key, tokens);
callback(null, tokens);
});
}
}...
// Add additional properties to info.
info = util.objectAssign(info, additional, false);
if (info.formats.some(function(f) { return !!f.s; }) ||
config.args.dashmpd || info.dashmpd || info.hlsvp) {
var html5playerfile = urllib.resolve(VIDEO_URL, config.assets.js);
sig.getTokens(html5playerfile, options, function(err, tokens) {
if (err) return callback(err);
sig.decipherFormats(info.formats, tokens, options.debug);
var funcs = [];
var dashmpd;
if (config.args.dashmpd) {
...setDownloadURL = function (format, sig, debug) {
var decodedUrl;
if (format.url) {
decodedUrl = format.url;
} else {
if (debug) {
console.warn('Download url not found for itag ' + format.itag);
}
return;
}
try {
decodedUrl = decodeURIComponent(decodedUrl);
} catch (err) {
if (debug) {
console.warn('Could not decode url: ' + err.message);
}
return;
}
// Make some adjustments to the final url.
var parsedUrl = url.parse(decodedUrl, true);
// Deleting the `search` part is necessary otherwise changes to
// `query` won't reflect when running `url.format()`
delete parsedUrl.search;
var query = parsedUrl.query;
// This is needed for a speedier download.
// See https://github.com/fent/node-ytdl-core/issues/127
query.ratebypass = 'yes';
if (sig) {
query.signature = sig;
}
format.url = url.format(parsedUrl);
}...
* @param {Array.<Object>} formats
* @param {Array.<String>} tokens
* @param {Boolean} debug
*/
exports.decipherFormats = function(formats, tokens, debug) {
formats.forEach(function(format) {
var sig = tokens && format.s ? exports.decipher(tokens, format.s) : null;
exports.setDownloadURL(format, sig, debug);
});
};
...addFormatMeta = function (format) {
var meta = FORMATS[format.itag];
for (var key in meta) {
format[key] = meta[key];
}
if (/\/live\/1\//.test(format.url)) {
format.live = true;
}
}n/a
between = function (haystack, left, right) {
var pos;
pos = haystack.indexOf(left);
if (pos === -1) { return ''; }
haystack = haystack.slice(pos + left.length);
pos = haystack.indexOf(right);
if (pos === -1) { return ''; }
haystack = haystack.slice(0, pos);
return haystack;
}...
// Try getting config from the video page first.
var url = VIDEO_URL + id;
request(url, options.requestOptions, function(err, res, body) {
if (err) return callback(err);
// Check if there are any errors with this video page.
var unavailableMsg = util.between(body, '<div id="player-unavailable
x22;', '>');
if (!/\bhid\b/.test(util.between(unavailableMsg, 'class="', '"'))) {
// Ignore error about age restriction.
if (body.indexOf('<div id="watch7-player-age-gate-content"') < 0) {
return callback(new Error(util.between(body,
'<h1 id="unavailable-message" class="message">', '</h1>').trim
()));
}
}
...chooseFormat = function (formats, options) {
if (typeof options.format === 'object') {
return options.format;
}
if (options.filter) {
formats = exports.filterFormats(formats, options.filter);
if (formats.length === 0) {
return new Error('No formats found with custom filter');
}
}
var format;
var quality = options.quality || 'highest';
switch (quality) {
case 'highest':
format = formats[0];
break;
case 'lowest':
format = formats[formats.length - 1];
break;
default:
var getFormat = function(itag) {
for (var i = 0, len = formats.length; i < len; i++) {
if (formats[i].itag === '' + itag) {
return formats[i];
}
}
return null;
};
if (Array.isArray(quality)) {
for (var i = 0, len = quality.length; i < len; i++) {
format = getFormat(quality[i]);
if (format) { break; }
}
} else {
format = getFormat(quality);
}
}
if (!format) {
return new Error('No such format found: ' + quality);
} else if (format.rtmp) {
return new Error('rtmp protocol not supported');
}
return format;
}...
Use this if you only want to get metainfo from a video. If `callback` isn't given, returns a promise.
### ytdl.downloadFromInfo(info, options)
Once you have received metadata from a video with the `getInfo` function,
you may pass that `info`, along with other `options` to `downloadFromInfo`.
### ytdl.chooseFormat(formats, options)
Can be used if you'd like to choose a format yourself with the [options above](#ytdlurl-options).
### ytdl.filterFormats(formats, filter)
If you'd like to work with only some formats, you can use the [`filter` option above](#ytdlurl-options).
...filterFormats = function (formats, filter) {
var fn;
switch (filter) {
case 'video':
fn = function(format) { return format.bitrate; };
break;
case 'videoonly':
fn = function(format) { return format.bitrate && !format.audioBitrate; };
break;
case 'audio':
fn = function(format) { return format.audioBitrate; };
break;
case 'audioonly':
fn = function(format) { return !format.bitrate && format.audioBitrate; };
break;
default:
fn = filter;
}
return formats.filter(fn);
}...
Once you have received metadata from a video with the `getInfo` function,
you may pass that `info`, along with other `options` to `downloadFromInfo`.
### ytdl.chooseFormat(formats, options)
Can be used if you'd like to choose a format yourself with the [options above](#ytdlurl-options).
### ytdl.filterFormats(formats, filter)
If you'd like to work with only some formats, you can use the [`filter` option above](#ytdlurl-options).
## Limitations
ytdl cannot download videos that fall into the following
* Regionally restricted (requires a [proxy](example/proxy.js))
...fromHumanTime = function (time) {
if (typeof time === 'number') { return time; }
if (numberFormat.test(time)) { return +time; }
var firstFormat = timeFormat.exec(time);
if (firstFormat) {
return +(firstFormat[1] || 0) * timeUnits.h +
+(firstFormat[2] || 0) * timeUnits.m +
+(firstFormat[3] || 0) * timeUnits.s +
+(firstFormat[4] || 0);
} else {
var total = 0;
var r = /(\d+)(ms|s|m|h)/g;
var rs;
while ((rs = r.exec(time)) != null) {
total += +rs[1] * timeUnits[rs[2]];
}
return total;
}
}...
});
req.on('error', stream.emit.bind(stream, 'error'));
stream.destroy = req.end.bind(req);
req.pipe(stream);
} else {
if (options.begin) {
url += '&begin=' + util.fromHumanTime(options.begin);
}
doDownload(stream, url, options, {
trys: options.retries || 5,
range: {
start: options.range && options.range.start ? options.range.start : 0,
end: options.range && options.range.end ? options.range.end : -1,
},
...getAuthor = function (body) {
var ownerinfo = exports.between(body, '<div id="watch7-user-header" class=" spf-link ">', '<div id="watch8-action-buttons" class
="watch-action-buttons clearfix">');
if (ownerinfo === '') {
return {};
}
ownerinfo = new Entities().decode(ownerinfo);
var channelMatch = ownerinfo.match(authorRegexp);
var userMatch = ownerinfo.match(aliasRegExp);
return {
id: channelMatch[1],
name: channelMatch[2],
avatar: url.resolve(VIDEO_URL, exports.between(ownerinfo, 'data-thumb="', '"')),
user: userMatch ? userMatch[1] : null,
channel_url: 'https://www.youtube.com/channel/' + channelMatch[1],
user_url: userMatch ? 'https://www.youtube.com/user/' + userMatch[1] : null,
};
}...
'<h1 id="unavailable-message" class="message">', '</h1>').trim()));
}
}
// Parse out some additional informations since we already load that page.
var additional = {
// Get informations about the author/uploader.
author: util.getAuthor(body),
// Get the day the vid was published.
published: util.getPublished(body),
// Get description from #eow-description.
description: util.getVideoDescription(body),
...getPublished = function (body) {
return Date.parse(exports.between(body, '<meta itemprop="datePublished" content="', '">'));
}...
// Parse out some additional informations since we already load that page.
var additional = {
// Get informations about the author/uploader.
author: util.getAuthor(body),
// Get the day the vid was published.
published: util.getPublished(body),
// Get description from #eow-description.
description: util.getVideoDescription(body),
// Get related videos.
related_videos: util.getRelatedVideos(body),
...getRelatedVideos = function (body) {
var jsonStr = exports.between(body, '\'RELATED_PLAYER_ARGS\': {"rvs":', '},');
try {
jsonStr = JSON.parse(jsonStr);
}
catch (err) {
return [];
}
return jsonStr.split(',').map(function(link) {
return qs.parse(link);
});
}...
// Get the day the vid was published.
published: util.getPublished(body),
// Get description from #eow-description.
description: util.getVideoDescription(body),
// Get related videos.
related_videos: util.getRelatedVideos(body),
// Give the canonical link to the video.
video_url: url,
// Thumbnails.
iurlsd : THUMBNAIL_URL + id + '/sddefault.jpg',
iurlmq : THUMBNAIL_URL + id + '/mqdefault.jpg',
...getVideoDescription = function (html) {
var regex = /<p.*?id="eow-description".*?>(.+?)<\/p>[\n\r\s]*?<\/div>/im;
var description = html.match(regex);
return description ? new Entities().decode(description[1]
.replace(/\n/g, ' ')
.replace(/\s*<\s*br\s*\/?\s*>\s*/gi, '\n')
.replace(/<\s*\/\s*p\s*>\s*<\s*p[^>]*>/gi, '\n')
.replace(/<.*?>/gi, '')).trim() : ''
;
}...
// Get informations about the author/uploader.
author: util.getAuthor(body),
// Get the day the vid was published.
published: util.getPublished(body),
// Get description from #eow-description.
description: util.getVideoDescription(body),
// Get related videos.
related_videos: util.getRelatedVideos(body),
// Give the canonical link to the video.
video_url: url,
...getVideoID = function (link) {
if (idRegex.test(link)) {
return link;
}
var parsed = url.parse(link, true);
var id = parsed.query.v;
if (parsed.hostname === 'youtu.be' ||
(parsed.hostname === 'youtube.com' ||
parsed.hostname === 'www.youtube.com') && !id) {
var s = parsed.pathname.split('/');
id = s[s.length - 1];
}
if (!id) {
return new Error('No video id found: ' + link);
}
if (!idRegex.test(id)) {
return new Error('Video id (' + id + ') does not match expected format (' + idRegex.toString() + ')');
}
return id;
}...
});
}
if (options.request) {
console.warn('`options.request` is deprecated, please use `options.requestOptions.transform`');
}
var id = util.getVideoID(link);
if (id instanceof Error) return callback(id);
// Try getting config from the video page first.
var url = VIDEO_URL + id;
request(url, options.requestOptions, function(err, res, body) {
if (err) return callback(err);
...objectAssign = function (target, source, deep) {
for (var key in source) {
if (deep && typeof source[key] === 'object' && source[key] != null &&
target[key]) {
exports.objectAssign(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}...
info.fmt_list.map(function(format) {
return format.split('/');
}) : [];
info.formats = util.parseFormats(info);
// Add additional properties to info.
info = util.objectAssign(info, additional, false);
if (info.formats.some(function(f) { return !!f.s; }) ||
config.args.dashmpd || info.dashmpd || info.hlsvp) {
var html5playerfile = urllib.resolve(VIDEO_URL, config.assets.js);
sig.getTokens(html5playerfile, options, function(err, tokens) {
if (err) return callback(err);
...parallel = function (funcs, callback) {
var funcsDone = 0;
var len = funcs.length;
var errGiven = false;
var results = [];
function checkDone(index, err, result) {
if (errGiven) { return; }
if (err) {
errGiven = true;
callback(err);
return;
}
results[index] = result;
if (++funcsDone === len) {
callback(null, results);
}
}
if (len > 0) {
for (var i = 0; i < len; i++) {
funcs[i](checkDone.bind(null, i));
}
} else {
callback(null, results);
}
}...
}
if (info.hlsvp) {
info.hlsvp = decipherURL(info.hlsvp, tokens);
funcs.push(getM3U8.bind(null, info.hlsvp, options));
}
util.parallel(funcs, function(err, results) {
if (err) return callback(err);
if (results[0]) { mergeFormats(info, results[0]); }
if (results[1]) { mergeFormats(info, results[1]); }
if (results[2]) { mergeFormats(info, results[2]); }
if (!info.formats.length) {
callback(new Error('No formats found'));
return;
...parseFormats = function (info) {
var formats = [];
if (info.url_encoded_fmt_stream_map) {
formats = formats
.concat(info.url_encoded_fmt_stream_map.split(','));
}
if (info.adaptive_fmts) {
formats = formats.concat(info.adaptive_fmts.split(','));
}
formats = formats
.map(function(format) { return qs.parse(format); });
delete info.url_encoded_fmt_stream_map;
delete info.adaptive_fmts;
return formats;
}...
});
info.fmt_list = info.fmt_list ?
info.fmt_list.map(function(format) {
return format.split('/');
}) : [];
info.formats = util.parseFormats(info);
// Add additional properties to info.
info = util.objectAssign(info, additional, false);
if (info.formats.some(function(f) { return !!f.s; }) ||
config.args.dashmpd || info.dashmpd || info.hlsvp) {
var html5playerfile = urllib.resolve(VIDEO_URL, config.assets.js);
...parseTime = function (time) {
var result = timeRegexp.exec(time.toString());
var hours = result[1] || 0;
var mins = result[2] || 0;
var secs = result[3] || 0;
var ms = result[4] || 0;
return hours * 3600000 + mins * 60000 + secs * 1000 + parseInt(ms, 10);
}n/a
sortFormats = function (a, b) {
var ares = a.resolution ? parseInt(a.resolution.slice(0, -1), 10) : 0;
var bres = b.resolution ? parseInt(b.resolution.slice(0, -1), 10) : 0;
var afeats = ~~!!ares * 2 + ~~!!a.audioBitrate;
var bfeats = ~~!!bres * 2 + ~~!!b.audioBitrate;
function getBitrate(c) {
if (c.bitrate) {
var s = c.bitrate.split('-');
return parseFloat(s[s.length - 1], 10);
} else {
return 0;
}
}
function audioScore(c) {
var abitrate = c.audioBitrate || 0;
var aenc = audioEncodingRanks[c.audioEncoding] || 0;
return abitrate + aenc / 10;
}
if (afeats === bfeats) {
if (ares === bres) {
var avbitrate = getBitrate(a);
var bvbitrate = getBitrate(b);
if (avbitrate === bvbitrate) {
var aascore = audioScore(a);
var bascore = audioScore(b);
if (aascore === bascore) {
var avenc = videoEncodingRanks[a.encoding] || 0;
var bvenc = videoEncodingRanks[b.encoding] || 0;
return bvenc - avenc;
} else {
return bascore - aascore;
}
} else {
return bvbitrate - avbitrate;
}
} else {
return bres - ares;
}
} else {
return bfeats - afeats;
}
}n/a