use companion to get video info

This commit is contained in:
localhost 2025-11-12 15:39:56 +01:00
parent a6f51e37fb
commit a5ba61e066
2 changed files with 52 additions and 60 deletions

View File

@ -11,7 +11,7 @@ import {
type DownloadOutput type DownloadOutput
} from './utils/sabr-stream-factory.js'; } from './utils/sabr-stream-factory.js';
import type { SabrPlaybackOptions } from 'googlevideo/sabr-stream'; 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 ffmpeg = require('fluent-ffmpeg')
const ffmpegStatic = require('ffmpeg-static') const ffmpegStatic = require('ffmpeg-static')
@ -51,39 +51,22 @@ app.get('/health', async (req, res) => {
}) })
app.get('/video/:id', 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) { if (!info) {
error = 'ErrorCantConnectToServiceAPI' return res.json({ error: 'ErrorCantConnectToServiceAPI' })
continue;
} }
if (info.playability_status!.status !== 'OK') { if (info.playabilityStatus!.status !== 'OK') {
error = 'ErrorYTUnavailable' return res.json({ error: 'ErrorYTUnavailable' })
continue;
} }
if (info.basic_info.is_live) { if (info.videoDetails.isLiveContent) {
error = 'ErrorLiveVideo' return res.json({ error: 'ErrorLiveVideo' })
continue;
}
if (info.basic_info.title == 'Video Not Available') {
error = 'YoutubeIsFuckingWithMe'
continue;
}
return res.json(info)
} catch (error) {
continue
} }
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) => { app.get('/channel/:id', async (req, res) => {
@ -141,27 +124,24 @@ interface Config {
// @ts-ignore // @ts-ignore
app.ws('/download/:id', async (ws, req) => { app.ws('/download/:id', async (ws, req) => {
const config: Config = await Bun.file('config.json').json() const config: Config = await Bun.file('config.json').json()
const yt = await Innertube.create();
let quality = '480p' let quality = '480p'
const info = await yt.getInfo(req.params.id, { const info = await getInfo(req.params.id);
client: 'TV_SIMPLY' if (!info || info.videoDetails.lengthSeconds == undefined) {
});
if (!info || info.basic_info.duration == undefined) {
ws.send('Unable to retrieve video info from YouTube. Please try again later.'); ws.send('Unable to retrieve video info from YouTube. Please try again later.');
return ws.close() 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) 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}).`); ws.send(`This video is not available for download (${info.playability_status?.status} ${info.playability_status?.reason}).`);
return ws.close() 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.'); ws.send('This video is live, and cannot be downloaded.');
return ws.close() 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.'); ws.send('This video is not available for download. Youtube is serving a different video.');
return ws.close() return ws.close()
} }
@ -174,7 +154,7 @@ app.ws('/download/:id', async (ws, req) => {
videoQuality: quality, videoQuality: quality,
audioQuality: 'AUDIO_QUALITY_LOW' 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) { if (streamResults == false) {
ws.send(error) ws.send(error)
return ws.close() return ws.close()
@ -182,21 +162,21 @@ app.ws('/download/:id', async (ws, req) => {
const { videoStreamUrl, audioStreamUrl, selectedFormats } = streamResults; const { videoStreamUrl, audioStreamUrl, selectedFormats } = streamResults;
const videoSizeTotal = (selectedFormats.audioFormat.content_length || 0) const videoSizeTotal = (parseInt(selectedFormats.audioFormat.contentLength) || 0)
+ (selectedFormats.videoFormat.content_length || 0) + (parseInt(selectedFormats.videoFormat.contentLength) || 0)
if (videoSizeTotal > (1_048_576 * config.maxVideoSize) && !config.whitelist.includes(req.params.id)) { 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('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.'); ws.send('This video is too large, and unfortunately, Preservetube does not have unlimited storage.');
return ws.close() 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('Youtube isn\'t giving us enough information to be able to tell if we can process this video.')
ws.send('Please try again later.') ws.send('Please try again later.')
return ws.close() return ws.close()
} }
audioOutputStream = createOutputStream(req.params.id, selectedFormats.audioFormat.mime_type!); audioOutputStream = createOutputStream(req.params.id, selectedFormats.audioFormat.mimeType!);
videoOutputStream = createOutputStream(req.params.id, selectedFormats.videoFormat.mime_type!); videoOutputStream = createOutputStream(req.params.id, selectedFormats.videoFormat.mimeType!);
await Promise.all([ await Promise.all([
downloadStream(videoStreamUrl, selectedFormats.videoFormat, videoOutputStream.stream, ws, 'video'), downloadStream(videoStreamUrl, selectedFormats.videoFormat, videoOutputStream.stream, ws, 'video'),
@ -277,21 +257,21 @@ app.get('/getWebpageJson', async (req, res) => {
}) })
function getVideoQuality(json: any, quality: string) { function getVideoQuality(json: any, quality: string) {
const adaptiveFormats = json['streaming_data']['adaptive_formats']; const adaptiveFormats = json.streamingData.adaptiveFormats;
let video = adaptiveFormats.find((f: any) => f.quality_label === quality && !f.has_audio); let video = adaptiveFormats.find((f: any) => f.qualityLabel === quality && !f.audioQuality);
if (!video) { if (!video) {
const target = parseInt(quality); const target = parseInt(quality);
video = adaptiveFormats // find the quality thats closest to the one we wanted 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) => { .reduce((prev: any, curr: any) => {
const currDiff = Math.abs(parseInt(curr.quality_label) - target); const currDiff = Math.abs(parseInt(curr.qualityLabel) - target);
const prevDiff = prev ? Math.abs(parseInt(prev.quality_label) - target) : Infinity; const prevDiff = prev ? Math.abs(parseInt(prev.qualityLabel) - target) : Infinity;
return currDiff < prevDiff ? curr : prev; return currDiff < prevDiff ? curr : prev;
}, null); }, null);
} }
return video ? video.quality_label : null; return video ? video.qualityLabel : null;
} }
function mergeIt(audioPath: string, videoPath: string, outputPath: string, ws: any) { function mergeIt(audioPath: string, videoPath: string, outputPath: string, ws: any) {

View File

@ -19,24 +19,24 @@ export async function getVideoStreams(
error?: string error?: string
}> { }> {
const lowestStorageVideo = adaptiveFormats const lowestStorageVideo = adaptiveFormats
.filter((format) => !!format.quality_label?.toLowerCase().includes(options.videoQuality?.toLowerCase() || '')) .filter((format) => !!format.qualityLabel?.toLowerCase().includes(options.videoQuality?.toLowerCase() || ''))
.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 lowestStorageAudio = adaptiveFormats const lowestStorageAudio = adaptiveFormats
.filter(f => { .filter(f => {
if (f.is_auto_dubbed || !f.audio_quality) return false if (f.audioTrack?.isAutoDubbed || !f.audioQuality) return false
if (f.audio_track && !f.audio_track.display_name.endsWith('original')) return false if (f.audioTrack && !f.audioTrack.displayName.endsWith('original')) return false
return true return true
}) })
.sort((a, b) => { // if the wanted audio quality isnt avalible, just accept smth else. but take the one requested if its there .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 aMatches = a.audioQuality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '')
const bMatches = b.audio_quality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '') const bMatches = b.audioQuality?.toLowerCase().includes(options.audioQuality?.toLowerCase() || '')
if (aMatches && !bMatches) return -1 if (aMatches && !bMatches) return -1
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 = { const lowestOptions = {
videoFormat: lowestStorageVideo?.itag, videoFormat: lowestStorageVideo?.itag,
audioFormat: lowestStorageAudio?.itag audioFormat: lowestStorageAudio?.itag
@ -175,3 +175,15 @@ function secondsToTime(seconds: number) {
const formattedSeconds = remainingSeconds < 10 ? '0' + remainingSeconds : remainingSeconds; const formattedSeconds = remainingSeconds < 10 ? '0' + remainingSeconds : remainingSeconds;
return `${minutes}:${formattedSeconds}`; 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()
}