Compare commits

...

1 Commits

Author SHA1 Message Date
LuanRT
8293af93fa chore: v10.1.0 release 2024-07-10 06:45:35 +00:00
26 changed files with 599 additions and 164 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "10.0.0",
"version": "10.1.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",
@@ -103,7 +103,7 @@
},
"license": "MIT",
"dependencies": {
"jintr": "^1.1.0",
"jintr": "^2.0.0",
"tslib": "^2.5.0",
"undici": "^5.19.1"
},

View File

@@ -220,12 +220,20 @@ export default class Player {
}
static extractNSigSourceCode(data: string): string {
const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`;
let sc = getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}');
if (!sc)
Log.warn(TAG, 'Failed to extract n-token decipher algorithm');
if (sc)
return `function descramble_nsig(a) { let b=a.split("")${sc}} return b.join(""); } descramble_nsig(nsig)`;
return sc;
sc = getStringBetweenStrings(data, 'b=String.prototype.split.call(a,"")', '}return Array.prototype.join.call(b,"")}');
if (sc)
return `function descramble_nsig(a) { let b=String.prototype.split.call(a, "")${sc}} return Array.prototype.join.call(b, ""); } descramble_nsig(nsig)`;
// We really should throw an error here to avoid errors later, returning a pass-through function for backwards-compatibility
Log.warn(TAG, 'Failed to extract n-token decipher algorithm');
return 'function descramble_nsig(a) { return a; } descramble_nsig(nsig)';
}
get url(): string {

View File

@@ -59,6 +59,9 @@ export type Context = {
isWebNativeShareAvailable: boolean;
};
memoryTotalKbytes?: string;
configInfo?: {
appInstallData: string;
},
kidsAppInfo?: {
categorySettings: {
enabledCategories: string[];
@@ -97,6 +100,7 @@ type ContextData = {
enable_safety_mode: boolean;
browser_name?: string;
browser_version?: string;
app_install_data?: string;
device_make: string;
device_model: string;
on_behalf_of_user?: string;
@@ -435,6 +439,9 @@ export default class Session extends EventEmitter {
const [ [ device_info ], api_key ] = ytcfg;
const config_info = device_info[61];
const app_install_data = config_info[config_info.length - 1];
const context_info = {
hl: options.lang || device_info[0],
gl: options.location || device_info[2],
@@ -450,6 +457,7 @@ export default class Session extends EventEmitter {
browser_version: device_info[87],
device_make: device_info[11],
device_model: device_info[12],
app_install_data: app_install_data,
enable_safety_mode: options.enable_safety_mode
};
@@ -480,7 +488,7 @@ export default class Session extends EventEmitter {
deviceModel: args.device_model,
browserName: args.browser_name,
browserVersion: args.browser_version,
utcOffsetMinutes: -new Date().getTimezoneOffset(),
utcOffsetMinutes: -Math.floor((new Date()).getTimezoneOffset()),
memoryTotalKbytes: '8000000',
mainAppWebInfo: {
graftUrl: Constants.URLS.YT_BASE,
@@ -499,6 +507,9 @@ export default class Session extends EventEmitter {
}
};
if (args.app_install_data)
context.client.configInfo = { appInstallData: args.app_install_data };
if (args.on_behalf_of_user)
context.user.onBehalfOfUser = args.on_behalf_of_user;

View File

@@ -54,11 +54,16 @@ export default class MediaInfo {
}
let storyboards;
let captions;
if (options.include_thumbnails && player_response.storyboards) {
storyboards = player_response.storyboards;
}
if (typeof options.captions_format === 'string' && player_response.captions?.caption_tracks) {
captions = player_response.captions.caption_tracks;
}
return FormatUtils.toDash(
this.streaming_data,
this.page[0].video_details?.is_post_live_dvr,
@@ -68,6 +73,7 @@ export default class MediaInfo {
this.#actions.session.player,
this.#actions,
storyboards,
captions,
options
);
}

View File

@@ -10,7 +10,8 @@ export default class InfoPanelContent extends YTNode {
title: Text;
source: Text;
attributed_paragraphs: Text[];
paragraphs?: Text[];
attributed_paragraphs?: Text[];
thumbnail: Thumbnail[];
source_endpoint: NavigationEndpoint;
truncate_paragraphs: boolean;
@@ -21,7 +22,13 @@ export default class InfoPanelContent extends YTNode {
super();
this.title = new Text(data.title);
this.source = new Text(data.source);
this.attributed_paragraphs = data.attributedParagraphs.map((p: AttributedText) => Text.fromAttributed(p));
if (Reflect.has(data, 'paragraphs'))
this.paragraphs = data.paragraphs.map((p: RawNode) => new Text(p));
if (Reflect.has(data, 'attributedParagraphs'))
this.attributed_paragraphs = data.attributedParagraphs.map((p: AttributedText) => Text.fromAttributed(p));
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
this.source_endpoint = new NavigationEndpoint(data.sourceEndpoint);
this.truncate_paragraphs = !!data.truncateParagraphs;

View File

@@ -5,11 +5,13 @@ export default class MusicEditablePlaylistDetailHeader extends YTNode {
static type = 'MusicEditablePlaylistDetailHeader';
header: YTNode;
edit_header: YTNode;
playlist_id: string;
constructor(data: RawNode) {
super();
this.header = Parser.parseItem(data.header);
// TODO: Parse data.editHeader.musicPlaylistEditHeaderRenderer.
this.edit_header = Parser.parseItem(data.editHeader);
this.playlist_id = data.playlistId;
}
}

View File

@@ -0,0 +1,28 @@
import { Parser, type RawNode } from '../index.ts';
import { YTNode } from '../helpers.ts';
import NavigationEndpoint from './NavigationEndpoint.ts';
import Dropdown from './Dropdown.ts';
import Text from './misc/Text.ts';
export default class MusicPlaylistEditHeader extends YTNode {
static type = 'MusicPlaylistEditHeader';
title: Text;
edit_title: Text;
edit_description: Text;
privacy: string;
playlist_id: string;
endpoint: NavigationEndpoint;
privacy_dropdown: Dropdown | null;
constructor(data: RawNode) {
super();
this.title = new Text(data.title);
this.edit_title = new Text(data.editTitle);
this.edit_description = new Text(data.editDescription);
this.privacy = data.privacy;
this.playlist_id = data.playlistId;
this.endpoint = new NavigationEndpoint(data.collaborationSettingsCommand);
this.privacy_dropdown = Parser.parseItem(data.privacyDropdown, Dropdown);
}
}

View File

@@ -7,6 +7,8 @@ import MusicPlayButton from './MusicPlayButton.ts';
import ToggleButton from './ToggleButton.ts';
import Menu from './menus/Menu.ts';
import Text from './misc/Text.ts';
import Button from './Button.ts';
import DownloadButton from './DownloadButton.ts';
import type { ObservedArray } from '../helpers.ts';
@@ -14,7 +16,7 @@ export default class MusicResponsiveHeader extends YTNode {
static type = 'MusicResponsiveHeader';
thumbnail: MusicThumbnail | null;
buttons: ObservedArray<ToggleButton | MusicPlayButton | Menu> | null;
buttons: ObservedArray<DownloadButton | ToggleButton | MusicPlayButton | Button | Menu>;
title: Text;
subtitle: Text;
strapline_text_one: Text;
@@ -26,7 +28,7 @@ export default class MusicResponsiveHeader extends YTNode {
constructor(data: RawNode) {
super();
this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail);
this.buttons = Parser.parseArray(data.buttons, [ ToggleButton, MusicPlayButton, Menu ]);
this.buttons = Parser.parseArray(data.buttons, [ DownloadButton, ToggleButton, MusicPlayButton, Button, Menu ]);
this.title = new Text(data.title);
this.subtitle = new Text(data.subtitle);
this.strapline_text_one = new Text(data.straplineTextOne);

View File

@@ -2,17 +2,19 @@ import Text from './misc/Text.ts';
import { YTNode } from '../helpers.ts';
import type { RawNode } from '../index.ts';
export interface CaptionTrackData {
base_url: string;
name: Text;
vss_id: string;
language_code: string;
kind?: 'asr' | 'frc';
is_translatable: boolean;
}
export default class PlayerCaptionsTracklist extends YTNode {
static type = 'PlayerCaptionsTracklist';
caption_tracks?: {
base_url: string;
name: Text;
vss_id: string;
language_code: string;
kind?: 'asr' | 'frc';
is_translatable: boolean;
}[];
caption_tracks?: CaptionTrackData[];
audio_tracks?: {
audio_track_id: string;

View File

@@ -6,46 +6,51 @@ export default class Format {
#this_response_nsig_cache?: Map<string, string>;
itag: number;
url?: string;
width?: number;
height?: number;
last_modified: Date;
content_length?: number;
quality?: string;
xtags?: string;
drm_families?: string[];
fps?: number;
quality_label?: string;
projection_type?: 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
average_bitrate?: number;
bitrate: number;
spatial_audio_type?: 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
target_duration_dec?: number;
fair_play_key_uri?: string;
stereo_layout?: 'LEFT_RIGHT' | 'TOP_BOTTOM';
max_dvr_duration_sec?: number;
high_replication?: boolean;
audio_quality?: string;
approx_duration_ms: number;
audio_sample_rate?: number;
audio_channels?: number;
loudness_db?: number;
signature_cipher?: string;
is_drc?: boolean;
drm_track_type?: string;
distinct_params?: string;
track_absolute_loudness_lkfs?: number;
mime_type: string;
is_type_otf: boolean;
bitrate: number;
average_bitrate?: number;
width: number;
height: number;
projection_type?: 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
stereo_layout?: 'LEFT_RIGHT' | 'TOP_BOTTOM';
init_range?: {
start: number;
end: number;
};
index_range?: {
start: number;
end: number;
};
last_modified: Date;
content_length?: number;
quality?: string;
quality_label?: string;
fps?: number;
url?: string;
cipher?: string;
signature_cipher?: string;
audio_quality?: string;
audio_track?: {
audio_is_default: boolean;
display_name: string;
id: string;
};
approx_duration_ms: number;
audio_sample_rate?: number;
audio_channels?: number;
loudness_db?: number;
spatial_audio_type?: 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
max_dvr_duration_sec?: number;
target_duration_dec?: number;
has_audio: boolean;
has_video: boolean;
has_text: boolean;
@@ -53,14 +58,11 @@ export default class Format {
is_dubbed?: boolean;
is_descriptive?: boolean;
is_original?: boolean;
is_drc?: boolean;
color_info?: {
primaries?: string;
transfer_characteristics?: string;
matrix_coefficients?: string;
};
caption_track?: {
display_name: string;
vss_id: string;
@@ -79,56 +81,116 @@ export default class Format {
this.is_type_otf = data.type === 'FORMAT_STREAM_TYPE_OTF';
this.bitrate = data.bitrate;
this.average_bitrate = data.averageBitrate;
this.width = data.width;
this.height = data.height;
this.projection_type = data.projectionType;
this.stereo_layout = data.stereoLayout?.replace('STEREO_LAYOUT_', '');
this.init_range = data.initRange ? {
start: parseInt(data.initRange.start),
end: parseInt(data.initRange.end)
} : undefined;
if (Reflect.has(data, 'width') && Reflect.has(data, 'height')) {
this.width = parseInt(data.width);
this.height = parseInt(data.height);
}
this.index_range = data.indexRange ? {
start: parseInt(data.indexRange.start),
end: parseInt(data.indexRange.end)
} : undefined;
if (Reflect.has(data, 'projectionType'))
this.projection_type = data.projectionType;
if (Reflect.has(data, 'stereoLayout'))
this.stereo_layout = data.stereoLayout?.replace('STEREO_LAYOUT_', '');
if (Reflect.has(data, 'initRange'))
this.init_range = {
start: parseInt(data.initRange.start),
end: parseInt(data.initRange.end)
};
if (Reflect.has(data, 'indexRange'))
this.index_range = {
start: parseInt(data.indexRange.start),
end: parseInt(data.indexRange.end)
};
this.last_modified = new Date(Math.floor(parseInt(data.lastModified) / 1000));
this.content_length = parseInt(data.contentLength);
this.quality = data.quality;
this.quality_label = data.qualityLabel;
this.fps = data.fps;
this.url = data.url;
this.cipher = data.cipher;
this.signature_cipher = data.signatureCipher;
this.audio_quality = data.audioQuality;
if (Reflect.has(data, 'contentLength'))
this.content_length = parseInt(data.contentLength);
if (Reflect.has(data, 'quality'))
this.quality = data.quality;
if (Reflect.has(data, 'qualityLabel'))
this.quality_label = data.qualityLabel;
if (Reflect.has(data, 'fps'))
this.fps = data.fps;
if (Reflect.has(data, 'url'))
this.url = data.url;
if (Reflect.has(data, 'cipher'))
this.cipher = data.cipher;
if (Reflect.has(data, 'signatureCipher'))
this.signature_cipher = data.signatureCipher;
if (Reflect.has(data, 'audioQuality'))
this.audio_quality = data.audioQuality;
this.approx_duration_ms = parseInt(data.approxDurationMs);
this.audio_sample_rate = parseInt(data.audioSampleRate);
this.audio_channels = data.audioChannels;
this.loudness_db = data.loudnessDb;
this.spatial_audio_type = data.spatialAudioType?.replace('SPATIAL_AUDIO_TYPE_', '');
this.max_dvr_duration_sec = data.maxDvrDurationSec;
this.target_duration_dec = data.targetDurationSec;
if (Reflect.has(data, 'audioSampleRate'))
this.audio_sample_rate = parseInt(data.audioSampleRate);
if (Reflect.has(data, 'audioChannels'))
this.audio_channels = data.audioChannels;
if (Reflect.has(data, 'loudnessDb'))
this.loudness_db = data.loudnessDb;
if (Reflect.has(data, 'spatialAudioType'))
this.spatial_audio_type = data.spatialAudioType?.replace('SPATIAL_AUDIO_TYPE_', '');
if (Reflect.has(data, 'maxDvrDurationSec'))
this.max_dvr_duration_sec = data.maxDvrDurationSec;
if (Reflect.has(data, 'targetDurationSec'))
this.target_duration_dec = data.targetDurationSec;
this.has_audio = !!data.audioBitrate || !!data.audioQuality;
this.has_video = !!data.qualityLabel;
this.has_text = !!data.captionTrack;
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 (Reflect.has(data, 'xtags'))
this.xtags = data.xtags;
if (Reflect.has(data, 'audioTrack')) {
if (Reflect.has(data, 'fairPlayKeyUri'))
this.fair_play_key_uri = data.fairPlayKeyUri;
if (Reflect.has(data, 'drmFamilies'))
this.drm_families = data.drmFamilies;
if (Reflect.has(data, 'drmTrackType'))
this.drm_track_type = data.drmTrackType;
if (Reflect.has(data, 'distinctParams'))
this.distinct_params = data.distinctParams;
if (Reflect.has(data, 'trackAbsoluteLoudnessLkfs'))
this.track_absolute_loudness_lkfs = data.trackAbsoluteLoudnessLkfs;
if (Reflect.has(data, 'highReplication'))
this.high_replication = data.highReplication;
if (Reflect.has(data, 'colorInfo'))
this.color_info = {
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_', '')
};
if (Reflect.has(data, 'audioTrack'))
this.audio_track = {
audio_is_default: data.audioTrack.audioIsDefault,
display_name: data.audioTrack.displayName,
id: data.audioTrack.id
};
}
if (Reflect.has(data, 'captionTrack')) {
if (Reflect.has(data, 'captionTrack'))
this.caption_track = {
display_name: data.captionTrack.displayName,
vss_id: data.captionTrack.vssId,
@@ -136,7 +198,6 @@ export default class Format {
kind: data.captionTrack.kind,
id: data.captionTrack.id
};
}
if (this.has_audio || this.has_text) {
const args = new URLSearchParams(this.cipher || this.signature_cipher);

View File

@@ -261,6 +261,7 @@ export { default as MusicLargeCardItemCarousel } from './classes/MusicLargeCardI
export { default as MusicMultiRowListItem } from './classes/MusicMultiRowListItem.ts';
export { default as MusicNavigationButton } from './classes/MusicNavigationButton.ts';
export { default as MusicPlayButton } from './classes/MusicPlayButton.ts';
export { default as MusicPlaylistEditHeader } from './classes/MusicPlaylistEditHeader.ts';
export { default as MusicPlaylistShelf } from './classes/MusicPlaylistShelf.ts';
export { default as MusicQueue } from './classes/MusicQueue.ts';
export { default as MusicResponsiveHeader } from './classes/MusicResponsiveHeader.ts';

View File

@@ -26,6 +26,7 @@ import Format from './classes/misc/Format.ts';
import VideoDetails from './classes/misc/VideoDetails.ts';
import NavigationEndpoint from './classes/NavigationEndpoint.ts';
import CommentView from './classes/comments/CommentView.ts';
import MusicThumbnail from './classes/MusicThumbnail.ts';
import type { KeyInfo } from './generator.ts';
import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.ts';
@@ -367,6 +368,11 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
parsed_data.player_overlays = player_overlays;
}
const background = parseItem(data.background, MusicThumbnail);
if (background) {
parsed_data.background = background;
}
const playback_tracking = data.playbackTracking ? {
videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl,
videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl

View File

@@ -19,9 +19,10 @@ import type AlertWithButton from '../classes/AlertWithButton.ts';
import type NavigationEndpoint from '../classes/NavigationEndpoint.ts';
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.ts';
import type EngagementPanelSectionList from '../classes/EngagementPanelSectionList.ts';
import type { AppendContinuationItemsAction } from '../nodes.ts';
import type { AppendContinuationItemsAction, MusicThumbnail } from '../nodes.ts';
export interface IParsedResponse {
background?: MusicThumbnail;
actions?: SuperParsedResult<YTNode>;
actions_memo?: Memo;
contents?: SuperParsedResult<YTNode>;
@@ -134,6 +135,7 @@ export interface INextResponse {
}
export interface IBrowseResponse {
background?: MusicThumbnail;
continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation |
MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation;
continuation_contents_memo?: Memo;

View File

@@ -20,6 +20,7 @@ export interface IRawPlayerConfig {
}
export interface IRawResponse {
background?: RawNode;
contents?: RawData;
onResponseReceivedActions?: RawNode[];
onResponseReceivedEndpoints?: RawNode[];

View File

@@ -3,33 +3,35 @@ import { Parser } from '../index.ts';
import MicroformatData from '../classes/MicroformatData.ts';
import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts';
import MusicDetailHeader from '../classes/MusicDetailHeader.ts';
import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.ts';
import MusicShelf from '../classes/MusicShelf.ts';
import type MusicThumbnail from '../classes/MusicThumbnail.ts';
import type { ApiResponse } from '../../core/index.ts';
import type { ObservedArray } from '../helpers.ts';
import { observe, type ObservedArray } from '../helpers.ts';
import type { IBrowseResponse } from '../types/index.ts';
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts';
class Album {
#page: IBrowseResponse;
header?: MusicDetailHeader;
header?: MusicDetailHeader | MusicResponsiveHeader;
contents: ObservedArray<MusicResponsiveListItem>;
sections: ObservedArray<MusicCarouselShelf>;
url: string | null;
background?: MusicThumbnail;
url?: string;
constructor(response: ApiResponse) {
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
this.header = this.#page.header?.item().as(MusicDetailHeader);
this.url = this.#page.microformat?.as(MicroformatData).url_canonical || null;
if (!this.#page.contents_memo)
throw new Error('No contents found in the response');
this.contents = this.#page.contents_memo.getType(MusicShelf)?.first().contents;
this.sections = this.#page.contents_memo.getType(MusicCarouselShelf) || [];
this.header = this.#page.contents_memo.getType(MusicDetailHeader, MusicResponsiveHeader)?.first();
this.contents = this.#page.contents_memo.getType(MusicShelf)?.first().contents || observe([]);
this.sections = this.#page.contents_memo.getType(MusicCarouselShelf) || observe([]);
this.background = this.#page.background;
this.url = this.#page.microformat?.as(MicroformatData).url_canonical;
}
get page(): IBrowseResponse {

View File

@@ -6,22 +6,25 @@ import MusicEditablePlaylistDetailHeader from '../classes/MusicEditablePlaylistD
import MusicPlaylistShelf from '../classes/MusicPlaylistShelf.ts';
import MusicShelf from '../classes/MusicShelf.ts';
import SectionList from '../classes/SectionList.ts';
import MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts';
import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.ts';
import { InnertubeError } from '../../utils/Utils.ts';
import type { ObservedArray, YTNode } from '../helpers.ts';
import { observe, type ObservedArray } from '../helpers.ts';
import type { ApiResponse, Actions } from '../../core/index.ts';
import type { IBrowseResponse } from '../types/index.ts';
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts';
import type MusicThumbnail from '../classes/MusicThumbnail.ts';
class Playlist {
#page: IBrowseResponse;
#actions: Actions;
#continuation: string | null;
#last_fetched_suggestions: any;
#suggestions_continuation: any;
#last_fetched_suggestions: ObservedArray<MusicResponsiveListItem> | null;
#suggestions_continuation: string | null;
header?: MusicDetailHeader;
items?: ObservedArray<YTNode> | null;
header?: MusicResponsiveHeader | MusicDetailHeader | MusicEditablePlaylistDetailHeader;
contents?: ObservedArray<MusicResponsiveListItem>;
background?: MusicThumbnail;
constructor(response: ApiResponse, actions: Actions) {
this.#actions = actions;
@@ -32,16 +35,17 @@ class Playlist {
if (this.#page.continuation_contents) {
const data = this.#page.continuation_contents?.as(MusicPlaylistShelfContinuation);
this.items = data.contents;
if (!data.contents)
throw new InnertubeError('No contents found in the response');
this.contents = data.contents.as(MusicResponsiveListItem);
this.#continuation = data.continuation;
} else {
if (this.#page.header?.item().type === 'MusicEditablePlaylistDetailHeader') {
this.header = this.#page.header?.item().as(MusicEditablePlaylistDetailHeader).header?.as(MusicDetailHeader);
} else {
this.header = this.#page.header?.item().as(MusicDetailHeader);
}
this.items = this.#page.contents_memo?.getType(MusicPlaylistShelf).first().contents || null;
this.#continuation = this.#page.contents_memo?.getType(MusicPlaylistShelf).first().continuation || null;
if (!this.#page.contents_memo)
throw new InnertubeError('No contents found in the response');
this.header = this.#page.contents_memo.getType(MusicResponsiveHeader, MusicEditablePlaylistDetailHeader, MusicDetailHeader)?.first();
this.contents = this.#page.contents_memo.getType(MusicPlaylistShelf)?.first()?.contents || observe([]);
this.background = this.#page.background;
this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.first()?.continuation || null;
}
}
@@ -64,7 +68,12 @@ class Playlist {
* Retrieves related playlists
*/
async getRelated(): Promise<MusicCarouselShelf> {
let section_continuation = this.#page.contents_memo?.getType(SectionList)?.[0].continuation;
const target_section_list = this.#page.contents_memo?.getType(SectionList).find((section_list) => section_list.continuation);
if (!target_section_list)
throw new InnertubeError('Could not find "Related" section.');
let section_continuation = target_section_list.continuation;
while (section_continuation) {
const data = await this.#actions.execute('/browse', {
@@ -76,7 +85,7 @@ class Playlist {
const section_list = data.continuation_contents?.as(SectionListContinuation);
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
const related = sections?.matchCondition((section) => section.is(MusicCarouselShelf))?.as(MusicCarouselShelf);
const related = sections?.find((section) => section.is(MusicCarouselShelf))?.as(MusicCarouselShelf);
if (related)
return related;
@@ -84,10 +93,10 @@ class Playlist {
section_continuation = section_list?.continuation;
}
throw new InnertubeError('Target section not found.');
throw new InnertubeError('Could not fetch related playlists.');
}
async getSuggestions(refresh = true) {
async getSuggestions(refresh = true): Promise<ObservedArray<MusicResponsiveListItem>> {
const require_fetch = refresh || !this.#last_fetched_suggestions;
const fetch_promise = require_fetch ? this.#fetchSuggestions() : Promise.resolve(null);
const fetch_result = await fetch_promise;
@@ -97,11 +106,12 @@ class Playlist {
this.#suggestions_continuation = fetch_result.continuation;
}
return fetch_result?.items || this.#last_fetched_suggestions;
return fetch_result?.items || this.#last_fetched_suggestions || observe([]);
}
async #fetchSuggestions(): Promise<{ items: never[] | ObservedArray<MusicResponsiveListItem>, continuation: string | null }> {
const continuation = this.#suggestions_continuation || this.#page.contents_memo?.get('SectionList')?.[0].as(SectionList).continuation;
async #fetchSuggestions(): Promise<{ items: ObservedArray<MusicResponsiveListItem>, continuation: string | null }> {
const target_section_list = this.#page.contents_memo?.getType(SectionList).find((section_list) => section_list.continuation);
const continuation = this.#suggestions_continuation || target_section_list?.continuation;
if (continuation) {
const page = await this.#actions.execute('/browse', {
@@ -113,16 +123,16 @@ class Playlist {
const section_list = page.continuation_contents?.as(SectionListContinuation);
const sections = section_list?.contents?.as(MusicCarouselShelf, MusicShelf);
const suggestions = sections?.matchCondition((section) => section.is(MusicShelf))?.as(MusicShelf);
const suggestions = sections?.find((section) => section.is(MusicShelf))?.as(MusicShelf);
return {
items: suggestions?.contents || [],
items: suggestions?.contents || observe([]),
continuation: suggestions?.continuation || null
};
}
return {
items: [],
items: observe([]),
continuation: null
};
}
@@ -131,6 +141,10 @@ class Playlist {
return this.#page;
}
get items(): ObservedArray<MusicResponsiveListItem> {
return this.contents || observe([]);
}
get has_continuation(): boolean {
return !!this.#continuation;
}

View File

@@ -7,13 +7,13 @@ const TAG = 'JsRuntime';
export default function evaluate(code: string, env: Record<string, VMPrimative>) {
Log.debug(TAG, 'Evaluating JavaScript:\n', code);
const runtime = new Jinter(code);
const runtime = new Jinter();
for (const [ key, value ] of Object.entries(env)) {
runtime.scope.set(key, value);
}
const result = runtime.interpret();
const result = runtime.evaluate(code);
Log.debug(TAG, 'Done. Result:', result);

View File

@@ -18,9 +18,17 @@ import {
export declare namespace $.youtube.InnertubePayload.Context {
export type Client = {
unkparam: number;
deviceMake: string;
deviceModel: string;
nameId: number;
clientVersion: string;
clientName: string;
osName: string;
osVersion: string;
acceptLanguage: string;
acceptRegion: string;
androidSdkVersion: number;
windowWidthPoints: number;
windowHeightPoints: number;
}
}
@@ -28,9 +36,17 @@ export type Type = $.youtube.InnertubePayload.Context.Client;
export function getDefaultValue(): $.youtube.InnertubePayload.Context.Client {
return {
unkparam: 0,
deviceMake: "",
deviceModel: "",
nameId: 0,
clientVersion: "",
clientName: "",
osName: "",
osVersion: "",
acceptLanguage: "",
acceptRegion: "",
androidSdkVersion: 0,
windowWidthPoints: 0,
windowHeightPoints: 0,
};
}
@@ -43,24 +59,52 @@ export function createValue(partialValue: Partial<$.youtube.InnertubePayload.Con
export function encodeJson(value: $.youtube.InnertubePayload.Context.Client): unknown {
const result: any = {};
if (value.unkparam !== undefined) result.unkparam = tsValueToJsonValueFns.int32(value.unkparam);
if (value.deviceMake !== undefined) result.deviceMake = tsValueToJsonValueFns.string(value.deviceMake);
if (value.deviceModel !== undefined) result.deviceModel = tsValueToJsonValueFns.string(value.deviceModel);
if (value.nameId !== undefined) result.nameId = tsValueToJsonValueFns.int32(value.nameId);
if (value.clientVersion !== undefined) result.clientVersion = tsValueToJsonValueFns.string(value.clientVersion);
if (value.clientName !== undefined) result.clientName = tsValueToJsonValueFns.string(value.clientName);
if (value.osName !== undefined) result.osName = tsValueToJsonValueFns.string(value.osName);
if (value.osVersion !== undefined) result.osVersion = tsValueToJsonValueFns.string(value.osVersion);
if (value.acceptLanguage !== undefined) result.acceptLanguage = tsValueToJsonValueFns.string(value.acceptLanguage);
if (value.acceptRegion !== undefined) result.acceptRegion = tsValueToJsonValueFns.string(value.acceptRegion);
if (value.androidSdkVersion !== undefined) result.androidSdkVersion = tsValueToJsonValueFns.int32(value.androidSdkVersion);
if (value.windowWidthPoints !== undefined) result.windowWidthPoints = tsValueToJsonValueFns.int32(value.windowWidthPoints);
if (value.windowHeightPoints !== undefined) result.windowHeightPoints = tsValueToJsonValueFns.int32(value.windowHeightPoints);
return result;
}
export function decodeJson(value: any): $.youtube.InnertubePayload.Context.Client {
const result = getDefaultValue();
if (value.unkparam !== undefined) result.unkparam = jsonValueToTsValueFns.int32(value.unkparam);
if (value.deviceMake !== undefined) result.deviceMake = jsonValueToTsValueFns.string(value.deviceMake);
if (value.deviceModel !== undefined) result.deviceModel = jsonValueToTsValueFns.string(value.deviceModel);
if (value.nameId !== undefined) result.nameId = jsonValueToTsValueFns.int32(value.nameId);
if (value.clientVersion !== undefined) result.clientVersion = jsonValueToTsValueFns.string(value.clientVersion);
if (value.clientName !== undefined) result.clientName = jsonValueToTsValueFns.string(value.clientName);
if (value.osName !== undefined) result.osName = jsonValueToTsValueFns.string(value.osName);
if (value.osVersion !== undefined) result.osVersion = jsonValueToTsValueFns.string(value.osVersion);
if (value.acceptLanguage !== undefined) result.acceptLanguage = jsonValueToTsValueFns.string(value.acceptLanguage);
if (value.acceptRegion !== undefined) result.acceptRegion = jsonValueToTsValueFns.string(value.acceptRegion);
if (value.androidSdkVersion !== undefined) result.androidSdkVersion = jsonValueToTsValueFns.int32(value.androidSdkVersion);
if (value.windowWidthPoints !== undefined) result.windowWidthPoints = jsonValueToTsValueFns.int32(value.windowWidthPoints);
if (value.windowHeightPoints !== undefined) result.windowHeightPoints = jsonValueToTsValueFns.int32(value.windowHeightPoints);
return result;
}
export function encodeBinary(value: $.youtube.InnertubePayload.Context.Client): Uint8Array {
const result: WireMessage = [];
if (value.unkparam !== undefined) {
const tsValue = value.unkparam;
if (value.deviceMake !== undefined) {
const tsValue = value.deviceMake;
result.push(
[12, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.deviceModel !== undefined) {
const tsValue = value.deviceModel;
result.push(
[13, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.nameId !== undefined) {
const tsValue = value.nameId;
result.push(
[16, tsValueToWireValueFns.int32(tsValue)],
);
@@ -71,12 +115,48 @@ export function encodeBinary(value: $.youtube.InnertubePayload.Context.Client):
[17, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.clientName !== undefined) {
const tsValue = value.clientName;
if (value.osName !== undefined) {
const tsValue = value.osName;
result.push(
[18, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.osVersion !== undefined) {
const tsValue = value.osVersion;
result.push(
[19, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.acceptLanguage !== undefined) {
const tsValue = value.acceptLanguage;
result.push(
[21, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.acceptRegion !== undefined) {
const tsValue = value.acceptRegion;
result.push(
[22, tsValueToWireValueFns.string(tsValue)],
);
}
if (value.androidSdkVersion !== undefined) {
const tsValue = value.androidSdkVersion;
result.push(
[34, tsValueToWireValueFns.int32(tsValue)],
);
}
if (value.windowWidthPoints !== undefined) {
const tsValue = value.windowWidthPoints;
result.push(
[37, tsValueToWireValueFns.int32(tsValue)],
);
}
if (value.windowHeightPoints !== undefined) {
const tsValue = value.windowHeightPoints;
result.push(
[38, tsValueToWireValueFns.int32(tsValue)],
);
}
return serialize(result);
}
@@ -84,12 +164,26 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload.Con
const result = getDefaultValue();
const wireMessage = deserialize(binary);
const wireFields = new Map(wireMessage);
field: {
const wireValue = wireFields.get(12);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.deviceMake = value;
}
field: {
const wireValue = wireFields.get(13);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.deviceModel = value;
}
field: {
const wireValue = wireFields.get(16);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.unkparam = value;
result.nameId = value;
}
field: {
const wireValue = wireFields.get(17);
@@ -103,7 +197,49 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload.Con
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.clientName = value;
result.osName = value;
}
field: {
const wireValue = wireFields.get(19);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.osVersion = value;
}
field: {
const wireValue = wireFields.get(21);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.acceptLanguage = value;
}
field: {
const wireValue = wireFields.get(22);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.acceptRegion = value;
}
field: {
const wireValue = wireFields.get(34);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.androidSdkVersion = value;
}
field: {
const wireValue = wireFields.get(37);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.windowWidthPoints = value;
}
field: {
const wireValue = wireFields.get(38);
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.int32(wireValue);
if (value === undefined) break field;
result.windowHeightPoints = value;
}
return result;
}

View File

@@ -90,7 +90,7 @@ import {
export declare namespace $.youtube {
export type InnertubePayload = {
context?: Context;
target?: string;
videoId?: string;
title?: Title;
description?: Description;
tags?: Tags;
@@ -108,7 +108,7 @@ export type Type = $.youtube.InnertubePayload;
export function getDefaultValue(): $.youtube.InnertubePayload {
return {
context: undefined,
target: undefined,
videoId: undefined,
title: undefined,
description: undefined,
tags: undefined,
@@ -131,7 +131,7 @@ export function createValue(partialValue: Partial<$.youtube.InnertubePayload>):
export function encodeJson(value: $.youtube.InnertubePayload): unknown {
const result: any = {};
if (value.context !== undefined) result.context = encodeJson_1(value.context);
if (value.target !== undefined) result.target = tsValueToJsonValueFns.string(value.target);
if (value.videoId !== undefined) result.videoId = tsValueToJsonValueFns.string(value.videoId);
if (value.title !== undefined) result.title = encodeJson_2(value.title);
if (value.description !== undefined) result.description = encodeJson_3(value.description);
if (value.tags !== undefined) result.tags = encodeJson_4(value.tags);
@@ -147,7 +147,7 @@ export function encodeJson(value: $.youtube.InnertubePayload): unknown {
export function decodeJson(value: any): $.youtube.InnertubePayload {
const result = getDefaultValue();
if (value.context !== undefined) result.context = decodeJson_1(value.context);
if (value.target !== undefined) result.target = jsonValueToTsValueFns.string(value.target);
if (value.videoId !== undefined) result.videoId = jsonValueToTsValueFns.string(value.videoId);
if (value.title !== undefined) result.title = decodeJson_2(value.title);
if (value.description !== undefined) result.description = decodeJson_3(value.description);
if (value.tags !== undefined) result.tags = decodeJson_4(value.tags);
@@ -168,8 +168,8 @@ export function encodeBinary(value: $.youtube.InnertubePayload): Uint8Array {
[1, { type: WireType.LengthDelimited as const, value: encodeBinary_1(tsValue) }],
);
}
if (value.target !== undefined) {
const tsValue = value.target;
if (value.videoId !== undefined) {
const tsValue = value.videoId;
result.push(
[2, tsValueToWireValueFns.string(tsValue)],
);
@@ -247,7 +247,7 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload {
if (wireValue === undefined) break field;
const value = wireValueToTsValueFns.string(wireValue);
if (value === undefined) break field;
result.target = value;
result.videoId = value;
}
field: {
const wireValue = wireFields.get(3);

View File

@@ -240,12 +240,20 @@ export function encodeVideoMetadataPayload(video_id: string, metadata: UpdateVid
const data: InnertubePayload.Type = {
context: {
client: {
unkparam: 14,
clientName: CLIENTS.ANDROID.NAME,
clientVersion: CLIENTS.YTSTUDIO_ANDROID.VERSION
nameId: 3,
osName: 'Android',
androidSdkVersion: CLIENTS.ANDROID.SDK_VERSION,
osVersion: '13',
acceptLanguage: 'en-US',
acceptRegion: 'US',
deviceMake: 'Google',
deviceModel: 'sdk_gphone64_x86_64',
windowHeightPoints: 840,
windowWidthPoints: 432,
clientVersion: CLIENTS.ANDROID.VERSION
}
},
target: video_id
videoId: video_id
};
if (Reflect.has(metadata, 'title'))
@@ -302,12 +310,20 @@ export function encodeCustomThumbnailPayload(video_id: string, bytes: Uint8Array
const data: InnertubePayload.Type = {
context: {
client: {
unkparam: 14,
clientName: CLIENTS.ANDROID.NAME,
clientVersion: CLIENTS.YTSTUDIO_ANDROID.VERSION
nameId: 3,
osName: 'Android',
androidSdkVersion: CLIENTS.ANDROID.SDK_VERSION,
osVersion: '13',
acceptLanguage: 'en-US',
acceptRegion: 'US',
deviceMake: 'Google',
deviceModel: 'sdk_gphone64_x86_64',
windowHeightPoints: 840,
windowWidthPoints: 432,
clientVersion: CLIENTS.ANDROID.VERSION
}
},
target: video_id,
videoId: video_id,
videoThumbnail: {
type: 3,
thumbnail: {

View File

@@ -11,17 +11,24 @@ message VisitorData {
message InnertubePayload {
message Context {
message Client {
required int32 unkparam = 16;
required string client_version = 17;
required string client_name = 18;
string deviceMake = 12;
string deviceModel = 13;
int32 nameId = 16;
string clientVersion = 17;
string osName = 18;
string osVersion = 19;
string acceptLanguage = 21;
string acceptRegion = 22;
int32 windowWidthPoints = 37;
int32 windowHeightPoints = 38;
int32 androidSdkVersion = 34;
}
required Client client = 1;
}
required Context context = 1;
// This can be either a target id or a video id.
optional string target = 2;
optional string videoId = 2;
/**** YT Sudio stuff ****/

View File

@@ -1,4 +1,13 @@
export interface StreamingInfoOptions {
/**
* The format to use for the captions, when the video has captions.
* If this option is not set, the DASH manifest will not include the captions.
*
* Possible values:
* * `vtt`: Tells YouTube to return the captions in the WebVTT format
* * `ttml`: Tells YouTube to return the captions in the TTML format
*/
captions_format?: 'vtt' | 'ttml';
/**
* The label to use for the non-DRC streams when a video has DRC and streams.
*

View File

@@ -40,14 +40,16 @@ async function DashManifest({
player,
actions,
storyboards,
captionTracks,
options
}) {
const {
getDuration,
audio_sets,
video_sets,
image_sets
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards, options);
image_sets,
text_sets
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards, captionTracks, options);
return /* @__PURE__ */ DashUtils.createElement("mpd", {
xmlns: "urn:mpeg:dash:schema:mpd:2011",
minBufferTime: "PT1.500S",
@@ -128,10 +130,25 @@ async function DashManifest({
duration: rep.template_duration,
startNumber: "0"
}))));
}), text_sets.map((set, index) => {
return /* @__PURE__ */ DashUtils.createElement("adaptation-set", {
id: index + audio_sets.length + video_sets.length + image_sets.length,
mimeType: set.mime_type,
lang: set.language,
contentType: "text"
}, set.track_roles.map((role) => /* @__PURE__ */ DashUtils.createElement("role", {
schemeIdUri: "urn:mpeg:dash:role:2011",
value: role
})), /* @__PURE__ */ DashUtils.createElement("label", {
id: index + audio_sets.length
}, set.track_name), /* @__PURE__ */ DashUtils.createElement("representation", {
id: set.representation.uid,
bandwidth: "0"
}, /* @__PURE__ */ DashUtils.createElement("base-url", null, set.representation.base_url)));
})));
}
__name(DashManifest, "DashManifest");
function toDash(streaming_data, is_post_live_dvr = false, url_transformer = (url) => url, format_filter, cpn, player, actions, storyboards, options) {
function toDash(streaming_data, is_post_live_dvr = false, url_transformer = (url) => url, format_filter, cpn, player, actions, storyboards, caption_tracks, options) {
if (!streaming_data)
throw new InnertubeError("Streaming data not available");
return DashUtils.renderToString(
@@ -144,7 +161,8 @@ function toDash(streaming_data, is_post_live_dvr = false, url_transformer = (url
cpn,
player,
actions,
storyboards
storyboards,
captionTracks: caption_tracks
})
);
}

View File

@@ -13,6 +13,7 @@ import type { SegmentInfo as FSegmentInfo } from './StreamingInfo.ts';
import type { FormatFilter, URLTransformer } from '../types/FormatUtils.ts';
import type PlayerLiveStoryboardSpec from '../parser/classes/PlayerLiveStoryboardSpec.ts';
import type { StreamingInfoOptions } from '../types/StreamingInfoOptions.ts';
import type { CaptionTrackData } from '../parser/classes/PlayerCaptionsTracklist.ts';
interface DashManifestProps {
streamingData: IStreamingData;
@@ -24,6 +25,7 @@ interface DashManifestProps {
player?: Player;
actions?: Actions;
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
captionTracks?: CaptionTrackData[];
}
async function OTFPostLiveDvrSegmentInfo({ info }: { info: FSegmentInfo }) {
@@ -73,14 +75,16 @@ async function DashManifest({
player,
actions,
storyboards,
captionTracks,
options
}: DashManifestProps) {
const {
getDuration,
audio_sets,
video_sets,
image_sets
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards, options);
image_sets,
text_sets
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards, captionTracks, options);
// XXX: DASH spec: https://standards.iso.org/ittf/PubliclyAvailableStandards/c083314_ISO_IEC%2023009-1_2022(en).zip
@@ -229,6 +233,36 @@ async function DashManifest({
</adaptation-set>;
})
}
{
text_sets.map((set, index) => {
return <adaptation-set
id={index + audio_sets.length + video_sets.length + image_sets.length}
mimeType={set.mime_type}
lang={set.language}
contentType="text"
>
{
set.track_roles.map((role) => (
<role
schemeIdUri="urn:mpeg:dash:role:2011"
value={role}
/>
))
}
<label id={index + audio_sets.length}>
{set.track_name}
</label>
<representation
id={set.representation.uid}
bandwidth="0"
>
<base-url>
{set.representation.base_url}
</base-url>
</representation>
</adaptation-set>;
})
}
</period>
</mpd>;
}
@@ -242,6 +276,7 @@ export function toDash(
player?: Player,
actions?: Actions,
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec,
caption_tracks?: CaptionTrackData[],
options?: StreamingInfoOptions
) {
if (!streaming_data)
@@ -258,6 +293,7 @@ export function toDash(
player={player}
actions={actions}
storyboards={storyboards}
captionTracks={caption_tracks}
/>
);
}

View File

@@ -90,6 +90,10 @@ export async function download(
signal: cancel.signal
});
// Throw if the response is not 2xx
if (!response.ok)
throw new InnertubeError('The server responded with a non 2xx status code', { error_type: 'FETCH_FAILED', response });
const body = response.body;
if (!body)
@@ -155,7 +159,7 @@ export function chooseFormat(options: FormatOptions, streaming_data?: IStreaming
return false;
if (!is_best && format.quality_label !== quality)
return false;
if (best_width < format.width)
if (format.width && (best_width < format.width))
best_width = format.width;
return true;
});

View File

@@ -12,6 +12,7 @@ import type { Format } from '../parser/misc.ts';
import type { PlayerLiveStoryboardSpec } from '../parser/nodes.ts';
import type { FormatFilter, URLTransformer } from '../types/FormatUtils.ts';
import type { StreamingInfoOptions } from '../types/StreamingInfoOptions.ts';
import type { CaptionTrackData } from '../parser/classes/PlayerCaptionsTracklist.ts';
const TAG_ = 'StreamingInfo';
@@ -20,6 +21,7 @@ export interface StreamingInfo {
audio_sets: AudioSet[];
video_sets: VideoSet[];
image_sets: ImageSet[];
text_sets: TextSet[];
}
export interface AudioSet {
@@ -85,8 +87,8 @@ export interface VideoSet {
export interface VideoRepresentation {
uid: string;
bitrate: number;
width: number;
height: number;
width?: number;
height?: number;
fps?: number;
codecs?: string;
segment_info: SegmentInfo;
@@ -122,6 +124,19 @@ export interface ImageRepresentation {
getURL(n: number): string;
}
export interface TextSet {
mime_type: string;
language: string;
track_name: string;
track_roles: ('caption' | 'dub')[];
representation: TextRepresentation;
}
export interface TextRepresentation {
uid: string;
base_url: string;
}
interface PostLiveDvrInfo {
duration: number,
segment_count: number
@@ -706,7 +721,7 @@ function getImageRepresentation(
thumbnail_width: board.thumbnail_width,
rows: board.rows,
columns: board.columns,
template_duration: template_duration,
template_duration: Math.round(template_duration),
template_url: transform_url(template_url).toString(),
getURL(n) {
return template_url.toString().replace('$Number$', n.toString());
@@ -735,6 +750,36 @@ function getImageSets(
}));
}
function getTextSets(
caption_tracks: CaptionTrackData[],
format: 'vtt' | 'ttml',
transform_url: URLTransformer
): TextSet[] {
const mime_type = format === 'vtt' ? 'text/vtt' : 'application/ttml+xml';
return caption_tracks.map((caption_track) => {
const url = new URL(caption_track.base_url);
url.searchParams.set('fmt', format);
const track_roles: ('caption' | 'dub')[] = [ 'caption' ];
if (url.searchParams.has('tlang')) {
track_roles.push('dub');
}
return {
mime_type,
language: caption_track.language_code,
track_name: caption_track.name.toString(),
track_roles,
representation: {
uid: `text-${caption_track.vss_id}`,
base_url: transform_url(url).toString()
}
};
});
}
export function getStreamingInfo(
streaming_data?: IStreamingData,
is_post_live_dvr = false,
@@ -744,6 +789,7 @@ export function getStreamingInfo(
player?: Player,
actions?: Actions,
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec,
caption_tracks?: CaptionTrackData[],
options?: StreamingInfoOptions
) {
if (!streaming_data)
@@ -839,11 +885,21 @@ export function getStreamingInfo(
image_sets = getImageSets(duration, actions, storyboards, url_transformer);
}
let text_sets: TextSet[] = [];
if (caption_tracks && options?.captions_format) {
if ((options.captions_format as string) !== 'vtt' && (options.captions_format as string) !== 'ttml') {
throw new InnertubeError('Invalid captions format', options.captions_format);
}
text_sets = getTextSets(caption_tracks, options.captions_format, url_transformer);
}
const info : StreamingInfo = {
getDuration,
audio_sets,
video_sets,
image_sets
image_sets,
text_sets
};
return info;