Spaces:
Running
Running
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" } | |
} | |