mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-19 12:31:17 +00:00
Compare commits
10 Commits
v6.2.0-den
...
v9.0.2-den
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
016a7aacae | ||
|
|
bd5b3470ec | ||
|
|
648488641d | ||
|
|
944f821d9a | ||
|
|
bd9ae29de3 | ||
|
|
1b159e5162 | ||
|
|
2b58b3887c | ||
|
|
2294bf7065 | ||
|
|
f2d673ac95 | ||
|
|
1b557f25e3 |
@@ -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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "6.2.0",
|
||||
"version": "9.0.2",
|
||||
"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",
|
||||
@@ -71,7 +71,7 @@
|
||||
"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/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: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",
|
||||
@@ -89,6 +89,9 @@
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/jest": "^28.1.7",
|
||||
@@ -104,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": {
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
import type { SessionOptions } from './core/Session.ts';
|
||||
import Session from './core/Session.ts';
|
||||
|
||||
import NavigationEndpoint from './parser/classes/NavigationEndpoint.ts';
|
||||
import type Format from './parser/classes/misc/Format.ts';
|
||||
import Channel from './parser/youtube/Channel.ts';
|
||||
import Comments from './parser/youtube/Comments.ts';
|
||||
import Guide from './parser/youtube/Guide.ts';
|
||||
import HashtagFeed from './parser/youtube/HashtagFeed.ts';
|
||||
import History from './parser/youtube/History.ts';
|
||||
import HomeFeed from './parser/youtube/HomeFeed.ts';
|
||||
import Library from './parser/youtube/Library.ts';
|
||||
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 { 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 * as Proto from './proto/index.ts';
|
||||
import * as Constants from './utils/Constants.ts';
|
||||
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts';
|
||||
|
||||
import {
|
||||
BrowseEndpoint,
|
||||
GetNotificationMenuEndpoint,
|
||||
@@ -30,15 +10,40 @@ import {
|
||||
NextEndpoint,
|
||||
PlayerEndpoint,
|
||||
ResolveURLEndpoint,
|
||||
SearchEndpoint
|
||||
SearchEndpoint,
|
||||
Reel,
|
||||
Notification
|
||||
} from './core/endpoints/index.ts';
|
||||
|
||||
import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.ts';
|
||||
import {
|
||||
Channel,
|
||||
Comments,
|
||||
Guide,
|
||||
HashtagFeed,
|
||||
History,
|
||||
HomeFeed,
|
||||
Library,
|
||||
NotificationsMenu,
|
||||
Playlist,
|
||||
Search,
|
||||
VideoInfo
|
||||
} from './parser/youtube/index.ts';
|
||||
|
||||
import { VideoInfo as ShortsVideoInfo } from './parser/ytshorts/index.ts';
|
||||
|
||||
import NavigationEndpoint from './parser/classes/NavigationEndpoint.ts';
|
||||
|
||||
import * as Proto from './proto/index.ts';
|
||||
import * as Constants from './utils/Constants.ts';
|
||||
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts';
|
||||
|
||||
|
||||
import type { ApiResponse } from './core/Actions.ts';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.ts';
|
||||
import type { INextRequest } from './types/index.ts';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.ts';
|
||||
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.ts';
|
||||
import type { SessionOptions } from './core/Session.ts';
|
||||
import type Format from './parser/classes/misc/Format.ts';
|
||||
|
||||
export type InnertubeConfig = SessionOptions;
|
||||
|
||||
@@ -131,6 +136,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: Proto.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.
|
||||
@@ -232,7 +263,7 @@ export default class Innertube {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves trending content.
|
||||
* Retrieves Trending content.
|
||||
*/
|
||||
async getTrending(): Promise<TabbedFeed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute(
|
||||
@@ -242,7 +273,7 @@ export default class Innertube {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves subscriptions feed.
|
||||
* Retrieves Subscriptions feed.
|
||||
*/
|
||||
async getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute(
|
||||
@@ -251,6 +282,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
|
||||
@@ -279,7 +320,7 @@ export default class Innertube {
|
||||
* Retrieves unseen notifications count.
|
||||
*/
|
||||
async getUnseenNotificationsCount(): Promise<number> {
|
||||
const response = await this.actions.execute(GetUnseenCountEndpoint.PATH);
|
||||
const response = await this.actions.execute(Notification.GetUnseenCountEndpoint.PATH);
|
||||
// TODO: properly parse this
|
||||
return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
import type { Session } from './index.ts';
|
||||
|
||||
import type {
|
||||
IBrowseResponse, IGetNotificationsMenuResponse,
|
||||
@@ -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',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as Constants from '../utils/Constants.ts';
|
||||
import { Log, Constants } from '../utils/index.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,7 +39,14 @@ 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 {
|
||||
static TAG = 'OAuth';
|
||||
|
||||
#identity?: Record<string, string>;
|
||||
#session: Session;
|
||||
#credentials?: Credentials;
|
||||
@@ -71,6 +89,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 +177,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 +228,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 +250,15 @@ 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) {
|
||||
Log.info(OAuth.TAG, 'Using custom OAuth2 credentials.\n');
|
||||
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();
|
||||
@@ -235,17 +267,21 @@ export default class OAuth {
|
||||
if (!url_body)
|
||||
throw new OAuthError('Could not obtain script url.', { status: 'FAILED' });
|
||||
|
||||
Log.info(OAuth.TAG, `Got YouTubeTV script URL (${url_body})`);
|
||||
|
||||
const script = await this.#session.http.fetch(url_body, { baseURL: Constants.URLS.YT_BASE });
|
||||
|
||||
const client_identity = (await script.text())
|
||||
.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' });
|
||||
|
||||
Log.info(OAuth.TAG, 'OAuth2 credentials retrieved.\n', groups);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Log, Constants } from '../utils/index.ts';
|
||||
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.ts';
|
||||
|
||||
import * as Constants from '../utils/Constants.ts';
|
||||
|
||||
import type { ICache } from '../types/Cache.ts';
|
||||
import type { FetchFunction } from '../types/PlatformShim.ts';
|
||||
import type { ICache, FetchFunction } from '../types/index.ts';
|
||||
|
||||
/**
|
||||
* Represents YouTube's player script. This is required to decipher signatures.
|
||||
*/
|
||||
export default class Player {
|
||||
static TAG = 'Player';
|
||||
|
||||
#nsig_sc;
|
||||
#sig_sc;
|
||||
#sig_sc_timestamp;
|
||||
@@ -17,9 +16,7 @@ export default class Player {
|
||||
constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) {
|
||||
this.#nsig_sc = nsig_sc;
|
||||
this.#sig_sc = sig_sc;
|
||||
|
||||
this.#sig_sc_timestamp = signature_timestamp;
|
||||
|
||||
this.#player_id = player_id;
|
||||
}
|
||||
|
||||
@@ -34,11 +31,14 @@ export default class Player {
|
||||
|
||||
const player_id = getStringBetweenStrings(js, 'player\\/', '\\/');
|
||||
|
||||
Log.info(Player.TAG, `Got player id (${player_id}). Checking for cached players..`);
|
||||
|
||||
if (!player_id)
|
||||
throw new PlayerError('Failed to get player id');
|
||||
|
||||
// We have the playerID now we can check if we have a cached player
|
||||
// We have the player id, now we can check if we have a cached player.
|
||||
if (cache) {
|
||||
Log.info(Player.TAG, 'Found a cached player.');
|
||||
const cached_player = await Player.fromCache(cache, player_id);
|
||||
if (cached_player)
|
||||
return cached_player;
|
||||
@@ -46,6 +46,8 @@ export default class Player {
|
||||
|
||||
const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE);
|
||||
|
||||
Log.info(Player.TAG, `Could not find any cached player. Will download a new player from ${player_url}.`);
|
||||
|
||||
const player_res = await fetch(player_url, {
|
||||
headers: {
|
||||
'user-agent': getRandomUserAgent('desktop')
|
||||
@@ -59,14 +61,15 @@ export default class Player {
|
||||
const player_js = await player_res.text();
|
||||
|
||||
const sig_timestamp = this.extractSigTimestamp(player_js);
|
||||
|
||||
const sig_sc = this.extractSigSourceCode(player_js);
|
||||
const nsig_sc = this.extractNSigSourceCode(player_js);
|
||||
|
||||
Log.info(Player.TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`);
|
||||
|
||||
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)
|
||||
@@ -80,6 +83,8 @@ export default class Player {
|
||||
sig: args.get('s')
|
||||
});
|
||||
|
||||
Log.info(Player.TAG, `Transformed signature ${args.get('s')} to ${signature}.`);
|
||||
|
||||
if (typeof signature !== 'string')
|
||||
throw new PlayerError('Failed to decipher signature');
|
||||
|
||||
@@ -93,15 +98,25 @@ 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!');
|
||||
Log.info(Player.TAG, `Transformed nsig ${n} to ${nsig}.`);
|
||||
|
||||
if (typeof nsig !== 'string')
|
||||
throw new PlayerError('Failed to decipher nsig');
|
||||
|
||||
if (nsig.startsWith('enhanced_except_')) {
|
||||
Log.warn(Player.TAG, 'Could 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);
|
||||
@@ -130,6 +145,10 @@ export default class Player {
|
||||
break;
|
||||
}
|
||||
|
||||
const result = url_components.toString();
|
||||
|
||||
Log.info(Player.TAG, `Full deciphered URL: ${result}`);
|
||||
|
||||
return url_components.toString();
|
||||
}
|
||||
|
||||
@@ -196,7 +215,7 @@ export default class Player {
|
||||
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
|
||||
|
||||
if (!functions || !calls)
|
||||
console.warn(new PlayerError('Failed to extract signature decipher algorithm'));
|
||||
Log.warn(Player.TAG, 'Failed to extract signature decipher algorithm.');
|
||||
|
||||
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
|
||||
}
|
||||
@@ -205,7 +224,7 @@ export default class Player {
|
||||
const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`;
|
||||
|
||||
if (!sc)
|
||||
console.warn(new PlayerError('Failed to extract n-token decipher algorithm'));
|
||||
Log.warn(Player.TAG, 'Failed to extract n-token decipher algorithm');
|
||||
|
||||
return sc;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import OAuth from './OAuth.ts';
|
||||
import { Log, EventEmitter, HTTPClient } from '../utils/index.ts';
|
||||
import * as Constants from '../utils/Constants.ts';
|
||||
import EventEmitterLike from '../utils/EventEmitterLike.ts';
|
||||
import * as Proto from '../proto/index.ts';
|
||||
import Actions from './Actions.ts';
|
||||
import Player from './Player.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';
|
||||
import {
|
||||
generateRandomString, getRandomUserAgent,
|
||||
InnertubeError, Platform, SessionError
|
||||
} from '../utils/Utils.ts';
|
||||
|
||||
import type { DeviceCategory } from '../utils/Utils.ts';
|
||||
import { generateRandomString, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.ts';
|
||||
import type { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.ts';
|
||||
import OAuth from './OAuth.ts';
|
||||
import type { FetchFunction, ICache } from '../types/index.ts';
|
||||
import type {
|
||||
Credentials, OAuthAuthErrorEventHandler,
|
||||
OAuthAuthEventHandler, OAuthAuthPendingEventHandler
|
||||
} from './OAuth.ts';
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
@@ -63,6 +68,7 @@ export interface Context {
|
||||
user: {
|
||||
enableSafetyMode: boolean;
|
||||
lockedSafetyMode: boolean;
|
||||
onBehalfOfUser?: string;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
@@ -84,6 +90,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.
|
||||
@@ -135,10 +145,23 @@ export interface SessionData {
|
||||
api_version: string;
|
||||
}
|
||||
|
||||
export type SessionArgs = {
|
||||
lang: string;
|
||||
location: string;
|
||||
time_zone: string;
|
||||
device_category: DeviceCategory;
|
||||
client_name: ClientType;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
on_behalf_of_user: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an InnerTube session. This holds all the data needed to make requests to YouTube.
|
||||
*/
|
||||
export default class Session extends EventEmitterLike {
|
||||
export default class Session extends EventEmitter {
|
||||
static TAG = 'Session';
|
||||
|
||||
#api_version: string;
|
||||
#key: string;
|
||||
#context: Context;
|
||||
@@ -193,7 +216,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(
|
||||
@@ -213,11 +237,14 @@ 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 };
|
||||
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
|
||||
|
||||
Log.info(Session.TAG, 'Retrieving InnerTube session.');
|
||||
|
||||
if (generate_session_locally) {
|
||||
session_data = this.#generateSessionData(session_args);
|
||||
@@ -226,29 +253,29 @@ export default class Session extends EventEmitterLike {
|
||||
// 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) {
|
||||
Log.error(Session.TAG, 'Failed to retrieve session data from server. Will try to generate it locally.');
|
||||
session_data = this.#generateSessionData(session_args);
|
||||
}
|
||||
}
|
||||
|
||||
Log.info(Session.TAG, 'Got session data.\n', session_data);
|
||||
|
||||
return { ...session_data, account_index };
|
||||
}
|
||||
|
||||
static async #retrieveSessionData(options: {
|
||||
lang: string;
|
||||
location: string;
|
||||
time_zone: string;
|
||||
device_category: string;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
static #getVisitorID(visitor_data: string) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(visitor_data);
|
||||
Log.info(Session.TAG, 'Custom visitor data decoded successfully.\n', decoded_visitor_data);
|
||||
return decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
static async #retrieveSessionData(options: SessionArgs, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
let visitor_id = generateRandomString(11);
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
visitor_id = this.#getVisitorID(options.visitor_data);
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
@@ -300,27 +327,19 @@ export default class Session extends EventEmitterLike {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
return { context, api_key, api_version };
|
||||
}
|
||||
|
||||
static #generateSessionData(options: {
|
||||
lang: string;
|
||||
location: string;
|
||||
time_zone: string;
|
||||
device_category: DeviceCategory;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}): SessionData {
|
||||
static #generateSessionData(options: SessionArgs): SessionData {
|
||||
let visitor_id = generateRandomString(11);
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
visitor_id = this.#getVisitorID(options.visitor_data);
|
||||
}
|
||||
|
||||
const context: Context = {
|
||||
@@ -347,7 +366,8 @@ export default class Session extends EventEmitterLike {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
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 { generateRandomString } from '../../utils/Utils.ts';
|
||||
import { Parser } from '../../parser/index.ts';
|
||||
import { Channel, HomeFeed, Search, VideoInfo } from '../../parser/ytkids/index.ts';
|
||||
import { InnertubeError, generateRandomString } from '../../utils/Utils.ts';
|
||||
import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.ts';
|
||||
|
||||
import {
|
||||
BrowseEndpoint, NextEndpoint,
|
||||
PlayerEndpoint, SearchEndpoint
|
||||
} from '../endpoints/index.ts';
|
||||
|
||||
import { BlocklistPickerEndpoint } from '../endpoints/kids/index.ts';
|
||||
|
||||
import type { Session, ApiResponse } from '../index.ts';
|
||||
|
||||
export default class Kids {
|
||||
#session: Session;
|
||||
|
||||
@@ -80,4 +81,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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import Album from '../../parser/ytmusic/Album.ts';
|
||||
import Artist from '../../parser/ytmusic/Artist.ts';
|
||||
import Explore from '../../parser/ytmusic/Explore.ts';
|
||||
import HomeFeed from '../../parser/ytmusic/HomeFeed.ts';
|
||||
import Library from '../../parser/ytmusic/Library.ts';
|
||||
import Playlist from '../../parser/ytmusic/Playlist.ts';
|
||||
import Recap from '../../parser/ytmusic/Recap.ts';
|
||||
import Search from '../../parser/ytmusic/Search.ts';
|
||||
import TrackInfo from '../../parser/ytmusic/TrackInfo.ts';
|
||||
import * as Proto from '../../proto/index.ts';
|
||||
import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.ts';
|
||||
|
||||
import {
|
||||
Album, Artist, Explore,
|
||||
HomeFeed, Library, Playlist,
|
||||
Recap, Search, TrackInfo
|
||||
} from '../../parser/ytmusic/index.ts';
|
||||
|
||||
import AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.ts';
|
||||
import Message from '../../parser/classes/Message.ts';
|
||||
@@ -18,13 +17,6 @@ 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 * as Proto from '../../proto/index.ts';
|
||||
|
||||
import type { ObservedArray, YTNode } 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';
|
||||
import type Session from '../Session.ts';
|
||||
|
||||
import {
|
||||
BrowseEndpoint,
|
||||
@@ -35,6 +27,10 @@ import {
|
||||
|
||||
import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.ts';
|
||||
|
||||
import type { ObservedArray } from '../../parser/helpers.ts';
|
||||
import type { MusicSearchFilters } from '../../types/index.ts';
|
||||
import type { Actions, Session } from '../index.ts';
|
||||
|
||||
export default class Music {
|
||||
#session: Session;
|
||||
#actions: Actions;
|
||||
@@ -355,17 +351,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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import * as Proto from '../../proto/index.ts';
|
||||
import * as Constants from '../../utils/Constants.ts';
|
||||
import { Constants } from '../../utils/index.ts';
|
||||
import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.ts';
|
||||
import { CreateVideoEndpoint } from '../endpoints/upload/index.ts';
|
||||
|
||||
import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Clients.ts';
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
import type Session from '../Session.ts';
|
||||
|
||||
import { CreateVideoEndpoint } from '../endpoints/upload/index.ts';
|
||||
import type { ApiResponse, Session } from '../index.ts';
|
||||
|
||||
interface UploadResult {
|
||||
status: string;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
12
deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Normal file
12
deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Normal 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 } };
|
||||
}
|
||||
1
deno/src/core/endpoints/kids/index.ts
Normal file
1
deno/src/core/endpoints/kids/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.ts';
|
||||
18
deno/src/core/endpoints/reel/WatchEndpoint.ts
Normal file
18
deno/src/core/endpoints/reel/WatchEndpoint.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
14
deno/src/core/endpoints/reel/WatchSequenceEndpoint.ts
Normal file
14
deno/src/core/endpoints/reel/WatchSequenceEndpoint.ts
Normal 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
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/reel/index.ts
Normal file
2
deno/src/core/endpoints/reel/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as WatchEndpoint from './WatchEndpoint.ts';
|
||||
export * as WatchSequenceEndpoint from './WatchSequenceEndpoint.ts';
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Actions, ApiResponse } from '../index.ts';
|
||||
|
||||
import AccountInfo from '../../parser/youtube/AccountInfo.ts';
|
||||
import Analytics from '../../parser/youtube/Analytics.ts';
|
||||
import Settings from '../../parser/youtube/Settings.ts';
|
||||
@@ -7,9 +9,6 @@ import * as Proto from '../../proto/index.ts';
|
||||
import { InnertubeError } from '../../utils/Utils.ts';
|
||||
import { Account, BrowseEndpoint, Channel } from '../endpoints/index.ts';
|
||||
|
||||
import type Actions from '../Actions.ts';
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
|
||||
export default class AccountManager {
|
||||
#actions: Actions;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import * as Proto from '../../proto/index.ts';
|
||||
import type Actions from '../Actions.ts';
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
|
||||
import { throwIfMissing } from '../../utils/Utils.ts';
|
||||
import { LikeEndpoint, DislikeEndpoint, RemoveLikeEndpoint } from '../endpoints/like/index.ts';
|
||||
@@ -8,6 +6,8 @@ import { SubscribeEndpoint, UnsubscribeEndpoint } from '../endpoints/subscriptio
|
||||
import { CreateCommentEndpoint, PerformCommentActionEndpoint } from '../endpoints/comment/index.ts';
|
||||
import { ModifyChannelPreferenceEndpoint } from '../endpoints/notification/index.ts';
|
||||
|
||||
import type { Actions, ApiResponse } from '../index.ts';
|
||||
|
||||
export default class InteractionManager {
|
||||
#actions: Actions;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Playlist from '../../parser/youtube/Playlist.ts';
|
||||
import type Actions from '../Actions.ts';
|
||||
import type Feed from '../mixins/Feed.ts';
|
||||
|
||||
import type { EditPlaylistEndpointOptions } from '../../types/index.ts';
|
||||
import { InnertubeError, throwIfMissing } from '../../utils/Utils.ts';
|
||||
import { EditPlaylistEndpoint } from '../endpoints/browse/index.ts';
|
||||
import { BrowseEndpoint } from '../endpoints/index.ts';
|
||||
import { CreateEndpoint, DeleteEndpoint } from '../endpoints/playlist/index.ts';
|
||||
import Playlist from '../../parser/youtube/Playlist.ts';
|
||||
|
||||
import type { Actions } from '../index.ts';
|
||||
import type { Feed } from '../mixins/index.ts';
|
||||
import type { EditPlaylistEndpointOptions } from '../../types/index.ts';
|
||||
|
||||
export default class PlaylistManager {
|
||||
#actions: Actions;
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +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';
|
||||
|
||||
import BackstagePost from '../../parser/classes/BackstagePost.ts';
|
||||
import SharedPost from '../../parser/classes/SharedPost.ts';
|
||||
@@ -27,12 +25,15 @@ import TwoColumnBrowseResults from '../../parser/classes/TwoColumnBrowseResults.
|
||||
import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.ts';
|
||||
import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.ts';
|
||||
|
||||
import type { ApiResponse, Actions } from '../index.ts';
|
||||
import type {
|
||||
Memo, ObservedArray,
|
||||
SuperParsedResult, YTNode
|
||||
} from '../../parser/helpers.ts';
|
||||
import type MusicQueue from '../../parser/classes/MusicQueue.ts';
|
||||
import type RichGrid from '../../parser/classes/RichGrid.ts';
|
||||
import type SectionList from '../../parser/classes/SectionList.ts';
|
||||
|
||||
import type { IParsedResponse } from '../../parser/types/index.ts';
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
|
||||
export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
#page: T;
|
||||
@@ -177,7 +178,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 +186,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 +209,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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import Feed from './Feed.ts';
|
||||
import ChipCloudChip from '../../parser/classes/ChipCloudChip.ts';
|
||||
import FeedFilterChipBar from '../../parser/classes/FeedFilterChipBar.ts';
|
||||
import { InnertubeError } from '../../utils/Utils.ts';
|
||||
import Feed from './Feed.ts';
|
||||
|
||||
import type { ObservedArray } from '../../parser/helpers.ts';
|
||||
import type { IParsedResponse } from '../../parser/types/ParsedResponse.ts';
|
||||
import type Actions from '../Actions.ts';
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
import type { IParsedResponse } from '../../parser/types/index.ts';
|
||||
import type { ApiResponse, Actions } from '../index.ts';
|
||||
|
||||
export default class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#chips?: ObservedArray<ChipCloudChip>;
|
||||
|
||||
@@ -1,16 +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 '../../types/FormatUtils.ts';
|
||||
import * as FormatUtils from '../../utils/FormatUtils.ts';
|
||||
import { Constants, FormatUtils } from '../../utils/index.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 { DashOptions } from '../../types/DashOptions.ts';
|
||||
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.ts';
|
||||
import { getStreamingInfo } from '../../utils/StreamingInfo.ts';
|
||||
|
||||
import { Parser } from '../../parser/index.ts';
|
||||
import { TranscriptInfo } from '../../parser/youtube/index.ts';
|
||||
import ContinuationItem from '../../parser/classes/ContinuationItem.ts';
|
||||
|
||||
import type { ApiResponse, Actions } from '../index.ts';
|
||||
import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.ts';
|
||||
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.ts';
|
||||
import type Format from '../../parser/classes/misc/Format.ts';
|
||||
import type { DashOptions } from '../../types/DashOptions.ts';
|
||||
|
||||
export default class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
#actions: Actions;
|
||||
@@ -18,6 +19,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;
|
||||
@@ -33,6 +35,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;
|
||||
}
|
||||
|
||||
@@ -44,13 +47,19 @@ export default class MediaInfo {
|
||||
* @returns DASH manifest
|
||||
*/
|
||||
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
|
||||
let storyboards;
|
||||
const player_response = this.#page[0];
|
||||
|
||||
if (options.include_thumbnails && this.#page[0].storyboards?.is(PlayerStoryboardSpec)) {
|
||||
storyboards = this.#page[0].storyboards;
|
||||
if (player_response.video_details && (player_response.video_details.is_live)) {
|
||||
throw new InnertubeError('Generating DASH manifests for live 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.');
|
||||
}
|
||||
|
||||
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
|
||||
let storyboards;
|
||||
|
||||
if (options.include_thumbnails && player_response.storyboards) {
|
||||
storyboards = player_response.storyboards;
|
||||
}
|
||||
|
||||
return FormatUtils.toDash(this.streaming_data, this.page[0].video_details?.is_post_live_dvr, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,12 +68,13 @@ export default class MediaInfo {
|
||||
getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter) {
|
||||
return getStreamingInfo(
|
||||
this.streaming_data,
|
||||
this.page[0].video_details?.is_post_live_dvr,
|
||||
url_transformer,
|
||||
format_filter,
|
||||
this.cpn,
|
||||
this.#actions.session.player,
|
||||
this.#actions,
|
||||
this.#page[0].storyboards?.is(PlayerStoryboardSpec) ? this.#page[0].storyboards : undefined
|
||||
this.#page[0].storyboards ? this.#page[0].storyboards : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,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.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import Tab from '../../parser/classes/Tab.ts';
|
||||
import Feed from './Feed.ts';
|
||||
import { Feed } from './index.ts';
|
||||
import { InnertubeError } from '../../utils/Utils.ts';
|
||||
import Tab from '../../parser/classes/Tab.ts';
|
||||
|
||||
import type Actions from '../Actions.ts';
|
||||
import type { Actions, ApiResponse } from '../index.ts';
|
||||
import type { ObservedArray } from '../../parser/helpers.ts';
|
||||
import type { IParsedResponse } from '../../parser/types/ParsedResponse.ts';
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
|
||||
export default class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#tabs?: ObservedArray<Tab>;
|
||||
|
||||
18
deno/src/parser/classes/AboutChannel.ts
Normal file
18
deno/src/parser/classes/AboutChannel.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
87
deno/src/parser/classes/AboutChannelView.ts
Normal file
87
deno/src/parser/classes/AboutChannelView.ts
Normal 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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser from '../index.ts';
|
||||
import { Parser } from '../index.ts';
|
||||
import AccountChannel from './AccountChannel.ts';
|
||||
import AccountItemSection from './AccountItemSection.ts';
|
||||
|
||||
|
||||
17
deno/src/parser/classes/AttributionView.ts
Normal file
17
deno/src/parser/classes/AttributionView.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
export default class AttributionView extends YTNode {
|
||||
static type = 'AttributionView';
|
||||
|
||||
text: Text;
|
||||
suffix: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.text = Text.fromAttributed(data.text);
|
||||
this.suffix = Text.fromAttributed(data.suffix);
|
||||
}
|
||||
}
|
||||
26
deno/src/parser/classes/AvatarView.ts
Normal file
26
deno/src/parser/classes/AvatarView.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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: Thumbnail[];
|
||||
image_processor: {
|
||||
border_image_processor: {
|
||||
circular: boolean
|
||||
}
|
||||
};
|
||||
avatar_image_size: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
this.image_processor = {
|
||||
border_image_processor: {
|
||||
circular: data.image.processor.borderImageProcessor.circular
|
||||
}
|
||||
};
|
||||
this.avatar_image_size = data.avatarImageSize;
|
||||
}
|
||||
}
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
28
deno/src/parser/classes/ButtonView.ts
Normal file
28
deno/src/parser/classes/ButtonView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +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';
|
||||
@@ -22,6 +23,7 @@ export default class C4TabbedHeader extends YTNode {
|
||||
header_links?: ChannelHeaderLinks | ChannelHeaderLinksView | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
tagline?: ChannelTagline | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -69,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Log } from '../../utils/index.ts';
|
||||
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';
|
||||
@@ -44,6 +45,7 @@ export default class Channel extends YTNode {
|
||||
* Please use {@link Channel.subscriber_count} instead.
|
||||
*/
|
||||
get subscribers(): Text {
|
||||
Log.warnOnce(Channel.type, 'Channel#subscribers is deprecated. Please use Channel#subscriber_count instead.');
|
||||
return this.subscriber_count;
|
||||
}
|
||||
|
||||
@@ -53,6 +55,7 @@ export default class Channel extends YTNode {
|
||||
* Please use {@link Channel.video_count} instead.
|
||||
*/
|
||||
get videos(): Text {
|
||||
Log.warnOnce(Channel.type, 'Channel#videos is deprecated. Please use Channel#video_count instead.');
|
||||
return this.video_count;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Log } from '../../utils/index.ts';
|
||||
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';
|
||||
@@ -55,6 +56,7 @@ export default class ChannelAboutFullMetadata extends YTNode {
|
||||
* Please use {@link Channel.view_count} instead.
|
||||
*/
|
||||
get views() {
|
||||
Log.warnOnce(ChannelAboutFullMetadata.type, 'ChannelAboutFullMetadata#views is deprecated. Please use ChannelAboutFullMetadata#view_count instead.');
|
||||
return this.view_count;
|
||||
}
|
||||
|
||||
@@ -64,6 +66,7 @@ export default class ChannelAboutFullMetadata extends YTNode {
|
||||
* Please use {@link Channel.joined_date} instead.
|
||||
*/
|
||||
get joined(): Text {
|
||||
Log.warnOnce(ChannelAboutFullMetadata.type, 'ChannelAboutFullMetadata#joined is deprecated. Please use ChannelAboutFullMetadata#joined_date instead.');
|
||||
return this.joined_date;
|
||||
}
|
||||
}
|
||||
20
deno/src/parser/classes/ChannelExternalLinkView.ts
Normal file
20
deno/src/parser/classes/ChannelExternalLinkView.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
17
deno/src/parser/classes/ChannelOwnerEmptyState.ts
Normal file
17
deno/src/parser/classes/ChannelOwnerEmptyState.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
44
deno/src/parser/classes/ChannelTagline.ts
Normal file
44
deno/src/parser/classes/ChannelTagline.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Text from './misc/Text.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import { Log } from '../../utils/index.ts';
|
||||
|
||||
export default class ChannelVideoPlayer extends YTNode {
|
||||
static type = 'ChannelVideoPlayer';
|
||||
@@ -26,6 +27,7 @@ export default class ChannelVideoPlayer extends YTNode {
|
||||
* Please use {@link ChannelVideoPlayer.view_count} instead.
|
||||
*/
|
||||
get views(): Text {
|
||||
Log.warnOnce(ChannelVideoPlayer.type, 'ChannelVideoPlayer#views is deprecated. Please use ChannelVideoPlayer#view_count instead.');
|
||||
return this.view_count;
|
||||
}
|
||||
|
||||
@@ -35,6 +37,7 @@ export default class ChannelVideoPlayer extends YTNode {
|
||||
* Please use {@link ChannelVideoPlayer.published_time} instead.
|
||||
*/
|
||||
get published(): Text {
|
||||
Log.warnOnce(ChannelVideoPlayer.type, 'ChannelVideoPlayer#published is deprecated. Please use ChannelVideoPlayer#published_time instead.');
|
||||
return this.published_time;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
17
deno/src/parser/classes/ClipAdState.ts
Normal file
17
deno/src/parser/classes/ClipAdState.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
40
deno/src/parser/classes/ClipCreation.ts
Normal file
40
deno/src/parser/classes/ClipCreation.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
28
deno/src/parser/classes/ClipCreationScrubber.ts
Normal file
28
deno/src/parser/classes/ClipCreationScrubber.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
17
deno/src/parser/classes/ClipCreationTextInput.ts
Normal file
17
deno/src/parser/classes/ClipCreationTextInput.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
19
deno/src/parser/classes/ClipSection.ts
Normal file
19
deno/src/parser/classes/ClipSection.ts
Normal 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 ]);
|
||||
}
|
||||
}
|
||||
14
deno/src/parser/classes/Command.ts
Normal file
14
deno/src/parser/classes/Command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Parser 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';
|
||||
@@ -29,7 +30,6 @@ export default class CompactMovie extends YTNode {
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
console.log(data);
|
||||
const overlay_time_status = data.thumbnailOverlays
|
||||
.find((overlay: RawNode) => overlay.thumbnailOverlayTimeStatusRenderer)
|
||||
?.thumbnailOverlayTimeStatusRenderer.text || 'N/A';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
26
deno/src/parser/classes/ContentMetadataView.ts
Normal file
26
deno/src/parser/classes/ContentMetadataView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
21
deno/src/parser/classes/DecoratedAvatarView.ts
Normal file
21
deno/src/parser/classes/DecoratedAvatarView.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, 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 | null;
|
||||
a11y_label: string;
|
||||
on_tap_endpoint?: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.avatar = Parser.parseItem(data.avatar, AvatarView);
|
||||
this.a11y_label = data.a11yLabel;
|
||||
if (data.rendererContext?.commandContext?.onTap) {
|
||||
this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
47
deno/src/parser/classes/DescriptionPreviewView.ts
Normal file
47
deno/src/parser/classes/DescriptionPreviewView.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import EngagementPanelSectionList from './EngagementPanelSectionList.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
export default class DescriptionPreviewView extends YTNode {
|
||||
static type = 'DescriptionPreviewView';
|
||||
|
||||
description: Text;
|
||||
max_lines: number;
|
||||
truncation_text: Text;
|
||||
always_show_truncation_text: boolean;
|
||||
more_endpoint?: {
|
||||
show_engagement_panel_endpoint: {
|
||||
engagement_panel: EngagementPanelSectionList | null,
|
||||
engagement_panel_popup_type: string;
|
||||
identifier: {
|
||||
surface: string,
|
||||
tag: string
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.description = Text.fromAttributed(data.description);
|
||||
this.max_lines = parseInt(data.maxLines);
|
||||
this.truncation_text = Text.fromAttributed(data.truncationText);
|
||||
this.always_show_truncation_text = !!data.alwaysShowTruncationText;
|
||||
|
||||
if (data.rendererContext.commandContext?.onTap?.innertubeCommand?.showEngagementPanelEndpoint) {
|
||||
const endpoint = data.rendererContext.commandContext?.onTap?.innertubeCommand?.showEngagementPanelEndpoint;
|
||||
|
||||
this.more_endpoint = {
|
||||
show_engagement_panel_endpoint: {
|
||||
engagement_panel: Parser.parseItem(endpoint.engagementPanel, EngagementPanelSectionList),
|
||||
engagement_panel_popup_type: endpoint.engagementPanelPresentationConfigs.engagementPanelPopupPresentationConfig.popupType,
|
||||
identifier: {
|
||||
surface: endpoint.identifier.surface,
|
||||
tag: endpoint.identifier.tag
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
16
deno/src/parser/classes/DislikeButtonView.ts
Normal file
16
deno/src/parser/classes/DislikeButtonView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
export default class DynamicTextView extends YTNode {
|
||||
static type = 'DynamicTextView';
|
||||
|
||||
text: string;
|
||||
text: Text;
|
||||
max_lines: number;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.text = data.text.content;
|
||||
this.text = Text.fromAttributed(data.text);
|
||||
this.max_lines = parseInt(data.maxLines);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
16
deno/src/parser/classes/FancyDismissibleDialog.ts
Normal file
16
deno/src/parser/classes/FancyDismissibleDialog.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
26
deno/src/parser/classes/FeedNudge.ts
Normal file
26
deno/src/parser/classes/FeedNudge.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
22
deno/src/parser/classes/FlexibleActionsView.ts
Normal file
22
deno/src/parser/classes/FlexibleActionsView.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
16
deno/src/parser/classes/ImageBannerView.ts
Normal file
16
deno/src/parser/classes/ImageBannerView.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
|
||||
export default class ImageBannerView extends YTNode {
|
||||
static type = 'ImageBannerView';
|
||||
|
||||
image: Thumbnail[];
|
||||
style: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
this.style = data.style;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import Parser, { type RawNode } from '../index.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import InfoPanelContent from './InfoPanelContent.ts';
|
||||
import Menu from './menus/Menu.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user