@np-dev/youtubei-js
Version:
A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).
158 lines • 7.52 kB
JavaScript
import { __asyncValues, __awaiter } from "tslib";
import * as Constants from './Constants.js';
import { InnertubeError, Platform, streamToIterable } from './Utils.js';
export function download(options, actions, playability_status, streaming_data, player, cpn) {
return __awaiter(this, void 0, void 0, function* () {
if ((playability_status === null || playability_status === void 0 ? void 0 : playability_status.status) === 'UNPLAYABLE')
throw new InnertubeError('Video is unplayable', { error_type: 'UNPLAYABLE' });
if ((playability_status === null || playability_status === void 0 ? void 0 : playability_status.status) === 'LOGIN_REQUIRED')
throw new InnertubeError('Video is login required', { error_type: 'LOGIN_REQUIRED' });
if (!streaming_data)
throw new InnertubeError('Streaming data not available.', { error_type: 'NO_STREAMING_DATA' });
const opts = Object.assign({ quality: '360p', type: 'video+audio', format: 'mp4', range: undefined }, options);
const format = chooseFormat(opts, streaming_data);
const format_url = format.decipher(player);
// If we're not downloading the video in chunks, we just use fetch once.
if (opts.type === 'video+audio' && !options.range) {
const response = yield actions.session.http.fetch_function(`${format_url}&cpn=${cpn}`, {
method: 'GET',
headers: Constants.STREAM_HEADERS,
redirect: 'follow'
});
// Throw if the response is not 2xx
if (!response.ok)
throw new InnertubeError('The server responded with a non 2xx status code', { error_type: 'FETCH_FAILED', response });
const body = response.body;
if (!body)
throw new InnertubeError('Could not get ReadableStream from fetch Response.', { error_type: 'FETCH_FAILED', response });
return body;
}
// We need to download in chunks.
const chunk_size = 1048576 * 10; // 10MB
let chunk_start = (options.range ? options.range.start : 0);
let chunk_end = (options.range ? options.range.end : chunk_size);
let must_end = false;
let cancel;
const readable_stream = new Platform.shim.ReadableStream({
// eslint-disable-next-line @typescript-eslint/no-empty-function
start() { },
pull: (controller) => __awaiter(this, void 0, void 0, function* () {
if (must_end) {
controller.close();
return;
}
if ((chunk_end >= (format.content_length ? format.content_length : 0)) || options.range) {
must_end = true;
}
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
var _a, e_1, _b, _c;
try {
cancel = new AbortController();
const response = yield actions.session.http.fetch_function(`${format_url}&cpn=${cpn}&range=${chunk_start}-${chunk_end || ''}`, {
method: 'GET',
headers: Object.assign({}, Constants.STREAM_HEADERS
// XXX: use YouTube's range parameter instead of a Range header.
// Range: `bytes=${chunk_start}-${chunk_end}`
),
signal: cancel.signal
});
const body = response.body;
if (!body)
throw new InnertubeError('Could not get ReadableStream from fetch Response.', { error_type: 'FETCH_FAILED', response });
try {
for (var _d = true, _e = __asyncValues(streamToIterable(body)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
_c = _f.value;
_d = false;
const chunk = _c;
controller.enqueue(chunk);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
chunk_start = chunk_end + 1;
chunk_end += chunk_size;
resolve();
}
catch (e) {
reject(e);
}
}));
}),
cancel(reason) {
return __awaiter(this, void 0, void 0, function* () {
cancel.abort(reason);
});
}
}, {
highWaterMark: 1,
size(chunk) {
return chunk.byteLength;
}
});
return readable_stream;
});
}
/**
* Selects the format that best matches the given options.
* @param options - Options
* @param streaming_data - Streaming data
*/
export function chooseFormat(options, streaming_data) {
if (!streaming_data)
throw new InnertubeError('Streaming data not available');
const formats = [
...(streaming_data.formats || []),
...(streaming_data.adaptive_formats || [])
];
const requires_audio = options.type ? options.type.includes('audio') : true;
const requires_video = options.type ? options.type.includes('video') : true;
const language = options.language || 'original';
const quality = options.quality || 'best';
let best_width = -1;
const is_best = ['best', 'bestefficiency'].includes(quality);
const use_most_efficient = quality !== 'best';
let candidates = formats.filter((format) => {
if (requires_audio && !format.has_audio)
return false;
if (requires_video && !format.has_video)
return false;
if (options.format !== 'any' && !format.mime_type.includes(options.format || 'mp4'))
return false;
if (!is_best && format.quality_label !== quality)
return false;
if (best_width < format.width)
best_width = format.width;
return true;
});
if (!candidates.length)
throw new InnertubeError('No matching formats found', { options });
if (is_best && requires_video)
candidates = candidates.filter((format) => format.width === best_width);
if (requires_audio && !requires_video) {
const audio_only = candidates.filter((format) => {
if (language !== 'original') {
return !format.has_video && format.language === language;
}
return !format.has_video && format.is_original;
});
if (audio_only.length > 0) {
candidates = audio_only;
}
}
if (use_most_efficient) {
// Sort by bitrate (lower is better)
candidates.sort((a, b) => a.bitrate - b.bitrate);
}
else {
// Sort by bitrate (higher is better)
candidates.sort((a, b) => b.bitrate - a.bitrate);
}
return candidates[0];
}
export { toDash } from './DashManifest.js';
//# sourceMappingURL=FormatUtils.js.map