mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 01:52:11 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e5c3648c1 | ||
|
|
bdd98a3b9b | ||
|
|
06750aaa74 | ||
|
|
708c5f7394 | ||
|
|
a9cdbf7010 | ||
|
|
b50d1ef67d | ||
|
|
555d257459 | ||
|
|
2aef67876e | ||
|
|
ae2557d15c | ||
|
|
8c688efb4a | ||
|
|
cffa868c6e | ||
|
|
f267fcd8be |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
@@ -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.3.0",
|
||||
"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"
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
@@ -9,7 +10,7 @@ export default class EngagementPanelSectionList extends YTNode {
|
||||
static type = 'EngagementPanelSectionList';
|
||||
|
||||
header: EngagementPanelTitleHeader | null;
|
||||
content: SectionList | ContinuationItem | StructuredDescriptionContent | null;
|
||||
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | null;
|
||||
target_id?: string;
|
||||
panel_identifier?: string;
|
||||
visibility?: string;
|
||||
@@ -17,7 +18,7 @@ export default class EngagementPanelSectionList extends YTNode {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
|
||||
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]);
|
||||
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList ]);
|
||||
this.panel_identifier = data.panelIdentifier;
|
||||
this.target_id = data.targetId;
|
||||
this.visibility = data.visibility;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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,7 @@
|
||||
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';
|
||||
@@ -13,7 +14,7 @@ export default class Playlist extends YTNode {
|
||||
title: Text;
|
||||
author: Text | Author;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_renderer?: PlaylistVideoThumbnail;
|
||||
thumbnail_renderer?: PlaylistVideoThumbnail | PlaylistCustomThumbnail;
|
||||
video_count: Text;
|
||||
video_count_short: Text;
|
||||
first_videos: ObservedArray<YTNode>;
|
||||
@@ -44,7 +45,7 @@ export default class Playlist extends YTNode {
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
|
||||
if (Reflect.has(data, 'thumbnailRenderer')) {
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, PlaylistVideoThumbnail) || undefined;
|
||||
this.thumbnail_renderer = Parser.parseItem(data.thumbnailRenderer, [ PlaylistVideoThumbnail, PlaylistCustomThumbnail ]) || undefined;
|
||||
}
|
||||
|
||||
if (Reflect.has(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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -8,10 +9,10 @@ import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
|
||||
export default class StructuredDescriptionContent extends YTNode {
|
||||
static type = 'StructuredDescriptionContent';
|
||||
|
||||
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection>;
|
||||
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection | HorizontalCardList>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection ]);
|
||||
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -58,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';
|
||||
@@ -115,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';
|
||||
@@ -178,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';
|
||||
@@ -265,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';
|
||||
|
||||
@@ -413,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)
|
||||
@@ -574,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';
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -99,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;
|
||||
|
||||
@@ -146,7 +146,7 @@ class FormatUtils {
|
||||
chunk_end += chunk_size;
|
||||
|
||||
resolve();
|
||||
return;
|
||||
|
||||
} catch (e: any) {
|
||||
reject(e);
|
||||
}
|
||||
@@ -491,6 +491,11 @@ class FormatUtils {
|
||||
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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user