mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 01:52:11 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a10c6a1c23 | ||
|
|
b4981ca1fd | ||
|
|
7f1eeb6b5b | ||
|
|
5e665e8f0b | ||
|
|
ff1aa67b8b | ||
|
|
dce51cdc4a | ||
|
|
4808d2e13a | ||
|
|
98a2ba8c46 | ||
|
|
a1fd51fbb5 | ||
|
|
c7048368cc | ||
|
|
458c4900d7 | ||
|
|
f8690118c3 | ||
|
|
14ea875c67 | ||
|
|
95d9211eae | ||
|
|
393c243495 | ||
|
|
df6b0cccc4 | ||
|
|
a95f52a477 | ||
|
|
4284fa6d06 | ||
|
|
d8f731b2fa | ||
|
|
9694a48270 | ||
|
|
edfd65f5e0 | ||
|
|
cbb2535b24 |
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## [13.4.0](https://github.com/LuanRT/YouTube.js/compare/v13.3.0...v13.4.0) (2025-04-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **MultiPageMenuNotificationSection:** Add `notification_section_title` ([f869011](https://github.com/LuanRT/YouTube.js/commit/f8690118c3e4edbc22a6b9f59477a8e40e39d9a3))
|
||||
* **Parser:** Add `OpenOnePickAddVideoModalCommand` node ([#901](https://github.com/LuanRT/YouTube.js/issues/901)) ([ff1aa67](https://github.com/LuanRT/YouTube.js/commit/ff1aa67b8bebd8e038ea77a124d0be0ed420bc55))
|
||||
* **parser:** Parse `targetId` ([458c490](https://github.com/LuanRT/YouTube.js/commit/458c4900d78f9b16761faae421bcd8cb302f19b8))
|
||||
* **RichRenderers:** Parse more UI elements ([d8f731b](https://github.com/LuanRT/YouTube.js/commit/d8f731b2fa4b755324b0ef4ad68be45f735b29a9))
|
||||
* **RichShelf:** Add `icon_type` property ([dce51cd](https://github.com/LuanRT/YouTube.js/commit/dce51cdc4abad21812ee9e7ba02885dffde35d3a))
|
||||
* **Session:** Add `deviceExperimentId` and `rolloutToken` to session data ([c704836](https://github.com/LuanRT/YouTube.js/commit/c7048368cc38a6a322c8006a4cfd208235d27caf))
|
||||
* **Session:** Add `retrieve_innertube_config` option ([#949](https://github.com/LuanRT/YouTube.js/issues/949)) ([4808d2e](https://github.com/LuanRT/YouTube.js/commit/4808d2e13a90710cdea2c72332deb7809d5fdbdd))
|
||||
* **Session:** Add option to override Player ID ([#951](https://github.com/LuanRT/YouTube.js/issues/951)) ([5e665e8](https://github.com/LuanRT/YouTube.js/commit/5e665e8f0ba6e68d689b42be852ec20597c6b6a6))
|
||||
* **Text:** Parse accessibility data ([a95f52a](https://github.com/LuanRT/YouTube.js/commit/a95f52a4777ea158f3d3e85f4213aee364a2d38d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **HTTPClient:** Use the correct constant for the iOS client OS name ([#938](https://github.com/LuanRT/YouTube.js/issues/938)) ([95d9211](https://github.com/LuanRT/YouTube.js/commit/95d9211eae51c2d47468cacbed8cd9bdaa2fcd33))
|
||||
* **Player:** Use global var to find signature algorithm ([#953](https://github.com/LuanRT/YouTube.js/issues/953)) ([7f1eeb6](https://github.com/LuanRT/YouTube.js/commit/7f1eeb6b5bbae72d455609e8fd972ad936a69e27))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
124
dev-scripts/enum-optimising-transformer.cjs
Normal file
124
dev-scripts/enum-optimising-transformer.cjs
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @param {import('typescript').Program} program
|
||||
* @param {import('ts-patch').PluginConfig} pluginConfig
|
||||
* @param {import('ts-patch').TransformerExtras} extras
|
||||
*/
|
||||
module.exports = (program, pluginConfig, { ts: tsInstance }) => {
|
||||
/** @param {import('typescript').TransformationContext} context */
|
||||
return (context) => {
|
||||
const { factory } = context
|
||||
|
||||
/** @param {import('typescript').SourceFile} sourceFile */
|
||||
return (sourceFile) => {
|
||||
/**
|
||||
* @param {import('typescript').Node} node
|
||||
* @returns {import('typescript').Node}
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
if (
|
||||
tsInstance.isEnumDeclaration(node) &&
|
||||
(!node.modifiers || node.modifiers.every(modifier => modifier.kind !== tsInstance.SyntaxKind.DeclareKeyword))
|
||||
) {
|
||||
let variableStatementModifiers
|
||||
|
||||
if (node.modifiers?.some(modifier => modifier.kind === tsInstance.SyntaxKind.ExportKeyword)) {
|
||||
variableStatementModifiers = [
|
||||
factory.createModifier(tsInstance.SyntaxKind.ExportKeyword)
|
||||
]
|
||||
}
|
||||
|
||||
const properties = []
|
||||
let currentValue = 0
|
||||
|
||||
for (const member of node.members) {
|
||||
const name = member.name.text
|
||||
let value
|
||||
let isNumeric = true
|
||||
let hasMinus = false
|
||||
|
||||
if (!member.initializer) {
|
||||
value = currentValue.toString()
|
||||
currentValue++
|
||||
} else if (tsInstance.isStringLiteral(member.initializer)) {
|
||||
value = member.initializer.text
|
||||
isNumeric = false
|
||||
} else if (tsInstance.isNumericLiteral(member.initializer)) {
|
||||
value = member.initializer.text
|
||||
currentValue = Number(value) + 1
|
||||
} else if (
|
||||
tsInstance.isPrefixUnaryExpression(member.initializer) &&
|
||||
member.initializer.operator === tsInstance.SyntaxKind.MinusToken
|
||||
) {
|
||||
value = member.initializer.operand.text
|
||||
hasMinus = true
|
||||
currentValue = (-1 * Number(value)) + 1
|
||||
} else {
|
||||
console.warn(`Unsupported enum member initializer "${tsInstance.SyntaxKind[member.initializer.kind]}" in "${node.name.text}", using original enum output.`)
|
||||
return tsInstance.visitEachChild(node, visitor, context);
|
||||
}
|
||||
|
||||
if (isNumeric) {
|
||||
if (hasMinus) {
|
||||
properties.push(
|
||||
factory.createPropertyAssignment(
|
||||
name,
|
||||
factory.createPrefixUnaryExpression(
|
||||
tsInstance.SyntaxKind.MinusToken,
|
||||
factory.createNumericLiteral(value)
|
||||
)
|
||||
),
|
||||
factory.createPropertyAssignment(
|
||||
factory.createStringLiteral(`-${value}`),
|
||||
factory.createStringLiteral(name)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
properties.push(
|
||||
factory.createPropertyAssignment(
|
||||
name,
|
||||
factory.createNumericLiteral(value)
|
||||
),
|
||||
factory.createPropertyAssignment(
|
||||
value,
|
||||
factory.createStringLiteral(name)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
properties.push(
|
||||
factory.createPropertyAssignment(
|
||||
name,
|
||||
factory.createStringLiteral(value)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const convertedEnum = factory.createVariableStatement(
|
||||
variableStatementModifiers,
|
||||
factory.createVariableDeclarationList(
|
||||
[
|
||||
factory.createVariableDeclaration(
|
||||
factory.createIdentifier(node.name.text),
|
||||
undefined,
|
||||
undefined,
|
||||
factory.createObjectLiteralExpression(
|
||||
properties,
|
||||
true
|
||||
)
|
||||
)
|
||||
],
|
||||
tsInstance.NodeFlags.Const
|
||||
)
|
||||
)
|
||||
|
||||
return convertedEnum
|
||||
}
|
||||
|
||||
return tsInstance.visitEachChild(node, visitor, context);
|
||||
};
|
||||
|
||||
return tsInstance.visitNode(sourceFile, visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
examples/browser/web/package-lock.json
generated
8
examples/browser/web/package-lock.json
generated
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"patch-package": "^6.5.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^5.4.12"
|
||||
"vite": "^5.4.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
@@ -1412,9 +1412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
|
||||
"integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
|
||||
"version": "5.4.18",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
|
||||
"integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"devDependencies": {
|
||||
"patch-package": "^6.5.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^5.4.12"
|
||||
"vite": "^5.4.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"bgutils-js": "^3.1.2",
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.2.0",
|
||||
"version": "13.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "13.2.0",
|
||||
"version": "13.4.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"jintr": "^3.2.1",
|
||||
"jintr": "^3.3.1",
|
||||
"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.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.1.tgz",
|
||||
"integrity": "sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.2.0",
|
||||
"version": "13.4.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.1",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
|
||||
@@ -50,18 +50,6 @@ export default class Actions {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mimics the Axios API using Fetch's Response object.
|
||||
* @param response - The response object.
|
||||
*/
|
||||
async #wrap(response: Response): Promise<ApiResponse> {
|
||||
return {
|
||||
success: response.ok,
|
||||
status_code: response.status,
|
||||
data: JSON.parse(await response.text())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes calls to the playback tracking API.
|
||||
* @param url - The URL to call.
|
||||
@@ -187,7 +175,12 @@ export default class Actions {
|
||||
return parsed_response;
|
||||
}
|
||||
|
||||
return this.#wrap(response);
|
||||
// Mimics the Axios API using Fetch's Response object.
|
||||
return {
|
||||
success: response.ok,
|
||||
status_code: response.status,
|
||||
data: JSON.parse(await response.text())
|
||||
};
|
||||
}
|
||||
|
||||
#isBrowse(response: IParsedResponse): response is IBrowseResponse {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -21,18 +30,21 @@ export default class Player {
|
||||
this.sig_sc = sig_sc;
|
||||
}
|
||||
|
||||
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch, po_token?: string): Promise<Player> {
|
||||
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
|
||||
const res = await fetch(url);
|
||||
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch, po_token?: string, player_id?: string): Promise<Player> {
|
||||
|
||||
if (!res.ok)
|
||||
throw new PlayerError(`Failed to get player id: ${res.status} (${res.statusText})`);
|
||||
if (!player_id) {
|
||||
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
|
||||
const res = await fetch(url);
|
||||
|
||||
const js = await res.text();
|
||||
if (!res.ok)
|
||||
throw new PlayerError(`Failed to get player id: ${res.status} (${res.statusText})`);
|
||||
|
||||
const player_id = getStringBetweenStrings(js, 'player\\/', '\\/');
|
||||
const js = await res.text();
|
||||
|
||||
Log.info(TAG, `Got player id (${player_id}). Checking for cached players..`);
|
||||
player_id = getStringBetweenStrings(js, 'player\\/', '\\/');
|
||||
}
|
||||
|
||||
Log.info(TAG, `Using player id (${player_id}). Checking for cached players..`);
|
||||
|
||||
if (!player_id)
|
||||
throw new PlayerError('Failed to get player id');
|
||||
@@ -63,9 +75,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 +238,38 @@ 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 {
|
||||
// Classic static split/join.
|
||||
const split_join_regex = /function\(([A-Za-z_0-9]+)\)\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\.split\((?:[^)]+)\)(.+?)\.join\((?:[^)]+)\))\}/;
|
||||
|
||||
// Using the global lookup variable.
|
||||
const lookup_var = global_variable?.name?.replace(/[$^\\.*+?()[\]{}|]/g, '\\$&');
|
||||
const lookup_regex = lookup_var
|
||||
? new RegExp(
|
||||
`function\\(([A-Za-z_0-9]+)\\)\\{([A-Za-z_0-9]+=[A-Za-z_0-9]+\\[${lookup_var}\\[\\d+\\]\\]\\([^)]*\\)([\\s\\S]+?)\\[${lookup_var}\\[\\d+\\]\\]\\([^)]*\\))\\}`
|
||||
)
|
||||
: null;
|
||||
|
||||
const match = data.match(split_join_regex) || (lookup_regex ? data.match(lookup_regex) : null);
|
||||
|
||||
if (!match) {
|
||||
Log.warn(TAG, 'Failed to extract signature decipher algorithm.');
|
||||
@@ -232,30 +277,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 +323,6 @@ export default class Player {
|
||||
}
|
||||
|
||||
static get LIBRARY_VERSION(): number {
|
||||
return 13;
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,13 @@ import {
|
||||
|
||||
import type { DeviceCategory } from '../utils/Utils.js';
|
||||
import type { FetchFunction, ICache } from '../types/index.js';
|
||||
import type { OAuth2Tokens, OAuth2AuthErrorEventHandler, OAuth2AuthPendingEventHandler, OAuth2AuthEventHandler } from './OAuth2.js';
|
||||
import type {
|
||||
OAuth2Tokens,
|
||||
OAuth2AuthErrorEventHandler,
|
||||
OAuth2AuthPendingEventHandler,
|
||||
OAuth2AuthEventHandler
|
||||
} from './OAuth2.js';
|
||||
import type { IRawResponse } from '../parser/index.js';
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
@@ -54,6 +60,8 @@ export type Context = {
|
||||
originalUrl?: string;
|
||||
deviceMake: string;
|
||||
deviceModel: string;
|
||||
deviceExperimentId?: string;
|
||||
rolloutToken?: string;
|
||||
utcOffsetMinutes: number;
|
||||
mainAppWebInfo?: {
|
||||
graftUrl: string;
|
||||
@@ -63,7 +71,10 @@ export type Context = {
|
||||
};
|
||||
memoryTotalKbytes?: string;
|
||||
configInfo?: {
|
||||
appInstallData: string;
|
||||
appInstallData?: string;
|
||||
coldConfigData?: string;
|
||||
coldHashData?: string;
|
||||
hotHashData?: string;
|
||||
},
|
||||
kidsAppInfo?: {
|
||||
categorySettings: {
|
||||
@@ -108,6 +119,8 @@ type ContextData = {
|
||||
device_make: string;
|
||||
device_model: string;
|
||||
on_behalf_of_user?: string;
|
||||
device_experiment_id?: string;
|
||||
rollout_token?: string;
|
||||
}
|
||||
|
||||
export type SessionOptions = {
|
||||
@@ -143,6 +156,10 @@ export type SessionOptions = {
|
||||
* Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content.
|
||||
*/
|
||||
enable_safety_mode?: boolean;
|
||||
/**
|
||||
* Specifies whether to retrieve the InnerTube config. Useful for "onesie" requests.
|
||||
*/
|
||||
retrieve_innertube_config?: boolean;
|
||||
/**
|
||||
* Specifies whether to generate the session data locally or retrieve it from YouTube.
|
||||
* This can be useful if you need more performance.
|
||||
@@ -188,12 +205,19 @@ export type SessionOptions = {
|
||||
* Proof of Origin Token. This is an attestation token generated by BotGuard/DroidGuard. It is used to confirm that the request is coming from a genuine client.
|
||||
*/
|
||||
po_token?: string;
|
||||
/**
|
||||
* Player ID override.
|
||||
* In most cases, this isn't necessary; but when YouTube introduces breaking changes,
|
||||
* forcing an older Player ID can help work around temporary issues.
|
||||
*/
|
||||
player_id?: string;
|
||||
}
|
||||
|
||||
export type SessionData = {
|
||||
context: Context;
|
||||
api_key: string;
|
||||
api_version: string;
|
||||
config_data?: string;
|
||||
}
|
||||
|
||||
export type SWSessionData = {
|
||||
@@ -220,34 +244,29 @@ const TAG = 'Session';
|
||||
* Represents an InnerTube session. This holds all the data needed to make requests to YouTube.
|
||||
*/
|
||||
export default class Session extends EventEmitter {
|
||||
public context: Context;
|
||||
public player?: Player;
|
||||
public oauth: OAuth2;
|
||||
public http: HTTPClient;
|
||||
public logged_in: boolean;
|
||||
public actions: Actions;
|
||||
public cache?: ICache;
|
||||
public key: string;
|
||||
public api_version: string;
|
||||
public account_index: number;
|
||||
public po_token?: string;
|
||||
public cookie?: string;
|
||||
public user_agent?: string;
|
||||
|
||||
constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache, po_token?: string) {
|
||||
constructor(
|
||||
public context: Context,
|
||||
public api_key: string,
|
||||
public api_version: string,
|
||||
public account_index: number,
|
||||
public config_data?: string,
|
||||
public player?: Player,
|
||||
public cookie?: string,
|
||||
fetch?: FetchFunction,
|
||||
public cache?: ICache,
|
||||
public po_token?: string
|
||||
) {
|
||||
super();
|
||||
this.http = new HTTPClient(this, cookie, fetch);
|
||||
this.actions = new Actions(this);
|
||||
this.oauth = new OAuth2(this);
|
||||
this.logged_in = !!cookie;
|
||||
this.cache = cache;
|
||||
this.account_index = account_index;
|
||||
this.key = api_key;
|
||||
this.api_version = api_version;
|
||||
this.context = context;
|
||||
this.player = player;
|
||||
this.po_token = po_token;
|
||||
this.cookie = cookie;
|
||||
this.user_agent = context.client.userAgent;
|
||||
}
|
||||
|
||||
@@ -269,7 +288,7 @@ export default class Session extends EventEmitter {
|
||||
}
|
||||
|
||||
static async create(options: SessionOptions = {}) {
|
||||
const { context, api_key, api_version, account_index } = await Session.getSessionData(
|
||||
const { context, api_key, api_version, account_index, config_data } = await Session.getSessionData(
|
||||
options.lang,
|
||||
options.location,
|
||||
options.account_index,
|
||||
@@ -284,12 +303,13 @@ export default class Session extends EventEmitter {
|
||||
options.on_behalf_of_user,
|
||||
options.cache,
|
||||
options.enable_session_cache,
|
||||
options.po_token
|
||||
options.po_token,
|
||||
options.retrieve_innertube_config
|
||||
);
|
||||
|
||||
return new Session(
|
||||
context, api_key, api_version, account_index,
|
||||
options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch, options.po_token),
|
||||
context, api_key, api_version, account_index, config_data,
|
||||
options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch, options.po_token, options.player_id),
|
||||
options.cookie, options.fetch, options.cache, options.po_token
|
||||
);
|
||||
}
|
||||
@@ -322,7 +342,7 @@ export default class Session extends EventEmitter {
|
||||
|
||||
if (session_args.on_behalf_of_user)
|
||||
result.context.user.onBehalfOfUser = session_args.on_behalf_of_user;
|
||||
|
||||
|
||||
if (session_args.user_agent)
|
||||
result.context.client.userAgent = session_args.user_agent;
|
||||
|
||||
@@ -353,9 +373,21 @@ export default class Session extends EventEmitter {
|
||||
on_behalf_of_user?: string,
|
||||
cache?: ICache,
|
||||
enable_session_cache = true,
|
||||
po_token?: string
|
||||
po_token?: string,
|
||||
retrieve_innertube_config = true
|
||||
) {
|
||||
const session_args = { lang, location, time_zone: tz, user_agent, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user, po_token };
|
||||
const session_args = {
|
||||
lang,
|
||||
location,
|
||||
time_zone: tz,
|
||||
user_agent,
|
||||
device_category,
|
||||
client_name,
|
||||
enable_safety_mode,
|
||||
visitor_data,
|
||||
on_behalf_of_user,
|
||||
po_token
|
||||
};
|
||||
|
||||
let session_data: SessionData | undefined;
|
||||
|
||||
@@ -413,6 +445,48 @@ export default class Session extends EventEmitter {
|
||||
context: this.#buildContext(context_data)
|
||||
};
|
||||
|
||||
if (retrieve_innertube_config) {
|
||||
try {
|
||||
Log.info(TAG, 'Retrieving InnerTube config data.');
|
||||
|
||||
const config_headers: Record<string, any> = {
|
||||
'Accept-Language': lang,
|
||||
'Accept': '*/*',
|
||||
'Referer': Constants.URLS.YT_BASE,
|
||||
'X-Goog-Visitor-Id': context_data.visitor_data,
|
||||
'X-Origin': Constants.URLS.YT_BASE,
|
||||
'X-Youtube-Client-Version': context_data.client_version
|
||||
};
|
||||
|
||||
if (Platform.shim.server) {
|
||||
config_headers['User-Agent'] = user_agent;
|
||||
config_headers['Origin'] = Constants.URLS.YT_BASE;
|
||||
}
|
||||
|
||||
const config = await fetch(`${Constants.URLS.API.PRODUCTION_1}v1/config?prettyPrint=false`, {
|
||||
headers: config_headers,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ context: session_data.context })
|
||||
});
|
||||
|
||||
const configJson = await config.json() as IRawResponse;
|
||||
|
||||
const coldConfigData = configJson.responseContext?.globalConfigGroup?.rawColdConfigGroup?.configData;
|
||||
const coldHashData = configJson.responseContext?.globalConfigGroup?.coldHashData;
|
||||
const hotHashData = configJson.responseContext?.globalConfigGroup?.hotHashData;
|
||||
|
||||
session_data.config_data = configJson.configData;
|
||||
session_data.context.client.configInfo = {
|
||||
...session_data.context.client.configInfo,
|
||||
coldConfigData,
|
||||
coldHashData,
|
||||
hotHashData
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error(TAG, 'Failed to retrieve config data.', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (enable_session_cache)
|
||||
await this.#storeSession(session_data, cache);
|
||||
}
|
||||
@@ -475,7 +549,7 @@ export default class Session extends EventEmitter {
|
||||
|
||||
const context_info = {
|
||||
hl: options.lang || device_info[0],
|
||||
gl: options.location || device_info[2],
|
||||
gl: options.location || device_info[1],
|
||||
remote_host: device_info[3],
|
||||
visitor_data: options.visitor_data || device_info[13],
|
||||
user_agent: options.user_agent,
|
||||
@@ -490,6 +564,8 @@ export default class Session extends EventEmitter {
|
||||
device_make: device_info[11],
|
||||
device_model: device_info[12],
|
||||
app_install_data: app_install_data,
|
||||
device_experiment_id: device_info[103],
|
||||
rollout_token: device_info[107],
|
||||
enable_safety_mode: options.enable_safety_mode
|
||||
};
|
||||
|
||||
@@ -523,6 +599,8 @@ export default class Session extends EventEmitter {
|
||||
browserVersion: args.browser_version,
|
||||
utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),
|
||||
memoryTotalKbytes: '8000000',
|
||||
rolloutToken: args.rollout_token,
|
||||
deviceExperimentId: args.device_experiment_id,
|
||||
mainAppWebInfo: {
|
||||
graftUrl: Constants.URLS.YT_BASE,
|
||||
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
|
||||
|
||||
@@ -2,20 +2,22 @@ import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from './misc/AccessibilityData.js';
|
||||
|
||||
export default class Button extends YTNode {
|
||||
static type = 'Button';
|
||||
|
||||
text?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
style?: string;
|
||||
size?: string;
|
||||
icon_type?: string;
|
||||
is_disabled?: boolean;
|
||||
target_id?: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
public text?: string;
|
||||
public label?: string;
|
||||
public tooltip?: string;
|
||||
public style?: string;
|
||||
public size?: string;
|
||||
public icon_type?: string;
|
||||
public is_disabled?: boolean;
|
||||
public target_id?: string;
|
||||
public endpoint: NavigationEndpoint;
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if (Reflect.has(data, 'text'))
|
||||
@@ -23,12 +25,13 @@ export default class Button extends YTNode {
|
||||
|
||||
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'label')) {
|
||||
this.label = data.accessibility.label;
|
||||
} else if (
|
||||
Reflect.has(data, 'accessibilityData') &&
|
||||
Reflect.has(data.accessibilityData, 'accessibilityData') &&
|
||||
Reflect.has(data.accessibilityData.accessibilityData, 'label')
|
||||
) {
|
||||
this.label = data.accessibilityData.accessibilityData.label;
|
||||
}
|
||||
|
||||
if ('accessibilityData' in data
|
||||
&& 'accessibilityData' in data.accessibilityData) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibilityData.accessibilityData)
|
||||
};
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'tooltip'))
|
||||
|
||||
@@ -94,8 +94,8 @@ export default class ButtonView extends YTNode {
|
||||
if ('customFontColor' in data)
|
||||
this.custom_font_color = data.customFontColor;
|
||||
|
||||
if ('buttonType' in data)
|
||||
this.button_type = data.buttonType;
|
||||
if ('type' in data)
|
||||
this.button_type = data.type;
|
||||
|
||||
if ('enabled' in data)
|
||||
this.enabled = data.enabled;
|
||||
|
||||
@@ -2,18 +2,29 @@ import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from './misc/AccessibilityData.js';
|
||||
|
||||
export default class ChannelThumbnailWithLink extends YTNode {
|
||||
static type = 'ChannelThumbnailWithLink';
|
||||
|
||||
thumbnails: Thumbnail[];
|
||||
endpoint: NavigationEndpoint;
|
||||
label?: string;
|
||||
public thumbnails: Thumbnail[];
|
||||
public endpoint: NavigationEndpoint;
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.label = data.accessibility?.accessibilityData?.label;
|
||||
|
||||
if ('accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get label(): string | undefined {
|
||||
return this.accessibility?.accessibility_data?.label;
|
||||
}
|
||||
}
|
||||
@@ -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,15 +1,26 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from './misc/AccessibilityData.js';
|
||||
|
||||
export default class MusicInlineBadge extends YTNode {
|
||||
static type = 'MusicInlineBadge';
|
||||
|
||||
icon_type: string;
|
||||
label: string;
|
||||
public icon_type: string;
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.icon_type = data.icon.iconType;
|
||||
this.label = data.accessibilityData.accessibilityData.label;
|
||||
|
||||
if ('accessibilityData' in data
|
||||
&& 'accessibilityData' in data.accessibilityData) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibilityData.accessibilityData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get label(): string | undefined {
|
||||
return this.accessibility?.accessibility_data?.label;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from './misc/AccessibilityData.js';
|
||||
|
||||
export default class MusicPlayButton extends YTNode {
|
||||
static type = 'MusicPlayButton';
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
play_icon_type: string;
|
||||
pause_icon_type: string;
|
||||
play_label?: string;
|
||||
pause_label?: string;
|
||||
icon_color: string;
|
||||
public endpoint: NavigationEndpoint;
|
||||
public play_icon_type: string;
|
||||
public pause_icon_type: string;
|
||||
public icon_color: string;
|
||||
public accessibility_play_data?: AccessibilitySupportedDatas;
|
||||
public accessibility_pause_data?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -18,14 +19,28 @@ export default class MusicPlayButton extends YTNode {
|
||||
this.play_icon_type = data.playIcon.iconType;
|
||||
this.pause_icon_type = data.pauseIcon.iconType;
|
||||
|
||||
if (Reflect.has(data, 'accessibilityPlayData')) {
|
||||
this.play_label = data.accessibilityPlayData.accessibilityData?.label;
|
||||
if ('accessibilityPlayData' in data
|
||||
&& 'accessibilityData' in data.accessibilityPlayData) {
|
||||
this.accessibility_play_data = {
|
||||
accessibility_data: new AccessibilityData(data.accessibilityPlayData.accessibilityData)
|
||||
};
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'accessibilityPauseData')) {
|
||||
this.pause_label = data.accessibilityPauseData.accessibilityData?.label;
|
||||
if ('accessibilityPauseData' in data
|
||||
&& 'accessibilityData' in data.accessibilityPauseData) {
|
||||
this.accessibility_pause_data = {
|
||||
accessibility_data: new AccessibilityData(data.accessibilityPauseData.accessibilityData)
|
||||
};
|
||||
}
|
||||
|
||||
this.icon_color = data.iconColor;
|
||||
}
|
||||
|
||||
get play_label(): string | undefined {
|
||||
return this.accessibility_play_data?.accessibility_data?.label;
|
||||
}
|
||||
|
||||
get pause_label(): string | undefined {
|
||||
return this.accessibility_pause_data?.accessibility_data?.label;
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/OpenOnePickAddVideoModalCommand.ts
Normal file
17
src/parser/classes/OpenOnePickAddVideoModalCommand.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
export default class OpenOnePickAddVideoModalCommand extends YTNode {
|
||||
static type = 'OpenOnePickAddVideoModalCommand';
|
||||
|
||||
list_id: string;
|
||||
modal_title: string;
|
||||
select_button_label: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.list_id = data.listId;
|
||||
this.modal_title = data.modalTitle;
|
||||
this.select_button_label = data.selectButtonLabel;
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,17 @@ import type { RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from './misc/AccessibilityData.js';
|
||||
|
||||
export default class ReelItem extends YTNode {
|
||||
static type = 'ReelItem';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
views: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
accessibility_label?: string;
|
||||
public id: string;
|
||||
public title: Text;
|
||||
public thumbnails: Thumbnail[];
|
||||
public views: Text;
|
||||
public endpoint: NavigationEndpoint;
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -21,6 +22,16 @@ export default class ReelItem extends YTNode {
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.accessibility_label = data.accessibility.accessibilityData.label;
|
||||
|
||||
if ('accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.accessibility?.accessibility_data?.label;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,17 @@ import { Parser, type RawNode } from '../index.js';
|
||||
export default class RichSection extends YTNode {
|
||||
static type = 'RichSection';
|
||||
|
||||
content: YTNode;
|
||||
public content: YTNode;
|
||||
public full_bleed: boolean;
|
||||
public target_id?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.content = Parser.parseItem(data.content);
|
||||
this.full_bleed = !!data.fullBleed;
|
||||
|
||||
if ('targetId' in data) {
|
||||
this.target_id = data.targetId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
@@ -6,22 +6,50 @@ import Text from './misc/Text.js';
|
||||
export default class RichShelf extends YTNode {
|
||||
static type = 'RichShelf';
|
||||
|
||||
title: Text;
|
||||
contents: ObservedArray<YTNode>;
|
||||
endpoint?: NavigationEndpoint;
|
||||
subtitle?: Text;
|
||||
|
||||
public title: Text;
|
||||
public contents: ObservedArray<YTNode>;
|
||||
public endpoint?: NavigationEndpoint;
|
||||
public subtitle?: Text;
|
||||
public is_expanded: boolean;
|
||||
public is_bottom_divider_hidden: boolean;
|
||||
public is_top_divider_hidden: boolean;
|
||||
public layout_sizing?: 'RICH_GRID_LAYOUT_SIZING_UNSPECIFIED'
|
||||
| 'RICH_GRID_LAYOUT_SIZING_STANDARD'
|
||||
| 'RICH_GRID_LAYOUT_SIZING_COMPACT'
|
||||
| 'RICH_GRID_LAYOUT_SIZING_EXTRA_COMPACT'
|
||||
| 'RICH_GRID_LAYOUT_SIZING_TINY';
|
||||
public icon_type?: string;
|
||||
public menu: YTNode | null;
|
||||
public next_button: YTNode | null;
|
||||
public previous_button: YTNode | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
|
||||
if (Reflect.has(data, 'endpoint')) {
|
||||
this.is_expanded = !!data.is_expanded;
|
||||
this.is_bottom_divider_hidden = !!data.isBottomDividerHidden;
|
||||
this.is_top_divider_hidden = !!data.isTopDividerHidden;
|
||||
|
||||
if ('endpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'subtitle')) {
|
||||
if ('subtitle' in data) {
|
||||
this.subtitle = new Text(data.subtitle);
|
||||
}
|
||||
|
||||
if ('layoutSizing' in data) {
|
||||
this.layout_sizing = data.layoutSizing;
|
||||
}
|
||||
|
||||
if ('icon' in data) {
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
this.next_button = Parser.parseItem(data.nextButton);
|
||||
this.previous_button = Parser.parseItem(data.previousButton);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,40 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from './misc/AccessibilityData.js';
|
||||
|
||||
export interface SubMenuItem {
|
||||
title: string;
|
||||
selected: boolean;
|
||||
continuation: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
subtitle: string | null;
|
||||
}
|
||||
|
||||
export default class SortFilterSubMenu extends YTNode {
|
||||
static type = 'SortFilterSubMenu';
|
||||
|
||||
title?: string;
|
||||
icon_type?: string;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
|
||||
sub_menu_items?: {
|
||||
title: string;
|
||||
selected: boolean;
|
||||
continuation: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
subtitle: string | null;
|
||||
}[];
|
||||
public title?: string;
|
||||
public icon_type?: string;
|
||||
public tooltip?: string;
|
||||
public sub_menu_items?: SubMenuItem[];
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if (Reflect.has(data, 'title')) {
|
||||
if ('title' in data) {
|
||||
this.title = data.title;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'icon')) {
|
||||
if ('icon' in data) {
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'accessibility')) {
|
||||
this.label = data.accessibility.accessibilityData.label;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'tooltip')) {
|
||||
if ('tooltip' in data) {
|
||||
this.tooltip = data.tooltip;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'subMenuItems')) {
|
||||
if ('subMenuItems' in data) {
|
||||
this.sub_menu_items = data.subMenuItems.map((item: RawNode) => ({
|
||||
title: item.title,
|
||||
selected: item.selected,
|
||||
@@ -45,5 +43,16 @@ export default class SortFilterSubMenu extends YTNode {
|
||||
subtitle: item.subtitle || null
|
||||
}));
|
||||
}
|
||||
|
||||
if ('accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get label(): string | undefined {
|
||||
return this.accessibility?.accessibility_data?.label;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import MenuFlexibleItem from './MenuFlexibleItem.js';
|
||||
import LikeButton from '../LikeButton.js';
|
||||
import ToggleButton from '../ToggleButton.js';
|
||||
import FlexibleActionsView from '../FlexibleActionsView.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from '../misc/AccessibilityData.js';
|
||||
|
||||
export default class Menu extends YTNode {
|
||||
static type = 'Menu';
|
||||
@@ -16,7 +17,7 @@ export default class Menu extends YTNode {
|
||||
public items: ObservedArray<YTNode>;
|
||||
public flexible_items: ObservedArray<MenuFlexibleItem>;
|
||||
public top_level_buttons: ObservedArray<ToggleButton | LikeButton | Button |ButtonView | SegmentedLikeDislikeButtonView | FlexibleActionsView>;
|
||||
public label?: string;
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -24,10 +25,17 @@ export default class Menu extends YTNode {
|
||||
this.flexible_items = Parser.parseArray(data.flexibleItems, MenuFlexibleItem);
|
||||
this.top_level_buttons = Parser.parseArray(data.topLevelButtons, [ ToggleButton, LikeButton, Button, ButtonView, SegmentedLikeDislikeButtonView, FlexibleActionsView ]);
|
||||
|
||||
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'accessibilityData')) {
|
||||
this.label = data.accessibility.accessibilityData.label;
|
||||
if ('accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get label(): string | undefined {
|
||||
return this.accessibility?.accessibility_data?.label;
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
get contents() {
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { Parser } from '../../index.js';
|
||||
import { type ObservedArray, YTNode } from '../../helpers.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
import { Parser, type RawNode } from '../../index.js';
|
||||
import ContinuationItem from '../ContinuationItem.js';
|
||||
import Message from '../Message.js';
|
||||
import Notification from '../Notification.js';
|
||||
import Text from '../misc/Text.js';
|
||||
|
||||
export default class MultiPageMenuNotificationSection extends YTNode {
|
||||
static type = 'MultiPageMenuNotificationSection';
|
||||
|
||||
public items: ObservedArray<YTNode>;
|
||||
public notification_section_title?: Text;
|
||||
public items: ObservedArray<Notification | Message | ContinuationItem>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items);
|
||||
|
||||
if ('notificationSectionTitle' in data) {
|
||||
this.notification_section_title = new Text(data.notificationSectionTitle);
|
||||
}
|
||||
|
||||
this.items = Parser.parseArray(data.items, [ Notification, Message, ContinuationItem ]);
|
||||
}
|
||||
|
||||
// XXX: Alias for consistency.
|
||||
|
||||
135
src/parser/classes/misc/AccessibilityData.ts
Normal file
135
src/parser/classes/misc/AccessibilityData.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { RawNode } from '../../types/index.js';
|
||||
|
||||
export interface AccessibilitySupportedDatas {
|
||||
accessibility_data: AccessibilityData;
|
||||
}
|
||||
|
||||
export interface AccessibilityId {
|
||||
accessibility_id_type?:
|
||||
| 'UNKNOWN'
|
||||
| 'MENU_ADD_TO_WATCH_LATER'
|
||||
| 'MENU_REMOVE_FROM_WATCH_LATER'
|
||||
| 'MENU_ADD_TO_PLAYLIST'
|
||||
| 'MENU_REMOVE_FROM_PLAYLIST'
|
||||
| 'MENU_SHARE_VIDEO'
|
||||
| 'MENU_SHARE_PLAYLIST'
|
||||
| 'MENU_OFFLINE_VIDEO'
|
||||
| 'MENU_OFFLINE_PLAYLIST'
|
||||
| 'MENU_DELETE_VIDEO'
|
||||
| 'MENU_DELETE_PLAYLIST'
|
||||
| 'MENU_EDIT_VIDEO_METADATA'
|
||||
| 'MENU_HIDE'
|
||||
| 'MENU_REMOVE_FROM_HISTORY'
|
||||
| 'MENU_LIKE'
|
||||
| 'MENU_INFO'
|
||||
| 'MENU_ADD_TO_REMOTE_QUEUE'
|
||||
| 'MENU_REMOVE_FROM_REMOTE_QUEUE'
|
||||
| 'MENU_CREATE_PLAYLIST'
|
||||
| 'MENU_SETTINGS'
|
||||
| 'MENU_PRIVACY'
|
||||
| 'MENU_FEEDBACK'
|
||||
| 'MENU_HELP'
|
||||
| 'MENU_DELETE_CHANNEL_POST'
|
||||
| 'MENU_PLAYLIST_JOIN_COLLABORATION'
|
||||
| 'MENU_EDIT_PLAYLIST'
|
||||
| 'MENU_OFFLINE_REMOVE'
|
||||
| 'MENU_OFFLINE_PAUSE'
|
||||
| 'MENU_OFFLINE_RESUME'
|
||||
| 'MENU_UNSUBSCRIBE'
|
||||
| 'MENU_GET_ALL_UPDATES'
|
||||
| 'MENU_DISMISS'
|
||||
| 'MENU_CANCEL_UPLOAD'
|
||||
| 'MENU_TAKE_PHOTO'
|
||||
| 'MENU_CHOOSE_PHOTO'
|
||||
| 'MENU_CHOOSE_FROM_CHANNEL_ART_GALLERY'
|
||||
| 'MENU_FILTER_VIDEOS_ONLY'
|
||||
| 'MENU_FILTER_VIDEOS_AND_POSTS'
|
||||
| 'MENU_WATCH_ON_TV'
|
||||
| 'MENU_INSERT_IN_REMOTE_QUEUE'
|
||||
| 'MENU_ADD_UPCOMING_EVENT_REMINDER'
|
||||
| 'MENU_REMOVE_UPCOMING_EVENT_REMINDER'
|
||||
| 'MENU_TOGGLE_DENSITY_MODE'
|
||||
| 'MENU_OFFLINE_UPSELL'
|
||||
| 'MENU_MORE_LIKE_THIS'
|
||||
| 'MENU_CREATE_VIDEO'
|
||||
| 'MENU_CREATE_LIVE_STREAM'
|
||||
| 'MENU_CREATE_REEL_ITEM'
|
||||
| 'MENU_CREATE_POST'
|
||||
| 'MENU_LESS_LIKE_THIS'
|
||||
| 'MENU_REEL_OVERFLOW'
|
||||
| 'MENU_DELETE_REEL'
|
||||
| 'MENU_EDIT_REEL'
|
||||
| 'MENU_REMOVE_FROM_QUEUE'
|
||||
| 'MENU_REEL_SHELF_OVERFLOW'
|
||||
| 'MENU_REEL_SHELF_DISMISS'
|
||||
| 'MENU_SHARE_ARTIST'
|
||||
| 'MENU_ABOUT_RECOMMENDATION'
|
||||
| 'MENU_REPORT'
|
||||
| 'EXPLORE_DESTINATION_TRENDING'
|
||||
| 'EXPLORE_DESTINATION_MUSIC'
|
||||
| 'EXPLORE_DESTINATION_GAMING'
|
||||
| 'EXPLORE_DESTINATION_NEWS'
|
||||
| 'EXPLORE_DESTINATION_MOVIES'
|
||||
| 'EXPLORE_DESTINATION_FASHION'
|
||||
| 'EXPLORE_DESTINATION_LEARNING'
|
||||
| 'EXPLORE_DESTINATION_STAY_HOME'
|
||||
| 'MENU_ABOUT_GAMING_RECOMMENDATAION'
|
||||
| 'EXPLORE_DESTINATION_LIVE'
|
||||
| 'MENU_QUALITY'
|
||||
| 'MENU_CAPTIONS'
|
||||
| 'MENU_PLAYBACK_SPEED'
|
||||
| 'MENU_SHARE_PLAYLIST_UNAVAILABLE'
|
||||
| 'MENU_INFO_CARD'
|
||||
| 'EXPLORE_DESTINATION_SPORTS'
|
||||
| 'MENU_SINGLE_LOOP'
|
||||
| 'MENU_HIDE_VIDEO'
|
||||
| 'MENU_CLEAR_QUEUE'
|
||||
| 'EXPLORE_DESTINATION_SHOPPING'
|
||||
| 'MENU_PLAY_NEXT_IN_QUEUE'
|
||||
| 'MENU_PLAY_LAST_IN_QUEUE'
|
||||
| 'MENU_GO_TO_CHANNEL'
|
||||
| 'EXPLORE_DESTINATION_PODCASTS'
|
||||
| 'MEDIA_GENERATOR_PROMPT_INPUT'
|
||||
| 'MEDIA_GENERATOR_STYLE_SHELF'
|
||||
| 'MEDIA_GENERATOR_STYLE_ITEM'
|
||||
| 'MEDIA_GENERATOR_CREATE_BUTTON'
|
||||
| 'MEDIA_GENERATOR_T2V_ENTRYPOINT'
|
||||
| 'MEDIA_GENERATOR_T2I_ENTRYPOINT'
|
||||
| 'MEDIA_GENERATOR_T2M_ENTRYPOINT'
|
||||
| 'MEDIA_GENERATOR_BACK_BUTTON'
|
||||
| 'MEDIA_GENERATOR_HEADER'
|
||||
| 'MEDIA_GENERATOR_LOADING_PROGRESS'
|
||||
| 'MEDIA_GENERATOR_CANCEL_BUTTON'
|
||||
| 'MEDIA_GENERATOR_IMAGE_PREVIEW'
|
||||
| 'MEDIA_GENERATOR_VIDEO_PREVIEW'
|
||||
| 'MEDIA_GENERATOR_DONE_BUTTON'
|
||||
| 'MEDIA_GENERATOR_IMAGE_SELECTION'
|
||||
| 'MEDIA_GENERATOR_SOUND_METADATA'
|
||||
| 'MEDIA_GENERATOR_AUDIO_SELECT_BUTTON'
|
||||
| 'MEDIA_GENERATOR_T2I2V_ENTRYPOINT'
|
||||
| 'MENU_SAVE_QUEUE_TO_PLAYLIST'
|
||||
| 'MEDIA_GENERATOR_ANIMATE_BUTTON'
|
||||
| 'MEDIA_GENERATOR_SEGMENT_IMPORT_ENTRYPOINT';
|
||||
}
|
||||
|
||||
export default class AccessibilityData {
|
||||
public accessibility_identifier?: string;
|
||||
public identifier?: AccessibilityId;
|
||||
public label?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
if ('accessibilityIdentifier' in data) {
|
||||
this.accessibility_identifier = data.accessibilityIdentifier;
|
||||
}
|
||||
|
||||
if ('identifier' in data) {
|
||||
this.identifier = {
|
||||
accessibility_id_type: data.identifier.accessibilityIdType
|
||||
};
|
||||
}
|
||||
|
||||
if ('label' in data) {
|
||||
this.label = data.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,17 @@ import type { RawNode } from '../../index.js';
|
||||
import { escape, type Run } from './Text.js';
|
||||
import Thumbnail from './Thumbnail.js';
|
||||
|
||||
export interface Emoji {
|
||||
emoji_id: string;
|
||||
shortcuts: string[];
|
||||
search_terms: string[];
|
||||
image: Thumbnail[];
|
||||
is_custom: boolean;
|
||||
}
|
||||
|
||||
export default class EmojiRun implements Run {
|
||||
text: string;
|
||||
emoji: {
|
||||
emoji_id: string;
|
||||
shortcuts: string[];
|
||||
search_terms: string[];
|
||||
image: Thumbnail[];
|
||||
is_custom: boolean;
|
||||
};
|
||||
public text: string;
|
||||
public emoji: Emoji;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.text =
|
||||
|
||||
@@ -3,13 +3,20 @@ import type { RawNode } from '../../index.js';
|
||||
import NavigationEndpoint from '../NavigationEndpoint.js';
|
||||
import EmojiRun from './EmojiRun.js';
|
||||
import TextRun from './TextRun.js';
|
||||
import AccessibilityData from './AccessibilityData.js';
|
||||
|
||||
export interface Run {
|
||||
text: string;
|
||||
|
||||
toString(): string;
|
||||
|
||||
toHTML(): string;
|
||||
}
|
||||
|
||||
export interface FormattedStringSupportedAccessibilityDatas {
|
||||
accessibility_data: AccessibilityData;
|
||||
}
|
||||
|
||||
export function escape(text: string) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
@@ -19,36 +26,78 @@ export function escape(text: string) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Place this here, instead of in a private static property,
|
||||
// To avoid the performance penalty of the private field polyfill
|
||||
const TAG = 'Text';
|
||||
|
||||
/**
|
||||
* Represents text content that may include formatting, emojis, and navigation endpoints.
|
||||
*/
|
||||
export default class Text {
|
||||
text?: string;
|
||||
runs?: (EmojiRun | TextRun)[];
|
||||
endpoint?: NavigationEndpoint;
|
||||
/**
|
||||
* The plain text content.
|
||||
*/
|
||||
public text?: string;
|
||||
|
||||
/**
|
||||
* Individual text segments with their formatting.
|
||||
*/
|
||||
public runs?: (EmojiRun | TextRun)[];
|
||||
|
||||
/**
|
||||
* Navigation endpoint associated with this text.
|
||||
*/
|
||||
public endpoint?: NavigationEndpoint;
|
||||
|
||||
/**
|
||||
* Accessibility data associated with this text.
|
||||
*/
|
||||
public accessibility?: FormattedStringSupportedAccessibilityDatas;
|
||||
|
||||
/**
|
||||
* Indicates if the text is right-to-left.
|
||||
*/
|
||||
public rtl: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
if (typeof data === 'object' && data !== null && Reflect.has(data, 'runs') && Array.isArray(data.runs)) {
|
||||
this.runs = data.runs.map((run: RawNode) => run.emoji ?
|
||||
new EmojiRun(run) :
|
||||
new TextRun(run)
|
||||
if (this.isRunsData(data)) {
|
||||
this.runs = data.runs.map((run: RawNode) =>
|
||||
run.emoji ? new EmojiRun(run) : new TextRun(run)
|
||||
);
|
||||
this.text = this.runs.map((run) => run.text).join('');
|
||||
} else {
|
||||
this.text = data?.simpleText;
|
||||
}
|
||||
if (typeof data === 'object' && data !== null && Reflect.has(data, 'navigationEndpoint')) {
|
||||
|
||||
if (this.isObject(data) && 'accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
|
||||
this.rtl = !!data?.rtl;
|
||||
|
||||
this.parseEndpoint(data);
|
||||
}
|
||||
|
||||
private isRunsData(data: RawNode): data is { runs: RawNode[] } {
|
||||
return this.isObject(data) &&
|
||||
Reflect.has(data, 'runs') &&
|
||||
Array.isArray(data.runs);
|
||||
}
|
||||
|
||||
private parseEndpoint(data: RawNode): void {
|
||||
if (!this.isObject(data)) return;
|
||||
if ('navigationEndpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
}
|
||||
if (typeof data === 'object' && data !== null && Reflect.has(data, 'titleNavigationEndpoint')) {
|
||||
} else if ('titleNavigationEndpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.titleNavigationEndpoint);
|
||||
} else if ((this.runs?.[0] as TextRun)?.endpoint) {
|
||||
this.endpoint = (this.runs?.[0] as TextRun).endpoint;
|
||||
}
|
||||
if (!this.endpoint) {
|
||||
if ((this.runs?.[0] as TextRun)?.endpoint) {
|
||||
this.endpoint = (this.runs?.[0] as TextRun)?.endpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isObject(data: RawNode): boolean {
|
||||
return typeof data === 'object' && data !== null;
|
||||
}
|
||||
|
||||
static fromAttributed(data: AttributedText) {
|
||||
@@ -75,120 +124,127 @@ export default class Text {
|
||||
length: run.length ?? content.length
|
||||
}) as StyleRun & ResponseRun);
|
||||
|
||||
if (style_runs || command_runs || attachment_runs) {
|
||||
if (style_runs) {
|
||||
for (const style_run of style_runs) {
|
||||
if (
|
||||
style_run.italic ||
|
||||
style_run.strikethrough === 'LINE_STYLE_SINGLE' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_BOLD'
|
||||
) {
|
||||
const matching_run = findMatchingRun(runs, style_run);
|
||||
if (style_runs?.length)
|
||||
this.processStyleRuns(runs, style_runs, data);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for style run. Skipping...', {
|
||||
style_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
if (command_runs?.length)
|
||||
this.processCommandRuns(runs, command_runs, data);
|
||||
|
||||
continue;
|
||||
}
|
||||
if (attachment_runs?.length)
|
||||
this.processAttachmentRuns(runs, attachment_runs, data);
|
||||
|
||||
// Comments use MEDIUM for bold text and video descriptions use BOLD for bold text
|
||||
insertSubRun(runs, matching_run, style_run, {
|
||||
bold: style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' || style_run.weightLabel === 'FONT_WEIGHT_BOLD',
|
||||
italics: style_run.italic,
|
||||
strikethrough: style_run.strikethrough === 'LINE_STYLE_SINGLE'
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping style run as it is doesn\'t have any information that we parse.', {
|
||||
style_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
return new Text({ runs });
|
||||
}
|
||||
|
||||
private static processStyleRuns(runs: RawRun[], style_runs: (StyleRun & ResponseRun)[], data: AttributedText) {
|
||||
for (const style_run of style_runs) {
|
||||
if (
|
||||
style_run.italic ||
|
||||
style_run.strikethrough === 'LINE_STYLE_SINGLE' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' ||
|
||||
style_run.weightLabel === 'FONT_WEIGHT_BOLD'
|
||||
) {
|
||||
const matching_run = findMatchingRun(runs, style_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for style run. Skipping...', {
|
||||
style_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comments use MEDIUM for bold text and video descriptions use BOLD for bold text
|
||||
insertSubRun(runs, matching_run, style_run, {
|
||||
bold: style_run.weightLabel === 'FONT_WEIGHT_MEDIUM' || style_run.weightLabel === 'FONT_WEIGHT_BOLD',
|
||||
italics: style_run.italic,
|
||||
strikethrough: style_run.strikethrough === 'LINE_STYLE_SINGLE'
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping style run as it is doesn\'t have any information that we parse.', {
|
||||
style_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static processCommandRuns(runs: RawRun[], command_runs: CommandRun[], data: AttributedText) {
|
||||
for (const command_run of command_runs) {
|
||||
if (command_run.onTap) {
|
||||
const matching_run = findMatchingRun(runs, command_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for command run. Skipping...', {
|
||||
command_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
insertSubRun(runs, matching_run, command_run, {
|
||||
navigationEndpoint: command_run.onTap
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping command run as it is missing the "doTap" property.', {
|
||||
command_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static processAttachmentRuns(runs: RawRun[], attachment_runs: AttachmentRun[], data: AttributedText) {
|
||||
for (const attachment_run of attachment_runs) {
|
||||
const matching_run = findMatchingRun(runs, attachment_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for attachment run. Skipping...', {
|
||||
attachment_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command_runs) {
|
||||
for (const command_run of command_runs) {
|
||||
if (command_run.onTap) {
|
||||
const matching_run = findMatchingRun(runs, command_run);
|
||||
if (attachment_run.length === 0) {
|
||||
matching_run.attachment = attachment_run;
|
||||
} else {
|
||||
const offset_start_index = attachment_run.startIndex - matching_run.startIndex;
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for command run. Skipping...', {
|
||||
command_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
const text = matching_run.text.substring(offset_start_index, offset_start_index + attachment_run.length);
|
||||
|
||||
continue;
|
||||
}
|
||||
const is_custom_emoji = (/^:[^:]+:$/).test(text);
|
||||
|
||||
insertSubRun(runs, matching_run, command_run, {
|
||||
navigationEndpoint: command_run.onTap
|
||||
});
|
||||
} else {
|
||||
Log.debug(TAG, 'Skipping command run as it is missing the "doTap" property.', {
|
||||
command_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attachment_run.element?.type?.imageType?.image && (is_custom_emoji || (/^(?:\p{Emoji}|\u200d)+$/u).test(text))) {
|
||||
const emoji = {
|
||||
image: attachment_run.element.type.imageType.image,
|
||||
isCustomEmoji: is_custom_emoji,
|
||||
shortcuts: is_custom_emoji ? [ text ] : undefined
|
||||
};
|
||||
|
||||
if (attachment_runs) {
|
||||
for (const attachment_run of attachment_runs) {
|
||||
const matching_run = findMatchingRun(runs, attachment_run);
|
||||
|
||||
if (!matching_run) {
|
||||
Log.warn(TAG, 'Unable to find matching run for attachment run. Skipping...', {
|
||||
attachment_run,
|
||||
input_data: data,
|
||||
// For performance reasons, web browser consoles only expand an object, when the user clicks on it,
|
||||
// So if we log the original runs object, it might have changed by the time the user looks at it.
|
||||
// Deep clone, so that we log the exact state of the runs at this point.
|
||||
parsed_runs: JSON.parse(JSON.stringify(runs))
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attachment_run.length === 0) {
|
||||
matching_run.attachment = attachment_run;
|
||||
} else {
|
||||
const offset_start_index = attachment_run.startIndex - matching_run.startIndex;
|
||||
|
||||
const text = matching_run.text.substring(offset_start_index, offset_start_index + attachment_run.length);
|
||||
|
||||
const is_custom_emoji = (/^:[^:]+:$/).test(text);
|
||||
|
||||
if (attachment_run.element?.type?.imageType?.image && (is_custom_emoji || (/^(?:\p{Emoji}|\u200d)+$/u).test(text))) {
|
||||
const emoji = {
|
||||
image: attachment_run.element.type.imageType.image,
|
||||
isCustomEmoji: is_custom_emoji,
|
||||
shortcuts: is_custom_emoji ? [ text ] : undefined
|
||||
};
|
||||
|
||||
insertSubRun(runs, matching_run, attachment_run, { emoji });
|
||||
} else {
|
||||
insertSubRun(runs, matching_run, attachment_run, {
|
||||
attachment: attachment_run
|
||||
});
|
||||
}
|
||||
}
|
||||
insertSubRun(runs, matching_run, attachment_run, { emoji });
|
||||
} else {
|
||||
insertSubRun(runs, matching_run, attachment_run, {
|
||||
attachment: attachment_run
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Text({ runs });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +348,7 @@ interface StyleRun extends Partial<ResponseRun> {
|
||||
value: number
|
||||
}[]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface CommandRun extends ResponseRun {
|
||||
|
||||
@@ -3,25 +3,58 @@ import { escape, type Run } from './Text.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
export default class TextRun implements Run {
|
||||
text: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
bold: boolean;
|
||||
italics: boolean;
|
||||
strikethrough: boolean;
|
||||
deemphasize: boolean;
|
||||
attachment;
|
||||
public text: string;
|
||||
public text_color?: number;
|
||||
public endpoint?: NavigationEndpoint;
|
||||
public bold: boolean;
|
||||
public bracket: boolean;
|
||||
public dark_mode_text_color?: number;
|
||||
public deemphasize: boolean;
|
||||
public italics: boolean;
|
||||
public strikethrough: boolean;
|
||||
public error_underline: boolean;
|
||||
public underline: boolean;
|
||||
public font_face?:
|
||||
| 'FONT_FACE_UNKNOWN'
|
||||
| 'FONT_FACE_YT_SANS_MEDIUM'
|
||||
| 'FONT_FACE_ROBOTO_MEDIUM'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_LIGHT'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_REGULAR'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_MEDIUM'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_SEMIBOLD'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_BOLD'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_EXTRABOLD'
|
||||
| 'FONT_FACE_YOUTUBE_SANS_BLACK'
|
||||
| 'FONT_FACE_YT_SANS_BOLD'
|
||||
| 'FONT_FACE_ROBOTO_REGULAR';
|
||||
public attachment;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.text = data.text;
|
||||
this.bold = Boolean(data.bold);
|
||||
this.bracket = Boolean(data.bracket);
|
||||
this.italics = Boolean(data.italics);
|
||||
this.strikethrough = Boolean(data.strikethrough);
|
||||
this.error_underline = Boolean(data.error_underline);
|
||||
this.underline = Boolean(data.underline);
|
||||
this.deemphasize = Boolean(data.deemphasize);
|
||||
|
||||
if (Reflect.has(data, 'navigationEndpoint')) {
|
||||
if ('textColor' in data) {
|
||||
this.text_color = data.textColor;
|
||||
}
|
||||
|
||||
if ('navigationEndpoint' in data) {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
}
|
||||
|
||||
if ('darkModeTextColor' in data) {
|
||||
this.dark_mode_text_color = data.darkModeTextColor;
|
||||
}
|
||||
|
||||
if ('fontFace' in data) {
|
||||
this.font_face = data.fontFace;
|
||||
}
|
||||
|
||||
this.attachment = data.attachment;
|
||||
}
|
||||
|
||||
@@ -36,7 +69,9 @@ export default class TextRun implements Run {
|
||||
if (this.italics) tags.push('i');
|
||||
if (this.strikethrough) tags.push('s');
|
||||
if (this.deemphasize) tags.push('small');
|
||||
|
||||
if (this.underline) tags.push('u');
|
||||
if (this.error_underline) tags.push('u');
|
||||
|
||||
if (!this.text?.length)
|
||||
return '';
|
||||
|
||||
@@ -48,13 +83,13 @@ export default class TextRun implements Run {
|
||||
if (this.attachment.element.type.imageType.image.sources.length) {
|
||||
if (this.endpoint) {
|
||||
const { url } = this.attachment.element.type.imageType.image.sources[0];
|
||||
|
||||
|
||||
let image_el = '';
|
||||
|
||||
|
||||
if (url) {
|
||||
image_el = `<img src="${url}" style="vertical-align: middle; height: ${this.attachment.element.properties.layoutProperties.height.value}px; width: ${this.attachment.element.properties.layoutProperties.width.value}px;" alt="">`;
|
||||
}
|
||||
|
||||
|
||||
const nav_url = this.endpoint.toURL();
|
||||
if (nav_url) return `<a href="${nav_url}" class="yt-ch-link">${image_el}${wrapped_text}</a>`;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Parser, type RawNode } from '../../index.js';
|
||||
export default class MultiPageMenuSection extends YTNode {
|
||||
static type = 'MultiPageMenuSection';
|
||||
|
||||
public items: ObservedArray<YTNode> | null;
|
||||
public items: ObservedArray<YTNode>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import { type RawNode } from '../../index.js';
|
||||
import Text from '../misc/Text.js';
|
||||
import NavigationEndpoint from '../NavigationEndpoint.js';
|
||||
import AccessibilityData, { type AccessibilitySupportedDatas } from '../misc/AccessibilityData.js';
|
||||
|
||||
export default class PivotBarItem extends YTNode {
|
||||
static type = 'PivotBarItem';
|
||||
@@ -11,6 +11,7 @@ export default class PivotBarItem extends YTNode {
|
||||
public title: Text;
|
||||
public accessibility_label?: string;
|
||||
public icon_type?: string;
|
||||
public accessibility?: AccessibilitySupportedDatas;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -18,10 +19,18 @@ export default class PivotBarItem extends YTNode {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.title = new Text(data.title);
|
||||
|
||||
if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'accessibilityData'))
|
||||
this.accessibility_label = data.accessibility.accessibilityData.label;
|
||||
|
||||
if ('accessibility' in data
|
||||
&& 'accessibilityData' in data.accessibility) {
|
||||
this.accessibility = {
|
||||
accessibility_data: new AccessibilityData(data.accessibility.accessibilityData)
|
||||
};
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType'))
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.accessibility?.accessibility_data?.label;
|
||||
}
|
||||
}
|
||||
@@ -11,22 +11,13 @@ export class YTNode {
|
||||
this.type = (this.constructor as YTNodeConstructor).type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is of the given type.
|
||||
* @param type - The type to check
|
||||
* @returns whether the node is of the given type
|
||||
*/
|
||||
#is<T extends YTNode>(type: YTNodeConstructor<T>): this is T {
|
||||
return this.type === type.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the node is of the given type.
|
||||
* @param types - The type to check
|
||||
* @returns whether the node is of the given type
|
||||
*/
|
||||
is<T extends YTNode, K extends YTNodeConstructor<T>[]>(...types: K): this is InstanceType<K[number]> {
|
||||
return types.some((type) => this.#is(type));
|
||||
return types.some((type) => this.type === type.type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See ./scripts/build-parser-map.js
|
||||
|
||||
export { default as AccessibilityContext } from './classes/misc/AccessibilityContext.js';
|
||||
export { default as AccessibilityData } from './classes/misc/AccessibilityData.js';
|
||||
export { default as Author } from './classes/misc/Author.js';
|
||||
export { default as ChildElement } from './classes/misc/ChildElement.js';
|
||||
export { default as CommandContext } from './classes/misc/CommandContext.js';
|
||||
|
||||
@@ -346,6 +346,7 @@ export { default as TopbarMenuButton } from './classes/mweb/TopbarMenuButton.js'
|
||||
export { default as NavigationEndpoint } from './classes/NavigationEndpoint.js';
|
||||
export { default as Notification } from './classes/Notification.js';
|
||||
export { default as NotificationAction } from './classes/NotificationAction.js';
|
||||
export { default as OpenOnePickAddVideoModalCommand } from './classes/OpenOnePickAddVideoModalCommand.js';
|
||||
export { default as PageHeader } from './classes/PageHeader.js';
|
||||
export { default as PageHeaderView } from './classes/PageHeaderView.js';
|
||||
export { default as PageIntroduction } from './classes/PageIntroduction.js';
|
||||
|
||||
@@ -517,6 +517,10 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
if (data.entries) {
|
||||
parsed_data.entries = data.entries.map((entry) => new NavigationEndpoint(entry));
|
||||
}
|
||||
|
||||
if (data.targetId) {
|
||||
parsed_data.target_id = data.targetId;
|
||||
}
|
||||
|
||||
return parsed_data;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
PlaylistPanelContinuation, SectionListContinuation, ContinuationCommand, ShowMiniplayerCommand, NavigateAction
|
||||
} from '../index.js';
|
||||
|
||||
import type { CpnSource } from './RawResponse.js';
|
||||
import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js';
|
||||
import type CardCollection from '../classes/CardCollection.js';
|
||||
import type Endscreen from '../classes/Endscreen.js';
|
||||
@@ -68,13 +67,17 @@ export interface IParsedResponse {
|
||||
endscreen?: Endscreen;
|
||||
cards?: CardCollection;
|
||||
cpn_info?: {
|
||||
cpn: string;
|
||||
cpn_source: CpnSource;
|
||||
cpn?: string;
|
||||
cpn_source?:
|
||||
| 'CPN_SOURCE_TYPE_UNKNOWN'
|
||||
| 'CPN_SOURCE_TYPE_CLIENT'
|
||||
| 'CPN_SOURCE_TYPE_WATCH_SERVER';
|
||||
},
|
||||
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
|
||||
items?: SuperParsedResult<YTNode>;
|
||||
entries?: NavigationEndpoint[];
|
||||
entries_memo?: Memo;
|
||||
target_id?: string;
|
||||
continuation_endpoint?: YTNode;
|
||||
player_response?: IPlayerResponse;
|
||||
watch_next_response?: INextResponse;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -201,7 +201,7 @@ export default class HTTPClient {
|
||||
ctx.client.clientVersion = Constants.CLIENTS.IOS.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.IOS.NAME;
|
||||
ctx.client.platform = 'MOBILE';
|
||||
ctx.client.osName = Constants.CLIENTS.IOS.NAME;
|
||||
ctx.client.osName = Constants.CLIENTS.IOS.OS_NAME;
|
||||
ctx.client.osVersion = Constants.CLIENTS.IOS.OS_VERSION;
|
||||
delete ctx.client.browserName;
|
||||
delete ctx.client.browserVersion;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -100,7 +100,8 @@
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
"plugins": [
|
||||
{ "transform": "ts-transformer-inline-file/transformer" }
|
||||
{ "transform": "ts-transformer-inline-file/transformer" },
|
||||
{ "transform": "./dev-scripts/enum-optimising-transformer.cjs" }
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user