mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-27 00:29:16 +00:00
hotfix: use Android client when requesting initial video info
This commit is contained in:
@@ -70,12 +70,16 @@ class Innertube {
|
||||
/**
|
||||
* Retrieves video info.
|
||||
*/
|
||||
async getInfo(video_id: string) {
|
||||
async getInfo(video_id: string | undefined) {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.actions.getVideoInfo(video_id, cpn);
|
||||
const initial_info = await this.actions.execute('/player', {
|
||||
client: 'ANDROID',
|
||||
videoId: 'mFWf4Tb5m6Y'
|
||||
});
|
||||
|
||||
const continuation = this.actions.next({ video_id });
|
||||
|
||||
const response = await Promise.all([ initial_info, continuation ]);
|
||||
@@ -85,11 +89,15 @@ class Innertube {
|
||||
/**
|
||||
* Retrieves basic video info.
|
||||
*/
|
||||
async getBasicInfo(video_id: string) {
|
||||
async getBasicInfo(video_id: string | undefined) {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
const response = await this.actions.getVideoInfo(video_id, cpn);
|
||||
|
||||
const response = await this.actions.execute('/player', {
|
||||
client: 'ANDROID',
|
||||
videoId: 'mFWf4Tb5m6Y'
|
||||
});
|
||||
|
||||
return new VideoInfo([ response ], this.actions, this.session.player, cpn);
|
||||
}
|
||||
@@ -238,11 +246,11 @@ class Innertube {
|
||||
*
|
||||
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
|
||||
*/
|
||||
async download(video_id: string, options?: DownloadOptions) {
|
||||
async download(video_id: string | undefined, options?: DownloadOptions) {
|
||||
throwIfMissing({ video_id });
|
||||
const info = await this.getBasicInfo(video_id);
|
||||
return info.download(options);
|
||||
}
|
||||
}
|
||||
|
||||
export default Innertube;
|
||||
export default Innertube;
|
||||
@@ -4,19 +4,23 @@ import { getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../uti
|
||||
import Constants from '../utils/Constants';
|
||||
import UniversalCache from '../utils/Cache';
|
||||
|
||||
import NToken from '../deciphers/NToken';
|
||||
import Signature from '../deciphers/Signature';
|
||||
|
||||
// Import NToken from '../deciphers/NToken';
|
||||
// Import Signature from '../deciphers/Signature';
|
||||
|
||||
export default class Player {
|
||||
#ntoken;
|
||||
#signature;
|
||||
// #ntoken;
|
||||
// #signature;
|
||||
|
||||
#signature_timestamp;
|
||||
#player_id;
|
||||
|
||||
constructor(signature: Signature, ntoken: NToken, signature_timestamp: number, player_id: string) {
|
||||
this.#ntoken = ntoken;
|
||||
this.#signature = signature;
|
||||
constructor(signature_timestamp: number, player_id: string) {
|
||||
// This.#ntoken = ntoken;
|
||||
// This.#signature = signature;
|
||||
|
||||
this.#signature_timestamp = signature_timestamp;
|
||||
|
||||
this.#player_id = player_id;
|
||||
}
|
||||
|
||||
@@ -56,13 +60,15 @@ export default class Player {
|
||||
const player_js = await player_res.text();
|
||||
|
||||
const sig_timestamp = this.extractSigTimestamp(player_js);
|
||||
const sig_decipher_sc = this.extractSigDecipherSc(player_js);
|
||||
const ntoken_sc = this.extractNTokenSc(player_js);
|
||||
|
||||
return await Player.fromSource(cache, sig_timestamp, sig_decipher_sc, ntoken_sc, player_id);
|
||||
// Const sig_decipher_sc = this.extractSigDecipherSc(player_js);
|
||||
// Const ntoken_sc = this.extractNTokenSc(player_js);
|
||||
|
||||
return await Player.fromSource(cache, sig_timestamp, player_id);
|
||||
}
|
||||
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string) {
|
||||
/*
|
||||
Decipher(url?: string, signature_cipher?: string, cipher?: string) {
|
||||
url = url || signature_cipher || cipher;
|
||||
|
||||
if (!url)
|
||||
@@ -90,7 +96,7 @@ export default class Player {
|
||||
}
|
||||
|
||||
return url_components.toString();
|
||||
}
|
||||
}*/
|
||||
|
||||
static async fromCache(cache: UniversalCache, player_id: string) {
|
||||
const buffer = await cache.get(player_id);
|
||||
@@ -105,15 +111,18 @@ export default class Player {
|
||||
return null;
|
||||
|
||||
const sig_timestamp = view.getUint32(4, true);
|
||||
const sig_decipher_len = view.getUint32(8, true);
|
||||
const sig_decipher_buf = buffer.slice(12, 12 + sig_decipher_len);
|
||||
const ntoken_transform_buf = buffer.slice(12 + sig_decipher_len);
|
||||
|
||||
return new Player(Signature.fromArrayBuffer(sig_decipher_buf), NToken.fromArrayBuffer(ntoken_transform_buf), sig_timestamp, player_id);
|
||||
/*
|
||||
* Const sig_decipher_len = view.getUint32(8, true);
|
||||
* const sig_decipher_buf = buffer.slice(12, 12 + sig_decipher_len);
|
||||
* const ntoken_transform_buf = buffer.slice(12 + sig_decipher_len);
|
||||
*/
|
||||
|
||||
return new Player(sig_timestamp, player_id);
|
||||
}
|
||||
|
||||
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_decipher_sc: string, ntoken_sc: string, player_id: string) {
|
||||
const player = new Player(Signature.fromSourceCode(sig_decipher_sc), NToken.fromSourceCode(ntoken_sc), sig_timestamp, player_id);
|
||||
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, player_id: string) {
|
||||
const player = new Player(sig_timestamp, player_id);
|
||||
await player.cache(cache);
|
||||
return player;
|
||||
}
|
||||
@@ -121,17 +130,21 @@ export default class Player {
|
||||
async cache(cache?: UniversalCache) {
|
||||
if (!cache) return;
|
||||
|
||||
const ntoken_buf = this.#ntoken.toArrayBuffer();
|
||||
const sig_decipher_buf = this.#signature.toArrayBuffer();
|
||||
const buffer = new ArrayBuffer(12 + sig_decipher_buf.byteLength + ntoken_buf.byteLength);
|
||||
/**
|
||||
* Const ntoken_buf = this.#ntoken.toArrayBuffer();
|
||||
* const sig_decipher_buf = this.#signature.toArrayBuffer();
|
||||
*/
|
||||
|
||||
const buffer = new ArrayBuffer(12 /* + sig_decipher_buf.byteLength + ntoken_buf.byteLength */);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
view.setUint32(0, Player.LIBRARY_VERSION, true);
|
||||
view.setUint32(4, this.#signature_timestamp, true);
|
||||
view.setUint32(8, sig_decipher_buf.byteLength, true);
|
||||
|
||||
new Uint8Array(buffer).set(new Uint8Array(sig_decipher_buf), 12);
|
||||
new Uint8Array(buffer).set(new Uint8Array(ntoken_buf), 12 + sig_decipher_buf.byteLength);
|
||||
// View.setUint32(8, sig_decipher_buf.byteLength, true);
|
||||
|
||||
// New Uint8Array(buffer).set(new Uint8Array(sig_decipher_buf), 12);
|
||||
// New Uint8Array(buffer).set(new Uint8Array(ntoken_buf), 12 + sig_decipher_buf.byteLength);
|
||||
|
||||
await cache.set(this.#player_id, new Uint8Array(buffer));
|
||||
}
|
||||
@@ -162,6 +175,7 @@ export default class Player {
|
||||
static extractNTokenSc(data: string) {
|
||||
const sc = `var b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join("");`;
|
||||
|
||||
console.log(sc);
|
||||
if (!sc)
|
||||
throw new PlayerError('Failed to extract n-token decipher algorithm');
|
||||
|
||||
|
||||
@@ -36,12 +36,11 @@ class Format {
|
||||
|
||||
/**
|
||||
* Decipher the streaming url of the format.
|
||||
*
|
||||
* @param {import('../../../core/Player').default} player
|
||||
* @returns {string} Deciphered URL for downloading
|
||||
*/
|
||||
decipher(player) {
|
||||
return player.decipher(this.url, this.signature_cipher, this.cipher);
|
||||
decipher() {
|
||||
return this.url;
|
||||
// Return player.decipher(this.url, this.signature_cipher, this.cipher);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import Player from '../../core/Player';
|
||||
import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults';
|
||||
import VideoPrimaryInfo from '../classes/VideoPrimaryInfo';
|
||||
import VideoSecondaryInfo from '../classes/VideoSecondaryInfo';
|
||||
import PlayerMicroformat from '../classes/PlayerMicroformat';
|
||||
import Format from '../classes/misc/Format';
|
||||
|
||||
import MerchandiseShelf from '../classes/MerchandiseShelf';
|
||||
@@ -96,22 +95,8 @@ class VideoInfo {
|
||||
if (info.playability_status?.status === 'ERROR')
|
||||
throw new InnertubeError('This video is unavailable', info.playability_status);
|
||||
|
||||
if (!info.microformat?.is(PlayerMicroformat))
|
||||
throw new InnertubeError('Invalid microformat', info.microformat);
|
||||
|
||||
this.basic_info = { // This type is inferred so no need for an explicit type
|
||||
...info.video_details,
|
||||
...{
|
||||
/**
|
||||
* Microformat is a bit redundant, so only
|
||||
* a few things there are interesting to us.
|
||||
*/
|
||||
embed: info.microformat?.embed,
|
||||
channel: info.microformat?.channel,
|
||||
is_unlisted: info.microformat?.is_unlisted,
|
||||
is_family_safe: info.microformat?.is_family_safe,
|
||||
has_ypc_metadata: info.microformat?.has_ypc_metadata
|
||||
},
|
||||
like_count: undefined as number | undefined,
|
||||
is_liked: undefined as boolean | undefined,
|
||||
is_disliked: undefined as boolean | undefined
|
||||
@@ -438,7 +423,7 @@ class VideoInfo {
|
||||
if (!format.index_range || !format.init_range)
|
||||
throw new InnertubeError('Index and init ranges not available', { format });
|
||||
|
||||
const url = new URL(format.decipher(this.#player));
|
||||
const url = new URL(format.decipher());
|
||||
url.searchParams.set('cpn', this.#cpn);
|
||||
|
||||
set.appendChild(this.#el(document, 'Representation', {
|
||||
|
||||
@@ -83,24 +83,6 @@ describe('YouTube.js Tests', () => {
|
||||
expect(result).toBeTruthy();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
/*
|
||||
// TODO: fix this, doesn't run on node 12
|
||||
const { default: NToken } = require('../../src/deciphers/NToken');
|
||||
const { default: Signature} = require('../../src/deciphers/Signature');
|
||||
|
||||
describe('Deciphers', () => {
|
||||
it('Should decipher signature', () => {
|
||||
const result = Signature.fromSourceCode(Constants.DECIPHERS.SIG.ALGORITHM).decipher(Constants.DECIPHERS.SIG.ORIGINAL_URL);
|
||||
expect(result).toEqual(Constants.DECIPHERS.SIG.DECIPHERED_URL);
|
||||
});
|
||||
|
||||
it('Should decipher ntoken', () => {
|
||||
const result = NToken.fromSourceCode(Constants.DECIPHERS.N.ALGORITHM).transform(Constants.DECIPHERS.N.ORIGINAL_TOKEN);
|
||||
expect(result).toEqual(Constants.DECIPHERS.N.DECIPHERED_TOKEN);
|
||||
});
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
async function download(id, session) {
|
||||
|
||||
Reference in New Issue
Block a user