1
0
mirror of https://github.com/musix-org/musix-oss synced 2025-06-16 18:56:00 +00:00
This commit is contained in:
MatteZ02
2019-05-30 12:06:47 +03:00
parent cbdffcf19c
commit 5eb0264906
2502 changed files with 360854 additions and 0 deletions

737
node_modules/ytdl-core/lib/formats.js generated vendored Normal file
View File

@ -0,0 +1,737 @@
/**
* http://en.wikipedia.org/wiki/YouTube#Quality_and_formats
*/
module.exports = {
'5': {
container: 'flv',
resolution: '240p',
encoding: 'Sorenson H.283',
profile: null,
bitrate: '0.25',
audioEncoding: 'mp3',
audioBitrate: 64,
},
'6': {
container: 'flv',
resolution: '270p',
encoding: 'Sorenson H.263',
profile: null,
bitrate: '0.8',
audioEncoding: 'mp3',
audioBitrate: 64,
},
'13': {
container: '3gp',
resolution: null,
encoding: 'MPEG-4 Visual',
profile: null,
bitrate: '0.5',
audioEncoding: 'aac',
audioBitrate: null,
},
'17': {
container: '3gp',
resolution: '144p',
encoding: 'MPEG-4 Visual',
profile: 'simple',
bitrate: '0.05',
audioEncoding: 'aac',
audioBitrate: 24,
},
'18': {
container: 'mp4',
resolution: '360p',
encoding: 'H.264',
profile: 'baseline',
bitrate: '0.5',
audioEncoding: 'aac',
audioBitrate: 96,
},
'22': {
container: 'mp4',
resolution: '720p',
encoding: 'H.264',
profile: 'high',
bitrate: '2-3',
audioEncoding: 'aac',
audioBitrate: 192,
},
'34': {
container: 'flv',
resolution: '360p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.5',
audioEncoding: 'aac',
audioBitrate: 128,
},
'35': {
container: 'flv',
resolution: '480p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.8-1',
audioEncoding: 'aac',
audioBitrate: 128,
},
'36': {
container: '3gp',
resolution: '240p',
encoding: 'MPEG-4 Visual',
profile: 'simple',
bitrate: '0.175',
audioEncoding: 'aac',
audioBitrate: 32,
},
'37': {
container: 'mp4',
resolution: '1080p',
encoding: 'H.264',
profile: 'high',
bitrate: '3-5.9',
audioEncoding: 'aac',
audioBitrate: 192,
},
'38': {
container: 'mp4',
resolution: '3072p',
encoding: 'H.264',
profile: 'high',
bitrate: '3.5-5',
audioEncoding: 'aac',
audioBitrate: 192,
},
'43': {
container: 'webm',
resolution: '360p',
encoding: 'VP8',
profile: null,
bitrate: '0.5-0.75',
audioEncoding: 'vorbis',
audioBitrate: 128,
},
'44': {
container: 'webm',
resolution: '480p',
encoding: 'VP8',
profile: null,
bitrate: '1',
audioEncoding: 'vorbis',
audioBitrate: 128,
},
'45': {
container: 'webm',
resolution: '720p',
encoding: 'VP8',
profile: null,
bitrate: '2',
audioEncoding: 'vorbis',
audioBitrate: 192,
},
'46': {
container: 'webm',
resolution: '1080p',
encoding: 'vp8',
profile: null,
bitrate: null,
audioEncoding: 'vorbis',
audioBitrate: 192,
},
'82': {
container: 'mp4',
resolution: '360p',
encoding: 'H.264',
profile: '3d',
bitrate: '0.5',
audioEncoding: 'aac',
audioBitrate: 96,
},
'83': {
container: 'mp4',
resolution: '240p',
encoding: 'H.264',
profile: '3d',
bitrate: '0.5',
audioEncoding: 'aac',
audioBitrate: 96,
},
'84': {
container: 'mp4',
resolution: '720p',
encoding: 'H.264',
profile: '3d',
bitrate: '2-3',
audioEncoding: 'aac',
audioBitrate: 192,
},
'85': {
container: 'mp4',
resolution: '1080p',
encoding: 'H.264',
profile: '3d',
bitrate: '3-4',
audioEncoding: 'aac',
audioBitrate: 192,
},
'100': {
container: 'webm',
resolution: '360p',
encoding: 'VP8',
profile: '3d',
bitrate: null,
audioEncoding: 'vorbis',
audioBitrate: 128,
},
'101': {
container: 'webm',
resolution: '360p',
encoding: 'VP8',
profile: '3d',
bitrate: null,
audioEncoding: 'vorbis',
audioBitrate: 192,
},
'102': {
container: 'webm',
resolution: '720p',
encoding: 'VP8',
profile: '3d',
bitrate: null,
audioEncoding: 'vorbis',
audioBitrate: 192,
},
// DASH (video only)
'133': {
container: 'mp4',
resolution: '240p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.2-0.3',
audioEncoding: null,
audioBitrate: null,
},
'134': {
container: 'mp4',
resolution: '360p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.3-0.4',
audioEncoding: null,
audioBitrate: null,
},
'135': {
container: 'mp4',
resolution: '480p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.5-1',
audioEncoding: null,
audioBitrate: null,
},
'136': {
container: 'mp4',
resolution: '720p',
encoding: 'H.264',
profile: 'main',
bitrate: '1-1.5',
audioEncoding: null,
audioBitrate: null,
},
'137': {
container: 'mp4',
resolution: '1080p',
encoding: 'H.264',
profile: 'high',
bitrate: '2.5-3',
audioEncoding: null,
audioBitrate: null,
},
'138': {
container: 'mp4',
resolution: '4320p',
encoding: 'H.264',
profile: 'high',
bitrate: '13.5-25',
audioEncoding: null,
audioBitrate: null,
},
'160': {
container: 'mp4',
resolution: '144p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.1',
audioEncoding: null,
audioBitrate: null,
},
'242': {
container: 'webm',
resolution: '240p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '0.1-0.2',
audioEncoding: null,
audioBitrate: null,
},
'243': {
container: 'webm',
resolution: '360p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '0.25',
audioEncoding: null,
audioBitrate: null,
},
'244': {
container: 'webm',
resolution: '480p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '0.5',
audioEncoding: null,
audioBitrate: null,
},
'247': {
container: 'webm',
resolution: '720p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '0.7-0.8',
audioEncoding: null,
audioBitrate: null,
},
'248': {
container: 'webm',
resolution: '1080p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '1.5',
audioEncoding: null,
audioBitrate: null,
},
'264': {
container: 'mp4',
resolution: '1440p',
encoding: 'H.264',
profile: 'high',
bitrate: '4-4.5',
audioEncoding: null,
audioBitrate: null,
},
'266': {
container: 'mp4',
resolution: '2160p',
encoding: 'H.264',
profile: 'high',
bitrate: '12.5-16',
audioEncoding: null,
audioBitrate: null,
},
'271': {
container: 'webm',
resolution: '1440p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '9',
audioEncoding: null,
audioBitrate: null,
},
'272': {
container: 'webm',
resolution: '4320p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '20-25',
audioEncoding: null,
audioBitrate: null,
},
'278': {
container: 'webm',
resolution: '144p 15fps',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '0.08',
audioEncoding: null,
audioBitrate: null,
},
'298': {
container: 'mp4',
resolution: '720p',
encoding: 'H.264',
profile: 'main',
bitrate: '3-3.5',
audioEncoding: null,
audioBitrate: null,
},
'299': {
container: 'mp4',
resolution: '1080p',
encoding: 'H.264',
profile: 'high',
bitrate: '5.5',
audioEncoding: null,
audioBitrate: null,
},
'302': {
container: 'webm',
resolution: '720p HFR',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '2.5',
audioEncoding: null,
audioBitrate: null,
},
'303': {
container: 'webm',
resolution: '1080p HFR',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '5',
audioEncoding: null,
audioBitrate: null,
},
'308': {
container: 'webm',
resolution: '1440p HFR',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '10',
audioEncoding: null,
audioBitrate: null,
},
'313': {
container: 'webm',
resolution: '2160p',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '13-15',
audioEncoding: null,
audioBitrate: null,
},
'315': {
container: 'webm',
resolution: '2160p HFR',
encoding: 'VP9',
profile: 'profile 0',
bitrate: '20-25',
audioEncoding: null,
audioBitrate: null,
},
'330': {
container: 'webm',
resolution: '144p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '0.08',
audioEncoding: null,
audioBitrate: null,
},
'331': {
container: 'webm',
resolution: '240p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '0.1-0.15',
audioEncoding: null,
audioBitrate: null,
},
'332': {
container: 'webm',
resolution: '360p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '0.25',
audioEncoding: null,
audioBitrate: null,
},
'333': {
container: 'webm',
resolution: '240p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '0.5',
audioEncoding: null,
audioBitrate: null,
},
'334': {
container: 'webm',
resolution: '720p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '1',
audioEncoding: null,
audioBitrate: null,
},
'335': {
container: 'webm',
resolution: '1080p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '1.5-2',
audioEncoding: null,
audioBitrate: null,
},
'336': {
container: 'webm',
resolution: '1440p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '5-7',
audioEncoding: null,
audioBitrate: null,
},
'337': {
container: 'webm',
resolution: '2160p HDR, HFR',
encoding: 'VP9',
profile: 'profile 2',
bitrate: '12-14',
audioEncoding: null,
audioBitrate: null,
},
// DASH (audio only)
'139': {
container: 'mp4',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'aac',
audioBitrate: 48,
},
'140': {
container: 'm4a',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'aac',
audioBitrate: 128,
},
'141': {
container: 'mp4',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'aac',
audioBitrate: 256,
},
'171': {
container: 'webm',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'vorbis',
audioBitrate: 128,
},
'172': {
container: 'webm',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'vorbis',
audioBitrate: 192,
},
'249': {
container: 'webm',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'opus',
audioBitrate: 48,
},
'250': {
container: 'webm',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'opus',
audioBitrate: 64,
},
'251': {
container: 'webm',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'opus',
audioBitrate: 160,
},
// Live streaming
'91': {
container: 'ts',
resolution: '144p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.1',
audioEncoding: 'aac',
audioBitrate: 48,
},
'92': {
container: 'ts',
resolution: '240p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.15-0.3',
audioEncoding: 'aac',
audioBitrate: 48,
},
'93': {
container: 'ts',
resolution: '360p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.5-1',
audioEncoding: 'aac',
audioBitrate: 128,
},
'94': {
container: 'ts',
resolution: '480p',
encoding: 'H.264',
profile: 'main',
bitrate: '0.8-1.25',
audioEncoding: 'aac',
audioBitrate: 128,
},
'95': {
container: 'ts',
resolution: '720p',
encoding: 'H.264',
profile: 'main',
bitrate: '1.5-3',
audioEncoding: 'aac',
audioBitrate: 256,
},
'96': {
container: 'ts',
resolution: '1080p',
encoding: 'H.264',
profile: 'high',
bitrate: '2.5-6',
audioEncoding: 'aac',
audioBitrate: 256,
},
'120': {
container: 'flv',
resolution: '720p',
encoding: 'H.264',
profile: 'Main@L3.1',
bitrate: '2',
audioEncoding: 'aac',
audioBitrate: 128,
},
'127': {
container: 'ts',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'aac',
audioBitrate: 96,
},
'128': {
container: 'ts',
resolution: null,
encoding: null,
profile: null,
bitrate: null,
audioEncoding: 'aac',
audioBitrate: 96,
},
'132': {
container: 'ts',
resolution: '240p',
encoding: 'H.264',
profile: 'baseline',
bitrate: '0.15-0.2',
audioEncoding: 'aac',
audioBitrate: 48,
},
'151': {
container: 'ts',
resolution: '720p',
encoding: 'H.264',
profile: 'baseline',
bitrate: '0.05',
audioEncoding: 'aac',
audioBitrate: 24,
},
};

156
node_modules/ytdl-core/lib/index.js generated vendored Normal file
View File

@ -0,0 +1,156 @@
const PassThrough = require('stream').PassThrough;
const getInfo = require('./info');
const util = require('./util');
const sig = require('./sig');
const request = require('miniget');
const m3u8stream = require('m3u8stream');
const parseTime = require('m3u8stream/lib/parse-time');
/**
* @param {string} link
* @param {!Object} options
* @return {ReadableStream}
*/
const ytdl = (link, options) => {
const stream = createStream(options);
ytdl.getInfo(link, options, (err, info) => {
if (err) {
stream.emit('error', err);
return;
}
downloadFromInfoCallback(stream, info, options);
});
return stream;
};
module.exports = ytdl;
ytdl.getBasicInfo = getInfo.getBasicInfo;
ytdl.getInfo = getInfo.getFullInfo;
ytdl.chooseFormat = util.chooseFormat;
ytdl.filterFormats = util.filterFormats;
ytdl.validateID = util.validateID;
ytdl.validateURL = util.validateURL;
ytdl.getURLVideoID = util.getURLVideoID;
ytdl.getVideoID = util.getVideoID;
ytdl.cache = {
sig: sig.cache,
info: getInfo.cache,
};
const createStream = (options) => {
const stream = new PassThrough({
highWaterMark: options && options.highWaterMark || null,
});
stream.destroy = () => { stream._isDestroyed = true; };
return stream;
};
/**
* Chooses a format to download.
*
* @param {stream.Readable} stream
* @param {Object} info
* @param {Object} options
*/
const downloadFromInfoCallback = (stream, info, options) => {
options = options || {};
const format = util.chooseFormat(info.formats, options);
if (format instanceof Error) {
// The caller expects this function to be async.
setImmediate(() => {
stream.emit('error', format);
});
return;
}
stream.emit('info', info, format);
if (stream._isDestroyed) { return; }
let contentLength, downloaded = 0;
const ondata = (chunk) => {
downloaded += chunk.length;
stream.emit('progress', chunk.length, downloaded, contentLength);
};
let req;
if (format.isHLS || format.isDashMPD) {
req = m3u8stream(format.url, {
chunkReadahead: +info.live_chunk_readahead,
begin: options.begin || format.live && Date.now(),
liveBuffer: options.liveBuffer,
requestOptions: options.requestOptions,
parser: format.isDashMPD ? 'dash-mpd' : 'm3u8',
id: format.itag,
});
req.on('progress', (segment, totalSegments) => {
stream.emit('progress', segment.size, segment.num, totalSegments);
});
} else {
if (options.begin) {
format.url += '&begin=' + parseTime.humanStr(options.begin);
}
let requestOptions = Object.assign({}, options.requestOptions, {
maxReconnects: 5
});
if (options.range && (options.range.start || options.range.end)) {
requestOptions.headers = Object.assign({}, requestOptions.headers, {
Range: `bytes=${options.range.start || '0'}-${options.range.end || ''}`
});
}
req = request(format.url, requestOptions);
req.on('response', (res) => {
if (stream._isDestroyed) { return; }
if (!contentLength) {
contentLength = parseInt(res.headers['content-length'], 10);
}
});
req.on('data', ondata);
}
stream.destroy = () => {
stream._isDestroyed = true;
if (req.abort) req.abort();
req.end();
req.removeListener('data', ondata);
req.unpipe();
};
// Forward events from the request to the stream.
[
'abort', 'request', 'response', 'error', 'retry', 'reconnect'
].forEach((event) => {
req.prependListener(event, (arg) => {
stream.emit(event, arg); });
});
req.pipe(stream);
};
/**
* Can be used to download video after its `info` is gotten through
* `ytdl.getInfo()`. In case the user might want to look at the
* `info` object before deciding to download.
*
* @param {Object} info
* @param {!Object} options
*/
ytdl.downloadFromInfo = (info, options) => {
const stream = createStream(options);
if (!info.full) {
throw new Error('Cannot use `ytdl.downloadFromInfo()` when called ' +
'with info from `ytdl.getBasicInfo()`');
}
setImmediate(() => {
downloadFromInfoCallback(stream, info, options);
});
return stream;
};

134
node_modules/ytdl-core/lib/info-extras.js generated vendored Normal file
View File

@ -0,0 +1,134 @@
const qs = require('querystring');
const url = require('url');
const Entities = require('html-entities').AllHtmlEntities;
const util = require('./util');
const VIDEO_URL = 'https://www.youtube.com/watch?v=';
const getMetaItem = (body, name) => {
return util.between(body, `<meta itemprop="${name}" content="`, '">');
};
/**
* Get video description from html
*
* @param {string} html
* @return {string}
*/
exports.getVideoDescription = (html) => {
const regex = /<p.*?id="eow-description".*?>(.+?)<\/p>[\n\r\s]*?<\/div>/im;
const description = html.match(regex);
return description ?
Entities.decode(util.stripHTML(description[1])) : '';
};
/**
* Get video media (extra information) from html
*
* @param {string} body
* @return {Object}
*/
exports.getVideoMedia = (body) => {
let mediainfo = util.between(body,
'<div id="watch-description-extras">',
'<div id="watch-discussion" class="branded-page-box yt-card">');
if (mediainfo === '') {
return {};
}
const regexp = /<h4 class="title">([\s\S]*?)<\/h4>[\s\S]*?<ul .*?class=".*?watch-info-tag-list">[\s\S]*?<li>([\s\S]*?)<\/li>(?:\s*?<li>([\s\S]*?)<\/li>)?/g;
const contentRegexp = /(?: - (\d{4}) \()?<a .*?(?:href="([^"]+)")?.*?>(.*?)<\/a>/;
const imgRegexp = /<img src="([^"]+)".*?>/;
const media = {};
const image = imgRegexp.exec(mediainfo);
if (image) {
media.image = url.resolve(VIDEO_URL, image[1]);
}
let match;
while ((match = regexp.exec(mediainfo)) != null) {
let [, key, value, detail] = match;
key = Entities.decode(key).trim().replace(/\s/g, '_').toLowerCase();
const content = contentRegexp.exec(value);
if (content) {
let [, year, mediaUrl, value2] = content;
if (year) {
media.year = parseInt(year);
} else if (detail) {
media.year = parseInt(detail);
}
value = value.slice(0, content.index);
if (key !== 'game' || value2 !== 'YouTube Gaming') {
value += value2;
}
media[key + '_url'] = url.resolve(VIDEO_URL, mediaUrl);
}
media[key] = Entities.decode(value);
}
return media;
};
/**
* Get video Owner from html.
*
* @param {string} body
* @return {Object}
*/
const userRegexp = /<a href="\/user\/([^"]+)/;
const verifiedRegexp = /<span .*?(aria-label="Verified")(.*?(?=<\/span>))/;
exports.getAuthor = (body) => {
let ownerinfo = util.between(body,
'<div id="watch7-user-header" class=" spf-link ">',
'<div id="watch8-action-buttons" class="watch-action-buttons clearfix">');
if (ownerinfo === '') {
return {};
}
const channelName = Entities.decode(util.between(util.between(
ownerinfo, '<div class="yt-user-info">', '</div>'), '>', '</a>'));
const userMatch = ownerinfo.match(userRegexp);
const verifiedMatch = ownerinfo.match(verifiedRegexp);
const channelID = getMetaItem(body, 'channelId');
const username = userMatch ? userMatch[1] : util.between(
util.between(body, '<span itemprop="author"', '</span>'), '/user/', '">');
return {
id: channelID,
name: channelName,
avatar: url.resolve(VIDEO_URL, util.between(ownerinfo,
'data-thumb="', '"')),
verified: !!verifiedMatch,
user: username,
channel_url: 'https://www.youtube.com/channel/' + channelID,
user_url: 'https://www.youtube.com/user/' + username,
};
};
/**
* Get video published at from html.
*
* @param {string} body
* @return {string}
*/
exports.getPublished = (body) => {
return Date.parse(getMetaItem(body, 'datePublished'));
};
/**
* Get video published at from html.
* Credits to https://github.com/paixaop.
*
* @param {string} body
* @return {Array.<Object>}
*/
exports.getRelatedVideos = (body) => {
let jsonStr = util.between(body, '\'RELATED_PLAYER_ARGS\': {"rvs":', '},');
try {
jsonStr = JSON.parse(jsonStr);
} catch (err) {
return [];
}
return jsonStr.split(',').map((link) => qs.parse(link));
};

376
node_modules/ytdl-core/lib/info.js generated vendored Normal file
View File

@ -0,0 +1,376 @@
const urllib = require('url');
const querystring = require('querystring');
const sax = require('sax');
const request = require('miniget');
const util = require('./util');
const extras = require('./info-extras');
const sig = require('./sig');
const FORMATS = require('./formats');
const VIDEO_URL = 'https://www.youtube.com/watch?v=';
const EMBED_URL = 'https://www.youtube.com/embed/';
const VIDEO_EURL = 'https://youtube.googleapis.com/v/';
const INFO_HOST = 'www.youtube.com';
const INFO_PATH = '/get_video_info';
const KEYS_TO_SPLIT = [
'fmt_list',
'fexp',
'watermark'
];
/**
* Gets info from a video without getting additional formats.
*
* @param {string} id
* @param {Object} options
* @param {Function(Error, Object)} callback
*/
exports.getBasicInfo = (id, options, callback) => {
// Try getting config from the video page first.
const params = 'hl=' + (options.lang || 'en');
let url = VIDEO_URL + id + '&' + params +
'&bpctr=' + Math.ceil(Date.now() / 1000);
// Remove header from watch page request.
// Otherwise, it'll use a different framework for rendering content.
const reqOptions = Object.assign({}, options.requestOptions);
reqOptions.headers = Object.assign({}, reqOptions.headers, {
'User-Agent': ''
});
request(url, reqOptions, (err, res, body) => {
if (err) return callback(err);
// Check if there are any errors with this video page.
const unavailableMsg = util.between(body, '<div id="player-unavailable"', '>');
if (unavailableMsg &&
!/\bhid\b/.test(util.between(unavailableMsg, 'class="', '"'))) {
// Ignore error about age restriction.
if (!body.includes('<div id="watch7-player-age-gate-content"')) {
return callback(Error(util.between(body,
'<h1 id="unavailable-message" class="message">', '</h1>').trim()));
}
}
// Parse out additional metadata from this page.
const additional = {
// Get the author/uploader.
author: extras.getAuthor(body),
// Get the day the vid was published.
published: extras.getPublished(body),
// Get description.
description: extras.getVideoDescription(body),
// Get media info.
media: extras.getVideoMedia(body),
// Get related videos.
related_videos: extras.getRelatedVideos(body),
// Give the standard link to the video.
video_url: VIDEO_URL + id,
};
const jsonStr = util.between(body, 'ytplayer.config = ', '</script>');
let config;
if (jsonStr) {
config = jsonStr.slice(0, jsonStr.lastIndexOf(';ytplayer.load'));
gotConfig(id, options, additional, config, false, callback);
} else {
// If the video page doesn't work, maybe because it has mature content.
// and requires an account logged in to view, try the embed page.
url = EMBED_URL + id + '?' + params;
request(url, options.requestOptions, (err, res, body) => {
if (err) return callback(err);
config = util.between(body, 't.setConfig({\'PLAYER_CONFIG\': ', /\}(,'|\}\);)/);
gotConfig(id, options, additional, config, true, callback);
});
}
});
};
/**
* @param {Object} id
* @param {Object} options
* @param {Object} additional
* @param {Object} config
* @param {boolean} fromEmbed
* @param {Function(Error, Object)} callback
*/
const gotConfig = (id, options, additional, config, fromEmbed, callback) => {
if (!config) {
return callback(Error('Could not find player config'));
}
try {
config = JSON.parse(config + (fromEmbed ? '}' : ''));
} catch (err) {
return callback(Error('Error parsing config: ' + err.message));
}
const url = urllib.format({
protocol: 'https',
host: INFO_HOST,
pathname: INFO_PATH,
query: {
video_id: id,
eurl: VIDEO_EURL + id,
ps: 'default',
gl: 'US',
hl: (options.lang || 'en'),
sts: config.sts,
},
});
request(url, options.requestOptions, (err, res, body) => {
if (err) return callback(err);
let info = querystring.parse(body);
if (info.status === 'fail') {
if (config.args && (config.args.fmt_list ||
config.args.url_encoded_fmt_stream_map || config.args.adaptive_fmts)) {
info = config.args;
info.no_embed_allowed = true;
} else {
return callback(
Error(`Code ${info.errorcode}: ${util.stripHTML(info.reason)}`));
}
}
const player_response = config.args.player_response || info.player_response;
if (player_response) {
try {
info.player_response = JSON.parse(player_response);
} catch (err) {
return callback(
Error('Error parsing `player_response`: ' + err.message));
}
let playability = info.player_response.playabilityStatus;
if (playability && playability.status === 'UNPLAYABLE') {
return callback(Error(playability.reason));
}
}
// Split some keys by commas.
KEYS_TO_SPLIT.forEach((key) => {
if (!info[key]) return;
info[key] = info[key]
.split(',')
.filter((v) => v !== '');
});
info.fmt_list = info.fmt_list ?
info.fmt_list.map((format) => format.split('/')) : [];
info.formats = util.parseFormats(info);
// Add additional properties to info.
Object.assign(info, additional);
info.age_restricted = fromEmbed;
info.html5player = config.assets.js;
callback(null, info);
});
};
/**
* Gets info from a video additional formats and deciphered URLs.
*
* @param {string} id
* @param {Object} options
* @param {Function(Error, Object)} callback
*/
exports.getFullInfo = (id, options, callback) => {
return exports.getBasicInfo(id, options, (err, info) => {
if (err) return callback(err);
const hasManifest =
info.player_response && info.player_response.streamingData && (
info.player_response.streamingData.dashManifestUrl ||
info.player_response.streamingData.hlsManifestUrl
);
if (info.formats.length || hasManifest) {
const html5playerfile = urllib.resolve(VIDEO_URL, info.html5player);
sig.getTokens(html5playerfile, options, (err, tokens) => {
if (err) return callback(err);
sig.decipherFormats(info.formats, tokens, options.debug);
let funcs = [];
if (hasManifest && info.player_response.streamingData.dashManifestUrl) {
let url = decipherURL(info.player_response.streamingData.dashManifestUrl, tokens);
funcs.push(getDashManifest.bind(null, url, options));
}
if (hasManifest && info.player_response.streamingData.hlsManifestUrl) {
let url = decipherURL(info.player_response.streamingData.hlsManifestUrl, tokens);
funcs.push(getM3U8.bind(null, url, options));
}
util.parallel(funcs, (err, results) => {
if (err) return callback(err);
if (results[0]) { mergeFormats(info, results[0]); }
if (results[1]) { mergeFormats(info, results[1]); }
if (!info.formats.length) {
callback(Error('No formats found'));
return;
}
if (options.debug) {
info.formats.forEach((format) => {
const itag = format.itag;
if (!FORMATS[itag]) {
console.warn(`No format metadata for itag ${itag} found`);
}
});
}
info.formats.forEach(util.addFormatMeta);
info.formats.sort(util.sortFormats);
info.full = true;
callback(null, info);
});
});
} else {
callback(Error('This video is unavailable'));
}
});
};
/**
* @param {string} url
* @param {Array.<string>} tokens
*/
const decipherURL = (url, tokens) => {
return url.replace(/\/s\/([a-fA-F0-9.]+)/, (_, s) => {
return '/signature/' + sig.decipher(tokens, s);
});
};
/**
* Merges formats from DASH or M3U8 with formats from video info page.
*
* @param {Object} info
* @param {Object} formatsMap
*/
const mergeFormats = (info, formatsMap) => {
info.formats.forEach((f) => {
if (!formatsMap[f.itag]) {
formatsMap[f.itag] = f;
}
});
info.formats = [];
for (let itag in formatsMap) { info.formats.push(formatsMap[itag]); }
};
/**
* Gets additional DASH formats.
*
* @param {string} url
* @param {Object} options
* @param {Function(!Error, Array.<Object>)} callback
*/
const getDashManifest = (url, options, callback) => {
let formats = {};
const parser = sax.parser(false);
parser.onerror = callback;
parser.onopentag = (node) => {
if (node.name === 'REPRESENTATION') {
const itag = node.attributes.ID;
formats[itag] = { itag, url };
}
};
parser.onend = () => { callback(null, formats); };
const req = request(urllib.resolve(VIDEO_URL, url), options.requestOptions);
req.setEncoding('utf8');
req.on('error', callback);
req.on('data', (chunk) => { parser.write(chunk); });
req.on('end', parser.close.bind(parser));
};
/**
* Gets additional formats.
*
* @param {string} url
* @param {Object} options
* @param {Function(!Error, Array.<Object>)} callback
*/
const getM3U8 = (url, options, callback) => {
url = urllib.resolve(VIDEO_URL, url);
request(url, options.requestOptions, (err, res, body) => {
if (err) return callback(err);
let formats = {};
body
.split('\n')
.filter((line) => /https?:\/\//.test(line))
.forEach((line) => {
const itag = line.match(/\/itag\/(\d+)\//)[1];
formats[itag] = { itag: itag, url: line };
});
callback(null, formats);
});
};
// Cached for getting basic/full info.
exports.cache = new Map();
exports.cache.timeout = 1000;
// Cache get info functions.
// In case a user wants to get a video's info before downloading.
for (let fnName of ['getBasicInfo', 'getFullInfo']) {
/**
* @param {string} link
* @param {Object} options
* @param {Function(Error, Object)} callback
*/
const fn = exports[fnName];
exports[fnName] = (link, options, callback) => {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (!options) {
options = {};
}
if (!callback) {
return new Promise((resolve, reject) => {
exports[fnName](link, options, (err, info) => {
if (err) return reject(err);
resolve(info);
});
});
}
const id = util.getVideoID(link);
if (id instanceof Error) return callback(id);
const key = [fnName, id, options.lang].join('-');
if (exports.cache.has(key)) {
callback(null, exports.cache.get(key));
} else {
fn(id, options, (err, info) => {
if (err) return callback(err);
exports.cache.set(key, info);
setTimeout(() => { exports.cache.delete(key); }, exports.cache.timeout);
callback(null, info);
});
}
};
}
// Export a few helpers.
exports.validateID = util.validateID;
exports.validateURL = util.validateURL;
exports.getURLVideoID = util.getURLVideoID;
exports.getVideoID = util.getVideoID;

264
node_modules/ytdl-core/lib/sig.js generated vendored Normal file
View File

@ -0,0 +1,264 @@
const url = require('url');
const request = require('miniget');
// A shared cache to keep track of html5player.js tokens.
exports.cache = new Map();
/**
* Extract signature deciphering tokens from html5player file.
*
* @param {string} html5playerfile
* @param {Object} options
* @param {Function(!Error, Array.<string>)} callback
*/
exports.getTokens = (html5playerfile, options, callback) => {
let key, cachedTokens;
const rs = /(?:html5)?player[-_]([a-zA-Z0-9\-_]+)(?:\.js|\/)/
.exec(html5playerfile);
if (rs) {
key = rs[1];
cachedTokens = exports.cache.get(key);
} else {
console.warn('Could not extract html5player key:', html5playerfile);
}
if (cachedTokens) {
callback(null, cachedTokens);
} else {
request(html5playerfile, options.requestOptions, (err, res, body) => {
if (err) return callback(err);
const tokens = exports.extractActions(body);
if (key && (!tokens || !tokens.length)) {
callback(Error('Could not extract signature deciphering actions'));
return;
}
exports.cache.set(key, tokens);
callback(null, tokens);
});
}
};
/**
* Decipher a signature based on action tokens.
*
* @param {Array.<string>} tokens
* @param {string} sig
* @return {string}
*/
exports.decipher = (tokens, sig) => {
sig = sig.split('');
for (let i = 0, len = tokens.length; i < len; i++) {
let token = tokens[i], pos;
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('');
};
/**
* Swaps the first element of an array with one of given position.
*
* @param {Array.<Object>} arr
* @param {number} position
* @return {Array.<Object>}
*/
const swapHeadAndPosition = (arr, position) => {
const first = arr[0];
arr[0] = arr[position % arr.length];
arr[position] = first;
return arr;
};
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
const jsEmptyStr = `(?:''|"")`;
const reverseStr = ':function\\(a\\)\\{' +
'(?:return )?a\\.reverse\\(\\)' +
'\\}';
const sliceStr = ':function\\(a,b\\)\\{' +
'return a\\.slice\\(b\\)' +
'\\}';
const spliceStr = ':function\\(a,b\\)\\{' +
'a\\.splice\\(0,b\\)' +
'\\}';
const swapStr = ':function\\(a,b\\)\\{' +
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
'\\}';
const actionsObjRegexp = new RegExp(
`var (${jsVarStr})=\\{((?:(?:` +
jsKeyStr + reverseStr + '|' +
jsKeyStr + sliceStr + '|' +
jsKeyStr + spliceStr + '|' +
jsKeyStr + swapStr +
'),?\\r?\\n?)+)\\};'
);
const actionsFuncRegexp = new RegExp(`function(?: ${jsVarStr})?\\(a\\)\\{` +
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
`((?:(?:a=)?${jsVarStr}` +
jsPropStr +
'\\(a,\\d+\\);)+)' +
`return a\\.join\\(${jsEmptyStr}\\)` +
'\\}'
);
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
/**
* Extracts the actions that should be taken to decipher a signature.
*
* This searches for a function that performs string manipulations on
* the signature. We already know what the 3 possible changes to a signature
* are in order to decipher it. There is
*
* * Reversing the string.
* * Removing a number of characters from the beginning.
* * Swapping the first character with another position.
*
* Note, `Array#slice()` used to be used instead of `Array#splice()`,
* it's kept in case we encounter any older html5player files.
*
* After retrieving the function that does this, we can see what actions
* it takes on a signature.
*
* @param {string} body
* @return {Array.<string>}
*/
exports.extractActions = (body) => {
const objResult = actionsObjRegexp.exec(body);
const funcResult = actionsFuncRegexp.exec(body);
if (!objResult || !funcResult) { return null; }
const obj = objResult[1].replace(/\$/g, '\\$');
const objBody = objResult[2].replace(/\$/g, '\\$');
const funcBody = funcResult[1].replace(/\$/g, '\\$');
let result = reverseRegexp.exec(objBody);
const reverseKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
result = sliceRegexp.exec(objBody);
const sliceKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
result = spliceRegexp.exec(objBody);
const spliceKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
result = swapRegexp.exec(objBody);
const swapKey = result && result[1]
.replace(/\$/g, '\\$')
.replace(/\$|^'|^"|'$|"$/g, '');
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
const myreg = '(?:a=)?' + obj +
`(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
'\\(a,(\\d+)\\)';
const tokenizeRegexp = new RegExp(myreg, 'g');
const tokens = [];
while ((result = tokenizeRegexp.exec(funcBody)) !== null) {
let key = result[1] || result[2] || result[3];
switch (key) {
case swapKey:
tokens.push('w' + result[4]);
break;
case reverseKey:
tokens.push('r');
break;
case sliceKey:
tokens.push('s' + result[4]);
break;
case spliceKey:
tokens.push('p' + result[4]);
break;
}
}
return tokens;
};
/**
* @param {Object} format
* @param {string} sig
* @param {boolean} debug
*/
exports.setDownloadURL = (format, sig, debug) => {
let 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.
const 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;
let 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);
};
/**
* Applies `sig.decipher()` to all format URL's.
*
* @param {Array.<Object>} formats
* @param {Array.<string>} tokens
* @param {boolean} debug
*/
exports.decipherFormats = (formats, tokens, debug) => {
formats.forEach((format) => {
const sig = tokens && format.s ? exports.decipher(tokens, format.s) : null;
exports.setDownloadURL(format, sig, debug);
});
};

414
node_modules/ytdl-core/lib/util.js generated vendored Normal file
View File

@ -0,0 +1,414 @@
const qs = require('querystring');
const url = require('url');
const FORMATS = require('./formats');
// Use these to help sort formats, higher is better.
const audioEncodingRanks = {
mp3: 1,
vorbis: 2,
aac: 3,
opus: 4,
flac: 5,
};
const videoEncodingRanks = {
'Sorenson H.283': 1,
'MPEG-4 Visual': 2,
'VP8': 3,
'VP9': 4,
'H.264': 5,
};
/**
* Sort formats from highest quality to lowest.
* By resolution, then video bitrate, then audio bitrate.
*
* @param {Object} a
* @param {Object} b
*/
exports.sortFormats = (a, b) => {
const ares = a.resolution ? parseInt(a.resolution.slice(0, -1), 10) : 0;
const bres = b.resolution ? parseInt(b.resolution.slice(0, -1), 10) : 0;
const afeats = ~~!!ares * 2 + ~~!!a.audioBitrate;
const bfeats = ~~!!bres * 2 + ~~!!b.audioBitrate;
const getBitrate = (c) => {
if (c.bitrate) {
let s = c.bitrate.split('-');
return parseFloat(s[s.length - 1], 10);
} else {
return 0;
}
};
const audioScore = (c) => {
const abitrate = c.audioBitrate || 0;
const aenc = audioEncodingRanks[c.audioEncoding] || 0;
return abitrate + aenc / 10;
};
if (afeats === bfeats) {
if (ares === bres) {
let avbitrate = getBitrate(a);
let bvbitrate = getBitrate(b);
if (avbitrate === bvbitrate) {
let aascore = audioScore(a);
let bascore = audioScore(b);
if (aascore === bascore) {
let avenc = videoEncodingRanks[a.encoding] || 0;
let bvenc = videoEncodingRanks[b.encoding] || 0;
return bvenc - avenc;
} else {
return bascore - aascore;
}
} else {
return bvbitrate - avbitrate;
}
} else {
return bres - ares;
}
} else {
return bfeats - afeats;
}
};
/**
* Choose a format depending on the given options.
*
* @param {Array.<Object>} formats
* @param {Object} options
* @return {Object|Error}
*/
exports.chooseFormat = (formats, options) => {
if (typeof options.format === 'object') {
return options.format;
}
if (options.filter) {
formats = exports.filterFormats(formats, options.filter);
if (formats.length === 0) {
return Error('No formats found with custom filter');
}
}
let format;
const quality = options.quality || 'highest';
const getBitrate = (f) => {
let s = f.bitrate.split('-');
return parseFloat(s[s.length - 1], 10);
};
switch (quality) {
case 'highest':
format = formats[0];
break;
case 'lowest':
format = formats[formats.length - 1];
break;
case 'highestaudio':
formats = exports.filterFormats(formats, 'audio');
format = null;
for (let f of formats) {
if (!format
|| f.audioBitrate > format.audioBitrate
|| (f.audioBitrate === format.audioBitrate && format.encoding && !f.encoding))
format = f;
}
break;
case 'lowestaudio':
formats = exports.filterFormats(formats, 'audio')
format = null;
for (let f of formats) {
if (!format
|| f.audioBitrate < format.audioBitrate
|| (f.audioBitrate === format.audioBitrate && format.encoding && !f.encoding))
format = f;
}
break;
case 'highestvideo':
formats = exports.filterFormats(formats, 'video');
format = null;
for (let f of formats) {
if (!format
|| getBitrate(f) > getBitrate(format)
|| (getBitrate(f) === getBitrate(format) && format.audioEncoding && !f.audioEncoding))
format = f;
}
break;
case 'lowestvideo':
formats = exports.filterFormats(formats, 'video')
format = null;
for (let f of formats) {
if (!format
|| getBitrate(f) < getBitrate(format)
|| (getBitrate(f) === getBitrate(format) && format.audioEncoding && !f.audioEncoding))
format = f;
}
break;
default: {
let getFormat = (itag) => {
return formats.find((format) => format.itag === '' + itag);
};
if (Array.isArray(quality)) {
quality.find((q) => format = getFormat(q));
} else {
format = getFormat(quality);
}
}
}
if (!format) {
return Error('No such format found: ' + quality);
}
return format;
};
/**
* @param {Array.<Object>} formats
* @param {Function} filter
* @return {Array.<Object>}
*/
exports.filterFormats = (formats, filter) => {
let fn;
switch (filter) {
case 'audioandvideo':
fn = (format) => format.bitrate && format.audioBitrate;
break;
case 'video':
fn = (format) => format.bitrate;
break;
case 'videoonly':
fn = (format) => format.bitrate && !format.audioBitrate;
break;
case 'audio':
fn = (format) => format.audioBitrate;
break;
case 'audioonly':
fn = (format) => !format.bitrate && format.audioBitrate;
break;
default:
if (typeof filter === 'function') {
fn = filter;
} else {
throw TypeError(`Given filter (${filter}) is not supported`);
}
}
return formats.filter(fn);
};
/**
* String#indexOf() that supports regex too.
*
* @param {string} haystack
* @param {string|RegExp} needle
* @return {number}
*/
const indexOf = (haystack, needle) => {
return needle instanceof RegExp ?
haystack.search(needle) : haystack.indexOf(needle);
};
/**
* Extract string inbetween another.
*
* @param {string} haystack
* @param {string} left
* @param {string} right
* @return {string}
*/
exports.between = (haystack, left, right) => {
let pos = indexOf(haystack, left);
if (pos === -1) { return ''; }
haystack = haystack.slice(pos + left.length);
pos = indexOf(haystack, right);
if (pos === -1) { return ''; }
haystack = haystack.slice(0, pos);
return haystack;
};
/**
* Get video ID.
*
* There are a few type of video URL formats.
* - https://www.youtube.com/watch?v=VIDEO_ID
* - https://m.youtube.com/watch?v=VIDEO_ID
* - https://youtu.be/VIDEO_ID
* - https://www.youtube.com/v/VIDEO_ID
* - https://www.youtube.com/embed/VIDEO_ID
* - https://music.youtube.com/watch?v=VIDEO_ID
* - https://gaming.youtube.com/watch?v=VIDEO_ID
*
* @param {string} link
* @return {string|Error}
*/
const validQueryDomains = new Set([
'youtube.com',
'www.youtube.com',
'm.youtube.com',
'music.youtube.com',
'gaming.youtube.com',
]);
const validPathDomains = new Set([
'youtu.be',
'youtube.com',
'www.youtube.com',
]);
exports.getURLVideoID = (link) => {
const parsed = url.parse(link, true);
let id = parsed.query.v;
if (validPathDomains.has(parsed.hostname) && !id) {
const paths = parsed.pathname.split('/');
id = paths[paths.length - 1];
} else if (parsed.hostname && !validQueryDomains.has(parsed.hostname)) {
return Error('Not a YouTube domain');
}
if (!id) {
return Error('No video id found: ' + link);
}
id = id.substring(0, 11);
if (!exports.validateID(id)) {
return TypeError(`Video id (${id}) does not match expected ` +
`format (${idRegex.toString()})`);
}
return id;
};
/**
* Gets video ID either from a url or by checking if the given string
* matches the video ID format.
*
* @param {string} str
* @return {string|Error}
*/
exports.getVideoID = (str) => {
if (exports.validateID(str)) {
return str;
} else {
return exports.getURLVideoID(str);
}
};
/**
* Returns true if given id satifies YouTube's id format.
*
* @param {string} id
* @return {boolean}
*/
const idRegex = /^[a-zA-Z0-9-_]{11}$/;
exports.validateID = (id) => {
return idRegex.test(id);
};
/**
* Checks wether the input string includes a valid id.
*
* @param {string} string
* @return {boolean}
*/
exports.validateURL = (string) => {
return !(exports.getURLVideoID(string) instanceof Error);
};
/**
* @param {Object} info
* @return {Array.<Object>}
*/
exports.parseFormats = (info) => {
let 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((format) => qs.parse(format));
delete info.url_encoded_fmt_stream_map;
delete info.adaptive_fmts;
return formats;
};
/**
* @param {Object} format
*/
exports.addFormatMeta = (format) => {
const meta = FORMATS[format.itag];
for (let key in meta) {
format[key] = meta[key];
}
format.live = /\/source\/yt_live_broadcast\//.test(format.url);
format.isHLS = /\/manifest\/hls_(variant|playlist)\//.test(format.url);
format.isDashMPD = /\/manifest\/dash\//.test(format.url);
};
/**
* Get only the string from an HTML string.
*
* @param {string} html
* @return {string}
*/
exports.stripHTML = (html) => {
return html
.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();
};
/**
* @param {Array.<Function>} funcs
* @param {Function(!Error, Array.<Object>)} callback
*/
exports.parallel = (funcs, callback) => {
let funcsDone = 0;
let errGiven = false;
let results = [];
const len = funcs.length;
const 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) {
funcs.forEach((f, i) => { f(checkDone.bind(null, i)); });
} else {
callback(null, results);
}
};