mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e5c3648c1 | ||
|
|
bdd98a3b9b | ||
|
|
06750aaa74 | ||
|
|
708c5f7394 | ||
|
|
a9cdbf7010 | ||
|
|
b50d1ef67d | ||
|
|
555d257459 | ||
|
|
2aef67876e | ||
|
|
ae2557d15c | ||
|
|
8c688efb4a | ||
|
|
cffa868c6e | ||
|
|
f267fcd8be | ||
|
|
23c22a93c4 | ||
|
|
1ca20836bf | ||
|
|
5f058e69ae | ||
|
|
3500e92632 | ||
|
|
3f57c2fa5c | ||
|
|
7528ebdb60 | ||
|
|
5e3846259f | ||
|
|
222dfce6bb | ||
|
|
83cbfd631b | ||
|
|
4f9427d752 | ||
|
|
07c1b3e0e5 | ||
|
|
89548ad48a | ||
|
|
519be72445 | ||
|
|
e434bb2632 | ||
|
|
a11e5962c6 | ||
|
|
77b39c79ee | ||
|
|
7c530d30ee | ||
|
|
1e07a184ff | ||
|
|
5de7b24dc5 | ||
|
|
01fd1ee72a | ||
|
|
84b4f1efd1 | ||
|
|
046103a4d8 |
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,5 +1,86 @@
|
||||
# Changelog
|
||||
|
||||
## [5.5.0](https://github.com/LuanRT/YouTube.js/compare/v5.4.0...v5.5.0) (2023-07-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Format:** Populate audio language from captions when available ([#445](https://github.com/LuanRT/YouTube.js/issues/445)) ([bdd98a3](https://github.com/LuanRT/YouTube.js/commit/bdd98a3b9be39c11942043a300a6ebce9a15efc6))
|
||||
* **parser:** Add `CommentsSimplebox` parser ([#442](https://github.com/LuanRT/YouTube.js/issues/442)) ([555d257](https://github.com/LuanRT/YouTube.js/commit/555d257459b76d7c0158e9c6b189a75a82b10faf))
|
||||
* **parser:** Add `HashtagTile` ([#440](https://github.com/LuanRT/YouTube.js/issues/440)) ([ae2557d](https://github.com/LuanRT/YouTube.js/commit/ae2557d15c9df09bb92e0dc6191670d72b36631a))
|
||||
* **parser:** add `MacroMarkersList` ([#444](https://github.com/LuanRT/YouTube.js/issues/444)) ([708c5f7](https://github.com/LuanRT/YouTube.js/commit/708c5f7394b4ea140836b9483848cb61b97ea1af))
|
||||
* **parser:** Add `ShowMiniplayerCommand` ([#443](https://github.com/LuanRT/YouTube.js/issues/443)) ([a9cdbf7](https://github.com/LuanRT/YouTube.js/commit/a9cdbf7010e7b9b9cfde5db645d51bdad51006c5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **package:** Bump Jinter to fix bad export order ([#439](https://github.com/LuanRT/YouTube.js/issues/439)) ([2aef678](https://github.com/LuanRT/YouTube.js/commit/2aef67876ec19118b37d3cecd429ccf8239989e0))
|
||||
* **StructuredDescriptionContent:** `items` can also be a `HorizontalCardList` ([b50d1ef](https://github.com/LuanRT/YouTube.js/commit/b50d1ef67d81276864818de10c61b5a7980cbc1a))
|
||||
|
||||
## [5.4.0](https://github.com/LuanRT/YouTube.js/compare/v5.3.0...v5.4.0) (2023-07-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Channel:** Add `getPodcasts()` method ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
|
||||
* **Channel:** Add `getReleases()` method ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
|
||||
* **parser:** Add `Quiz` ([#437](https://github.com/LuanRT/YouTube.js/issues/437)) ([cffa868](https://github.com/LuanRT/YouTube.js/commit/cffa868c6eeb579047653fac65da8e913fb3c621))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Playlist:** Parse `PlaylistCustomThumbnail` for `thumbnail_renderer` ([f267fcd](https://github.com/LuanRT/YouTube.js/commit/f267fcd8beccf237b8d1924463990273887cae28))
|
||||
|
||||
## [5.3.0](https://github.com/LuanRT/YouTube.js/compare/v5.2.1...v5.3.0) (2023-07-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **toDash:** Add color information ([#430](https://github.com/LuanRT/YouTube.js/issues/430)) ([3500e92](https://github.com/LuanRT/YouTube.js/commit/3500e926327d560b1db036bfe503c276b91922ac))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **Format:** Cleanup the xtags parsing ([#434](https://github.com/LuanRT/YouTube.js/issues/434)) ([1ca2083](https://github.com/LuanRT/YouTube.js/commit/1ca20836bf343c78461fab7ad3b71db2b96e65c3))
|
||||
* **toDash:** Hoist duplicates from Representation to AdaptationSet ([#431](https://github.com/LuanRT/YouTube.js/issues/431)) ([5f058e6](https://github.com/LuanRT/YouTube.js/commit/5f058e69ae8594491133f7f96287bea4137f7822))
|
||||
|
||||
## [5.2.1](https://github.com/LuanRT/YouTube.js/compare/v5.2.0...v5.2.1) (2023-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* incorrect node parser implementations ([#428](https://github.com/LuanRT/YouTube.js/issues/428)) ([222dfce](https://github.com/LuanRT/YouTube.js/commit/222dfce6bbd13b2cd80ae11540cbc0edd9053fc5))
|
||||
|
||||
## [5.2.0](https://github.com/LuanRT/YouTube.js/compare/v5.1.0...v5.2.0) (2023-06-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **VideoDetails:** Add is_post_live_dvr property ([#411](https://github.com/LuanRT/YouTube.js/issues/411)) ([a11e596](https://github.com/LuanRT/YouTube.js/commit/a11e5962c6eb73b14623a9de1e6c8c2534146b1e))
|
||||
* **ytmusic:** Add support for YouTube Music mood filters ([#404](https://github.com/LuanRT/YouTube.js/issues/404)) ([77b39c7](https://github.com/LuanRT/YouTube.js/commit/77b39c79ee0768eb203b7d47ea81286d470c21f2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **OAuth:** client identity matching ([#421](https://github.com/LuanRT/YouTube.js/issues/421)) ([07c1b3e](https://github.com/LuanRT/YouTube.js/commit/07c1b3e0e57cb1fa42e4772775bfd1437bbc731f))
|
||||
* **PlayerEndpoint:** Use different player params ([#419](https://github.com/LuanRT/YouTube.js/issues/419)) ([519be72](https://github.com/LuanRT/YouTube.js/commit/519be72445b7ff392b396e16bcb1dc05c7df8976))
|
||||
* **Playlist:** Add thumbnail_renderer on Playlist when response includes it ([#424](https://github.com/LuanRT/YouTube.js/issues/424)) ([4f9427d](https://github.com/LuanRT/YouTube.js/commit/4f9427d752e89faec8dd1c4fd7a9607dca998c7a))
|
||||
* **VideoInfo.ts:** reimplement `get music_tracks` ([#409](https://github.com/LuanRT/YouTube.js/issues/409)) ([e434bb2](https://github.com/LuanRT/YouTube.js/commit/e434bb2632fe2b20aab6f1e707a93ca76f9d5c91))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **Search:** Speed up results parsing ([#408](https://github.com/LuanRT/YouTube.js/issues/408)) ([1e07a18](https://github.com/LuanRT/YouTube.js/commit/1e07a184ffaff508ad5ba869cb5e7dc9f095f744))
|
||||
* **toDash:** Speed up format filtering ([#405](https://github.com/LuanRT/YouTube.js/issues/405)) ([5de7b24](https://github.com/LuanRT/YouTube.js/commit/5de7b24dc55fca3eb8fccc6fa30d3c2cd60b8184))
|
||||
|
||||
## [5.1.0](https://github.com/LuanRT/YouTube.js/compare/v5.0.4...v5.1.0) (2023-05-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ReelItem:** Add accessibility label ([#401](https://github.com/LuanRT/YouTube.js/issues/401)) ([046103a](https://github.com/LuanRT/YouTube.js/commit/046103a4d8af09fafefab6e9f971184eeca75c2e))
|
||||
* **toDash:** Add audio track labels to the manifest when available ([#402](https://github.com/LuanRT/YouTube.js/issues/402)) ([84b4f1e](https://github.com/LuanRT/YouTube.js/commit/84b4f1efd111321e4f3e5a87844790c4ec9b0b52))
|
||||
|
||||
## [5.0.4](https://github.com/LuanRT/YouTube.js/compare/v5.0.3...v5.0.4) (2023-05-10)
|
||||
|
||||
|
||||
|
||||
@@ -542,6 +542,8 @@ Retrieves contents for a given channel.
|
||||
- `<channel>#getVideos()`
|
||||
- `<channel>#getShorts()`
|
||||
- `<channel>#getLiveStreams()`
|
||||
- `<channel>#getReleases()`
|
||||
- `<channel>#getPodcasts()`
|
||||
- `<channel>#getPlaylists()`
|
||||
- `<channel>#getHome()`
|
||||
- `<channel>#getCommunity()`
|
||||
|
||||
1857
package-lock.json
generated
1857
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "5.0.4",
|
||||
"version": "5.5.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",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jintr": "^1.0.0",
|
||||
"jintr": "^1.1.0",
|
||||
"linkedom": "^0.14.12",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
|
||||
@@ -96,7 +96,7 @@ export default class OAuth {
|
||||
client_id: this.#identity.client_id,
|
||||
scope: Constants.OAUTH.SCOPE,
|
||||
device_id: Platform.shim.uuidv4(),
|
||||
model_name: Constants.OAUTH.MODEL_NAME
|
||||
device_model: Constants.OAUTH.MODEL_NAME
|
||||
};
|
||||
|
||||
const response = await this.#session.http.fetch_function(new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE), {
|
||||
|
||||
@@ -8,6 +8,11 @@ export const PATH = '/player';
|
||||
* @returns The payload.
|
||||
*/
|
||||
export function build(opts: PlayerEndpointOptions): IPlayerRequest {
|
||||
const is_android =
|
||||
opts.client === 'ANDROID' ||
|
||||
opts.client === 'YTMUSIC_ANDROID' ||
|
||||
opts.client === 'YTSTUDIO_ANDROID';
|
||||
|
||||
return {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
@@ -37,8 +42,8 @@ export function build(opts: PlayerEndpointOptions): IPlayerRequest {
|
||||
...{
|
||||
client: opts.client,
|
||||
playlistId: opts.playlist_id,
|
||||
// Workaround streaming URLs returning 403 when using Android clients and throttling in web clients.
|
||||
params: '8AEB'
|
||||
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
|
||||
params: is_android ? '2AMBCgIQBg' : opts.params
|
||||
}
|
||||
};
|
||||
}
|
||||
17
src/parser/classes/CarouselLockup.ts
Normal file
17
src/parser/classes/CarouselLockup.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type ObservedArray, YTNode } from '../helpers.js';
|
||||
import InfoRow from './InfoRow.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import CompactVideo from './CompactVideo.js';
|
||||
|
||||
export default class CarouselLockup extends YTNode {
|
||||
static type = 'CarouselLockup';
|
||||
|
||||
info_rows: ObservedArray<InfoRow>;
|
||||
video_lockup?: CompactVideo | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.info_rows = Parser.parseArray(data.infoRows, InfoRow);
|
||||
this.video_lockup = Parser.parseItem(data.videoLockup, CompactVideo);
|
||||
}
|
||||
}
|
||||
26
src/parser/classes/EngagementPanelSectionList.ts
Normal file
26
src/parser/classes/EngagementPanelSectionList.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import ContinuationItem from './ContinuationItem.js';
|
||||
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.js';
|
||||
import MacroMarkersList from './MacroMarkersList.js';
|
||||
import SectionList from './SectionList.js';
|
||||
import StructuredDescriptionContent from './StructuredDescriptionContent.js';
|
||||
|
||||
export default class EngagementPanelSectionList extends YTNode {
|
||||
static type = 'EngagementPanelSectionList';
|
||||
|
||||
header: EngagementPanelTitleHeader | null;
|
||||
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | null;
|
||||
target_id?: string;
|
||||
panel_identifier?: string;
|
||||
visibility?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
|
||||
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList ]);
|
||||
this.panel_identifier = data.panelIdentifier;
|
||||
this.target_id = data.targetId;
|
||||
this.visibility = data.visibility;
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/EngagementPanelTitleHeader.ts
Normal file
17
src/parser/classes/EngagementPanelTitleHeader.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
export default class EngagementPanelTitleHeader extends YTNode {
|
||||
static type = 'EngagementPanelTitleHeader';
|
||||
|
||||
title: Text;
|
||||
visibility_button: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.visibility_button = Parser.parseItem(data.visibilityButton, Button);
|
||||
}
|
||||
}
|
||||
22
src/parser/classes/ExpandableVideoDescriptionBody.ts
Normal file
22
src/parser/classes/ExpandableVideoDescriptionBody.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class ExpandableVideoDescriptionBody extends YTNode {
|
||||
static type = 'ExpandableVideoDescriptionBody';
|
||||
|
||||
show_more_text: Text;
|
||||
show_less_text: Text;
|
||||
attributed_description_body_text?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.show_more_text = new Text(data.showMoreText);
|
||||
this.show_less_text = new Text(data.showLessText);
|
||||
|
||||
if (Reflect.has(data, 'attributedDescriptionBodyText')) {
|
||||
this.attributed_description_body_text = data.attributedDescriptionBodyText?.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/Factoid.ts
Normal file
18
src/parser/classes/Factoid.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { type RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export default class Factoid extends YTNode {
|
||||
static type = 'Factoid';
|
||||
|
||||
label: Text;
|
||||
value: Text;
|
||||
accessibility_text: String;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.label = new Text(data.label);
|
||||
this.value = new Text(data.value);
|
||||
this.accessibility_text = data.accessibilityText;
|
||||
}
|
||||
}
|
||||
28
src/parser/classes/HashtagTile.ts
Normal file
28
src/parser/classes/HashtagTile.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { Thumbnail } from '../misc.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class HashtagTile extends YTNode {
|
||||
static type = 'HashtagTile';
|
||||
|
||||
hashtag: Text;
|
||||
hashtag_info_text: Text;
|
||||
hashtag_thumbnail: Thumbnail[];
|
||||
endpoint: NavigationEndpoint;
|
||||
hashtag_background_color: number;
|
||||
hashtag_video_count: Text;
|
||||
hashtag_channel_count: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.hashtag = new Text(data.hashtag);
|
||||
this.hashtag_info_text = new Text(data.hashtagInfoText);
|
||||
this.hashtag_thumbnail = Thumbnail.fromResponse(data.hashtagThumbnail);
|
||||
this.endpoint = new NavigationEndpoint(data.onTapCommand);
|
||||
this.hashtag_background_color = data.hashtagBackgroundColor;
|
||||
this.hashtag_video_count = new Text(data.hashtagVideoCount);
|
||||
this.hashtag_channel_count = new Text(data.hashtagChannelCount);
|
||||
}
|
||||
}
|
||||
29
src/parser/classes/InfoRow.ts
Normal file
29
src/parser/classes/InfoRow.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Text } from '../misc.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
export default class InfoRow extends YTNode {
|
||||
static type = 'InfoRow';
|
||||
|
||||
title: Text;
|
||||
default_metadata?: Text;
|
||||
expanded_metadata?: Text;
|
||||
info_row_expand_status_key?: String;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
|
||||
if (Reflect.has(data, 'defaultMetadata')) {
|
||||
this.default_metadata = new Text(data.defaultMetadata);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'expandedMetadata')) {
|
||||
this.expanded_metadata = new Text(data.expandedMetadata);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'infoRowExpandStatusKey')) {
|
||||
this.info_row_expand_status_key = data.infoRowExpandStatusKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/MacroMarkersInfoItem.ts
Normal file
17
src/parser/classes/MacroMarkersInfoItem.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class MacroMarkersInfoItem extends YTNode {
|
||||
static type = 'MacroMarkersInfoItem';
|
||||
|
||||
info_text: Text;
|
||||
menu: Menu | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.info_text = new Text(data.infoText);
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/MacroMarkersList.ts
Normal file
18
src/parser/classes/MacroMarkersList.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
import MacroMarkersInfoItem from './MacroMarkersInfoItem.js';
|
||||
import MacroMarkersListItem from './MacroMarkersListItem.js';
|
||||
|
||||
export default class MacroMarkersList extends YTNode {
|
||||
static type = 'MacroMarkersList';
|
||||
|
||||
contents: ObservedArray<MacroMarkersInfoItem | MacroMarkersListItem>;
|
||||
sync_button_label: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents, [ MacroMarkersInfoItem, MacroMarkersListItem ]);
|
||||
this.sync_button_label = new Text(data.syncButtonLabel);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ export default class PlayerCaptionsTracklist extends YTNode {
|
||||
audio_tracks?: {
|
||||
audio_track_id: string;
|
||||
captions_initial_state: string;
|
||||
default_caption_track_index: number;
|
||||
default_caption_track_index?: number;
|
||||
has_default_track: boolean;
|
||||
visibility: string;
|
||||
caption_track_indices: number;
|
||||
caption_track_indices: number[];
|
||||
}[];
|
||||
|
||||
default_audio_track_index?: number;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import PlaylistCustomThumbnail from './PlaylistCustomThumbnail.js';
|
||||
import PlaylistVideoThumbnail from './PlaylistVideoThumbnail.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
@@ -12,6 +14,7 @@ export default class Playlist extends YTNode {
|
||||
title: Text;
|
||||
author: Text | Author;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_renderer?: PlaylistVideoThumbnail | PlaylistCustomThumbnail;
|
||||
video_count: Text;
|
||||
video_count_short: Text;
|
||||
first_videos: ObservedArray<YTNode>;
|
||||
@@ -41,6 +44,10 @@ export default class Playlist extends YTNode {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
|
||||
if (Reflect.has(data, 'thumbnailRenderer')) {
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, [ PlaylistVideoThumbnail, PlaylistCustomThumbnail ]) || undefined;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'viewPlaylistText')) {
|
||||
this.view_playlist = new Text(data.viewPlaylistText);
|
||||
}
|
||||
|
||||
25
src/parser/classes/Quiz.ts
Normal file
25
src/parser/classes/Quiz.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class Quiz extends YTNode {
|
||||
static type = 'Quiz';
|
||||
|
||||
choices: {
|
||||
text: Text;
|
||||
is_correct: boolean;
|
||||
}[];
|
||||
|
||||
total_votes: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.choices = data.choices.map((choice: RawNode) => ({
|
||||
text: new Text(choice.text),
|
||||
is_correct: choice.isCorrect
|
||||
}));
|
||||
|
||||
this.total_votes = new Text(data.totalVotes);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export default class ReelItem extends YTNode {
|
||||
thumbnails: Thumbnail[];
|
||||
views: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
accessibility_label?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
@@ -20,5 +21,6 @@ export default class ReelItem extends YTNode {
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.accessibility_label = data.accessibility.accessibilityData.label;
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/StructuredDescriptionContent.ts
Normal file
18
src/parser/classes/StructuredDescriptionContent.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.js';
|
||||
import HorizontalCardList from './HorizontalCardList.js';
|
||||
import VideoDescriptionHeader from './VideoDescriptionHeader.js';
|
||||
import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js';
|
||||
import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
|
||||
|
||||
export default class StructuredDescriptionContent extends YTNode {
|
||||
static type = 'StructuredDescriptionContent';
|
||||
|
||||
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection | HorizontalCardList>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]);
|
||||
}
|
||||
}
|
||||
28
src/parser/classes/VideoDescriptionHeader.ts
Normal file
28
src/parser/classes/VideoDescriptionHeader.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import { Text, Thumbnail } from '../misc.js';
|
||||
import Factoid from './Factoid.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
export default class VideoDescriptionHeader extends YTNode {
|
||||
static type = 'VideoDescriptionHeader';
|
||||
|
||||
channel: Text;
|
||||
channel_navigation_endpoint?: NavigationEndpoint;
|
||||
channel_thumbnail: Thumbnail[];
|
||||
factoids: ObservedArray<Factoid>;
|
||||
publish_date: Text;
|
||||
title: Text;
|
||||
views: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.channel = new Text(data.channel);
|
||||
this.channel_navigation_endpoint = new NavigationEndpoint(data.channelNavigationEndpoint);
|
||||
this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail);
|
||||
this.publish_date = new Text(data.publishDate);
|
||||
this.views = new Text(data.views);
|
||||
this.factoids = Parser.parseArray(data.factoid, Factoid);
|
||||
}
|
||||
}
|
||||
28
src/parser/classes/VideoDescriptionInfocardsSection.ts
Normal file
28
src/parser/classes/VideoDescriptionInfocardsSection.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import Button from './Button.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
export default class VideoDescriptionInfocardsSection extends YTNode {
|
||||
static type = 'VideoDescriptionInfocardsSection';
|
||||
|
||||
section_title: Text;
|
||||
creator_videos_button: Button | null;
|
||||
creator_about_button: Button | null;
|
||||
section_subtitle: Text;
|
||||
channel_avatar: Thumbnail[];
|
||||
channel_endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.section_title = new Text(data.sectionTitle);
|
||||
this.creator_videos_button = Parser.parseItem(data.creatorVideosButton, Button);
|
||||
this.creator_about_button = Parser.parseItem(data.creatorAboutButton, Button);
|
||||
this.section_subtitle = new Text(data.sectionSubtitle);
|
||||
this.channel_avatar = Thumbnail.fromResponse(data.channelAvatar);
|
||||
this.channel_endpoint = new NavigationEndpoint(data.channelEndpoint);
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/VideoDescriptionMusicSection.ts
Normal file
17
src/parser/classes/VideoDescriptionMusicSection.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
import CarouselLockup from './CarouselLockup.js';
|
||||
|
||||
export default class VideoDescriptionMusicSection extends YTNode {
|
||||
static type = 'VideoDescriptionMusicSection';
|
||||
|
||||
carousel_lockups: ObservedArray<CarouselLockup>;
|
||||
section_title: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.carousel_lockups = Parser.parseArray(data.carouselLockups, CarouselLockup);
|
||||
this.section_title = new Text(data.sectionTitle);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import Text from '../misc/Text.js';
|
||||
import Thumbnail from '../misc/Thumbnail.js';
|
||||
import CommentsSimplebox from './CommentsSimplebox.js';
|
||||
import CommentsEntryPointTeaser from './CommentsEntryPointTeaser.js';
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
import { Parser } from '../../index.js';
|
||||
import CommentsEntryPointTeaser from './CommentsEntryPointTeaser.js';
|
||||
|
||||
export default class CommentsEntryPointHeader extends YTNode {
|
||||
static type = 'CommentsEntryPointHeader';
|
||||
@@ -12,7 +13,7 @@ export default class CommentsEntryPointHeader extends YTNode {
|
||||
comment_count?: Text;
|
||||
teaser_avatar?: Thumbnail[];
|
||||
teaser_content?: Text;
|
||||
content_renderer?: CommentsEntryPointTeaser | null;
|
||||
content_renderer?: CommentsEntryPointTeaser | CommentsSimplebox | null;
|
||||
simplebox_placeholder?: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
@@ -35,7 +36,7 @@ export default class CommentsEntryPointHeader extends YTNode {
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'contentRenderer')) {
|
||||
this.content_renderer = Parser.parseItem(data.contentRenderer, CommentsEntryPointTeaser);
|
||||
this.content_renderer = Parser.parseItem(data.contentRenderer, [ CommentsEntryPointTeaser, CommentsSimplebox ]);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'simpleboxPlaceholder')) {
|
||||
|
||||
17
src/parser/classes/comments/CommentsSimplebox.ts
Normal file
17
src/parser/classes/comments/CommentsSimplebox.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import Text from '../misc/Text.js';
|
||||
import Thumbnail from '../misc/Thumbnail.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
export default class CommentsSimplebox extends YTNode {
|
||||
static type = 'CommentsSimplebox';
|
||||
|
||||
simplebox_avatar: Thumbnail[];
|
||||
simplebox_placeholder: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.simplebox_avatar = Thumbnail.fromResponse(data.simpleboxAvatar);
|
||||
this.simplebox_placeholder = new Text(data.simpleboxPlaceholder);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,12 @@ export default class Format {
|
||||
is_descriptive?: boolean;
|
||||
is_original?: boolean;
|
||||
|
||||
color_info?: {
|
||||
primaries?: string;
|
||||
transfer_characteristics?: string;
|
||||
matrix_coefficients?: string;
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.itag = data.itag;
|
||||
this.mime_type = data.mimeType;
|
||||
@@ -81,14 +87,24 @@ export default class Format {
|
||||
this.has_audio = !!data.audioBitrate || !!data.audioQuality;
|
||||
this.has_video = !!data.qualityLabel;
|
||||
|
||||
this.color_info = data.colorInfo ? {
|
||||
primaries: data.colorInfo.primaries?.replace('COLOR_PRIMARIES_', ''),
|
||||
transfer_characteristics: data.colorInfo.transferCharacteristics?.replace('COLOR_TRANSFER_CHARACTERISTICS_', ''),
|
||||
matrix_coefficients: data.colorInfo.matrixCoefficients?.replace('COLOR_MATRIX_COEFFICIENTS_', '')
|
||||
} : undefined;
|
||||
|
||||
if (this.has_audio) {
|
||||
const args = new URLSearchParams(this.cipher || this.signature_cipher);
|
||||
const url_components = new URLSearchParams(args.get('url') || this.url);
|
||||
|
||||
this.language = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('lang='))?.split('=').at(1) || null;
|
||||
this.is_dubbed = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'dubbed';
|
||||
this.is_descriptive = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'descriptive';
|
||||
this.is_original = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'original' || !this.is_dubbed;
|
||||
const xtags = url_components.get('xtags')?.split(':');
|
||||
|
||||
const audio_content = xtags?.find((x) => x.startsWith('acont='))?.split('=')[1];
|
||||
|
||||
this.language = xtags?.find((x: string) => x.startsWith('lang='))?.split('=')[1] || null;
|
||||
this.is_dubbed = audio_content === 'dubbed';
|
||||
this.is_descriptive = audio_content === 'descriptive';
|
||||
this.is_original = audio_content === 'original' || !this.is_dubbed || !this.is_descriptive;
|
||||
|
||||
if (Reflect.has(data, 'audioTrack')) {
|
||||
this.audio_track = {
|
||||
|
||||
@@ -18,6 +18,7 @@ export default class VideoDetails {
|
||||
is_live_content: boolean;
|
||||
is_upcoming: boolean;
|
||||
is_crawlable: boolean;
|
||||
is_post_live_dvr: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
this.id = data.videoId;
|
||||
@@ -35,6 +36,7 @@ export default class VideoDetails {
|
||||
this.is_live = !!data.isLive;
|
||||
this.is_live_content = !!data.isLiveContent;
|
||||
this.is_upcoming = !!data.isUpcoming;
|
||||
this.is_post_live_dvr = !!data.isPostLiveDvr;
|
||||
this.is_crawlable = !!data.isCrawlable;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-cond-assign */
|
||||
import { Platform } from '../utils/Utils.js';
|
||||
import { InnertubeError, Platform } from '../utils/Utils.js';
|
||||
import Author from './classes/misc/Author.js';
|
||||
import Text from './classes/misc/Text.js';
|
||||
import Thumbnail from './classes/misc/Thumbnail.js';
|
||||
@@ -62,7 +62,9 @@ export class YTNodeGenerator {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
||||
}
|
||||
static #logNewClass(classname: string, key_info: KeyInfo) {
|
||||
console.warn(`${classname} not found!\nThis is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`);
|
||||
console.warn(
|
||||
new InnertubeError(`${classname} not found!\nThis is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`)
|
||||
);
|
||||
}
|
||||
static #logChangedKeys(classname: string, key_info: KeyInfo, changed_keys: KeyInfo) {
|
||||
console.warn(`${classname} changed!\nThe following keys where altered: ${changed_keys.map(([ key ]) => this.#camelToSnake(key)).join(', ')}\nThe class has changed to:\n${this.generateTypescriptClass(classname, key_info)}`);
|
||||
@@ -460,15 +462,15 @@ export class YTNodeGenerator {
|
||||
* @returns The parsed value
|
||||
*/
|
||||
static parse(key: string, inference_type: InferenceType, data: any, key_path: string[] = [ 'data' ]) {
|
||||
const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, key ]);
|
||||
const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({ data }, [ ...key_path, key ]);
|
||||
switch (inference_type.type) {
|
||||
case 'renderer':
|
||||
{
|
||||
return should_optional ? Parser.parseItem(this.#accessDataFromKeyPath({data}, [ ...key_path, key ]), inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
return should_optional ? Parser.parseItem(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]), inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
}
|
||||
case 'renderer_list':
|
||||
{
|
||||
return should_optional ? Parser.parse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
return should_optional ? Parser.parse(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
}
|
||||
case 'object':
|
||||
{
|
||||
@@ -482,25 +484,25 @@ export class YTNodeGenerator {
|
||||
case 'misc':
|
||||
switch (inference_type.misc_type) {
|
||||
case 'NavigationEndpoint':
|
||||
return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined;
|
||||
return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined;
|
||||
case 'Text':
|
||||
return should_optional ? new Text(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined;
|
||||
return should_optional ? new Text(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined;
|
||||
case 'Thumbnail':
|
||||
return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined;
|
||||
return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined;
|
||||
case 'Author':
|
||||
{
|
||||
const author_should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]);
|
||||
const author_should_optional = !inference_type.optional || this.#hasDataFromKeyPath({ data }, [ ...key_path, inference_type.params[0] ]);
|
||||
return author_should_optional ? new Author(
|
||||
this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]),
|
||||
this.#accessDataFromKeyPath({ data }, [ ...key_path, inference_type.params[0] ]),
|
||||
inference_type.params[1] ?
|
||||
this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[1] ]) : undefined
|
||||
this.#accessDataFromKeyPath({ data }, [ ...key_path, inference_type.params[1] ]) : undefined
|
||||
) : undefined;
|
||||
}
|
||||
}
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
case 'primative':
|
||||
case 'unknown':
|
||||
return this.#accessDataFromKeyPath({data}, [ ...key_path, key ]);
|
||||
return this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]);
|
||||
}
|
||||
}
|
||||
static #passOne(classdata: any) {
|
||||
|
||||
@@ -30,6 +30,7 @@ export { default as Card } from './classes/Card.js';
|
||||
export { default as CardCollection } from './classes/CardCollection.js';
|
||||
export { default as CarouselHeader } from './classes/CarouselHeader.js';
|
||||
export { default as CarouselItem } from './classes/CarouselItem.js';
|
||||
export { default as CarouselLockup } from './classes/CarouselLockup.js';
|
||||
export { default as Channel } from './classes/Channel.js';
|
||||
export { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullMetadata.js';
|
||||
export { default as ChannelAgeGate } from './classes/ChannelAgeGate.js';
|
||||
@@ -57,6 +58,7 @@ export { default as CommentsEntryPointHeader } from './classes/comments/Comments
|
||||
export { default as CommentsEntryPointTeaser } from './classes/comments/CommentsEntryPointTeaser.js';
|
||||
export { default as CommentsHeader } from './classes/comments/CommentsHeader.js';
|
||||
export { default as CommentSimplebox } from './classes/comments/CommentSimplebox.js';
|
||||
export { default as CommentsSimplebox } from './classes/comments/CommentsSimplebox.js';
|
||||
export { default as CommentThread } from './classes/comments/CommentThread.js';
|
||||
export { default as CreatorHeart } from './classes/comments/CreatorHeart.js';
|
||||
export { default as EmojiPicker } from './classes/comments/EmojiPicker.js';
|
||||
@@ -88,9 +90,13 @@ export { default as Endscreen } from './classes/Endscreen.js';
|
||||
export { default as EndscreenElement } from './classes/EndscreenElement.js';
|
||||
export { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.js';
|
||||
export { default as EndScreenVideo } from './classes/EndScreenVideo.js';
|
||||
export { default as EngagementPanelSectionList } from './classes/EngagementPanelSectionList.js';
|
||||
export { default as EngagementPanelTitleHeader } from './classes/EngagementPanelTitleHeader.js';
|
||||
export { default as ExpandableMetadata } from './classes/ExpandableMetadata.js';
|
||||
export { default as ExpandableTab } from './classes/ExpandableTab.js';
|
||||
export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js';
|
||||
export { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.js';
|
||||
export { default as Factoid } from './classes/Factoid.js';
|
||||
export { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.js';
|
||||
export { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.js';
|
||||
export { default as GameCard } from './classes/GameCard.js';
|
||||
@@ -110,6 +116,7 @@ export { default as GuideEntry } from './classes/GuideEntry.js';
|
||||
export { default as GuideSection } from './classes/GuideSection.js';
|
||||
export { default as GuideSubscriptionsSection } from './classes/GuideSubscriptionsSection.js';
|
||||
export { default as HashtagHeader } from './classes/HashtagHeader.js';
|
||||
export { default as HashtagTile } from './classes/HashtagTile.js';
|
||||
export { default as Heatmap } from './classes/Heatmap.js';
|
||||
export { default as HeatMarker } from './classes/HeatMarker.js';
|
||||
export { default as HeroPlaylistThumbnail } from './classes/HeroPlaylistThumbnail.js';
|
||||
@@ -121,6 +128,7 @@ export { default as HorizontalMovieList } from './classes/HorizontalMovieList.js
|
||||
export { default as IconLink } from './classes/IconLink.js';
|
||||
export { default as InfoPanelContainer } from './classes/InfoPanelContainer.js';
|
||||
export { default as InfoPanelContent } from './classes/InfoPanelContent.js';
|
||||
export { default as InfoRow } from './classes/InfoRow.js';
|
||||
export { default as InteractiveTabbedHeader } from './classes/InteractiveTabbedHeader.js';
|
||||
export { default as ItemSection } from './classes/ItemSection.js';
|
||||
export { default as ItemSectionHeader } from './classes/ItemSectionHeader.js';
|
||||
@@ -172,6 +180,8 @@ export { default as LiveChatItemList } from './classes/LiveChatItemList.js';
|
||||
export { default as LiveChatMessageInput } from './classes/LiveChatMessageInput.js';
|
||||
export { default as LiveChatParticipant } from './classes/LiveChatParticipant.js';
|
||||
export { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList.js';
|
||||
export { default as MacroMarkersInfoItem } from './classes/MacroMarkersInfoItem.js';
|
||||
export { default as MacroMarkersList } from './classes/MacroMarkersList.js';
|
||||
export { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.js';
|
||||
export { default as Menu } from './classes/menus/Menu.js';
|
||||
export { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem.js';
|
||||
@@ -259,6 +269,7 @@ export { default as ProfileColumn } from './classes/ProfileColumn.js';
|
||||
export { default as ProfileColumnStats } from './classes/ProfileColumnStats.js';
|
||||
export { default as ProfileColumnStatsEntry } from './classes/ProfileColumnStatsEntry.js';
|
||||
export { default as ProfileColumnUserInfo } from './classes/ProfileColumnUserInfo.js';
|
||||
export { default as Quiz } from './classes/Quiz.js';
|
||||
export { default as RecognitionShelf } from './classes/RecognitionShelf.js';
|
||||
export { default as ReelItem } from './classes/ReelItem.js';
|
||||
export { default as ReelShelf } from './classes/ReelShelf.js';
|
||||
@@ -299,6 +310,7 @@ export { default as SingleHeroImage } from './classes/SingleHeroImage.js';
|
||||
export { default as SlimOwner } from './classes/SlimOwner.js';
|
||||
export { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.js';
|
||||
export { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.js';
|
||||
export { default as StructuredDescriptionContent } from './classes/StructuredDescriptionContent.js';
|
||||
export { default as SubFeedOption } from './classes/SubFeedOption.js';
|
||||
export { default as SubFeedSelector } from './classes/SubFeedSelector.js';
|
||||
export { default as SubscribeButton } from './classes/SubscribeButton.js';
|
||||
@@ -335,6 +347,9 @@ export { default as VerticalList } from './classes/VerticalList.js';
|
||||
export { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.js';
|
||||
export { default as Video } from './classes/Video.js';
|
||||
export { default as VideoCard } from './classes/VideoCard.js';
|
||||
export { default as VideoDescriptionHeader } from './classes/VideoDescriptionHeader.js';
|
||||
export { default as VideoDescriptionInfocardsSection } from './classes/VideoDescriptionInfocardsSection.js';
|
||||
export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.js';
|
||||
export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js';
|
||||
export { default as VideoOwner } from './classes/VideoOwner.js';
|
||||
export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js';
|
||||
|
||||
@@ -264,6 +264,13 @@ export default class Parser {
|
||||
parsed_data.cards = cards;
|
||||
}
|
||||
|
||||
const engagement_panels = data.engagementPanels?.map((e) => {
|
||||
const item = this.parseItem(e, YTNodes.EngagementPanelSectionList) as YTNodes.EngagementPanelSectionList;
|
||||
return item;
|
||||
});
|
||||
if (engagement_panels) {
|
||||
parsed_data.engagement_panels = engagement_panels;
|
||||
}
|
||||
this.#createMemo();
|
||||
const items = this.parse(data.items);
|
||||
if (items) {
|
||||
@@ -406,6 +413,8 @@ export default class Parser {
|
||||
return observe(actions.map((action: any) => {
|
||||
if (action.navigateAction)
|
||||
return new NavigateAction(action.navigateAction);
|
||||
if (action.showMiniplayerCommand)
|
||||
return new ShowMiniplayerCommand(action.showMiniplayerCommand);
|
||||
if (action.reloadContinuationItemsCommand)
|
||||
return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand);
|
||||
if (action.appendContinuationItemsAction)
|
||||
@@ -502,14 +511,15 @@ export default class Parser {
|
||||
'BrandVideoShelf',
|
||||
'BrandVideoSingleton',
|
||||
'StatementBanner',
|
||||
'GuideSigninPromo'
|
||||
'GuideSigninPromo',
|
||||
'AdsEngagementPanelContent'
|
||||
]);
|
||||
|
||||
static shouldIgnore(classname: string) {
|
||||
return this.ignore_list.has(classname);
|
||||
}
|
||||
|
||||
static #rt_nodes = new Map<string, YTNodeConstructor>(Array.from(Object.entries(YTNodes)));
|
||||
static #rt_nodes = new Map<string, YTNodeConstructor>(Object.entries(YTNodes));
|
||||
static #dynamic_nodes = new Map<string, YTNodeConstructor>();
|
||||
|
||||
static getParserByName(classname: string) {
|
||||
@@ -566,6 +576,19 @@ export class NavigateAction extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowMiniplayerCommand extends YTNode {
|
||||
static readonly type = 'showMiniplayerCommand';
|
||||
|
||||
miniplayer_command: NavigationEndpoint;
|
||||
show_premium_branding: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.miniplayer_command = new NavigationEndpoint(data.miniplayerCommand);
|
||||
this.show_premium_branding = data.showPremiumBranding;
|
||||
}
|
||||
}
|
||||
|
||||
export class AppendContinuationItemsAction extends YTNode {
|
||||
static readonly type = 'appendContinuationItemsAction';
|
||||
|
||||
@@ -739,4 +762,4 @@ export class LiveChatContinuation extends YTNode {
|
||||
|
||||
this.viewer_name = data.viewerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type VideoDetails from '../classes/misc/VideoDetails.js';
|
||||
import type Alert from '../classes/Alert.js';
|
||||
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
|
||||
|
||||
import type EngagementPanelSectionList from '../classes/EngagementPanelSectionList.js';
|
||||
export interface IParsedResponse {
|
||||
actions?: SuperParsedResult<YTNode>;
|
||||
actions_memo?: Memo;
|
||||
@@ -73,6 +73,7 @@ export interface IParsedResponse {
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
endscreen?: Endscreen;
|
||||
cards?: CardCollection;
|
||||
engagement_panels?: EngagementPanelSectionList[];
|
||||
items?: SuperParsedResult<YTNode>;
|
||||
}
|
||||
|
||||
@@ -111,6 +112,7 @@ export interface INextResponse {
|
||||
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_endpoints_memo?: Memo;
|
||||
player_overlays?: SuperParsedResult<YTNode>;
|
||||
engagement_panels?: EngagementPanelSectionList[];
|
||||
}
|
||||
|
||||
export interface IBrowseResponse {
|
||||
@@ -163,4 +165,4 @@ export interface IUpdatedMetadataResponse {
|
||||
export interface IGuideResponse {
|
||||
items: SuperParsedResult<YTNode>;
|
||||
items_memo: Memo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,4 +53,5 @@ export interface IRawResponse {
|
||||
cards?: RawNode;
|
||||
items?: RawNode[];
|
||||
frameworkUpdates?: any;
|
||||
}
|
||||
engagementPanels: RawNode[];
|
||||
}
|
||||
|
||||
@@ -163,6 +163,16 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getReleases(): Promise<Channel> {
|
||||
const tab = await this.getTabByURL('releases');
|
||||
return new Channel(this.actions, tab.page, true);
|
||||
}
|
||||
|
||||
async getPodcasts(): Promise<Channel> {
|
||||
const tab = await this.getTabByURL('podcasts');
|
||||
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);
|
||||
@@ -217,6 +227,14 @@ export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
return this.hasTabWithURL('streams');
|
||||
}
|
||||
|
||||
get has_releases(): boolean {
|
||||
return this.hasTabWithURL('releases');
|
||||
}
|
||||
|
||||
get has_podcasts(): boolean {
|
||||
return this.hasTabWithURL('podcasts');
|
||||
}
|
||||
|
||||
get has_playlists(): boolean {
|
||||
return this.hasTabWithURL('playlists');
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class Search extends Feed<ISearchResponse> {
|
||||
if (!contents)
|
||||
throw new InnertubeError('No contents found in search response');
|
||||
|
||||
this.results = contents.filterType(ItemSection).find((section) => section.contents && section.contents.length > 0)?.contents;
|
||||
this.results = contents.find((content) => content.is(ItemSection) && content.contents && content.contents.length > 0)?.as(ItemSection).contents;
|
||||
|
||||
this.refinements = this.page.refinements || [];
|
||||
this.estimated_results = this.page.estimated_results;
|
||||
|
||||
@@ -33,6 +33,8 @@ import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import { MediaInfo } from '../../core/mixins/index.js';
|
||||
import StructuredDescriptionContent from '../classes/StructuredDescriptionContent.js';
|
||||
import { VideoDescriptionMusicSection } from '../nodes.js';
|
||||
|
||||
class VideoInfo extends MediaInfo {
|
||||
#watch_next_continuation?: ContinuationItem;
|
||||
@@ -97,6 +99,28 @@ class VideoInfo extends MediaInfo {
|
||||
this.captions = info.captions;
|
||||
this.cards = info.cards;
|
||||
|
||||
if (this.streaming_data) {
|
||||
const default_audio_track = this.streaming_data.adaptive_formats.find((format) => format.audio_track?.audio_is_default);
|
||||
if (default_audio_track) {
|
||||
// The combined formats only exist for the default language, even for videos with multiple audio tracks
|
||||
// So we can copy the language from the default audio track to the combined formats
|
||||
this.streaming_data.formats.forEach((format) => format.language = default_audio_track.language);
|
||||
} else if (typeof this.captions?.default_audio_track_index !== 'undefined' && this.captions?.audio_tracks && this.captions.caption_tracks) {
|
||||
// For videos with a single audio track and captions, we can use the captions to figure out the language of the audio and combined formats
|
||||
const audioTrack = this.captions.audio_tracks[this.captions.default_audio_track_index];
|
||||
const defaultCaptionTrackIndex = audioTrack.default_caption_track_index;
|
||||
const index = audioTrack.caption_track_indices[defaultCaptionTrackIndex ? defaultCaptionTrackIndex : 0];
|
||||
const language_code = this.captions.caption_tracks[index].language_code;
|
||||
|
||||
this.streaming_data.adaptive_formats.forEach((format) => {
|
||||
if (format.has_audio) {
|
||||
format.language = language_code;
|
||||
}
|
||||
});
|
||||
this.streaming_data.formats.forEach((format) => format.language = language_code);
|
||||
}
|
||||
}
|
||||
|
||||
const two_col = next?.contents?.item().as(TwoColumnWatchNextResults);
|
||||
|
||||
const results = two_col?.results;
|
||||
@@ -336,41 +360,55 @@ class VideoInfo extends MediaInfo {
|
||||
/**
|
||||
* Get songs used in the video.
|
||||
*/
|
||||
// TODO: this seems to be broken with the new UI, further investigation needed
|
||||
get music_tracks() {
|
||||
/*
|
||||
Const metadata = this.secondary_info?.metadata;
|
||||
if (!metadata)
|
||||
return [];
|
||||
const songs = [];
|
||||
let current_song: Record<string, Text[]> = {};
|
||||
let is_music_section = false;
|
||||
for (let i = 0; i < metadata.rows.length; i++) {
|
||||
const row = metadata.rows[i];
|
||||
if (row.is(MetadataRowHeader)) {
|
||||
if (row.content?.toString().toLowerCase().startsWith('music')) {
|
||||
is_music_section = true;
|
||||
i++; // Skip the learn more link
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!is_music_section)
|
||||
continue;
|
||||
if (row.is(MetadataRow))
|
||||
current_song[row.title?.toString().toLowerCase().replace(/ /g, '_')] = row.contents;
|
||||
// TODO: this makes no sense, we continue above when
|
||||
if (row.has_divider_line) {
|
||||
songs.push(current_song);
|
||||
current_song = {};
|
||||
}
|
||||
// @TODO: Refactor this.
|
||||
const description_content = this.page[1]?.engagement_panels?.filter((panel) => panel.content?.is(StructuredDescriptionContent));
|
||||
if (description_content !== undefined && description_content.length > 0) {
|
||||
const music_section = description_content[0].content?.as(StructuredDescriptionContent)?.items?.filterType(VideoDescriptionMusicSection);
|
||||
if (music_section !== undefined && music_section.length > 0) {
|
||||
return music_section[0].carousel_lockups?.map((lookup) => {
|
||||
let song: string | undefined;
|
||||
let artist: string | undefined;
|
||||
let album: string | undefined;
|
||||
let license: string | undefined;
|
||||
let videoId: string | undefined;
|
||||
let channelId: string | undefined;
|
||||
|
||||
}
|
||||
if (is_music_section)
|
||||
songs.push(current_song);
|
||||
return songs;
|
||||
*/
|
||||
// If the song isn't in the video_lockup, it should be in the info_rows
|
||||
song = lookup.video_lockup?.title?.toString();
|
||||
// If the video id isn't in the video_lockup, it should be in the info_rows
|
||||
videoId = lookup.video_lockup?.endpoint.payload.videoId;
|
||||
for (let i = 0; i < lookup.info_rows.length; i++) {
|
||||
const info_row = lookup.info_rows[i];
|
||||
if (info_row.info_row_expand_status_key === undefined) {
|
||||
if (song === undefined) {
|
||||
song = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
|
||||
if (videoId === undefined) {
|
||||
const endpoint = info_row.default_metadata?.endpoint || info_row.expanded_metadata?.endpoint;
|
||||
videoId = endpoint?.payload?.videoId;
|
||||
}
|
||||
} else {
|
||||
album = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
|
||||
}
|
||||
} else {
|
||||
if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-artists-row-state-id') !== -1) {
|
||||
artist = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
|
||||
if (channelId === undefined) {
|
||||
const endpoint = info_row.default_metadata?.endpoint || info_row.expanded_metadata?.endpoint;
|
||||
channelId = endpoint?.payload?.browseId;
|
||||
}
|
||||
}
|
||||
if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-licenses-row-state-id') !== -1) {
|
||||
license = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return { song, artist, album, license, videoId, channelId };
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoInfo;
|
||||
export default VideoInfo;
|
||||
|
||||
@@ -9,6 +9,8 @@ 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';
|
||||
import ChipCloud from '../classes/ChipCloud.js';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
|
||||
class HomeFeed {
|
||||
#page: IBrowseResponse;
|
||||
@@ -16,6 +18,7 @@ class HomeFeed {
|
||||
#continuation?: string;
|
||||
|
||||
sections?: ObservedArray<MusicCarouselShelf | MusicTastebuilderShelf>;
|
||||
header?: ChipCloud;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#actions = actions;
|
||||
@@ -36,6 +39,7 @@ class HomeFeed {
|
||||
return;
|
||||
}
|
||||
|
||||
this.header = tab.content?.as(SectionList).header?.as(ChipCloud);
|
||||
this.#continuation = tab.content?.as(SectionList).continuation;
|
||||
this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf, MusicTastebuilderShelf);
|
||||
}
|
||||
@@ -55,6 +59,33 @@ class HomeFeed {
|
||||
return new HomeFeed(response, this.#actions);
|
||||
}
|
||||
|
||||
async applyFilter(target_filter: string | ChipCloudChip): Promise<HomeFeed> {
|
||||
let cloud_chip: ChipCloudChip | undefined;
|
||||
|
||||
if (typeof target_filter === 'string') {
|
||||
cloud_chip = this.header?.chips?.as(ChipCloudChip).get({ text: target_filter });
|
||||
if (!cloud_chip)
|
||||
throw new InnertubeError('Could not find filter with given name.', { available_filters: this.filters });
|
||||
} else if (target_filter?.is(ChipCloudChip)) {
|
||||
cloud_chip = target_filter;
|
||||
}
|
||||
|
||||
if (!cloud_chip)
|
||||
throw new InnertubeError('Invalid filter', { available_filters: this.filters });
|
||||
|
||||
if (cloud_chip?.is_selected) return this;
|
||||
|
||||
if (!cloud_chip.endpoint)
|
||||
throw new InnertubeError('Selected filter does not have an endpoint.');
|
||||
|
||||
const response = await cloud_chip.endpoint.call(this.#actions, { client: 'YTMUSIC' });
|
||||
return new HomeFeed(response, this.#actions);
|
||||
}
|
||||
|
||||
get filters(): string[] {
|
||||
return this.header?.chips?.as(ChipCloudChip).map((chip) => chip.text) || [];
|
||||
}
|
||||
|
||||
get has_continuation(): boolean {
|
||||
return !!this.#continuation;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ export const OAUTH = Object.freeze({
|
||||
}),
|
||||
REGEX: Object.freeze({
|
||||
AUTH_SCRIPT: /<script id="base-js" src="(.*?)" nonce=".*?"><\/script>/,
|
||||
CLIENT_IDENTITY: /.+?={};var .+?={clientId:"(?<client_id>.+?)",.+?:"(?<client_secret>.+?)"},/
|
||||
CLIENT_IDENTITY: /var .+?={clientId:"(?<client_id>.+?)",.+?:"(?<client_secret>.+?)".+?}/
|
||||
})
|
||||
});
|
||||
export const CLIENTS = Object.freeze({
|
||||
WEB: {
|
||||
NAME: 'WEB',
|
||||
VERSION: '2.20230104.01.00',
|
||||
VERSION: '2.20230622.06.00',
|
||||
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
|
||||
API_VERSION: 'v1',
|
||||
STATIC_VISITOR_ID: '6zpwvWUNAco'
|
||||
|
||||
@@ -146,7 +146,7 @@ class FormatUtils {
|
||||
chunk_end += chunk_size;
|
||||
|
||||
resolve();
|
||||
return;
|
||||
|
||||
} catch (e: any) {
|
||||
reject(e);
|
||||
}
|
||||
@@ -249,22 +249,14 @@ class FormatUtils {
|
||||
if (!streaming_data)
|
||||
throw new InnertubeError('Streaming data not available');
|
||||
|
||||
let filtered_streaming_data;
|
||||
let adaptive_formats;
|
||||
|
||||
if (format_filter) {
|
||||
filtered_streaming_data = {
|
||||
formats: streaming_data.formats.filter((fmt: Format) => !(format_filter(fmt))),
|
||||
adaptive_formats: streaming_data.adaptive_formats.filter((fmt: Format) => !(format_filter(fmt))),
|
||||
expires: streaming_data.expires,
|
||||
dash_manifest_url: streaming_data.dash_manifest_url,
|
||||
hls_manifest_url: streaming_data.hls_manifest_url
|
||||
};
|
||||
adaptive_formats = streaming_data.adaptive_formats.filter((fmt: Format) => !(format_filter(fmt)));
|
||||
} else {
|
||||
filtered_streaming_data = streaming_data;
|
||||
adaptive_formats = streaming_data.adaptive_formats;
|
||||
}
|
||||
|
||||
const { adaptive_formats } = filtered_streaming_data;
|
||||
|
||||
if (!adaptive_formats.length)
|
||||
throw new InnertubeError('No adaptive formats found');
|
||||
|
||||
@@ -363,7 +355,12 @@ class FormatUtils {
|
||||
this.#el(document, 'Role', {
|
||||
schemeIdUri: 'urn:mpeg:dash:role:2011',
|
||||
value: role
|
||||
})
|
||||
}),
|
||||
this.#el(document, 'Label', {
|
||||
id: set_id.toString()
|
||||
}, [
|
||||
document.createTextNode(first_format.audio_track?.display_name as string)
|
||||
])
|
||||
);
|
||||
|
||||
const set = this.#el(document, 'AdaptationSet', {
|
||||
@@ -371,13 +368,22 @@ class FormatUtils {
|
||||
mimeType: mime_types[i].split(';')[0],
|
||||
startWithSAP: '1',
|
||||
subsegmentAlignment: 'true',
|
||||
lang: first_format.language as string
|
||||
lang: first_format.language as string,
|
||||
// Non-standard attribute used by shaka instead of the standard Label element
|
||||
label: first_format.audio_track?.display_name as string
|
||||
}, children);
|
||||
|
||||
const hoisted: string[] = [];
|
||||
|
||||
this.#hoistCodecsIfPossible(set, track_objects[j], hoisted);
|
||||
this.#hoistNumberAttributeIfPossible(set, track_objects[j], 'audioSamplingRate', 'audio_sample_rate', hoisted);
|
||||
|
||||
this.#hoistAudioChannelsIfPossible(document, set, track_objects[j], hoisted);
|
||||
|
||||
period.appendChild(set);
|
||||
|
||||
for (const format of track_objects[j]) {
|
||||
await this.#generateRepresentationAudio(document, set, format, url_transformer, cpn, player, actions);
|
||||
await this.#generateRepresentationAudio(document, set, format, url_transformer, hoisted, cpn, player, actions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -388,43 +394,179 @@ class FormatUtils {
|
||||
subsegmentAlignment: 'true'
|
||||
});
|
||||
|
||||
const color_info = mime_objects[i][0].color_info;
|
||||
if (typeof color_info !== 'undefined') {
|
||||
// Section 5.5 Video source metadata signalling https://dashif.org/docs/IOP-Guidelines/DASH-IF-IOP-Part7-v5.0.0.pdf
|
||||
// Section 8 Video code points https://www.itu.int/rec/T-REC-H.273-202107-I/en
|
||||
// The player.js file was also helpful
|
||||
|
||||
if (color_info.primaries) {
|
||||
let primaries = '';
|
||||
|
||||
switch (color_info.primaries) {
|
||||
case 'BT709':
|
||||
primaries = '1';
|
||||
break;
|
||||
case 'BT2020':
|
||||
primaries = '9';
|
||||
break;
|
||||
}
|
||||
|
||||
if (primaries !== '') {
|
||||
set.appendChild(this.#el(document, 'EssentialProperty', {
|
||||
schemeIdUri: 'urn:mpeg:mpegB:cicp:ColourPrimaries',
|
||||
value: primaries
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (color_info.transfer_characteristics) {
|
||||
let transfer_characteristics = '';
|
||||
|
||||
switch (color_info.transfer_characteristics) {
|
||||
case 'BT709':
|
||||
transfer_characteristics = '1';
|
||||
break;
|
||||
case 'BT2020_10':
|
||||
transfer_characteristics = '14';
|
||||
break;
|
||||
case 'SMPTEST2084':
|
||||
transfer_characteristics = '16';
|
||||
break;
|
||||
case 'ARIB_STD_B67':
|
||||
transfer_characteristics = '18';
|
||||
break;
|
||||
}
|
||||
|
||||
if (transfer_characteristics !== '') {
|
||||
set.appendChild(this.#el(document, 'EssentialProperty', {
|
||||
schemeIdUri: 'urn:mpeg:mpegB:cicp:TransferCharacteristics',
|
||||
value: transfer_characteristics
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (color_info.matrix_coefficients) {
|
||||
let matrix_coefficients = '';
|
||||
|
||||
// This list is incomplete, as the player.js doesn't currently have any code for matrix coefficients,
|
||||
// So it doesn't have a list like with the other two, so this is just based on what we've seen in responses
|
||||
switch (color_info.matrix_coefficients) {
|
||||
case 'BT709':
|
||||
matrix_coefficients = '1';
|
||||
break;
|
||||
case 'BT2020_NCL':
|
||||
matrix_coefficients = '14';
|
||||
break;
|
||||
default: {
|
||||
const format = mime_objects[i][0];
|
||||
const url = new URL(format.url as string);
|
||||
|
||||
const anonymisedFormat = JSON.parse(JSON.stringify(format));
|
||||
anonymisedFormat.url = 'REDACTED';
|
||||
anonymisedFormat.signature_cipher = 'REDACTED';
|
||||
anonymisedFormat.cipher = 'REDACTED';
|
||||
|
||||
console.warn(`YouTube.js toDash(): Unknown matrix coefficients "${color_info.matrix_coefficients}", the DASH manifest is still usuable without this.\n`
|
||||
+ `Please report it at ${Platform.shim.info.bugs_url} so we can add support for it.\n`
|
||||
+ `Innertube client: ${url.searchParams.get('c')}\nformat:`, anonymisedFormat);
|
||||
}
|
||||
}
|
||||
|
||||
if (matrix_coefficients !== '') {
|
||||
set.appendChild(this.#el(document, 'EssentialProperty', {
|
||||
schemeIdUri: 'urn:mpeg:mpegB:cicp:MatrixCoefficients',
|
||||
value: matrix_coefficients
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hoisted: string[] = [];
|
||||
|
||||
this.#hoistCodecsIfPossible(set, mime_objects[i], hoisted);
|
||||
|
||||
if (mime_objects[i][0].has_audio) {
|
||||
this.#hoistNumberAttributeIfPossible(set, mime_objects[i], 'audioSamplingRate', 'audio_sample_rate', hoisted);
|
||||
|
||||
this.#hoistAudioChannelsIfPossible(document, set, mime_objects[i], hoisted);
|
||||
|
||||
const language = mime_objects[i][0].language;
|
||||
if (language) {
|
||||
set.setAttribute('lang', language);
|
||||
}
|
||||
} else {
|
||||
set.setAttribute('maxPlayoutRate', '1');
|
||||
|
||||
this.#hoistNumberAttributeIfPossible(set, mime_objects[i], 'frameRate', 'fps', hoisted);
|
||||
}
|
||||
|
||||
period.appendChild(set);
|
||||
|
||||
for (const format of mime_objects[i]) {
|
||||
if (format.has_video) {
|
||||
await this.#generateRepresentationVideo(document, set, format, url_transformer, cpn, player, actions);
|
||||
await this.#generateRepresentationVideo(document, set, format, url_transformer, hoisted, cpn, player, actions);
|
||||
} else {
|
||||
await this.#generateRepresentationAudio(document, set, format, url_transformer, cpn, player, actions);
|
||||
await this.#generateRepresentationAudio(document, set, format, url_transformer, hoisted, cpn, player, actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async #generateRepresentationVideo(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, cpn?: string, player?: Player, actions?: Actions) {
|
||||
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
|
||||
static #hoistCodecsIfPossible(set: Element, formats: Format[], hoisted: string[]) {
|
||||
if (formats.length > 1 && new Set(formats.map((format) => getStringBetweenStrings(format.mime_type, 'codecs="', '"'))).size === 1) {
|
||||
set.setAttribute('codecs', getStringBetweenStrings(formats[0].mime_type, 'codecs="', '"') as string);
|
||||
hoisted.push('codecs');
|
||||
}
|
||||
}
|
||||
|
||||
static #hoistNumberAttributeIfPossible(set: Element, formats: Format[], attribute: 'audioSamplingRate' | 'frameRate', property: 'audio_sample_rate' | 'fps', hoisted: string[]) {
|
||||
if (formats.length > 1 && new Set(formats.map((format) => format.fps)).size === 1) {
|
||||
set.setAttribute(attribute, formats[0][property]?.toString() as string);
|
||||
hoisted.push(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
static #hoistAudioChannelsIfPossible(document: XMLDocument, set: Element, formats: Format[], hoisted: string[]) {
|
||||
if (formats.length > 1 && new Set(formats.map((format) => format.audio_channels?.toString() || '2')).size === 1) {
|
||||
set.appendChild(
|
||||
this.#el(document, 'AudioChannelConfiguration', {
|
||||
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
|
||||
value: formats[0].audio_channels?.toString() || '2'
|
||||
})
|
||||
);
|
||||
hoisted.push('AudioChannelConfiguration');
|
||||
}
|
||||
}
|
||||
|
||||
static async #generateRepresentationVideo(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, hoisted: string[], cpn?: string, player?: Player, actions?: Actions) {
|
||||
const url = new URL(format.decipher(player));
|
||||
url.searchParams.set('cpn', cpn || '');
|
||||
|
||||
const representation = this.#el(document, 'Representation', {
|
||||
id: format.itag?.toString(),
|
||||
codecs,
|
||||
bandwidth: format.bitrate?.toString(),
|
||||
width: format.width?.toString(),
|
||||
height: format.height?.toString(),
|
||||
maxPlayoutRate: '1',
|
||||
frameRate: format.fps?.toString()
|
||||
height: format.height?.toString()
|
||||
});
|
||||
|
||||
if (!hoisted.includes('codecs')) {
|
||||
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
|
||||
representation.setAttribute('codecs', codecs as string);
|
||||
}
|
||||
|
||||
if (!hoisted.includes('frameRate')) {
|
||||
representation.setAttribute('frameRate', format.fps?.toString() as string);
|
||||
}
|
||||
|
||||
set.appendChild(representation);
|
||||
|
||||
await this.#generateSegmentInformation(document, representation, format, url_transformer(url)?.toString(), actions);
|
||||
}
|
||||
|
||||
static async #generateRepresentationAudio(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, cpn?: string, player?: Player, actions?: Actions) {
|
||||
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
|
||||
|
||||
static async #generateRepresentationAudio(document: XMLDocument, set: Element, format: Format, url_transformer: URLTransformer, hoisted: string[], cpn?: string, player?: Player, actions?: Actions) {
|
||||
const url = new URL(format.decipher(player));
|
||||
url.searchParams.set('cpn', cpn || '');
|
||||
|
||||
@@ -437,15 +579,26 @@ class FormatUtils {
|
||||
|
||||
const representation = this.#el(document, 'Representation', {
|
||||
id,
|
||||
codecs,
|
||||
bandwidth: format.bitrate?.toString(),
|
||||
audioSamplingRate: format.audio_sample_rate?.toString()
|
||||
}, [
|
||||
this.#el(document, 'AudioChannelConfiguration', {
|
||||
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
|
||||
value: format.audio_channels?.toString() || '2'
|
||||
})
|
||||
]);
|
||||
bandwidth: format.bitrate?.toString()
|
||||
});
|
||||
|
||||
if (!hoisted.includes('codecs')) {
|
||||
const codecs = getStringBetweenStrings(format.mime_type, 'codecs="', '"');
|
||||
representation.setAttribute('codecs', codecs as string);
|
||||
}
|
||||
|
||||
if (!hoisted.includes('audioSamplingRate')) {
|
||||
representation.setAttribute('audioSamplingRate', format.audio_sample_rate?.toString() as string);
|
||||
}
|
||||
|
||||
if (!hoisted.includes('AudioChannelConfiguration')) {
|
||||
representation.appendChild(
|
||||
this.#el(document, 'AudioChannelConfiguration', {
|
||||
schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
|
||||
value: format.audio_channels?.toString() || '2'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
set.appendChild(representation);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createWriteStream, existsSync } from 'node:fs';
|
||||
import { Innertube, IBrowseResponse, Utils, YT, YTMusic, YTNodes } from '../bundle/node.cjs';
|
||||
import { Innertube, Utils, YT, YTMusic, YTNodes } from '../bundle/node.cjs';
|
||||
|
||||
describe('YouTube.js Tests', () => {
|
||||
let innertube: Innertube;
|
||||
@@ -19,6 +19,30 @@ describe('YouTube.js Tests', () => {
|
||||
expect(info.basic_info.id).toBe('bUHZ2k9DYHY');
|
||||
});
|
||||
|
||||
describe('Innertube#getBasicInfo', () => {
|
||||
test('Format#language multiple audio tracks', async () => {
|
||||
const info = await innertube.getBasicInfo('Kn56bMZ9OE8')
|
||||
expect(info.streaming_data).toBeDefined()
|
||||
|
||||
const default_track_format = info.streaming_data?.adaptive_formats.find(format => format.audio_track?.audio_is_default)
|
||||
expect(default_track_format).toBeDefined()
|
||||
expect(default_track_format?.language).toBe('en')
|
||||
|
||||
// check that the language is properly propagated to the non-dash formats
|
||||
expect(info.streaming_data?.formats[0].language).toBe('en')
|
||||
})
|
||||
|
||||
test('Format#language single audio track with captions', async () => {
|
||||
const info = await innertube.getBasicInfo('gisdyTBMNyQ')
|
||||
expect(info.streaming_data).toBeDefined()
|
||||
|
||||
const audio_format = info.streaming_data?.adaptive_formats.find(format => format.has_audio)
|
||||
expect(audio_format).toBeDefined()
|
||||
expect(audio_format?.language).toBe('en')
|
||||
expect(info.streaming_data?.formats[0].language).toBe('en')
|
||||
})
|
||||
})
|
||||
|
||||
test('Innertube#search', async () => {
|
||||
const search = await innertube.search('Anton Petrov');
|
||||
expect(search.page).toBeDefined();
|
||||
@@ -296,6 +320,13 @@ describe('YouTube.js Tests', () => {
|
||||
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();
|
||||
expect(home.sections).toBeDefined();
|
||||
expect(home.sections?.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test('Innertube#music.getExplore', async () => {
|
||||
@@ -314,7 +345,7 @@ describe('YouTube.js Tests', () => {
|
||||
});
|
||||
|
||||
test('Innertube#music.getAlbum', async () => {
|
||||
const album = await innertube.music.getAlbum('MPREb_aWzgoemOnfa');
|
||||
const album = await innertube.music.getAlbum('MPREb_YpQ7SWMPLvu');
|
||||
expect(album).toBeDefined();
|
||||
expect(album.header).toBeDefined();
|
||||
expect(album.sections).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user