From c64fa7e595faba141f80a135afc67470d32428d3 Mon Sep 17 00:00:00 2001 From: drunkwinter <38593134+drunkwinter@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:06:18 +0200 Subject: [PATCH] [proxy] do not use `adaptiveFormats` This is a workaround to videos not playing on the client, but causes us to only be able to play low quality streams. Additional changes: Cleaned up the horrible mess. --- account-proxy/controllers/proxyController.js | 78 ++++++++----------- account-proxy/lib/innertubeApi.js | 79 ++++---------------- 2 files changed, 46 insertions(+), 111 deletions(-) diff --git a/account-proxy/controllers/proxyController.js b/account-proxy/controllers/proxyController.js index 037e654..58e4759 100644 --- a/account-proxy/controllers/proxyController.js +++ b/account-proxy/controllers/proxyController.js @@ -1,5 +1,3 @@ -import process from 'node:process'; - import dotenv from 'dotenv'; import { ProxyAgent } from 'undici'; @@ -14,14 +12,6 @@ const credentials = new YouTubeCredentials(); const proxy = process.env.PROXY; const proxyAgent = proxy ? new ProxyAgent(proxy) : undefined; -const relevantAttributes = [ - 'playabilityStatus', - 'videoDetails', - 'streamingData', - 'contents', - 'engagementPanels' -] - function getPlayer(req, res) { handleProxyRequest(req, res, 'player'); } @@ -53,51 +43,49 @@ async function handleProxyRequest(req, res, endpoint) { pRequests.push(innertubeApi.sendApiRequest('next', clientParams, credentials, proxyAgent)); } - const youtubeResponses = await Promise.all(pRequests); - let result = handleResponses(youtubeResponses, clientParams, endpoint); + const [playerResponse, nextResponse] = await Promise.all(pRequests); - res.code(200).send(result); - } catch (err) { - console.error(endpoint, err.message); - res.code(500).send({ errorMessage: err.message }); - stats.countResponse(endpoint, 'EXCEPTION'); - stats.countException(endpoint, err.message); - } finally { - let latencyMs = new Date().getTime() - tsStart; - stats.countLatency(latencyMs); - } -} + if (!playerResponse || typeof playerResponse !== 'object') { + throw new Error(`Invalid YouTube response received for endpoint /player`); + } -function handleResponses(youtubeResponses, clientParams, endpoint) { - let responseData = {}; + if (clientParams.includeNext && (!nextResponse || typeof nextResponse !== 'object')) { + throw new Error(`Invalid YouTube response received for endpoint /next`); + } - youtubeResponses.forEach((response, index) => { - const currentEndpoint = ((endpoint === 'next' && index === 0) || (endpoint === 'player' && index === 1)) ? 'next' : 'player'; + const youtubeStatus = getYoutubeResponseStatus(playerResponse); + const youtubeGcrFlagSet = checkForGcrFlag(playerResponse); - if (response === null || typeof response !== 'object') { - throw new Error(`Invalid YouTube response received for endpoint /${currentEndpoint}`); - } + stats.countResponse('player', youtubeStatus, youtubeGcrFlagSet); + + const responseData = extractAttributes(playerResponse, ['playabilityStatus', 'videoDetails', 'streamingData']); - const youtubeStatus = getYoutubeResponseStatus(response); - const youtubeGcrFlagSet = checkForGcrFlag(response); - const relevantData = extractAttributes(response, relevantAttributes); + responseData.proxy = { clientParams, youtubeGcrFlagSet, youtubeStatus }; - stats.countResponse(currentEndpoint, youtubeStatus, youtubeGcrFlagSet); + /** + * Workaround: when we provide `adaptiveFormats` the client cannot playback the video. + * + * It seems the URLs we get here or the one the client constructs from these URLs are tied to the requesting account. + * The low quality `formats` URLs seem fine. + */ + delete responseData.streamingData.adaptiveFormats; - if (index === 0) { - responseData = relevantData; + if (nextResponse) { + stats.countResponse('next', getYoutubeResponseStatus(nextResponse), null); + responseData.nextResponse = extractAttributes(nextResponse, ['contents', 'engagementPanels']); + } - responseData.proxy = { - clientParams, - youtubeGcrFlagSet, - youtubeStatus - }; - } else { - responseData[currentEndpoint + 'Response'] = relevantData; + res.code(200).send(responseData); + } catch (err) { + if (err instanceof Error) { + console.error(endpoint, err.message); + res.code(500).send({ errorMessage: err.message }); + stats.countResponse(endpoint, 'EXCEPTION'); + stats.countException(endpoint, err.message); } - }); + } - return responseData; + stats.countLatency(new Date().getTime() - tsStart); } export default { diff --git a/account-proxy/lib/innertubeApi.js b/account-proxy/lib/innertubeApi.js index aa6f049..7c12784 100644 --- a/account-proxy/lib/innertubeApi.js +++ b/account-proxy/lib/innertubeApi.js @@ -2,6 +2,13 @@ import crypto from 'node:crypto'; import { fetch } from 'undici'; +const generateSidBasedAuth = function (sapisid) { + const timestamp = Math.floor(new Date().getTime() / 1000); + const hashInput = timestamp + " " + sapisid + " " + "https://www.youtube.com"; + const hashDigest = crypto.createHash("sha1").update(hashInput).digest("hex"); + return `SAPISIDHASH ${timestamp}_${hashDigest}`; +} + const generateApiRequestData = function (clientParams) { return { "videoId": clientParams.videoId, @@ -9,94 +16,34 @@ const generateApiRequestData = function (clientParams) { "client": { "hl": clientParams.hl, "gl": "US", - "deviceMake": "", - "deviceModel": "", - "visitorData": "CgtIZDE3aXJqLXFwNCiZpPaHBg%3D%3D", - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36,gzip(gfe)", "clientName": clientParams.clientName, "clientVersion": clientParams.clientVersion, - "osName": "Windows", - "osVersion": "10.0", - "originalUrl": "https://www.youtube.com/watch?v=" + clientParams.videoId, - "screenPixelDensity": 1, - "platform": "DESKTOP", - "clientFormFactor": "UNKNOWN_FORM_FACTOR", - "screenDensityFloat": 1.25, - "timeZone": "Europe/Berlin", - "browserName": "Chrome", - "browserVersion": "91.0.4472.164", - "screenWidthPoints": 834, - "screenHeightPoints": 1051, - "utcOffsetMinutes": 120, "userInterfaceTheme": clientParams.userInterfaceTheme, - "connectionType": "CONN_CELLULAR_4G", - "mainAppWebInfo": { - "graftUrl": "https://www.youtube.com/watch?v=" + clientParams.videoId, - "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER", - "isWebNativeShareAvailable": true - }, - "playerType": "UNIPLAYER", - "tvAppInfo": { - "livingRoomAppMode": "LIVING_ROOM_APP_MODE_UNSPECIFIED" - }, - "clientScreen": "WATCH_FULL_SCREEN" - }, - "user": { - "lockedSafetyMode": false - }, - "request": { - "useSsl": true, - "internalExperimentFlags": [], - "consistencyTokenJars": [] }, }, "playbackContext": { "contentPlaybackContext": { - "html5Preference": "HTML5_PREF_WANTS", - "lactMilliseconds": "2132", - "referer": "https://www.youtube.com/watch?v=" + clientParams.videoId, "signatureTimestamp": clientParams.signatureTimestamp, - "autoCaptionsDefaultOn": false, - "autoplay": true, - "mdxContext": {}, - "playerWidthPixels": 770, - "playerHeightPixels": 433 } }, - "captionParams": {}, "racyCheckOk": true, "contentCheckOk": true, "startTimeSecs": clientParams.startTimeSecs, } } -const generateSidBasedAuth = function (sapisid, origin) { - const timestamp = Math.floor(new Date().getTime() / 1000); - const hashInput = timestamp + " " + sapisid + " " + origin; - const hashDigest = crypto.createHash("sha1").update(hashInput).digest("hex"); - return `SAPISIDHASH ${timestamp}_${hashDigest}`; -} - const generateApiRequestHeaders = function (credentials) { - const origin = "https://www.youtube.com"; - return { - "Cookie": `SID=${credentials.SID}; HSID=${credentials.HSID}; SSID=${credentials.SSID}; APISID=${credentials.APISID}; SAPISID=${credentials.SAPISID}; __Secure-1PSIDTS=${credentials.PSIDTS};`, - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36", - "Content-Type": "application/json", - "Authorization": generateSidBasedAuth(credentials.SAPISID, origin), - "X-Origin": origin, - "X-Youtube-Client-Name": "1", - "X-Youtube-Client-Version": "2.20210721.00.00", - "Accept-Language": "en-US;q=0.8,en;q=0.7", - "Origin": origin, - "Referer": "https://www.youtube.com/watch?v=ENdgyD7Uar4" + "cookie": `SID=${credentials.SID}; HSID=${credentials.HSID}; SSID=${credentials.SSID}; APISID=${credentials.APISID}; SAPISID=${credentials.SAPISID}; __Secure-1PSIDTS=${credentials.PSIDTS};`, + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "content-type": "application/json", + "authorization": generateSidBasedAuth(credentials.SAPISID), + "origin": "https://www.youtube.com", } } const sendApiRequest = async function (endpoint, clientParams, credentials, proxyAgent) { - - const url = `https://www.youtube.com/youtubei/v1/${endpoint}?key=${credentials.API_KEY}&prettyPrint=false`; + const url = `https://www.youtube.com/youtubei/v1/${endpoint}?prettyPrint=false`; const headers = generateApiRequestHeaders(credentials); const data = generateApiRequestData(clientParams);