From 6ddf70d8d74ba73ea434dd085357989648340883 Mon Sep 17 00:00:00 2001 From: Skick Date: Sun, 4 Aug 2024 19:21:13 +0700 Subject: [PATCH] feat(info): use iOS client to fetch streaming data --- lib/{cookie.js => agent.js} | 0 lib/index.js | 6 +-- lib/info.js | 103 ++++++++++++++++++++++++++++++------ lib/utils.js | 15 ++++-- 4 files changed, 100 insertions(+), 24 deletions(-) rename lib/{cookie.js => agent.js} (100%) diff --git a/lib/cookie.js b/lib/agent.js similarity index 100% rename from lib/cookie.js rename to lib/agent.js diff --git a/lib/index.js b/lib/index.js index 74202da4..4b53da26 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,7 +7,7 @@ const sig = require('./sig'); const miniget = require('miniget'); const m3u8stream = require('m3u8stream'); const { parseTimestamp } = require('m3u8stream'); -const cookie = require('./cookie'); +const agent = require('./agent'); /** @@ -32,8 +32,8 @@ ytdl.validateID = urlUtils.validateID; ytdl.validateURL = urlUtils.validateURL; ytdl.getURLVideoID = urlUtils.getURLVideoID; ytdl.getVideoID = urlUtils.getVideoID; -ytdl.createAgent = cookie.createAgent; -ytdl.createProxyAgent = cookie.createProxyAgent; +ytdl.createAgent = agent.createAgent; +ytdl.createProxyAgent = agent.createProxyAgent; ytdl.cache = { sig: sig.cache, info: getInfo.cache, diff --git a/lib/info.js b/lib/info.js index 60f7f137..61df21cf 100644 --- a/lib/info.js +++ b/lib/info.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-vars */ const sax = require('sax'); const utils = require('./utils'); // Forces Node JS version of setTimeout for Electron based applications @@ -56,7 +57,8 @@ exports.getBasicInfo = async(id, options) => { Object.assign(info, { - formats: parseFormats(info.player_response), + // Replace with formats from iosPlayerResponse + // formats: parseFormats(info.player_response), related_videos: extras.getRelatedVideos(info), }); @@ -239,28 +241,32 @@ const parseFormats = player_response => { * @returns {Promise} */ exports.getInfo = async(id, options) => { - let info = await exports.getBasicInfo(id, options); + const info = await exports.getBasicInfo(id, options); + const iosPlayerResponse = await fetchIosJsonPlayer(id, options); + info.formats = parseFormats(iosPlayerResponse); const hasManifest = - info.player_response && info.player_response.streamingData && ( - info.player_response.streamingData.dashManifestUrl || - info.player_response.streamingData.hlsManifestUrl + iosPlayerResponse && iosPlayerResponse.streamingData && ( + iosPlayerResponse.streamingData.dashManifestUrl || + iosPlayerResponse.streamingData.hlsManifestUrl ); let funcs = []; if (info.formats.length) { - info.html5player = info.html5player || - getHTML5player(await getWatchHTMLPageBody(id, options)) || getHTML5player(await getEmbedPageBody(id, options)); - if (!info.html5player) { - throw Error('Unable to find html5player file'); - } - const html5player = new URL(info.html5player, BASE_URL).toString(); - funcs.push(sig.decipherFormats(info.formats, html5player, options)); + // Stream from ios player doesn't need to be deciphered. + // info.html5player = info.html5player || + // getHTML5player(await getWatchHTMLPageBody(id, options)) || getHTML5player(await getEmbedPageBody(id, options)); + // if (!info.html5player) { + // throw Error('Unable to find html5player file'); + // } + // const html5player = new URL(info.html5player, BASE_URL).toString(); + // funcs.push(sig.decipherFormats(info.formats, html5player, options)); + funcs.push(info.formats); } - if (hasManifest && info.player_response.streamingData.dashManifestUrl) { - let url = info.player_response.streamingData.dashManifestUrl; + if (hasManifest && iosPlayerResponse.streamingData.dashManifestUrl) { + let url = iosPlayerResponse.streamingData.dashManifestUrl; funcs.push(getDashManifest(url, options)); } - if (hasManifest && info.player_response.streamingData.hlsManifestUrl) { - let url = info.player_response.streamingData.hlsManifestUrl; + if (hasManifest && iosPlayerResponse.streamingData.hlsManifestUrl) { + let url = iosPlayerResponse.streamingData.hlsManifestUrl; funcs.push(getM3U8(url, options)); } @@ -268,11 +274,76 @@ exports.getInfo = async(id, options) => { info.formats = Object.values(Object.assign({}, ...results)); info.formats = info.formats.map(formatUtils.addFormatMeta); info.formats.sort(formatUtils.sortFormats); + + info.full = true; return info; }; +// TODO: Clean up this code to impliment Android player. +const IOS_CLIENT_VERSION = '19.28.1', + IOS_DEVICE_MODEL = 'iPhone16,2', + IOS_USER_AGENT_VERSION = '17_5_1', + IOS_OS_VERSION = '17.5.1.21F90'; + +const fetchIosJsonPlayer = async(videoId, options) => { + const cpn = utils.generateClientPlaybackNonce(16); + const payload = { + videoId, + cpn, + contentCheckOk: true, + racyCheckOk: true, + context: { + client: { + clientName: 'IOS', + clientVersion: IOS_CLIENT_VERSION, + deviceMake: 'Apple', + deviceModel: IOS_DEVICE_MODEL, + platform: 'MOBILE', + osName: 'iOS', + osVersion: IOS_OS_VERSION, + hl: 'en', + gl: 'US', + utcOffsetMinutes: -240, + }, + request: { + internalExperimentFlags: [], + useSsl: true, + }, + user: { + lockedSafetyMode: false, + }, + }, + }; + + const { jar, dispatcher } = options.agent; + const opts = { + requestOptions: { + method: 'POST', + dispatcher, + query: { + prettyPrint: false, + t: utils.generateClientPlaybackNonce(12), + id: videoId, + }, + headers: { + 'Content-Type': 'application/json', + cookie: jar.getCookieStringSync('https://www.youtube.com'), + 'User-Agent': `com.google.ios.youtube/${IOS_CLIENT_VERSION}(${ + IOS_DEVICE_MODEL + }; U; CPU iOS ${IOS_USER_AGENT_VERSION} like Mac OS X; en_US)`, + 'X-Goog-Api-Format-Version': '2', + }, + body: JSON.stringify(payload), + }, + }; + const response = await utils.request('https://youtubei.googleapis.com/youtubei/v1/player', opts); + if (videoId !== response.videoDetails.videoId) throw Error('Video ID mismatch'); + return response; +}; + + /** * Gets additional DASH formats. * diff --git a/lib/utils.js b/lib/utils.js index a382f78d..0bd852bc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,6 @@ const { request } = require('undici'); const { writeFileSync } = require('fs'); -const cookie = require('./cookie'); +const AGENT = require('./agent'); /** @@ -320,11 +320,11 @@ let oldCookieWarning = true; let oldDispatcherWarning = true; exports.applyDefaultAgent = options => { if (!options.agent) { - const { jar } = cookie.defaultAgent; + const { jar } = AGENT.defaultAgent; const c = exports.getPropInsensitive(options.requestOptions.headers, 'cookie'); if (c) { jar.removeAllCookiesSync(); - cookie.addCookiesFromString(jar, c); + AGENT.addCookiesFromString(jar, c); if (oldCookieWarning) { oldCookieWarning = false; console.warn( @@ -341,7 +341,7 @@ exports.applyDefaultAgent = options => { '(https://github.com/distubejs/ytdl-core#how-to-implement-ytdlagent-with-your-own-dispatcher)', ); } - options.agent = cookie.defaultAgent; + options.agent = AGENT.defaultAgent; } }; @@ -352,7 +352,7 @@ exports.applyOldLocalAddress = options => { !options.requestOptions.localAddress || options.requestOptions.localAddress === options.agent.localAddress ) return; - options.agent = cookie.createAgent(undefined, { localAddress: options.requestOptions.localAddress }); + options.agent = AGENT.createAgent(undefined, { localAddress: options.requestOptions.localAddress }); if (oldLocalAddressWarning) { oldLocalAddressWarning = false; console.warn( @@ -386,3 +386,8 @@ exports.applyDefaultHeaders = options => { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36', }, options.requestOptions.headers); }; + +exports.generateClientPlaybackNonce = length => { + const CPN_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + return Array.from({ length }, () => CPN_CHARS[Math.floor(Math.random() * CPN_CHARS.length)]).join(''); +};