From 0feacf0ae5b3d816a1a9b88128d1d2e7f3ab4b8a Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 12:25:22 +0600 Subject: [PATCH 1/3] youtube: use web client and decipher urls --- src/modules/processing/services/youtube.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 8a773375..63e02eb3 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -33,7 +33,7 @@ export default async function(o) { } try { - info = await yt.getBasicInfo(o.id, 'YTMUSIC_ANDROID'); + info = await yt.getBasicInfo(o.id, 'WEB'); } catch (e) { return { error: 'ErrorCantConnectToServiceAPI' }; } @@ -43,7 +43,9 @@ export default async function(o) { if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' }; if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' }; - let bestQuality, hasAudio, adaptive_formats = info.streaming_data.adaptive_formats.filter(e => + let bestQuality, hasAudio; + + let adaptive_formats = info.streaming_data.adaptive_formats.filter(e => e.mime_type.includes(c[o.format].codec) || e.mime_type.includes(c[o.format].aCodec) ).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); @@ -96,7 +98,7 @@ export default async function(o) { if (hasAudio && o.isAudioOnly) return { type: "render", isAudioOnly: true, - urls: audio.url, + urls: audio.decipher(yt.session.player), filenameAttributes: filenameAttributes, fileMetadata: fileMetadata } @@ -108,14 +110,14 @@ export default async function(o) { if (!o.isAudioOnly && !o.isAudioMuted && o.format === 'h264') { match = info.streaming_data.formats.find(checkSingle); type = "bridge"; - urls = match?.url; + urls = match?.decipher(yt.session.player); } const video = adaptive_formats.find(checkRender); if (!match && video) { match = video; type = "render"; - urls = [video.url, audio.url]; + urls = [video.decipher(yt.session.player), audio.decipher(yt.session.player)]; } if (match) { From 8771b7d7d4fc854c2e0d12eb10a7bc523730fb80 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 12:25:46 +0600 Subject: [PATCH 2/3] package: bump youtubei.js version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b0b3443..dddc5a03 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "set-cookie-parser": "2.6.0", "undici": "^6.7.0", "url-pattern": "1.0.3", - "youtubei.js": "^9.2.0" + "youtubei.js": "^9.3.0" } } From 43101b604c734827e5937695ba11584b791a78a0 Mon Sep 17 00:00:00 2001 From: wukko Date: Fri, 26 Apr 2024 15:07:32 +0600 Subject: [PATCH 3/3] stream/types: proper headers for all http requests & refactor --- src/modules/stream/types.js | 79 +++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index 2b7d7482..d3e33438 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -5,6 +5,30 @@ import { metadataManager } from "../sub/utils.js"; import { request } from "undici"; import { create as contentDisposition } from "content-disposition-header"; +const defaultHeaders = { + 'user-agent': genericUserAgent +} +const serviceHeaders = { + bilibili: { + referer: 'https://www.bilibili.com/' + }, + youtube: { + accept: '*/*', + origin: 'https://www.youtube.com', + referer: 'https://www.youtube.com', + DNT: '?1' + } +} + +function getHeaders(service) { + return { ...defaultHeaders, ...serviceHeaders[service] } +} +function toRawHeaders(headers) { + return Object.entries(headers) + .map(([key, value]) => `${key}: ${value}\r\n`) + .join(''); +} + function closeRequest(controller) { try { controller.abort() } catch {} } @@ -53,7 +77,7 @@ export async function streamDefault(streamInfo, res) { res.setHeader('Content-disposition', contentDisposition(filename)); const { body: stream, headers } = await request(streamInfo.urls, { - headers: { 'user-agent': genericUserAgent }, + headers: getHeaders(streamInfo.service), signal: abortController.signal, maxRedirections: 16 }); @@ -68,49 +92,43 @@ export async function streamDefault(streamInfo, res) { } export async function streamLiveRender(streamInfo, res) { - let abortController = new AbortController(), process; + let process, abortController = new AbortController(); + const shutdown = () => ( closeRequest(abortController), killProcess(process), closeResponse(res) ); + const headers = getHeaders(streamInfo.service); + const rawHeaders = toRawHeaders(headers); + try { if (streamInfo.urls.length !== 2) return shutdown(); const { body: audio } = await request(streamInfo.urls[1], { - maxRedirections: 16, signal: abortController.signal, - headers: { - 'user-agent': genericUserAgent, - referer: streamInfo.service === 'bilibili' - ? 'https://www.bilibili.com/' - : undefined, - } + headers, + signal: abortController.signal, + maxRedirections: 16 }); const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]; + let args = [ '-loglevel', '-8', - '-user_agent', genericUserAgent - ]; - - if (streamInfo.service === 'bilibili') { - args.push( - '-headers', 'Referer: https://www.bilibili.com/\r\n', - ) - } - - args.push( + '-headers', rawHeaders, '-i', streamInfo.urls[0], '-i', 'pipe:3', '-map', '0:v', '-map', '1:a', - ); + ] args = args.concat(ffmpegArgs[format]); + if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) } + args.push('-f', format, 'pipe:4'); process = spawn(...getCommand(args), { @@ -128,6 +146,7 @@ export async function streamLiveRender(streamInfo, res) { audio.on('error', shutdown); audioInput.on('error', shutdown); + audio.pipe(audioInput); pipe(muxOutput, res, shutdown); @@ -145,13 +164,11 @@ export function streamAudioOnly(streamInfo, res) { try { let args = [ '-loglevel', '-8', - '-user_agent', genericUserAgent - ]; + '-headers', toRawHeaders(getHeaders(streamInfo.service)), + ] if (streamInfo.service === "twitter") { args.push('-seekable', '0'); - } else if (streamInfo.service === 'bilibili') { - args.push('-headers', 'Referer: https://www.bilibili.com/\r\n'); } args.push( @@ -162,12 +179,12 @@ export function streamAudioOnly(streamInfo, res) { if (streamInfo.metadata) { args = args.concat(metadataManager(streamInfo.metadata)) } - let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]; - args = args.concat(arg); + args = args.concat(ffmpegArgs[streamInfo.copy ? 'copy' : 'audio']); if (ffmpegArgs[streamInfo.audioFormat]) { args = args.concat(ffmpegArgs[streamInfo.audioFormat]) } + args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:3'); process = spawn(...getCommand(args), { @@ -196,13 +213,12 @@ export function streamVideoOnly(streamInfo, res) { try { let args = [ - '-loglevel', '-8' + '-loglevel', '-8', + '-headers', toRawHeaders(getHeaders(streamInfo.service)), ] if (streamInfo.service === "twitter") { args.push('-seekable', '0') - } else if (streamInfo.service === 'bilibili') { - args.push('-headers', 'Referer: https://www.bilibili.com/\r\n') } args.push( @@ -222,6 +238,7 @@ export function streamVideoOnly(streamInfo, res) { if (format === "mp4") { args.push('-movflags', 'faststart+frag_keyframe+empty_moov') } + args.push('-f', format, 'pipe:3'); process = spawn(...getCommand(args), { @@ -254,10 +271,12 @@ export function convertToGif(streamInfo, res) { let args = [ '-loglevel', '-8' ] + if (streamInfo.service === "twitter") { args.push('-seekable', '0') } - args.push('-i', streamInfo.urls) + + args.push('-i', streamInfo.urls); args = args.concat(ffmpegArgs["gif"]); args.push('-f', "gif", 'pipe:3');