Spaces:
Paused
Paused
| import { fetch } from "undici"; | |
| import { Innertube, Session } from "youtubei.js"; | |
| import { env } from "../../config.js"; | |
| import { cleanString } from "../../misc/utils.js"; | |
| import { getCookie, updateCookieValues } from "../cookie/manager.js"; | |
| const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms | |
| let innertube, lastRefreshedAt; | |
| const codecMatch = { | |
| h264: { | |
| videoCodec: "avc1", | |
| audioCodec: "mp4a", | |
| container: "mp4" | |
| }, | |
| av1: { | |
| videoCodec: "av01", | |
| audioCodec: "opus", | |
| container: "webm" | |
| }, | |
| vp9: { | |
| videoCodec: "vp9", | |
| audioCodec: "opus", | |
| container: "webm" | |
| } | |
| } | |
| const transformSessionData = (cookie) => { | |
| if (!cookie) | |
| return; | |
| const values = { ...cookie.values() }; | |
| const REQUIRED_VALUES = [ 'access_token', 'refresh_token' ]; | |
| if (REQUIRED_VALUES.some(x => typeof values[x] !== 'string')) { | |
| return; | |
| } | |
| if (values.expires) { | |
| values.expiry_date = values.expires; | |
| delete values.expires; | |
| } else if (!values.expiry_date) { | |
| return; | |
| } | |
| return values; | |
| } | |
| const cloneInnertube = async (customFetch) => { | |
| const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date(); | |
| if (!innertube || shouldRefreshPlayer) { | |
| innertube = await Innertube.create({ | |
| fetch: customFetch | |
| }); | |
| lastRefreshedAt = +new Date(); | |
| } | |
| const session = new Session( | |
| innertube.session.context, | |
| innertube.session.key, | |
| innertube.session.api_version, | |
| innertube.session.account_index, | |
| innertube.session.player, | |
| undefined, | |
| customFetch ?? innertube.session.http.fetch, | |
| innertube.session.cache | |
| ); | |
| const cookie = getCookie('youtube_oauth'); | |
| const oauthData = transformSessionData(cookie); | |
| if (!session.logged_in && oauthData) { | |
| await session.oauth.init(oauthData); | |
| session.logged_in = true; | |
| } | |
| if (session.logged_in) { | |
| if (session.oauth.shouldRefreshToken()) { | |
| await session.oauth.refreshAccessToken(); | |
| } | |
| const cookieValues = cookie.values(); | |
| const oldExpiry = new Date(cookieValues.expiry_date); | |
| const newExpiry = new Date(session.oauth.oauth2_tokens.expiry_date); | |
| if (oldExpiry.getTime() !== newExpiry.getTime()) { | |
| updateCookieValues(cookie, { | |
| ...session.oauth.client_id, | |
| ...session.oauth.oauth2_tokens, | |
| expiry_date: newExpiry.toISOString() | |
| }); | |
| } | |
| } | |
| const yt = new Innertube(session); | |
| return yt; | |
| } | |
| export default async function(o) { | |
| let yt; | |
| try { | |
| yt = await cloneInnertube( | |
| (input, init) => fetch(input, { | |
| ...init, | |
| dispatcher: o.dispatcher | |
| }) | |
| ); | |
| } catch(e) { | |
| if (e.message?.endsWith("decipher algorithm")) { | |
| return { error: "youtube.decipher" } | |
| } else if (e.message?.includes("refresh access token")) { | |
| return { error: "youtube.token_expired" } | |
| } else throw e; | |
| } | |
| const quality = o.quality === "max" ? "9000" : o.quality; | |
| let info, isDubbed, | |
| format = o.format || "h264"; | |
| function qual(i) { | |
| if (!i.quality_label) { | |
| return; | |
| } | |
| return i.quality_label.split('p')[0].split('s')[0] | |
| } | |
| try { | |
| info = await yt.getBasicInfo(o.id, yt.session.logged_in ? 'ANDROID' : 'IOS'); | |
| } catch(e) { | |
| if (e?.info?.reason === "This video is private") { | |
| return { error: "content.video.private" }; | |
| } else if (e?.message === "This video is unavailable") { | |
| return { error: "content.video.unavailable" }; | |
| } else { | |
| return { error: "fetch.fail" }; | |
| } | |
| } | |
| if (!info) return { error: "fetch.fail" }; | |
| const playability = info.playability_status; | |
| const basicInfo = info.basic_info; | |
| if (playability.status === "LOGIN_REQUIRED") { | |
| if (playability.reason.endsWith("bot")) { | |
| return { error: "youtube.login" } | |
| } | |
| if (playability.reason.endsWith("age")) { | |
| return { error: "content.video.age" } | |
| } | |
| if (playability?.error_screen?.reason?.text === "Private video") { | |
| return { error: "content.video.private" } | |
| } | |
| } | |
| if (playability.status === "UNPLAYABLE") { | |
| if (playability?.reason?.endsWith("request limit.")) { | |
| return { error: "fetch.rate" } | |
| } | |
| if (playability?.error_screen?.subreason?.text?.endsWith("in your country")) { | |
| return { error: "content.video.region" } | |
| } | |
| if (playability?.error_screen?.reason?.text === "Private video") { | |
| return { error: "content.video.private" } | |
| } | |
| } | |
| if (playability.status !== "OK") { | |
| return { error: "content.video.unavailable" }; | |
| } | |
| if (basicInfo.is_live) { | |
| return { error: "content.video.live" }; | |
| } | |
| // return a critical error if returned video is "Video Not Available" | |
| // or a similar stub by youtube | |
| if (basicInfo.id !== o.id) { | |
| return { | |
| error: "fetch.fail", | |
| critical: true | |
| } | |
| } | |
| const filterByCodec = (formats) => | |
| formats | |
| .filter(e => | |
| e.mime_type.includes(codecMatch[format].videoCodec) | |
| || e.mime_type.includes(codecMatch[format].audioCodec) | |
| ) | |
| .sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); | |
| let adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats); | |
| if (adaptive_formats.length === 0 && format === "vp9") { | |
| format = "h264" | |
| adaptive_formats = filterByCodec(info.streaming_data.adaptive_formats) | |
| } | |
| let bestQuality; | |
| const bestVideo = adaptive_formats.find(i => i.has_video && i.content_length); | |
| const hasAudio = adaptive_formats.find(i => i.has_audio && i.content_length); | |
| if (bestVideo) bestQuality = qual(bestVideo); | |
| if ((!bestQuality && !o.isAudioOnly) || !hasAudio) | |
| return { error: "youtube.codec" }; | |
| if (basicInfo.duration > env.durationLimit) | |
| return { error: "content.too_long" }; | |
| const checkBestAudio = (i) => (i.has_audio && !i.has_video); | |
| let audio = adaptive_formats.find(i => | |
| checkBestAudio(i) && i.is_original | |
| ); | |
| if (o.dubLang) { | |
| let dubbedAudio = adaptive_formats.find(i => | |
| checkBestAudio(i) | |
| && i.language === o.dubLang | |
| && i.audio_track | |
| ) | |
| if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) { | |
| audio = dubbedAudio; | |
| isDubbed = true; | |
| } | |
| } | |
| if (!audio) { | |
| audio = adaptive_formats.find(i => checkBestAudio(i)); | |
| } | |
| let fileMetadata = { | |
| title: cleanString(basicInfo.title.trim()), | |
| artist: cleanString(basicInfo.author.replace("- Topic", "").trim()), | |
| } | |
| if (basicInfo?.short_description?.startsWith("Provided to YouTube by")) { | |
| let descItems = basicInfo.short_description.split("\n\n", 5); | |
| if (descItems.length === 5) { | |
| fileMetadata.album = descItems[2]; | |
| fileMetadata.copyright = descItems[3]; | |
| if (descItems[4].startsWith("Released on:")) { | |
| fileMetadata.date = descItems[4].replace("Released on: ", '').trim(); | |
| } | |
| } | |
| } | |
| let filenameAttributes = { | |
| service: "youtube", | |
| id: o.id, | |
| title: fileMetadata.title, | |
| author: fileMetadata.artist, | |
| youtubeDubName: isDubbed ? o.dubLang : false | |
| } | |
| if (audio && o.isAudioOnly) return { | |
| type: "audio", | |
| isAudioOnly: true, | |
| urls: audio.decipher(yt.session.player), | |
| filenameAttributes: filenameAttributes, | |
| fileMetadata: fileMetadata, | |
| bestAudio: format === "h264" ? "m4a" : "opus" | |
| } | |
| const matchingQuality = Number(quality) > Number(bestQuality) ? bestQuality : quality, | |
| checkSingle = i => | |
| qual(i) === matchingQuality && i.mime_type.includes(codecMatch[format].videoCodec), | |
| checkRender = i => | |
| qual(i) === matchingQuality && i.has_video && !i.has_audio; | |
| let match, type, urls; | |
| // prefer good premuxed videos if available | |
| if (!o.isAudioOnly && !o.isAudioMuted && format === "h264" && bestVideo.fps <= 30) { | |
| match = info.streaming_data.formats.find(checkSingle); | |
| type = "proxy"; | |
| urls = match?.decipher(yt.session.player); | |
| } | |
| const video = adaptive_formats.find(checkRender); | |
| if (!match && video && audio) { | |
| match = video; | |
| type = "merge"; | |
| urls = [ | |
| video.decipher(yt.session.player), | |
| audio.decipher(yt.session.player) | |
| ] | |
| } | |
| if (match) { | |
| filenameAttributes.qualityLabel = match.quality_label; | |
| filenameAttributes.resolution = `${match.width}x${match.height}`; | |
| filenameAttributes.extension = codecMatch[format].container; | |
| filenameAttributes.youtubeFormat = format; | |
| return { | |
| type, | |
| urls, | |
| filenameAttributes, | |
| fileMetadata | |
| } | |
| } | |
| return { error: "fetch.fail" } | |
| } | |