mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 01:52:11 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4024fae5 | ||
|
|
216cb3139a | ||
|
|
4a4c37001d | ||
|
|
219d88b200 | ||
|
|
923e9c28e3 | ||
|
|
5ef7ea8034 | ||
|
|
00c199ac69 | ||
|
|
06a0090ae6 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [13.2.0](https://github.com/LuanRT/YouTube.js/compare/v13.1.0...v13.2.0) (2025-03-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add AccessibilityContext and CommandContext classes + improve type definitions and parsing logic across multiple nodes ([923e9c2](https://github.com/LuanRT/YouTube.js/commit/923e9c28e34b00841413824d82d10bf644186edc))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Constants:** Update the iOS client version ([#924](https://github.com/LuanRT/YouTube.js/issues/924)) ([219d88b](https://github.com/LuanRT/YouTube.js/commit/219d88b2005431c6697f04e1fa2c5e8528a9ce57))
|
||||
* **Format:** Parse xtags from protobuf to support SABR-only responses ([#909](https://github.com/LuanRT/YouTube.js/issues/909)) ([00c199a](https://github.com/LuanRT/YouTube.js/commit/00c199ac69bc6d7be19aeae04a245f30b64272c2))
|
||||
|
||||
## [13.1.0](https://github.com/LuanRT/YouTube.js/compare/v13.0.0...v13.1.0) (2025-02-21)
|
||||
|
||||
|
||||
|
||||
9
examples/browser/web/package-lock.json
generated
9
examples/browser/web/package-lock.json
generated
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"patch-package": "^6.5.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^5.4.7"
|
||||
"vite": "^5.4.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@bufbuild/protobuf": {
|
||||
@@ -1412,10 +1412,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"version": "5.4.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
|
||||
"integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"devDependencies": {
|
||||
"patch-package": "^6.5.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^5.4.7"
|
||||
"vite": "^5.4.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"bgutils-js": "^3.1.2",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.1.0",
|
||||
"version": "13.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "13.1.0",
|
||||
"version": "13.2.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.1.0",
|
||||
"version": "13.2.0",
|
||||
"description": "A JavaScript client for YouTube's private API, known as InnerTube.",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
|
||||
@@ -35,6 +35,10 @@ export interface KeyValuePair {
|
||||
value?: string | undefined;
|
||||
}
|
||||
|
||||
export interface FormatXTags {
|
||||
xtags: KeyValuePair[];
|
||||
}
|
||||
|
||||
function createBaseHttpHeader(): HttpHeader {
|
||||
return { name: undefined, value: undefined };
|
||||
}
|
||||
@@ -275,6 +279,42 @@ export const KeyValuePair: MessageFns<KeyValuePair> = {
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseFormatXTags(): FormatXTags {
|
||||
return { xtags: [] };
|
||||
}
|
||||
|
||||
export const FormatXTags: MessageFns<FormatXTags> = {
|
||||
encode(message: FormatXTags, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.xtags) {
|
||||
KeyValuePair.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): FormatXTags {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseFormatXTags();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.xtags.push(KeyValuePair.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function longToNumber(int64: { toString(): string }): number {
|
||||
const num = globalThis.Number(int64.toString());
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
|
||||
@@ -25,4 +25,8 @@ message IndexRange {
|
||||
message KeyValuePair {
|
||||
optional string key = 1;
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
message FormatXTags {
|
||||
repeated KeyValuePair xtags = 1;
|
||||
}
|
||||
@@ -85,32 +85,34 @@ export default class Innertube {
|
||||
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload });
|
||||
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload });
|
||||
|
||||
const session = this.#session;
|
||||
|
||||
const extra_payload: Record<string, any> = {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
vis: 0,
|
||||
splay: false,
|
||||
lactMilliseconds: '-1',
|
||||
signatureTimestamp: this.#session.player?.sts
|
||||
signatureTimestamp: session.player?.sts
|
||||
}
|
||||
},
|
||||
client
|
||||
};
|
||||
|
||||
if (this.#session.po_token) {
|
||||
if (session.po_token) {
|
||||
extra_payload.serviceIntegrityDimensions = {
|
||||
poToken: this.#session.po_token
|
||||
poToken: session.po_token
|
||||
};
|
||||
}
|
||||
|
||||
const watch_response = watch_endpoint.call(this.#session.actions, extra_payload);
|
||||
const watch_next_response = watch_next_endpoint.call(this.#session.actions);
|
||||
const watch_response = watch_endpoint.call(session.actions, extra_payload);
|
||||
const watch_next_response = watch_next_endpoint.call(session.actions);
|
||||
|
||||
const response = await Promise.all([ watch_response, watch_next_response ]);
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
return new VideoInfo(response, this.actions, cpn);
|
||||
return new VideoInfo(response, session.actions, cpn);
|
||||
}
|
||||
|
||||
async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
@@ -118,29 +120,31 @@ export default class Innertube {
|
||||
|
||||
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: { videoId: video_id } });
|
||||
|
||||
const session = this.#session;
|
||||
|
||||
const extra_payload: Record<string, any> = {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
vis: 0,
|
||||
splay: false,
|
||||
lactMilliseconds: '-1',
|
||||
signatureTimestamp: this.#session.player?.sts
|
||||
signatureTimestamp: session.player?.sts
|
||||
}
|
||||
},
|
||||
client
|
||||
};
|
||||
|
||||
if (this.#session.po_token) {
|
||||
if (session.po_token) {
|
||||
extra_payload.serviceIntegrityDimensions = {
|
||||
poToken: this.#session.po_token
|
||||
poToken: session.po_token
|
||||
};
|
||||
}
|
||||
|
||||
const watch_response = await watch_endpoint.call(this.#session.actions, extra_payload);
|
||||
const watch_response = await watch_endpoint.call(session.actions, extra_payload);
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
return new VideoInfo([ watch_response ], this.actions, cpn);
|
||||
return new VideoInfo([ watch_response ], session.actions, cpn);
|
||||
}
|
||||
|
||||
async getShortsVideoInfo(video_id: string, client?: InnerTubeClient): Promise<ShortFormVideoInfo> {
|
||||
@@ -154,7 +158,9 @@ export default class Innertube {
|
||||
}
|
||||
});
|
||||
|
||||
const reel_watch_response = reel_watch_endpoint.call(this.#session.actions, { client });
|
||||
const actions = this.#session.actions;
|
||||
|
||||
const reel_watch_response = reel_watch_endpoint.call(actions, { client });
|
||||
|
||||
const writer = ReelSequence.encode({
|
||||
shortId: video_id,
|
||||
@@ -167,13 +173,13 @@ export default class Innertube {
|
||||
|
||||
const params = encodeURIComponent(u8ToBase64(writer.finish()));
|
||||
|
||||
const sequence_response = this.actions.execute('/reel/reel_watch_sequence', { sequenceParams: params });
|
||||
const sequence_response = actions.execute('/reel/reel_watch_sequence', { sequenceParams: params });
|
||||
|
||||
const response = await Promise.all([ reel_watch_response, sequence_response ]);
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
return new ShortFormVideoInfo([ response[0] ], this.actions, cpn, response[1]);
|
||||
return new ShortFormVideoInfo([ response[0] ], actions, cpn, response[1]);
|
||||
}
|
||||
|
||||
async search(query: string, filters: SearchFilters = {}): Promise<Search> {
|
||||
@@ -253,6 +259,8 @@ export default class Innertube {
|
||||
}
|
||||
|
||||
async getSearchSuggestions(query: string, previous_query?: string): Promise<string[]> {
|
||||
const session = this.#session;
|
||||
|
||||
const url = new URL(`${Constants.URLS.YT_SUGGESTIONS}/complete/search`);
|
||||
url.searchParams.set('client', 'youtube');
|
||||
url.searchParams.set('gs_ri', 'youtube');
|
||||
@@ -260,16 +268,16 @@ export default class Innertube {
|
||||
url.searchParams.set('cp', '0');
|
||||
url.searchParams.set('ds', 'yt');
|
||||
url.searchParams.set('sugexp', Constants.CLIENTS.WEB.SUGG_EXP_ID);
|
||||
url.searchParams.set('hl', this.#session.context.client.hl);
|
||||
url.searchParams.set('gl', this.#session.context.client.gl);
|
||||
url.searchParams.set('hl', session.context.client.hl);
|
||||
url.searchParams.set('gl', session.context.client.gl);
|
||||
url.searchParams.set('q', query);
|
||||
|
||||
if (previous_query)
|
||||
url.searchParams.set('pq', previous_query);
|
||||
|
||||
const response = await this.#session.http.fetch_function(url, {
|
||||
const response = await session.http.fetch_function(url, {
|
||||
headers: {
|
||||
'Cookie': this.#session.cookie || ''
|
||||
'Cookie': session.cookie || ''
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -24,32 +24,34 @@ export default class Kids {
|
||||
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: payload });
|
||||
const watch_next_endpoint = new NavigationEndpoint({ watchNextEndpoint: payload });
|
||||
|
||||
const session = this.#session;
|
||||
|
||||
const extra_payload: Record<string, any> = {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
vis: 0,
|
||||
splay: false,
|
||||
lactMilliseconds: '-1',
|
||||
signatureTimestamp: this.#session.player?.sts
|
||||
signatureTimestamp: session.player?.sts
|
||||
}
|
||||
},
|
||||
client: 'YTKIDS'
|
||||
};
|
||||
|
||||
if (this.#session.po_token) {
|
||||
if (session.po_token) {
|
||||
extra_payload.serviceIntegrityDimensions = {
|
||||
poToken: this.#session.po_token
|
||||
poToken: session.po_token
|
||||
};
|
||||
}
|
||||
|
||||
const watch_response = watch_endpoint.call(this.#session.actions, extra_payload);
|
||||
const watch_response = watch_endpoint.call(session.actions, extra_payload);
|
||||
|
||||
const watch_next_response = watch_next_endpoint.call(this.#session.actions, { client: 'YTKIDS' });
|
||||
const watch_next_response = watch_next_endpoint.call(session.actions, { client: 'YTKIDS' });
|
||||
|
||||
const response = await Promise.all([ watch_response, watch_next_response ]);
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
return new VideoInfo(response, this.#session.actions, cpn);
|
||||
return new VideoInfo(response, session.actions, cpn);
|
||||
}
|
||||
|
||||
async getChannel(channel_id: string): Promise<Channel> {
|
||||
@@ -71,7 +73,9 @@ export default class Kids {
|
||||
* @returns A list of API responses.
|
||||
*/
|
||||
async blockChannel(channel_id: string): Promise<ApiResponse[]> {
|
||||
if (!this.#session.logged_in)
|
||||
const session = this.#session;
|
||||
|
||||
if (!session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const kids_blocklist_picker_command = new NavigationEndpoint({
|
||||
@@ -82,7 +86,7 @@ export default class Kids {
|
||||
}
|
||||
});
|
||||
|
||||
const response = await kids_blocklist_picker_command.call(this.#session.actions, { client: 'YTKIDS' });
|
||||
const response = await kids_blocklist_picker_command.call(session.actions, { client: 'YTKIDS' });
|
||||
const popup = response.data.command.confirmDialogEndpoint;
|
||||
const popup_fragment = { contents: popup.content, engagementPanels: [] };
|
||||
const kid_picker = Parser.parseResponse(popup_fragment);
|
||||
@@ -96,7 +100,7 @@ export default class Kids {
|
||||
|
||||
for (const kid of kids) {
|
||||
if (!kid.block_button?.is_toggled) {
|
||||
kid.setActions(this.#session.actions);
|
||||
kid.setActions(session.actions);
|
||||
// Block channel and add to the response list.
|
||||
responses.push(await kid.blockChannel());
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export default class Studio {
|
||||
* ```
|
||||
*/
|
||||
async updateVideoMetadata(video_id: string, metadata: UpdateVideoMetadataOptions): Promise<ApiResponse> {
|
||||
if (!this.#session.logged_in)
|
||||
const session = this.#session;
|
||||
|
||||
if (!session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const payload: MetadataUpdateRequest = {
|
||||
@@ -55,18 +57,18 @@ export default class Studio {
|
||||
clientName: parseInt(Constants.CLIENT_NAME_IDS.ANDROID),
|
||||
clientVersion: Constants.CLIENTS.ANDROID.VERSION,
|
||||
androidSdkVersion: Constants.CLIENTS.ANDROID.SDK_VERSION,
|
||||
visitorData: this.#session.context.client.visitorData,
|
||||
visitorData: session.context.client.visitorData,
|
||||
osVersion: '13',
|
||||
acceptLanguage: this.#session.context.client.hl,
|
||||
acceptRegion: this.#session.context.client.gl,
|
||||
acceptLanguage: session.context.client.hl,
|
||||
acceptRegion: session.context.client.gl,
|
||||
deviceMake: 'Google',
|
||||
deviceModel: 'sdk_gphone64_x86_64',
|
||||
screenHeightPoints: 840,
|
||||
screenWidthPoints: 432,
|
||||
configInfo: {
|
||||
appInstallData: this.#session.context.client.configInfo?.appInstallData
|
||||
appInstallData: session.context.client.configInfo?.appInstallData
|
||||
},
|
||||
timeZone: this.#session.context.client.timeZone,
|
||||
timeZone: session.context.client.timeZone,
|
||||
chipset: 'qcom;taro'
|
||||
},
|
||||
activePlayers: []
|
||||
@@ -131,7 +133,7 @@ export default class Studio {
|
||||
|
||||
const writer = MetadataUpdateRequest.encode(payload);
|
||||
|
||||
return await this.#session.actions.execute('/video_manager/metadata_update', {
|
||||
return await session.actions.execute('/video_manager/metadata_update', {
|
||||
protobuf: true,
|
||||
serialized_data: writer.finish()
|
||||
});
|
||||
|
||||
@@ -29,10 +29,13 @@ import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.
|
||||
import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.js';
|
||||
|
||||
import type { Actions, ApiResponse } from '../index.js';
|
||||
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../../parser/helpers.js';
|
||||
import type { Memo, ObservedArray } from '../../parser/helpers.js';
|
||||
import type MusicQueue from '../../parser/classes/MusicQueue.js';
|
||||
import type RichGrid from '../../parser/classes/RichGrid.js';
|
||||
import type SectionList from '../../parser/classes/SectionList.js';
|
||||
import type SecondarySearchContainer from '../../parser/classes/SecondarySearchContainer.js';
|
||||
import type BrowseFeedActions from '../../parser/classes/BrowseFeedActions.js';
|
||||
import type ProfileColumn from '../../parser/classes/ProfileColumn.js';
|
||||
|
||||
export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
readonly #page: T;
|
||||
@@ -163,14 +166,14 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
/**
|
||||
* Returns secondary contents from the page.
|
||||
*/
|
||||
get secondary_contents(): SuperParsedResult<YTNode> | undefined {
|
||||
get secondary_contents(): SectionList | SecondarySearchContainer | BrowseFeedActions | ProfileColumn | null {
|
||||
if (!this.#page.contents?.is_node)
|
||||
return undefined;
|
||||
return null;
|
||||
|
||||
const node = this.#page.contents?.item();
|
||||
|
||||
if (!node.is(TwoColumnBrowseResults, TwoColumnSearchResults))
|
||||
return undefined;
|
||||
return null;
|
||||
|
||||
return node.secondary_contents;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export type AlertType = 'UNKNOWN' | 'WARNING' | 'ERROR' | 'SUCCESS' | 'INFO';
|
||||
|
||||
export default class Alert extends YTNode {
|
||||
static type = 'Alert';
|
||||
|
||||
text: Text;
|
||||
alert_type: string;
|
||||
alert_type: AlertType;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../types/RawResponse.js';
|
||||
import type { RawNode } from '../types/index.js';
|
||||
|
||||
export default class BadgeView extends YTNode {
|
||||
text: string;
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import SubFeedSelector from './SubFeedSelector.js';
|
||||
import EomSettingsDisclaimer from './EomSettingsDisclaimer.js';
|
||||
import ToggleButton from './ToggleButton.js';
|
||||
import CompactLink from './CompactLink.js';
|
||||
import SearchBox from './SearchBox.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
export default class BrowseFeedActions extends YTNode {
|
||||
static type = 'BrowseFeedActions';
|
||||
|
||||
contents: ObservedArray<YTNode>;
|
||||
public contents: ObservedArray<SubFeedSelector | EomSettingsDisclaimer | ToggleButton | CompactLink | SearchBox | Button>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.contents = Parser.parseArray(data.contents, [ SubFeedSelector, EomSettingsDisclaimer, ToggleButton, CompactLink, SearchBox, Button ]);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,124 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
export default class ButtonView extends YTNode {
|
||||
static type = 'ButtonView';
|
||||
|
||||
icon_name: string;
|
||||
title: string;
|
||||
accessibility_text: string;
|
||||
style: string;
|
||||
is_full_width: boolean;
|
||||
button_type: string;
|
||||
button_size: string;
|
||||
on_tap: NavigationEndpoint;
|
||||
public secondary_icon_image?: Thumbnail[];
|
||||
public icon_name?: string;
|
||||
public enable_icon_button?: boolean;
|
||||
public tooltip?: string;
|
||||
public icon_image_flip_for_rtl?: boolean;
|
||||
public button_size?: 'BUTTON_VIEW_MODEL_SIZE_UNKNOWN' | 'BUTTON_VIEW_MODEL_SIZE_DEFAULT' | 'BUTTON_VIEW_MODEL_SIZE_COMPACT' | 'BUTTON_VIEW_MODEL_SIZE_XSMALL' | 'BUTTON_VIEW_MODEL_SIZE_LARGE' | 'BUTTON_VIEW_MODEL_SIZE_XLARGE' | 'BUTTON_VIEW_MODEL_SIZE_XXLARGE';
|
||||
public icon_position?: 'BUTTON_VIEW_MODEL_ICON_POSITION_UNKNOWN' | 'BUTTON_VIEW_MODEL_ICON_POSITION_TRAILING' | 'BUTTON_VIEW_MODEL_ICON_POSITION_LEADING' | 'BUTTON_VIEW_MODEL_ICON_POSITION_ABOVE' | 'BUTTON_VIEW_MODEL_ICON_POSITION_LEADING_TRAILING';
|
||||
public is_full_width?: boolean;
|
||||
public state?: 'BUTTON_VIEW_MODEL_STATE_UNKNOWN' | 'BUTTON_VIEW_MODEL_STATE_ACTIVE' | 'BUTTON_VIEW_MODEL_STATE_INACTIVE' | 'BUTTON_VIEW_MODEL_STATE_DISABLED';
|
||||
public on_disabled_tap?: NavigationEndpoint;
|
||||
public custom_border_color?: number;
|
||||
public on_tap?: NavigationEndpoint;
|
||||
public style?: 'BUTTON_VIEW_MODEL_STYLE_UNKNOWN' | 'BUTTON_VIEW_MODEL_STYLE_CTA' | 'BUTTON_VIEW_MODEL_STYLE_BRAND' | 'BUTTON_VIEW_MODEL_STYLE_ADS_CTA' | 'BUTTON_VIEW_MODEL_STYLE_OVERLAY' | 'BUTTON_VIEW_MODEL_STYLE_CTA_THEMED' | 'BUTTON_VIEW_MODEL_STYLE_BLACK_CTA' | 'BUTTON_VIEW_MODEL_STYLE_CUSTOM' | 'BUTTON_VIEW_MODEL_STYLE_MONO' | 'BUTTON_VIEW_MODEL_STYLE_OVERLAY_DARK' | 'BUTTON_VIEW_MODEL_STYLE_CTA_OVERLAY' | 'BUTTON_VIEW_MODEL_STYLE_BRAND_AI' | 'BUTTON_VIEW_MODEL_STYLE_YT_GRADIENT' | 'BUTTON_VIEW_MODEL_STYLE_BRAND_GRADIENT';
|
||||
public icon_image?: object;
|
||||
public custom_dark_theme_border_color?: number;
|
||||
public title?: string;
|
||||
public target_id?: string;
|
||||
public enable_full_width_margins?: boolean;
|
||||
public custom_font_color?: number;
|
||||
public button_type?: 'BUTTON_VIEW_MODEL_TYPE_UNKNOWN' | 'BUTTON_VIEW_MODEL_TYPE_FILLED' | 'BUTTON_VIEW_MODEL_TYPE_OUTLINE' | 'BUTTON_VIEW_MODEL_TYPE_TEXT' | 'BUTTON_VIEW_MODEL_TYPE_TONAL';
|
||||
public enabled?: boolean;
|
||||
public accessibility_id?: string;
|
||||
public custom_background_color?: number;
|
||||
public on_long_press?: NavigationEndpoint;
|
||||
public title_formatted?: object;
|
||||
public on_visible?: object;
|
||||
public icon_trailing?: boolean;
|
||||
public accessibility_text?: string;
|
||||
|
||||
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.button_type = data.type;
|
||||
this.button_size = data.buttonSize;
|
||||
this.on_tap = new NavigationEndpoint(data.onTap);
|
||||
if ('secondaryIconImage' in data)
|
||||
this.secondary_icon_image = Thumbnail.fromResponse(data.secondaryIconImage);
|
||||
|
||||
if ('iconName' in data)
|
||||
this.icon_name = data.iconName;
|
||||
|
||||
if ('enableIconButton' in data)
|
||||
this.enable_icon_button = data.enableIconButton;
|
||||
|
||||
if ('tooltip' in data)
|
||||
this.tooltip = data.tooltip;
|
||||
|
||||
if ('iconImageFlipForRtl' in data)
|
||||
this.icon_image_flip_for_rtl = data.iconImageFlipForRtl;
|
||||
|
||||
if ('buttonSize' in data)
|
||||
this.button_size = data.buttonSize;
|
||||
|
||||
if ('iconPosition' in data)
|
||||
this.icon_position = data.iconPosition;
|
||||
|
||||
if ('isFullWidth' in data)
|
||||
this.is_full_width = data.isFullWidth;
|
||||
|
||||
if ('state' in data)
|
||||
this.state = data.state;
|
||||
|
||||
if ('onDisabledTap' in data)
|
||||
this.on_disabled_tap = new NavigationEndpoint(data.onDisabledTap);
|
||||
|
||||
if ('customBorderColor' in data)
|
||||
this.custom_border_color = data.customBorderColor;
|
||||
|
||||
if ('onTap' in data)
|
||||
this.on_tap = new NavigationEndpoint(data.onTap);
|
||||
|
||||
if ('style' in data)
|
||||
this.style = data.style;
|
||||
|
||||
if ('iconImage' in data)
|
||||
this.icon_image = data.iconImage;
|
||||
|
||||
if ('customDarkThemeBorderColor' in data)
|
||||
this.custom_dark_theme_border_color = data.customDarkThemeBorderColor;
|
||||
|
||||
if ('title' in data)
|
||||
this.title = data.title;
|
||||
|
||||
if ('targetId' in data)
|
||||
this.target_id = data.targetId;
|
||||
|
||||
if ('enableFullWidthMargins' in data)
|
||||
this.enable_full_width_margins = data.enableFullWidthMargins;
|
||||
|
||||
if ('customFontColor' in data)
|
||||
this.custom_font_color = data.customFontColor;
|
||||
|
||||
if ('buttonType' in data)
|
||||
this.button_type = data.buttonType;
|
||||
|
||||
if ('enabled' in data)
|
||||
this.enabled = data.enabled;
|
||||
|
||||
if ('accessibilityId' in data)
|
||||
this.accessibility_id = data.accessibilityId;
|
||||
|
||||
if ('customBackgroundColor' in data)
|
||||
this.custom_background_color = data.customBackgroundColor;
|
||||
|
||||
if ('onLongPress' in data)
|
||||
this.on_long_press = new NavigationEndpoint(data.onLongPress);
|
||||
|
||||
if ('titleFormatted' in data)
|
||||
this.title_formatted = data.titleFormatted;
|
||||
|
||||
if ('onVisible' in data)
|
||||
this.on_visible = data.onVisible;
|
||||
|
||||
if ('iconTrailing' in data)
|
||||
this.icon_trailing = data.iconTrailing;
|
||||
|
||||
if ('accessibilityText' in data)
|
||||
this.accessibility_text = data.accessibilityText;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
@@ -7,53 +7,90 @@ import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import ThumbnailOverlayTimeStatus from './ThumbnailOverlayTimeStatus.js';
|
||||
|
||||
export default class CompactVideo extends YTNode {
|
||||
static type = 'CompactVideo';
|
||||
|
||||
id: string;
|
||||
thumbnails: Thumbnail[];
|
||||
rich_thumbnail?: YTNode;
|
||||
title: Text;
|
||||
author: Author;
|
||||
view_count: Text;
|
||||
short_view_count: Text;
|
||||
published: Text;
|
||||
badges: MetadataBadge[];
|
||||
|
||||
duration: {
|
||||
text: string;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
thumbnail_overlays: ObservedArray<YTNode>;
|
||||
endpoint: NavigationEndpoint;
|
||||
menu: Menu | null;
|
||||
public video_id: string;
|
||||
public thumbnails: Thumbnail[];
|
||||
public rich_thumbnail?: YTNode;
|
||||
public title: Text;
|
||||
public author: Author;
|
||||
public view_count?: Text;
|
||||
public short_view_count?: Text;
|
||||
public short_byline_text?: Text;
|
||||
public long_byline_text?: Text;
|
||||
public published?: Text;
|
||||
public badges: MetadataBadge[];
|
||||
public thumbnail_overlays: ObservedArray<YTNode>;
|
||||
public endpoint?: NavigationEndpoint;
|
||||
public menu: Menu | null;
|
||||
public length_text?: Text;
|
||||
public is_watched: boolean;
|
||||
public service_endpoints?: NavigationEndpoint[];
|
||||
public service_endpoint?: NavigationEndpoint;
|
||||
public style?: 'COMPACT_VIDEO_STYLE_TYPE_UNKNOWN' | 'COMPACT_VIDEO_STYLE_TYPE_NORMAL' | 'COMPACT_VIDEO_STYLE_TYPE_PROMINENT_THUMBNAIL' | 'COMPACT_VIDEO_STYLE_TYPE_HERO';
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.videoId;
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail) || null;
|
||||
|
||||
if (Reflect.has(data, 'richThumbnail')) {
|
||||
this.rich_thumbnail = Parser.parseItem(data.richThumbnail);
|
||||
}
|
||||
|
||||
this.video_id = data.videoId;
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.title = new Text(data.title);
|
||||
this.author = new Author(data.longBylineText, data.ownerBadges, data.channelThumbnail);
|
||||
this.view_count = new Text(data.viewCountText);
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
this.is_watched = !!data.isWatched;
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.badges = Parser.parseArray(data.badges, MetadataBadge);
|
||||
|
||||
this.duration = {
|
||||
text: new Text(data.lengthText).toString(),
|
||||
seconds: timeToSeconds(new Text(data.lengthText).toString())
|
||||
};
|
||||
if ('publishedTimeText' in data)
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
|
||||
if ('shortBylineText' in data)
|
||||
this.view_count = new Text(data.viewCountText);
|
||||
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
if ('shortViewCountText' in data)
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
|
||||
if ('richThumbnail' in data)
|
||||
this.rich_thumbnail = Parser.parseItem(data.richThumbnail);
|
||||
|
||||
if ('shortBylineText' in data)
|
||||
this.short_byline_text = new Text(data.shortBylineText);
|
||||
|
||||
if ('longBylineText' in data)
|
||||
this.long_byline_text = new Text(data.longBylineText);
|
||||
|
||||
if ('lengthText' in data)
|
||||
this.length_text = new Text(data.lengthText);
|
||||
|
||||
if ('serviceEndpoints' in data)
|
||||
this.service_endpoints = data.serviceEndpoints.map((endpoint: RawNode) => new NavigationEndpoint(endpoint));
|
||||
|
||||
if ('serviceEndpoint' in data)
|
||||
this.service_endpoint = new NavigationEndpoint(data.serviceEndpoint);
|
||||
|
||||
if ('navigationEndpoint' in data)
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
|
||||
if ('style' in data)
|
||||
this.style = data.style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@linkcode video_id} instead.
|
||||
*/
|
||||
get id(): string {
|
||||
return this.video_id;
|
||||
}
|
||||
|
||||
get duration() {
|
||||
const overlay_time_status = this.thumbnail_overlays.firstOfType(ThumbnailOverlayTimeStatus);
|
||||
const length_text = this.length_text?.toString() || overlay_time_status?.text.toString();
|
||||
return {
|
||||
text: length_text,
|
||||
seconds: length_text ? timeToSeconds(length_text) : 0
|
||||
};
|
||||
}
|
||||
|
||||
get best_thumbnail() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
@@ -9,28 +9,28 @@ import Thumbnail from './misc/Thumbnail.js';
|
||||
export default class GridVideo extends YTNode {
|
||||
static type = 'GridVideo';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_overlays: ObservedArray<YTNode>;
|
||||
rich_thumbnail: YTNode;
|
||||
published: Text;
|
||||
duration: Text | null;
|
||||
author: Author;
|
||||
views: Text;
|
||||
short_view_count: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
menu: Menu | null;
|
||||
buttons?: ObservedArray<YTNode>;
|
||||
upcoming?: Date;
|
||||
upcoming_text?: Text;
|
||||
is_reminder_set?: boolean;
|
||||
public video_id: string;
|
||||
public title: Text;
|
||||
public thumbnails: Thumbnail[];
|
||||
public thumbnail_overlays: ObservedArray<YTNode>;
|
||||
public rich_thumbnail: YTNode;
|
||||
public published: Text;
|
||||
public duration: Text | null;
|
||||
public author: Author;
|
||||
public views: Text;
|
||||
public short_view_count: Text;
|
||||
public endpoint: NavigationEndpoint;
|
||||
public menu: Menu | null;
|
||||
public buttons?: ObservedArray<YTNode>;
|
||||
public upcoming?: Date;
|
||||
public upcoming_text?: Text;
|
||||
public is_reminder_set?: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const length_alt = data.thumbnailOverlays.find((overlay: RawNode) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer;
|
||||
|
||||
this.id = data.videoId;
|
||||
this.video_id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
@@ -54,6 +54,13 @@ export default class GridVideo extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@linkcode video_id} instead.
|
||||
*/
|
||||
get id(): string {
|
||||
return this.video_id;
|
||||
}
|
||||
|
||||
get is_upcoming(): boolean {
|
||||
return Boolean(this.upcoming && this.upcoming > new Date());
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default class LockupView extends YTNode {
|
||||
public content_image: CollectionThumbnailView | ThumbnailView | null;
|
||||
public metadata: LockupMetadataView | null;
|
||||
public content_id: string;
|
||||
public content_type: 'VIDEO' | 'MOVIE' | 'CHANNEL' | 'CLIP' | 'SOURCE' | 'PLAYLIST' | 'ALBUM' | 'PODCAST' | 'SHOPPING_COLLECTION' | 'SHORT' | 'GAME' | 'PRODUCT';
|
||||
public content_type: 'UNSPECIFIED' | 'VIDEO' | 'PLAYLIST' | 'SHORT' | 'CHANNEL' | 'ALBUM' | 'PRODUCT' | 'GAME' | 'CLIP' | 'PODCAST' | 'SOURCE' | 'SHOPPING_COLLECTION' | 'MOVIE';
|
||||
public renderer_context: RendererContext;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
|
||||
@@ -15,15 +15,16 @@ import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
interface PlaylistItemData {
|
||||
video_id: string;
|
||||
playlist_set_video_id: string;
|
||||
}
|
||||
|
||||
export default class MusicResponsiveListItem extends YTNode {
|
||||
static type = 'MusicResponsiveListItem';
|
||||
|
||||
flex_columns: ObservedArray<MusicResponsiveListItemFlexColumn>;
|
||||
fixed_columns: ObservedArray<MusicResponsiveListItemFixedColumn>;
|
||||
#playlist_item_data: {
|
||||
video_id: string;
|
||||
playlist_set_video_id: string;
|
||||
};
|
||||
|
||||
endpoint?: NavigationEndpoint;
|
||||
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'non_music_track' | 'video' | 'song' | 'endpoint' | 'unknown' | 'podcast_show' | undefined;
|
||||
@@ -78,7 +79,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
this.flex_columns = Parser.parseArray(data.flexColumns, MusicResponsiveListItemFlexColumn);
|
||||
this.fixed_columns = Parser.parseArray(data.fixedColumns, MusicResponsiveListItemFixedColumn);
|
||||
|
||||
this.#playlist_item_data = {
|
||||
const playlist_item_data: PlaylistItemData = {
|
||||
video_id: data?.playlistItemData?.videoId || null,
|
||||
playlist_set_video_id: data?.playlistItemData?.playlistSetVideoId || null
|
||||
};
|
||||
@@ -119,7 +120,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
break;
|
||||
case 'MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE':
|
||||
this.item_type = 'non_music_track';
|
||||
this.#parseNonMusicTrack();
|
||||
this.#parseNonMusicTrack(playlist_item_data);
|
||||
break;
|
||||
case 'MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE':
|
||||
this.item_type = 'podcast_show';
|
||||
@@ -127,7 +128,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
break;
|
||||
default:
|
||||
if (this.flex_columns[1]) {
|
||||
this.#parseVideoOrSong();
|
||||
this.#parseVideoOrSong(playlist_item_data);
|
||||
} else {
|
||||
this.#parseOther();
|
||||
}
|
||||
@@ -164,25 +165,25 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
#parseVideoOrSong() {
|
||||
#parseVideoOrSong(playlist_item_data: PlaylistItemData) {
|
||||
const music_video_type = (this.flex_columns.at(0)?.title.runs?.at(0) as TextRun)?.endpoint?.payload?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType;
|
||||
switch (music_video_type) {
|
||||
case 'MUSIC_VIDEO_TYPE_UGC':
|
||||
case 'MUSIC_VIDEO_TYPE_OMV':
|
||||
this.item_type = 'video';
|
||||
this.#parseVideo();
|
||||
this.#parseVideo(playlist_item_data);
|
||||
break;
|
||||
case 'MUSIC_VIDEO_TYPE_ATV':
|
||||
this.item_type = 'song';
|
||||
this.#parseSong();
|
||||
this.#parseSong(playlist_item_data);
|
||||
break;
|
||||
default:
|
||||
this.#parseOther();
|
||||
}
|
||||
}
|
||||
|
||||
#parseSong() {
|
||||
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
#parseSong(playlist_item_data: PlaylistItemData) {
|
||||
this.id = playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
|
||||
const duration_text = this.flex_columns.at(1)?.title.runs?.find(
|
||||
@@ -228,8 +229,8 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
#parseVideo() {
|
||||
this.id = this.#playlist_item_data.video_id;
|
||||
#parseVideo(playlist_item_data: PlaylistItemData) {
|
||||
this.id = playlist_item_data.video_id;
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
this.views = this.flex_columns.at(1)?.title.runs?.find((run) => run.text.match(/(.*?) views/))?.toString();
|
||||
|
||||
@@ -273,8 +274,8 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
this.song_count = this.subtitle?.runs?.find((run) => (/^\d+(,\d+)? songs?$/i).test(run.text))?.text || '';
|
||||
}
|
||||
|
||||
#parseNonMusicTrack() {
|
||||
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
#parseNonMusicTrack(playlist_item_data: PlaylistItemData) {
|
||||
this.id = playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Parser, type RawNode } from '../index.js';
|
||||
export default class ProfileColumn extends YTNode {
|
||||
static type = 'ProfileColumn';
|
||||
|
||||
items: ObservedArray<YTNode>;
|
||||
public items: ObservedArray<YTNode>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import UniversalWatchCard from './UniversalWatchCard.js';
|
||||
|
||||
export default class SecondarySearchContainer extends YTNode {
|
||||
static type = 'SecondarySearchContainer';
|
||||
|
||||
contents: ObservedArray<YTNode>;
|
||||
public target_id?: string;
|
||||
public contents: ObservedArray<UniversalWatchCard>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.contents = Parser.parseArray(data.contents, [ UniversalWatchCard ]);
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,12 @@ export default class SegmentedLikeDislikeButtonView extends YTNode {
|
||||
if (toggle_button.default_button) {
|
||||
this.short_like_count = toggle_button.default_button.title;
|
||||
|
||||
this.like_count = parseInt(toggle_button.default_button.accessibility_text.replace(/\D/g, ''));
|
||||
if (toggle_button.default_button.accessibility_text)
|
||||
this.like_count = parseInt(toggle_button.default_button.accessibility_text.replace(/\D/g, ''));
|
||||
} else if (toggle_button.toggled_button) {
|
||||
this.short_like_count = toggle_button.toggled_button.title;
|
||||
|
||||
this.like_count = parseInt(toggle_button.toggled_button.accessibility_text.replace(/\D/g, ''));
|
||||
if (toggle_button.toggled_button.accessibility_text)
|
||||
this.like_count = parseInt(toggle_button.toggled_button.accessibility_text.replace(/\D/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { YTNode, type SuperParsedResult } from '../helpers.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import SectionList from './SectionList.js';
|
||||
import BrowseFeedActions from './BrowseFeedActions.js';
|
||||
import ProfileColumn from './ProfileColumn.js';
|
||||
import Tab from './Tab.js';
|
||||
import ExpandableTab from './ExpandableTab.js';
|
||||
|
||||
export default class TwoColumnBrowseResults extends YTNode {
|
||||
static type = 'TwoColumnBrowseResults';
|
||||
|
||||
tabs: SuperParsedResult<YTNode>;
|
||||
secondary_contents: SuperParsedResult<YTNode>;
|
||||
public tabs: ObservedArray<Tab | ExpandableTab>;
|
||||
public secondary_contents: SectionList | BrowseFeedActions | ProfileColumn | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.tabs = Parser.parse(data.tabs);
|
||||
this.secondary_contents = Parser.parse(data.secondaryContents);
|
||||
this.tabs = Parser.parseArray(data.tabs, [ Tab, ExpandableTab ]);
|
||||
this.secondary_contents = Parser.parseItem(data.secondaryContents, [ SectionList, BrowseFeedActions, ProfileColumn ]);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
import { YTNode, type SuperParsedResult } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import SecondarySearchContainer from './SecondarySearchContainer.js';
|
||||
import RichGrid from './RichGrid.js';
|
||||
import SectionList from './SectionList.js';
|
||||
|
||||
export default class TwoColumnSearchResults extends YTNode {
|
||||
static type = 'TwoColumnSearchResults';
|
||||
|
||||
primary_contents: SuperParsedResult<YTNode>;
|
||||
secondary_contents: SuperParsedResult<YTNode>;
|
||||
|
||||
public header: YTNode | null;
|
||||
public primary_contents: RichGrid | SectionList | null;
|
||||
public secondary_contents: SecondarySearchContainer | null;
|
||||
public target_id?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.primary_contents = Parser.parse(data.primaryContents);
|
||||
this.secondary_contents = Parser.parse(data.secondaryContents);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
this.primary_contents = Parser.parseItem(data.primaryContents, [ RichGrid, SectionList ]);
|
||||
this.secondary_contents = Parser.parseItem(data.secondaryContents, [ SecondarySearchContainer ]);
|
||||
|
||||
if ('targetId' in data) {
|
||||
this.target_id = data.targetId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ExpandableMetadata from './ExpandableMetadata.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
@@ -13,96 +13,112 @@ import Thumbnail from './misc/Thumbnail.js';
|
||||
export default class Video extends YTNode {
|
||||
static type = 'Video';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
description_snippet?: Text;
|
||||
snippets?: {
|
||||
text: Text;
|
||||
hover_text: Text;
|
||||
}[];
|
||||
expandable_metadata: ExpandableMetadata | null;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_overlays: ObservedArray<YTNode>;
|
||||
rich_thumbnail?: YTNode;
|
||||
author: Author;
|
||||
badges: MetadataBadge[];
|
||||
endpoint: NavigationEndpoint;
|
||||
published: Text;
|
||||
view_count: Text;
|
||||
short_view_count: Text;
|
||||
upcoming?: Date;
|
||||
duration: {
|
||||
text: string;
|
||||
seconds: number;
|
||||
};
|
||||
show_action_menu: boolean;
|
||||
is_watched: boolean;
|
||||
menu: Menu | null;
|
||||
byline_text?: Text;
|
||||
search_video_result_entity_key?: string;
|
||||
public video_id: string;
|
||||
public title: Text;
|
||||
public untranslated_title?: Text;
|
||||
public description_snippet?: Text;
|
||||
public snippets?: { text: Text; hover_text: Text; }[];
|
||||
public expandable_metadata: ExpandableMetadata | null;
|
||||
public additional_metadatas?: Text[];
|
||||
public thumbnails: Thumbnail[];
|
||||
public thumbnail_overlays: ObservedArray<YTNode>;
|
||||
public rich_thumbnail?: YTNode;
|
||||
public author: Author;
|
||||
public badges: MetadataBadge[];
|
||||
public endpoint?: NavigationEndpoint;
|
||||
public published?: Text;
|
||||
public view_count?: Text;
|
||||
public short_view_count?: Text;
|
||||
public upcoming?: Date;
|
||||
public length_text?: Text;
|
||||
public show_action_menu: boolean;
|
||||
public is_watched: boolean;
|
||||
public menu: Menu | null;
|
||||
public byline_text?: Text;
|
||||
public search_video_result_entity_key?: string;
|
||||
public service_endpoints?: NavigationEndpoint[];
|
||||
public service_endpoint?: NavigationEndpoint;
|
||||
public style?: 'VIDEO_STYLE_TYPE_UNKNOWN' | 'VIDEO_STYLE_TYPE_NORMAL' | 'VIDEO_STYLE_TYPE_POST' | 'VIDEO_STYLE_TYPE_SUB' | 'VIDEO_STYLE_TYPE_LIVE_POST' | 'VIDEO_STYLE_TYPE_FULL_BLEED_ISOLATED' | 'VIDEO_STYLE_TYPE_WITH_EXPANDED_METADATA';
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const overlay_time_status = data.thumbnailOverlays
|
||||
.find((overlay: any) => overlay.thumbnailOverlayTimeStatusRenderer)
|
||||
?.thumbnailOverlayTimeStatusRenderer.text || 'N/A';
|
||||
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.video_id = data.videoId;
|
||||
this.expandable_metadata = Parser.parseItem(data.expandableMetadata, ExpandableMetadata);
|
||||
|
||||
if (Reflect.has(data, 'descriptionSnippet')) {
|
||||
if ('untranslatedTitle' in data)
|
||||
this.untranslated_title = new Text(data.untranslatedTitle);
|
||||
|
||||
if ('descriptionSnippet' in data)
|
||||
this.description_snippet = new Text(data.descriptionSnippet);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'detailedMetadataSnippets')) {
|
||||
if ('detailedMetadataSnippets' in data) {
|
||||
this.snippets = data.detailedMetadataSnippets.map((snippet: RawNode) => ({
|
||||
text: new Text(snippet.snippetText),
|
||||
hover_text: new Text(snippet.snippetHoverText)
|
||||
}));
|
||||
}
|
||||
|
||||
this.expandable_metadata = Parser.parseItem(data.expandableMetadata, ExpandableMetadata);
|
||||
if ('additionalMetadatas' in data)
|
||||
this.additional_metadatas = data.additionalMetadatas.map((meta: RawNode) => new Text(meta));
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
|
||||
if (Reflect.has(data, 'richThumbnail')) {
|
||||
if ('richThumbnail' in data)
|
||||
this.rich_thumbnail = Parser.parseItem(data.richThumbnail);
|
||||
}
|
||||
|
||||
this.author = new Author(data.ownerText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
|
||||
this.badges = Parser.parseArray(data.badges, MetadataBadge);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
this.view_count = new Text(data.viewCountText);
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
|
||||
if (Reflect.has(data, 'upcomingEventData')) {
|
||||
if ('navigationEndpoint' in data)
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
|
||||
if ('publishedTimeText' in data)
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
|
||||
if ('viewCountText' in data)
|
||||
this.view_count = new Text(data.viewCountText);
|
||||
|
||||
if ('shortViewCountText' in data)
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
|
||||
if ('upcomingEventData' in data)
|
||||
this.upcoming = new Date(Number(`${data.upcomingEventData.startTime}000`));
|
||||
}
|
||||
|
||||
this.duration = {
|
||||
text: data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString(),
|
||||
seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString())
|
||||
};
|
||||
|
||||
this.show_action_menu = !!data.showActionMenu;
|
||||
this.is_watched = !!data.isWatched;
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
|
||||
if (Reflect.has(data, 'searchVideoResultEntityKey')) {
|
||||
if ('searchVideoResultEntityKey' in data)
|
||||
this.search_video_result_entity_key = data.searchVideoResultEntityKey;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'bylineText')) {
|
||||
|
||||
if ('bylineText' in data)
|
||||
this.byline_text = new Text(data.bylineText);
|
||||
}
|
||||
|
||||
if ('lengthText' in data)
|
||||
this.length_text = new Text(data.lengthText);
|
||||
|
||||
if ('serviceEndpoints' in data)
|
||||
this.service_endpoints = data.serviceEndpoints.map((endpoint: RawNode) => new NavigationEndpoint(endpoint));
|
||||
|
||||
if ('serviceEndpoint' in data)
|
||||
this.service_endpoint = new NavigationEndpoint(data.serviceEndpoint);
|
||||
|
||||
if ('style' in data)
|
||||
this.style = data.style;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@linkcode video_id} instead.
|
||||
*/
|
||||
get id(): string {
|
||||
return this.video_id;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
if (this.snippets) {
|
||||
if (this.snippets)
|
||||
return this.snippets.map((snip) => snip.text.toString()).join('');
|
||||
}
|
||||
|
||||
return this.description_snippet?.toString() || '';
|
||||
}
|
||||
|
||||
@@ -132,4 +148,13 @@ export default class Video extends YTNode {
|
||||
get best_thumbnail(): Thumbnail | undefined {
|
||||
return this.thumbnails[0];
|
||||
}
|
||||
|
||||
get duration() {
|
||||
const overlay_time_status = this.thumbnail_overlays.firstOfType(ThumbnailOverlayTimeStatus);
|
||||
const length_text = this.length_text?.toString() || overlay_time_status?.text.toString();
|
||||
return {
|
||||
text: length_text,
|
||||
seconds: length_text ? timeToSeconds(length_text) : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@ import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import SubscriptionButton from './misc/SubscriptionButton.js';
|
||||
|
||||
export default class VideoOwner extends YTNode {
|
||||
static type = 'VideoOwner';
|
||||
|
||||
subscription_button;
|
||||
subscriber_count: Text;
|
||||
author: Author;
|
||||
public subscription_button?: SubscriptionButton;
|
||||
public subscriber_count: Text;
|
||||
public author: Author;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
// TODO: check this
|
||||
this.subscription_button = data.subscriptionButton;
|
||||
if ('subscriptionButton' in data)
|
||||
this.subscription_button = new SubscriptionButton(data.subscriptionButton);
|
||||
this.subscriber_count = new Text(data.subscriberCountText);
|
||||
|
||||
this.author = new Author({
|
||||
...data.title,
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
|
||||
import Text from './misc/Text.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
@@ -11,6 +11,7 @@ export default class VideoPrimaryInfo extends YTNode {
|
||||
|
||||
public title: Text;
|
||||
public super_title_link?: Text;
|
||||
public station_name?: Text;
|
||||
public view_count: VideoViewCount | null;
|
||||
public badges: ObservedArray<MetadataBadge>;
|
||||
public published: Text;
|
||||
@@ -21,9 +22,12 @@ export default class VideoPrimaryInfo extends YTNode {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
|
||||
if (Reflect.has(data, 'superTitleLink'))
|
||||
if ('superTitleLink' in data)
|
||||
this.super_title_link = new Text(data.superTitleLink);
|
||||
|
||||
if ('stationName' in data)
|
||||
this.station_name = new Text(data.stationName);
|
||||
|
||||
this.view_count = Parser.parseItem(data.viewCount, VideoViewCount);
|
||||
this.badges = Parser.parseArray(data.badges, MetadataBadge);
|
||||
this.published = new Text(data.dateText);
|
||||
|
||||
@@ -9,23 +9,26 @@ import { YTNode } from '../helpers.js';
|
||||
export default class VideoSecondaryInfo extends YTNode {
|
||||
static type = 'VideoSecondaryInfo';
|
||||
|
||||
owner: VideoOwner | null;
|
||||
description: Text;
|
||||
subscribe_button: SubscribeButton | Button | null;
|
||||
metadata: MetadataRowContainer | null;
|
||||
show_more_text: Text;
|
||||
show_less_text: Text;
|
||||
default_expanded: string;
|
||||
description_collapsed_lines: string;
|
||||
public owner: VideoOwner | null;
|
||||
public description: Text;
|
||||
public description_placeholder?: Text;
|
||||
public subscribe_button: SubscribeButton | Button | null;
|
||||
public metadata: MetadataRowContainer | null;
|
||||
public show_more_text: Text;
|
||||
public show_less_text: Text;
|
||||
public default_expanded: string;
|
||||
public description_collapsed_lines: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.owner = Parser.parseItem(data.owner, VideoOwner);
|
||||
this.description = new Text(data.description);
|
||||
|
||||
if (Reflect.has(data, 'attributedDescription')) {
|
||||
if ('attributedDescription' in data)
|
||||
this.description = Text.fromAttributed(data.attributedDescription);
|
||||
}
|
||||
|
||||
if ('descriptionPlaceholder' in data)
|
||||
this.description_placeholder = new Text(data.descriptionPlaceholder);
|
||||
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.metadata = Parser.parseItem(data.metadataRowContainer, MetadataRowContainer);
|
||||
|
||||
9
src/parser/classes/misc/AccessibilityContext.ts
Normal file
9
src/parser/classes/misc/AccessibilityContext.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { RawNode } from '../../types/index.js';
|
||||
|
||||
export default class AccessibilityContext {
|
||||
public label: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.label = data.label;
|
||||
}
|
||||
}
|
||||
47
src/parser/classes/misc/CommandContext.ts
Normal file
47
src/parser/classes/misc/CommandContext.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { RawNode } from '../../types/index.js';
|
||||
import NavigationEndpoint from '../NavigationEndpoint.js';
|
||||
|
||||
export default class CommandContext {
|
||||
public on_focus?: NavigationEndpoint;
|
||||
public on_hidden?: NavigationEndpoint;
|
||||
public on_touch_end?: NavigationEndpoint;
|
||||
public on_touch_move?: NavigationEndpoint;
|
||||
public on_long_press?: NavigationEndpoint;
|
||||
public on_tap?: NavigationEndpoint;
|
||||
public on_touch_start?: NavigationEndpoint;
|
||||
public on_visible?: NavigationEndpoint;
|
||||
public on_first_visible?: NavigationEndpoint;
|
||||
public on_hover?: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
if ('onFocus' in data)
|
||||
this.on_focus = new NavigationEndpoint(data.onFocus);
|
||||
|
||||
if ('onHidden' in data)
|
||||
this.on_hidden = new NavigationEndpoint(data.onHidden);
|
||||
|
||||
if ('onTouchEnd' in data)
|
||||
this.on_touch_end = new NavigationEndpoint(data.onTouchEnd);
|
||||
|
||||
if ('onTouchMove' in data)
|
||||
this.on_touch_move = new NavigationEndpoint(data.onTouchMove);
|
||||
|
||||
if ('onLongPress' in data)
|
||||
this.on_long_press = new NavigationEndpoint(data.onLongPress);
|
||||
|
||||
if ('onTap' in data)
|
||||
this.on_tap = new NavigationEndpoint(data.onTap);
|
||||
|
||||
if ('onTouchStart' in data)
|
||||
this.on_touch_start = new NavigationEndpoint(data.onTouchStart);
|
||||
|
||||
if ('onVisible' in data)
|
||||
this.on_visible = new NavigationEndpoint(data.onVisible);
|
||||
|
||||
if ('onFirstVisible' in data)
|
||||
this.on_first_visible = new NavigationEndpoint(data.onFirstVisible);
|
||||
|
||||
if ('onHover' in data)
|
||||
this.on_hover = new NavigationEndpoint(data.onHover);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type Player from '../../../core/Player.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
import { FormatXTags } from '../../../../protos/generated/misc/common.js';
|
||||
import { base64ToU8 } from '../../../utils/Utils.js';
|
||||
|
||||
export type ProjectionType = 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
|
||||
export type SpatialAudioType = 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
|
||||
@@ -211,17 +213,16 @@ export default class Format {
|
||||
};
|
||||
|
||||
if (this.has_audio || this.has_text) {
|
||||
const args = new URLSearchParams(this.cipher || this.signature_cipher);
|
||||
const url_components = new URLSearchParams(args.get('url') || this.url);
|
||||
const xtags = this.xtags
|
||||
? FormatXTags.decode(base64ToU8(decodeURIComponent(this.xtags).replace(/-/g, '+').replace(/_/g, '/'))).xtags
|
||||
: [];
|
||||
|
||||
const xtags = url_components.get('xtags')?.split(':');
|
||||
|
||||
this.language = xtags?.find((x: string) => x.startsWith('lang='))?.split('=')[1] || null;
|
||||
this.language = xtags.find((tag) => tag.key === 'lang')?.value || null;
|
||||
|
||||
if (this.has_audio) {
|
||||
this.is_drc = !!data.isDrc || !!xtags?.includes('drc=1');
|
||||
this.is_drc = !!data.isDrc || xtags.some((tag) => tag.key === 'drc' && tag.value === '1');
|
||||
|
||||
const audio_content = xtags?.find((x) => x.startsWith('acont='))?.split('=')[1];
|
||||
const audio_content = xtags.find((tag) => tag.key === 'acont')?.value;
|
||||
this.is_dubbed = audio_content === 'dubbed';
|
||||
this.is_descriptive = audio_content === 'descriptive';
|
||||
this.is_secondary = audio_content === 'secondary';
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import type { RawNode } from '../../types/index.js';
|
||||
import NavigationEndpoint from '../NavigationEndpoint.js';
|
||||
|
||||
export type CommandContext = {
|
||||
on_tap?: NavigationEndpoint;
|
||||
};
|
||||
|
||||
export type AccessibilityContext = {
|
||||
label?: string;
|
||||
};
|
||||
import CommandContext from './CommandContext.js';
|
||||
import AccessibilityContext from './AccessibilityContext.js';
|
||||
|
||||
export default class RendererContext {
|
||||
public command_context: CommandContext;
|
||||
public accessibility_context: AccessibilityContext;
|
||||
public command_context?: CommandContext;
|
||||
public accessibility_context?: AccessibilityContext;
|
||||
|
||||
constructor(data?: RawNode) {
|
||||
this.command_context = {};
|
||||
this.accessibility_context = {};
|
||||
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
if (Reflect.has(data, 'commandContext')) {
|
||||
if (Reflect.has(data.commandContext, 'onTap')) {
|
||||
this.command_context.on_tap = new NavigationEndpoint(data.commandContext.onTap);
|
||||
}
|
||||
if ('commandContext' in data) {
|
||||
this.command_context = new CommandContext(data.commandContext);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'accessibilityContext')) {
|
||||
if (Reflect.has(data.accessibilityContext, 'label')) {
|
||||
this.accessibility_context.label = data.accessibilityContext.label;
|
||||
}
|
||||
if ('accessibilityContext' in data) {
|
||||
this.accessibility_context = new AccessibilityContext(data.accessibilityContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/misc/SubscriptionButton.ts
Normal file
17
src/parser/classes/misc/SubscriptionButton.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { RawNode } from '../../index.js';
|
||||
import Text from './Text.js';
|
||||
|
||||
export default class SubscriptionButton {
|
||||
static type = 'SubscriptionButton';
|
||||
|
||||
public text: Text;
|
||||
public subscribed: boolean;
|
||||
public subscription_type?: 'FREE' | 'PAID' | 'UNAVAILABLE';
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.text = new Text(data.text);
|
||||
this.subscribed = data.isSubscribed;
|
||||
if ('subscriptionType' in data)
|
||||
this.subscription_type = data.subscriptionType;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
// This file was auto generated, do not edit.
|
||||
// See ./scripts/build-parser-map.js
|
||||
|
||||
export { default as AccessibilityContext } from './classes/misc/AccessibilityContext.js';
|
||||
export { default as Author } from './classes/misc/Author.js';
|
||||
export { default as ChildElement } from './classes/misc/ChildElement.js';
|
||||
export { default as CommandContext } from './classes/misc/CommandContext.js';
|
||||
export { default as EmojiRun } from './classes/misc/EmojiRun.js';
|
||||
export { default as Format } from './classes/misc/Format.js';
|
||||
export { default as RendererContext } from './classes/misc/RendererContext.js';
|
||||
export { default as SubscriptionButton } from './classes/misc/SubscriptionButton.js';
|
||||
export { default as Text } from './classes/misc/Text.js';
|
||||
export { default as TextRun } from './classes/misc/TextRun.js';
|
||||
export { default as Thumbnail } from './classes/misc/Thumbnail.js';
|
||||
|
||||
@@ -13,7 +13,7 @@ import MicroformatData from '../classes/MicroformatData.js';
|
||||
import SubscribeButton from '../classes/SubscribeButton.js';
|
||||
import ExpandableTab from '../classes/ExpandableTab.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import Tab from '../classes/Tab.js';
|
||||
import type Tab from '../classes/Tab.js';
|
||||
import PageHeader from '../classes/PageHeader.js';
|
||||
import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.js';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
@@ -62,7 +62,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
this.subscribe_button = this.page.header_memo?.getType(SubscribeButton)[0];
|
||||
|
||||
if (this.page.contents)
|
||||
this.current_tab = this.page.contents.item().as(TwoColumnBrowseResults).tabs.array().filterType(Tab, ExpandableTab).get({ selected: true });
|
||||
this.current_tab = this.page.contents.item().as(TwoColumnBrowseResults).tabs.get({ selected: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class History extends Feed<IBrowseResponse> {
|
||||
for (const section of this.sections) {
|
||||
for (const content of section.contents) {
|
||||
const video = content as Video;
|
||||
if (video.id === video_id && video.menu) {
|
||||
if (video.video_id === video_id && video.menu) {
|
||||
feedbackToken = video.menu.top_level_buttons[0].as(Button).endpoint.payload.feedbackToken;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import SettingsSwitch from '../classes/SettingsSwitch.js';
|
||||
import CommentsHeader from '../classes/comments/CommentsHeader.js';
|
||||
import ItemSectionHeader from '../classes/ItemSectionHeader.js';
|
||||
import ItemSectionTabbedHeader from '../classes/ItemSectionTabbedHeader.js';
|
||||
import Tab from '../classes/Tab.js';
|
||||
import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.js';
|
||||
|
||||
import type { ApiResponse, Actions } from '../../core/index.js';
|
||||
@@ -34,7 +33,7 @@ export default class Settings {
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const tab = this.#page.contents.item().as(TwoColumnBrowseResults).tabs.array().as(Tab).get({ selected: true });
|
||||
const tab = this.#page.contents.item().as(TwoColumnBrowseResults).tabs.get({ selected: true });
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Target tab not found');
|
||||
|
||||
@@ -215,6 +215,9 @@ export default class VideoInfo extends MediaInfo {
|
||||
if (like_status === 'LIKE')
|
||||
throw new InnertubeError('This video is already liked', { video_id: this.basic_info.id });
|
||||
|
||||
if (!button.default_button.on_tap)
|
||||
throw new InnertubeError('onTap command not found', { video_id: this.basic_info.id });
|
||||
|
||||
const endpoint = new NavigationEndpoint(button.default_button.on_tap.payload.commands.find((cmd: RawNode) => cmd.innertubeCommand));
|
||||
|
||||
return await endpoint.call(this.actions);
|
||||
@@ -252,6 +255,9 @@ export default class VideoInfo extends MediaInfo {
|
||||
if (like_status === 'DISLIKE')
|
||||
throw new InnertubeError('This video is already disliked', { video_id: this.basic_info.id });
|
||||
|
||||
if (!button.default_button.on_tap)
|
||||
throw new InnertubeError('onTap command not found', { video_id: this.basic_info.id });
|
||||
|
||||
const endpoint = new NavigationEndpoint(button.default_button.on_tap.payload.commands.find((cmd: RawNode) => cmd.innertubeCommand));
|
||||
|
||||
return await endpoint.call(this.actions);
|
||||
@@ -298,7 +304,10 @@ export default class VideoInfo extends MediaInfo {
|
||||
|
||||
if (!button || !button.toggled_button)
|
||||
throw new InnertubeError('Like/Dislike button not found', { video_id: this.basic_info.id });
|
||||
|
||||
|
||||
if (!button.toggled_button.on_tap)
|
||||
throw new InnertubeError('onTap command not found', { video_id: this.basic_info.id });
|
||||
|
||||
const endpoint = new NavigationEndpoint(button.toggled_button.on_tap.payload.commands.find((cmd: RawNode) => cmd.innertubeCommand));
|
||||
|
||||
return await endpoint.call(this.actions);
|
||||
@@ -401,7 +410,7 @@ export default class VideoInfo extends MediaInfo {
|
||||
// If the song isn't in the video_lockup, it should be in the info_rows
|
||||
song = lookup.video_lockup?.title?.toString();
|
||||
// If the video id isn't in the video_lockup, it should be in the info_rows
|
||||
videoId = lookup.video_lockup?.endpoint.payload.videoId;
|
||||
videoId = lookup.video_lockup?.endpoint?.payload.videoId;
|
||||
for (let i = 0; i < lookup.info_rows.length; i++) {
|
||||
const info_row = lookup.info_rows[i];
|
||||
if (info_row.info_row_expand_status_key === undefined) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class TrackInfo extends MediaInfo {
|
||||
if (next) {
|
||||
const tabbed_results = next.contents_memo?.getType(WatchNextTabbedResults)?.[0];
|
||||
|
||||
this.tabs = tabbed_results?.tabs.array().as(Tab);
|
||||
this.tabs = tabbed_results?.tabs.as(Tab);
|
||||
this.current_video_endpoint = next.current_video_endpoint;
|
||||
|
||||
// TODO: update PlayerOverlay, YTMusic's is a little bit different.
|
||||
|
||||
@@ -25,13 +25,15 @@ export const OAUTH = {
|
||||
export const CLIENTS = {
|
||||
IOS: {
|
||||
NAME: 'iOS',
|
||||
VERSION: '18.06.35',
|
||||
USER_AGENT: 'com.google.ios.youtube/18.06.35 (iPhone; CPU iPhone OS 14_4 like Mac OS X; en_US)',
|
||||
DEVICE_MODEL: 'iPhone10,6'
|
||||
VERSION: '20.11.6',
|
||||
USER_AGENT: 'com.google.ios.youtube/20.11.6 (iPhone10,4; U; CPU iOS 16_7_7 like Mac OS X)',
|
||||
DEVICE_MODEL: 'iPhone10,4',
|
||||
OS_NAME: 'iOS',
|
||||
OS_VERSION: '16.7.7.20H330'
|
||||
},
|
||||
WEB: {
|
||||
NAME: 'WEB',
|
||||
VERSION: '2.20241121.01.00',
|
||||
VERSION: '2.20250222.10.00',
|
||||
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
|
||||
API_VERSION: 'v1',
|
||||
STATIC_VISITOR_ID: '6zpwvWUNAco',
|
||||
@@ -39,12 +41,12 @@ export const CLIENTS = {
|
||||
},
|
||||
MWEB: {
|
||||
NAME: 'MWEB',
|
||||
VERSION: '2.20241205.01.00',
|
||||
VERSION: '2.20250224.01.00',
|
||||
API_VERSION: 'v1'
|
||||
},
|
||||
WEB_KIDS: {
|
||||
NAME: 'WEB_KIDS',
|
||||
VERSION: '2.20230111.00.00'
|
||||
VERSION: '2.20250221.11.00'
|
||||
},
|
||||
YTMUSIC: {
|
||||
NAME: 'WEB_REMIX',
|
||||
@@ -66,7 +68,7 @@ export const CLIENTS = {
|
||||
},
|
||||
TV: {
|
||||
NAME: 'TVHTML5',
|
||||
VERSION: '7.20241016.15.00',
|
||||
VERSION: '7.20250219.14.00',
|
||||
USER_AGENT: 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version'
|
||||
},
|
||||
TV_EMBEDDED: {
|
||||
@@ -75,7 +77,7 @@ export const CLIENTS = {
|
||||
},
|
||||
WEB_EMBEDDED: {
|
||||
NAME: 'WEB_EMBEDDED_PLAYER',
|
||||
VERSION: '2.20240111.09.00',
|
||||
VERSION: '1.20250219.01.00',
|
||||
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
|
||||
API_VERSION: 'v1',
|
||||
STATIC_VISITOR_ID: '6zpwvWUNAco'
|
||||
|
||||
@@ -33,7 +33,9 @@ export default class HTTPClient {
|
||||
input: URL | Request | string,
|
||||
init?: RequestInit & HTTPClientInit
|
||||
): Promise<Response> {
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION_1 + this.#session.api_version;
|
||||
const session = this.#session;
|
||||
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION_1 + session.api_version;
|
||||
const baseURL = init?.baseURL || innertube_url;
|
||||
|
||||
const request_url =
|
||||
@@ -55,17 +57,17 @@ export default class HTTPClient {
|
||||
|
||||
request_headers.set('Accept', '*/*');
|
||||
request_headers.set('Accept-Language', '*');
|
||||
request_headers.set('X-Goog-Visitor-Id', this.#session.context.client.visitorData || '');
|
||||
request_headers.set('X-Youtube-Client-Version', this.#session.context.client.clientVersion || '');
|
||||
request_headers.set('X-Goog-Visitor-Id', session.context.client.visitorData || '');
|
||||
request_headers.set('X-Youtube-Client-Version', session.context.client.clientVersion || '');
|
||||
|
||||
const client_name_id = Constants.CLIENT_NAME_IDS[this.#session.context.client.clientName as keyof typeof Constants.CLIENT_NAME_IDS];
|
||||
const client_name_id = Constants.CLIENT_NAME_IDS[session.context.client.clientName as keyof typeof Constants.CLIENT_NAME_IDS];
|
||||
|
||||
if (client_name_id) {
|
||||
request_headers.set('X-Youtube-Client-Name', client_name_id);
|
||||
}
|
||||
|
||||
if (Platform.shim.server) {
|
||||
request_headers.set('User-Agent', this.#session.user_agent || '');
|
||||
request_headers.set('User-Agent', session.user_agent || '');
|
||||
request_headers.set('Origin', request_url.origin);
|
||||
}
|
||||
|
||||
@@ -88,7 +90,7 @@ export default class HTTPClient {
|
||||
const n_body = {
|
||||
...json,
|
||||
// Deep copy since we're going to be modifying it
|
||||
context: JSON.parse(JSON.stringify(this.#session.context)) as Context
|
||||
context: JSON.parse(JSON.stringify(session.context)) as Context
|
||||
};
|
||||
|
||||
this.#adjustContext(n_body.context, n_body.client);
|
||||
@@ -121,8 +123,8 @@ export default class HTTPClient {
|
||||
}
|
||||
|
||||
// Authenticate (NOTE: YouTube Kids does not support regular bearer tokens)
|
||||
if (this.#session.logged_in && is_innertube_req && !is_web_kids) {
|
||||
const oauth = this.#session.oauth;
|
||||
if (session.logged_in && is_innertube_req && !is_web_kids) {
|
||||
const oauth = session.oauth;
|
||||
|
||||
if (oauth.oauth2_tokens) {
|
||||
if (oauth.shouldRefreshToken()) {
|
||||
@@ -132,17 +134,19 @@ export default class HTTPClient {
|
||||
request_headers.set('Authorization', `Bearer ${oauth.oauth2_tokens.access_token}`);
|
||||
}
|
||||
|
||||
if (this.#cookie) {
|
||||
const sapisid = getCookie(this.#cookie, 'SAPISID');
|
||||
const cookie = this.#cookie;
|
||||
|
||||
if (cookie) {
|
||||
const sapisid = getCookie(cookie, 'SAPISID');
|
||||
|
||||
if (sapisid) {
|
||||
request_headers.set('Authorization', await generateSidAuth(sapisid));
|
||||
request_headers.set('X-Goog-Authuser', this.#session.account_index.toString());
|
||||
if (this.#session.context.user.onBehalfOfUser)
|
||||
request_headers.set('X-Goog-PageId', this.#session.context.user.onBehalfOfUser);
|
||||
request_headers.set('X-Goog-Authuser', session.account_index.toString());
|
||||
if (session.context.user.onBehalfOfUser)
|
||||
request_headers.set('X-Goog-PageId', session.context.user.onBehalfOfUser);
|
||||
}
|
||||
|
||||
request_headers.set('Cookie', this.#cookie);
|
||||
request_headers.set('Cookie', cookie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +201,8 @@ export default class HTTPClient {
|
||||
ctx.client.clientVersion = Constants.CLIENTS.IOS.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.IOS.NAME;
|
||||
ctx.client.platform = 'MOBILE';
|
||||
ctx.client.osName = 'iOS';
|
||||
ctx.client.osName = Constants.CLIENTS.IOS.NAME;
|
||||
ctx.client.osVersion = Constants.CLIENTS.IOS.OS_VERSION;
|
||||
delete ctx.client.browserName;
|
||||
delete ctx.client.browserVersion;
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user