UNPKG

youtube-sr

Version:

Simple package to make YouTube search.

1,211 lines (1,204 loc) 53.9 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); var __typeError = (msg) => { throw TypeError(msg); }; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; var __await = function(promise, isYieldStar) { this[0] = promise; this[1] = isYieldStar; }; var __yieldStar = (value) => { var obj = value[__knownSymbol("asyncIterator")], isAwait = false, method, it = {}; if (obj == null) { obj = value[__knownSymbol("iterator")](); method = (k) => it[k] = (x) => obj[k](x); } else { obj = obj.call(value); method = (k) => it[k] = (v) => { if (isAwait) { isAwait = false; if (k === "throw") throw v; return v; } isAwait = true; return { done: false, value: new __await(new Promise((resolve) => { var x = obj[k](v); if (!(x instanceof Object)) __typeError("Object expected"); resolve(x); }), 1) }; }; } return it[__knownSymbol("iterator")] = () => it, method("next"), "throw" in obj ? method("throw") : it.throw = (x) => { throw x; }, "return" in obj && method("return"), it; }; // src/formatter.ts var _Formatter = class _Formatter { constructor() { return _Formatter; } static formatSearchResult(details, options = { limit: 100, type: "all" }) { const results = []; for (let i = 0; i < details.length; i++) { if (typeof options.limit === "number" && options.limit > 0 && results.length >= options.limit) break; let data = details[i]; let res; if (options.type === "all") { if (!!data.videoRenderer) options.type = "video"; else if (!!data.channelRenderer) options.type = "channel"; else if (!!data.playlistRenderer) options.type = "playlist"; else continue; } if (options.type === "video" || options.type === "film") { const parsed = Util_default.parseVideo(data); if (!parsed) continue; res = parsed; } else if (options.type === "channel") { const parsed = Util_default.parseChannel(data); if (!parsed) continue; res = parsed; } else if (options.type === "playlist") { const parsed = Util_default.parsePlaylist(data); if (!parsed) continue; res = parsed; } results.push(res); } return results; } }; __name(_Formatter, "Formatter"); var Formatter = _Formatter; // src/Structures/Channel.ts var _Channel = class _Channel { constructor(data) { if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); this._patch(data); } /** * Patch raw data * @private * @ignore */ _patch(data) { var _a; if (!data) data = {}; this.name = data.name || null; this.verified = !!data.verified || false; this.id = data.id || null; this.url = data.url || null; this.icon = data.icon || { url: null, width: 0, height: 0 }; this.subscribers = data.subscribers || null; if ((_a = this.icon.url) == null ? void 0 : _a.startsWith("//")) this.icon.url = `https:${this.icon.url}`; } /** * Returns channel icon url * @param {object} options Icon options * @param {number} [options.size=0] Icon size. **Default is 0** */ iconURL(options = { size: 0 }) { if (typeof options.size !== "number" || options.size < 0) throw new Error("invalid icon size"); if (!this.icon.url) return null; const def = this.icon.url.split("=s")[1].split("-c")[0]; return this.icon.url.replace(`=s${def}-c`, `=s${options.size}-c`); } get type() { return "channel"; } toString() { return this.name || ""; } toJSON() { return { name: this.name, verified: this.verified, id: this.id, url: this.url, iconURL: this.iconURL(), type: this.type, subscribers: this.subscribers }; } }; __name(_Channel, "Channel"); var Channel = _Channel; // src/Structures/Thumbnail.ts var _Thumbnail = class _Thumbnail { /** * Thumbnail constructor * @param data Thumbnail constructor params */ constructor(data) { if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); this._patch(data); } /** * Patch raw data * @param data Raw Data * @private * @ignore */ _patch(data) { if (!data) data = {}; this.id = data.id || null; this.width = data.width || 0; this.height = data.height || 0; this.url = data.url || null; } /** * Returns thumbnail url * @param {"default"|"hqdefault"|"mqdefault"|"sddefault"|"maxresdefault"|"ultrares"} thumbnailType Thumbnail type */ displayThumbnailURL(thumbnailType = "ultrares") { if (!["default", "hqdefault", "mqdefault", "sddefault", "maxresdefault", "ultrares"].includes(thumbnailType)) throw new Error(`Invalid thumbnail type "${thumbnailType}"!`); if (thumbnailType === "ultrares") return this.url; return `https://i3.ytimg.com/vi/${this.id}/${thumbnailType}.jpg`; } /** * Returns default thumbnail * @param {"0"|"1"|"2"|"3"|"4"} id Thumbnail id. **4 returns thumbnail placeholder.** */ defaultThumbnailURL(id) { if (!id) id = "0"; if (!["0", "1", "2", "3", "4"].includes(id)) throw new Error(`Invalid thumbnail id "${id}"!`); return `https://i3.ytimg.com/vi/${this.id}/${id}.jpg`; } toString() { return this.url ? `${this.url}` : ""; } toJSON() { return { id: this.id, width: this.width, height: this.height, url: this.url }; } }; __name(_Thumbnail, "Thumbnail"); var Thumbnail = _Thumbnail; // src/Structures/Playlist.ts var BASE_API = "https://www.youtube.com/youtubei/v1/browse?key="; var _Playlist = class _Playlist { constructor(data = {}, searchResult = false) { this.fake = false; this._continuation = {}; if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); Object.defineProperty(this, "_continuation", { enumerable: false, configurable: true, writable: true }); if (!!searchResult) this._patchSearch(data); else this._patch(data); } _patch(data) { var _a, _b, _c, _d, _e, _f; this.id = data.id || null; this.title = data.title || null; this.videoCount = data.videoCount || 0; this.lastUpdate = data.lastUpdate || null; this.views = data.views || 0; this.url = data.url || data.link || this.id ? `https://www.youtube.com/playlist?list=${this.id}` : null; this.link = data.link || data.url || null; this.channel = data.author || null; this.thumbnail = new Thumbnail(data.thumbnail || {}); this.videos = data.videos || []; this._continuation.api = (_b = (_a = data.continuation) == null ? void 0 : _a.api) != null ? _b : null; this._continuation.token = (_d = (_c = data.continuation) == null ? void 0 : _c.token) != null ? _d : null; this._continuation.clientVersion = (_f = (_e = data.continuation) == null ? void 0 : _e.clientVersion) != null ? _f : "<important data>"; this.mix = data.mix || false; this.fake = Boolean(data.fake); } _patchSearch(data) { var _a; this.id = data.id || null; this.title = data.title || null; this.thumbnail = new Thumbnail(data.thumbnail || {}); this.channel = data.channel || null; this.videos = data.videos || []; this.videoCount = ((_a = data.videos) == null ? void 0 : _a.length) || 0; this.url = data.url || data.link || this.id ? `https://www.youtube.com/playlist?list=${this.id}` : null; this.link = data.link || data.url || null; this.lastUpdate = null; this.views = 0; this.mix = data.mix || false; this.fake = Boolean(data.fake); } /** * @param limit Max items to parse from current chunk */ next(limit = Infinity) { return __async(this, null, function* () { var _a, _b, _c; if (!this._continuation || !this._continuation.token) return []; const nextPage = yield Util_default.getHTML(`${BASE_API}${this._continuation.api}`, { method: "POST", body: JSON.stringify({ continuation: this._continuation.token, context: { client: { utcOffsetMinutes: 0, gl: "US", hl: "en", clientName: "WEB", clientVersion: this._continuation.clientVersion }, user: {}, request: {} } }) }); const contents = (_c = (_b = (_a = Util_default.json(nextPage)) == null ? void 0 : _a.onResponseReceivedActions[0]) == null ? void 0 : _b.appendContinuationItemsAction) == null ? void 0 : _c.continuationItems; if (!contents) return []; const partial = Util_default.getPlaylistVideos(contents, limit); this._continuation.token = Util_default.getContinuationToken(contents); this.videos = [...this.videos, ...partial]; return partial; }); } fetch(max = Infinity) { return __async(this, null, function* () { const ctn = this._continuation.token; if (!ctn) return this; if (max < 1) max = Infinity; while (typeof this._continuation.token === "string" && this._continuation.token.length) { if (this.videos.length >= max) break; const res = yield this.next(); if (!res.length) break; } return this; }); } get type() { return "playlist"; } *[Symbol.iterator]() { yield* __yieldStar(this.videos); } toJSON() { var _a, _b, _c; return { id: this.id, title: this.title, thumbnail: ((_a = this.thumbnail) == null ? void 0 : _a.toJSON()) || null, channel: { name: this.channel.name, id: this.channel.id, icon: (_c = (_b = this.channel) == null ? void 0 : _b.iconURL) == null ? void 0 : _c.call(_b) }, mix: this.mix, url: this.url, videos: this.videos }; } }; __name(_Playlist, "Playlist"); var Playlist = _Playlist; // src/Structures/Video.ts var _Video = class _Video { constructor(data) { this.nsfw = false; this.shorts = false; this.unlisted = false; if (!data) throw new Error(`Cannot instantiate the ${this.constructor.name} class without data!`); this._patch(data); } /** * Patch raw data * @private * @ignore */ _patch(data) { var _a, _b; if (!data) data = {}; this.id = data.id || null; this.title = data.title || null; this.description = data.description || null; this.durationFormatted = data.duration_raw || "0:00"; this.duration = (data.duration < 0 ? 0 : data.duration) || 0; this.uploadedAt = data.uploadedAt || null; this.views = parseInt(data.views) || 0; this.thumbnail = new Thumbnail(data.thumbnail || {}); this.channel = new Channel(data.channel || {}); this.likes = ((_a = data.ratings) == null ? void 0 : _a.likes) || 0; this.dislikes = ((_b = data.ratings) == null ? void 0 : _b.dislikes) || 0; this.live = !!data.live; this.private = !!data.private; this.tags = data.tags || []; this.nsfw = Boolean(data.nsfw); this.unlisted = Boolean(data.unlisted); this.shorts = Boolean(data.shorts); this.music = data.music; Object.defineProperty(this, "streamingData", { enumerable: false, configurable: true, writable: true, value: data.streamingData || null }); Object.defineProperty(this, "videos", { enumerable: false, configurable: true, writable: true, value: data.videos || [] }); } get formats() { var _a; return ((_a = this.streamingData) == null ? void 0 : _a.formats) || []; } get adaptiveFormats() { var _a; return ((_a = this.streamingData) == null ? void 0 : _a.adaptiveFormats) || []; } get url() { if (!this.id) return null; return `https://www.youtube.com/watch?v=${this.id}`; } get shortsURL() { if (!this.shorts) return this.url; return `https://www.youtube.com/shorts/${this.id}`; } /** * YouTube video embed html * @param {object} options Options * @param {string} [options.id] DOM element id * @param {number} [options.width] Iframe width * @param {number} [options.height] Iframe height */ embedHTML(options = { id: "ytplayer", width: 640, height: 360 }) { if (!this.id) return null; return `<iframe title="__youtube_sr_frame__" id="${options.id || "ytplayer"}" type="text/html" width="${options.width || 640}" height="${options.height || 360}" src="${this.embedURL}" frameborder="0"></iframe>`; } /** * Creates mix playlist url from this video */ createMixURL() { return `${this.url}&list=RD${this.id}`; } /** * YouTube video embed url */ get embedURL() { if (!this.id) return null; return `https://www.youtube.com/embed/${this.id}`; } get type() { return "video"; } toString() { return this.url || ""; } toJSON() { const res = { id: this.id, url: this.url, shorts_url: this.shortsURL, title: this.title, description: this.description, duration: this.duration, duration_formatted: this.durationFormatted, uploadedAt: this.uploadedAt, unlisted: this.unlisted, nsfw: this.nsfw, thumbnail: this.thumbnail.toJSON(), channel: { name: this.channel.name, id: this.channel.id, icon: this.channel.iconURL() }, views: this.views, type: this.type, tags: this.tags, ratings: { likes: this.likes, dislikes: this.dislikes }, shorts: this.shorts, live: this.live, private: this.private, music: this.music }; return res; } }; __name(_Video, "Video"); var Video = _Video; // src/Util.ts var PLAYLIST_REGEX = /^https?:\/\/(www.)?youtube.com\/playlist\?list=((PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{16,41})$/; var PLAYLIST_ID = /(PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{11,41}/; var ALBUM_REGEX = /(RDC|O)LAK5uy_[a-zA-Z0-9-_]{33}/; var VIDEO_URL = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; var VIDEO_ID = /^[a-zA-Z0-9-_]{11}$/; var DEFAULT_INNERTUBE_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"; var innertubeCache = null; var __fetch; var isNode = typeof process !== "undefined" && "node" in (process.versions || {}); var FETCH_LIBS = ["node-fetch", "cross-fetch", "undici"]; function getFetch() { return __async(this, null, function* () { if (typeof __fetch === "function") return __fetch; if (typeof window !== "undefined" && "fetch" in window) return window.fetch; if ("fetch" in globalThis) return globalThis.fetch; for (const fetchLib of FETCH_LIBS) { try { const pkg = yield import(fetchLib); const mod = pkg.fetch || pkg.default || pkg; if (mod) return __fetch = mod; } catch (e) { } } if (isNode) throw new Error(`Could not resolve fetch library. Install one of ${FETCH_LIBS.map((m) => `"${m}"`).join(", ")} or define "fetch" in global scope!`); throw new Error("Could not resolve fetch in global scope"); }); } __name(getFetch, "getFetch"); var _Util = class _Util { constructor() { return _Util; } static innertubeKey() { return __async(this, null, function* () { if (innertubeCache) return innertubeCache; return yield _Util.fetchInnerTubeKey(); }); } static get VideoRegex() { return VIDEO_URL; } static get VideoIDRegex() { return VIDEO_ID; } static get AlbumRegex() { return ALBUM_REGEX; } /** * YouTube playlist URL Regex * @type {RegExp} */ static get PlaylistURLRegex() { return PLAYLIST_REGEX; } /** * YouTube Playlist ID regex * @type {RegExp} */ static get PlaylistIDRegex() { return PLAYLIST_ID; } static fetchInnerTubeKey() { return __async(this, null, function* () { var _a, _b, _c; const html = yield _Util.getHTML("https://www.youtube.com?hl=en"); const key = (_c = (_a = html.split('INNERTUBE_API_KEY":"')[1]) == null ? void 0 : _a.split('"')[0]) != null ? _c : (_b = html.split('innertubeApiKey":"')[1]) == null ? void 0 : _b.split('"')[0]; if (key) innertubeCache = key; return key != null ? key : DEFAULT_INNERTUBE_KEY; }); } /** * Parse HTML * @param {string} url Website URL * @param {RequestInit} [requestOptions] Request Options * @returns {Promise<string>} */ static getHTML(url, requestOptions = {}) { requestOptions = Object.assign( {}, { headers: Object.assign( {}, { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:140.0) Gecko/20100101 Firefox/140.0" }, (requestOptions == null ? void 0 : requestOptions.headers) || {} ) }, requestOptions || {} ); return new Promise((resolve, reject) => __async(null, null, function* () { if (!__fetch) __fetch = yield getFetch(); __fetch(url, requestOptions).then((res) => { if (!res.ok) throw new Error(`Rejected with status code: ${res.status}`); return res.text(); }).then((html) => resolve(html)).catch((e) => reject(e)); })); } /** * Returns duration in ms * @param {string} duration Duration to parse */ static parseDuration(duration) { duration != null ? duration : duration = "0:00"; const args = duration.split(":"); let dur = 0; switch (args.length) { case 3: dur = parseInt(args[0]) * 60 * 60 * 1e3 + parseInt(args[1]) * 60 * 1e3 + parseInt(args[2]) * 1e3; break; case 2: dur = parseInt(args[0]) * 60 * 1e3 + parseInt(args[1]) * 1e3; break; default: dur = parseInt(args[0]) * 1e3; } return dur; } /** * Parse items from html * @param {string} html HTML * @param options Options */ static parseSearchResult(html, options) { if (!html) throw new Error("Invalid raw data"); if (!options) options = { type: "video", limit: 0 }; if (!options.type) options.type = "video"; let details = []; let fetched = false; try { let data = html.split("ytInitialData = JSON.parse('")[1].split("');</script>")[0]; html = data.replace(/\\x([0-9A-F]{2})/gi, (...items) => { return String.fromCharCode(parseInt(items[1], 16)); }); } catch (e) { } try { details = JSON.parse(html.split('{"itemSectionRenderer":{"contents":')[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(',"continuations":[{')[0]); fetched = true; } catch (e) { } if (!fetched) { try { details = JSON.parse(html.split('{"itemSectionRenderer":')[html.split('{"itemSectionRenderer":').length - 1].split('},{"continuationItemRenderer":{')[0]).contents; fetched = true; } catch (e) { } } if (!fetched) return []; return Formatter.formatSearchResult(details, options); } /** * Parse channel from raw data * @param {object} data Raw data to parse video from */ static parseChannel(data) { if (!data || !data.channelRenderer) return; const badges = data.channelRenderer.ownerBadges; let url = `https://www.youtube.com${data.channelRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.channelRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url}`; let res = new Channel({ id: data.channelRenderer.channelId, name: data.channelRenderer.title.simpleText, icon: data.channelRenderer.thumbnail.thumbnails[data.channelRenderer.thumbnail.thumbnails.length - 1], url, verified: !(badges == null ? void 0 : badges.length) ? false : badges.some((badge) => { var _a, _b; return badge["verifiedBadge"] || ((_b = (_a = badge == null ? void 0 : badge.metadataBadgeRenderer) == null ? void 0 : _a.style) == null ? void 0 : _b.toLowerCase().includes("verified")); }), subscribers: data.channelRenderer.subscriberCountText.simpleText }); return res; } /** * Parse video from raw data * @param {object} data Raw data to parse video from */ static parseVideo(data) { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E; if (!data || !data.videoRenderer) return; const badge = data.videoRenderer.ownerBadges && data.videoRenderer.ownerBadges[0]; let res = new Video({ id: data.videoRenderer.videoId, url: `https://www.youtube.com/watch?v=${data.videoRenderer.videoId}`, title: data.videoRenderer.title.runs[0].text, description: data.videoRenderer.descriptionSnippet && data.videoRenderer.descriptionSnippet.runs[0] ? data.videoRenderer.descriptionSnippet.runs[0].text : "", duration: data.videoRenderer.lengthText ? _Util.parseDuration(data.videoRenderer.lengthText.simpleText) : 0, duration_raw: data.videoRenderer.lengthText ? data.videoRenderer.lengthText.simpleText : null, thumbnail: { id: data.videoRenderer.videoId, url: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].url, height: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].height, width: data.videoRenderer.thumbnail.thumbnails[data.videoRenderer.thumbnail.thumbnails.length - 1].width }, channel: { id: data.videoRenderer.ownerText.runs[0].navigationEndpoint.browseEndpoint.browseId || null, name: data.videoRenderer.ownerText.runs[0].text || null, url: `https://www.youtube.com${data.videoRenderer.ownerText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || data.videoRenderer.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`, icon: { url: ((_c = (_b = (_a = data.videoRenderer.channelThumbnail) == null ? void 0 : _a.thumbnails) == null ? void 0 : _b[0]) == null ? void 0 : _c.url) || ((_h = (_g = (_f = (_e = (_d = data.videoRenderer.channelThumbnailSupportedRenderers) == null ? void 0 : _d.channelThumbnailWithLinkRenderer) == null ? void 0 : _e.thumbnail) == null ? void 0 : _f.thumbnails) == null ? void 0 : _g[0]) == null ? void 0 : _h.url), width: ((_k = (_j = (_i = data.videoRenderer.channelThumbnail) == null ? void 0 : _i.thumbnails) == null ? void 0 : _j[0]) == null ? void 0 : _k.width) || ((_p = (_o = (_n = (_m = (_l = data.videoRenderer.channelThumbnailSupportedRenderers) == null ? void 0 : _l.channelThumbnailWithLinkRenderer) == null ? void 0 : _m.thumbnail) == null ? void 0 : _n.thumbnails) == null ? void 0 : _o[0]) == null ? void 0 : _p.width), height: ((_s = (_r = (_q = data.videoRenderer.channelThumbnail) == null ? void 0 : _q.thumbnails) == null ? void 0 : _r[0]) == null ? void 0 : _s.height) || ((_x = (_w = (_v = (_u = (_t = data.videoRenderer.channelThumbnailSupportedRenderers) == null ? void 0 : _t.channelThumbnailWithLinkRenderer) == null ? void 0 : _u.thumbnail) == null ? void 0 : _v.thumbnails) == null ? void 0 : _w[0]) == null ? void 0 : _x.height) }, verified: Boolean((_z = (_y = badge == null ? void 0 : badge.metadataBadgeRenderer) == null ? void 0 : _y.style) == null ? void 0 : _z.toLowerCase().includes("verified")) }, uploadedAt: (_B = (_A = data.videoRenderer.publishedTimeText) == null ? void 0 : _A.simpleText) != null ? _B : null, views: (_E = (_D = (_C = data.videoRenderer.viewCountText) == null ? void 0 : _C.simpleText) == null ? void 0 : _D.replace(/[^0-9]/g, "")) != null ? _E : 0 }); return res; } static parsePlaylist(data) { var _a, _b, _c; if (!data.playlistRenderer) return; const res = new Playlist( { id: data.playlistRenderer.playlistId, title: data.playlistRenderer.title.simpleText, thumbnail: { id: data.playlistRenderer.playlistId, url: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].url, height: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].height, width: data.playlistRenderer.thumbnails[0].thumbnails[data.playlistRenderer.thumbnails[0].thumbnails.length - 1].width }, channel: { id: data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, name: data.playlistRenderer.shortBylineText.runs[0].text, url: `https://www.youtube.com${((_a = data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.browseEndpoint) == null ? void 0 : _a.canonicalBaseUrl) || ((_c = (_b = data.playlistRenderer.shortBylineText.runs[0].navigationEndpoint.commandMetadata) == null ? void 0 : _b.webCommandMetadata) == null ? void 0 : _c.url)}` }, videos: parseInt(data.playlistRenderer.videoCount.replace(/[^0-9]/g, "")) }, true ); return res; } static getPlaylistVideos(data, limit = Infinity) { var _a, _b, _c, _d; const videos = []; for (let i = 0; i < data.length; i++) { if (limit === videos.length) break; const info = data[i].playlistVideoRenderer; if (!info || !info.shortBylineText) continue; videos.push( new Video({ id: info.videoId, index: parseInt((_a = info.index) == null ? void 0 : _a.simpleText) || 0, duration: _Util.parseDuration((_b = info.lengthText) == null ? void 0 : _b.simpleText) || 0, duration_raw: (_d = (_c = info.lengthText) == null ? void 0 : _c.simpleText) != null ? _d : "0:00", thumbnail: { id: info.videoId, url: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].url, height: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].height, width: info.thumbnail.thumbnails[info.thumbnail.thumbnails.length - 1].width }, title: info.title.runs[0].text, channel: { id: info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId || null, name: info.shortBylineText.runs[0].text || null, url: `https://www.youtube.com${info.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl || info.shortBylineText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`, icon: null } }) ); } return videos; } static getPlaylist(html, limit) { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n; if (!limit || typeof limit !== "number") limit = 100; if (limit <= 0) limit = Infinity; let parsed; let playlistDetails; try { const rawJSON = `${html.split('{"playlistVideoListRenderer":{"contents":')[1].split('}],"playlistId"')[0]}}]`; parsed = JSON.parse(rawJSON); playlistDetails = JSON.parse(html.split('{"playlistSidebarRenderer":')[1].split("}};</script>")[0]).items; } catch (e) { return null; } const API_KEY = (_d = (_c = (_a = html.split('INNERTUBE_API_KEY":"')[1]) == null ? void 0 : _a.split('"')[0]) != null ? _c : (_b = html.split('innertubeApiKey":"')[1]) == null ? void 0 : _b.split('"')[0]) != null ? _d : DEFAULT_INNERTUBE_KEY; const videos = _Util.getPlaylistVideos(parsed, limit); const data = playlistDetails[0].playlistSidebarPrimaryInfoRenderer; if (!data.title.runs || !data.title.runs.length) return null; const author = (_e = playlistDetails[1]) == null ? void 0 : _e.playlistSidebarSecondaryInfoRenderer.videoOwner; const views = data.stats.length === 3 ? data.stats[1].simpleText.replace(/[^0-9]/g, "") : 0; const lastUpdate = (_h = (_g = (_f = data.stats.find((x) => "runs" in x && x["runs"].find((y) => y.text.toLowerCase().includes("last update")))) == null ? void 0 : _f.runs.pop()) == null ? void 0 : _g.text) != null ? _h : null; const videosCount = data.stats[0].runs[0].text.replace(/[^0-9]/g, "") || 0; const res = new Playlist({ continuation: { api: API_KEY, token: _Util.getContinuationToken(parsed), clientVersion: (_l = (_k = (_i = html.split('"INNERTUBE_CONTEXT_CLIENT_VERSION":"')[1]) == null ? void 0 : _i.split('"')[0]) != null ? _k : (_j = html.split('"innertube_context_client_version":"')[1]) == null ? void 0 : _j.split('"')[0]) != null ? _l : "<some version>" }, id: data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId, title: data.title.runs[0].text, videoCount: parseInt(videosCount) || 0, lastUpdate, views: parseInt(views) || 0, videos, url: `https://www.youtube.com/playlist?list=${data.title.runs[0].navigationEndpoint.watchEndpoint.playlistId}`, link: `https://www.youtube.com${data.title.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url}`, author: author ? { name: author.videoOwnerRenderer.title.runs[0].text, id: author.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.browseId, url: `https://www.youtube.com${author.videoOwnerRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url || author.videoOwnerRenderer.navigationEndpoint.browseEndpoint.canonicalBaseUrl}`, icon: author.videoOwnerRenderer.thumbnail.thumbnails.length ? (_m = author.videoOwnerRenderer.thumbnail.thumbnails.pop()) == null ? void 0 : _m.url : null } : {}, thumbnail: ((_n = data.thumbnailRenderer.playlistVideoThumbnailRenderer) == null ? void 0 : _n.thumbnail.thumbnails.length) ? data.thumbnailRenderer.playlistVideoThumbnailRenderer.thumbnail.thumbnails.pop() : null }); return res; } static getContinuationToken(ctx) { var _a, _b, _c; const continuationToken = (_c = (_b = (_a = ctx.find((x) => Object.keys(x)[0] === "continuationItemRenderer")) == null ? void 0 : _a.continuationItemRenderer.continuationEndpoint) == null ? void 0 : _b.continuationCommand) == null ? void 0 : _c.token; return continuationToken; } static getVideo(html) { var _a, _b, _c, _d, _e, _f, _g, _h; let data, nextData = {}; try { const parsed = JSON.parse(html.split("var ytInitialData = ")[1].split(";</script>")[0]); data = parsed.contents.twoColumnWatchNextResults.results.results.contents; try { nextData = parsed.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results; } catch (e) { } } catch (e) { throw new Error("Could not parse video metadata!"); } const raw = { primary: ((_a = data == null ? void 0 : data.find((section) => "videoPrimaryInfoRenderer" in section)) == null ? void 0 : _a.videoPrimaryInfoRenderer) || {}, secondary: ((_b = data == null ? void 0 : data.find((section) => "videoSecondaryInfoRenderer" in section)) == null ? void 0 : _b.videoSecondaryInfoRenderer) || {} }; let info; try { info = JSON.parse(html.split("var ytInitialPlayerResponse = ")[1].split(";</script>")[0]); } catch (e) { info = JSON.parse(html.split("var ytInitialPlayerResponse = ")[1].split(";var")[0]); } if (!(info == null ? void 0 : info.videoDetails)) return null; info = __spreadProps(__spreadValues(__spreadValues({}, raw.primary), raw.secondary), { info }); let musicInfo = []; try { const jsonData = html.split('{"horizontalCardListRenderer":')[1].split(',{"reelShelfRenderer"')[0]; musicInfo = JSON.parse('{"horizontalCardListRenderer":' + jsonData).horizontalCardListRenderer.cards.map((val) => { return { title: val.videoAttributeViewModel.title, cover: val.videoAttributeViewModel.image.sources[0].url, artist: val.videoAttributeViewModel.subtitle, album: val.videoAttributeViewModel.secondarySubtitle.content }; }); } catch (e) { musicInfo = []; } const payload = new Video({ id: info.info.videoDetails.videoId, title: info.info.videoDetails.title, views: parseInt(info.info.videoDetails.viewCount) || 0, tags: info.info.videoDetails.keywords, private: info.info.videoDetails.isPrivate, unlisted: !!((_d = (_c = info.info.microformat) == null ? void 0 : _c.playerMicroformatRenderer) == null ? void 0 : _d.isUnlisted), nsfw: ((_f = (_e = info.info.microformat) == null ? void 0 : _e.playerMicroformatRenderer) == null ? void 0 : _f.isFamilySafe) === false, live: info.info.videoDetails.isLiveContent, duration: parseInt(info.info.videoDetails.lengthSeconds) * 1e3, shorts: [`{"webCommandMetadata":{"url":"/shorts/${info.info.videoDetails.videoId}"`, `{window['ytPageType'] = "shorts";`, `"/hashtag/shorts"`].some((r) => html.includes(r)), duration_raw: _Util.durationString(_Util.parseMS(parseInt(info.info.videoDetails.lengthSeconds) * 1e3 || 0)), channel: { name: info.info.videoDetails.author, id: info.info.videoDetails.channelId, url: `https://www.youtube.com${info.owner.videoOwnerRenderer.title.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl}`, icon: info.owner.videoOwnerRenderer.thumbnail.thumbnails[0], subscribers: (_h = (_g = info.owner.videoOwnerRenderer.subscriberCountText) == null ? void 0 : _g.simpleText) == null ? void 0 : _h.replace(" subscribers", "") }, description: info.info.videoDetails.shortDescription, thumbnail: __spreadProps(__spreadValues({}, info.info.videoDetails.thumbnail.thumbnails[info.info.videoDetails.thumbnail.thumbnails.length - 1]), { id: info.info.videoDetails.videoId }), uploadedAt: info.dateText.simpleText, ratings: { likes: this.getInfoLikesCount(info) || 0, dislikes: 0 }, videos: _Util.getNext(nextData != null ? nextData : {}) || [], streamingData: info.info.streamingData || null, music: musicInfo }); return payload; } static getInfoLikesCount(info) { var _a; const buttons = info.videoActions.menuRenderer.topLevelButtons; const button = buttons.find((button2) => { var _a2; return ((_a2 = button2.toggleButtonRenderer) == null ? void 0 : _a2.defaultIcon.iconType) === "LIKE"; }); if (!button) return 0; return parseInt((_a = button.toggleButtonRenderer.defaultText.accessibility) == null ? void 0 : _a.accessibilityData.label.split(" ")[0].replace(/,/g, "")); } static getNext(body, home = false) { var _a, _b, _c, _d, _e; const results = []; if (typeof body[Symbol.iterator] !== "function") return results; for (const result of body) { const details = home ? result : result.compactVideoRenderer; if (details) { try { let viewCount = details.viewCountText.simpleText; viewCount = (/^\d/.test(viewCount) ? viewCount : "0").split(" ")[0]; results.push( new Video({ id: details.videoId, title: (_b = details.title.simpleText) != null ? _b : (_a = details.title.runs[0]) == null ? void 0 : _a.text, views: parseInt(viewCount.replace(/,/g, "")) || 0, duration_raw: (_c = details.lengthText.simpleText) != null ? _c : details.lengthText.accessibility.accessibilityData.label, duration: _Util.parseDuration(details.lengthText.simpleText) / 1e3, channel: { name: details.shortBylineText.runs[0].text, id: details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, url: `https://www.youtube.com${details.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl}`, icon: home ? details.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails[0] : details.channelThumbnail.thumbnails[0], subscribers: "0", verified: Boolean(details.ownerBadges[0].metadataBadgeRenderer.tooltip === "Verified") }, thumbnail: __spreadProps(__spreadValues({}, details.thumbnail.thumbnails[details.thumbnail.thumbnails.length - 1]), { id: details.videoId }), uploadedAt: details.publishedTimeText.simpleText, ratings: { likes: 0, dislikes: 0 }, description: (_e = (_d = details.descriptionSnippet) == null ? void 0 : _d.runs[0]) == null ? void 0 : _e.text }) ); } catch (e) { continue; } } } return results; } static getMix(html) { var _a, _b; let data = null; try { const parsed = JSON.parse(html.split("var ytInitialData = ")[1].split(";</script>")[0]); data = parsed.contents.twoColumnWatchNextResults.playlist.playlist; } catch (e) { } if (!data) return null; const videos = data.contents.map((m) => { const t = m.playlistPanelVideoRenderer; return new Video({ id: t.videoId, title: t.title.simpleText, thumbnail: { id: t.videoId, url: t.thumbnail.thumbnails[t.thumbnail.thumbnails.length - 1].url, height: t.thumbnail.thumbnails[t.thumbnail.thumbnails.length - 1].height, width: t.thumbnail.thumbnails[t.thumbnail.thumbnails.length - 1].width }, duration: _Util.parseDuration(t.lengthText.simpleText), duration_raw: t.lengthText.simpleText, channel: { name: t.shortBylineText.runs[0].text, id: t.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.browseId, url: `https://www.youtube.com${t.shortBylineText.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl}`, icon: null } }); }); const list = new Playlist( { id: data.playlistId, title: data.title, videoCount: data.contents.length, videos, link: data.playlistShareUrl, url: data.playlistShareUrl, thumbnail: ((_b = (_a = videos[0]) == null ? void 0 : _a.thumbnail) == null ? void 0 : _b.toJSON()) || null, channel: { name: data.ownerName.simpleText }, mix: true }, true ); return list; } static parseHomepage(html) { let contents; try { contents = html.split("var ytInitialData = ")[1].split(";</script>")[0]; contents = JSON.parse(contents).contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.richGridRenderer.contents; } catch (e) { return []; } if (!contents || !contents.length || !contents.find((x) => Object.keys(x)[0] === "richItemRenderer")) return []; contents = contents.filter((a) => Object.keys(a)[0] === "richItemRenderer").map((m) => m.richItemRenderer.content.videoRenderer); return _Util.getNext(contents, true); } static getPlaylistURL(url) { if (typeof url !== "string") return null; const group = PLAYLIST_ID.exec(url) || ALBUM_REGEX.exec(url); if (!group) return null; if (group[0].startsWith("RD") && !ALBUM_REGEX.exec(group[0])) return { url, mix: true }; const finalURL = `https://www.youtube.com/playlist?list=${group[0]}`; return { url: finalURL, mix: false }; } static validatePlaylist(url) { if (typeof url === "string" && (url.match(PLAYLIST_ID) !== null || url.match(ALBUM_REGEX) !== null)) return; throw new Error("Invalid playlist url"); } static filter(ftype) { switch (ftype) { case "playlist": return "EgIQAw%253D%253D"; case "video": return "EgIQAQ%253D%253D"; case "channel": return "EgIQAg%253D%253D"; case "film": return "EgIQBA%253D%253D"; default: throw new TypeError(`Invalid filter type "${ftype}"!`); } } static parseMS(milliseconds) { return { days: Math.trunc(milliseconds / 864e5), hours: Math.trunc(milliseconds / 36e5) % 24, minutes: Math.trunc(milliseconds / 6e4) % 60, seconds: Math.trunc(milliseconds / 1e3) % 60 }; } static durationString(data) { const items = Object.keys(data); const required = ["days", "hours", "minutes", "seconds"]; const parsed = items.filter((x) => required.includes(x)).map((m) => data[m] > 0 ? data[m] : ""); const final = parsed.slice(parsed.findIndex((x) => !!x)).map((x, i) => i == 0 ? x.toString() : x.toString().padStart(2, "0")).join(":"); return final.length <= 3 ? `0:${final.padStart(2, "0") || 0}` : final; } static json(data) { try { return JSON.parse(data); } catch (e) { return null; } } static makeRequest() { return __async(this, arguments, function* (url = "", data = { data: {}, requestOptions: {} }) { const key = yield _Util.innertubeKey(); const res = yield _Util.getHTML(`https://youtube.com/youtubei/v1${url}?key=${key}`, __spreadValues({ method: "POST", headers: { "Content-Type": "application/json", Host: "www.youtube.com", Referer: "https://www.youtube.com" }, body: JSON.stringify(__spreadValues({ context: __spreadValues({ client: __spreadValues({ utcOffsetMinutes: 0, gl: "US", hl: "en", clientName: "WEB", clientVersion: "1.20220406.00.00", originalUrl: "https://www.youtube.com/" }, data.clientCtx || {}) }, data.ctx || {}) }, data.data || {})) }, data.requestOptions || {})); return _Util.json(res); }); } }; __name(_Util, "Util"); var Util = _Util; var Util_default = Util; // src/mod.ts var SAFE_SEARCH_COOKIE = "PREF=f2=8000000"; var TrendingFilter = { MUSIC: "4gINGgt5dG1hX2NoYXJ0cw%3D%3D", GAMING: "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D", MOVIES: "4gIKGgh0cmFpbGVycw%3D%3D" }; var _YouTube = class _YouTube { constructor() { return _YouTube; } static search(query, options) { return __async(this, null, function* () { if (!options) options = { limit: 100, type: "video", requestOptions: {} }; if (!query || typeof query !== "string") throw new Error(`Invalid search query "${query}"!`); options.type = options.type || "video"; const requestOptions = options.safeSearch ? __spreadProps(__spreadValues({}, options.requestOptions), { headers: { cookie: SAFE_SEARCH_COOKIE } }) : {}; try { const filter = options.type === "all" ? null : Util_default.filter(options.type); const res = yield Util_default.makeRequest("/search", { data: { params: filter, query }, clientCtx: { originalUrl: `https://youtube.com/results?search_query=${encodeURIComponent(query.trim()).replace(/%20/g, "+")}${filter}` }, requestOptions }); return Formatter.formatSearchResult(res.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents, options); } catch (e) { const filter = options.type === "all" ? "" : `&sp=${Util_default.filter(options.type)}`; const url = `https://youtube.com/results?search_query=${encodeURIComponent(query.trim()).replace(/%20/g, "+")}&hl=en${filter}`; const html = yield Util_default.getHTML(url, requestOptions); return Util_default.parseSearchResult(html, options); } }); } static searchOne(query, type, safeSearch, requestOptions) { if (!type) type = "video"; return new Promise((resolve) => { _YouTube.search(query, { limit: 1, type, requestOptions, safeSearch: Boolean(safeSearch) }).then((res) => { if (!res || !res.length) return resolve(null); resolve(res[0]); }).catch(() => { resolve(null); }); }); } /** * Returns playlist details * @param {string} url Playlist URL * @param {object} [options] Options * @param {number} [options.limit=100] Playlist video limit * @param {RequestInit} [options.requestOptions] Request Options */ static getPlaylist(url, options) { return __async(this, null, function* () { var _a; if (!options) options = { limit: 100, requestOptions: {}, fetchAll: false }; if (!url || typeof url !== "string") throw new Error(`Expected playlist url, received ${typeof url}!`); Util_default.validatePlaylist(url); const resolved = Util_default.getPlaylistURL(url); if (!resolved) return null; url = resolved.url; const html = yield Util_default.getHTML(`${url}&hl=en`, options && options.requestOptions); const res = resolved.mix ? Util_default.getMix(html) : Util_default.getPlaylist(html, options && options.limit); try { if (!res && resolved.mix) { const videoId = new URL(url).searchParams.get("v"); if (videoId) { const vid = yield _YouTube.getVideo(`https://www.youtube.com/watch?v=${videoId}`).catch(() => null); if (vid) { return new Playlist( { id: vid.id, title: `Mix - ${vid.title}`, videoCount: 1, lastUpdate: null, views: vid.views, url: `https://www.youtube.com/watch?v=${vid.id}`, link: `https://www.youtube.com/watch?v=${vid.id}`, channel: { name: "youtube-sr" }, thumbnail: ((_a = vid.thumbnail) == null ? void 0 : _a.toJSON()) || null, videos: [vid], mix: true, fake: true }, true ); } } } } catch (e) { } if (res && res instanceof Playlist && options.fetchAll) { return yield res.fetch(options && options.limit).catch(() => res); } return res; }); } /** * Returns basic video info * @param url Video url to parse * @param requestOptions Request options */ static getVideo(url, requestOptions) { return __async(this, null, function* () { if (!url) throw new Error("Missing video url"); if (url instanceof Video) url = url.url; const isValid = _YouTube.validate(url, "VIDEO"); if (!isValid) throw new Error("Invalid video url"); const html = yield Util_default.getHTML(`${url}&hl=en`, requestOptions); return Util_default.getVideo(html); }); } /** * Fetches homepage videos */ static homepage() { return __async(this, null, function* () { const html = yield Util_default.getHTML("https://www.youtube.com?hl=en"); return Util_default.parseHomepage(html); }); } /** * Attempts to parse `INNERTUBE_API_KEY` */ static fetchInnerTubeKey() { return __async(this, null, function* () { return Util_default.fetchInnerTubeKey(); }); } static trending(options) { return __async(this, null, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n; const type = TrendingFilter[options == null ? void 0 : options.type]; const html = yield Util_default.getHTML(`https://www.youtube.com/feed/trending?${type ? "bp=" + type + "&hl=en" : "hl=en"}`); let json; try { json = JSON.parse(html.split("var ytInitialData =")[1].split(";</script>")[0]); } catch (e) { return []; } const content = (_k = (_j = (_i = (_h = (_g = (_f = (_e = (_d = (_c = (_b = (_a = json.contents) == null ? void 0 : _a.twoColumnBrowseResultsRenderer) == null ? void 0 : _b.tabs[0].tabRenderer) == null ? void 0 : _c.content) == null ? void 0 : _d.sectionListRenderer) == null ? void 0 : _e.contents[1]) == null ? void 0 : _f.itemSectionRenderer) == null ? void 0 : _g.contents[0]) == null ? void 0 : _h.shelfRenderer) == null ? void 0 : _i.content) == null ? void 0 : _j.expandedShelfContentsRenderer) == null ? void 0 : _k.items; if (!content || !Array.isArray(content)) return []; const res = []; for (const item of content.map((m) => m.videoRenderer)) { res.push( new Video({ title: item.title.runs[0].text, id: item.videoId, url: `https://www.youtube.com/watch?v=${item.videoId}`, description: (_m = (_l = item.descriptionSnippet) == null ? void 0 : _l.runs[0])