From a5ba61e066a54ddffecfc1e5e730d33a8c0b84c0 Mon Sep 17 00:00:00 2001 From: localhost Date: Wed, 12 Nov 2025 15:39:56 +0100 Subject: [PATCH] use companion to get video info --- index.ts | 84 ++++++++++++++++++---------------------------- utils/companion.ts | 28 +++++++++++----- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/index.ts b/index.ts index 4258b72..6e45422 100644 --- a/index.ts +++ b/index.ts @@ -11,7 +11,7 @@ import { type DownloadOutput } from './utils/sabr-stream-factory.js'; import type { SabrPlaybackOptions } from 'googlevideo/sabr-stream'; -import { getVideoStreams, downloadStream } from './utils/companion'; +import { getVideoStreams, downloadStream, getInfo } from './utils/companion'; const ffmpeg = require('fluent-ffmpeg') const ffmpegStatic = require('ffmpeg-static') @@ -51,39 +51,22 @@ app.get('/health', async (req, res) => { }) app.get('/video/:id', async (req, res) => { - let error = '' + const info = await getInfo(req.params.id); - for (let retries = 0; retries < maxRetries; retries++) { - try { - const platform = platforms[retries % platforms.length]; - const yt = await Innertube.create(); - const info = await yt.getInfo(req.params.id, { // @ts-ignore - client: platform - }); - - if (!info) { - error = 'ErrorCantConnectToServiceAPI' - continue; - } - if (info.playability_status!.status !== 'OK') { - error = 'ErrorYTUnavailable' - continue; - } - if (info.basic_info.is_live) { - error = 'ErrorLiveVideo' - continue; - } - if (info.basic_info.title == 'Video Not Available') { - error = 'YoutubeIsFuckingWithMe' - continue; - } - return res.json(info) - } catch (error) { - continue - } + if (!info) { + return res.json({ error: 'ErrorCantConnectToServiceAPI' }) + } + if (info.playabilityStatus!.status !== 'OK') { + return res.json({ error: 'ErrorYTUnavailable' }) + } + if (info.videoDetails.isLiveContent) { + return res.json({ error: 'ErrorLiveVideo' }) + } + if (info.videoDetails.title == 'Video Not Available') { + return res.json({ error: 'YoutubeIsFuckingWithMe' }) } - res.json({ error: error || 'ErrorUnknown' }) + return res.json(info) }) app.get('/channel/:id', async (req, res) => { @@ -141,27 +124,24 @@ interface Config { // @ts-ignore app.ws('/download/:id', async (ws, req) => { const config: Config = await Bun.file('config.json').json() - const yt = await Innertube.create(); let quality = '480p' - const info = await yt.getInfo(req.params.id, { - client: 'TV_SIMPLY' - }); - if (!info || info.basic_info.duration == undefined) { + const info = await getInfo(req.params.id); + if (!info || info.videoDetails.lengthSeconds == undefined) { ws.send('Unable to retrieve video info from YouTube. Please try again later.'); return ws.close() } - if (info.basic_info.duration >= 900) quality = '360p' // 15min + if (parseInt(info.videoDetails.lengthSeconds) >= 900) quality = '360p' // 15min quality = getVideoQuality(info, quality) - if (info.playability_status?.status !== 'OK') { + if (info.playabilityStatus?.status !== 'OK') { ws.send(`This video is not available for download (${info.playability_status?.status} ${info.playability_status?.reason}).`); return ws.close() - } else if (info.basic_info.is_live) { + } else if (info.videoDetails.isLiveContent) { ws.send('This video is live, and cannot be downloaded.'); return ws.close() - } else if (info.basic_info.id != req.params.id) { + } else if (info.videoDetails.videoId != req.params.id) { ws.send('This video is not available for download. Youtube is serving a different video.'); return ws.close() } @@ -174,7 +154,7 @@ app.ws('/download/:id', async (ws, req) => { videoQuality: quality, audioQuality: 'AUDIO_QUALITY_LOW' }; - const { streamResults, error } = await getVideoStreams(req.params.id, info.streaming_data!.adaptive_formats, streamOptions); + const { streamResults, error } = await getVideoStreams(req.params.id, info.streamingData!.adaptiveFormats, streamOptions); if (streamResults == false) { ws.send(error) return ws.close() @@ -182,21 +162,21 @@ app.ws('/download/:id', async (ws, req) => { const { videoStreamUrl, audioStreamUrl, selectedFormats } = streamResults; - const videoSizeTotal = (selectedFormats.audioFormat.content_length || 0) - + (selectedFormats.videoFormat.content_length || 0) + const videoSizeTotal = (parseInt(selectedFormats.audioFormat.contentLength) || 0) + + (parseInt(selectedFormats.videoFormat.contentLength) || 0) if (videoSizeTotal > (1_048_576 * config.maxVideoSize) && !config.whitelist.includes(req.params.id)) { ws.send('Is this content considered high risk? If so, please email me at admin@preservetube.com.'); ws.send('This video is too large, and unfortunately, Preservetube does not have unlimited storage.'); return ws.close() - } else if (!selectedFormats.videoFormat.content_length) { + } else if (!selectedFormats.videoFormat.contentLength) { ws.send('Youtube isn\'t giving us enough information to be able to tell if we can process this video.') ws.send('Please try again later.') return ws.close() } - audioOutputStream = createOutputStream(req.params.id, selectedFormats.audioFormat.mime_type!); - videoOutputStream = createOutputStream(req.params.id, selectedFormats.videoFormat.mime_type!); + audioOutputStream = createOutputStream(req.params.id, selectedFormats.audioFormat.mimeType!); + videoOutputStream = createOutputStream(req.params.id, selectedFormats.videoFormat.mimeType!); await Promise.all([ downloadStream(videoStreamUrl, selectedFormats.videoFormat, videoOutputStream.stream, ws, 'video'), @@ -277,21 +257,21 @@ app.get('/getWebpageJson', async (req, res) => { }) function getVideoQuality(json: any, quality: string) { - const adaptiveFormats = json['streaming_data']['adaptive_formats']; - let video = adaptiveFormats.find((f: any) => f.quality_label === quality && !f.has_audio); + const adaptiveFormats = json.streamingData.adaptiveFormats; + let video = adaptiveFormats.find((f: any) => f.qualityLabel === quality && !f.audioQuality); if (!video) { const target = parseInt(quality); video = adaptiveFormats // find the quality thats closest to the one we wanted - .filter((f: any) => !f.has_audio && f.quality_label) + .filter((f: any) => !f.audioQuality && f.qualityLabel) .reduce((prev: any, curr: any) => { - const currDiff = Math.abs(parseInt(curr.quality_label) - target); - const prevDiff = prev ? Math.abs(parseInt(prev.quality_label) - target) : Infinity; + const currDiff = Math.abs(parseInt(curr.qualityLabel) - target); + const prevDiff = prev ? Math.abs(parseInt(prev.qualityLabel) - target) : Infinity; return currDiff < prevDiff ? curr : prev; }, null); } - return video ? video.quality_label : null; + return video ? video.qualityLabel : null; } function mergeIt(audioPath: string, videoPath: string, outputPath: string, ws: any) { diff --git a/utils/companion.ts b/utils/companion.ts index 7326af9..6028ba1 100644 --- a/utils/companion.ts +++ b/utils/companion.ts @@ -19,24 +19,24 @@ export async function getVideoStreams( error?: string }> { const lowestStorageVideo = adaptiveFormats - .filter((format) => !!format.quality_label?.toLowerCase().includes(options.videoQuality?.toLowerCase() || '')) - .sort((a, b) => (a.content_length || 0) - (b.content_length || 0))?.[0] + .filter((format) => !!format.qualityLabel?.toLowerCase().includes(options.videoQuality?.toLowerCase() || '')) + .sort((a, b) => (parseInt(a.contentLength) || 0) - (parseInt(b.contentLength) || 0))?.[0] const lowestStorageAudio = adaptiveFormats .filter(f => { - if (f.is_auto_dubbed || !f.audio_quality) return false - if (f.audio_track && !f.audio_track.display_name.endsWith('original')) return false + if (f.audioTrack?.isAutoDubbed || !f.audioQuality) return false + if (f.audioTrack && !f.audioTrack.displayName.endsWith('original')) return false return true }) .sort((a, b) => { // if the wanted audio quality isnt avalible, just accept smth else. but take the one requested if its there - const aMatches = a.audio_quality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '') - const bMatches = b.audio_quality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '') + const aMatches = a.audioQuality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '') + const bMatches = b.audioQuality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '') if (aMatches && !bMatches) return -1 if (!aMatches && bMatches) return 1 - return (a.content_length || Infinity) - (b.content_length || Infinity) + return (parseInt(a.contentLength) || Infinity) - (parseInt(b.contentLength) || Infinity) }) - .sort((a, b) => (a.content_length || 0) - (b.content_length || 0))?.[0] + .sort((a, b) => (parseInt(a.contentLength) || 0) - (parseInt(b.contentLength) || 0))?.[0] const lowestOptions = { videoFormat: lowestStorageVideo?.itag, audioFormat: lowestStorageAudio?.itag @@ -174,4 +174,16 @@ function secondsToTime(seconds: number) { const remainingSeconds = seconds % 60; const formattedSeconds = remainingSeconds < 10 ? '0' + remainingSeconds : remainingSeconds; return `${minutes}:${formattedSeconds}`; +} + +export async function getInfo(videoId: string) { + const req = await fetch('http://127.0.0.1:8282/companion/youtubei/v1/player', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer EiChaeKiefei7Ahj' // this is internal. this is not a security issue. + }, + body: JSON.stringify({ videoId }) + }) + return await req.json() } \ No newline at end of file