mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-18 03:59:38 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9694a48270 | ||
|
|
edfd65f5e0 | ||
|
|
cbb2535b24 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [13.3.0](https://github.com/LuanRT/YouTube.js/compare/v13.2.0...v13.3.0) (2025-03-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **MusicImmersiveHeader:** Parse buttons and menu ([cbb2535](https://github.com/LuanRT/YouTube.js/commit/cbb2535b2492777b0045be5fcf9bece03fe4f84e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Player:** Parse global variable used by nsig/sig ([#935](https://github.com/LuanRT/YouTube.js/issues/935)) ([edfd65f](https://github.com/LuanRT/YouTube.js/commit/edfd65f5e08a9155b8c31d8127a4e309313b39de))
|
||||
|
||||
## [13.2.0](https://github.com/LuanRT/YouTube.js/compare/v13.1.0...v13.2.0) (2025-03-20)
|
||||
|
||||
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.2.0",
|
||||
"version": "13.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "13.2.0",
|
||||
"version": "13.3.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"jintr": "^3.2.1",
|
||||
"jintr": "^3.3.0",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
@@ -6155,9 +6155,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.1.tgz",
|
||||
"integrity": "sha512-yjKUBuwTTg4nc4izMysxuIk0BKh45hnbc1KnXE6LxagIGZn5od+I2elpuRY9IIm3EiKiUZxhxV89a0iX+xoEZg==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.0.tgz",
|
||||
"integrity": "sha512-ZsaajJ4Hr5XR0tSPhOZOTjFhxA0qscKNSOs41NRjx7ZOGwpfdp8NKIBEUtvUPbA37JXyv1sJlgeOOZHjr3h76Q==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.2.0",
|
||||
"version": "13.3.0",
|
||||
"description": "A JavaScript client for YouTube's private API, known as InnerTube.",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
@@ -103,7 +103,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"jintr": "^3.2.1",
|
||||
"jintr": "^3.3.0",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { Log, LZW, Constants } from '../utils/index.js';
|
||||
import { Platform, getRandomUserAgent, getStringBetweenStrings, findFunction, PlayerError } from '../utils/Utils.js';
|
||||
import type { ICache, FetchFunction } from '../types/index.js';
|
||||
import { Jinter } from 'jintr';
|
||||
import type { FetchFunction, ICache } from '../types/index.js';
|
||||
import { Constants, Log, LZW } from '../utils/index.js';
|
||||
import {
|
||||
type ASTLookupResult,
|
||||
findFunction,
|
||||
findVariable,
|
||||
getRandomUserAgent,
|
||||
getStringBetweenStrings,
|
||||
Platform,
|
||||
PlayerError
|
||||
} from '../utils/Utils.js';
|
||||
|
||||
const TAG = 'Player';
|
||||
|
||||
@@ -63,9 +72,12 @@ export default class Player {
|
||||
|
||||
const player_js = await player_res.text();
|
||||
|
||||
const ast = Jinter.parseScript(player_js, { ecmaVersion: 'latest', ranges: true });
|
||||
|
||||
const sig_timestamp = this.extractSigTimestamp(player_js);
|
||||
const sig_sc = this.extractSigSourceCode(player_js);
|
||||
const nsig_sc = this.extractNSigSourceCode(player_js);
|
||||
const global_variable = this.extractGlobalVariable(player_js, ast);
|
||||
const sig_sc = this.extractSigSourceCode(player_js, global_variable);
|
||||
const nsig_sc = this.extractNSigSourceCode(player_js, ast, global_variable);
|
||||
|
||||
Log.info(TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`);
|
||||
|
||||
@@ -223,8 +235,27 @@ export default class Player {
|
||||
return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0');
|
||||
}
|
||||
|
||||
static extractSigSourceCode(data: string): string | undefined {
|
||||
const match = data.match(/function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\(""\)(.+?)\.join\(""\))\}/);
|
||||
static extractGlobalVariable(data: string, ast: ReturnType<typeof Jinter.parseScript>): ASTLookupResult | undefined {
|
||||
let variable = findVariable(data, { includes: '-_w8_', ast });
|
||||
|
||||
// For redundancy/the above fails:
|
||||
if (!variable)
|
||||
variable = findVariable(data, { includes: 'Untrusted URL{', ast });
|
||||
|
||||
if (!variable)
|
||||
variable = findVariable(data, { includes: '1969', ast });
|
||||
|
||||
if (!variable)
|
||||
variable = findVariable(data, { includes: '1970', ast });
|
||||
|
||||
if (!variable)
|
||||
variable = findVariable(data, { includes: 'playerfallback', ast });
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
static extractSigSourceCode(data: string, global_variable?: ASTLookupResult): string | undefined {
|
||||
const match = data.match(/function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\((?:[^)]+)\)(.+?)\.join\((?:[^)]+)\))\}/);
|
||||
|
||||
if (!match) {
|
||||
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
|
||||
@@ -232,30 +263,45 @@ export default class Player {
|
||||
}
|
||||
|
||||
const var_name = match[1];
|
||||
|
||||
const obj_name = match[3].split(/\.|\[/)[0]?.replace(';', '').trim();
|
||||
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
|
||||
|
||||
if (!functions || !var_name)
|
||||
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
|
||||
|
||||
return `function descramble_sig(${var_name}) { let ${obj_name}={${functions}}; ${match[2]} } descramble_sig(sig);`;
|
||||
return `${global_variable?.result || ''} function descramble_sig(${var_name}) { let ${obj_name}={${functions}}; ${match[2]} } descramble_sig(sig);`;
|
||||
}
|
||||
|
||||
static extractNSigSourceCode(data: string): string | undefined {
|
||||
// This used to be the prefix of the error tag (leaving it here for reference).
|
||||
let nsig_function = findFunction(data, { includes: 'enhanced_except' });
|
||||
|
||||
static extractNSigSourceCode(data: string, ast?: ReturnType<typeof Jinter.parseScript>, global_variable?: ASTLookupResult): string | undefined {
|
||||
let nsig_function;
|
||||
|
||||
if (global_variable) {
|
||||
nsig_function = findFunction(data, { includes: `new Date(${global_variable.name}`, ast });
|
||||
|
||||
// For redundancy/the above fails:
|
||||
if (!nsig_function)
|
||||
nsig_function = findFunction(data, { includes: '.push(String.fromCharCode(', ast });
|
||||
|
||||
if (!nsig_function)
|
||||
nsig_function = findFunction(data, { includes: '.reverse().forEach(function', ast });
|
||||
|
||||
if (nsig_function)
|
||||
return `${global_variable.result} var ${nsig_function.result} ${nsig_function.name}(nsig);`;
|
||||
}
|
||||
|
||||
// This is the suffix of the error tag.
|
||||
if (!nsig_function)
|
||||
nsig_function = findFunction(data, { includes: '-_w8_' });
|
||||
nsig_function = findFunction(data, { includes: '-_w8_', ast });
|
||||
|
||||
// Usually, only this function uses these dates in the entire script.
|
||||
if (!nsig_function)
|
||||
nsig_function = findFunction(data, { includes: '1969' });
|
||||
nsig_function = findFunction(data, { includes: '1969', ast });
|
||||
|
||||
// This used to be the prefix of the error tag (leaving it here for reference).
|
||||
if (!nsig_function)
|
||||
nsig_function = findFunction(data, { includes: 'enhanced_except', ast });
|
||||
|
||||
if (nsig_function)
|
||||
return `${nsig_function.result} ${nsig_function.name}(nsig);`;
|
||||
return `let ${nsig_function.result} ${nsig_function.name}(nsig);`;
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
@@ -263,6 +309,6 @@ export default class Player {
|
||||
}
|
||||
|
||||
static get LIBRARY_VERSION(): number {
|
||||
return 13;
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,41 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
|
||||
import Button from './Button.js';
|
||||
import MusicThumbnail from './MusicThumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import SubscribeButton from './SubscribeButton.js';
|
||||
import ToggleButton from './ToggleButton.js';
|
||||
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class MusicImmersiveHeader extends YTNode {
|
||||
static type = 'MusicImmersiveHeader';
|
||||
|
||||
title: Text;
|
||||
description: Text;
|
||||
thumbnail: MusicThumbnail | null;
|
||||
public title: Text;
|
||||
public menu: Menu | null;
|
||||
public more_button: ToggleButton | null;
|
||||
public play_button: Button | null;
|
||||
public share_endpoint?: NavigationEndpoint;
|
||||
public start_radio_button: Button | null;
|
||||
public subscription_button: SubscribeButton | null;
|
||||
public description: Text;
|
||||
public thumbnail: MusicThumbnail | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.more_button = Parser.parseItem(data.moreButton, ToggleButton);
|
||||
this.play_button = Parser.parseItem(data.playButton, Button);
|
||||
|
||||
if ('shareEndpoint' in data)
|
||||
this.share_endpoint = new NavigationEndpoint(data.shareEndpoint);
|
||||
|
||||
this.start_radio_button = Parser.parseItem(data.startRadioButton, Button);
|
||||
this.subscription_button = Parser.parseItem(data.subscriptionButton, SubscribeButton);
|
||||
this.description = new Text(data.description);
|
||||
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
|
||||
/**
|
||||
Not useful for now.
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.play_button = Parser.parse(data.playButton);
|
||||
this.start_radio_button = Parser.parse(data.startRadioButton);
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Memo } from '../parser/helpers.js';
|
||||
import { Text } from '../parser/misc.js';
|
||||
import * as Log from './Log.js';
|
||||
import userAgents from './user-agents.js';
|
||||
import type { Node } from 'estree';
|
||||
import { Jinter } from 'jintr';
|
||||
|
||||
import type { EmojiRun, TextRun } from '../parser/misc.js';
|
||||
import type { FetchFunction } from '../types/index.js';
|
||||
import type PlatformShim from '../types/PlatformShim.js';
|
||||
import type { Node } from 'estree';
|
||||
|
||||
import { Memo } from '../parser/helpers.js';
|
||||
import { Text } from '../parser/misc.js';
|
||||
import * as Log from './Log.js';
|
||||
import userAgents from './user-agents.js';
|
||||
|
||||
const TAG_ = 'Utils';
|
||||
|
||||
@@ -17,6 +18,7 @@ export class Platform {
|
||||
static load(platform: PlatformShim): void {
|
||||
shim = platform;
|
||||
}
|
||||
|
||||
static get shim(): PlatformShim {
|
||||
if (!shim) {
|
||||
throw new Error('Platform is not loaded');
|
||||
@@ -24,6 +26,7 @@ export class Platform {
|
||||
return shim;
|
||||
}
|
||||
}
|
||||
|
||||
export class InnertubeError extends Error {
|
||||
date: Date;
|
||||
version: string;
|
||||
@@ -41,12 +44,23 @@ export class InnertubeError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class ParsingError extends InnertubeError { }
|
||||
export class MissingParamError extends InnertubeError { }
|
||||
export class OAuth2Error extends InnertubeError { }
|
||||
export class PlayerError extends Error { }
|
||||
export class SessionError extends Error { }
|
||||
export class ChannelError extends Error { }
|
||||
export class ParsingError extends InnertubeError {
|
||||
}
|
||||
|
||||
export class MissingParamError extends InnertubeError {
|
||||
}
|
||||
|
||||
export class OAuth2Error extends InnertubeError {
|
||||
}
|
||||
|
||||
export class PlayerError extends Error {
|
||||
}
|
||||
|
||||
export class SessionError extends Error {
|
||||
}
|
||||
|
||||
export class ChannelError extends Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares given objects. May not work correctly for
|
||||
@@ -251,7 +265,7 @@ export function getCookie(cookies: string, name: string, matchWholeName = false)
|
||||
return match ? match[2] : undefined;
|
||||
}
|
||||
|
||||
export type FindFunctionArgs = {
|
||||
export type ASTLookupArgs = {
|
||||
/**
|
||||
* The name of the function.
|
||||
*/
|
||||
@@ -266,9 +280,14 @@ export type FindFunctionArgs = {
|
||||
* A regular expression that the function's code must match.
|
||||
*/
|
||||
regexp?: RegExp;
|
||||
|
||||
/**
|
||||
* The abstract syntax tree of the source code.
|
||||
*/
|
||||
ast?: ReturnType<typeof Jinter.parseScript>;
|
||||
};
|
||||
|
||||
export type FindFunctionResult = {
|
||||
export type ASTLookupResult = {
|
||||
start: number;
|
||||
end: number;
|
||||
name: string;
|
||||
@@ -277,7 +296,7 @@ export type FindFunctionResult = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a function in a source string based on the provided search criteria.
|
||||
* Searches for a function in the given code based on specified criteria.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
@@ -286,12 +305,14 @@ export type FindFunctionResult = {
|
||||
* console.log(result);
|
||||
* // Output: { start: 69, end: 110, name: 'bar', node: { ... }, result: 'bar = function() { console.log("bar"); };' }
|
||||
* ```
|
||||
*
|
||||
* @returns An object containing the function's details if found, `undefined` otherwise.
|
||||
*/
|
||||
export function findFunction(source: string, args: FindFunctionArgs): FindFunctionResult | undefined {
|
||||
const { name, includes, regexp } = args;
|
||||
export function findFunction(source: string, args: ASTLookupArgs): ASTLookupResult | undefined {
|
||||
const { name, includes, regexp, ast } = args;
|
||||
|
||||
const node = Jinter.parseScript(source);
|
||||
const stack = [ node ] as (Node & { start: number; end: number})[];
|
||||
const node = ast ? ast : Jinter.parseScript(source);
|
||||
const stack = [ node ] as (Node & { start: number; end: number })[];
|
||||
|
||||
for (let i = 0; i < stack.length; i++) {
|
||||
const current = stack[i];
|
||||
@@ -307,7 +328,7 @@ export function findFunction(source: string, args: FindFunctionArgs): FindFuncti
|
||||
|
||||
if (
|
||||
(name && current.expression.left.name === name) ||
|
||||
(includes && code.indexOf(includes) > -1) ||
|
||||
(includes && code.includes(includes)) ||
|
||||
(regexp && regexp.test(code))
|
||||
) {
|
||||
return {
|
||||
@@ -329,4 +350,76 @@ export function findFunction(source: string, args: FindFunctionArgs): FindFuncti
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a variable declaration in the given code based on specified criteria.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Find a variable by name
|
||||
* const code = 'const x = 5; let y = "hello";';
|
||||
* const a = findVariable(code, { name: 'y' });
|
||||
* console.log(a?.result);
|
||||
*
|
||||
* // Find a variable containing specific text
|
||||
* const b = findVariable(code, { includes: 'hello' });
|
||||
* console.log(b?.result);
|
||||
*
|
||||
* // Find a variable matching a pattern
|
||||
* const c = findVariable(code, { regexp: /y\s*=\s*"hello"/ });
|
||||
* console.log(c?.result);
|
||||
* ```
|
||||
*
|
||||
* @returns An object containing the variable's details if found, `undefined` otherwise.
|
||||
*/
|
||||
export function findVariable(code: string, options: ASTLookupArgs): ASTLookupResult | undefined {
|
||||
const ast = options.ast ? options.ast : Jinter.parseScript(code, { ecmaVersion: 'latest', ranges: true });
|
||||
|
||||
let found: ASTLookupResult | undefined;
|
||||
|
||||
function walk(node: Node): void {
|
||||
if (found) return;
|
||||
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
const [ start, end ] = node.range!;
|
||||
const node_source = code.slice(start, end);
|
||||
|
||||
for (const declarator of node.declarations) {
|
||||
if (declarator.id.type === 'Identifier') {
|
||||
const var_name = declarator.id.name;
|
||||
if (options.name && var_name === options.name) {
|
||||
found = { start, end, name: var_name, node, result: node_source };
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
(options.includes && node_source.includes(options.includes)) ||
|
||||
(options.regexp && options.regexp.test(node_source))) {
|
||||
found = { start, end, name: (node.declarations?.[0]?.id as any)?.name, node, result: node_source };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in node) {
|
||||
if (Object.prototype.hasOwnProperty.call(node, key)) {
|
||||
const child = node[key as keyof typeof node] as any;
|
||||
if (Array.isArray(child)) {
|
||||
for (const c of child) {
|
||||
if (c && typeof c.type === 'string') {
|
||||
walk(c);
|
||||
if (found) return;
|
||||
}
|
||||
}
|
||||
} else if (child && typeof child.type === 'string') {
|
||||
walk(child);
|
||||
if (found) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(ast);
|
||||
return found;
|
||||
}
|
||||
Reference in New Issue
Block a user