mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 10:02:16 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f66f0bd656 | ||
|
|
05de3ec97a | ||
|
|
a0566969ba | ||
|
|
a9cad49333 | ||
|
|
096bf362c9 | ||
|
|
ec9c0979f5 | ||
|
|
342d1d95e9 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [4.3.0](https://github.com/LuanRT/YouTube.js/compare/v4.2.0...v4.3.0) (2023-04-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **GridVideo:** add `upcoming`, `upcoming_text`, `is_reminder_set` and `buttons` ([05de3ec](https://github.com/LuanRT/YouTube.js/commit/05de3ec97a1fea92543b5e5f84933b86a07ab830)), closes [#380](https://github.com/LuanRT/YouTube.js/issues/380)
|
||||
* **MusicResponsiveListItem:** make flex/fixed cols public ([#382](https://github.com/LuanRT/YouTube.js/issues/382)) ([096bf36](https://github.com/LuanRT/YouTube.js/commit/096bf362c9bd46a510ecb0d01623c70841e26e26))
|
||||
* **ToggleMenuServiceItem:** parse default nav endpoint ([a056696](https://github.com/LuanRT/YouTube.js/commit/a0566969ba436f31ca3722d09442a0c6302235d7))
|
||||
* **ytmusic:** add taste builder nodes ([#383](https://github.com/LuanRT/YouTube.js/issues/383)) ([a9cad49](https://github.com/LuanRT/YouTube.js/commit/a9cad49333aa85c98bbb96e5f2d5b57d9beeb0c7))
|
||||
|
||||
## [4.2.0](https://github.com/LuanRT/YouTube.js/compare/v4.1.1...v4.2.0) (2023-04-09)
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://serpapi.com#gh-light-mode-only" target="_blank">
|
||||
<a href="https://serpapi.com" target="_blank">
|
||||
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
|
||||
<br>
|
||||
<sub>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
|
||||
@@ -2,8 +2,9 @@ import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class Button extends YTNode {
|
||||
export default class Button extends YTNode {
|
||||
static type = 'Button';
|
||||
|
||||
text?: string;
|
||||
@@ -15,7 +16,7 @@ class Button extends YTNode {
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
if (data.text) {
|
||||
@@ -40,6 +41,4 @@ class Button extends YTNode {
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import Menu from './menus/Menu.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GridVideo extends YTNode {
|
||||
export default class GridVideo extends YTNode {
|
||||
static type = 'GridVideo';
|
||||
|
||||
id: string;
|
||||
@@ -23,10 +23,15 @@ class GridVideo extends YTNode {
|
||||
short_view_count: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
menu: Menu | null;
|
||||
buttons?;
|
||||
upcoming?: Date;
|
||||
upcoming_text?: Text;
|
||||
is_reminder_set?: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const length_alt = data.thumbnailOverlays.find((overlay: any) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer;
|
||||
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
@@ -39,7 +44,19 @@ class GridVideo extends YTNode {
|
||||
this.short_view_count = new Text(data.shortViewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
|
||||
export default GridVideo;
|
||||
if (Reflect.has(data, 'buttons')) {
|
||||
this.buttons = Parser.parseArray(data.buttons);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'upcomingEventData')) {
|
||||
this.upcoming = new Date(Number(`${data.upcomingEventData.startTime}000`));
|
||||
this.upcoming_text = new Text(data.upcomingEventData.upcomingEventText);
|
||||
this.is_reminder_set = !!data.upcomingEventData?.isReminderSet;
|
||||
}
|
||||
}
|
||||
|
||||
get is_upcoming(): boolean {
|
||||
return Boolean(this.upcoming && this.upcoming > new Date());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser from '../parser.js';
|
||||
import GuideEntry from './GuideEntry.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GuideCollapsibleEntry extends YTNode {
|
||||
export default class GuideCollapsibleEntry extends YTNode {
|
||||
static type = 'GuideCollapsibleEntry';
|
||||
|
||||
expander_item: {
|
||||
title: string,
|
||||
icon_type: string
|
||||
};
|
||||
collapser_item: {
|
||||
title: string,
|
||||
icon_type: string
|
||||
};
|
||||
expander_item: GuideEntry | null;
|
||||
collapser_item: GuideEntry | null;
|
||||
expandable_items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.expander_item = {
|
||||
title: new Text(data.expanderItem.guideEntryRenderer.formattedTitle).toString(),
|
||||
icon_type: data.expanderItem.guideEntryRenderer.icon.iconType
|
||||
};
|
||||
|
||||
this.collapser_item = {
|
||||
title: new Text(data.collapserItem.guideEntryRenderer.formattedTitle).toString(),
|
||||
icon_type: data.collapserItem.guideEntryRenderer.icon.iconType
|
||||
};
|
||||
|
||||
this.expander_item = Parser.parseItem(data.expanderItem, GuideEntry);
|
||||
this.collapser_item = Parser.parseItem(data.collapserItem, GuideEntry);
|
||||
this.expandable_items = Parser.parseArray(data.expandableItems);
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideCollapsibleEntry;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser from '../parser.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class GuideCollapsibleSectionEntry extends YTNode {
|
||||
export default class GuideCollapsibleSectionEntry extends YTNode {
|
||||
static type = 'GuideCollapsibleSectionEntry';
|
||||
|
||||
header_entry;
|
||||
@@ -9,15 +10,11 @@ class GuideCollapsibleSectionEntry extends YTNode {
|
||||
collapser_icon: string;
|
||||
section_items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.header_entry = Parser.parseItem(data.headerEntry);
|
||||
this.expander_icon = data.expanderIcon.iconType;
|
||||
this.collapser_icon = data.collapserIcon.iconType;
|
||||
this.section_items = Parser.parseArray(data.sectionItems);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideCollapsibleSectionEntry;
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import GuideEntry from './GuideEntry.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class GuideDownloadsEntry extends GuideEntry {
|
||||
export default class GuideDownloadsEntry extends GuideEntry {
|
||||
static type = 'GuideDownloadsEntry';
|
||||
|
||||
always_show: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super(data.entryRenderer.guideEntryRenderer);
|
||||
this.always_show = !!data.alwaysShow;
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideDownloadsEntry;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
class GuideEntry extends YTNode {
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class GuideEntry extends YTNode {
|
||||
static type = 'GuideEntry';
|
||||
|
||||
title: Text;
|
||||
@@ -13,21 +15,24 @@ class GuideEntry extends YTNode {
|
||||
badges?: any;
|
||||
is_primary: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.formattedTitle);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint);
|
||||
if (data.icon?.iconType) {
|
||||
|
||||
if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType')) {
|
||||
this.icon_type = data.icon.iconType;
|
||||
}
|
||||
if (data.thumbnail) {
|
||||
|
||||
if (Reflect.has(data, 'thumbnail')) {
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
if (data.badges) {
|
||||
|
||||
// (LuanRT) XXX: Check this property's data and parse it.
|
||||
if (Reflect.has(data, 'badges')) {
|
||||
this.badges = data.badges;
|
||||
}
|
||||
|
||||
this.is_primary = !!data.isPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideEntry;
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser from '../parser.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class GuideSection extends YTNode {
|
||||
export default class GuideSection extends YTNode {
|
||||
static type = 'GuideSection';
|
||||
|
||||
title?: Text;
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if (data.formattedTitle) {
|
||||
if (Reflect.has(data, 'formattedTitle')) {
|
||||
this.title = new Text(data.formattedTitle);
|
||||
}
|
||||
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
}
|
||||
|
||||
export default GuideSection;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import GuideSection from './GuideSection.js';
|
||||
|
||||
class GuideSubscriptionsSection extends GuideSection {
|
||||
export default class GuideSubscriptionsSection extends GuideSection {
|
||||
static type = 'GuideSubscriptionsSection';
|
||||
}
|
||||
|
||||
export default GuideSubscriptionsSection;
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
// TODO: this needs a refactor
|
||||
// Seems like a mess to use
|
||||
// TODO: Clean up and refactor this.
|
||||
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import TextRun from './misc/TextRun.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.js';
|
||||
import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn.js';
|
||||
import MusicResponsiveListItemFixedColumn from './MusicResponsiveListItemFixedColumn.js';
|
||||
import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn.js';
|
||||
import MusicThumbnail from './MusicThumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { isTextRun, timeToSeconds } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class MusicResponsiveListItem extends YTNode {
|
||||
export default class MusicResponsiveListItem extends YTNode {
|
||||
static type = 'MusicResponsiveListItem';
|
||||
|
||||
#flex_columns;
|
||||
#fixed_columns;
|
||||
flex_columns: ObservedArray<MusicResponsiveListItemFlexColumn>;
|
||||
fixed_columns: ObservedArray<MusicResponsiveListItemFixedColumn>;
|
||||
#playlist_item_data;
|
||||
|
||||
endpoint;
|
||||
item_type;
|
||||
index;
|
||||
thumbnails;
|
||||
endpoint: NavigationEndpoint | null;
|
||||
item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'video' | 'song' | 'endpoint' | 'unknown' | undefined;
|
||||
index?: Text;
|
||||
thumbnail?: MusicThumbnail | null;
|
||||
badges;
|
||||
menu;
|
||||
overlay;
|
||||
menu?: Menu | null;
|
||||
overlay?: MusicItemThumbnailOverlay | null;
|
||||
|
||||
id?: string;
|
||||
title?: string;
|
||||
@@ -59,19 +59,20 @@ class MusicResponsiveListItem extends YTNode {
|
||||
subtitle?: Text;
|
||||
subscribers?: string;
|
||||
song_count?: string;
|
||||
|
||||
// TODO: these might be replaceable with Author class
|
||||
author?: {
|
||||
name: string,
|
||||
channel_id?: string
|
||||
endpoint?: NavigationEndpoint
|
||||
};
|
||||
item_count?: string | undefined;
|
||||
item_count?: string;
|
||||
year?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.#flex_columns = Parser.parseArray<MusicResponsiveListItemFlexColumn>(data.flexColumns, MusicResponsiveListItemFlexColumn);
|
||||
this.#fixed_columns = Parser.parseArray<MusicResponsiveListItemFixedColumn>(data.fixedColumns, MusicResponsiveListItemFixedColumn);
|
||||
this.flex_columns = Parser.parseArray(data.flexColumns, MusicResponsiveListItemFlexColumn);
|
||||
this.fixed_columns = Parser.parseArray(data.fixedColumns, MusicResponsiveListItemFixedColumn);
|
||||
|
||||
this.#playlist_item_data = {
|
||||
video_id: data?.playlistItemData?.videoId || null,
|
||||
@@ -101,7 +102,7 @@ class MusicResponsiveListItem extends YTNode {
|
||||
this.#parseLibraryArtist();
|
||||
break;
|
||||
default:
|
||||
if (this.#flex_columns[1]) {
|
||||
if (this.flex_columns[1]) {
|
||||
this.#parseVideoOrSong();
|
||||
} else {
|
||||
this.#parseOther();
|
||||
@@ -113,14 +114,14 @@ class MusicResponsiveListItem extends YTNode {
|
||||
this.index = new Text(data.index);
|
||||
}
|
||||
|
||||
this.thumbnails = data.thumbnail ? Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer?.thumbnail) : [];
|
||||
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
|
||||
this.badges = Parser.parseArray(data.badges);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
this.overlay = Parser.parseItem<MusicItemThumbnailOverlay>(data.overlay, MusicItemThumbnailOverlay);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay);
|
||||
}
|
||||
|
||||
#parseOther() {
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
|
||||
if (this.endpoint) {
|
||||
this.item_type = 'endpoint';
|
||||
@@ -130,7 +131,7 @@ class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
|
||||
#parseVideoOrSong() {
|
||||
const is_video = this.#flex_columns[1].key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/));
|
||||
const is_video = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/));
|
||||
if (is_video) {
|
||||
this.item_type = 'video';
|
||||
this.#parseVideo();
|
||||
@@ -142,105 +143,144 @@ class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
#parseSong() {
|
||||
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
|
||||
const duration_text =
|
||||
this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text ||
|
||||
this.#fixed_columns?.[0]?.key('title').instanceof(Text)?.toString();
|
||||
const duration_text = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.key('title').instanceof(Text)?.toString();
|
||||
|
||||
duration_text && (this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
});
|
||||
|
||||
const album = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('MPR')) as TextRun ||
|
||||
this.#flex_columns[2]?.key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('MPR')) as TextRun;
|
||||
if (album) {
|
||||
this.album = {
|
||||
id: album.endpoint?.payload?.browseId,
|
||||
name: album.text,
|
||||
endpoint: album.endpoint
|
||||
if (duration_text) {
|
||||
this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
};
|
||||
}
|
||||
|
||||
const artists = this.#flex_columns[1].key('title').instanceof(Text).runs?.filter((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun[];
|
||||
if (artists) {
|
||||
this.artists = artists.map((artist) => ({
|
||||
name: artist.text,
|
||||
channel_id: artist.endpoint?.payload?.browseId,
|
||||
endpoint: artist.endpoint
|
||||
const album_run =
|
||||
this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('MPR')
|
||||
) ||
|
||||
this.flex_columns.at(2)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('MPR')
|
||||
);
|
||||
|
||||
if (album_run && isTextRun(album_run)) {
|
||||
this.album = {
|
||||
id: album_run.endpoint?.payload?.browseId,
|
||||
name: album_run.text,
|
||||
endpoint: album_run.endpoint
|
||||
};
|
||||
}
|
||||
|
||||
const artist_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter(
|
||||
(run) => (isTextRun(run) && run.endpoint) && run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
if (artist_runs) {
|
||||
this.artists = artist_runs.map((run) => ({
|
||||
name: run.text,
|
||||
channel_id: isTextRun(run) ? run.endpoint?.payload?.browseId : undefined,
|
||||
endpoint: isTextRun(run) ? run.endpoint : undefined
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#parseVideo() {
|
||||
this.id = this.#playlist_item_data.video_id;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.views = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => run.text.match(/(.*?) views/))?.text;
|
||||
this.title = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
this.views = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find((run) => run.text.match(/(.*?) views/))?.toString();
|
||||
|
||||
const authors = this.#flex_columns[1].key('title').instanceof(Text).runs?.filter((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun[];
|
||||
if (authors) {
|
||||
this.authors = authors.map((author) => ({
|
||||
name: author.text,
|
||||
channel_id: author.endpoint?.payload?.browseId,
|
||||
endpoint: author.endpoint
|
||||
}));
|
||||
const author_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
if (author_runs) {
|
||||
this.authors = author_runs.map((run) => {
|
||||
return {
|
||||
name: run.text,
|
||||
channel_id: isTextRun(run) ? run.endpoint?.payload?.browseId : undefined,
|
||||
endpoint: isTextRun(run) ? run.endpoint : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const duration_text = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text ||
|
||||
this.#fixed_columns[0]?.key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
|
||||
duration_text && (this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
});
|
||||
const duration_text = this.flex_columns[1].key('title').instanceof(Text).runs?.find(
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
|
||||
|
||||
if (duration_text) {
|
||||
this.duration = {
|
||||
text: duration_text,
|
||||
seconds: timeToSeconds(duration_text)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#parseArtist() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.name = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.#flex_columns[1].key('title').instanceof(Text);
|
||||
this.subscribers = this.subtitle.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || '';
|
||||
this.name = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text);
|
||||
this.subscribers = this.subtitle?.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || '';
|
||||
}
|
||||
|
||||
#parseLibraryArtist() {
|
||||
this.name = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.#flex_columns[1].key('title').instanceof(Text);
|
||||
this.name = this.flex_columns.first().key('title').instanceof(Text).toString();
|
||||
this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text);
|
||||
this.song_count = this.subtitle?.runs?.find((run) => (/^\d+(,\d+)? songs?$/i).test(run.text))?.text || '';
|
||||
}
|
||||
|
||||
#parseAlbum() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
|
||||
const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun;
|
||||
author && (this.author = {
|
||||
name: author.text,
|
||||
channel_id: author.endpoint?.payload?.browseId,
|
||||
endpoint: author.endpoint
|
||||
});
|
||||
const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
this.year = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^[12][0-9]{3}$/).test(run.text))?.text;
|
||||
if (author_run && isTextRun(author_run)) {
|
||||
this.author = {
|
||||
name: author_run.text,
|
||||
channel_id: author_run.endpoint?.payload?.browseId,
|
||||
endpoint: author_run.endpoint
|
||||
};
|
||||
}
|
||||
|
||||
this.year = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) => (/^[12][0-9]{3}$/).test(run.text)
|
||||
)?.text;
|
||||
}
|
||||
|
||||
#parsePlaylist() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.#flex_columns[0].key('title').instanceof(Text).toString();
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
|
||||
const item_count_run = this.#flex_columns[1].key('title')
|
||||
const item_count_run = this.flex_columns.at(1)?.key('title')
|
||||
.instanceof(Text).runs?.find((run) => run.text.match(/\d+ (song|songs)/));
|
||||
|
||||
this.item_count = item_count_run ? item_count_run.text : undefined;
|
||||
|
||||
const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun;
|
||||
const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find(
|
||||
(run) =>
|
||||
(isTextRun(run) && run.endpoint) &&
|
||||
run.endpoint.payload.browseId.startsWith('UC')
|
||||
);
|
||||
|
||||
if (author) {
|
||||
if (author_run && isTextRun(author_run)) {
|
||||
this.author = {
|
||||
name: author.text,
|
||||
channel_id: author.endpoint?.payload?.browseId,
|
||||
endpoint: author.endpoint
|
||||
name: author_run.text,
|
||||
channel_id: author_run.endpoint?.payload?.browseId,
|
||||
endpoint: author_run.endpoint
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicResponsiveListItem;
|
||||
get thumbnails() {
|
||||
return this.thumbnail?.contents || [];
|
||||
}
|
||||
}
|
||||
26
src/parser/classes/MusicTastebuilderShelf.ts
Normal file
26
src/parser/classes/MusicTastebuilderShelf.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Parser from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import Text from './misc/Text.js';
|
||||
import MusicTastebuilderShelfThumbnail from './MusicTastebuilderShelfThumbnail.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class MusicTasteBuilderShelf extends YTNode {
|
||||
static type = 'MusicTasteBuilderShelf';
|
||||
|
||||
thumbnail: MusicTastebuilderShelfThumbnail | null;
|
||||
primary_text: Text;
|
||||
secondary_text: Text;
|
||||
action_button: Button | null;
|
||||
is_visible: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Parser.parseItem(data.thumbnail, MusicTastebuilderShelfThumbnail);
|
||||
this.primary_text = new Text(data.primaryText);
|
||||
this.secondary_text = new Text(data.secondaryText);
|
||||
this.action_button = Parser.parseItem(data.actionButton, Button);
|
||||
this.is_visible = data.isVisible;
|
||||
}
|
||||
}
|
||||
14
src/parser/classes/MusicTastebuilderShelfThumbnail.ts
Normal file
14
src/parser/classes/MusicTastebuilderShelfThumbnail.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Thumbnail } from '../misc.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class MusicTastebuilderShelfThumbnail extends YTNode {
|
||||
static type = 'MusicTastebuilderShelfThumbnail';
|
||||
|
||||
thumbnail: Thumbnail[];
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class ToggleMenuServiceItem extends YTNode {
|
||||
export default class ToggleMenuServiceItem extends YTNode {
|
||||
static type = 'ToggleMenuServiceItem';
|
||||
|
||||
text: Text;
|
||||
toggled_text: Text;
|
||||
icon_type: string;
|
||||
toggled_icon_type: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
default_endpoint: NavigationEndpoint;
|
||||
toggled_endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.text = new Text(data.defaultText);
|
||||
this.toggled_text = new Text(data.toggledText);
|
||||
this.icon_type = data.defaultIcon.iconType;
|
||||
this.toggled_icon_type = data.toggledIcon.iconType;
|
||||
this.endpoint = new NavigationEndpoint(data.toggledServiceEndpoint);
|
||||
this.default_endpoint = new NavigationEndpoint(data.defaultServiceEndpoint);
|
||||
this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToggleMenuServiceItem;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button from '../Button.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
class MenuNavigationItem extends Button {
|
||||
static type = 'MenuNavigationItem';
|
||||
|
||||
|
||||
@@ -219,6 +219,8 @@ export { default as MusicResponsiveListItemFlexColumn } from './classes/MusicRes
|
||||
export { default as MusicShelf } from './classes/MusicShelf.js';
|
||||
export { default as MusicSideAlignedItem } from './classes/MusicSideAlignedItem.js';
|
||||
export { default as MusicSortFilterButton } from './classes/MusicSortFilterButton.js';
|
||||
export { default as MusicTastebuilderShelf } from './classes/MusicTastebuilderShelf.js';
|
||||
export { default as MusicTastebuilderShelfThumbnail } from './classes/MusicTastebuilderShelfThumbnail.js';
|
||||
export { default as MusicThumbnail } from './classes/MusicThumbnail.js';
|
||||
export { default as MusicTwoRowItem } from './classes/MusicTwoRowItem.js';
|
||||
export { default as MusicVisualHeader } from './classes/MusicVisualHeader.js';
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import Parser, { SectionListContinuation } from '../index.js';
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.js';
|
||||
import Parser, { SectionListContinuation } from '../index.js';
|
||||
import MusicTastebuilderShelf from '../classes/MusicTastebuilderShelf.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
class HomeFeed {
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
#continuation?: string;
|
||||
|
||||
sections?: ObservedArray<MusicCarouselShelf>;
|
||||
sections?: ObservedArray<MusicCarouselShelf | MusicTastebuilderShelf>;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#actions = actions;
|
||||
@@ -36,7 +37,7 @@ class HomeFeed {
|
||||
}
|
||||
|
||||
this.#continuation = tab.content?.as(SectionList).continuation;
|
||||
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf);
|
||||
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf, MusicTastebuilderShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Memo } from '../parser/helpers.js';
|
||||
import { EmojiRun, TextRun } from '../parser/misc.js';
|
||||
import PlatformShim, { FetchFunction } from '../types/PlatformShim.js';
|
||||
import userAgents from './user-agents.js';
|
||||
|
||||
@@ -221,4 +222,8 @@ export function u8ToBase64(u8: Uint8Array): string {
|
||||
|
||||
export function base64ToU8(base64: string): Uint8Array {
|
||||
return new Uint8Array(atob(base64).split('').map((char) => char.charCodeAt(0)));
|
||||
}
|
||||
|
||||
export function isTextRun(run: TextRun | EmojiRun): run is TextRun {
|
||||
return !('emoji' in run);
|
||||
}
|
||||
Reference in New Issue
Block a user