mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-15 02:22:11 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
401b4c3858 | ||
|
|
5aecd0ace9 | ||
|
|
432571769e | ||
|
|
2b4219959c | ||
|
|
b731db86c5 | ||
|
|
e2ee822b9d | ||
|
|
3243339b37 | ||
|
|
c9328c59f9 | ||
|
|
a3fafe2f79 | ||
|
|
ca7c3164e1 | ||
|
|
02dfcae612 | ||
|
|
0cb92d9620 | ||
|
|
5394edc9bd | ||
|
|
7d5c972c98 | ||
|
|
b5c9581bec | ||
|
|
774b3a7524 | ||
|
|
b3a4862151 | ||
|
|
75d39e7afb | ||
|
|
c776b9f349 | ||
|
|
0e869020db | ||
|
|
d0d48bf525 | ||
|
|
3f960effa2 | ||
|
|
1c1577e85f | ||
|
|
424c65356c | ||
|
|
3ffdee9554 | ||
|
|
083aec1c80 | ||
|
|
6d57353a80 | ||
|
|
32125c7045 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -74,6 +74,9 @@ deno/
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
# Webstorm files
|
||||
.idea/
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [13.1.0](https://github.com/LuanRT/YouTube.js/compare/v13.0.0...v13.1.0) (2025-02-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Channel:** Add `getCourses` method ([#883](https://github.com/LuanRT/YouTube.js/issues/883)) ([b3a4862](https://github.com/LuanRT/YouTube.js/commit/b3a48621518f09d1ce309071499d9626cc1a8488))
|
||||
* **CommentView:** Parse `prepareAccountCommand` ([d0d48bf](https://github.com/LuanRT/YouTube.js/commit/d0d48bf525cc2a95dd3397a3142bea113ea9782e))
|
||||
* **CommentView:** Parse some extra tooltips ([32125c7](https://github.com/LuanRT/YouTube.js/commit/32125c704565f425806a0721edd96e01028e3fdd))
|
||||
* **CompactLink:** Parse `subtitle`, `iconType`, and `iconType` ([6d57353](https://github.com/LuanRT/YouTube.js/commit/6d57353a8021430a5253e2fb2c974ca98d731791))
|
||||
* **FormatUtils:** choose more specific format by itag or codec ([#884](https://github.com/LuanRT/YouTube.js/issues/884)) ([1c1577e](https://github.com/LuanRT/YouTube.js/commit/1c1577e85fd46cbfa15bcee6531d9aafdda787e5))
|
||||
* **parser:** `Add AnimatedThumbnailOverlayView` ([#903](https://github.com/LuanRT/YouTube.js/issues/903)) ([0cb92d9](https://github.com/LuanRT/YouTube.js/commit/0cb92d9620c13bf6b719b384f917ad2a658e15b1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dependencies:** Update `jintr` to version 3.2.1 ([02dfcae](https://github.com/LuanRT/YouTube.js/commit/02dfcae612dd528ce4f1f3f6c62ceefd02a5c790))
|
||||
* **DialogView:** Type mismatch ([#897](https://github.com/LuanRT/YouTube.js/issues/897)) ([b731db8](https://github.com/LuanRT/YouTube.js/commit/b731db86c51ba292c848272f28b5a9aa2e2a6956))
|
||||
* **FormatUtils:** itag matching ([#886](https://github.com/LuanRT/YouTube.js/issues/886)) ([774b3a7](https://github.com/LuanRT/YouTube.js/commit/774b3a75244db85ceb7b00f658dc2dfeb2eb4e7e))
|
||||
* **innertube:** Allowing `getStreamingData` to use client ([#895](https://github.com/LuanRT/YouTube.js/issues/895)) ([5aecd0a](https://github.com/LuanRT/YouTube.js/commit/5aecd0ace96c371f0b15cdc6e45ef09beb5696af))
|
||||
* **Innertube:** Properly encoded params in getPost() ([#882](https://github.com/LuanRT/YouTube.js/issues/882)) ([7d5c972](https://github.com/LuanRT/YouTube.js/commit/7d5c972c98d7c69b0b687b241c652f3098907a9f))
|
||||
* **LockupMetadataView:** Parse `menuButton` ([3ffdee9](https://github.com/LuanRT/YouTube.js/commit/3ffdee9554b06db137d93e43b33fac124becf31f))
|
||||
* **LockupView:** Add overlay nodes used by `VIDEO` views ([424c653](https://github.com/LuanRT/YouTube.js/commit/424c65356c24d19a921e24aadcbbb3cd03ab103a))
|
||||
* **LockupView:** Fix `content_image` parsing ([083aec1](https://github.com/LuanRT/YouTube.js/commit/083aec1c805cce6b04a75e4f017b5cdf0bb6108e))
|
||||
* **music#getPlaylist:** Handle `ContinuationItem` nodes ([a3fafe2](https://github.com/LuanRT/YouTube.js/commit/a3fafe2f7979313906dbaf1a7f9779f411266d6b)), closes [#904](https://github.com/LuanRT/YouTube.js/issues/904)
|
||||
* **Parser:** Add `UpdateEngagementPanelContentCommand` ([3f960ef](https://github.com/LuanRT/YouTube.js/commit/3f960effa24c3b14fa3c6aadf4c7badf0ac965c9))
|
||||
* **Playlist:** is_editable ([#894](https://github.com/LuanRT/YouTube.js/issues/894)) ([2b42199](https://github.com/LuanRT/YouTube.js/commit/2b4219959cbbb27cd80788e66b608fdeed3a1f1e))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* "fix(toDash): Fix default audio stream for dubbed movie trailers ([#858](https://github.com/LuanRT/YouTube.js/issues/858))" ([#896](https://github.com/LuanRT/YouTube.js/issues/896)) ([4325717](https://github.com/LuanRT/YouTube.js/commit/432571769ebc6634c2c9a4c1b5e53cfbbd2a5f0a))
|
||||
|
||||
## [13.0.0](https://github.com/LuanRT/YouTube.js/compare/v12.2.0...v13.0.0) (2025-01-20)
|
||||
|
||||
|
||||
|
||||
@@ -37,9 +37,12 @@ yarn add youtubei.js@latest
|
||||
|
||||
# Git (edge version)
|
||||
npm install github:LuanRT/YouTube.js
|
||||
|
||||
# Deno
|
||||
deno add npm:youtubei.js@latest
|
||||
```
|
||||
|
||||
Deno:
|
||||
Deno (deprecated):
|
||||
```ts
|
||||
import { Innertube } from 'https://deno.land/x/youtubei/deno.ts';
|
||||
```
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.0.0",
|
||||
"version": "13.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "13.0.0",
|
||||
"version": "13.1.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"jintr": "^3.2.0",
|
||||
"jintr": "^3.2.1",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
@@ -6155,12 +6155,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.0.tgz",
|
||||
"integrity": "sha512-psD1yf05kMKDNsUdW1l5YhO59pHScQ6OIHHb8W5SKSM2dCOFPsqolmIuSHgVA8+3Dc47NJR181CXZ4alCAPTkA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.1.tgz",
|
||||
"integrity": "sha512-yjKUBuwTTg4nc4izMysxuIk0BKh45hnbc1KnXE6LxagIGZn5od+I2elpuRY9IIm3EiKiUZxhxV89a0iX+xoEZg==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "13.0.0",
|
||||
"version": "13.1.0",
|
||||
"description": "A JavaScript client for YouTube's private API, known as InnerTube.",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
@@ -103,7 +103,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.0.0",
|
||||
"jintr": "^3.2.0",
|
||||
"jintr": "^3.2.1",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
|
||||
@@ -2230,10 +2230,10 @@ function createBaseCommunityPostParams_Field2(): CommunityPostParams_Field2 {
|
||||
export const CommunityPostParams_Field2: MessageFns<CommunityPostParams_Field2> = {
|
||||
encode(message: CommunityPostParams_Field2, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.p1 !== 0) {
|
||||
writer.uint32(16).int64(message.p1);
|
||||
writer.uint32(16).uint32(message.p1);
|
||||
}
|
||||
if (message.p2 !== 0) {
|
||||
writer.uint32(24).int64(message.p2);
|
||||
writer.uint32(24).uint32(message.p2);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
@@ -2250,14 +2250,14 @@ export const CommunityPostParams_Field2: MessageFns<CommunityPostParams_Field2>
|
||||
break;
|
||||
}
|
||||
|
||||
message.p1 = longToNumber(reader.int64());
|
||||
message.p1 = reader.uint32();
|
||||
continue;
|
||||
case 3:
|
||||
if (tag !== 24) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.p2 = longToNumber(reader.int64());
|
||||
message.p2 = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
@@ -2636,17 +2636,6 @@ export const CommunityPostCommentsParam_CommentDataContainer_CommentData: Messag
|
||||
},
|
||||
};
|
||||
|
||||
function longToNumber(int64: { toString(): string }): number {
|
||||
const num = globalThis.Number(int64.toString());
|
||||
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||
}
|
||||
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
|
||||
@@ -238,8 +238,8 @@ message CommunityPostParams {
|
||||
}
|
||||
|
||||
message Field2 {
|
||||
int64 p1 = 2;
|
||||
int64 p2 = 3;
|
||||
uint32 p1 = 2;
|
||||
uint32 p2 = 3;
|
||||
}
|
||||
|
||||
Field1 f1 = 25;
|
||||
|
||||
@@ -431,7 +431,7 @@ export default class Innertube {
|
||||
* @param options - Format options.
|
||||
*/
|
||||
async getStreamingData(video_id: string, options: FormatOptions = {}): Promise<Format> {
|
||||
const info = await this.getBasicInfo(video_id);
|
||||
const info = await this.getBasicInfo(video_id, options?.client);
|
||||
|
||||
const format = info.chooseFormat(options);
|
||||
format.url = format.decipher(this.#session.player);
|
||||
@@ -478,7 +478,7 @@ export default class Innertube {
|
||||
}
|
||||
});
|
||||
|
||||
const params = encodeURIComponent(u8ToBase64(writer.finish()));
|
||||
const params = encodeURIComponent(u8ToBase64(writer.finish()).replace(/\+/g, '-').replace(/\//g, '_'));
|
||||
|
||||
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: channel_id, params: params } });
|
||||
|
||||
@@ -616,4 +616,4 @@ export default class Innertube {
|
||||
get session() {
|
||||
return this.#session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ export default class Actions {
|
||||
let parsed_response = Parser.parseResponse<ParsedResponse<T>>(await response.json());
|
||||
|
||||
// Handle redirects
|
||||
if (this.#isBrowse(parsed_response) && parsed_response.on_response_received_actions?.first()?.type === 'navigateAction') {
|
||||
if (this.#isBrowse(parsed_response) && parsed_response.on_response_received_actions?.[0]?.type === 'navigateAction') {
|
||||
const navigate_action = parsed_response.on_response_received_actions.firstOfType(NavigateAction);
|
||||
if (navigate_action) {
|
||||
parsed_response = await navigate_action.endpoint.call(this, { parse: true });
|
||||
|
||||
@@ -201,7 +201,7 @@ export default class Music {
|
||||
const response = await watch_next_endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
const tabs = response.contents_memo?.getType(Tab);
|
||||
const tab = tabs?.first();
|
||||
const tab = tabs?.[0];
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -228,7 +228,7 @@ export default class Music {
|
||||
if (!page || !page.contents_memo)
|
||||
throw new InnertubeError('Could not fetch automix');
|
||||
|
||||
return page.contents_memo.getType(PlaylistPanel).first();
|
||||
return page.contents_memo.getType(PlaylistPanel)[0];
|
||||
}
|
||||
|
||||
return playlist_panel;
|
||||
@@ -242,7 +242,7 @@ export default class Music {
|
||||
|
||||
const tabs = response.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
const tab = tabs?.find((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -263,7 +263,7 @@ export default class Music {
|
||||
|
||||
const tabs = response.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
|
||||
const tab = tabs?.find((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
@@ -237,12 +237,11 @@ export default class PlaylistManager {
|
||||
}
|
||||
|
||||
async #getPlaylist(playlist_id: string): Promise<Playlist> {
|
||||
let id = playlist_id;
|
||||
if (!playlist_id.startsWith('VL')) {
|
||||
playlist_id = `VL${playlist_id}`;
|
||||
}
|
||||
|
||||
if (!id.startsWith('VL'))
|
||||
id = `VL${id}`;
|
||||
|
||||
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: `VL${id}` } });
|
||||
const browse_endpoint = new NavigationEndpoint({ browseEndpoint: { browseId: playlist_id } });
|
||||
const browse_response = await browse_endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
return new Playlist(this.#actions, browse_response, true);
|
||||
|
||||
@@ -139,9 +139,9 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Returns contents from the page.
|
||||
*/
|
||||
get page_contents(): SectionList | MusicQueue | RichGrid | ReloadContinuationItemsCommand {
|
||||
const tab_content = this.#memo.getType(Tab)?.first().content;
|
||||
const reload_continuation_items = this.#memo.getType(ReloadContinuationItemsCommand).first();
|
||||
const append_continuation_items = this.#memo.getType(AppendContinuationItemsAction).first();
|
||||
const tab_content = this.#memo.getType(Tab)?.[0].content;
|
||||
const reload_continuation_items = this.#memo.getType(ReloadContinuationItemsCommand)[0];
|
||||
const append_continuation_items = this.#memo.getType(AppendContinuationItemsAction)[0];
|
||||
|
||||
return tab_content || reload_continuation_items || append_continuation_items;
|
||||
}
|
||||
|
||||
14
src/parser/classes/AnimatedThumbnailOverlayView.ts
Normal file
14
src/parser/classes/AnimatedThumbnailOverlayView.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../types/index.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
export default class AnimatedThumbnailOverlayView extends YTNode {
|
||||
static type = 'AnimatedThumbnailOverlayView';
|
||||
|
||||
public thumbnail: Thumbnail[];
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,27 @@ import type { RawNode } from '../index.js';
|
||||
export default class CompactLink extends YTNode {
|
||||
static type = 'CompactLink';
|
||||
|
||||
title: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
style: string;
|
||||
public title: string;
|
||||
public subtitle?: Text;
|
||||
public endpoint: NavigationEndpoint;
|
||||
public style: string;
|
||||
public icon_type?: string;
|
||||
public secondary_icon_type?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title).toString();
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
|
||||
if ('subtitle' in data)
|
||||
this.subtitle = new Text(data.subtitle);
|
||||
|
||||
if ('icon' in data && 'iconType' in data.icon)
|
||||
this.icon_type = data.icon.iconType;
|
||||
|
||||
if ('secondaryIcon' in data && 'iconType' in data.secondaryIcon)
|
||||
this.secondary_icon_type = data.secondaryIcon.iconType;
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint);
|
||||
this.style = data.style;
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,19 @@ import { Parser, type RawNode } from '../index.js';
|
||||
import DialogHeaderView from './DialogHeaderView.js';
|
||||
import FormFooterView from './FormFooterView.js';
|
||||
import CreatePlaylistDialogFormView from './CreatePlaylistDialogFormView.js';
|
||||
import PanelFooterView from './PanelFooterView.js';
|
||||
|
||||
export default class DialogView extends YTNode {
|
||||
static type = 'DialogView';
|
||||
|
||||
public header: DialogHeaderView | null;
|
||||
public footer: FormFooterView | null;
|
||||
public footer: FormFooterView | PanelFooterView | null;
|
||||
public custom_content: CreatePlaylistDialogFormView | null;
|
||||
|
||||
constructor (data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parseItem(data.header, DialogHeaderView);
|
||||
this.footer = Parser.parseItem(data.footer, FormFooterView);
|
||||
this.footer = Parser.parseItem(data.footer, [ FormFooterView, PanelFooterView ]);
|
||||
this.custom_content = Parser.parseItem(data.customContent, CreatePlaylistDialogFormView);
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,21 @@ import { Parser, type RawNode } from '../index.js';
|
||||
import ContentMetadataView from './ContentMetadataView.js';
|
||||
import DecoratedAvatarView from './DecoratedAvatarView.js';
|
||||
import Text from './misc/Text.js';
|
||||
import ButtonView from './ButtonView.js';
|
||||
|
||||
export default class LockupMetadataView extends YTNode {
|
||||
static type = 'LockupMetadataView';
|
||||
|
||||
title: Text;
|
||||
metadata: ContentMetadataView | null;
|
||||
image: DecoratedAvatarView | null;
|
||||
public title: Text;
|
||||
public metadata: ContentMetadataView | null;
|
||||
public image: DecoratedAvatarView | null;
|
||||
public menu_button: ButtonView | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.title = Text.fromAttributed(data.title);
|
||||
this.metadata = Parser.parseItem(data.metadata, ContentMetadataView);
|
||||
this.image = Parser.parseItem(data.image, DecoratedAvatarView);
|
||||
this.menu_button = Parser.parseItem(data.menuButton, ButtonView);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ThumbnailView from './ThumbnailView.js';
|
||||
import CollectionThumbnailView from './CollectionThumbnailView.js';
|
||||
import LockupMetadataView from './LockupMetadataView.js';
|
||||
import RendererContext from './misc/RendererContext.js';
|
||||
@@ -7,7 +8,7 @@ import RendererContext from './misc/RendererContext.js';
|
||||
export default class LockupView extends YTNode {
|
||||
static type = 'LockupView';
|
||||
|
||||
public content_image: CollectionThumbnailView | null;
|
||||
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';
|
||||
@@ -15,7 +16,7 @@ export default class LockupView extends YTNode {
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.content_image = Parser.parseItem(data.contentImage, CollectionThumbnailView);
|
||||
this.content_image = Parser.parseItem(data.contentImage, [ CollectionThumbnailView, ThumbnailView ]);
|
||||
this.metadata = Parser.parseItem(data.metadata, LockupMetadataView);
|
||||
this.content_id = data.contentId;
|
||||
this.content_type = data.contentType.replace('LOCKUP_CONTENT_TYPE_', '');
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import MusicResponsiveListItem from './MusicResponsiveListItem.js';
|
||||
import ContinuationItem from './ContinuationItem.js';
|
||||
|
||||
export default class MusicPlaylistShelf extends YTNode {
|
||||
static type = 'MusicPlaylistShelf';
|
||||
|
||||
playlist_id: string;
|
||||
contents: ObservedArray<MusicResponsiveListItem>;
|
||||
collapsed_item_count: number;
|
||||
continuation: string | null;
|
||||
public playlist_id: string;
|
||||
public contents: ObservedArray<MusicResponsiveListItem | ContinuationItem>;
|
||||
public collapsed_item_count: number;
|
||||
public continuation: string | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.playlist_id = data.playlistId;
|
||||
this.contents = Parser.parseArray(data.contents, MusicResponsiveListItem);
|
||||
this.contents = Parser.parseArray(data.contents, [ MusicResponsiveListItem, ContinuationItem ]);
|
||||
this.collapsed_item_count = data.collapsedItemCount;
|
||||
this.continuation = data.continuations?.[0]?.nextContinuationData?.continuation || null;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
|
||||
#parseOther() {
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
|
||||
if (this.endpoint) {
|
||||
this.item_type = 'endpoint';
|
||||
@@ -183,10 +183,10 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
#parseSong() {
|
||||
this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
|
||||
const duration_text = this.flex_columns.at(1)?.title.runs?.find(
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.title?.toString();
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns[0]?.title?.toString();
|
||||
|
||||
if (duration_text) {
|
||||
this.duration = {
|
||||
@@ -230,7 +230,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
#parseVideo() {
|
||||
this.id = this.#playlist_item_data.video_id;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
this.views = this.flex_columns.at(1)?.title.runs?.find((run) => run.text.match(/(.*?) views/))?.toString();
|
||||
|
||||
const author_runs = this.flex_columns.at(1)?.title.runs?.filter(
|
||||
@@ -250,7 +250,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
}
|
||||
|
||||
const duration_text = this.flex_columns[1].title.runs?.find(
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.title.runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
|
||||
(run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns[0]?.title.runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text;
|
||||
|
||||
if (duration_text) {
|
||||
this.duration = {
|
||||
@@ -262,30 +262,30 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
#parseArtist() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.name = this.flex_columns.first().title.toString();
|
||||
this.name = this.flex_columns[0].title.toString();
|
||||
this.subtitle = this.flex_columns.at(1)?.title;
|
||||
this.subscribers = this.subtitle?.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || '';
|
||||
}
|
||||
|
||||
#parseLibraryArtist() {
|
||||
this.name = this.flex_columns.first().title.toString();
|
||||
this.name = this.flex_columns[0].title.toString();
|
||||
this.subtitle = this.flex_columns.at(1)?.title;
|
||||
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;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
}
|
||||
|
||||
#parsePodcastShow() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
}
|
||||
|
||||
#parseAlbum() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
|
||||
const author_run = this.flex_columns.at(1)?.title.runs?.find(
|
||||
(run) =>
|
||||
@@ -308,7 +308,7 @@ export default class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
#parsePlaylist() {
|
||||
this.id = this.endpoint?.payload?.browseId;
|
||||
this.title = this.flex_columns.first().title.toString();
|
||||
this.title = this.flex_columns[0].title.toString();
|
||||
|
||||
const item_count_run = this.flex_columns.at(1)?.title
|
||||
.runs?.find((run) => run.text.match(/\d+ (song|songs)/));
|
||||
|
||||
18
src/parser/classes/ThumbnailBottomOverlayView.ts
Normal file
18
src/parser/classes/ThumbnailBottomOverlayView.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ThumbnailBadgeView from './ThumbnailBadgeView.js';
|
||||
import ThumbnailOverlayProgressBarView from './ThumbnailOverlayProgressBarView.js';
|
||||
|
||||
export default class ThumbnailBottomOverlayView extends YTNode {
|
||||
static type = 'ThumbnailBottomOverlayView';
|
||||
|
||||
public progress_bar: ThumbnailOverlayProgressBarView | null;
|
||||
public badges: ObservedArray<ThumbnailBadgeView>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.progress_bar = Parser.parseItem(data.progressBar, ThumbnailOverlayProgressBarView);
|
||||
this.badges = Parser.parseArray(data.badges, ThumbnailBadgeView);
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/ThumbnailHoverOverlayToggleActionsView.ts
Normal file
15
src/parser/classes/ThumbnailHoverOverlayToggleActionsView.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import ToggleButtonView from './ToggleButtonView.js';
|
||||
|
||||
export default class ThumbnailHoverOverlayToggleActionsView extends YTNode {
|
||||
static type = 'ThumbnailHoverOverlayToggleActionsView';
|
||||
|
||||
public buttons: ObservedArray<ToggleButtonView>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.buttons = Parser.parseArray(data.buttons, ToggleButtonView);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import ThumbnailBadgeView from './ThumbnailBadgeView.js';
|
||||
export default class ThumbnailOverlayBadgeView extends YTNode {
|
||||
static type = 'ThumbnailOverlayBadgeView';
|
||||
|
||||
badges: ThumbnailBadgeView[];
|
||||
position: string;
|
||||
public badges: ThumbnailBadgeView[];
|
||||
public position: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
13
src/parser/classes/ThumbnailOverlayProgressBarView.ts
Normal file
13
src/parser/classes/ThumbnailOverlayProgressBarView.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { type RawNode } from '../index.js';
|
||||
|
||||
export default class ThumbnailOverlayProgressBarView extends YTNode {
|
||||
static type = 'ThumbnailOverlayProgressBarView';
|
||||
|
||||
public start_percent: number;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.start_percent = data.startPercent;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,40 @@
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import AnimatedThumbnailOverlayView from './AnimatedThumbnailOverlayView.js';
|
||||
import ThumbnailHoverOverlayView from './ThumbnailHoverOverlayView.js';
|
||||
import ThumbnailOverlayBadgeView from './ThumbnailOverlayBadgeView.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import ThumbnailHoverOverlayToggleActionsView from './ThumbnailHoverOverlayToggleActionsView.js';
|
||||
import ThumbnailBottomOverlayView from './ThumbnailBottomOverlayView.js';
|
||||
|
||||
export type ThumbnailBackgroundColor = {
|
||||
light_theme: number;
|
||||
dark_theme: number;
|
||||
};
|
||||
|
||||
export default class ThumbnailView extends YTNode {
|
||||
static type = 'ThumbnailView';
|
||||
|
||||
image: Thumbnail[];
|
||||
overlays: (ThumbnailOverlayBadgeView | ThumbnailHoverOverlayView)[];
|
||||
background_color?: {
|
||||
light_theme: number;
|
||||
dark_theme: number;
|
||||
};
|
||||
public image: Thumbnail[];
|
||||
public overlays: ObservedArray<
|
||||
ThumbnailHoverOverlayToggleActionsView | ThumbnailBottomOverlayView |
|
||||
ThumbnailOverlayBadgeView | ThumbnailHoverOverlayView
|
||||
| AnimatedThumbnailOverlayView
|
||||
>;
|
||||
public background_color?: ThumbnailBackgroundColor;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
this.overlays = Parser.parseArray(data.overlays, [ ThumbnailOverlayBadgeView, ThumbnailHoverOverlayView ]);
|
||||
if (data.backgroundColor) {
|
||||
this.overlays = Parser.parseArray(data.overlays, [
|
||||
ThumbnailHoverOverlayToggleActionsView, ThumbnailBottomOverlayView,
|
||||
ThumbnailOverlayBadgeView, ThumbnailHoverOverlayView,
|
||||
AnimatedThumbnailOverlayView
|
||||
]);
|
||||
|
||||
if ('backgroundColor' in data) {
|
||||
this.background_color = {
|
||||
light_theme: data.backgroundColor.lightTheme,
|
||||
dark_theme: data.backgroundColor.darkTheme
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import { type RawNode } from '../../index.js';
|
||||
|
||||
type Identifier = {
|
||||
surface: string,
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export default class UpdateEngagementPanelContentCommand extends YTNode {
|
||||
static type = 'UpdateEngagementPanelContentCommand';
|
||||
|
||||
public content_source_panel_identifier?: Identifier;
|
||||
public target_panel_identifier?: Identifier;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.content_source_panel_identifier = data.contentSourcePanelIdentifier;
|
||||
this.target_panel_identifier = data.targetPanelIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export default class CommentThread extends YTNode {
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
|
||||
this.replies = this.#getPatchedReplies(response.on_response_received_endpoints_memo);
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem).first();
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem)[0];
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export default class CommentThread extends YTNode {
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
|
||||
this.replies = this.#getPatchedReplies(response.on_response_received_endpoints_memo);
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem).first();
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem)[0];
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export default class CommentView extends YTNode {
|
||||
public unlike_command?: NavigationEndpoint;
|
||||
public undislike_command?: NavigationEndpoint;
|
||||
public reply_command?: NavigationEndpoint;
|
||||
public prepare_account_command?: NavigationEndpoint;
|
||||
|
||||
public comment_id: string;
|
||||
public is_pinned: boolean;
|
||||
@@ -42,8 +43,18 @@ export default class CommentView extends YTNode {
|
||||
public content?: Text;
|
||||
public published_time?: string;
|
||||
public author_is_channel_owner?: boolean;
|
||||
public creator_thumbnail_url?: string;
|
||||
public like_button_a11y?: string;
|
||||
public like_count?: string;
|
||||
public like_count_liked?: string;
|
||||
public like_count_a11y?: string;
|
||||
public like_active_tooltip?: string;
|
||||
public like_inactive_tooltip?: string;
|
||||
public dislike_active_tooltip?: string;
|
||||
public dislike_inactive_tooltip?: string;
|
||||
public heart_active_tooltip?: string;
|
||||
public reply_count?: string;
|
||||
public reply_count_a11y?: string;
|
||||
public is_member?: boolean;
|
||||
public member_badge?: MemberBadge;
|
||||
public author?: Author;
|
||||
@@ -72,8 +83,18 @@ export default class CommentView extends YTNode {
|
||||
this.content = Text.fromAttributed(comment.properties.content);
|
||||
this.published_time = comment.properties.publishedTime;
|
||||
this.author_is_channel_owner = !!comment.author.isCreator;
|
||||
|
||||
this.creator_thumbnail_url = comment.toolbar.creatorThumbnailUrl;
|
||||
|
||||
this.like_count = comment.toolbar.likeCountNotliked ? comment.toolbar.likeCountNotliked : '0';
|
||||
this.like_count_liked = comment.toolbar.likeCountLiked ? comment.toolbar.likeCountLiked : '0';
|
||||
this.like_count_a11y = comment.toolbar.likeCountA11y;
|
||||
this.like_active_tooltip = comment.toolbar.likeActiveTooltip;
|
||||
this.like_inactive_tooltip = comment.toolbar.likeInactiveTooltip;
|
||||
this.dislike_active_tooltip = comment.toolbar.dislikeActiveTooltip;
|
||||
this.dislike_inactive_tooltip = comment.toolbar.dislikeInactiveTooltip;
|
||||
this.like_button_a11y = comment.toolbar.likeButtonA11y;
|
||||
this.heart_active_tooltip = comment.toolbar.heartActiveTooltip;
|
||||
this.reply_count_a11y = comment.toolbar.replyCountA11y;
|
||||
this.reply_count = comment.toolbar.replyCount ? comment.toolbar.replyCount : '0';
|
||||
|
||||
this.is_member = !!comment.author.sponsorBadgeUrl;
|
||||
@@ -97,12 +118,16 @@ export default class CommentView extends YTNode {
|
||||
this.is_disliked = toolbar_state.likeState === 'TOOLBAR_LIKE_STATE_DISLIKED';
|
||||
}
|
||||
|
||||
if (toolbar_surface && !Reflect.has(toolbar_surface, 'prepareAccountCommand')) {
|
||||
this.like_command = new NavigationEndpoint(toolbar_surface.likeCommand);
|
||||
this.dislike_command = new NavigationEndpoint(toolbar_surface.dislikeCommand);
|
||||
this.unlike_command = new NavigationEndpoint(toolbar_surface.unlikeCommand);
|
||||
this.undislike_command = new NavigationEndpoint(toolbar_surface.undislikeCommand);
|
||||
this.reply_command = new NavigationEndpoint(toolbar_surface.replyCommand);
|
||||
if (toolbar_surface) {
|
||||
if ('prepareAccountCommand' in toolbar_surface) {
|
||||
this.prepare_account_command = new NavigationEndpoint(toolbar_surface.prepareAccountCommand);
|
||||
} else {
|
||||
this.like_command = new NavigationEndpoint(toolbar_surface.likeCommand);
|
||||
this.dislike_command = new NavigationEndpoint(toolbar_surface.dislikeCommand);
|
||||
this.unlike_command = new NavigationEndpoint(toolbar_surface.unlikeCommand);
|
||||
this.undislike_command = new NavigationEndpoint(toolbar_surface.undislikeCommand);
|
||||
this.reply_command = new NavigationEndpoint(toolbar_surface.replyCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export { default as ActiveAccountHeader } from './classes/ActiveAccountHeader.js
|
||||
export { default as AddToPlaylist } from './classes/AddToPlaylist.js';
|
||||
export { default as Alert } from './classes/Alert.js';
|
||||
export { default as AlertWithButton } from './classes/AlertWithButton.js';
|
||||
export { default as AnimatedThumbnailOverlayView } from './classes/AnimatedThumbnailOverlayView.js';
|
||||
export { default as AttributionView } from './classes/AttributionView.js';
|
||||
export { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability.js';
|
||||
export { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo.js';
|
||||
@@ -83,6 +84,7 @@ export { default as ContinuationCommand } from './classes/commands/ContinuationC
|
||||
export { default as GetKidsBlocklistPickerCommand } from './classes/commands/GetKidsBlocklistPickerCommand.js';
|
||||
export { default as RunAttestationCommand } from './classes/commands/RunAttestationCommand.js';
|
||||
export { default as ShowDialogCommand } from './classes/commands/ShowDialogCommand.js';
|
||||
export { default as UpdateEngagementPanelContentCommand } from './classes/commands/UpdateEngagementPanelContentCommand.js';
|
||||
export { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge.js';
|
||||
export { default as CommentActionButtons } from './classes/comments/CommentActionButtons.js';
|
||||
export { default as CommentDialog } from './classes/comments/CommentDialog.js';
|
||||
@@ -455,6 +457,8 @@ export { default as TextFieldView } from './classes/TextFieldView.js';
|
||||
export { default as TextHeader } from './classes/TextHeader.js';
|
||||
export { default as ThirdPartyShareTargetSection } from './classes/ThirdPartyShareTargetSection.js';
|
||||
export { default as ThumbnailBadgeView } from './classes/ThumbnailBadgeView.js';
|
||||
export { default as ThumbnailBottomOverlayView } from './classes/ThumbnailBottomOverlayView.js';
|
||||
export { default as ThumbnailHoverOverlayToggleActionsView } from './classes/ThumbnailHoverOverlayToggleActionsView.js';
|
||||
export { default as ThumbnailHoverOverlayView } from './classes/ThumbnailHoverOverlayView.js';
|
||||
export { default as ThumbnailLandscapePortrait } from './classes/ThumbnailLandscapePortrait.js';
|
||||
export { default as ThumbnailOverlayBadgeView } from './classes/ThumbnailOverlayBadgeView.js';
|
||||
@@ -466,6 +470,7 @@ export { default as ThumbnailOverlayLoadingPreview } from './classes/ThumbnailOv
|
||||
export { default as ThumbnailOverlayNowPlaying } from './classes/ThumbnailOverlayNowPlaying.js';
|
||||
export { default as ThumbnailOverlayPinking } from './classes/ThumbnailOverlayPinking.js';
|
||||
export { default as ThumbnailOverlayPlaybackStatus } from './classes/ThumbnailOverlayPlaybackStatus.js';
|
||||
export { default as ThumbnailOverlayProgressBarView } from './classes/ThumbnailOverlayProgressBarView.js';
|
||||
export { default as ThumbnailOverlayResumePlayback } from './classes/ThumbnailOverlayResumePlayback.js';
|
||||
export { default as ThumbnailOverlaySidePanel } from './classes/ThumbnailOverlaySidePanel.js';
|
||||
export { default as ThumbnailOverlayTimeStatus } from './classes/ThumbnailOverlayTimeStatus.js';
|
||||
|
||||
@@ -17,12 +17,12 @@ export default class AccountInfo {
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const account_section_list = this.#page.contents.array().as(AccountSectionList).first();
|
||||
const account_section_list = this.#page.contents.array().as(AccountSectionList)[0];
|
||||
|
||||
if (!account_section_list)
|
||||
throw new InnertubeError('Account section list not found');
|
||||
|
||||
this.contents = account_section_list.contents.first();
|
||||
this.contents = account_section_list.contents[0];
|
||||
}
|
||||
|
||||
get page(): IParsedResponse {
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
const microformat = this.page.microformat?.as(MicroformatData);
|
||||
|
||||
if (this.page.alerts) {
|
||||
const alert = this.page.alerts.first();
|
||||
const alert = this.page.alerts[0];
|
||||
if (alert?.alert_type === 'ERROR') {
|
||||
throw new ChannelError(alert.text.toString());
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
|
||||
this.metadata = { ...metadata, ...(microformat || {}) };
|
||||
|
||||
this.subscribe_button = this.page.header_memo?.getType(SubscribeButton).first();
|
||||
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 });
|
||||
@@ -72,7 +72,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
async applyFilter(filter: string | ChipCloudChip): Promise<FilteredChannelList> {
|
||||
let target_filter: ChipCloudChip | undefined;
|
||||
|
||||
const filter_chipbar = this.memo.getType(FeedFilterChipBar).first();
|
||||
const filter_chipbar = this.memo.getType(FeedFilterChipBar)[0];
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
target_filter = filter_chipbar?.contents.get({ text: filter });
|
||||
@@ -98,7 +98,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
* @param sort - The sort filter to apply
|
||||
*/
|
||||
async applySort(sort: string): Promise<Channel> {
|
||||
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu).first();
|
||||
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu)[0];
|
||||
|
||||
if (!sort_filter_sub_menu || !sort_filter_sub_menu.sub_menu_items)
|
||||
throw new InnertubeError('No sort filter sub menu found');
|
||||
@@ -144,7 +144,7 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
}
|
||||
|
||||
get sort_filters(): string[] {
|
||||
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu).first();
|
||||
const sort_filter_sub_menu = this.memo.getType(SortFilterSubMenu)[0];
|
||||
return sort_filter_sub_menu?.sub_menu_items?.map((item) => item.title) || [];
|
||||
}
|
||||
|
||||
@@ -183,6 +183,11 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getCourses(): Promise<Channel> {
|
||||
const tab = await this.getTabByURL('courses');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getPlaylists(): Promise<Channel> {
|
||||
const tab = await this.getTabByURL('playlists');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
@@ -269,6 +274,10 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
return this.hasTabWithURL('podcasts');
|
||||
}
|
||||
|
||||
get has_courses(): boolean {
|
||||
return this.hasTabWithURL('courses');
|
||||
}
|
||||
|
||||
get has_playlists(): boolean {
|
||||
return this.hasTabWithURL('playlists');
|
||||
}
|
||||
@@ -302,8 +311,8 @@ export class ChannelListContinuation extends Feed<IBrowseResponse> {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.contents =
|
||||
this.page.on_response_received_actions?.first() ||
|
||||
this.page.on_response_received_endpoints?.first();
|
||||
this.page.on_response_received_actions?.[0] ||
|
||||
this.page.on_response_received_endpoints?.[0];
|
||||
}
|
||||
|
||||
async getContinuation(): Promise<ChannelListContinuation> {
|
||||
@@ -331,7 +340,7 @@ export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
|
||||
this.page.on_response_received_actions.shift();
|
||||
}
|
||||
|
||||
this.contents = this.page.on_response_received_actions?.first();
|
||||
this.contents = this.page.on_response_received_actions?.[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class HashtagFeed extends FilterableFeed<IBrowseResponse> {
|
||||
if (!this.page.contents_memo)
|
||||
throw new InnertubeError('Unexpected response', this.page);
|
||||
|
||||
const tab = this.page.contents_memo.getType(Tab).first();
|
||||
const tab = this.page.contents_memo.getType(Tab)[0];
|
||||
|
||||
if (!tab.content)
|
||||
throw new InnertubeError('Content tab has no content', tab);
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class History extends Feed<IBrowseResponse> {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.sections = this.memo.getType(ItemSection);
|
||||
this.feed_actions = this.memo.getType(BrowseFeedActions).first();
|
||||
this.feed_actions = this.memo.getType(BrowseFeedActions)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,8 +13,8 @@ export default class HomeFeed extends FilterableFeed<IBrowseResponse> {
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.header = this.memo.getType(FeedTabbedHeader).first();
|
||||
this.contents = this.memo.getType(RichGrid).first() || this.page.on_response_received_actions?.first();
|
||||
this.header = this.memo.getType(FeedTabbedHeader)[0];
|
||||
this.contents = this.memo.getType(RichGrid)[0] || this.page.on_response_received_actions?.[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class Library extends Feed<IBrowseResponse> {
|
||||
if (!this.page.contents_memo)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
this.header = this.memo.getType(PageHeader).first();
|
||||
this.header = this.memo.getType(PageHeader)[0];
|
||||
|
||||
const shelves = this.page.contents_memo.getType(Shelf);
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ export default class NotificationsMenu {
|
||||
if (!this.#page.actions_memo)
|
||||
throw new InnertubeError('Page actions not found');
|
||||
|
||||
this.header = this.#page.actions_memo.getType(SimpleMenuHeader).first();
|
||||
this.header = this.#page.actions_memo.getType(SimpleMenuHeader)[0];
|
||||
this.contents = this.#page.actions_memo.getType(Notification);
|
||||
}
|
||||
|
||||
async getContinuation(): Promise<NotificationsMenu> {
|
||||
const continuation = this.#page.actions_memo?.getType(ContinuationItem).first();
|
||||
const continuation = this.#page.actions_memo?.getType(ContinuationItem)[0];
|
||||
|
||||
if (!continuation)
|
||||
throw new InnertubeError('Continuation not found');
|
||||
|
||||
@@ -7,6 +7,7 @@ import PlaylistHeader from '../classes/PlaylistHeader.js';
|
||||
import PlaylistMetadata from '../classes/PlaylistMetadata.js';
|
||||
import PlaylistSidebarPrimaryInfo from '../classes/PlaylistSidebarPrimaryInfo.js';
|
||||
import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInfo.js';
|
||||
import PlaylistVideoList from '../classes/PlaylistVideoList.js';
|
||||
import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail.js';
|
||||
import ReelItem from '../classes/ReelItem.js';
|
||||
import ShortsLockupView from '../classes/ShortsLockupView.js';
|
||||
@@ -31,9 +32,10 @@ export default class Playlist extends Feed<IBrowseResponse> {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const header = this.memo.getType(PlaylistHeader).first();
|
||||
const primary_info = this.memo.getType(PlaylistSidebarPrimaryInfo).first();
|
||||
const secondary_info = this.memo.getType(PlaylistSidebarSecondaryInfo).first();
|
||||
const header = this.memo.getType(PlaylistHeader)[0];
|
||||
const primary_info = this.memo.getType(PlaylistSidebarPrimaryInfo)[0];
|
||||
const secondary_info = this.memo.getType(PlaylistSidebarSecondaryInfo)[0];
|
||||
const video_list = this.memo.getType(PlaylistVideoList)[0];
|
||||
const alert = this.page.alerts?.firstOfType(Alert);
|
||||
|
||||
if (alert && alert.alert_type === 'ERROR')
|
||||
@@ -53,7 +55,8 @@ export default class Playlist extends Feed<IBrowseResponse> {
|
||||
last_updated: this.#getStat(2, primary_info),
|
||||
can_share: header?.can_share,
|
||||
can_delete: header?.can_delete,
|
||||
is_editable: header?.is_editable,
|
||||
can_reorder: video_list?.can_reorder,
|
||||
is_editable: video_list?.is_editable,
|
||||
privacy: header?.privacy
|
||||
}
|
||||
};
|
||||
@@ -68,7 +71,7 @@ export default class Playlist extends Feed<IBrowseResponse> {
|
||||
}
|
||||
|
||||
get has_continuation() {
|
||||
const section_list = this.memo.getType(SectionList).first();
|
||||
const section_list = this.memo.getType(SectionList)[0];
|
||||
|
||||
if (!section_list)
|
||||
return super.has_continuation;
|
||||
@@ -77,7 +80,7 @@ export default class Playlist extends Feed<IBrowseResponse> {
|
||||
}
|
||||
|
||||
async getContinuationData(): Promise<IBrowseResponse | undefined> {
|
||||
const section_list = this.memo.getType(SectionList).first();
|
||||
const section_list = this.memo.getType(SectionList)[0];
|
||||
|
||||
/**
|
||||
* No section list means there can't be additional continuation nodes here,
|
||||
|
||||
@@ -29,8 +29,8 @@ export default class Search extends Feed<ISearchResponse> {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents_memo?.getType(SectionList).first().contents ||
|
||||
this.page.on_response_received_commands?.first().as(AppendContinuationItemsAction, ReloadContinuationItemsCommand).contents;
|
||||
this.page.contents_memo?.getType(SectionList)[0].contents ||
|
||||
this.page.on_response_received_commands?.[0].as(AppendContinuationItemsAction, ReloadContinuationItemsCommand).contents;
|
||||
|
||||
if (!contents)
|
||||
throw new InnertubeError('No contents found in search response');
|
||||
@@ -44,8 +44,8 @@ export default class Search extends Feed<ISearchResponse> {
|
||||
this.estimated_results = this.page.estimated_results || 0;
|
||||
|
||||
if (this.page.contents_memo) {
|
||||
this.sub_menu = this.page.contents_memo.getType(SearchSubMenu).first();
|
||||
this.watch_card = this.page.contents_memo.getType(UniversalWatchCard).first();
|
||||
this.sub_menu = this.page.contents_memo.getType(SearchSubMenu)[0];
|
||||
this.watch_card = this.page.contents_memo.getType(UniversalWatchCard)[0];
|
||||
}
|
||||
|
||||
this.refinement_cards = this.results?.firstOfType(HorizontalCardList);
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class TranscriptInfo {
|
||||
if (!this.#page.actions_memo)
|
||||
throw new Error('Page actions not found');
|
||||
|
||||
this.transcript = this.#page.actions_memo.getType(Transcript).first();
|
||||
this.transcript = this.#page.actions_memo.getType(Transcript)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -131,7 +131,7 @@ export default class VideoInfo extends MediaInfo {
|
||||
const comments_entry_point = results.get({ target_id: 'comments-entry-point' })?.as(ItemSection);
|
||||
|
||||
this.comments_entry_point_header = comments_entry_point?.contents?.firstOfType(CommentsEntryPointHeader);
|
||||
this.livechat = next?.contents_memo?.getType(LiveChat).first();
|
||||
this.livechat = next?.contents_memo?.getType(LiveChat)[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class Channel extends Feed<IBrowseResponse> {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.header = this.page.header?.item().as(C4TabbedHeader);
|
||||
this.contents = this.memo.getType(ItemSection).first() || this.page.continuation_contents?.as(ItemSectionContinuation);
|
||||
this.contents = this.memo.getType(ItemSection)[0] || this.page.continuation_contents?.as(ItemSectionContinuation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class Search extends Feed<ISearchResponse> {
|
||||
super(actions, data);
|
||||
this.estimated_results = this.page.estimated_results;
|
||||
|
||||
const item_section = this.memo.getType(ItemSection).first();
|
||||
const item_section = this.memo.getType(ItemSection)[0];
|
||||
|
||||
if (!item_section)
|
||||
throw new InnertubeError('No item section found in search response.');
|
||||
|
||||
@@ -27,8 +27,8 @@ export default class Album {
|
||||
if (!this.#page.contents_memo)
|
||||
throw new Error('No contents found in the response');
|
||||
|
||||
this.header = this.#page.contents_memo.getType(MusicDetailHeader, MusicResponsiveHeader)?.first();
|
||||
this.contents = this.#page.contents_memo.getType(MusicShelf)?.first().contents || observe([]);
|
||||
this.header = this.#page.contents_memo.getType(MusicDetailHeader, MusicResponsiveHeader)?.[0];
|
||||
this.contents = this.#page.contents_memo.getType(MusicShelf)?.[0].contents || observe([]);
|
||||
this.sections = this.#page.contents_memo.getType(MusicCarouselShelf) || observe([]);
|
||||
this.background = this.#page.background;
|
||||
this.url = this.#page.microformat?.as(MicroformatData).url_canonical;
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class Artist {
|
||||
throw new InnertubeError('Target shelf (Songs) did not have an endpoint.');
|
||||
|
||||
const page = await shelf.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
return page.contents_memo?.getType(MusicPlaylistShelf)?.first();
|
||||
return page.contents_memo?.getType(MusicPlaylistShelf)?.[0];
|
||||
}
|
||||
|
||||
get page(): IBrowseResponse {
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class Library {
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
this.#actions = actions;
|
||||
|
||||
const section_list = this.#page.contents_memo?.getType(SectionList).first();
|
||||
const section_list = this.#page.contents_memo?.getType(SectionList)[0];
|
||||
|
||||
this.header = section_list?.header?.as(MusicSideAlignedItem);
|
||||
this.contents = section_list?.contents?.as(Grid, MusicShelf);
|
||||
@@ -45,7 +45,7 @@ export default class Library {
|
||||
let target_item: MusicMultiSelectMenuItem | undefined;
|
||||
|
||||
if (typeof sort_by === 'string') {
|
||||
const button = this.#page.contents_memo?.getType(MusicSortFilterButton).first();
|
||||
const button = this.#page.contents_memo?.getType(MusicSortFilterButton)[0];
|
||||
|
||||
const options = button?.menu?.options
|
||||
.filter(
|
||||
@@ -94,7 +94,7 @@ export default class Library {
|
||||
async applyFilter(filter: string | ChipCloudChip): Promise<Library> {
|
||||
let target_chip: ChipCloudChip | undefined;
|
||||
|
||||
const chip_cloud = this.#page.contents_memo?.getType(ChipCloud).first();
|
||||
const chip_cloud = this.#page.contents_memo?.getType(ChipCloud)[0];
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
target_chip = chip_cloud?.chips.get({ text: filter });
|
||||
@@ -134,13 +134,13 @@ export default class Library {
|
||||
}
|
||||
|
||||
get sort_options(): string[] {
|
||||
const button = this.#page.contents_memo?.getType(MusicSortFilterButton).first();
|
||||
const button = this.#page.contents_memo?.getType(MusicSortFilterButton)[0];
|
||||
const options = button?.menu?.options.filter((item: MusicMultiSelectMenuItem | MusicMenuItemDivider) => item instanceof MusicMultiSelectMenuItem) as MusicMultiSelectMenuItem[];
|
||||
return options.map((item) => item.title);
|
||||
}
|
||||
|
||||
get filters(): string[] {
|
||||
return this.#page.contents_memo?.getType(ChipCloud)?.first().chips.map((chip: ChipCloudChip) => chip.text) || [];
|
||||
return this.#page.contents_memo?.getType(ChipCloud)?.[0].chips.map((chip: ChipCloudChip) => chip.text) || [];
|
||||
}
|
||||
|
||||
get page(): IBrowseResponse {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// noinspection ES6MissingAwait
|
||||
|
||||
import { Parser, MusicPlaylistShelfContinuation, SectionListContinuation } from '../index.js';
|
||||
import { MusicPlaylistShelfContinuation, Parser, SectionListContinuation } from '../index.js';
|
||||
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
import MusicDetailHeader from '../classes/MusicDetailHeader.js';
|
||||
@@ -13,17 +13,19 @@ import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import { observe, type ObservedArray } from '../helpers.js';
|
||||
import type { ApiResponse, Actions } from '../../core/index.js';
|
||||
import type { Actions, ApiResponse } from '../../core/index.js';
|
||||
import type { IBrowseResponse } from '../types/index.js';
|
||||
import type MusicThumbnail from '../classes/MusicThumbnail.js';
|
||||
import ContinuationItem from '../classes/ContinuationItem.js';
|
||||
import AppendContinuationItemsAction from '../classes/actions/AppendContinuationItemsAction.js';
|
||||
|
||||
export default class Playlist {
|
||||
readonly #page: IBrowseResponse;
|
||||
readonly #actions: Actions;
|
||||
readonly #continuation: string | null;
|
||||
readonly #continuation?: string | ContinuationItem;
|
||||
|
||||
public header?: MusicResponsiveHeader | MusicDetailHeader | MusicEditablePlaylistDetailHeader;
|
||||
public contents?: ObservedArray<MusicResponsiveListItem>;
|
||||
public contents?: ObservedArray<MusicResponsiveListItem | ContinuationItem>;
|
||||
public background?: MusicThumbnail;
|
||||
|
||||
#last_fetched_suggestions: ObservedArray<MusicResponsiveListItem> | null;
|
||||
@@ -40,15 +42,19 @@ export default class Playlist {
|
||||
const data = this.#page.continuation_contents?.as(MusicPlaylistShelfContinuation);
|
||||
if (!data.contents)
|
||||
throw new InnertubeError('No contents found in the response');
|
||||
this.contents = data.contents.as(MusicResponsiveListItem);
|
||||
this.#continuation = data.continuation;
|
||||
} else {
|
||||
if (!this.#page.contents_memo)
|
||||
throw new InnertubeError('No contents found in the response');
|
||||
this.header = this.#page.contents_memo.getType(MusicResponsiveHeader, MusicEditablePlaylistDetailHeader, MusicDetailHeader)?.first();
|
||||
this.contents = this.#page.contents_memo.getType(MusicPlaylistShelf)?.first()?.contents || observe([]);
|
||||
this.contents = data.contents.as(MusicResponsiveListItem, ContinuationItem);
|
||||
const continuation_item = this.contents.firstOfType(ContinuationItem);
|
||||
this.#continuation = data.continuation || continuation_item;
|
||||
} else if (this.#page.contents_memo) {
|
||||
this.header = this.#page.contents_memo.getType(MusicResponsiveHeader, MusicEditablePlaylistDetailHeader, MusicDetailHeader)?.[0];
|
||||
this.contents = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0]?.contents.as(MusicResponsiveListItem, ContinuationItem) || observe([]);
|
||||
this.background = this.#page.background;
|
||||
this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.first()?.continuation || null;
|
||||
const continuation_item = this.contents.firstOfType(ContinuationItem);
|
||||
this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0]?.continuation || continuation_item;
|
||||
} else if (this.#page.on_response_received_actions) {
|
||||
const append_continuation_action = this.#page.on_response_received_actions.firstOfType(AppendContinuationItemsAction);
|
||||
this.contents = append_continuation_action?.contents?.as(MusicResponsiveListItem, ContinuationItem);
|
||||
this.#continuation = this.contents?.firstOfType(ContinuationItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +65,17 @@ export default class Playlist {
|
||||
if (!this.#continuation)
|
||||
throw new InnertubeError('Continuation not found.');
|
||||
|
||||
const response = await this.#actions.execute('/browse', {
|
||||
client: 'YTMUSIC',
|
||||
continuation: this.#continuation
|
||||
});
|
||||
|
||||
let response: ApiResponse;
|
||||
|
||||
if (typeof this.#continuation === 'string') {
|
||||
response = await this.#actions.execute('/browse', {
|
||||
client: 'YTMUSIC',
|
||||
continuation: this.#continuation
|
||||
});
|
||||
} else {
|
||||
response = await this.#continuation.endpoint.call(this.#actions, { client: 'YTMUSIC' });
|
||||
}
|
||||
|
||||
return new Playlist(response, this.#actions);
|
||||
}
|
||||
|
||||
@@ -144,7 +156,7 @@ export default class Playlist {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
get items(): ObservedArray<MusicResponsiveListItem> {
|
||||
get items(): ObservedArray<MusicResponsiveListItem | ContinuationItem> {
|
||||
return this.contents || observe([]);
|
||||
}
|
||||
|
||||
|
||||
@@ -115,15 +115,15 @@ export default class Search {
|
||||
}
|
||||
|
||||
get did_you_mean(): DidYouMean | undefined {
|
||||
return this.#page.contents_memo?.getType(DidYouMean).first();
|
||||
return this.#page.contents_memo?.getType(DidYouMean)[0];
|
||||
}
|
||||
|
||||
get showing_results_for(): ShowingResultsFor | undefined {
|
||||
return this.#page.contents_memo?.getType(ShowingResultsFor).first();
|
||||
return this.#page.contents_memo?.getType(ShowingResultsFor)[0];
|
||||
}
|
||||
|
||||
get message(): Message | undefined {
|
||||
return this.#page.contents_memo?.getType(Message).first();
|
||||
return this.#page.contents_memo?.getType(Message)[0];
|
||||
}
|
||||
|
||||
get songs(): MusicShelf | undefined {
|
||||
|
||||
@@ -49,7 +49,7 @@ class TrackInfo extends MediaInfo {
|
||||
|
||||
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.find((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === title_or_page_type) ||
|
||||
this.tabs?.[0];
|
||||
|
||||
if (!target_tab)
|
||||
|
||||
@@ -5,6 +5,10 @@ export type URLTransformer = (url: URL) => URL;
|
||||
export type FormatFilter = (format: Format) => boolean;
|
||||
|
||||
export interface FormatOptions {
|
||||
/**
|
||||
* Video or audio itag
|
||||
*/
|
||||
itag?: number;
|
||||
/**
|
||||
* Video quality; 360p, 720p, 1080p, etc... also accepts 'best' and 'bestefficiency'.
|
||||
*/
|
||||
@@ -21,6 +25,10 @@ export interface FormatOptions {
|
||||
* File format, use 'any' to download any format
|
||||
*/
|
||||
format?: string;
|
||||
/**
|
||||
* Video or audio codec, e.g. 'avc', 'vp9', 'av01' for video, 'opus', 'mp4a' for audio
|
||||
*/
|
||||
codec?: string;
|
||||
/**
|
||||
* InnerTube client.
|
||||
*/
|
||||
@@ -35,4 +43,4 @@ export interface DownloadOptions extends FormatOptions {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const CLIENTS = {
|
||||
},
|
||||
YTMUSIC: {
|
||||
NAME: 'WEB_REMIX',
|
||||
VERSION: '1.20211213.00.00'
|
||||
VERSION: '1.20250219.01.00'
|
||||
},
|
||||
ANDROID: {
|
||||
NAME: 'ANDROID',
|
||||
@@ -82,7 +82,7 @@ export const CLIENTS = {
|
||||
},
|
||||
WEB_CREATOR: {
|
||||
NAME: 'WEB_CREATOR',
|
||||
VERSION: '1.20240918.03.00',
|
||||
VERSION: '1.20241203.01.00',
|
||||
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
|
||||
API_VERSION: 'v1',
|
||||
STATIC_VISITOR_ID: '6zpwvWUNAco'
|
||||
@@ -117,4 +117,4 @@ export const INNERTUBE_HEADERS_BASE = {
|
||||
'content-type': 'application/json'
|
||||
} as const;
|
||||
|
||||
export const SUPPORTED_CLIENTS = [ 'IOS', 'WEB', 'MWEB', 'YTKIDS', 'YTMUSIC', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID', 'TV', 'TV_EMBEDDED', 'WEB_EMBEDDED', 'WEB_CREATOR' ];
|
||||
export const SUPPORTED_CLIENTS = [ 'IOS', 'WEB', 'MWEB', 'YTKIDS', 'YTMUSIC', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID', 'TV', 'TV_EMBEDDED', 'WEB_EMBEDDED', 'WEB_CREATOR' ];
|
||||
|
||||
@@ -143,6 +143,14 @@ export function chooseFormat(options: FormatOptions, streaming_data?: IStreaming
|
||||
...(streaming_data.formats || []),
|
||||
...(streaming_data.adaptive_formats || [])
|
||||
];
|
||||
|
||||
if (options.itag) {
|
||||
const candidates = formats.filter((format) => format.itag === options.itag);
|
||||
if (!candidates.length)
|
||||
throw new InnertubeError('No matching formats found', { options });
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
const requires_audio = options.type ? options.type.includes('audio') : true;
|
||||
const requires_video = options.type ? options.type.includes('video') : true;
|
||||
@@ -159,6 +167,8 @@ export function chooseFormat(options: FormatOptions, streaming_data?: IStreaming
|
||||
return false;
|
||||
if (requires_video && !format.has_video)
|
||||
return false;
|
||||
if (options.codec && !format.mime_type.includes(options.codec))
|
||||
return false;
|
||||
if (options.format !== 'any' && !format.mime_type.includes(options.format || 'mp4'))
|
||||
return false;
|
||||
if (!is_best && format.quality_label !== quality)
|
||||
|
||||
@@ -425,8 +425,7 @@ function getTrackRoles(format: Format, has_drc_streams: boolean) {
|
||||
}
|
||||
|
||||
const roles: ('main' | 'dub' | 'description' | 'enhanced-audio-intelligibility' | 'alternate')[] = [
|
||||
// movie trailers can have a dubbed track as the only audio track so is_original is false but format.audio_track.audio_is_default is true
|
||||
format.is_original || format.audio_track?.audio_is_default ? 'main' : 'alternate'
|
||||
format.is_original ? 'main' : 'alternate'
|
||||
];
|
||||
|
||||
if (format.is_dubbed || format.is_auto_dubbed)
|
||||
|
||||
@@ -11,16 +11,17 @@ import type { Node } from 'estree';
|
||||
|
||||
const TAG_ = 'Utils';
|
||||
|
||||
let shim: PlatformShim | undefined;
|
||||
|
||||
export class Platform {
|
||||
static #shim: PlatformShim | undefined;
|
||||
static load(platform: PlatformShim): void {
|
||||
Platform.#shim = platform;
|
||||
shim = platform;
|
||||
}
|
||||
static get shim(): PlatformShim {
|
||||
if (!Platform.#shim) {
|
||||
if (!shim) {
|
||||
throw new Error('Platform is not loaded');
|
||||
}
|
||||
return Platform.#shim;
|
||||
return shim;
|
||||
}
|
||||
}
|
||||
export class InnertubeError extends Error {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createWriteStream, existsSync } from 'node:fs';
|
||||
import { Innertube, Utils, YT, YTMusic, YTNodes } from '../bundle/node.cjs';
|
||||
import { Innertube, YT, YTMusic, YTNodes } from '../bundle/node.cjs';
|
||||
|
||||
jest.useRealTimers();
|
||||
|
||||
@@ -317,7 +316,7 @@ describe('YouTube.js Tests', () => {
|
||||
expect(playlist.contents).toBeDefined();
|
||||
expect(playlist.contents?.length).toBeGreaterThan(0);
|
||||
|
||||
const info = await innertube.music.getInfo(playlist.contents!.first())
|
||||
const info = await innertube.music.getInfo(playlist.contents!.first().as(YTNodes.MusicResponsiveListItem))
|
||||
expect(info).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -394,13 +393,6 @@ describe('YouTube.js Tests', () => {
|
||||
expect(home.sections?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('HomeFeed#getContinuation', async () => {
|
||||
const incremental_continuation = await home.getContinuation();
|
||||
expect(incremental_continuation).toBeDefined();
|
||||
expect(incremental_continuation.sections).toBeDefined();
|
||||
expect(incremental_continuation.sections?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('HomeFeed#applyFilter', async () => {
|
||||
home = await home.applyFilter(home.filters[1]);
|
||||
expect(home).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user