mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ecfb08772 | ||
|
|
2029aec90d | ||
|
|
d589365ea2 | ||
|
|
45f33d8c04 | ||
|
|
92117eaaa0 | ||
|
|
39725374e3 | ||
|
|
213d78b1ab | ||
|
|
28f53a698d | ||
|
|
776a156f65 | ||
|
|
4a9bd32fd7 | ||
|
|
3170659880 | ||
|
|
e6f1f078a8 | ||
|
|
900f557202 | ||
|
|
7ca2a0c3e4 | ||
|
|
f95283b236 | ||
|
|
f6a7bcc44a | ||
|
|
c444843799 |
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -13,6 +13,6 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -13,5 +13,6 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. Remove the stale label or comment or this will be closed in 2 days'
|
||||
any-of-issue-labels: 'needs-more-info,cannot-reproduce,question,help-wanted'
|
||||
days-before-stale: 60
|
||||
days-before-close: 4
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -13,6 +13,6 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## [9.2.0](https://github.com/LuanRT/YouTube.js/compare/v9.1.0...v9.2.0) (2024-03-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support of cloudflare workers ([#596](https://github.com/LuanRT/YouTube.js/issues/596)) ([2029aec](https://github.com/LuanRT/YouTube.js/commit/2029aec90de3c0fdb022094d7b704a2feed4133b))
|
||||
* **parser:** Support CommentView nodes ([#614](https://github.com/LuanRT/YouTube.js/issues/614)) ([900f557](https://github.com/LuanRT/YouTube.js/commit/900f5572021d348e7012909f2080e52eac06adae))
|
||||
* **parser:** Support LockupView and it's child nodes ([#609](https://github.com/LuanRT/YouTube.js/issues/609)) ([7ca2a0c](https://github.com/LuanRT/YouTube.js/commit/7ca2a0c3e43ebd4b9443e69b7432f302b09e9c7a))
|
||||
* **Text:** Support formatting and emojis in `fromAttributed` ([#615](https://github.com/LuanRT/YouTube.js/issues/615)) ([e6f1f07](https://github.com/LuanRT/YouTube.js/commit/e6f1f078a828f8ea5db1fe7aec9f677bc53694e3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Cache:** handle the value read from the db correctly according to its type ([#620](https://github.com/LuanRT/YouTube.js/issues/620)) ([3170659](https://github.com/LuanRT/YouTube.js/commit/317065988007c860bf6173b0ac3c3d685ed81d20))
|
||||
* **PlayerEndpoint:** Workaround for "The following content is not available on this app" (Android) ([#624](https://github.com/LuanRT/YouTube.js/issues/624)) ([d589365](https://github.com/LuanRT/YouTube.js/commit/d589365ea27f540ff36e33a65362c932cd28c274))
|
||||
|
||||
## [9.1.0](https://github.com/LuanRT/YouTube.js/compare/v9.0.2...v9.1.0) (2024-02-23)
|
||||
|
||||
|
||||
|
||||
18
examples/cloudflare-worker/package.json
Normal file
18
examples/cloudflare-worker/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "cf-worker",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev",
|
||||
"start": "wrangler dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240208.0",
|
||||
"typescript": "^5.0.4",
|
||||
"wrangler": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"youtubei.js": "latest"
|
||||
}
|
||||
}
|
||||
19
examples/cloudflare-worker/src/index.ts
Normal file
19
examples/cloudflare-worker/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Innertube } from "youtubei.js/cf-worker";
|
||||
|
||||
export interface Env {}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
// cannot initialize Innertube in global scope as it makes fetch requests
|
||||
const yt = await Innertube.create();
|
||||
|
||||
const video = await yt.getInfo("jNQXAC9IVRw");
|
||||
console.log("Video title is", video.basic_info.title);
|
||||
|
||||
return new Response("Hello World!");
|
||||
},
|
||||
};
|
||||
19
examples/cloudflare-worker/tsconfig.json
Normal file
19
examples/cloudflare-worker/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"lib": ["es2021"],
|
||||
"jsx": "react",
|
||||
"module": "es2022",
|
||||
"moduleResolution": "node",
|
||||
"types": ["@cloudflare/workers-types/2023-07-01"],
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
3
examples/cloudflare-worker/wrangler.toml
Normal file
3
examples/cloudflare-worker/wrangler.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
name = "cf-worker-youtubei"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-02-08"
|
||||
@@ -62,4 +62,4 @@ Returns original InnerTube response (sanitized).
|
||||
**Returns:** `ParsedResponse`
|
||||
|
||||
## Example
|
||||
See [`index.ts`]('./index.ts').
|
||||
See [`index.ts`](./index.ts).
|
||||
@@ -109,4 +109,4 @@ Sends a message.
|
||||
**Returns:** `Promise<ObservedArray<AddChatItemAction>>`
|
||||
|
||||
## Example
|
||||
See [`index.ts`]('./index.ts').
|
||||
See [`index.ts`](./index.ts).
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "9.1.0",
|
||||
"version": "9.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "9.1.0",
|
||||
"version": "9.2.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "9.1.0",
|
||||
"version": "9.2.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
@@ -17,6 +17,9 @@
|
||||
],
|
||||
"web.bundle.min": [
|
||||
"./dist/src/platform/lib.d.ts"
|
||||
],
|
||||
"cf-worker": [
|
||||
"./dist/src/platform/lib.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -46,6 +49,10 @@
|
||||
"./web.bundle.min": {
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"default": "./bundle/browser.min.js"
|
||||
},
|
||||
"./cf-worker": {
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"default": "./dist/src/platform/cf-worker.js"
|
||||
}
|
||||
},
|
||||
"author": "LuanRT <luan.lrt4@gmail.com> (https://github.com/LuanRT)",
|
||||
@@ -68,7 +75,7 @@
|
||||
"test": "npx jest --verbose",
|
||||
"lint": "npx eslint ./src",
|
||||
"lint:fix": "npx eslint --fix ./src",
|
||||
"build": "npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod",
|
||||
"build": "npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod && npm run bundle:cf-worker",
|
||||
"build:parser-map": "node ./scripts/gen-parser-map.mjs",
|
||||
"build:proto": "npx pb-gen-ts --entry-path=\"src/proto\" --out-dir=\"src/proto/generated\" --ext-in-import=\".js\"",
|
||||
"build:esm": "npx tspc",
|
||||
@@ -76,6 +83,7 @@
|
||||
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --external:tslib --sourcemap --banner:js=\"/* eslint-disable */\"",
|
||||
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/browser.js --platform=browser",
|
||||
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
|
||||
"bundle:cf-worker": "npx esbuild ./dist/src/platform/cf-worker.js --banner:js=\"/* eslint-disable */\" --bundle --target=es2020 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/cf-worker.js --platform=node",
|
||||
"prepare": "npm run build",
|
||||
"watch": "npx tsc --watch"
|
||||
},
|
||||
|
||||
@@ -33,25 +33,25 @@ export interface Context {
|
||||
hl: string;
|
||||
gl: string;
|
||||
remoteHost?: string;
|
||||
screenDensityFloat: number;
|
||||
screenHeightPoints: number;
|
||||
screenPixelDensity: number;
|
||||
screenWidthPoints: number;
|
||||
visitorData: string;
|
||||
screenDensityFloat?: number;
|
||||
screenHeightPoints?: number;
|
||||
screenPixelDensity?: number;
|
||||
screenWidthPoints?: number;
|
||||
visitorData?: string;
|
||||
clientName: string;
|
||||
clientVersion: string;
|
||||
clientScreen?: string,
|
||||
androidSdkVersion?: string;
|
||||
androidSdkVersion?: number;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
platform: string;
|
||||
clientFormFactor: string;
|
||||
userInterfaceTheme: string;
|
||||
userInterfaceTheme?: string;
|
||||
timeZone: string;
|
||||
userAgent?: string;
|
||||
browserName?: string;
|
||||
browserVersion?: string;
|
||||
originalUrl: string;
|
||||
originalUrl?: string;
|
||||
deviceMake: string;
|
||||
deviceModel: string;
|
||||
utcOffsetMinutes: number;
|
||||
@@ -73,6 +73,10 @@ export interface Context {
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
};
|
||||
request?: {
|
||||
useSsl: boolean;
|
||||
internalExperimentFlags: any[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
@@ -327,11 +331,17 @@ export default class Session extends EventEmitter {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
lockedSafetyMode: false
|
||||
},
|
||||
request: {
|
||||
useSsl: true,
|
||||
internalExperimentFlags: []
|
||||
}
|
||||
};
|
||||
|
||||
if (options.on_behalf_of_user)
|
||||
context.user.onBehalfOfUser = options.on_behalf_of_user;
|
||||
|
||||
return { context, api_key, api_version };
|
||||
}
|
||||
|
||||
@@ -366,11 +376,17 @@ export default class Session extends EventEmitter {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
lockedSafetyMode: false
|
||||
},
|
||||
request: {
|
||||
useSsl: true,
|
||||
internalExperimentFlags: []
|
||||
}
|
||||
};
|
||||
|
||||
if (options.on_behalf_of_user)
|
||||
context.user.onBehalfOfUser = options.on_behalf_of_user;
|
||||
|
||||
return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { encodeShortsParam } from '../../proto/index.js';
|
||||
import type { IPlayerRequest, PlayerEndpointOptions } from '../../types/index.js';
|
||||
|
||||
export const PATH = '/player';
|
||||
@@ -43,7 +44,7 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
|
||||
client: opts.client,
|
||||
playlistId: opts.playlist_id,
|
||||
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
|
||||
params: is_android ? '2AMBCgIQBg' : opts.params
|
||||
params: is_android ? encodeShortsParam() : opts.params
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import CompactVideo from '../../parser/classes/CompactVideo.js';
|
||||
import GridChannel from '../../parser/classes/GridChannel.js';
|
||||
import GridPlaylist from '../../parser/classes/GridPlaylist.js';
|
||||
import GridVideo from '../../parser/classes/GridVideo.js';
|
||||
import LockupView from '../../parser/classes/LockupView.js';
|
||||
import Playlist from '../../parser/classes/Playlist.js';
|
||||
import PlaylistPanelVideo from '../../parser/classes/PlaylistPanelVideo.js';
|
||||
import PlaylistVideo from '../../parser/classes/PlaylistVideo.js';
|
||||
@@ -88,7 +89,18 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Get all playlists on a given page via memo
|
||||
*/
|
||||
static getPlaylistsFromMemo(memo: Memo) {
|
||||
return memo.getType(Playlist, GridPlaylist);
|
||||
const playlists: ObservedArray<Playlist | GridPlaylist | LockupView> = memo.getType(Playlist, GridPlaylist);
|
||||
|
||||
const lockup_views = memo.getType(LockupView)
|
||||
.filter((lockup) => {
|
||||
return [ 'PLAYLIST', 'ALBUM', 'PODCAST' ].includes(lockup.content_type);
|
||||
});
|
||||
|
||||
if (lockup_views.length > 0) {
|
||||
playlists.push(...lockup_views);
|
||||
}
|
||||
|
||||
return playlists;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
23
src/parser/classes/CollectionThumbnailView.ts
Normal file
23
src/parser/classes/CollectionThumbnailView.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ThumbnailView from './ThumbnailView.js';
|
||||
|
||||
export default class CollectionThumbnailView extends YTNode {
|
||||
static type = 'CollectionThumbnailView';
|
||||
|
||||
primary_thumbnail: ThumbnailView | null;
|
||||
stack_color: {
|
||||
light_theme: number;
|
||||
dark_theme: number;
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.primary_thumbnail = Parser.parseItem(data.primaryThumbnail, ThumbnailView);
|
||||
this.stack_color = {
|
||||
light_theme: data.stackColor.lightTheme,
|
||||
dark_theme: data.stackColor.darkTheme
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export type MetadataRow = {
|
||||
metadata_parts: {
|
||||
metadata_parts?: {
|
||||
text: Text;
|
||||
}[];
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export default class ContentMetadataView extends YTNode {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.metadata_rows = data.metadataRows.map((row: RawNode) => ({
|
||||
metadata_parts: row.metadataParts.map((part: RawNode) => ({
|
||||
metadata_parts: row.metadataParts?.map((part: RawNode) => ({
|
||||
text: Text.fromAttributed(part.text)
|
||||
}))
|
||||
}));
|
||||
|
||||
18
src/parser/classes/LockupMetadataView.ts
Normal file
18
src/parser/classes/LockupMetadataView.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ContentMetadataView from './ContentMetadataView.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class LockupMetadataView extends YTNode {
|
||||
static type = 'LockupMetadataView';
|
||||
|
||||
title: Text;
|
||||
metadata: ContentMetadataView | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.title = Text.fromAttributed(data.title);
|
||||
this.metadata = Parser.parseItem(data.metadata, ContentMetadataView);
|
||||
}
|
||||
}
|
||||
25
src/parser/classes/LockupView.ts
Normal file
25
src/parser/classes/LockupView.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import CollectionThumbnailView from './CollectionThumbnailView.js';
|
||||
import LockupMetadataView from './LockupMetadataView.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
export default class LockupView extends YTNode {
|
||||
static type = 'LockupView';
|
||||
|
||||
content_image: CollectionThumbnailView | null;
|
||||
metadata: LockupMetadataView | null;
|
||||
content_id: string;
|
||||
content_type: 'SOURCE' | 'PLAYLIST' | 'ALBUM' | 'PODCAST' | 'SHOPPING_COLLECTION' | 'SHORT' | 'GAME' | 'PRODUCT';
|
||||
on_tap_endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.content_image = Parser.parseItem(data.contentImage, CollectionThumbnailView);
|
||||
this.metadata = Parser.parseItem(data.metadata, LockupMetadataView);
|
||||
this.content_id = data.contentId;
|
||||
this.content_type = data.contentType.replace('LOCKUP_CONTENT_TYPE_', '');
|
||||
this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { YTNode } from '../helpers.js';
|
||||
import { isTextRun, timeToSeconds } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import type TextRun from './misc/TextRun.js';
|
||||
|
||||
import { Parser } from '../index.js';
|
||||
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.js';
|
||||
@@ -25,7 +26,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
};
|
||||
|
||||
endpoint?: NavigationEndpoint;
|
||||
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'non_music_track' | 'video' | 'song' | 'endpoint' | 'unknown' | undefined;
|
||||
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'non_music_track' | 'video' | 'song' | 'endpoint' | 'unknown' | 'podcast_show' | undefined;
|
||||
index?: Text;
|
||||
thumbnail?: MusicThumbnail | null;
|
||||
badges;
|
||||
@@ -120,6 +121,10 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
this.item_type = 'non_music_track';
|
||||
this.#parseNonMusicTrack();
|
||||
break;
|
||||
case 'MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE':
|
||||
this.item_type = 'podcast_show';
|
||||
this.#parsePodcastShow();
|
||||
break;
|
||||
default:
|
||||
if (this.flex_columns[1]) {
|
||||
this.#parseVideoOrSong();
|
||||
@@ -160,13 +165,19 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
|
||||
#parseVideoOrSong() {
|
||||
const is_video = this.flex_columns.at(1)?.title.runs?.some((run) => run.text.match(/(.*?) views/));
|
||||
if (is_video) {
|
||||
this.item_type = 'video';
|
||||
this.#parseVideo();
|
||||
} else {
|
||||
this.item_type = 'song';
|
||||
this.#parseSong();
|
||||
const music_video_type = (this.flex_columns.at(0)?.title.runs?.at(0) as TextRun)?.endpoint?.payload?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType;
|
||||
switch (music_video_type) {
|
||||
case 'MUSIC_VIDEO_TYPE_UGC':
|
||||
case 'MUSIC_VIDEO_TYPE_OMV':
|
||||
this.item_type = 'video';
|
||||
this.#parseVideo();
|
||||
break;
|
||||
case 'MUSIC_VIDEO_TYPE_ATV':
|
||||
this.item_type = 'song';
|
||||
this.#parseSong();
|
||||
break;
|
||||
default:
|
||||
this.#parseOther();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +278,11 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
}
|
||||
|
||||
#parsePodcastShow() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
}
|
||||
|
||||
#parseAlbum() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
|
||||
26
src/parser/classes/ThumbnailBadgeView.ts
Normal file
26
src/parser/classes/ThumbnailBadgeView.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class ThumbnailBadgeView extends YTNode {
|
||||
static type = 'ThumbnailBadgeView';
|
||||
|
||||
icon_name: string;
|
||||
text: string;
|
||||
badge_style: string;
|
||||
background_color: {
|
||||
light_theme: number;
|
||||
dark_theme: number;
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.icon_name = data.icon.sources[0].clientResource.imageName;
|
||||
this.text = data.text;
|
||||
this.badge_style = data.badgeStyle;
|
||||
this.background_color = {
|
||||
light_theme: data.backgroundColor.lightTheme,
|
||||
dark_theme: data.backgroundColor.darkTheme
|
||||
};
|
||||
}
|
||||
}
|
||||
19
src/parser/classes/ThumbnailHoverOverlayView.ts
Normal file
19
src/parser/classes/ThumbnailHoverOverlayView.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class ThumbnailHoverOverlayView extends YTNode {
|
||||
static type = 'ThumbnailHoverOverlayView';
|
||||
|
||||
icon_name: string;
|
||||
text: Text;
|
||||
style: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.icon_name = data.icon.sources[0].clientResource.imageName;
|
||||
this.text = Text.fromAttributed(data.text);
|
||||
this.style = data.style;
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/ThumbnailOverlayBadgeView.ts
Normal file
17
src/parser/classes/ThumbnailOverlayBadgeView.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ThumbnailBadgeView from './ThumbnailBadgeView.js';
|
||||
|
||||
export default class ThumbnailOverlayBadgeView extends YTNode {
|
||||
static type = 'ThumbnailOverlayBadgeView';
|
||||
|
||||
badges: ThumbnailBadgeView[];
|
||||
position: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.badges = Parser.parseArray(data.thumbnailBadges, ThumbnailBadgeView);
|
||||
this.position = data.position;
|
||||
}
|
||||
}
|
||||
27
src/parser/classes/ThumbnailView.ts
Normal file
27
src/parser/classes/ThumbnailView.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ThumbnailHoverOverlayView from './ThumbnailHoverOverlayView.js';
|
||||
import ThumbnailOverlayBadgeView from './ThumbnailOverlayBadgeView.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
export default class ThumbnailView extends YTNode {
|
||||
static type = 'ThumbnailView';
|
||||
|
||||
image: Thumbnail[];
|
||||
overlays: (ThumbnailOverlayBadgeView | ThumbnailHoverOverlayView)[];
|
||||
background_color: {
|
||||
light_theme: number;
|
||||
dark_theme: number;
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
this.overlays = Parser.parseArray(data.overlays, [ ThumbnailOverlayBadgeView, ThumbnailHoverOverlayView ]);
|
||||
this.background_color = {
|
||||
light_theme: data.backgroundColor.lightTheme,
|
||||
dark_theme: data.backgroundColor.darkTheme
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Parser } from '../../index.js';
|
||||
import Button from '../Button.js';
|
||||
import ContinuationItem from '../ContinuationItem.js';
|
||||
import Comment from './Comment.js';
|
||||
import CommentView from './CommentView.js';
|
||||
import CommentReplies from './CommentReplies.js';
|
||||
|
||||
import { InnertubeError } from '../../../utils/Utils.js';
|
||||
@@ -17,15 +18,20 @@ export default class CommentThread extends YTNode {
|
||||
#actions?: Actions;
|
||||
#continuation?: ContinuationItem;
|
||||
|
||||
comment: Comment | null;
|
||||
replies?: ObservedArray<Comment>;
|
||||
comment: Comment | CommentView | null;
|
||||
replies?: ObservedArray<Comment | CommentView>;
|
||||
comment_replies_data: CommentReplies | null;
|
||||
is_moderated_elq_comment: boolean;
|
||||
has_replies: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.comment = Parser.parseItem(data.comment, Comment);
|
||||
|
||||
if (Reflect.has(data, 'commentViewModel')) {
|
||||
this.comment = Parser.parseItem(data.commentViewModel, CommentView);
|
||||
} else {
|
||||
this.comment = Parser.parseItem(data.comment, Comment);
|
||||
}
|
||||
this.comment_replies_data = Parser.parseItem(data.replies, CommentReplies);
|
||||
this.is_moderated_elq_comment = data.isModeratedElqComment;
|
||||
this.has_replies = !!this.comment_replies_data;
|
||||
@@ -51,7 +57,7 @@ export default class CommentThread extends YTNode {
|
||||
if (!response.on_response_received_endpoints_memo)
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
|
||||
this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment).map((comment) => {
|
||||
this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment, CommentView).map((comment) => {
|
||||
comment.setActions(this.#actions);
|
||||
return comment;
|
||||
}));
|
||||
|
||||
88
src/parser/classes/comments/CommentView.ts
Normal file
88
src/parser/classes/comments/CommentView.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
import type Actions from '../../../core/Actions.js';
|
||||
import Author from '../misc/Author.js';
|
||||
import Text from '../misc/Text.js';
|
||||
|
||||
export default class CommentView extends YTNode {
|
||||
static type = 'CommentView';
|
||||
|
||||
#actions?: Actions;
|
||||
|
||||
comment_id: string;
|
||||
is_pinned: boolean;
|
||||
keys: {
|
||||
comment: string;
|
||||
comment_surface: string;
|
||||
toolbar_state: string;
|
||||
toolbar_surface: string;
|
||||
shared: string;
|
||||
};
|
||||
|
||||
content?: Text;
|
||||
published_time?: string;
|
||||
author_is_channel_owner?: boolean;
|
||||
like_count?: string;
|
||||
reply_count?: string;
|
||||
is_member?: boolean;
|
||||
member_badge?: {
|
||||
url: string,
|
||||
a11y: string;
|
||||
};
|
||||
author?: Author;
|
||||
|
||||
is_liked?: boolean;
|
||||
is_disliked?: boolean;
|
||||
is_hearted?: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.comment_id = data.commentId;
|
||||
this.is_pinned = !!data.pinnedText;
|
||||
|
||||
this.keys = {
|
||||
comment: data.commentKey,
|
||||
comment_surface: data.commentSurfaceKey,
|
||||
toolbar_state: data.toolbarStateKey,
|
||||
toolbar_surface: data.toolbarSurfaceKey,
|
||||
shared: data.sharedKey
|
||||
};
|
||||
}
|
||||
|
||||
applyMutations(comment?: RawNode, toolbar_state?: RawNode) {
|
||||
if (comment) {
|
||||
this.content = Text.fromAttributed(comment.properties.content);
|
||||
this.published_time = comment.properties.publishedTime;
|
||||
this.author_is_channel_owner = !!comment.author.isCreator;
|
||||
|
||||
this.like_count = comment.toolbar.likeCountNotliked ? comment.toolbar.likeCountNotliked : '0';
|
||||
this.reply_count = comment.toolbar.replyCount ? comment.toolbar.replyCount : '0';
|
||||
|
||||
this.is_member = !!comment.author.sponsorBadgeUrl;
|
||||
|
||||
if (Reflect.has(comment.author, 'sponsorBadgeUrl')) {
|
||||
this.member_badge = {
|
||||
url: comment.author.sponsorBadgeUrl,
|
||||
a11y: comment.author.A11y
|
||||
};
|
||||
}
|
||||
|
||||
this.author = new Author({
|
||||
simpleText: comment.author.displayName,
|
||||
navigationEndpoint: comment.avatar.endpoint
|
||||
}, comment.author, comment.avatar.image, comment.author.channelId);
|
||||
}
|
||||
|
||||
if (toolbar_state) {
|
||||
this.is_hearted = toolbar_state.heartState === 'TOOLBAR_HEART_STATE_HEARTED';
|
||||
this.is_liked = toolbar_state.likeState === 'TOOLBAR_LIKE_STATE_LIKED';
|
||||
this.is_disliked = toolbar_state.likeState === 'TOOLBAR_HEART_STATE_DISLIKED';
|
||||
}
|
||||
}
|
||||
|
||||
setActions(actions: Actions | undefined) {
|
||||
this.#actions = actions;
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,21 @@ export default class Author {
|
||||
this.name = nav_text?.text || 'N/A';
|
||||
this.thumbnails = thumbs ? Thumbnail.fromResponse(thumbs) : [];
|
||||
this.endpoint = ((nav_text?.runs?.[0] as TextRun) as TextRun)?.endpoint || nav_text?.endpoint;
|
||||
this.badges = Array.isArray(badges) ? Parser.parseArray(badges) : observe([] as YTNode[]);
|
||||
this.is_moderator = this.badges?.some((badge: any) => badge.icon_type == 'MODERATOR');
|
||||
this.is_verified = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED');
|
||||
this.is_verified_artist = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST');
|
||||
|
||||
if (badges) {
|
||||
if (Array.isArray(badges)) {
|
||||
this.badges = Parser.parseArray(badges);
|
||||
this.is_moderator = this.badges?.some((badge: any) => badge.icon_type == 'MODERATOR');
|
||||
this.is_verified = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED');
|
||||
this.is_verified_artist = this.badges?.some((badge: any) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST');
|
||||
} else {
|
||||
this.badges = observe([] as YTNode[]);
|
||||
this.is_verified = !!badges.isVerified;
|
||||
this.is_verified_artist = !!badges.isArtist;
|
||||
}
|
||||
} else {
|
||||
this.badges = observe([] as YTNode[]);
|
||||
}
|
||||
|
||||
// @TODO: Refactor this mess.
|
||||
this.url =
|
||||
|
||||
@@ -16,6 +16,7 @@ export default class EmojiRun implements Run {
|
||||
this.text =
|
||||
data.emoji?.emojiId ||
|
||||
data.emoji?.shortcuts?.[0] ||
|
||||
data.text ||
|
||||
'';
|
||||
|
||||
this.emoji = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Log } from '../../../utils/index.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
import NavigationEndpoint from '../NavigationEndpoint.js';
|
||||
import EmojiRun from './EmojiRun.js';
|
||||
@@ -18,6 +19,10 @@ 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';
|
||||
|
||||
export default class Text {
|
||||
text?: string;
|
||||
runs?: (EmojiRun | TextRun)[];
|
||||
@@ -46,73 +51,132 @@ export default class Text {
|
||||
}
|
||||
}
|
||||
|
||||
static fromAttributed(data: RawNode): Text {
|
||||
const runs: {
|
||||
text: string,
|
||||
navigationEndpoint?: RawNode,
|
||||
attachment?: RawNode
|
||||
}[] = [];
|
||||
static fromAttributed(data: AttributedText) {
|
||||
const {
|
||||
content,
|
||||
styleRuns: style_runs,
|
||||
commandRuns: command_runs,
|
||||
attachmentRuns: attachment_runs
|
||||
} = data;
|
||||
|
||||
const content = data.content;
|
||||
const command_runs = data.commandRuns;
|
||||
const runs: RawRun[] = [
|
||||
{
|
||||
text: content,
|
||||
startIndex: 0
|
||||
}
|
||||
];
|
||||
|
||||
// Haven't found an actually useful one yet, but they look like this:
|
||||
// [ { startIndex: 0, length: 19 } ] (for a string that is 19 characters long)
|
||||
// Const style_runs = data.styleRuns;
|
||||
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);
|
||||
|
||||
let last_end_index = 0;
|
||||
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) {
|
||||
for (const item of command_runs) {
|
||||
const length: number = item.length;
|
||||
const start_index: number = item.startIndex;
|
||||
|
||||
if (start_index > last_end_index) {
|
||||
runs.push({
|
||||
text: content.slice(last_end_index, start_index)
|
||||
});
|
||||
}
|
||||
|
||||
if (Reflect.has(item, 'onTap')) {
|
||||
let attachment = null;
|
||||
|
||||
if (Reflect.has(data, 'attachmentRuns')) {
|
||||
const attachment_runs = data.attachmentRuns;
|
||||
|
||||
for (const attatchment_run of attachment_runs) {
|
||||
if ((attatchment_run.startIndex - 2) == start_index) {
|
||||
attachment = attatchment_run;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
runs.push({
|
||||
text: content.slice(start_index, start_index + length),
|
||||
navigationEndpoint: item.onTap,
|
||||
attachment
|
||||
// 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 {
|
||||
runs.push({
|
||||
text: content.slice(start_index, start_index + length),
|
||||
navigationEndpoint: item.onTap
|
||||
Log.debug(TAG, 'Skipping style run as it is doesn\'t have any information that we parse.', {
|
||||
style_run,
|
||||
input_data: data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
last_end_index = start_index + length;
|
||||
}
|
||||
|
||||
if (last_end_index < content.length) {
|
||||
runs.push({
|
||||
text: content.slice(last_end_index)
|
||||
});
|
||||
if (command_runs) {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runs.push({
|
||||
text: content
|
||||
});
|
||||
}
|
||||
|
||||
return new Text({ runs });
|
||||
@@ -141,4 +205,100 @@ export default class Text {
|
||||
toString(): string {
|
||||
return this.text || 'N/A';
|
||||
}
|
||||
}
|
||||
|
||||
function findMatchingRun(runs: RawRun[], response_run: ResponseRun) {
|
||||
return runs.find((run) => {
|
||||
return run.startIndex <= response_run.startIndex &&
|
||||
response_run.startIndex + response_run.length <= run.startIndex + run.text.length;
|
||||
});
|
||||
}
|
||||
|
||||
function insertSubRun(runs: RawRun[], original_run: RawRun, response_run: ResponseRun, properties_to_add: Omit<RawRun, 'text' | 'startIndex'>) {
|
||||
const replace_index = runs.indexOf(original_run);
|
||||
const replacement_runs = [];
|
||||
|
||||
const offset_start_index = response_run.startIndex - original_run.startIndex;
|
||||
|
||||
// Stuff before the run
|
||||
if (response_run.startIndex > original_run.startIndex) {
|
||||
replacement_runs.push({
|
||||
...original_run,
|
||||
text: original_run.text.substring(0, offset_start_index)
|
||||
});
|
||||
}
|
||||
|
||||
replacement_runs.push({
|
||||
...original_run,
|
||||
text: original_run.text.substring(offset_start_index, offset_start_index + response_run.length),
|
||||
startIndex: response_run.startIndex,
|
||||
...properties_to_add
|
||||
});
|
||||
|
||||
// Stuff after the run
|
||||
if (response_run.startIndex + response_run.length < original_run.startIndex + original_run.text.length) {
|
||||
replacement_runs.push({
|
||||
...original_run,
|
||||
text: original_run.text.substring(offset_start_index + response_run.length),
|
||||
startIndex: response_run.startIndex + response_run.length
|
||||
});
|
||||
}
|
||||
|
||||
runs.splice(replace_index, 1, ...replacement_runs);
|
||||
}
|
||||
|
||||
interface RawRun {
|
||||
text: string,
|
||||
bold?: boolean;
|
||||
italics?: boolean;
|
||||
strikethrough?: boolean;
|
||||
navigationEndpoint?: RawNode;
|
||||
attachment?: RawNode;
|
||||
emoji?: RawNode;
|
||||
startIndex: number;
|
||||
}
|
||||
|
||||
interface AttributedText {
|
||||
content: string;
|
||||
styleRuns?: StyleRun[];
|
||||
commandRuns?: CommandRun[];
|
||||
attachmentRuns?: AttachmentRun[];
|
||||
decorationRuns?: ResponseRun[];
|
||||
}
|
||||
|
||||
interface ResponseRun {
|
||||
startIndex: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface StyleRun extends ResponseRun {
|
||||
italic?: boolean;
|
||||
weightLabel?: string;
|
||||
strikethrough?: string;
|
||||
fontFamilyName?: string;
|
||||
styleRunExtensions?: {
|
||||
styleRunColorMapExtension?: {
|
||||
colorMap?: {
|
||||
key: string,
|
||||
value: number
|
||||
}[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CommandRun extends ResponseRun {
|
||||
onTap?: RawNode;
|
||||
}
|
||||
|
||||
interface AttachmentRun extends ResponseRun {
|
||||
alignment?: string;
|
||||
element?: {
|
||||
type?: {
|
||||
imageType?: {
|
||||
image: RawNode,
|
||||
playbackState?: string;
|
||||
}
|
||||
};
|
||||
properties?: RawNode
|
||||
};
|
||||
}
|
||||
@@ -64,6 +64,7 @@ export { default as ClipCreationTextInput } from './classes/ClipCreationTextInpu
|
||||
export { default as ClipSection } from './classes/ClipSection.js';
|
||||
export { default as CollaboratorInfoCardContent } from './classes/CollaboratorInfoCardContent.js';
|
||||
export { default as CollageHeroImage } from './classes/CollageHeroImage.js';
|
||||
export { default as CollectionThumbnailView } from './classes/CollectionThumbnailView.js';
|
||||
export { default as Command } from './classes/Command.js';
|
||||
export { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge.js';
|
||||
export { default as Comment } from './classes/comments/Comment.js';
|
||||
@@ -77,6 +78,7 @@ export { default as CommentsHeader } from './classes/comments/CommentsHeader.js'
|
||||
export { default as CommentSimplebox } from './classes/comments/CommentSimplebox.js';
|
||||
export { default as CommentsSimplebox } from './classes/comments/CommentsSimplebox.js';
|
||||
export { default as CommentThread } from './classes/comments/CommentThread.js';
|
||||
export { default as CommentView } from './classes/comments/CommentView.js';
|
||||
export { default as CreatorHeart } from './classes/comments/CreatorHeart.js';
|
||||
export { default as EmojiPicker } from './classes/comments/EmojiPicker.js';
|
||||
export { default as PdgCommentChip } from './classes/comments/PdgCommentChip.js';
|
||||
@@ -210,6 +212,8 @@ export { default as LiveChatItemList } from './classes/LiveChatItemList.js';
|
||||
export { default as LiveChatMessageInput } from './classes/LiveChatMessageInput.js';
|
||||
export { default as LiveChatParticipant } from './classes/LiveChatParticipant.js';
|
||||
export { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList.js';
|
||||
export { default as LockupMetadataView } from './classes/LockupMetadataView.js';
|
||||
export { default as LockupView } from './classes/LockupView.js';
|
||||
export { default as MacroMarkersInfoItem } from './classes/MacroMarkersInfoItem.js';
|
||||
export { default as MacroMarkersList } from './classes/MacroMarkersList.js';
|
||||
export { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.js';
|
||||
@@ -367,7 +371,10 @@ export { default as Tab } from './classes/Tab.js';
|
||||
export { default as Tabbed } from './classes/Tabbed.js';
|
||||
export { default as TabbedSearchResults } from './classes/TabbedSearchResults.js';
|
||||
export { default as TextHeader } from './classes/TextHeader.js';
|
||||
export { default as ThumbnailBadgeView } from './classes/ThumbnailBadgeView.js';
|
||||
export { default as ThumbnailHoverOverlayView } from './classes/ThumbnailHoverOverlayView.js';
|
||||
export { default as ThumbnailLandscapePortrait } from './classes/ThumbnailLandscapePortrait.js';
|
||||
export { default as ThumbnailOverlayBadgeView } from './classes/ThumbnailOverlayBadgeView.js';
|
||||
export { default as ThumbnailOverlayBottomPanel } from './classes/ThumbnailOverlayBottomPanel.js';
|
||||
export { default as ThumbnailOverlayEndorsement } from './classes/ThumbnailOverlayEndorsement.js';
|
||||
export { default as ThumbnailOverlayHoverText } from './classes/ThumbnailOverlayHoverText.js';
|
||||
@@ -380,6 +387,7 @@ export { default as ThumbnailOverlayResumePlayback } from './classes/ThumbnailOv
|
||||
export { default as ThumbnailOverlaySidePanel } from './classes/ThumbnailOverlaySidePanel.js';
|
||||
export { default as ThumbnailOverlayTimeStatus } from './classes/ThumbnailOverlayTimeStatus.js';
|
||||
export { default as ThumbnailOverlayToggleButton } from './classes/ThumbnailOverlayToggleButton.js';
|
||||
export { default as ThumbnailView } from './classes/ThumbnailView.js';
|
||||
export { default as TimedMarkerDecoration } from './classes/TimedMarkerDecoration.js';
|
||||
export { default as TitleAndButtonListHeader } from './classes/TitleAndButtonListHeader.js';
|
||||
export { default as ToggleButton } from './classes/ToggleButton.js';
|
||||
|
||||
@@ -25,6 +25,7 @@ import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem.j
|
||||
import Format from './classes/misc/Format.js';
|
||||
import VideoDetails from './classes/misc/VideoDetails.js';
|
||||
import NavigationEndpoint from './classes/NavigationEndpoint.js';
|
||||
import CommentView from './classes/comments/CommentView.js';
|
||||
|
||||
import type { KeyInfo } from './generator.js';
|
||||
import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.js';
|
||||
@@ -43,7 +44,8 @@ export type ParserError = {
|
||||
classdata: RawNode,
|
||||
error: unknown
|
||||
} | {
|
||||
error_type: 'mutation_data_missing'
|
||||
error_type: 'mutation_data_missing',
|
||||
classname: string
|
||||
} | {
|
||||
error_type: 'mutation_data_invalid',
|
||||
total: number,
|
||||
@@ -108,7 +110,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError)
|
||||
case 'mutation_data_missing':
|
||||
Log.warn(TAG,
|
||||
new InnertubeError(
|
||||
'Mutation data required for processing MusicMultiSelectMenuItems, but none found.\n' +
|
||||
`Mutation data required for processing ${classname}, but none found.\n` +
|
||||
`This is a bug, please report it at ${Platform.shim.info.bugs_url}`
|
||||
)
|
||||
);
|
||||
@@ -316,6 +318,10 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
|
||||
applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
|
||||
|
||||
if (on_response_received_endpoints_memo) {
|
||||
applyCommentsMutations(on_response_received_endpoints_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
|
||||
}
|
||||
|
||||
const continuation = data.continuation ? parseC(data.continuation) : null;
|
||||
if (continuation) {
|
||||
parsed_data.continuation = continuation;
|
||||
@@ -683,3 +689,28 @@ export function applyMutations(memo: Memo, mutations: RawNode[]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCommentsMutations(memo: Memo, mutations: RawNode[]) {
|
||||
const comment_view_items = memo.getType(CommentView);
|
||||
|
||||
if (comment_view_items.length > 0) {
|
||||
if (!mutations) {
|
||||
ERROR_HANDLER({
|
||||
error_type: 'mutation_data_missing',
|
||||
classname: 'CommentView'
|
||||
});
|
||||
}
|
||||
|
||||
for (const comment_view of comment_view_items) {
|
||||
const comment_mutation = mutations
|
||||
.find((mutation) => mutation.payload?.commentEntityPayload?.key === comment_view.keys.comment)
|
||||
?.payload?.commentEntityPayload;
|
||||
|
||||
const toolbar_state_mutation = mutations
|
||||
.find((mutation) => mutation.payload?.engagementToolbarStateEntityPayload?.key === comment_view.keys.toolbar_state)
|
||||
?.payload?.engagementToolbarStateEntityPayload;
|
||||
|
||||
comment_view.applyMutations(comment_mutation, toolbar_state_mutation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
71
src/platform/cf-worker.ts
Normal file
71
src/platform/cf-worker.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ICache } from '../types/Cache.js';
|
||||
import { Platform } from '../utils/Utils.js';
|
||||
import evaluate from './jsruntime/jinter.js';
|
||||
import { $INLINE_JSON } from 'ts-transformer-inline-file';
|
||||
import sha1Hash from './polyfills/web-crypto.js';
|
||||
|
||||
const { homepage, version, bugs } = $INLINE_JSON('../../package.json');
|
||||
const repo_url = homepage?.split('#')[0];
|
||||
|
||||
class Cache implements ICache {
|
||||
#persistent_directory: string;
|
||||
#persistent: boolean;
|
||||
|
||||
constructor(persistent = false, persistent_directory?: string) {
|
||||
this.#persistent_directory = persistent_directory || '';
|
||||
this.#persistent = persistent;
|
||||
}
|
||||
|
||||
get cache_dir() {
|
||||
return this.#persistent ? this.#persistent_directory : '';
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
const cache = await caches.open('yt-api');
|
||||
|
||||
const response = await cache.match(key);
|
||||
if (!response) return undefined;
|
||||
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
async set(key: string, value: ArrayBuffer) {
|
||||
|
||||
const cache = await caches.open('yt-api');
|
||||
cache.put(key, new Response(value));
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
const cache = await caches.open('yt-api');
|
||||
|
||||
await cache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
Platform.load({
|
||||
runtime: 'cf-worker',
|
||||
info: {
|
||||
version: version,
|
||||
bugs_url: bugs?.url || `${repo_url}/issues`,
|
||||
repo_url
|
||||
},
|
||||
server: true,
|
||||
Cache: Cache,
|
||||
sha1Hash,
|
||||
uuidv4() {
|
||||
return crypto.randomUUID();
|
||||
},
|
||||
eval: evaluate,
|
||||
fetch: fetch.bind(globalThis),
|
||||
Request: Request,
|
||||
Response: Response,
|
||||
Headers: Headers,
|
||||
FormData: FormData,
|
||||
File: File,
|
||||
ReadableStream: ReadableStream,
|
||||
CustomEvent: CustomEvent
|
||||
});
|
||||
|
||||
export * from './lib.js';
|
||||
import Innertube from './lib.js';
|
||||
export default Innertube;
|
||||
@@ -56,8 +56,14 @@ class Cache implements ICache {
|
||||
const request = db.transaction('kv-store', 'readonly').objectStore('kv-store').get(key);
|
||||
request.onerror = reject;
|
||||
request.onsuccess = function () {
|
||||
const result: Uint8Array | undefined = this.result?.v;
|
||||
resolve(result ? result.buffer : undefined);
|
||||
const result: unknown = this.result?.v;
|
||||
if (result instanceof ArrayBuffer) {
|
||||
resolve(result);
|
||||
} else if (ArrayBuffer.isView(result)) {
|
||||
resolve(result.buffer);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
75
src/proto/generated/messages/youtube/(ShortsParam)/Field1.ts
Normal file
75
src/proto/generated/messages/youtube/(ShortsParam)/Field1.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
tsValueToJsonValueFns,
|
||||
jsonValueToTsValueFns,
|
||||
} from "../../../runtime/json/scalar.js";
|
||||
import {
|
||||
WireMessage,
|
||||
} from "../../../runtime/wire/index.js";
|
||||
import {
|
||||
default as serialize,
|
||||
} from "../../../runtime/wire/serialize.js";
|
||||
import {
|
||||
tsValueToWireValueFns,
|
||||
wireValueToTsValueFns,
|
||||
} from "../../../runtime/wire/scalar.js";
|
||||
import {
|
||||
default as deserialize,
|
||||
} from "../../../runtime/wire/deserialize.js";
|
||||
|
||||
export declare namespace $.youtube.ShortsParam {
|
||||
export type Field1 = {
|
||||
p1: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type Type = $.youtube.ShortsParam.Field1;
|
||||
|
||||
export function getDefaultValue(): $.youtube.ShortsParam.Field1 {
|
||||
return {
|
||||
p1: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function createValue(partialValue: Partial<$.youtube.ShortsParam.Field1>): $.youtube.ShortsParam.Field1 {
|
||||
return {
|
||||
...getDefaultValue(),
|
||||
...partialValue,
|
||||
};
|
||||
}
|
||||
|
||||
export function encodeJson(value: $.youtube.ShortsParam.Field1): unknown {
|
||||
const result: any = {};
|
||||
if (value.p1 !== undefined) result.p1 = tsValueToJsonValueFns.int32(value.p1);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function decodeJson(value: any): $.youtube.ShortsParam.Field1 {
|
||||
const result = getDefaultValue();
|
||||
if (value.p1 !== undefined) result.p1 = jsonValueToTsValueFns.int32(value.p1);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function encodeBinary(value: $.youtube.ShortsParam.Field1): Uint8Array {
|
||||
const result: WireMessage = [];
|
||||
if (value.p1 !== undefined) {
|
||||
const tsValue = value.p1;
|
||||
result.push(
|
||||
[1, tsValueToWireValueFns.int32(tsValue)],
|
||||
);
|
||||
}
|
||||
return serialize(result);
|
||||
}
|
||||
|
||||
export function decodeBinary(binary: Uint8Array): $.youtube.ShortsParam.Field1 {
|
||||
const result = getDefaultValue();
|
||||
const wireMessage = deserialize(binary);
|
||||
const wireFields = new Map(wireMessage);
|
||||
field: {
|
||||
const wireValue = wireFields.get(1);
|
||||
if (wireValue === undefined) break field;
|
||||
const value = wireValueToTsValueFns.int32(wireValue);
|
||||
if (value === undefined) break field;
|
||||
result.p1 = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type { Type as Field1 } from "./Field1.js";
|
||||
100
src/proto/generated/messages/youtube/ShortsParam.ts
Normal file
100
src/proto/generated/messages/youtube/ShortsParam.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
Type as Field1,
|
||||
encodeJson as encodeJson_1,
|
||||
decodeJson as decodeJson_1,
|
||||
encodeBinary as encodeBinary_1,
|
||||
decodeBinary as decodeBinary_1,
|
||||
} from "./(ShortsParam)/Field1.js";
|
||||
import {
|
||||
tsValueToJsonValueFns,
|
||||
jsonValueToTsValueFns,
|
||||
} from "../../runtime/json/scalar.js";
|
||||
import {
|
||||
WireMessage,
|
||||
WireType,
|
||||
} from "../../runtime/wire/index.js";
|
||||
import {
|
||||
default as serialize,
|
||||
} from "../../runtime/wire/serialize.js";
|
||||
import {
|
||||
tsValueToWireValueFns,
|
||||
wireValueToTsValueFns,
|
||||
} from "../../runtime/wire/scalar.js";
|
||||
import {
|
||||
default as deserialize,
|
||||
} from "../../runtime/wire/deserialize.js";
|
||||
|
||||
export declare namespace $.youtube {
|
||||
export type ShortsParam = {
|
||||
f1?: Field1;
|
||||
p59: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type Type = $.youtube.ShortsParam;
|
||||
|
||||
export function getDefaultValue(): $.youtube.ShortsParam {
|
||||
return {
|
||||
f1: undefined,
|
||||
p59: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function createValue(partialValue: Partial<$.youtube.ShortsParam>): $.youtube.ShortsParam {
|
||||
return {
|
||||
...getDefaultValue(),
|
||||
...partialValue,
|
||||
};
|
||||
}
|
||||
|
||||
export function encodeJson(value: $.youtube.ShortsParam): unknown {
|
||||
const result: any = {};
|
||||
if (value.f1 !== undefined) result.f1 = encodeJson_1(value.f1);
|
||||
if (value.p59 !== undefined) result.p59 = tsValueToJsonValueFns.int32(value.p59);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function decodeJson(value: any): $.youtube.ShortsParam {
|
||||
const result = getDefaultValue();
|
||||
if (value.f1 !== undefined) result.f1 = decodeJson_1(value.f1);
|
||||
if (value.p59 !== undefined) result.p59 = jsonValueToTsValueFns.int32(value.p59);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function encodeBinary(value: $.youtube.ShortsParam): Uint8Array {
|
||||
const result: WireMessage = [];
|
||||
if (value.f1 !== undefined) {
|
||||
const tsValue = value.f1;
|
||||
result.push(
|
||||
[1, { type: WireType.LengthDelimited as const, value: encodeBinary_1(tsValue) }],
|
||||
);
|
||||
}
|
||||
if (value.p59 !== undefined) {
|
||||
const tsValue = value.p59;
|
||||
result.push(
|
||||
[59, tsValueToWireValueFns.int32(tsValue)],
|
||||
);
|
||||
}
|
||||
return serialize(result);
|
||||
}
|
||||
|
||||
export function decodeBinary(binary: Uint8Array): $.youtube.ShortsParam {
|
||||
const result = getDefaultValue();
|
||||
const wireMessage = deserialize(binary);
|
||||
const wireFields = new Map(wireMessage);
|
||||
field: {
|
||||
const wireValue = wireFields.get(1);
|
||||
if (wireValue === undefined) break field;
|
||||
const value = wireValue.type === WireType.LengthDelimited ? decodeBinary_1(wireValue.value) : undefined;
|
||||
if (value === undefined) break field;
|
||||
result.f1 = value;
|
||||
}
|
||||
field: {
|
||||
const wireValue = wireFields.get(59);
|
||||
if (wireValue === undefined) break field;
|
||||
const value = wireValueToTsValueFns.int32(wireValue);
|
||||
if (value === undefined) break field;
|
||||
result.p59 = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -11,3 +11,4 @@ export type { Type as MusicSearchFilter } from "./MusicSearchFilter.js";
|
||||
export type { Type as SearchFilter } from "./SearchFilter.js";
|
||||
export type { Type as Hashtag } from "./Hashtag.js";
|
||||
export type { Type as ReelSequence } from "./ReelSequence.js";
|
||||
export type { Type as ShortsParam } from "./ShortsParam.js";
|
||||
|
||||
@@ -15,6 +15,7 @@ import * as NotificationPreferences from './generated/messages/youtube/Notificat
|
||||
import * as InnertubePayload from './generated/messages/youtube/InnertubePayload.js';
|
||||
import * as Hashtag from './generated/messages/youtube/Hashtag.js';
|
||||
import * as ReelSequence from './generated/messages/youtube/ReelSequence.js';
|
||||
import * as ShortsParam from './generated/messages/youtube/ShortsParam.js';
|
||||
|
||||
export function encodeVisitorData(id: string, timestamp: number): string {
|
||||
const buf = VisitorData.encodeBinary({ id, timestamp });
|
||||
@@ -341,4 +342,14 @@ export function encodeReelSequence(short_id: string): string {
|
||||
feature3: 0
|
||||
});
|
||||
return encodeURIComponent(u8ToBase64(buf));
|
||||
}
|
||||
|
||||
export function encodeShortsParam() {
|
||||
const buf = ShortsParam.encodeBinary({
|
||||
f1: {
|
||||
p1: 1
|
||||
},
|
||||
p59: 1
|
||||
});
|
||||
return encodeURIComponent(u8ToBase64(buf));
|
||||
}
|
||||
@@ -275,4 +275,12 @@ message ReelSequence {
|
||||
required Params params = 5;
|
||||
required int32 feature_2 = 10;
|
||||
required int32 feature_3 = 13;
|
||||
}
|
||||
|
||||
message ShortsParam {
|
||||
message Field1 {
|
||||
int32 p1 = 1;
|
||||
}
|
||||
Field1 f1 = 1;
|
||||
int32 p59 = 59;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ICacheConstructor } from './Cache.js';
|
||||
|
||||
export type Runtime = 'deno' | 'node' | 'browser' | 'unknown';
|
||||
export type Runtime = 'deno' | 'node' | 'browser' | 'cf-worker' | 'unknown';
|
||||
|
||||
export type FetchFunction = typeof fetch;
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ export const CLIENTS = Object.freeze({
|
||||
},
|
||||
ANDROID: {
|
||||
NAME: 'ANDROID',
|
||||
VERSION: '18.06.35',
|
||||
SDK_VERSION: '29',
|
||||
USER_AGENT: 'com.google.android.youtube/18.06.35 (Linux; U; Android 10; US)'
|
||||
VERSION: '18.48.37',
|
||||
SDK_VERSION: 33,
|
||||
USER_AGENT: 'com.google.android.youtube/18.48.37(Linux; U; Android 13; en_US; sdk_gphone64_x86_64 Build/UPB4.230623.005) gzip'
|
||||
},
|
||||
YTSTUDIO_ANDROID: {
|
||||
NAME: 'ANDROID_CREATOR',
|
||||
|
||||
@@ -65,7 +65,6 @@ export default class HTTPClient {
|
||||
request_headers.set('origin', request_url.origin);
|
||||
}
|
||||
|
||||
request_url.searchParams.set('key', this.#session.key);
|
||||
request_url.searchParams.set('prettyPrint', 'false');
|
||||
request_url.searchParams.set('alt', 'json');
|
||||
|
||||
@@ -96,6 +95,7 @@ export default class HTTPClient {
|
||||
if (Platform.shim.server) {
|
||||
if (n_body.context.client.clientName === 'ANDROID' || n_body.context.client.clientName === 'ANDROID_MUSIC') {
|
||||
request_headers.set('User-Agent', Constants.CLIENTS.ANDROID.USER_AGENT);
|
||||
request_headers.set('X-GOOG-API-FORMAT-VERSION', '2');
|
||||
} else if (n_body.context.client.clientName === 'iOS') {
|
||||
request_headers.set('User-Agent', Constants.CLIENTS.iOS.USER_AGENT);
|
||||
}
|
||||
@@ -103,6 +103,14 @@ export default class HTTPClient {
|
||||
|
||||
is_web_kids = n_body.context.client.clientName === 'WEB_KIDS';
|
||||
request_body = JSON.stringify(n_body);
|
||||
} else if (content_type === 'application/x-protobuf') {
|
||||
// Assume it is always an Android request.
|
||||
if (Platform.shim.server) {
|
||||
request_headers.set('User-Agent', Constants.CLIENTS.ANDROID.USER_AGENT);
|
||||
request_headers.set('X-GOOG-API-FORMAT-VERSION', '2');
|
||||
request_headers.delete('X-Youtube-Client-Version');
|
||||
request_headers.delete('X-Origin');
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate (NOTE: YouTube Kids does not support regular bearer tokens)
|
||||
@@ -113,9 +121,6 @@ export default class HTTPClient {
|
||||
await oauth.refreshIfRequired();
|
||||
|
||||
request_headers.set('authorization', `Bearer ${oauth.credentials.access_token}`);
|
||||
|
||||
// Remove API key as it is not required when using oauth.
|
||||
request_url.searchParams.delete('key');
|
||||
}
|
||||
|
||||
if (this.#cookie) {
|
||||
@@ -135,8 +140,8 @@ export default class HTTPClient {
|
||||
const response = await this.#fetch(request, {
|
||||
body: request_body,
|
||||
headers: request_headers,
|
||||
credentials: 'include',
|
||||
redirect: input instanceof Platform.shim.Request ? input.redirect : init?.redirect || 'follow'
|
||||
redirect: input instanceof Platform.shim.Request ? input.redirect : init?.redirect || 'follow',
|
||||
...(Platform.shim.runtime !== 'cf-worker' ? { credentials: 'include' } : {})
|
||||
});
|
||||
|
||||
// Check if 2xx
|
||||
@@ -156,7 +161,7 @@ export default class HTTPClient {
|
||||
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
|
||||
ctx.client.userAgent = Constants.CLIENTS.ANDROID.USER_AGENT;
|
||||
ctx.client.osName = 'Android';
|
||||
ctx.client.osVersion = '10';
|
||||
ctx.client.osVersion = '13';
|
||||
ctx.client.platform = 'MOBILE';
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createWriteStream, existsSync } from 'node:fs';
|
||||
import { Innertube, Utils, YT, YTMusic, YTNodes } from '../bundle/node.cjs';
|
||||
|
||||
describe('YouTube.js Tests', () => {
|
||||
let innertube: Innertube;
|
||||
let innertube: Innertube;
|
||||
|
||||
beforeAll(async () => {
|
||||
innertube = await Innertube.create({ generate_session_locally: true });
|
||||
@@ -19,6 +19,11 @@ describe('YouTube.js Tests', () => {
|
||||
expect(info.basic_info.id).toBe('bUHZ2k9DYHY');
|
||||
});
|
||||
|
||||
test('Innertube#getBasicInfo (Android)', async () => {
|
||||
const info = await innertube.getBasicInfo('ksEYRaIpP7A');
|
||||
expect(info.basic_info.id).toBe('ksEYRaIpP7A');
|
||||
});
|
||||
|
||||
test('Innertube#getShortsWatchItem', async () => {
|
||||
const info = await innertube.getShortsWatchItem('jOydBrmmjfk');
|
||||
expect(info.watch_next_feed?.length).toBeGreaterThan(0);
|
||||
@@ -29,7 +34,7 @@ describe('YouTube.js Tests', () => {
|
||||
expect(info.watch_next_feed?.length).toBeGreaterThan(0);
|
||||
const previousData = info.watch_next_feed?.map(value => value.as(YTNodes.Command).endpoint)
|
||||
const cont = await info.getWatchNextContinuation()
|
||||
|
||||
|
||||
expect(cont.watch_next_feed?.length).toBeGreaterThan(0);
|
||||
const newData = cont.watch_next_feed?.map(value => value.as(YTNodes.Command).endpoint)
|
||||
expect(previousData).not.toEqual(newData)
|
||||
@@ -94,7 +99,7 @@ describe('YouTube.js Tests', () => {
|
||||
let comments: YT.Comments;
|
||||
|
||||
beforeAll(async () => {
|
||||
comments = await innertube.getComments('bUHZ2k9DYHY');
|
||||
comments = await innertube.getComments('gmX-ceF-N1k');
|
||||
expect(comments).toBeDefined();
|
||||
expect(comments.header).toBeDefined();
|
||||
expect(comments.contents).toBeDefined();
|
||||
@@ -107,38 +112,32 @@ describe('YouTube.js Tests', () => {
|
||||
expect(incremental_continuation.contents.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe('CommentThread#getReplies', () => {
|
||||
let loaded_comment_thread: YTNodes.CommentThread;
|
||||
|
||||
beforeAll(async () => {
|
||||
let comment_thread = comments.contents.first();
|
||||
loaded_comment_thread = await comment_thread.getReplies();
|
||||
expect(loaded_comment_thread.replies).toBeDefined();
|
||||
});
|
||||
|
||||
test('CommentThread#getContinuation', async () => {
|
||||
const incremental_continuation = await loaded_comment_thread.getContinuation();
|
||||
expect(incremental_continuation.replies).toBeDefined();
|
||||
expect(incremental_continuation.replies?.length).toBeGreaterThan(0);
|
||||
});
|
||||
test('CommentThread#getReplies', async () => {
|
||||
let comment_thread = comments.contents.first();
|
||||
let loaded_comment_thread = await comment_thread.getReplies();
|
||||
expect(loaded_comment_thread.replies).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Innertube#getHomeFeed', () => {
|
||||
let home_feed: YT.HomeFeed;
|
||||
test('Innertube#getHomeFeed', async () => {
|
||||
const home_feed = await innertube.getHomeFeed();
|
||||
expect(home_feed).toBeDefined();
|
||||
expect(home_feed.contents).toBeDefined();
|
||||
expect(home_feed.contents.contents?.length).toBeGreaterThan(0);
|
||||
|
||||
beforeAll(async () => {
|
||||
home_feed = await innertube.getHomeFeed();
|
||||
expect(home_feed).toBeDefined();
|
||||
expect(home_feed.contents).toBeDefined();
|
||||
expect(home_feed.contents.contents?.length).toBeGreaterThan(0);
|
||||
});
|
||||
// YouTube tells anonymous users to sign in or search something first before showing them a valid home feed.
|
||||
// Otherwise, you get the following message:
|
||||
//
|
||||
// "Try searching to get started
|
||||
// Start watching videos to help us build a feed of videos you'll love"
|
||||
//
|
||||
// - Removing this test for now.
|
||||
|
||||
test('HomeFeed#getContinuation', async () => {
|
||||
const incremental_continuation = await home_feed.getContinuation();
|
||||
expect(incremental_continuation.contents).toBeDefined();
|
||||
expect(incremental_continuation.contents.contents?.length).toBeGreaterThan(0);
|
||||
});
|
||||
// test('HomeFeed#getContinuation', async () => {
|
||||
// const incremental_continuation = await home_feed.getContinuation();
|
||||
// expect(incremental_continuation.contents).toBeDefined();
|
||||
// expect(incremental_continuation.contents.contents?.length).toBeGreaterThan(0);
|
||||
// });
|
||||
});
|
||||
|
||||
test('Innertube#getGuide', async () => {
|
||||
@@ -335,7 +334,7 @@ describe('YouTube.js Tests', () => {
|
||||
expect(incremental_continuation.sections).toBeDefined();
|
||||
expect(incremental_continuation.sections?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
|
||||
test('HomeFeed#applyFilter', async () => {
|
||||
home = await home.applyFilter(home.filters[1]);
|
||||
expect(home).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user