mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d03469e64 | ||
|
|
62ac2f6f32 | ||
|
|
142a7d0428 | ||
|
|
efa7205723 | ||
|
|
84f90aaf29 | ||
|
|
858cdd197c | ||
|
|
5a8fd3ad37 | ||
|
|
a19511de24 | ||
|
|
bd9f6ac64c | ||
|
|
e5aab9a9b3 | ||
|
|
d6fa134c3d | ||
|
|
fe953072a2 | ||
|
|
055fa33403 | ||
|
|
14c3a06d40 | ||
|
|
67376afae6 | ||
|
|
4cbaa7983f |
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## [10.1.0](https://github.com/LuanRT/YouTube.js/compare/v10.0.0...v10.1.0) (2024-07-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Session:** Add `configInfo` to InnerTube context ([5a8fd3a](https://github.com/LuanRT/YouTube.js/commit/5a8fd3ad37bce1decad28ec3727453ddd430a561))
|
||||
* **toDash:** Add option to include WebVTT or TTML captions ([#673](https://github.com/LuanRT/YouTube.js/issues/673)) ([bd9f6ac](https://github.com/LuanRT/YouTube.js/commit/bd9f6ac64ca9ba96e856aabe5fcc175fd9c294dc))
|
||||
* **toDash:** Add the "dub" role to translated captions ([#677](https://github.com/LuanRT/YouTube.js/issues/677)) ([858cdd1](https://github.com/LuanRT/YouTube.js/commit/858cdd197cb2bb1e1d7a7285966cb56043ad8961))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **FormatUtils:** Throw an error if download requests fails ([a19511d](https://github.com/LuanRT/YouTube.js/commit/a19511de24bb82007aab072844efe64bbb8698da))
|
||||
* **InfoPanelContent:** Update InfoPanelContent node to also support `paragraphs` ([4cbaa79](https://github.com/LuanRT/YouTube.js/commit/4cbaa7983f35a82b9907197769672ac3b300bfbe))
|
||||
* **Player:** Fix extracting the n-token decipher algorithm ([#682](https://github.com/LuanRT/YouTube.js/issues/682)) ([142a7d0](https://github.com/LuanRT/YouTube.js/commit/142a7d042885188605bdc0655d3733502d1e20fa))
|
||||
* **proto:** Update `Context` message ([62ac2f6](https://github.com/LuanRT/YouTube.js/commit/62ac2f6f32d35fec3c31b5f5d556bd4569fa49f9)), closes [#681](https://github.com/LuanRT/YouTube.js/issues/681)
|
||||
* **Session:** Round UTC offset minutes ([84f90aa](https://github.com/LuanRT/YouTube.js/commit/84f90aaf2908ecacb9dfb6ce5497c4c4d14a72c3))
|
||||
* **toDash:** Fix image representations not being spec compliant ([#672](https://github.com/LuanRT/YouTube.js/issues/672)) ([e5aab9a](https://github.com/LuanRT/YouTube.js/commit/e5aab9a9b35f0752cd5ca50bfa25936dce4718c6))
|
||||
* **YTMusic:** Add support for new header layouts ([14c3a06](https://github.com/LuanRT/YouTube.js/commit/14c3a06d402989e98a9d32c79b2dc26f74fb0219))
|
||||
|
||||
## [10.0.0](https://github.com/LuanRT/YouTube.js/compare/v9.4.0...v10.0.0) (2024-06-09)
|
||||
|
||||
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "10.0.0",
|
||||
"version": "10.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "10.0.0",
|
||||
"version": "10.1.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jintr": "^1.1.0",
|
||||
"jintr": "^2.0.0",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
@@ -5829,9 +5829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-1.1.0.tgz",
|
||||
"integrity": "sha512-Tu9wk3BpN2v+kb8yT6YBtue+/nbjeLFv4vvVC4PJ7oCidHKbifWhvORrAbQfxVIQZG+67am/mDagpiGSVtvrZg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-2.0.0.tgz",
|
||||
"integrity": "sha512-RiVlevxttZ4eHEYB2dXKXDXluzHfRuw0DJQGsYuKCc5IvZj5/GbOakeqVX+Bar/G9kTty9xDJREcxukurkmYLA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
28
src/parser/classes/MusicPlaylistEditHeader.ts
Normal file
28
src/parser/classes/MusicPlaylistEditHeader.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Dropdown from './Dropdown.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import MusicPlayButton from './MusicPlayButton.js';
|
||||
import ToggleButton from './ToggleButton.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Button from './Button.js';
|
||||
import DownloadButton from './DownloadButton.js';
|
||||
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,17 +2,19 @@ import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -261,6 +261,7 @@ export { default as MusicLargeCardItemCarousel } from './classes/MusicLargeCardI
|
||||
export { default as MusicMultiRowListItem } from './classes/MusicMultiRowListItem.js';
|
||||
export { default as MusicNavigationButton } from './classes/MusicNavigationButton.js';
|
||||
export { default as MusicPlayButton } from './classes/MusicPlayButton.js';
|
||||
export { default as MusicPlaylistEditHeader } from './classes/MusicPlaylistEditHeader.js';
|
||||
export { default as MusicPlaylistShelf } from './classes/MusicPlaylistShelf.js';
|
||||
export { default as MusicQueue } from './classes/MusicQueue.js';
|
||||
export { default as MusicResponsiveHeader } from './classes/MusicResponsiveHeader.js';
|
||||
|
||||
@@ -26,6 +26,7 @@ import Format from './classes/misc/Format.js';
|
||||
import VideoDetails from './classes/misc/VideoDetails.js';
|
||||
import NavigationEndpoint from './classes/NavigationEndpoint.js';
|
||||
import CommentView from './classes/comments/CommentView.js';
|
||||
import MusicThumbnail from './classes/MusicThumbnail.js';
|
||||
|
||||
import type { KeyInfo } from './generator.js';
|
||||
import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.js';
|
||||
@@ -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
|
||||
|
||||
@@ -19,9 +19,10 @@ import type AlertWithButton from '../classes/AlertWithButton.js';
|
||||
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
|
||||
import type EngagementPanelSectionList from '../classes/EngagementPanelSectionList.js';
|
||||
import type { AppendContinuationItemsAction } from '../nodes.js';
|
||||
import type { AppendContinuationItemsAction, MusicThumbnail } from '../nodes.js';
|
||||
|
||||
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;
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface IRawPlayerConfig {
|
||||
}
|
||||
|
||||
export interface IRawResponse {
|
||||
background?: RawNode;
|
||||
contents?: RawData;
|
||||
onResponseReceivedActions?: RawNode[];
|
||||
onResponseReceivedEndpoints?: RawNode[];
|
||||
|
||||
@@ -3,33 +3,35 @@ import { Parser } from '../index.js';
|
||||
import MicroformatData from '../classes/MicroformatData.js';
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
import MusicDetailHeader from '../classes/MusicDetailHeader.js';
|
||||
import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.js';
|
||||
import MusicShelf from '../classes/MusicShelf.js';
|
||||
import type MusicThumbnail from '../classes/MusicThumbnail.js';
|
||||
|
||||
import type { ApiResponse } from '../../core/index.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { observe, type ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/index.js';
|
||||
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
|
||||
|
||||
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 {
|
||||
|
||||
@@ -6,22 +6,25 @@ import MusicEditablePlaylistDetailHeader from '../classes/MusicEditablePlaylistD
|
||||
import MusicPlaylistShelf from '../classes/MusicPlaylistShelf.js';
|
||||
import MusicShelf from '../classes/MusicShelf.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
|
||||
import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import { observe, type ObservedArray } from '../helpers.js';
|
||||
import type { ApiResponse, Actions } from '../../core/index.js';
|
||||
import type { IBrowseResponse } from '../types/index.js';
|
||||
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
|
||||
import type MusicThumbnail from '../classes/MusicThumbnail.js';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ import {
|
||||
export declare namespace $.youtube {
|
||||
export type InnertubePayload = {
|
||||
context?: Context;
|
||||
target?: string;
|
||||
videoId?: string;
|
||||
title?: Title;
|
||||
description?: Description;
|
||||
tags?: Tags;
|
||||
@@ -100,6 +100,7 @@ export declare namespace $.youtube {
|
||||
privacy?: Privacy;
|
||||
madeForKids?: MadeForKids;
|
||||
ageRestricted?: AgeRestricted;
|
||||
field83?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ export type Type = $.youtube.InnertubePayload;
|
||||
export function getDefaultValue(): $.youtube.InnertubePayload {
|
||||
return {
|
||||
context: undefined,
|
||||
target: undefined,
|
||||
videoId: undefined,
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
tags: undefined,
|
||||
@@ -118,6 +119,7 @@ export function getDefaultValue(): $.youtube.InnertubePayload {
|
||||
privacy: undefined,
|
||||
madeForKids: undefined,
|
||||
ageRestricted: undefined,
|
||||
field83: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,7 +133,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);
|
||||
@@ -141,13 +143,14 @@ export function encodeJson(value: $.youtube.InnertubePayload): unknown {
|
||||
if (value.privacy !== undefined) result.privacy = encodeJson_8(value.privacy);
|
||||
if (value.madeForKids !== undefined) result.madeForKids = encodeJson_9(value.madeForKids);
|
||||
if (value.ageRestricted !== undefined) result.ageRestricted = encodeJson_10(value.ageRestricted);
|
||||
if (value.field83 !== undefined) result.field83 = tsValueToJsonValueFns.int32(value.field83);
|
||||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -157,6 +160,7 @@ export function decodeJson(value: any): $.youtube.InnertubePayload {
|
||||
if (value.privacy !== undefined) result.privacy = decodeJson_8(value.privacy);
|
||||
if (value.madeForKids !== undefined) result.madeForKids = decodeJson_9(value.madeForKids);
|
||||
if (value.ageRestricted !== undefined) result.ageRestricted = decodeJson_10(value.ageRestricted);
|
||||
if (value.field83 !== undefined) result.field83 = jsonValueToTsValueFns.int32(value.field83);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -168,8 +172,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)],
|
||||
);
|
||||
@@ -228,6 +232,12 @@ export function encodeBinary(value: $.youtube.InnertubePayload): Uint8Array {
|
||||
[69, { type: WireType.LengthDelimited as const, value: encodeBinary_10(tsValue) }],
|
||||
);
|
||||
}
|
||||
if (value.field83 !== undefined) {
|
||||
const tsValue = value.field83;
|
||||
result.push(
|
||||
[83, tsValueToWireValueFns.int32(tsValue)],
|
||||
);
|
||||
}
|
||||
return serialize(result);
|
||||
}
|
||||
|
||||
@@ -247,7 +257,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);
|
||||
@@ -312,5 +322,12 @@ export function decodeBinary(binary: Uint8Array): $.youtube.InnertubePayload {
|
||||
if (value === undefined) break field;
|
||||
result.ageRestricted = value;
|
||||
}
|
||||
field: {
|
||||
const wireValue = wireFields.get(83);
|
||||
if (wireValue === undefined) break field;
|
||||
const value = wireValueToTsValueFns.int32(wireValue);
|
||||
if (value === undefined) break field;
|
||||
result.field83 = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 ****/
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { SegmentInfo as FSegmentInfo } from './StreamingInfo.js';
|
||||
import type { FormatFilter, URLTransformer } from '../types/FormatUtils.js';
|
||||
import type PlayerLiveStoryboardSpec from '../parser/classes/PlayerLiveStoryboardSpec.js';
|
||||
import type { StreamingInfoOptions } from '../types/StreamingInfoOptions.js';
|
||||
import type { CaptionTrackData } from '../parser/classes/PlayerCaptionsTracklist.js';
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Format } from '../parser/misc.js';
|
||||
import type { PlayerLiveStoryboardSpec } from '../parser/nodes.js';
|
||||
import type { FormatFilter, URLTransformer } from '../types/FormatUtils.js';
|
||||
import type { StreamingInfoOptions } from '../types/StreamingInfoOptions.js';
|
||||
import type { CaptionTrackData } from '../parser/classes/PlayerCaptionsTracklist.js';
|
||||
|
||||
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;
|
||||
|
||||
@@ -381,8 +381,8 @@ describe('YouTube.js Tests', () => {
|
||||
const playlist = await innertube.music.getPlaylist('PLQxo8OvVvJ1WI_Bp67F2wdIl_R2Rc_1-u');
|
||||
expect(playlist).toBeDefined();
|
||||
expect(playlist.header).toBeDefined();
|
||||
expect(playlist.items).toBeDefined();
|
||||
expect(playlist.items?.length).toBeGreaterThan(0);
|
||||
expect(playlist.contents).toBeDefined();
|
||||
expect(playlist.contents?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Innertube#music.getLyrics', async () => {
|
||||
|
||||
Reference in New Issue
Block a user