mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-15 02:22:11 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac9341c769 | ||
|
|
cac762569a | ||
|
|
9978ebf085 | ||
|
|
b036e2fcdc | ||
|
|
e37f42f41b | ||
|
|
883a023624 | ||
|
|
506834b253 |
52
README.md
52
README.md
@@ -120,7 +120,7 @@ Create an InnerTube instance:
|
||||
// const { Innertube } = require('youtubei.js');
|
||||
import { Innertube } from 'youtubei.js';
|
||||
const youtube = await Innertube.create();
|
||||
```****
|
||||
```
|
||||
|
||||
## Browser Usage
|
||||
To use YouTube.js in the browser you must proxy requests through your own server. You can see our simple reference implementation in Deno in [`examples/browser/proxy/deno.ts`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts).
|
||||
@@ -582,23 +582,23 @@ For example, let's say we want to implement a method to retrieve video info manu
|
||||
```ts
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
(async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
async function getVideoInfo(videoId: string) {
|
||||
const payload = {
|
||||
// anything added here will be merged with the default payload and sent to InnerTube.
|
||||
videoId,
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB or TV_EMBEDDED
|
||||
parse: true // tells YouTube.js to parse the response, this is not sent to InnerTube.
|
||||
};
|
||||
async function getVideoInfo(videoId: string) {
|
||||
const videoInfo = await yt.actions.execute('/player', {
|
||||
// anything added here will be merged with the default payload and sent to InnerTube.
|
||||
videoId,
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB or TV_EMBEDDED
|
||||
parse: true // tells YouTube.js to parse the response, this is not sent to InnerTube.
|
||||
});
|
||||
|
||||
const videoInfo = await yt.actions.execute('/player', payload);
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
const videoInfo = await getVideoInfo('jLTOuvBTLxA');
|
||||
console.info(videoInfo);
|
||||
const videoInfo = await getVideoInfo('jLTOuvBTLxA');
|
||||
console.info(videoInfo);
|
||||
})();
|
||||
```
|
||||
|
||||
Or perhaps there's a `NavigationEndpoint` in a parsed response and we want to call it to see what happens:
|
||||
@@ -606,19 +606,21 @@ Or perhaps there's a `NavigationEndpoint` in a parsed response and we want to ca
|
||||
```ts
|
||||
import { Innertube, YTNodes } from 'youtubei.js';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
(async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
|
||||
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
|
||||
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
|
||||
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
|
||||
|
||||
// Say we want to click the “More” button:
|
||||
const button = albums.as(YTNodes.MusicCarouselShelf).header?.more_content;
|
||||
// Say we want to click the “More” button:
|
||||
const button = albums.as(YTNodes.MusicCarouselShelf).header?.more_content;
|
||||
|
||||
if (button) {
|
||||
// After making sure it exists, we can call its navigation endpoint:
|
||||
const page = await button.endpoint.call(yt.actions);
|
||||
console.info(page);
|
||||
}
|
||||
if (button) {
|
||||
// After making sure it exists, we can call its navigation endpoint:
|
||||
const page = await button.endpoint.call(yt.actions);
|
||||
console.info(page);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### Parser
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.5.2",
|
||||
"version": "2.6.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.5.2",
|
||||
"version": "2.6.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.5.2",
|
||||
"version": "2.6.0",
|
||||
"description": "Full-featured wrapper around YouTube's private API. Supports YouTube, YouTube Music and YouTube Studio (WIP).",
|
||||
"main": "./dist/index.js",
|
||||
"browser": "./bundle/browser.js",
|
||||
|
||||
@@ -11,9 +11,6 @@ import Playlist from '../parser/ytmusic/Playlist';
|
||||
import Recap from '../parser/ytmusic/Recap';
|
||||
|
||||
import Tab from '../parser/classes/Tab';
|
||||
import Tabbed from '../parser/classes/Tabbed';
|
||||
import SingleColumnMusicWatchNextResults from '../parser/classes/SingleColumnMusicWatchNextResults';
|
||||
import WatchNextTabbedResults from '../parser/classes/WatchNextTabbedResults';
|
||||
import SectionList from '../parser/classes/SectionList';
|
||||
|
||||
import Message from '../parser/classes/Message';
|
||||
@@ -235,13 +232,9 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Up next' });
|
||||
const tab = tabs?.[0];
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -287,20 +280,16 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Related' });
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
const shelves = page.contents.item().as(SectionList).contents.array().as(MusicCarouselShelf, MusicDescriptionShelf);
|
||||
const shelves = page.contents.item().as(SectionList).contents.as(MusicCarouselShelf, MusicDescriptionShelf);
|
||||
|
||||
return shelves;
|
||||
}
|
||||
@@ -318,13 +307,9 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Lyrics' });
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -334,7 +319,7 @@ class Music {
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
|
||||
|
||||
const section_list = page.contents.item().as(SectionList).contents.array();
|
||||
const section_list = page.contents.item().as(SectionList).contents;
|
||||
return section_list.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface Context {
|
||||
|
||||
export interface SessionOptions {
|
||||
lang?: string;
|
||||
location?: string;
|
||||
account_index?: number;
|
||||
device_category?: DeviceCategory;
|
||||
client_type?: ClientType;
|
||||
@@ -112,6 +113,7 @@ export default class Session extends EventEmitterLike {
|
||||
static async create(options: SessionOptions = {}) {
|
||||
const { context, api_key, api_version, account_index } = await Session.getSessionData(
|
||||
options.lang,
|
||||
options.location,
|
||||
options.account_index,
|
||||
options.device_category,
|
||||
options.client_type,
|
||||
@@ -123,6 +125,7 @@ export default class Session extends EventEmitterLike {
|
||||
|
||||
static async getSessionData(
|
||||
lang = 'en-US',
|
||||
location = '',
|
||||
account_index = 0,
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
client_name: ClientType = ClientType.WEB,
|
||||
@@ -157,7 +160,7 @@ export default class Session extends EventEmitterLike {
|
||||
const context: Context = {
|
||||
client: {
|
||||
hl: device_info[0],
|
||||
gl: device_info[2],
|
||||
gl: location || device_info[2],
|
||||
remoteHost: device_info[3],
|
||||
screenDensityFloat: 1,
|
||||
screenHeightPoints: 720,
|
||||
|
||||
@@ -17,7 +17,7 @@ class TabbedFeed extends Feed {
|
||||
return this.#tabs.map((tab) => tab.title.toString());
|
||||
}
|
||||
|
||||
async getTab(title: string) {
|
||||
async getTabByName(title: string) {
|
||||
const tab = this.#tabs.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
|
||||
|
||||
if (!tab)
|
||||
@@ -28,8 +28,19 @@ class TabbedFeed extends Feed {
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Failed to call endpoint');
|
||||
return new TabbedFeed(this.#actions, response.data, false);
|
||||
}
|
||||
|
||||
async getTabByURL(url: string) {
|
||||
const tab = this.#tabs.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError(`Tab "${url}" not found`);
|
||||
|
||||
if (tab.selected)
|
||||
return this;
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
return new TabbedFeed(this.#actions, response.data, false);
|
||||
}
|
||||
|
||||
@@ -6,17 +6,21 @@ import { YTNode } from '../helpers';
|
||||
class Button extends YTNode {
|
||||
static type = 'Button';
|
||||
|
||||
text: string;
|
||||
text?: string;
|
||||
|
||||
label;
|
||||
tooltip;
|
||||
icon_type;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
icon_type?: string;
|
||||
is_disabled?: boolean;
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.text = new Text(data.text).toString();
|
||||
|
||||
if (data.text) {
|
||||
this.text = new Text(data.text).toString();
|
||||
}
|
||||
|
||||
if (data.accessibility?.label) {
|
||||
this.label = data.accessibility?.label;
|
||||
@@ -30,6 +34,10 @@ class Button extends YTNode {
|
||||
this.icon_type = data.icon?.iconType;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'isDisabled')) {
|
||||
this.is_disabled = data.isDisabled;
|
||||
}
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import Parser from '../index';
|
||||
import Author from './misc/Author';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
|
||||
import type Button from './Button';
|
||||
import type ChannelHeaderLinks from './ChannelHeaderLinks';
|
||||
import type SubscribeButton from './SubscribeButton';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class C4TabbedHeader extends YTNode {
|
||||
static type = 'C4TabbedHeader';
|
||||
|
||||
author;
|
||||
banner;
|
||||
tv_banner;
|
||||
mobile_banner;
|
||||
subscribers;
|
||||
sponsor_button;
|
||||
subscribe_button;
|
||||
header_links;
|
||||
author: Author;
|
||||
banner: Thumbnail[];
|
||||
tv_banner: Thumbnail[];
|
||||
mobile_banner: Thumbnail[];
|
||||
subscribers: Text;
|
||||
videos_count: Text;
|
||||
sponsor_button: Button | null;
|
||||
subscribe_button: SubscribeButton | null;
|
||||
header_links: ChannelHeaderLinks | null;
|
||||
channel_handle: Text;
|
||||
channel_id: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -23,13 +31,16 @@ class C4TabbedHeader extends YTNode {
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
}, data.badges, data.avatar);
|
||||
|
||||
this.banner = data.banner ? Thumbnail.fromResponse(data.banner) : [];
|
||||
this.tv_banner = data.tvBanner ? Thumbnail.fromResponse(data.tvBanner) : [];
|
||||
this.mobile_banner = data.mobileBanner ? Thumbnail.fromResponse(data.mobileBanner) : [];
|
||||
this.banner = Thumbnail.fromResponse(data.banner);
|
||||
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
|
||||
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.sponsor_button = data.sponsorButton ? Parser.parseItem(data.sponsorButton) : undefined;
|
||||
this.subscribe_button = data.subscribeButton ? Parser.parseItem(data.subscribeButton) : undefined;
|
||||
this.header_links = data.headerLinks ? Parser.parse(data.headerLinks) : undefined;
|
||||
this.videos_count = new Text(data.videosCountText);
|
||||
this.sponsor_button = Parser.parseItem<Button>(data.sponsorButton);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
|
||||
this.channel_handle = new Text(data.channelHandleText);
|
||||
this.channel_id = data.channelId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import Parser from '..';
|
||||
|
||||
import Text from './misc/Text';
|
||||
import Author from './misc/Author';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text';
|
||||
|
||||
import type SubscribeButton from './SubscribeButton';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class Channel extends YTNode {
|
||||
@@ -10,7 +15,10 @@ class Channel extends YTNode {
|
||||
author: Author;
|
||||
subscribers: Text;
|
||||
videos: Text;
|
||||
long_byline: Text;
|
||||
short_byline: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
subscribe_button: SubscribeButton | null;
|
||||
description_snippet: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -22,9 +30,13 @@ class Channel extends YTNode {
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
}, data.ownerBadges, data.thumbnail);
|
||||
|
||||
// TODO: subscriberCountText is now the channel's handle and videoCountText is the subscriber count. Why haven't they renamed the properties?
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.videos = new Text(data.videoCountText);
|
||||
this.long_byline = new Text(data.longBylineText);
|
||||
this.short_byline = new Text(data.shortBylineText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
this.description_snippet = new Text(data.descriptionSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import Parser from '../index';
|
||||
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text';
|
||||
import Parser from '../index';
|
||||
|
||||
import type Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class ChannelAboutFullMetadata extends YTNode {
|
||||
@@ -11,13 +15,20 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
name: Text;
|
||||
avatar: Thumbnail[];
|
||||
canonical_channel_url: string;
|
||||
|
||||
primary_links: {
|
||||
endpoint: NavigationEndpoint;
|
||||
icon: Thumbnail[];
|
||||
title: Text;
|
||||
}[];
|
||||
|
||||
views: Text;
|
||||
joined: Text;
|
||||
description: Text;
|
||||
email_reveal: NavigationEndpoint;
|
||||
can_reveal_email: boolean;
|
||||
country: Text;
|
||||
buttons;
|
||||
buttons: Button[];
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -25,13 +36,20 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
this.name = new Text(data.title);
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.canonical_channel_url = data.canonicalChannelUrl;
|
||||
|
||||
this.primary_links = data.primaryLinks.map((link: any) => ({
|
||||
endpoint: new NavigationEndpoint(link.navigationEndpoint),
|
||||
icon: Thumbnail.fromResponse(link.icon),
|
||||
title: new Text(link.title)
|
||||
}));
|
||||
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.joined = new Text(data.joinedDateText);
|
||||
this.description = new Text(data.description);
|
||||
this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand);
|
||||
this.can_reveal_email = !data.signInForBusinessEmail;
|
||||
this.country = new Text(data.country);
|
||||
this.buttons = Parser.parse(data.actionButtons);
|
||||
this.buttons = Parser.parseArray<Button>(data.actionButtons);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import Author from './misc/Author';
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import type Menu from './menus/Menu';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class CompactVideo extends YTNode {
|
||||
@@ -25,7 +27,7 @@ class CompactVideo extends YTNode {
|
||||
|
||||
thumbnail_overlays;
|
||||
endpoint: NavigationEndpoint;
|
||||
menu;
|
||||
menu: Menu | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -43,9 +45,9 @@ class CompactVideo extends YTNode {
|
||||
seconds: timeToSeconds(new Text(data.lengthText).toString())
|
||||
};
|
||||
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
}
|
||||
|
||||
get best_thumbnail() {
|
||||
|
||||
41
src/parser/classes/ExpandableMetadata.ts
Normal file
41
src/parser/classes/ExpandableMetadata.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import Parser from '..';
|
||||
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
|
||||
import Button from './Button';
|
||||
import HorizontalCardList from './HorizontalCardList';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class ExpandableMetadata extends YTNode {
|
||||
static type = 'ExpandableMetadata';
|
||||
|
||||
header: {
|
||||
collapsed_title: Text;
|
||||
collapsed_thumbnail: Thumbnail[];
|
||||
collapsed_label: Text;
|
||||
expanded_title: Text;
|
||||
};
|
||||
|
||||
expanded_content: HorizontalCardList | null;
|
||||
expand_button: Button | null;
|
||||
collapse_button: Button | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.header = {
|
||||
collapsed_title: new Text(data.header.collapsedTitle),
|
||||
collapsed_thumbnail: Thumbnail.fromResponse(data.header.collapsedThumbnail),
|
||||
collapsed_label: new Text(data.header.collapsedLabel),
|
||||
expanded_title: new Text(data.header.expandedTitle)
|
||||
};
|
||||
|
||||
this.expanded_content = Parser.parseItem<HorizontalCardList>(data.expandedContent);
|
||||
this.expand_button = Parser.parseItem<Button>(data.expandButton);
|
||||
this.collapse_button = Parser.parseItem<Button>(data.collapseButton);
|
||||
}
|
||||
}
|
||||
|
||||
export default ExpandableMetadata;
|
||||
@@ -3,6 +3,9 @@ import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Author from './misc/Author';
|
||||
|
||||
import type Menu from './menus/Menu';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class GridVideo extends YTNode {
|
||||
@@ -14,12 +17,12 @@ class GridVideo extends YTNode {
|
||||
thumbnail_overlays;
|
||||
rich_thumbnail;
|
||||
published: Text;
|
||||
duration: Text | string;
|
||||
duration: Text | null;
|
||||
author: Author;
|
||||
views: Text;
|
||||
short_view_count: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
menu;
|
||||
menu: Menu | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -27,15 +30,15 @@ class GridVideo extends YTNode {
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
this.duration = data.lengthText ? new Text(data.lengthText) : length_alt?.text ? new Text(length_alt.text) : '';
|
||||
this.duration = data.lengthText ? new Text(data.lengthText) : length_alt?.text ? new Text(length_alt.text) : null;
|
||||
this.author = data.shortBylineText && new Author(data.shortBylineText, data.ownerBadges);
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Parser from '../index';
|
||||
import { YTNode } from '../helpers';
|
||||
import SearchRefinementCard from './SearchRefinementCard';
|
||||
import Button from './Button';
|
||||
import MacroMarkersListItem from './MacroMarkersListItem';
|
||||
|
||||
class HorizontalCardList extends YTNode {
|
||||
static type = 'HorizontalCardList';
|
||||
@@ -13,7 +14,7 @@ class HorizontalCardList extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.cards = Parser.parseArray<SearchRefinementCard>(data.cards, SearchRefinementCard);
|
||||
this.cards = Parser.parseArray<SearchRefinementCard | MacroMarkersListItem>(data.cards);
|
||||
this.header = Parser.parseItem(data.header);
|
||||
this.previous_button = Parser.parseItem<Button>(data.previousButton, Button);
|
||||
this.next_button = Parser.parseItem<Button>(data.nextButton, Button);
|
||||
|
||||
29
src/parser/classes/MacroMarkersListItem.ts
Normal file
29
src/parser/classes/MacroMarkersListItem.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MacroMarkersListItem extends YTNode {
|
||||
static type = 'MacroMarkersListItem';
|
||||
|
||||
title: Text;
|
||||
time_description: Text;
|
||||
thumbnail: Thumbnail[];
|
||||
on_tap_endpoint: NavigationEndpoint;
|
||||
layout: string;
|
||||
is_highlighted: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.title = new Text(data.title);
|
||||
this.time_description = new Text(data.timeDescription);
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.on_tap_endpoint = new NavigationEndpoint(data.onTap);
|
||||
this.layout = data.layout;
|
||||
this.is_highlighted = data.isHighlighted;
|
||||
}
|
||||
}
|
||||
|
||||
export default MacroMarkersListItem;
|
||||
@@ -5,7 +5,8 @@ class MetadataBadge extends YTNode {
|
||||
|
||||
icon_type?: string;
|
||||
style?: string;
|
||||
tooltip: string | null;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -18,7 +19,13 @@ class MetadataBadge extends YTNode {
|
||||
this.style = data.style;
|
||||
}
|
||||
|
||||
this.tooltip = data?.tooltip || data?.iconTooltip || null;
|
||||
if (data?.label) {
|
||||
this.style = data.label;
|
||||
}
|
||||
|
||||
if (data?.tooltip || data?.iconTooltip) {
|
||||
this.tooltip = data.tooltip || data.iconTooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ class PlaylistPanelVideo extends YTNode {
|
||||
}));
|
||||
}
|
||||
|
||||
this.badges = Parser.parse(data.badges);
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.badges = Parser.parseArray(data.badges);
|
||||
this.menu = Parser.parseItem(data.menu);
|
||||
this.set_video_id = data.playlistSetVideoId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import Parser from '../index';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import PlaylistAuthor from './misc/PlaylistAuthor';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import type Menu from './menus/Menu';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class PlaylistVideo extends YTNode {
|
||||
@@ -17,7 +19,7 @@ class PlaylistVideo extends YTNode {
|
||||
set_video_id: string | undefined;
|
||||
endpoint: NavigationEndpoint;
|
||||
is_playable: boolean;
|
||||
menu;
|
||||
menu: Menu | null;
|
||||
|
||||
duration: {
|
||||
text: string;
|
||||
@@ -35,7 +37,7 @@ class PlaylistVideo extends YTNode {
|
||||
this.set_video_id = data?.setVideoId;
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.is_playable = data.isPlayable;
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu);
|
||||
this.duration = {
|
||||
text: new Text(data.lengthText).text,
|
||||
seconds: parseInt(data.lengthSeconds)
|
||||
|
||||
@@ -16,7 +16,7 @@ class SectionList extends YTNode {
|
||||
}
|
||||
|
||||
// TODO: this should be Parser#parseArray
|
||||
this.contents = Parser.parse(data.contents);
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
|
||||
if (data.continuations) {
|
||||
if (data.continuations[0].nextContinuationData) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import Author from './misc/Author';
|
||||
import Menu from './menus/Menu';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import MetadataBadge from './MetadataBadge';
|
||||
import ExpandableMetadata from './ExpandableMetadata';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
import { YTNode } from '../helpers';
|
||||
@@ -18,11 +20,13 @@ class Video extends YTNode {
|
||||
text: Text;
|
||||
hover_text: Text;
|
||||
}[];
|
||||
expandable_metadata: ExpandableMetadata | null;
|
||||
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_overlays;
|
||||
rich_thumbnail;
|
||||
author: Author;
|
||||
badges: MetadataBadge[];
|
||||
endpoint: NavigationEndpoint;
|
||||
published: Text;
|
||||
view_count: Text;
|
||||
@@ -36,7 +40,8 @@ class Video extends YTNode {
|
||||
|
||||
show_action_menu: boolean;
|
||||
is_watched: boolean;
|
||||
menu;
|
||||
menu: Menu | null;
|
||||
search_video_result_entity_key: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -54,10 +59,13 @@ class Video extends YTNode {
|
||||
hover_text: new Text(snippet.snippetHoverText)
|
||||
})) || [];
|
||||
|
||||
this.expandable_metadata = Parser.parseItem<ExpandableMetadata>(data.expandableMetadata);
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail ? Parser.parseItem(data.richThumbnail) : null;
|
||||
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);
|
||||
@@ -76,6 +84,7 @@ class Video extends YTNode {
|
||||
this.show_action_menu = data.showActionMenu;
|
||||
this.is_watched = data.isWatched || false;
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
this.search_video_result_entity_key = data.searchVideoResultEntityKey;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
@@ -85,22 +94,30 @@ class Video extends YTNode {
|
||||
return this.description_snippet?.toString() || '';
|
||||
}
|
||||
|
||||
/*
|
||||
Get is_live() {
|
||||
return this.badges.some((badge) => badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW');
|
||||
get is_live(): boolean {
|
||||
return this.badges.some((badge) => {
|
||||
if (badge.label === 'BADGE_STYLE_TYPE_LIVE_NOW' || badge.style === 'LIVE')
|
||||
return true;
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
get is_upcoming(): boolean | undefined {
|
||||
return this.upcoming && this.upcoming > new Date();
|
||||
}
|
||||
|
||||
/*
|
||||
Get has_captions() {
|
||||
return this.badges.some((badge) => badge.label === 'CC');
|
||||
}*/
|
||||
get is_premiere(): boolean {
|
||||
return this.badges.some((badge) => badge.style === 'PREMIERE');
|
||||
}
|
||||
|
||||
get best_thumbnail(): Thumbnail | undefined{
|
||||
get is_4k(): boolean {
|
||||
return this.badges.some((badge) => badge.style === '4K');
|
||||
}
|
||||
|
||||
get has_captions(): boolean {
|
||||
return this.badges.some((badge) => badge.style === 'CC');
|
||||
}
|
||||
|
||||
get best_thumbnail(): Thumbnail | undefined {
|
||||
return this.thumbnails[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,15 @@ import NavigationEndpoint from '../NavigationEndpoint';
|
||||
class TextRun {
|
||||
text: string;
|
||||
endpoint: NavigationEndpoint | undefined;
|
||||
bold: boolean;
|
||||
italics: boolean;
|
||||
strikethrough: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
this.text = data.text;
|
||||
this.bold = Boolean(data.bold);
|
||||
this.italics = Boolean(data.italics);
|
||||
this.strikethrough = Boolean(data.strikethrough);
|
||||
this.endpoint = data.navigationEndpoint ? new NavigationEndpoint(data.navigationEndpoint) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,6 +359,10 @@ export type ObservedArray<T extends YTNode = YTNode> = Array<T> & {
|
||||
* Returns all objects that match the rule.
|
||||
*/
|
||||
getAll: (rule: object, del_items?: boolean) => T[];
|
||||
/**
|
||||
* Returns the first object to match the condition.
|
||||
*/
|
||||
matchCondition: (condition: (node: T) => boolean) => T | undefined;
|
||||
/**
|
||||
* Removes the item at the given index.
|
||||
*/
|
||||
@@ -412,6 +416,14 @@ export function observe<T extends YTNode>(obj: Array<T>) {
|
||||
);
|
||||
}
|
||||
|
||||
if (prop == 'matchCondition') {
|
||||
return (condition: (node: T) => boolean) => (
|
||||
target.find((obj) => {
|
||||
return condition(obj);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (prop == 'filterType') {
|
||||
return (...types: YTNodeConstructor<YTNode>[]) => {
|
||||
return observe(target.filter((node: YTNode) => {
|
||||
|
||||
@@ -73,6 +73,7 @@ import { default as Endscreen } from './classes/Endscreen';
|
||||
import { default as EndscreenElement } from './classes/EndscreenElement';
|
||||
import { default as EndScreenPlaylist } from './classes/EndScreenPlaylist';
|
||||
import { default as EndScreenVideo } from './classes/EndScreenVideo';
|
||||
import { default as ExpandableMetadata } from './classes/ExpandableMetadata';
|
||||
import { default as ExpandableTab } from './classes/ExpandableTab';
|
||||
import { default as ExpandedShelfContents } from './classes/ExpandedShelfContents';
|
||||
import { default as FeedFilterChipBar } from './classes/FeedFilterChipBar';
|
||||
@@ -137,6 +138,7 @@ import { default as LiveChatItemList } from './classes/LiveChatItemList';
|
||||
import { default as LiveChatMessageInput } from './classes/LiveChatMessageInput';
|
||||
import { default as LiveChatParticipant } from './classes/LiveChatParticipant';
|
||||
import { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList';
|
||||
import { default as MacroMarkersListItem } from './classes/MacroMarkersListItem';
|
||||
import { default as Menu } from './classes/menus/Menu';
|
||||
import { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem';
|
||||
import { default as MenuServiceItem } from './classes/menus/MenuServiceItem';
|
||||
@@ -363,6 +365,7 @@ export const YTNodes = {
|
||||
EndscreenElement,
|
||||
EndScreenPlaylist,
|
||||
EndScreenVideo,
|
||||
ExpandableMetadata,
|
||||
ExpandableTab,
|
||||
ExpandedShelfContents,
|
||||
FeedFilterChipBar,
|
||||
@@ -427,6 +430,7 @@ export const YTNodes = {
|
||||
LiveChatMessageInput,
|
||||
LiveChatParticipant,
|
||||
LiveChatParticipantsList,
|
||||
MacroMarkersListItem,
|
||||
Menu,
|
||||
MenuNavigationItem,
|
||||
MenuServiceItem,
|
||||
|
||||
@@ -9,12 +9,13 @@ import MicroformatData from '../classes/MicroformatData';
|
||||
import SubscribeButton from '../classes/SubscribeButton';
|
||||
import Tab from '../classes/Tab';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
import FeedFilterChipBar from '../classes/FeedFilterChipBar';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip';
|
||||
import FilterableFeed from '../../core/FilterableFeed';
|
||||
import Feed from '../../core/Feed';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
|
||||
export default class Channel extends TabbedFeed {
|
||||
header;
|
||||
metadata;
|
||||
@@ -70,37 +71,37 @@ export default class Channel extends TabbedFeed {
|
||||
}
|
||||
|
||||
async getHome() {
|
||||
const tab = await this.getTab('Home');
|
||||
const tab = await this.getTabByURL('featured');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getVideos() {
|
||||
const tab = await this.getTab('Videos');
|
||||
const tab = await this.getTabByURL('videos');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getShorts() {
|
||||
const tab = await this.getTab('Shorts');
|
||||
const tab = await this.getTabByURL('shorts');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getLiveStreams() {
|
||||
const tab = await this.getTab('Live');
|
||||
const tab = await this.getTabByURL('streams');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getPlaylists() {
|
||||
const tab = await this.getTab('Playlists');
|
||||
const tab = await this.getTabByURL('playlists');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getCommunity() {
|
||||
const tab = await this.getTab('Community');
|
||||
const tab = await this.getTabByURL('community');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getChannels() {
|
||||
const tab = await this.getTab('Channels');
|
||||
const tab = await this.getTabByURL('channels');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ export default class Channel extends TabbedFeed {
|
||||
* Note that this does not return a new {@link Channel} object.
|
||||
*/
|
||||
async getAbout() {
|
||||
const tab = await this.getTab('About');
|
||||
const tab = await this.getTabByURL('about');
|
||||
return tab.memo.getType(ChannelAboutFullMetadata)?.[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class Search extends Feed {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents_memo.getType(SectionList)?.[0]?.contents?.array() ||
|
||||
this.page.contents_memo.getType(SectionList)?.[0]?.contents ||
|
||||
this.page.on_response_received_commands?.[0].contents;
|
||||
|
||||
this.results = contents.firstOfType(ItemSection)?.contents;
|
||||
@@ -40,7 +40,7 @@ export default class Search extends Feed {
|
||||
|
||||
if (typeof card === 'string') {
|
||||
if (!this.refinement_cards) throw new InnertubeError('No refinement cards found.');
|
||||
target_card = this.refinement_cards?.cards.get({ query: card });
|
||||
target_card = this.refinement_cards?.cards.get({ query: card })?.as(SearchRefinementCard);
|
||||
if (!target_card)
|
||||
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
|
||||
} else if (card.type === 'SearchRefinementCard') {
|
||||
@@ -58,7 +58,7 @@ export default class Search extends Feed {
|
||||
* Returns a list of refinement card queries.
|
||||
*/
|
||||
get refinement_card_queries() {
|
||||
return this.refinement_cards?.cards.map((card) => card.query);
|
||||
return this.refinement_cards?.cards.as(SearchRefinementCard).map((card) => card.query);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ class Settings {
|
||||
if (!tab)
|
||||
throw new InnertubeError('Target tab not found');
|
||||
|
||||
const contents = tab.content?.as(SectionList).contents.array().as(ItemSection);
|
||||
const contents = tab.content?.as(SectionList).contents.as(ItemSection);
|
||||
|
||||
this.introduction = contents?.shift()?.contents?.get({ type: 'PageIntroduction' })?.as(PageIntroduction);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class TimeWatched {
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
this.contents = tab.content?.as(SectionList).contents.array().as(ItemSection);
|
||||
this.contents = tab.content?.as(SectionList).contents.as(ItemSection);
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
|
||||
@@ -28,8 +28,8 @@ class Explore {
|
||||
if (!section_list)
|
||||
throw new InnertubeError('Target tab did not have any content.');
|
||||
|
||||
this.top_buttons = section_list.contents.array().firstOfType(Grid)?.items.as(MusicNavigationButton) || ([] as MusicNavigationButton[]);
|
||||
this.sections = section_list.contents.array().getAll({ type: 'MusicCarouselShelf' }) as MusicCarouselShelf[];
|
||||
this.top_buttons = section_list.contents.firstOfType(Grid)?.items.as(MusicNavigationButton) || ([] as MusicNavigationButton[]);
|
||||
this.sections = section_list.contents.getAll({ type: 'MusicCarouselShelf' }) as MusicCarouselShelf[];
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
|
||||
@@ -33,7 +33,7 @@ class HomeFeed {
|
||||
}
|
||||
|
||||
this.#continuation = tab.content?.as(SectionList).continuation;
|
||||
this.sections = tab.content?.as(SectionList).contents.array().as(MusicCarouselShelf);
|
||||
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ class Library {
|
||||
const section_list = this.#page.contents_memo.getType(SectionList)?.[0];
|
||||
|
||||
this.header = section_list?.header?.item().as(MusicSideAlignedItem);
|
||||
this.contents = section_list?.contents?.array().as(Grid, MusicShelf);
|
||||
this.contents = section_list?.contents?.as(Grid, MusicShelf);
|
||||
|
||||
this.#continuation = this.contents?.find((list: Grid | MusicShelf) => list.continuation)?.continuation;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class Playlist {
|
||||
/**
|
||||
* Retrieves related playlists
|
||||
*/
|
||||
async getRelated() {
|
||||
async getRelated(): Promise<MusicCarouselShelf> {
|
||||
let section_continuation = this.#page.contents_memo.get('SectionList')?.[0].as(SectionList).continuation;
|
||||
|
||||
while (section_continuation) {
|
||||
@@ -74,19 +74,15 @@ class Playlist {
|
||||
const section_list = data.continuation_contents?.as(SectionListContinuation);
|
||||
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
|
||||
|
||||
const related = sections?.filter(
|
||||
(section) =>
|
||||
section.is(MusicCarouselShelf) ? section.header?.title.toString() === 'Related playlists' :
|
||||
section.title.toString() === 'Related playlists'
|
||||
)[0];
|
||||
const related = sections?.matchCondition((section) => section.is(MusicCarouselShelf))?.as(MusicCarouselShelf);
|
||||
|
||||
if (related)
|
||||
return related.contents || [];
|
||||
return related;
|
||||
|
||||
section_continuation = section_list?.continuation;
|
||||
}
|
||||
|
||||
return [];
|
||||
throw new InnertubeError('Target section not found.');
|
||||
}
|
||||
|
||||
async getSuggestions(refresh = true) {
|
||||
@@ -115,9 +111,7 @@ class Playlist {
|
||||
const section_list = page.continuation_contents?.as(SectionListContinuation);
|
||||
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
|
||||
|
||||
const suggestions = sections?.filter(
|
||||
(section) => section.is(MusicShelf) && section.title.toString() === 'Suggestions'
|
||||
)[0] as MusicShelf | undefined;
|
||||
const suggestions = sections?.matchCondition((section) => section.is(MusicShelf))?.as(MusicShelf);
|
||||
|
||||
return {
|
||||
items: suggestions?.contents || [],
|
||||
|
||||
@@ -37,7 +37,7 @@ class Recap {
|
||||
if (!tab)
|
||||
throw new InnertubeError('Target tab not found');
|
||||
|
||||
this.sections = tab.content?.as(SectionList).contents.array().as(ItemSection, MusicCarouselShelf, Message);
|
||||
this.sections = tab.content?.as(SectionList).contents.as(ItemSection, MusicCarouselShelf, Message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,7 +49,7 @@ class Search {
|
||||
|
||||
this.header = tab_content.hasKey('header') ? tab_content.header?.item().as(ChipCloud) : null;
|
||||
|
||||
const shelves = tab_content.contents.array().as(MusicShelf, ItemSection);
|
||||
const shelves = tab_content.contents.as(MusicShelf, ItemSection);
|
||||
const item_section = shelves.firstOfType(ItemSection);
|
||||
|
||||
this.did_you_mean = item_section?.contents?.firstOfType(DidYouMean) || null;
|
||||
|
||||
@@ -4,9 +4,7 @@ import Constants from '../../utils/Constants';
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
|
||||
import Tab from '../classes/Tab';
|
||||
import Tabbed from '../classes/Tabbed';
|
||||
import WatchNextTabbedResults from '../classes/WatchNextTabbedResults';
|
||||
import SingleColumnMusicWatchNextResults from '../classes/SingleColumnMusicWatchNextResults';
|
||||
import MicroformatData from '../classes/MicroformatData';
|
||||
import PlayerOverlay from '../classes/PlayerOverlay';
|
||||
import PlaylistPanel from '../classes/PlaylistPanel';
|
||||
@@ -70,8 +68,7 @@ class TrackInfo {
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
|
||||
if (next) {
|
||||
const single_col = next.contents.item().as(SingleColumnMusicWatchNextResults);
|
||||
const tabbed_results = single_col.contents.item().as(Tabbed).contents.item().as(WatchNextTabbedResults);
|
||||
const tabbed_results = next.contents_memo.getType(WatchNextTabbedResults)?.[0];
|
||||
|
||||
this.tabs = tabbed_results.tabs.array().as(Tab);
|
||||
this.current_video_endpoint = next.current_video_endpoint;
|
||||
@@ -84,14 +81,17 @@ class TrackInfo {
|
||||
/**
|
||||
* Retrieves contents of the given tab.
|
||||
*/
|
||||
async getTab(title: string) {
|
||||
async getTab(title_or_page_type: string) {
|
||||
if (!this.tabs)
|
||||
throw new InnertubeError('Could not find any tab');
|
||||
|
||||
const target_tab = this.tabs.get({ title });
|
||||
const target_tab =
|
||||
this.tabs.get({ title: title_or_page_type }) ||
|
||||
this.tabs.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === title_or_page_type) ||
|
||||
this.tabs?.[0];
|
||||
|
||||
if (!target_tab)
|
||||
throw new InnertubeError(`Tab "${title}" not found`, { available_tabs: this.available_tabs });
|
||||
throw new InnertubeError(`Tab "${title_or_page_type}" not found`, { available_tabs: this.available_tabs });
|
||||
|
||||
if (target_tab.content)
|
||||
return target_tab.content;
|
||||
@@ -101,7 +101,7 @@ class TrackInfo {
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
return page.contents.item().as(Message);
|
||||
|
||||
return page.contents.item().as(SectionList).contents.array();
|
||||
return page.contents.item().as(SectionList).contents;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +140,7 @@ class TrackInfo {
|
||||
* Retrieves related content.
|
||||
*/
|
||||
async getRelated(): Promise<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
|
||||
const tab = await this.getTab('Related') as ObservedArray<MusicDescriptionShelf | MusicDescriptionShelf>;
|
||||
const tab = await this.getTab('MUSIC_PAGE_TYPE_TRACK_RELATED') as ObservedArray<MusicDescriptionShelf | MusicDescriptionShelf>;
|
||||
return tab;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ class TrackInfo {
|
||||
* Retrieves lyrics.
|
||||
*/
|
||||
async getLyrics(): Promise<MusicDescriptionShelf | undefined> {
|
||||
const tab = await this.getTab('Lyrics') as ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>;
|
||||
const tab = await this.getTab('MUSIC_PAGE_TYPE_TRACK_LYRICS') as ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>;
|
||||
return tab.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class HTTPClient {
|
||||
const request_headers = new Headers(headers);
|
||||
|
||||
request_headers.set('Accept', '*/*');
|
||||
request_headers.set('Accept-Language', `en-${this.#session.context.client.gl || 'US'}`);
|
||||
request_headers.set('Accept-Language', `${this.#session.context.client.hl}-${this.#session.context.client.gl}`);
|
||||
request_headers.set('x-goog-visitor-id', this.#session.context.client.visitorData || '');
|
||||
request_headers.set('x-origin', request_url.origin);
|
||||
request_headers.set('x-youtube-client-version', this.#session.context.client.clientVersion || '');
|
||||
|
||||
@@ -10,6 +10,10 @@ export const VIDEOS = [
|
||||
{
|
||||
ID: 'I1qsF0WQy8c',
|
||||
QUERY: 'mkbhd',
|
||||
},
|
||||
{
|
||||
ID: 'OqiXFXlYFi8',
|
||||
QUERY: 'formatted comment text'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'fs';
|
||||
import Innertube from '..';
|
||||
import { CHANNELS, VIDEOS } from './constants';
|
||||
import { streamToIterable } from '../src/utils/Utils';
|
||||
import TextRun from '../src/parser/classes/misc/TextRun';
|
||||
|
||||
describe('YouTube.js Tests', () => {
|
||||
let yt: Innertube;
|
||||
@@ -68,6 +69,19 @@ describe('YouTube.js Tests', () => {
|
||||
threads = await yt.getComments(VIDEOS[1].ID);
|
||||
expect(threads.contents.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should parse formatted comments', async () => {
|
||||
const threads = await yt.getComments(VIDEOS[3].ID);
|
||||
const authorComment = threads.contents.find(t => t.comment?.author_is_channel_owner)
|
||||
expect(authorComment).not.toBeUndefined();
|
||||
|
||||
expect(authorComment!.comment?.content.runs?.length).toBeGreaterThan(0)
|
||||
const runs = authorComment!.comment!.content.runs! as TextRun[]
|
||||
|
||||
expect(runs[0].bold).toBeTruthy()
|
||||
expect(runs[2].italics).toBeTruthy()
|
||||
expect(runs[4].strikethrough).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should retrieve next batch of comments', async () => {
|
||||
const next = await threads.getContinuation();
|
||||
|
||||
Reference in New Issue
Block a user