mirror of
https://github.com/musix-org/musix-oss
synced 2025-06-16 18:56:00 +00:00
fix
This commit is contained in:
737
node_modules/ytdl-core/lib/formats.js
generated
vendored
Normal file
737
node_modules/ytdl-core/lib/formats.js
generated
vendored
Normal 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
156
node_modules/ytdl-core/lib/index.js
generated
vendored
Normal 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
134
node_modules/ytdl-core/lib/info-extras.js
generated
vendored
Normal 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
376
node_modules/ytdl-core/lib/info.js
generated
vendored
Normal 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
264
node_modules/ytdl-core/lib/sig.js
generated
vendored
Normal 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
414
node_modules/ytdl-core/lib/util.js
generated
vendored
Normal 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);
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user