Compare commits

..

12 Commits

Author SHA1 Message Date
LuanRT
944f821d9a chore: v8.2.0 release 2024-01-08 23:47:23 +00:00
LuanRT
bd9ae29de3 chore: v8.1.0 release 2023-12-27 02:25:21 +00:00
LuanRT
1b159e5162 chore: v8.0.0 release 2023-12-01 03:55:35 +00:00
LuanRT
2b58b3887c chore: v7.0.0 release 2023-10-28 18:23:22 +00:00
LuanRT
2294bf7065 chore: v6.4.1 release 2023-10-02 03:05:14 +00:00
LuanRT
f2d673ac95 chore: v6.4.0 release 2023-09-10 05:09:20 +00:00
LuanRT
1b557f25e3 chore: v6.3.0 release 2023-08-31 23:42:13 +00:00
LuanRT
20f055b7ea chore: v6.2.0 release 2023-08-29 17:26:54 +00:00
LuanRT
09321408e5 chore: v6.1.0 release 2023-08-27 19:44:15 +00:00
LuanRT
848c7e68e2 chore: v6.0.2 release 2023-08-24 20:07:16 +00:00
LuanRT
bc437015e2 chore: v6.0.1 release 2023-08-22 12:09:04 +00:00
LuanRT
96e337a1d0 chore: v6.0.0 release 2023-08-18 11:40:24 +00:00
334 changed files with 6322 additions and 3035 deletions

View File

@@ -171,7 +171,7 @@ import dashjs from 'dashjs';
const youtube = await Innertube.create({ /* setup - see above */ });
// get the video info
// Get the video info
const videoInfo = await youtube.getInfo('videoId');
// now convert to a dash manifest
@@ -191,7 +191,7 @@ const player = dashjs.MediaPlayer().create();
player.initialize(videoElement, uri, true);
```
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web). Alternatively, you can view it live at [ytjsexample.pages.dev](https://ytjsexample.pages.dev/).
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web).
<a name="custom-fetch"></a>
@@ -325,6 +325,9 @@ Retrieves video info.
- `<info>#download(options)`
- Downloads the video. See [download](#download).
- `<info>#getTranscript()`
- Retrieves the video's transcript.
- `<info>#filters`
- Returns filters that can be applied to the watch next feed.
@@ -788,7 +791,7 @@ We are immensely grateful to all the wonderful people who have contributed to th
## Contact
LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
LuanRT - [@thesciencephile][twitter] - luanrt@thatsciencephile.com
Project Link: [https://github.com/LuanRT/YouTube.js][project]

View File

@@ -1,3 +1,3 @@
export * from './deno/src/platform/deno.ts';
import Innertube from './deno/src/platform/deno.ts';
export default Innertube;
export default Innertube;

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "5.8.0",
"version": "8.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",
@@ -56,7 +56,8 @@
"Wykerd (https://github.com/wykerd/)",
"MasterOfBob777 (https://github.com/MasterOfBob777)",
"patrickkfkan (https://github.com/patrickkfkan)",
"akkadaska (https://github.com/akkadaska)"
"akkadaska (https://github.com/akkadaska)",
"Absidue (https://github.com/absidue)"
],
"directories": {
"test": "./test",
@@ -68,10 +69,10 @@
"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:parser-map": "node ./scripts/build-parser-map.cjs",
"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 tsc",
"build:deno": "npx cpy ./src ./deno && npx cpy ./package.json ./deno && npx replace \".ts';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'https://esm.sh/linkedom';\" \"'https://esm.sh/linkedom';\" ./deno -r && npx replace \"'https://esm.sh/jintr';\" \"'https://esm.sh/jintr';\" ./deno -r && npx replace \"new Jinter\" \"new Jinter\" ./deno -r",
"build:esm": "npx tspc",
"build:deno": "npx cpy ./src ./deno && npx esbuild ./src/utils/DashManifest.tsx --keep-names --format=esm --platform=neutral --target=es2020 --outfile=./deno/src/utils/DashManifest.js && npx cpy ./package.json ./deno && npx replace \".ts';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'./DashManifest.js';\" \"'./DashManifest.js';\" ./deno -r && npx replace \"'https://esm.sh/jintr';\" \"'https://esm.sh/jintr';\" ./deno -r",
"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",
@@ -85,11 +86,14 @@
"license": "MIT",
"dependencies": {
"jintr": "^1.1.0",
"linkedom": "^0.14.12",
"tslib": "^2.5.0",
"undici": "^5.19.1"
},
"overrides": {
"typescript": "^5.0.0"
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/jest": "^28.1.7",
"@types/node": "^17.0.45",
"@typescript-eslint/eslint-plugin": "^5.30.6",
@@ -103,6 +107,8 @@
"pbkit": "^0.0.59",
"replace": "^1.2.2",
"ts-jest": "^28.0.8",
"ts-patch": "^3.0.2",
"ts-transformer-inline-file": "^0.2.0",
"typescript": "^5.0.0"
},
"bugs": {

View File

@@ -14,12 +14,13 @@ import NotificationsMenu from './parser/youtube/NotificationsMenu.ts';
import Playlist from './parser/youtube/Playlist.ts';
import Search from './parser/youtube/Search.ts';
import VideoInfo from './parser/youtube/VideoInfo.ts';
import ShortsVideoInfo from './parser/ytshorts/VideoInfo.ts';
import { Kids, Music, Studio } from './core/clients/index.ts';
import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.ts';
import { Feed, TabbedFeed } from './core/mixins/index.ts';
import Proto from './proto/index.ts';
import * as Proto from './proto/index.ts';
import * as Constants from './utils/Constants.ts';
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts';
@@ -30,19 +31,21 @@ import {
NextEndpoint,
PlayerEndpoint,
ResolveURLEndpoint,
SearchEndpoint
SearchEndpoint,
Reel
} from './core/endpoints/index.ts';
import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.ts';
import type { ApiResponse } from './core/Actions.ts';
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.ts';
import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.ts';
import type { INextRequest } from './types/index.ts';
import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.ts';
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.ts';
import { encodeReelSequence } from './proto/index.ts';
export type InnertubeConfig = SessionOptions;
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS'
export type InnerTubeClient = 'WEB' | 'iOS' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS';
export type SearchFilters = Partial<{
upload_date: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
@@ -131,6 +134,32 @@ export default class Innertube {
return new VideoInfo([ response ], this.actions, cpn);
}
/**
* Retrieves shorts info.
* @param short_id - The short id.
* @param client - The client to use.
*/
async getShortsWatchItem(short_id: string, client?: InnerTubeClient): Promise<ShortsVideoInfo> {
throwIfMissing({ short_id });
const watchResponse = this.actions.execute(
Reel.WatchEndpoint.PATH, Reel.WatchEndpoint.build({
short_id: short_id,
client: client
})
);
const sequenceResponse = this.actions.execute(
Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({
sequenceParams: encodeReelSequence(short_id)
})
);
const response = await Promise.all([ watchResponse, sequenceResponse ]);
return new ShortsVideoInfo(response, this.actions);
}
/**
* Searches a given query.
* @param query - The search query.
@@ -251,6 +280,16 @@ export default class Innertube {
return new Feed(this.actions, response);
}
/**
* Retrieves channels feed.
*/
async getChannelsFeed(): Promise<Feed<IBrowseResponse>> {
const response = await this.actions.execute(
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEchannels' }), parse: true }
);
return new Feed(this.actions, response);
}
/**
* Retrieves contents for a given channel.
* @param id - Channel id

View File

@@ -1,4 +1,4 @@
import Parser, { NavigateAction } from '../parser/index.ts';
import { Parser, NavigateAction } from '../parser/index.ts';
import { InnertubeError } from '../utils/Utils.ts';
import type Session from './Session.ts';
@@ -16,7 +16,7 @@ export interface ApiResponse {
data: IRawResponse;
}
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/updated_metadata' | '/notification/get_notification_menu' | string;
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/reel' | '/updated_metadata' | '/notification/get_notification_menu' | string;
export type ParsedResponse<T> =
T extends '/player' ? IPlayerResponse :
@@ -167,6 +167,7 @@ export default class Actions {
'FElibrary',
'FEhistory',
'FEsubscriptions',
'FEchannels',
'FEmusic_listening_review',
'FEmusic_library_landing',
'SPaccount_overview',

View File

@@ -2,6 +2,9 @@ import * as Constants from '../utils/Constants.ts';
import { OAuthError, Platform } from '../utils/Utils.ts';
import type Session from './Session.ts';
/**
* Represents the credentials used for authentication.
*/
export interface Credentials {
/**
* Token used to sign in.
@@ -15,6 +18,14 @@ export interface Credentials {
* Access token's expiration date, which is usually 24hrs-ish.
*/
expires: Date;
/**
* Optional client ID.
*/
client_id?: string;
/**
* Optional client secret.
*/
client_secret?: string;
}
// TODO: actual type info for this.
@@ -28,6 +39,11 @@ export type OAuthAuthEventHandler = (data: {
export type OAuthAuthPendingEventHandler = (data: OAuthAuthPendingData) => any;
export type OAuthAuthErrorEventHandler = (err: OAuthError) => any;
export type OAuthClientIdentity = {
client_id: string;
client_secret: string;
};
export default class OAuth {
#identity?: Record<string, string>;
#session: Session;
@@ -71,6 +87,8 @@ export default class OAuth {
this.#credentials = {
access_token: credentials.access_token,
refresh_token: credentials.refresh_token,
client_id: credentials.client_id,
client_secret: credentials.client_secret,
expires: new Date(credentials.expires)
};
@@ -157,6 +175,8 @@ export default class OAuth {
this.#credentials = {
access_token: response_data.access_token,
refresh_token: response_data.refresh_token,
client_id: this.#identity?.client_id,
client_secret: this.#identity?.client_secret,
expires: expiration_date
};
@@ -206,6 +226,8 @@ export default class OAuth {
this.#credentials = {
access_token: response_data.access_token,
refresh_token: response_data.refresh_token || this.#credentials.refresh_token,
client_id: this.#identity.client_id,
client_secret: this.#identity.client_secret,
expires: expiration_date
};
@@ -226,7 +248,14 @@ export default class OAuth {
/**
* Retrieves client identity from YouTube TV.
*/
async #getClientIdentity(): Promise<{ [key: string]: string; }> {
async #getClientIdentity(): Promise<OAuthClientIdentity> {
if (this.#credentials?.client_id && this.credentials?.client_secret) {
return {
client_id: this.#credentials.client_id,
client_secret: this.credentials.client_secret
};
}
const response = await this.#session.http.fetch_function(new URL('/tv', Constants.URLS.YT_BASE), { headers: Constants.OAUTH.HEADERS });
const response_data = await response.text();
@@ -241,7 +270,7 @@ export default class OAuth {
.replace(/\n/g, '')
.match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);
const groups = client_identity?.groups;
const groups = client_identity?.groups as OAuthClientIdentity | null;
if (!groups)
throw new OAuthError('Could not obtain client identity.', { status: 'FAILED' });

View File

@@ -66,7 +66,7 @@ export default class Player {
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
}
decipher(url?: string, signature_cipher?: string, cipher?: string): string {
decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map<string, string>): string {
url = url || signature_cipher || cipher;
if (!url)
@@ -93,15 +93,23 @@ export default class Player {
const n = url_components.searchParams.get('n');
if (n) {
const nsig = Platform.shim.eval(this.#nsig_sc, {
nsig: n
});
let nsig;
if (typeof nsig !== 'string')
throw new PlayerError('Failed to decipher nsig');
if (this_response_nsig_cache && this_response_nsig_cache.has(n)) {
nsig = this_response_nsig_cache.get(n) as string;
} else {
nsig = Platform.shim.eval(this.#nsig_sc, {
nsig: n
});
if (nsig.startsWith('enhanced_except_')) {
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
if (typeof nsig !== 'string')
throw new PlayerError('Failed to decipher nsig');
if (nsig.startsWith('enhanced_except_')) {
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
} else if (this_response_nsig_cache) {
this_response_nsig_cache.set(n, nsig);
}
}
url_components.searchParams.set('n', nsig);

View File

@@ -3,7 +3,7 @@ import EventEmitterLike from '../utils/EventEmitterLike.ts';
import Actions from './Actions.ts';
import Player from './Player.ts';
import Proto from '../proto/index.ts';
import * as Proto from '../proto/index.ts';
import type { ICache } from '../types/Cache.ts';
import type { FetchFunction } from '../types/PlatformShim.ts';
import HTTPClient from '../utils/HTTPClient.ts';
@@ -16,6 +16,7 @@ export enum ClientType {
WEB = 'WEB',
KIDS = 'WEB_KIDS',
MUSIC = 'WEB_REMIX',
IOS = 'iOS',
ANDROID = 'ANDROID',
ANDROID_MUSIC = 'ANDROID_MUSIC',
ANDROID_CREATOR = 'ANDROID_CREATOR',
@@ -62,6 +63,7 @@ export interface Context {
user: {
enableSafetyMode: boolean;
lockedSafetyMode: boolean;
onBehalfOfUser?: string;
};
thirdParty?: {
embedUrl: string;
@@ -83,6 +85,10 @@ export interface SessionOptions {
* Only works if you are signed in with cookies.
*/
account_index?: number;
/**
* Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles.
*/
on_behalf_of_user?: string;
/**
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
* **NOTE:** Deciphering formats is not possible without the JS player.
@@ -192,7 +198,8 @@ export default class Session extends EventEmitterLike {
options.device_category,
options.client_type,
options.timezone,
options.fetch
options.fetch,
options.on_behalf_of_user
);
return new Session(
@@ -212,14 +219,22 @@ export default class Session extends EventEmitterLike {
device_category: DeviceCategory = 'desktop',
client_name: ClientType = ClientType.WEB,
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
fetch: FetchFunction = Platform.shim.fetch
fetch: FetchFunction = Platform.shim.fetch,
on_behalf_of_user?: string
) {
let session_data: SessionData;
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
if (generate_session_locally) {
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
session_data = this.#generateSessionData(session_args);
} else {
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
try {
// This can fail if the data changes or the request is blocked for some reason.
session_data = await this.#retrieveSessionData(session_args, fetch);
} catch (err) {
session_data = this.#generateSessionData(session_args);
}
}
return { ...session_data, account_index };
@@ -233,6 +248,7 @@ export default class Session extends EventEmitterLike {
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user?: string;
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
@@ -292,7 +308,8 @@ export default class Session extends EventEmitterLike {
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
lockedSafetyMode: false,
onBehalfOfUser: options.on_behalf_of_user
}
};
@@ -307,6 +324,7 @@ export default class Session extends EventEmitterLike {
client_name: string;
enable_safety_mode: boolean;
visitor_data: string;
on_behalf_of_user?: string;
}): SessionData {
let visitor_id = generateRandomString(11);
@@ -339,7 +357,8 @@ export default class Session extends EventEmitterLike {
},
user: {
enableSafetyMode: options.enable_safety_mode,
lockedSafetyMode: false
lockedSafetyMode: false,
onBehalfOfUser: options.on_behalf_of_user
}
};

View File

@@ -1,16 +1,22 @@
import { Parser } from '../../parser/index.ts';
import Channel from '../../parser/ytkids/Channel.ts';
import HomeFeed from '../../parser/ytkids/HomeFeed.ts';
import Search from '../../parser/ytkids/Search.ts';
import VideoInfo from '../../parser/ytkids/VideoInfo.ts';
import type Session from '../Session.ts';
import { type ApiResponse } from '../Actions.ts';
import { generateRandomString } from '../../utils/Utils.ts';
import { InnertubeError, generateRandomString } from '../../utils/Utils.ts';
import {
BrowseEndpoint, NextEndpoint,
PlayerEndpoint, SearchEndpoint
} from '../endpoints/index.ts';
import { BlocklistPickerEndpoint } from '../endpoints/kids/index.ts';
import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.ts';
export default class Kids {
#session: Session;
@@ -80,4 +86,38 @@ export default class Kids {
);
return new HomeFeed(this.#session.actions, response);
}
/**
* Retrieves the list of supervised accounts that the signed-in user has
* access to, and blocks the given channel for each of them.
* @param channel_id - The channel id to block.
* @returns A list of API responses.
*/
async blockChannel(channel_id: string): Promise<ApiResponse[]> {
if (!this.#session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const blocklist_payload = BlocklistPickerEndpoint.build({ channel_id: channel_id });
const response = await this.#session.actions.execute(BlocklistPickerEndpoint.PATH, blocklist_payload );
const popup = response.data.command.confirmDialogEndpoint;
const popup_fragment = { contents: popup.content, engagementPanels: [] };
const kid_picker = Parser.parseResponse(popup_fragment);
const kids = kid_picker.contents_memo?.getType(KidsBlocklistPickerItem);
if (!kids)
throw new InnertubeError('Could not find any kids profiles or supervised accounts.');
// Iterate through the kids and block the channel if not already blocked.
const responses: ApiResponse[] = [];
for (const kid of kids) {
if (!kid.block_button?.is_toggled) {
kid.setActions(this.#session.actions);
// Block channel and add to the response list.
responses.push(await kid.blockChannel());
}
}
return responses;
}
}

View File

@@ -18,9 +18,9 @@ import PlaylistPanel from '../../parser/classes/PlaylistPanel.ts';
import SearchSuggestionsSection from '../../parser/classes/SearchSuggestionsSection.ts';
import SectionList from '../../parser/classes/SectionList.ts';
import Tab from '../../parser/classes/Tab.ts';
import Proto from '../../proto/index.ts';
import * as Proto from '../../proto/index.ts';
import type { ObservedArray, YTNode } from '../../parser/helpers.ts';
import type { ObservedArray } from '../../parser/helpers.ts';
import type { MusicSearchFilters } from '../../types/index.ts';
import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.ts';
import type Actions from '../Actions.ts';
@@ -329,7 +329,7 @@ export default class Music {
if (!page.contents)
throw new InnertubeError('Unexpected response', page);
if (page.contents.item().key('type').string() === 'Message')
if (page.contents.item().type === 'Message')
throw new InnertubeError(page.contents.item().as(Message).text.toString(), video_id);
const section_list = page.contents.item().as(SectionList).contents;
@@ -355,17 +355,17 @@ export default class Music {
* Retrieves search suggestions for the given query.
* @param query - The query.
*/
async getSearchSuggestions(query: string): Promise<ObservedArray<YTNode>> {
async getSearchSuggestions(query: string): Promise<ObservedArray<SearchSuggestionsSection>> {
const response = await this.#actions.execute(
GetSearchSuggestionsEndpoint.PATH,
{ ...GetSearchSuggestionsEndpoint.build({ input: query }), parse: true }
);
if (!response.contents_memo)
throw new InnertubeError('Unexpected response', response);
return [] as unknown as ObservedArray<SearchSuggestionsSection>;
const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection).first();
const search_suggestions_sections = response.contents_memo.getType(SearchSuggestionsSection);
return search_suggestions_section.contents;
return search_suggestions_sections;
}
}

View File

@@ -1,4 +1,4 @@
import Proto from '../../proto/index.ts';
import * as Proto from '../../proto/index.ts';
import * as Constants from '../../utils/Constants.ts';
import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.ts';

View File

@@ -15,7 +15,9 @@ export function build(opts: EditPlaylistEndpointOptions): IEditPlaylistRequest {
...{
addedVideoId: action.added_video_id,
setVideoId: action.set_video_id,
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor,
playlistDescription: action.playlist_description,
playlistName: action.playlist_name
}
}))
};

View File

@@ -15,4 +15,6 @@ export * as Music from './music/index.ts';
export * as Notification from './notification/index.ts';
export * as Playlist from './playlist/index.ts';
export * as Subscription from './subscription/index.ts';
export * as Upload from './upload/index.ts';
export * as Reel from './reel/index.ts';
export * as Upload from './upload/index.ts';
export * as Kids from './kids/index.ts';

View File

@@ -0,0 +1,12 @@
import type { IBlocklistPickerRequest, BlocklistPickerRequestEndpointOptions } from '../../../types/index.ts';
export const PATH = '/kids/get_kids_blocklist_picker';
/**
* Builds a `/kids/get_kids_blocklist_picker` request payload.
* @param options - The options to use.
* @returns The payload.
*/
export function build(options: BlocklistPickerRequestEndpointOptions): IBlocklistPickerRequest {
return { blockedForKidsContent: { external_channel_id: options.channel_id } };
}

View File

@@ -0,0 +1 @@
export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.ts';

View File

@@ -0,0 +1,18 @@
import type { IReelWatchRequest, ReelWatchEndpointOptions } from '../../../types/index.ts';
export const PATH = '/reel/reel_item_watch';
/**
* Builds a `/reel/reel_watch_sequence` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: ReelWatchEndpointOptions): IReelWatchRequest {
return {
playerRequest: {
videoId: opts.short_id,
params: opts.params ?? 'CAUwAg%3D%3D'
},
params: opts.params ?? 'CAUwAg%3D%3D'
};
}

View File

@@ -0,0 +1,14 @@
import type { IReelSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.ts';
export const PATH = '/reel/reel_watch_sequence';
/**
* Builds a `/reel/reel_watch_sequence` request payload.
* @param opts - The options to use.
* @returns The payload.
*/
export function build(opts: ReelWatchSequenceEndpointOptions): IReelSequenceRequest {
return {
sequenceParams: opts.sequenceParams
};
}

View File

@@ -0,0 +1,2 @@
export * as WatchEndpoint from './WatchEndpoint.ts';
export * as WatchSequenceEndpoint from './WatchSequenceEndpoint.ts';

View File

@@ -3,7 +3,7 @@ import Analytics from '../../parser/youtube/Analytics.ts';
import Settings from '../../parser/youtube/Settings.ts';
import TimeWatched from '../../parser/youtube/TimeWatched.ts';
import Proto from '../../proto/index.ts';
import * as Proto from '../../proto/index.ts';
import { InnertubeError } from '../../utils/Utils.ts';
import { Account, BrowseEndpoint, Channel } from '../endpoints/index.ts';

View File

@@ -1,4 +1,4 @@
import Proto from '../../proto/index.ts';
import * as Proto from '../../proto/index.ts';
import type Actions from '../Actions.ts';
import type { ApiResponse } from '../Actions.ts';

View File

@@ -200,4 +200,60 @@ export default class PlaylistManager {
action_result: response.data.actions // TODO: implement actions in the parser
};
}
}
/**
* Sets the name (title) for the given playlist.
* @param playlist_id - The playlist ID.
* @param name - The name / title to use for the playlist.
*/
async setName(playlist_id: string, name: string): Promise<{ playlist_id: string; action_result: any; }> {
throwIfMissing({ playlist_id, name });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
payload.actions.push({
action: 'ACTION_SET_PLAYLIST_NAME',
playlist_name: name
});
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
);
return {
playlist_id,
action_result: response.data.actions
};
}
/**
* Sets the description for the given playlist.
* @param playlist_id - The playlist ID.
* @param description - The description to use for the playlist.
*/
async setDescription(playlist_id: string, description: string): Promise<{ playlist_id: string; action_result: any; }> {
throwIfMissing({ playlist_id, description });
if (!this.#actions.session.logged_in)
throw new InnertubeError('You must be signed in to perform this operation.');
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
payload.actions.push({
action: 'ACTION_SET_PLAYLIST_DESCRIPTION',
playlist_description: description
});
const response = await this.#actions.execute(
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
);
return {
playlist_id,
action_result: response.data.actions
};
}
}

View File

@@ -1,5 +1,5 @@
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../../parser/helpers.ts';
import Parser, { ReloadContinuationItemsCommand } from '../../parser/index.ts';
import { Parser, ReloadContinuationItemsCommand } from '../../parser/index.ts';
import { concatMemos, InnertubeError } from '../../utils/Utils.ts';
import type Actions from '../Actions.ts';
@@ -177,7 +177,7 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
* Checks if the feed has continuation.
*/
get has_continuation(): boolean {
return (this.#memo.get('ContinuationItem') || []).length > 0;
return this.#getBodyContinuations().length > 0;
}
/**
@@ -185,17 +185,15 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
*/
async getContinuationData(): Promise<T | undefined> {
if (this.#continuation) {
if (this.#continuation.length > 1)
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
if (this.#continuation.length === 0)
throw new InnertubeError('There are no continuations');
throw new InnertubeError('There are no continuations.');
const response = await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });
return response;
}
this.#continuation = this.#memo.getType(ContinuationItem);
this.#continuation = this.#getBodyContinuations();
if (this.#continuation)
return this.getContinuationData();
@@ -210,4 +208,14 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
throw new InnertubeError('Could not get continuation data');
return new Feed<T>(this.actions, continuation_data, true);
}
#getBodyContinuations(): ObservedArray<ContinuationItem> {
if (this.#page.header_memo) {
const header_continuations = this.#page.header_memo.getType(ContinuationItem);
return this.#memo.getType(ContinuationItem).filter((continuation) => !header_continuations.includes(continuation)) as ObservedArray<ContinuationItem>;
}
return this.#memo.getType(ContinuationItem);
}
}

View File

@@ -1,14 +1,17 @@
import type { ApiResponse } from '../Actions.ts';
import type Actions from '../Actions.ts';
import * as Constants from '../../utils/Constants.ts';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.ts';
import FormatUtils from '../../utils/FormatUtils.ts';
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.ts';
import * as FormatUtils from '../../utils/FormatUtils.ts';
import { InnertubeError } from '../../utils/Utils.ts';
import type Format from '../../parser/classes/misc/Format.ts';
import type { INextResponse, IPlayerResponse } from '../../parser/index.ts';
import Parser from '../../parser/index.ts';
import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.ts';
import { Parser } from '../../parser/index.ts';
import type { DashOptions } from '../../types/DashOptions.ts';
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.ts';
import { getStreamingInfo } from '../../utils/StreamingInfo.ts';
import ContinuationItem from '../../parser/classes/ContinuationItem.ts';
import TranscriptInfo from '../../parser/youtube/TranscriptInfo.ts';
export default class MediaInfo {
#page: [IPlayerResponse, INextResponse?];
@@ -17,6 +20,7 @@ export default class MediaInfo {
#playback_tracking;
streaming_data;
playability_status;
player_config: IPlayerConfig;
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
this.#actions = actions;
@@ -32,6 +36,7 @@ export default class MediaInfo {
this.streaming_data = info.streaming_data;
this.playability_status = info.playability_status;
this.player_config = info.player_config;
this.#playback_tracking = info.playback_tracking;
}
@@ -43,15 +48,36 @@ export default class MediaInfo {
* @returns DASH manifest
*/
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
const player_response = this.#page[0];
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
throw new InnertubeError('Generating DASH manifests for live and Post-Live-DVR videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
}
let storyboards;
if (options.include_thumbnails && this.#page[0].storyboards?.is(PlayerStoryboardSpec)) {
storyboards = this.#page[0].storyboards;
if (options.include_thumbnails && player_response.storyboards?.is(PlayerStoryboardSpec)) {
storyboards = player_response.storyboards;
}
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
}
/**
* Get a cleaned up representation of the adaptive_formats
*/
getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter) {
return getStreamingInfo(
this.streaming_data,
url_transformer,
format_filter,
this.cpn,
this.#actions.session.player,
this.#actions,
this.#page[0].storyboards?.is(PlayerStoryboardSpec) ? this.#page[0].storyboards : undefined
);
}
/**
* Selects the format that best matches the given options.
* @param options - Options
@@ -65,9 +91,45 @@ export default class MediaInfo {
* @param options - Download options.
*/
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
const player_response = this.#page[0];
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
throw new InnertubeError('Downloading is not supported for live and Post-Live-DVR videos, as they are split up into 5 second segments that are individual files, which require using a tool such as ffmpeg to stitch them together, so they cannot be returned in a single stream.');
}
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
}
/**
* Retrieves the video's transcript.
* @param video_id - The video id.
*/
async getTranscript(): Promise<TranscriptInfo> {
const next_response = this.page[1];
if (!next_response)
throw new InnertubeError('Cannot get transcript from basic video info.');
if (!next_response.engagement_panels)
throw new InnertubeError('Engagement panels not found. Video likely has no transcript.');
const transcript_panel = next_response.engagement_panels.get({
panel_identifier: 'engagement-panel-searchable-transcript'
});
if (!transcript_panel)
throw new InnertubeError('Transcript panel not found. Video likely has no transcript.');
const transcript_continuation = transcript_panel.content?.as(ContinuationItem);
if (!transcript_continuation)
throw new InnertubeError('Transcript continuation not found.');
const response = await transcript_continuation.endpoint.call(this.actions);
return new TranscriptInfo(this.actions, response);
}
/**
* Adds video to the watch history.
*/

View File

@@ -310,7 +310,7 @@ const example_data = {
// The first argument is the name of the class, the second is the data you have for the node.
// It will return a class that extends YTNode.
const Example = Generator.YTNodeGenerator.generateRuntimeClass('Example', example_data);
const Example = Generator.generateRuntimeClass('Example', example_data);
// You may now use this class as you would any other node.
const example = new Example(example_data);

View File

@@ -0,0 +1,18 @@
import { YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import AboutChannelView from './AboutChannelView.ts';
import Button from './Button.ts';
export default class AboutChannel extends YTNode {
static type = 'AboutChannel';
metadata: AboutChannelView | null;
share_channel: Button | null;
constructor(data: RawNode) {
super();
this.metadata = Parser.parseItem(data.metadata, AboutChannelView);
this.share_channel = Parser.parseItem(data.shareChannel, Button);
}
}

View File

@@ -0,0 +1,87 @@
import type { ObservedArray } from '../helpers.ts';
import { YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import ChannelExternalLinkView from './ChannelExternalLinkView.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';
export default class AboutChannelView extends YTNode {
static type = 'AboutChannelView';
description?: string;
description_label?: Text;
country?: string;
custom_links_label?: Text;
subscriber_count?: string;
view_count?: string;
joined_date?: Text;
canonical_channel_url?: string;
channel_id?: string;
additional_info_label?: Text;
custom_url_on_tap?: NavigationEndpoint;
video_count?: string;
sign_in_for_business_email?: Text;
links: ObservedArray<ChannelExternalLinkView>;
constructor(data: RawNode) {
super();
if (Reflect.has(data, 'description')) {
this.description = data.description;
}
if (Reflect.has(data, 'descriptionLabel')) {
this.description_label = Text.fromAttributed(data.descriptionLabel);
}
if (Reflect.has(data, 'country')) {
this.country = data.country;
}
if (Reflect.has(data, 'customLinksLabel')) {
this.custom_links_label = Text.fromAttributed(data.customLinksLabel);
}
if (Reflect.has(data, 'subscriberCountText')) {
this.subscriber_count = data.subscriberCountText;
}
if (Reflect.has(data, 'viewCountText')) {
this.view_count = data.viewCountText;
}
if (Reflect.has(data, 'joinedDateText')) {
this.joined_date = Text.fromAttributed(data.joinedDateText);
}
if (Reflect.has(data, 'canonicalChannelUrl')) {
this.canonical_channel_url = data.canonicalChannelUrl;
}
if (Reflect.has(data, 'channelId')) {
this.channel_id = data.channelId;
}
if (Reflect.has(data, 'additionalInfoLabel')) {
this.additional_info_label = Text.fromAttributed(data.additionalInfoLabel);
}
if (Reflect.has(data, 'customUrlOnTap')) {
this.custom_url_on_tap = new NavigationEndpoint(data.customUrlOnTap);
}
if (Reflect.has(data, 'videoCountText')) {
this.video_count = data.videoCountText;
}
if (Reflect.has(data, 'signInForBusinessEmail')) {
this.sign_in_for_business_email = Text.fromAttributed(data.signInForBusinessEmail);
}
if (Reflect.has(data, 'links')) {
this.links = Parser.parseArray(data.links, ChannelExternalLinkView);
} else {
this.links = [] as unknown as ObservedArray<ChannelExternalLinkView>;
}
}
}

View File

@@ -1,4 +1,4 @@
import Parser from '../index.ts';
import { Parser } from '../index.ts';
import AccountItemSectionHeader from './AccountItemSectionHeader.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';

View File

@@ -1,4 +1,4 @@
import Parser from '../index.ts';
import { Parser } from '../index.ts';
import AccountChannel from './AccountChannel.ts';
import AccountItemSection from './AccountItemSection.ts';

View File

@@ -0,0 +1,19 @@
import Button from './Button.ts';
import Text from './misc/Text.ts';
import { YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
export default class AlertWithButton extends YTNode {
static type = 'AlertWithButton';
text: Text;
alert_type: string;
dismiss_button: Button | null;
constructor(data: RawNode) {
super();
this.text = new Text(data.text);
this.alert_type = data.type;
this.dismiss_button = Parser.parseItem(data.dismissButton, Button);
}
}

View File

@@ -0,0 +1,30 @@
import { YTNode } from '../helpers.ts';
import { type RawNode } from '../index.ts';
import { Thumbnail } from '../misc.ts';
export default class AvatarView extends YTNode {
static type = 'AvatarView';
image: {
sources: Thumbnail[],
processor: {
border_image_processor: {
circular: boolean
}
}
};
avatar_image_size: string;
constructor(data: RawNode) {
super();
this.image = {
sources: data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width),
processor: {
border_image_processor: {
circular: data.image.processor.borderImageProcessor.circular
}
}
};
this.avatar_image_size = data.avatarImageSize;
}
}

View File

@@ -1,5 +1,6 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import CommentActionButtons from './comments/CommentActionButtons.ts';
import Menu from './menus/Menu.ts';
@@ -18,7 +19,7 @@ export default class BackstagePost extends YTNode {
vote_count?: Text;
menu?: Menu | null;
action_buttons?: CommentActionButtons | null;
vote_button?: CommentActionButtons | null;
vote_button?: Button | null;
surface: string;
endpoint?: NavigationEndpoint;
attachment;
@@ -56,7 +57,7 @@ export default class BackstagePost extends YTNode {
}
if (Reflect.has(data, 'voteButton')) {
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
this.vote_button = Parser.parseItem(data.voteButton, Button);
}
if (Reflect.has(data, 'navigationEndpoint')) {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { YTNode } from '../helpers.ts';
export default class BackstagePostThread extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
export default class BrowseFeedActions extends YTNode {

View File

@@ -0,0 +1,28 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
export default class ButtonView extends YTNode {
static type = 'ButtonView';
icon_name: string;
title: string;
accessibility_text: string;
style: string;
is_full_width: boolean;
type: string;
button_size: string;
on_tap: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.icon_name = data.iconName;
this.title = data.title;
this.accessibility_text = data.accessibilityText;
this.style = data.style;
this.is_full_width = data.isFullWidth;
this.type = data.type;
this.button_size = data.buttonSize;
this.on_tap = new NavigationEndpoint(data.onTap);
}
}

View File

@@ -1,7 +1,9 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import ChannelHeaderLinks from './ChannelHeaderLinks.ts';
import ChannelHeaderLinksView from './ChannelHeaderLinksView.ts';
import ChannelTagline from './ChannelTagline.ts';
import SubscribeButton from './SubscribeButton.ts';
import Author from './misc/Author.ts';
import Text from './misc/Text.ts';
@@ -18,9 +20,10 @@ export default class C4TabbedHeader extends YTNode {
videos_count?: Text;
sponsor_button?: Button | null;
subscribe_button?: SubscribeButton | Button | null;
header_links?: ChannelHeaderLinks | null;
header_links?: ChannelHeaderLinks | ChannelHeaderLinksView | null;
channel_handle?: Text;
channel_id?: string;
tagline?: ChannelTagline | null;
constructor(data: RawNode) {
super();
@@ -58,7 +61,7 @@ export default class C4TabbedHeader extends YTNode {
}
if (Reflect.has(data, 'headerLinks')) {
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
this.header_links = Parser.parseItem(data.headerLinks, [ ChannelHeaderLinks, ChannelHeaderLinksView ]);
}
if (Reflect.has(data, 'channelHandleText')) {
@@ -68,5 +71,9 @@ export default class C4TabbedHeader extends YTNode {
if (Reflect.has(data, 'channelId')) {
this.channel_id = data.channelId;
}
if (Reflect.has(data, 'tagline')) {
this.tagline = Parser.parseItem(data.tagline, ChannelTagline);
}
}
}

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { YTNode } from '../helpers.ts';
export default class Card extends YTNode {

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Text from './misc/Text.ts';
export default class CardCollection extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
export default class CarouselHeader extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
import Thumbnail from './misc/Thumbnail.ts';

View File

@@ -1,6 +1,6 @@
import { type ObservedArray, YTNode } from '../helpers.ts';
import InfoRow from './InfoRow.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import CompactVideo from './CompactVideo.ts';
export default class CarouselLockup extends YTNode {

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import SubscribeButton from './SubscribeButton.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';

View File

@@ -0,0 +1,20 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';
export default class ChannelExternalLinkView extends YTNode {
static type = 'ChannelExternalLinkView';
title: Text;
link: Text;
favicon: Thumbnail[];
constructor(data: RawNode) {
super();
this.title = Text.fromAttributed(data.title);
this.link = Text.fromAttributed(data.link);
this.favicon = Thumbnail.fromResponse(data.favicon);
}
}

View File

@@ -1,5 +1,5 @@
import { type ObservedArray, YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Text from './misc/Text.ts';
export default class ChannelFeaturedContent extends YTNode {

View File

@@ -0,0 +1,22 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import Text from './misc/Text.ts';
export default class ChannelHeaderLinksView extends YTNode {
static type = 'ChannelHeaderLinksView';
first_link?: Text;
more?: Text;
constructor(data: RawNode) {
super();
if (Reflect.has(data, 'firstLink')) {
this.first_link = Text.fromAttributed(data.firstLink);
}
if (Reflect.has(data, 'more')) {
this.more = Text.fromAttributed(data.more);
}
}
}

View File

@@ -14,6 +14,7 @@ export default class ChannelMetadata extends YTNode {
is_family_safe: boolean;
keywords: string[];
avatar: Thumbnail[];
music_artist_name?: string;
available_countries: string[];
android_deep_link: string;
android_appindexing_link: string;
@@ -30,6 +31,8 @@ export default class ChannelMetadata extends YTNode {
this.is_family_safe = data.isFamilySafe;
this.keywords = data.keywords;
this.avatar = Thumbnail.fromResponse(data.avatar);
// Can be an empty string sometimes, so we need the extra length check
this.music_artist_name = typeof data.musicArtistName === 'string' && data.musicArtistName.length > 0 ? data.musicArtistName : undefined;
this.available_countries = data.availableCountryCodes;
this.android_deep_link = data.androidDeepLink;
this.android_appindexing_link = data.androidAppindexingLink;

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';
export default class ChannelOwnerEmptyState extends YTNode {
static type = 'ChannelOwnerEmptyState';
illustration: Thumbnail[];
description: Text;
constructor(data: RawNode) {
super();
this.illustration = Thumbnail.fromResponse(data.illustration);
this.description = new Text(data.description);
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
export default class ChannelSubMenu extends YTNode {

View File

@@ -0,0 +1,44 @@
import { YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import EngagementPanelSectionList from './EngagementPanelSectionList.ts';
export default class ChannelTagline extends YTNode {
static type = 'ChannelTagline';
content: string;
max_lines: number;
more_endpoint: {
show_engagement_panel_endpoint: {
engagement_panel: EngagementPanelSectionList | null,
engagement_panel_popup_type: string;
identifier: {
surface: string,
tag: string
}
}
} | NavigationEndpoint;
more_icon_type: string;
more_label: string;
target_id: string;
constructor(data: RawNode) {
super();
this.content = data.content;
this.max_lines = data.maxLines;
this.more_endpoint = data.moreEndpoint.showEngagementPanelEndpoint ? {
show_engagement_panel_endpoint: {
engagement_panel: Parser.parseItem(data.moreEndpoint.showEngagementPanelEndpoint.engagementPanel, EngagementPanelSectionList),
engagement_panel_popup_type: data.moreEndpoint.showEngagementPanelEndpoint.engagementPanelPresentationConfigs.engagementPanelPopupPresentationConfig.popupType,
identifier: {
surface: data.moreEndpoint.showEngagementPanelEndpoint.identifier.surface,
tag: data.moreEndpoint.showEngagementPanelEndpoint.identifier.tag
}
}
} : new NavigationEndpoint(data.moreEndpoint);
this.more_icon_type = data.moreIcon.iconType;
this.more_label = data.moreLabel;
this.target_id = data.targetId;
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import ChipCloudChip from './ChipCloudChip.ts';

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.ts';
import Text from './misc/Text.ts';
import type { RawNode } from '../types/index.ts';
export default class ClipAdState extends YTNode {
static type = 'ClipAdState';
title: Text;
body: Text;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.body = new Text(data.body);
}
}

View File

@@ -0,0 +1,40 @@
import { YTNode } from '../helpers.ts';
import Thumbnail from './misc/Thumbnail.ts';
import Button from './Button.ts';
import ClipCreationTextInput from './ClipCreationTextInput.ts';
import ClipCreationScrubber from './ClipCreationScrubber.ts';
import ClipAdState from './ClipAdState.ts';
import Text from './misc/Text.ts';
import { Parser } from '../index.ts';
import type { RawNode } from '../types/index.ts';
export default class ClipCreation extends YTNode {
static type = 'ClipCreation';
user_avatar: Thumbnail[];
title_input: ClipCreationTextInput | null;
scrubber: ClipCreationScrubber | null;
save_button: Button | null;
display_name: Text;
publicity_label: string;
cancel_button: Button | null;
ad_state_overlay: ClipAdState | null;
external_video_id: string;
publicity_label_icon: string;
constructor(data: RawNode) {
super();
this.user_avatar = Thumbnail.fromResponse(data.userAvatar);
this.title_input = Parser.parseItem(data.titleInput, [ ClipCreationTextInput ]);
this.scrubber = Parser.parseItem(data.scrubber, [ ClipCreationScrubber ]);
this.save_button = Parser.parseItem(data.saveButton, [ Button ]);
this.display_name = new Text(data.displayName);
this.publicity_label = data.publicityLabel;
this.cancel_button = Parser.parseItem(data.cancelButton, [ Button ]);
this.ad_state_overlay = Parser.parseItem(data.adStateOverlay, [ ClipAdState ]);
this.external_video_id = data.externalVideoId;
this.publicity_label_icon = data.publicityLabelIcon;
}
}

View File

@@ -0,0 +1,28 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../types/index.ts';
export default class ClipCreationScrubber extends YTNode {
static type = 'ClipCreationScrubber';
length_template: string;
max_length_ms: number;
min_length_ms: number;
default_length_ms: number;
window_size_ms: number;
start_label?: string;
end_label?: string;
duration_label?: string;
constructor(data: RawNode) {
super();
this.length_template = data.lengthTemplate;
this.max_length_ms = data.maxLengthMs;
this.min_length_ms = data.minLengthMs;
this.default_length_ms = data.defaultLengthMs;
this.window_size_ms = data.windowSizeMs;
this.start_label = data.startAccessibility?.accessibilityData?.label;
this.end_label = data.endAccessibility?.accessibilityData?.label;
this.duration_label = data.durationAccessibility?.accessibilityData?.label;
}
}

View File

@@ -0,0 +1,17 @@
import { YTNode } from '../helpers.ts';
import Text from './misc/Text.ts';
import type { RawNode } from '../types/index.ts';
export default class ClipCreationTextInput extends YTNode {
static type = 'ClipCreationTextInput';
placeholder_text: Text;
max_character_limit: number;
constructor(data: RawNode) {
super();
this.placeholder_text = new Text(data.placeholderText);
this.max_character_limit = data.maxCharacterLimit;
}
}

View File

@@ -0,0 +1,19 @@
import type { ObservedArray } from '../helpers.ts';
import { YTNode } from '../helpers.ts';
import ClipCreation from './ClipCreation.ts';
import { Parser } from '../index.ts';
import type { RawNode } from '../types/index.ts';
export default class ClipSection extends YTNode {
static type = 'ClipSection';
contents: ObservedArray<ClipCreation> | null;
constructor(data: RawNode) {
super();
this.contents = Parser.parse(data.contents, true, [ ClipCreation ]);
}
}

View File

@@ -0,0 +1,14 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
export default class Command extends YTNode {
static type = 'Command';
endpoint: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.endpoint = new NavigationEndpoint(data);
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Menu from './menus/Menu.ts';
import Text from './misc/Text.ts';

View File

@@ -0,0 +1,57 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import { Parser } from '../index.ts';
import Author from './misc/Author.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';
import Menu from './menus/Menu.ts';
import { timeToSeconds } from '../../utils/Utils.ts';
export default class CompactMovie extends YTNode {
static type = 'CompactMovie';
id: string;
title: Text;
top_metadata_items: Text;
thumbnails: Thumbnail[];
thumbnail_overlays: ObservedArray<YTNode>;
author: Author;
duration: {
text: string;
seconds: number;
};
endpoint: NavigationEndpoint;
badges: ObservedArray<YTNode>;
use_vertical_poster: boolean;
menu: Menu | null;
constructor(data: RawNode) {
super();
const overlay_time_status = data.thumbnailOverlays
.find((overlay: RawNode) => overlay.thumbnailOverlayTimeStatusRenderer)
?.thumbnailOverlayTimeStatusRenderer.text || 'N/A';
this.id = data.videoId;
this.title = new Text(data.title);
this.top_metadata_items = new Text(data.topMetadataItems);
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
this.author = new Author(data.shortBylineText);
const durationText = data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString();
this.duration = {
text: durationText,
seconds: timeToSeconds(durationText)
};
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.badges = Parser.parseArray(data.badges);
this.use_vertical_poster = data.useVerticalPoster;
this.menu = Parser.parseItem(data.menu, Menu);
}
}

View File

@@ -1,6 +1,6 @@
import { timeToSeconds } from '../../utils/Utils.ts';
import { YTNode, type ObservedArray, type SuperParsedResult } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Menu from './menus/Menu.ts';
import MetadataBadge from './MetadataBadge.ts';
import Author from './misc/Author.ts';

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Text from './misc/Text.ts';
import Button from './Button.ts';
import { YTNode } from '../helpers.ts';

View File

@@ -0,0 +1,26 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import { Text } from '../misc.ts';
export type MetadataRow = {
metadata_parts: {
text: Text;
}[];
};
export default class ContentMetadataView extends YTNode {
static type = 'ContentMetadataView';
metadata_rows: MetadataRow[];
delimiter: string;
constructor(data: RawNode) {
super();
this.metadata_rows = data.metadataRows.map((row: RawNode) => ({
metadata_parts: row.metadataParts.map((part: RawNode) => ({
text: Text.fromAttributed(part.text)
}))
}));
this.delimiter = data.delimiter;
}
}

View File

@@ -10,7 +10,7 @@ export default class ContentPreviewImageView extends YTNode {
constructor(data: RawNode) {
super();
this.image = data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width);
this.image = Thumbnail.fromResponse(data.image);
this.style = data.style;
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Message from './Message.ts';
export default class ConversationBar extends YTNode {

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
export default class CopyLink extends YTNode {

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import Dropdown from './Dropdown.ts';
import Text from './misc/Text.ts';

View File

@@ -0,0 +1,19 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import AvatarView from './AvatarView.ts';
export default class DecoratedAvatarView extends YTNode {
static type = 'DecoratedAvatarView';
avatar: AvatarView;
a11y_label: string;
on_tap_endpoint: NavigationEndpoint;
constructor(data: RawNode) {
super();
this.avatar = new AvatarView(data.avatar.avatarViewModel);
this.a11y_label = data.a11yLabel;
this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap.innertubeCommand);
}
}

View File

@@ -1,6 +1,6 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import Parser from '../index.ts';
import { Parser } from '../index.ts';
import Button from './Button.ts';
import MultiMarkersPlayerBar from './MultiMarkersPlayerBar.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import ToggleButtonView from './ToggleButtonView.ts';
export default class DislikeButtonView extends YTNode {
static type = 'DislikeButtonView';
toggle_button: ToggleButtonView | null;
dislike_entity_key: string;
constructor(data: RawNode) {
super();
this.toggle_button = Parser.parseItem(data.toggleButtonViewModel, ToggleButtonView);
this.dislike_entity_key = data.dislikeEntityKey;
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import DropdownItem from './DropdownItem.ts';
export default class Dropdown extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import ChildElement from './misc/ChildElement.ts';
import { type ObservedArray, YTNode, observe } from '../helpers.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Menu from './menus/Menu.ts';
import Text from './misc/Text.ts';

View File

@@ -1,5 +1,5 @@
import { type ObservedArray, YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Author from './misc/Author.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';

View File

@@ -1,4 +1,4 @@
import Parser from '../index.ts';
import { Parser } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
export default class Endscreen extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Thumbnail from './misc/Thumbnail.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';

View File

@@ -1,26 +1,37 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import ClipSection from './ClipSection.ts';
import ContinuationItem from './ContinuationItem.ts';
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.ts';
import MacroMarkersList from './MacroMarkersList.ts';
import ProductList from './ProductList.ts';
import SectionList from './SectionList.ts';
import StructuredDescriptionContent from './StructuredDescriptionContent.ts';
import VideoAttributeView from './VideoAttributeView.ts';
export default class EngagementPanelSectionList extends YTNode {
static type = 'EngagementPanelSectionList';
header: EngagementPanelTitleHeader | null;
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | null;
content: VideoAttributeView | SectionList | ContinuationItem | ClipSection | StructuredDescriptionContent | MacroMarkersList | ProductList | null;
target_id?: string;
panel_identifier?: string;
identifier?: {
surface: string,
tag: string
};
visibility?: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList ]);
this.content = Parser.parseItem(data.content, [ VideoAttributeView, SectionList, ContinuationItem, ClipSection, StructuredDescriptionContent, MacroMarkersList, ProductList ]);
this.panel_identifier = data.panelIdentifier;
this.identifier = data.identifier ? {
surface: data.identifier.surface,
tag: data.identifier.tag
} : undefined;
this.target_id = data.targetId;
this.visibility = data.visibility;
}
}
}

View File

@@ -1,7 +1,8 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Button from './Button.ts';
import HorizontalCardList from './HorizontalCardList.ts';
import HorizontalList from './HorizontalList.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';
@@ -15,7 +16,7 @@ export default class ExpandableMetadata extends YTNode {
expanded_title: Text;
};
expanded_content: HorizontalCardList | null;
expanded_content: HorizontalCardList | HorizontalList | null;
expand_button: Button | null;
collapse_button: Button | null;
@@ -31,7 +32,7 @@ export default class ExpandableMetadata extends YTNode {
};
}
this.expanded_content = Parser.parseItem(data.expandedContent, HorizontalCardList);
this.expanded_content = Parser.parseItem(data.expandedContent, [ HorizontalCardList, HorizontalList ]);
this.expand_button = Parser.parseItem(data.expandButton, Button);
this.collapse_button = Parser.parseItem(data.collapseButton, Button);
}

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
export default class ExpandableTab extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
export default class ExpandedShelfContents extends YTNode {

View File

@@ -0,0 +1,16 @@
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import { Text } from '../misc.ts';
export default class FancyDismissibleDialog extends YTNode {
static type = 'FancyDismissibleDialog';
dialog_message: Text;
confirm_label: Text;
constructor(data: RawNode) {
super();
this.dialog_message = new Text(data.dialogMessage);
this.confirm_label = new Text(data.confirmLabel);
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import ChipCloudChip from './ChipCloudChip.ts';
export default class FeedFilterChipBar extends YTNode {

View File

@@ -0,0 +1,26 @@
import { YTNode } from '../helpers.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';
import type { RawNode } from '../types/index.ts';
export default class FeedNudge extends YTNode {
static type = 'FeedNudge';
title: Text;
subtitle: Text;
endpoint: NavigationEndpoint;
apply_modernized_style: boolean;
trim_style: string;
background_style: string;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.subtitle = new Text(data.subtitle);
this.endpoint = new NavigationEndpoint(data.impressionEndpoint);
this.apply_modernized_style = data.applyModernizedStyle;
this.trim_style = data.trimStyle;
this.background_style = data.backgroundStyle;
}
}

View File

@@ -0,0 +1,22 @@
import { type ObservedArray, YTNode } from '../helpers.ts';
import { Parser, type RawNode } from '../index.ts';
import ButtonView from './ButtonView.ts';
export type ActionRow = {
actions: ObservedArray<ButtonView>;
};
export default class FlexibleActionsView extends YTNode {
static type = 'FlexibleActionsView';
actions_rows: ActionRow[];
style: string;
constructor(data: RawNode) {
super();
this.actions_rows = data.actionsRows.map((row: RawNode) => ({
actions: Parser.parseArray(row.actions, ButtonView)
}));
this.style = data.style;
}
}

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
export default class GameCard extends YTNode {
static type = 'GameCard';

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
export default class Grid extends YTNode {
static type = 'Grid';

View File

@@ -1,5 +1,5 @@
import { YTNode } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Author from './misc/Author.ts';
import Text from './misc/Text.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import MetadataBadge from './MetadataBadge.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Text from './misc/Text.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import Author from './misc/Author.ts';
import Text from './misc/Text.ts';
import Thumbnail from './misc/Thumbnail.ts';

View File

@@ -1,6 +1,6 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import type { RawNode } from '../index.ts';
import Parser from '../parser.ts';
import * as Parser from '../parser.ts';
import Author from './misc/Author.ts';
import Text from './misc/Text.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';

View File

@@ -1,5 +1,5 @@
import { YTNode, type ObservedArray } from '../helpers.ts';
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Menu from './menus/Menu.ts';
import Author from './misc/Author.ts';

View File

@@ -1,4 +1,4 @@
import Parser from '../parser.ts';
import * as Parser from '../parser.ts';
import GuideEntry from './GuideEntry.ts';
import type { RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';

View File

@@ -1,4 +1,4 @@
import Parser from '../parser.ts';
import * as Parser from '../parser.ts';
import type { RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';

View File

@@ -1,5 +1,5 @@
import Text from './misc/Text.ts';
import Parser from '../parser.ts';
import * as Parser from '../parser.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import HeatMarker from './HeatMarker.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';

View File

@@ -1,4 +1,5 @@
import NavigationEndpoint from './NavigationEndpoint.ts';
import Thumbnail from './misc/Thumbnail.ts';
import { YTNode, observe } from '../helpers.ts';
import { type RawNode } from '../index.ts';
@@ -6,11 +7,7 @@ export class Panel extends YTNode {
static type = 'Panel';
thumbnail?: {
image: {
url: string;
width: number;
height: number;
}[];
image: Thumbnail[];
endpoint: NavigationEndpoint;
on_long_press_endpoint: NavigationEndpoint;
content_mode: string;
@@ -18,16 +15,8 @@ export class Panel extends YTNode {
};
background_image: {
image: {
url: string;
width: number;
height: number;
}[];
gradient_image: {
url: string;
width: number;
height: number;
}[];
image: Thumbnail[];
gradient_image: Thumbnail[];
};
strapline: string;
@@ -48,7 +37,7 @@ export class Panel extends YTNode {
if (data.thumbnail) {
this.thumbnail = {
image: data.thumbnail.image.sources,
image: Thumbnail.fromResponse(data.thumbnail.image),
endpoint: new NavigationEndpoint(data.thumbnail.onTap),
on_long_press_endpoint: new NavigationEndpoint(data.thumbnail.onLongPress),
content_mode: data.thumbnail.contentMode,
@@ -57,8 +46,8 @@ export class Panel extends YTNode {
}
this.background_image = {
image: data.backgroundImage.image.sources,
gradient_image: data.backgroundImage.gradientImage.sources
image: Thumbnail.fromResponse(data.backgroundImage.image),
gradient_image: Thumbnail.fromResponse(data.backgroundImage.gradientImage)
};
this.strapline = data.strapline;

View File

@@ -1,22 +1,23 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
import SearchRefinementCard from './SearchRefinementCard.ts';
import Button from './Button.ts';
import MacroMarkersListItem from './MacroMarkersListItem.ts';
import GameCard from './GameCard.ts';
import VideoCard from './VideoCard.ts';
import VideoAttributeView from './VideoAttributeView.ts';
export default class HorizontalCardList extends YTNode {
static type = 'HorizontalCardList';
cards: ObservedArray<SearchRefinementCard | MacroMarkersListItem | GameCard | VideoCard>;
cards: ObservedArray<VideoAttributeView | SearchRefinementCard | MacroMarkersListItem | GameCard | VideoCard>;
header: YTNode;
previous_button: Button | null;
next_button: Button | null;
constructor(data: RawNode) {
super();
this.cards = Parser.parseArray(data.cards, [ SearchRefinementCard, MacroMarkersListItem, GameCard, VideoCard ]);
this.cards = Parser.parseArray(data.cards, [ VideoAttributeView, SearchRefinementCard, MacroMarkersListItem, GameCard, VideoCard ]);
this.header = Parser.parseItem(data.header);
this.previous_button = Parser.parseItem(data.previousButton, Button);
this.next_button = Parser.parseItem(data.nextButton, Button);

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
export default class HorizontalList extends YTNode {

View File

@@ -1,4 +1,4 @@
import Parser, { type RawNode } from '../index.ts';
import { Parser, type RawNode } from '../index.ts';
import { type ObservedArray, YTNode } from '../helpers.ts';
import Button from './Button.ts';

Some files were not shown because too many files have changed in this diff Show More