mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 01:52:11 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e54c0c4bf1 | ||
|
|
8e372d5c67 | ||
|
|
987f50604a | ||
|
|
69702085c6 | ||
|
|
d2959b3a55 | ||
|
|
68df321858 | ||
|
|
f4bc8508d0 | ||
|
|
e216124bb0 | ||
|
|
6d98abbd53 | ||
|
|
fba3fc9714 | ||
|
|
f94ea6cf91 | ||
|
|
86fb33ed03 | ||
|
|
bff4210349 | ||
|
|
91de6e5c0e | ||
|
|
c26972c42a | ||
|
|
8bc2aaa358 | ||
|
|
2e5f076fd7 | ||
|
|
0412fa05ff | ||
|
|
10c15bfb9f | ||
|
|
4862c35cee | ||
|
|
2eed1726d5 | ||
|
|
8b69587787 | ||
|
|
ed7be2a675 | ||
|
|
361fb4a9f1 | ||
|
|
1c3ea2acd3 | ||
|
|
859c4585d9 | ||
|
|
751f2b90fd | ||
|
|
90be877d28 |
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,5 +1,80 @@
|
||||
# Changelog
|
||||
|
||||
## [6.4.1](https://github.com/LuanRT/YouTube.js/compare/v6.4.0...v6.4.1) (2023-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Feed:** Do not throw when multiple continuations are present ([8e372d5](https://github.com/LuanRT/YouTube.js/commit/8e372d5c67f148be288bb0485f2c70ec43fbecd0))
|
||||
* **Playlist:** Throw a more helpful error when parsing empty responses ([987f506](https://github.com/LuanRT/YouTube.js/commit/987f50604a0163f9a07091ce787995c6f6fddb75))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* Cache deciphered n-params by info response ([#505](https://github.com/LuanRT/YouTube.js/issues/505)) ([d2959b3](https://github.com/LuanRT/YouTube.js/commit/d2959b3a55a5081295da4754627913933bbaf1e7))
|
||||
* **generator:** Remove duplicate checks in `isMiscType` ([#506](https://github.com/LuanRT/YouTube.js/issues/506)) ([68df321](https://github.com/LuanRT/YouTube.js/commit/68df3218580db10c9a0932c93ff2ce487526ff1e))
|
||||
|
||||
## [6.4.0](https://github.com/LuanRT/YouTube.js/compare/v6.3.0...v6.4.0) (2023-09-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for retrieving transcripts ([#500](https://github.com/LuanRT/YouTube.js/issues/500)) ([f94ea6c](https://github.com/LuanRT/YouTube.js/commit/f94ea6cf917f63f30dd66514b22a4cf43b948f07))
|
||||
* **PlaylistManager:** add .setName() and .setDescription() functions for editing playlists ([#498](https://github.com/LuanRT/YouTube.js/issues/498)) ([86fb33e](https://github.com/LuanRT/YouTube.js/commit/86fb33ed03a127d9fd4caa695ca97642bffe61bd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **BackstagePost:** `vote_button` type mismatch ([fba3fc9](https://github.com/LuanRT/YouTube.js/commit/fba3fc971454d66d80d4920fbd60889a221de381))
|
||||
|
||||
## [6.3.0](https://github.com/LuanRT/YouTube.js/compare/v6.2.0...v6.3.0) (2023-08-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ChannelMetadata:** Add `music_artist_name` ([#497](https://github.com/LuanRT/YouTube.js/issues/497)) ([91de6e5](https://github.com/LuanRT/YouTube.js/commit/91de6e5c0e5b27e6d12ce5db2f500c5ff78b9830))
|
||||
* **Session:** Add on_behalf_of_user session option. ([#494](https://github.com/LuanRT/YouTube.js/issues/494)) ([8bc2aaa](https://github.com/LuanRT/YouTube.js/commit/8bc2aaa3587fcf79f69eedbc2bf422a4c6fa7eb1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **CompactMovie:** Add missing import and remove unnecessary console.log ([#496](https://github.com/LuanRT/YouTube.js/issues/496)) ([c26972c](https://github.com/LuanRT/YouTube.js/commit/c26972c42a6368822ac254c00f1bbee5a1542486))
|
||||
|
||||
## [6.2.0](https://github.com/LuanRT/YouTube.js/compare/v6.1.0...v6.2.0) (2023-08-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Session:** Add fallback for session data retrieval ([#490](https://github.com/LuanRT/YouTube.js/issues/490)) ([10c15bf](https://github.com/LuanRT/YouTube.js/commit/10c15bfb9f131a2acea2f26ff3328993d8d8f4aa))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Format:** Fix `is_original` always being `true` ([#492](https://github.com/LuanRT/YouTube.js/issues/492)) ([0412fa0](https://github.com/LuanRT/YouTube.js/commit/0412fa05ff1f00960b398c2f18d5ce39ce0cb864))
|
||||
|
||||
## [6.1.0](https://github.com/LuanRT/YouTube.js/compare/v6.0.2...v6.1.0) (2023-08-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **parser:** Add `AlertWithButton` ([#486](https://github.com/LuanRT/YouTube.js/issues/486)) ([8b69587](https://github.com/LuanRT/YouTube.js/commit/8b6958778721ba274283f641779fb60bc6f42cd2))
|
||||
* **parser:** Add `ChannelHeaderLinksView` ([#484](https://github.com/LuanRT/YouTube.js/issues/484)) ([ed7be2a](https://github.com/LuanRT/YouTube.js/commit/ed7be2a675cf1ec663e743e90db6260c97546739))
|
||||
* **parser:** Add `CompactMovie` ([#487](https://github.com/LuanRT/YouTube.js/issues/487)) ([2eed172](https://github.com/LuanRT/YouTube.js/commit/2eed1726d5bde7648af09273cc14ab4a315cb23e))
|
||||
|
||||
## [6.0.2](https://github.com/LuanRT/YouTube.js/compare/v6.0.1...v6.0.2) (2023-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* invalid set ids in dash manifest ([#480](https://github.com/LuanRT/YouTube.js/issues/480)) ([1c3ea2a](https://github.com/LuanRT/YouTube.js/commit/1c3ea2acd38652c6b40a0817a7836c672a776c4e))
|
||||
|
||||
## [6.0.1](https://github.com/LuanRT/YouTube.js/compare/v6.0.0...v6.0.1) (2023-08-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SearchSubMenu:** Groups not being parsed due to a typo ([90be877](https://github.com/LuanRT/YouTube.js/commit/90be877d28e0ef013056eaeaa4f2765c91addd61))
|
||||
|
||||
## [6.0.0](https://github.com/LuanRT/YouTube.js/compare/v5.8.0...v6.0.0) (2023-08-18)
|
||||
|
||||
|
||||
|
||||
@@ -325,6 +325,9 @@ Retrieves video info.
|
||||
- `<info>#download(options)`
|
||||
- Downloads the video. See [download](#download).
|
||||
|
||||
- `<info>#getTranscript()`
|
||||
- Retrieves the video's transcript.
|
||||
|
||||
- `<info>#filters`
|
||||
- Returns filters that can be applied to the watch next feed.
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ Playlist management class.
|
||||
* [.addVideos(playlist_id, video_ids)](#addvideos)
|
||||
* [.removeVideos(playlist_id, video_ids)](#removevideos)
|
||||
* [.moveVideo(playlist_id, moved_video_id, predecessor_video_id)](#movevideo)
|
||||
* [.setName(playlist_id, name)](#setname)
|
||||
* [.setDescription(playlist_id, description)](#setdescription)
|
||||
|
||||
<a name="create"></a>
|
||||
### create(title, video_ids)
|
||||
@@ -69,4 +71,29 @@ Moves a video to a new position within a given playlist.
|
||||
| --- | --- | --- |
|
||||
| playlist_id | `string` | Playlist id |
|
||||
| moved_video_id | `string` | the video to be moved |
|
||||
| predecessor_video_id | `string` | the video present in the target position |
|
||||
| predecessor_video_id | `string` | the video present in the target position |
|
||||
|
||||
<a name="setname"></a>
|
||||
### setName(playlist_id, name)
|
||||
|
||||
Sets the name / title for the given playlist.
|
||||
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| playlist_id | `string` | Playlist id |
|
||||
| name | `string` | Name / title |
|
||||
|
||||
|
||||
<a name="setdescription"></a>
|
||||
### setDescription(playlist_id, description)
|
||||
|
||||
Sets the description for the given playlist.
|
||||
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| playlist_id | `string` | Playlist id |
|
||||
| description | `string` | Description |
|
||||
|
||||
@@ -31,24 +31,22 @@ For example, suppose we have found a new renderer named `verticalListRenderer`.
|
||||
> `../classes/VerticalList.ts`
|
||||
|
||||
```ts
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class VerticalList extends YTNode {
|
||||
export default class VerticalList extends YTNode {
|
||||
static type = 'VerticalList';
|
||||
|
||||
header;
|
||||
contents;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
// parse the data here, ex;
|
||||
this.header = Parser.parseItem(data.header);
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
}
|
||||
}
|
||||
|
||||
export default VerticalList;
|
||||
```
|
||||
|
||||
You may use the parser's generated class for the new renderer as a starting point for your own implementation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Innertube, UniversalCache } = require('youtubei.js');
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({
|
||||
|
||||
16
examples/transcript/index.ts
Normal file
16
examples/transcript/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ generate_session_locally: true });
|
||||
|
||||
const info = await yt.getInfo('hePb00CqvP0');
|
||||
|
||||
const defaultTranscriptInfo = await info.getTranscript();
|
||||
|
||||
console.log(`Got ${defaultTranscriptInfo.selectedLanguage} transcript with ${defaultTranscriptInfo.transcript.content.body.initial_segments.length} lines.`);
|
||||
|
||||
console.log("Fetching Hebrew transcript...");
|
||||
|
||||
const heTranscriptInfo = await defaultTranscriptInfo.selectLanguage('Hebrew');
|
||||
console.log(`Got ${heTranscriptInfo.selectedLanguage} transcript with ${heTranscriptInfo.transcript.content.body.initial_segments.length} lines.`);
|
||||
})();
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "6.0.0",
|
||||
"version": "6.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "6.0.0",
|
||||
"version": "6.4.1",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "6.0.0",
|
||||
"version": "6.4.1",
|
||||
"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",
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.js';
|
||||
|
||||
import type { ApiResponse } from './core/Actions.js';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
|
||||
import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.js';
|
||||
import type { INextRequest } from './types/index.js';
|
||||
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js';
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { FetchFunction } from '../types/PlatformShim.js';
|
||||
*/
|
||||
export default class Player {
|
||||
#nsig_sc;
|
||||
#nsig_cache;
|
||||
#sig_sc;
|
||||
#sig_sc_timestamp;
|
||||
#player_id;
|
||||
@@ -21,6 +22,8 @@ export default class Player {
|
||||
this.#sig_sc_timestamp = signature_timestamp;
|
||||
|
||||
this.#player_id = player_id;
|
||||
|
||||
this.#nsig_cache = new Map<string, string>();
|
||||
}
|
||||
|
||||
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise<Player> {
|
||||
@@ -66,7 +69,7 @@ export default class Player {
|
||||
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
|
||||
}
|
||||
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string): string {
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map<string, string>): string {
|
||||
url = url || signature_cipher || cipher;
|
||||
|
||||
if (!url)
|
||||
@@ -93,15 +96,23 @@ export default class Player {
|
||||
const n = url_components.searchParams.get('n');
|
||||
|
||||
if (n) {
|
||||
const nsig = Platform.shim.eval(this.#nsig_sc, {
|
||||
nsig: n
|
||||
});
|
||||
let nsig;
|
||||
|
||||
if (typeof nsig !== 'string')
|
||||
throw new PlayerError('Failed to decipher nsig');
|
||||
if (this_response_nsig_cache && this_response_nsig_cache.has(n)) {
|
||||
nsig = this_response_nsig_cache.get(n) as string;
|
||||
} else {
|
||||
nsig = Platform.shim.eval(this.#nsig_sc, {
|
||||
nsig: n
|
||||
});
|
||||
|
||||
if (nsig.startsWith('enhanced_except_')) {
|
||||
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
|
||||
if (typeof nsig !== 'string')
|
||||
throw new PlayerError('Failed to decipher nsig');
|
||||
|
||||
if (nsig.startsWith('enhanced_except_')) {
|
||||
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
|
||||
} else if (this_response_nsig_cache) {
|
||||
this_response_nsig_cache.set(n, nsig);
|
||||
}
|
||||
}
|
||||
|
||||
url_components.searchParams.set('n', nsig);
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface Context {
|
||||
user: {
|
||||
enableSafetyMode: boolean;
|
||||
lockedSafetyMode: boolean;
|
||||
onBehalfOfUser?: string;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
@@ -84,6 +85,10 @@ export interface SessionOptions {
|
||||
* Only works if you are signed in with cookies.
|
||||
*/
|
||||
account_index?: number;
|
||||
/**
|
||||
* Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles.
|
||||
*/
|
||||
on_behalf_of_user?: string;
|
||||
/**
|
||||
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
|
||||
* **NOTE:** Deciphering formats is not possible without the JS player.
|
||||
@@ -193,7 +198,8 @@ export default class Session extends EventEmitterLike {
|
||||
options.device_category,
|
||||
options.client_type,
|
||||
options.timezone,
|
||||
options.fetch
|
||||
options.fetch,
|
||||
options.on_behalf_of_user
|
||||
);
|
||||
|
||||
return new Session(
|
||||
@@ -213,14 +219,22 @@ export default class Session extends EventEmitterLike {
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
client_name: ClientType = ClientType.WEB,
|
||||
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
fetch: FetchFunction = Platform.shim.fetch
|
||||
fetch: FetchFunction = Platform.shim.fetch,
|
||||
on_behalf_of_user?: string
|
||||
) {
|
||||
let session_data: SessionData;
|
||||
|
||||
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
|
||||
|
||||
if (generate_session_locally) {
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
|
||||
session_data = this.#generateSessionData(session_args);
|
||||
} else {
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
|
||||
try {
|
||||
// This can fail if the data changes or the request is blocked for some reason.
|
||||
session_data = await this.#retrieveSessionData(session_args, fetch);
|
||||
} catch (err) {
|
||||
session_data = this.#generateSessionData(session_args);
|
||||
}
|
||||
}
|
||||
|
||||
return { ...session_data, account_index };
|
||||
@@ -234,6 +248,7 @@ export default class Session extends EventEmitterLike {
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
on_behalf_of_user?: string;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
@@ -293,7 +308,8 @@ export default class Session extends EventEmitterLike {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,6 +324,7 @@ export default class Session extends EventEmitterLike {
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
on_behalf_of_user?: string;
|
||||
}): SessionData {
|
||||
let visitor_id = generateRandomString(11);
|
||||
|
||||
@@ -340,7 +357,8 @@ export default class Session extends EventEmitterLike {
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ export function build(opts: EditPlaylistEndpointOptions): IEditPlaylistRequest {
|
||||
...{
|
||||
addedVideoId: action.added_video_id,
|
||||
setVideoId: action.set_video_id,
|
||||
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor
|
||||
movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor,
|
||||
playlistDescription: action.playlist_description,
|
||||
playlistName: action.playlist_name
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -200,4 +200,60 @@ export default class PlaylistManager {
|
||||
action_result: response.data.actions // TODO: implement actions in the parser
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name (title) for the given playlist.
|
||||
* @param playlist_id - The playlist ID.
|
||||
* @param name - The name / title to use for the playlist.
|
||||
*/
|
||||
async setName(playlist_id: string, name: string): Promise<{ playlist_id: string; action_result: any; }> {
|
||||
throwIfMissing({ playlist_id, name });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
|
||||
|
||||
payload.actions.push({
|
||||
action: 'ACTION_SET_PLAYLIST_NAME',
|
||||
playlist_name: name
|
||||
});
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
|
||||
);
|
||||
|
||||
return {
|
||||
playlist_id,
|
||||
action_result: response.data.actions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description for the given playlist.
|
||||
* @param playlist_id - The playlist ID.
|
||||
* @param description - The description to use for the playlist.
|
||||
*/
|
||||
async setDescription(playlist_id: string, description: string): Promise<{ playlist_id: string; action_result: any; }> {
|
||||
throwIfMissing({ playlist_id, description });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
|
||||
|
||||
payload.actions.push({
|
||||
action: 'ACTION_SET_PLAYLIST_DESCRIPTION',
|
||||
playlist_description: description
|
||||
});
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
|
||||
);
|
||||
|
||||
return {
|
||||
playlist_id,
|
||||
action_result: response.data.actions
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,10 +185,8 @@ export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
*/
|
||||
async getContinuationData(): Promise<T | undefined> {
|
||||
if (this.#continuation) {
|
||||
if (this.#continuation.length > 1)
|
||||
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
|
||||
if (this.#continuation.length === 0)
|
||||
throw new InnertubeError('There are no continuations');
|
||||
throw new InnertubeError('There are no continuations.');
|
||||
|
||||
const response = await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import Parser from '../../parser/index.js';
|
||||
import type { DashOptions } from '../../types/DashOptions.js';
|
||||
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
|
||||
import { getStreamingInfo } from '../../utils/StreamingInfo.js';
|
||||
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
|
||||
import TranscriptInfo from '../../parser/youtube/TranscriptInfo.js';
|
||||
|
||||
export default class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
@@ -84,6 +86,36 @@ export default class MediaInfo {
|
||||
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the video's transcript.
|
||||
* @param video_id - The video id.
|
||||
*/
|
||||
async getTranscript(): Promise<TranscriptInfo> {
|
||||
const next_response = this.page[1];
|
||||
|
||||
if (!next_response)
|
||||
throw new InnertubeError('Cannot get transcript from basic video info.');
|
||||
|
||||
if (!next_response.engagement_panels)
|
||||
throw new InnertubeError('Engagement panels not found. Video likely has no transcript.');
|
||||
|
||||
const transcript_panel = next_response.engagement_panels.get({
|
||||
panel_identifier: 'engagement-panel-searchable-transcript'
|
||||
});
|
||||
|
||||
if (!transcript_panel)
|
||||
throw new InnertubeError('Transcript panel not found. Video likely has no transcript.');
|
||||
|
||||
const transcript_continuation = transcript_panel.content?.as(ContinuationItem);
|
||||
|
||||
if (!transcript_continuation)
|
||||
throw new InnertubeError('Transcript continuation not found.');
|
||||
|
||||
const response = await transcript_continuation.endpoint.call(this.actions);
|
||||
|
||||
return new TranscriptInfo(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds video to the watch history.
|
||||
*/
|
||||
|
||||
19
src/parser/classes/AlertWithButton.ts
Normal file
19
src/parser/classes/AlertWithButton.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Button from './Button.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Parser, type RawNode } from '../index.js';
|
||||
|
||||
export default class AlertWithButton extends YTNode {
|
||||
static type = 'AlertWithButton';
|
||||
|
||||
text: Text;
|
||||
alert_type: string;
|
||||
dismiss_button: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.text = new Text(data.text);
|
||||
this.alert_type = data.type;
|
||||
this.dismiss_button = Parser.parseItem(data.dismissButton, Button);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import CommentActionButtons from './comments/CommentActionButtons.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
@@ -18,7 +19,7 @@ export default class BackstagePost extends YTNode {
|
||||
vote_count?: Text;
|
||||
menu?: Menu | null;
|
||||
action_buttons?: CommentActionButtons | null;
|
||||
vote_button?: CommentActionButtons | null;
|
||||
vote_button?: Button | null;
|
||||
surface: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
attachment;
|
||||
@@ -56,7 +57,7 @@ export default class BackstagePost extends YTNode {
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'voteButton')) {
|
||||
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
|
||||
this.vote_button = Parser.parseItem(data.voteButton, Button);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'navigationEndpoint')) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { YTNode } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import ChannelHeaderLinks from './ChannelHeaderLinks.js';
|
||||
import ChannelHeaderLinksView from './ChannelHeaderLinksView.js';
|
||||
import SubscribeButton from './SubscribeButton.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
@@ -18,7 +19,7 @@ export default class C4TabbedHeader extends YTNode {
|
||||
videos_count?: Text;
|
||||
sponsor_button?: Button | null;
|
||||
subscribe_button?: SubscribeButton | Button | null;
|
||||
header_links?: ChannelHeaderLinks | null;
|
||||
header_links?: ChannelHeaderLinks | ChannelHeaderLinksView | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
|
||||
@@ -58,7 +59,7 @@ export default class C4TabbedHeader extends YTNode {
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'headerLinks')) {
|
||||
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
|
||||
this.header_links = Parser.parseItem(data.headerLinks, [ ChannelHeaderLinks, ChannelHeaderLinksView ]);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'channelHandleText')) {
|
||||
|
||||
22
src/parser/classes/ChannelHeaderLinksView.ts
Normal file
22
src/parser/classes/ChannelHeaderLinksView.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class ChannelHeaderLinksView extends YTNode {
|
||||
static type = 'ChannelHeaderLinksView';
|
||||
|
||||
first_link?: Text;
|
||||
more?: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
if (Reflect.has(data, 'firstLink')) {
|
||||
this.first_link = Text.fromAttributed(data.firstLink);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'more')) {
|
||||
this.more = Text.fromAttributed(data.more);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default class ChannelMetadata extends YTNode {
|
||||
is_family_safe: boolean;
|
||||
keywords: string[];
|
||||
avatar: Thumbnail[];
|
||||
music_artist_name?: string;
|
||||
available_countries: string[];
|
||||
android_deep_link: string;
|
||||
android_appindexing_link: string;
|
||||
@@ -30,6 +31,8 @@ export default class ChannelMetadata extends YTNode {
|
||||
this.is_family_safe = data.isFamilySafe;
|
||||
this.keywords = data.keywords;
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
// Can be an empty string sometimes, so we need the extra length check
|
||||
this.music_artist_name = typeof data.musicArtistName === 'string' && data.musicArtistName.length > 0 ? data.musicArtistName : undefined;
|
||||
this.available_countries = data.availableCountryCodes;
|
||||
this.android_deep_link = data.androidDeepLink;
|
||||
this.android_appindexing_link = data.androidAppindexingLink;
|
||||
|
||||
57
src/parser/classes/CompactMovie.ts
Normal file
57
src/parser/classes/CompactMovie.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { YTNode, type ObservedArray } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
|
||||
export default class CompactMovie extends YTNode {
|
||||
static type = 'CompactMovie';
|
||||
|
||||
id: string;
|
||||
title: Text;
|
||||
top_metadata_items: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
thumbnail_overlays: ObservedArray<YTNode>;
|
||||
author: Author;
|
||||
|
||||
duration: {
|
||||
text: string;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
badges: ObservedArray<YTNode>;
|
||||
use_vertical_poster: boolean;
|
||||
menu: Menu | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
const overlay_time_status = data.thumbnailOverlays
|
||||
.find((overlay: RawNode) => overlay.thumbnailOverlayTimeStatusRenderer)
|
||||
?.thumbnailOverlayTimeStatusRenderer.text || 'N/A';
|
||||
|
||||
this.id = data.videoId;
|
||||
this.title = new Text(data.title);
|
||||
|
||||
this.top_metadata_items = new Text(data.topMetadataItems);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.author = new Author(data.shortBylineText);
|
||||
|
||||
const durationText = data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString();
|
||||
|
||||
this.duration = {
|
||||
text: durationText,
|
||||
seconds: timeToSeconds(durationText)
|
||||
};
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.badges = Parser.parseArray(data.badges);
|
||||
this.use_vertical_poster = data.useVerticalPoster;
|
||||
this.menu = Parser.parseItem(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Parser, { type RawNode } from '../index.js';
|
||||
import ContinuationItem from './ContinuationItem.js';
|
||||
import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.js';
|
||||
import MacroMarkersList from './MacroMarkersList.js';
|
||||
import ProductList from './ProductList.js';
|
||||
import SectionList from './SectionList.js';
|
||||
import StructuredDescriptionContent from './StructuredDescriptionContent.js';
|
||||
|
||||
@@ -10,7 +11,7 @@ export default class EngagementPanelSectionList extends YTNode {
|
||||
static type = 'EngagementPanelSectionList';
|
||||
|
||||
header: EngagementPanelTitleHeader | null;
|
||||
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | null;
|
||||
content: SectionList | ContinuationItem | StructuredDescriptionContent | MacroMarkersList | ProductList | null;
|
||||
target_id?: string;
|
||||
panel_identifier?: string;
|
||||
visibility?: string;
|
||||
@@ -18,9 +19,9 @@ export default class EngagementPanelSectionList extends YTNode {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader);
|
||||
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList ]);
|
||||
this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent, MacroMarkersList, ProductList ]);
|
||||
this.panel_identifier = data.panelIdentifier;
|
||||
this.target_id = data.targetId;
|
||||
this.visibility = data.visibility;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { YTNode } from '../helpers.js';
|
||||
import Parser, { type RawNode } from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import HorizontalCardList from './HorizontalCardList.js';
|
||||
import HorizontalList from './HorizontalList.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
@@ -15,7 +16,7 @@ export default class ExpandableMetadata extends YTNode {
|
||||
expanded_title: Text;
|
||||
};
|
||||
|
||||
expanded_content: HorizontalCardList | null;
|
||||
expanded_content: HorizontalCardList | HorizontalList | null;
|
||||
expand_button: Button | null;
|
||||
collapse_button: Button | null;
|
||||
|
||||
@@ -31,7 +32,7 @@ export default class ExpandableMetadata extends YTNode {
|
||||
};
|
||||
}
|
||||
|
||||
this.expanded_content = Parser.parseItem(data.expandedContent, HorizontalCardList);
|
||||
this.expanded_content = Parser.parseItem(data.expandedContent, [ HorizontalCardList, HorizontalList ]);
|
||||
this.expand_button = Parser.parseItem(data.expandButton, Button);
|
||||
this.collapse_button = Parser.parseItem(data.collapseButton, Button);
|
||||
}
|
||||
|
||||
16
src/parser/classes/FancyDismissibleDialog.ts
Normal file
16
src/parser/classes/FancyDismissibleDialog.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export default class FancyDismissibleDialog extends YTNode {
|
||||
static type = 'FancyDismissibleDialog';
|
||||
|
||||
dialog_message: Text;
|
||||
confirm_label: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.dialog_message = new Text(data.dialogMessage);
|
||||
this.confirm_label = new Text(data.confirmLabel);
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/ProductList.ts
Normal file
15
src/parser/classes/ProductList.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ObservedArray} from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
export default class ProductList extends YTNode {
|
||||
static type = 'ProductList';
|
||||
|
||||
contents: ObservedArray<YTNode>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
}
|
||||
}
|
||||
16
src/parser/classes/ProductListHeader.ts
Normal file
16
src/parser/classes/ProductListHeader.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export default class ProductListHeader extends YTNode {
|
||||
static type = 'ProductListHeader';
|
||||
|
||||
title: Text;
|
||||
suppress_padding_disclaimer: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.suppress_padding_disclaimer = !!data.suppressPaddingDisclaimer;
|
||||
}
|
||||
}
|
||||
31
src/parser/classes/ProductListItem.ts
Normal file
31
src/parser/classes/ProductListItem.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import { Text, Thumbnail } from '../misc.js';
|
||||
import Button from './Button.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
export default class ProductListItem extends YTNode {
|
||||
static type = 'ProductListItem';
|
||||
|
||||
title: Text;
|
||||
accessibility_title: string;
|
||||
thumbnail: Thumbnail[];
|
||||
price: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
merchant_name: string;
|
||||
stay_in_app: boolean;
|
||||
view_button: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.accessibility_title = data.accessibilityTitle;
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.price = data.price;
|
||||
this.endpoint = new NavigationEndpoint(data.onClickCommand);
|
||||
this.merchant_name = data.merchantName;
|
||||
this.stay_in_app = !!data.stayInApp;
|
||||
this.view_button = Parser.parseItem(data.viewButton, Button);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default class SearchSubMenu extends YTNode {
|
||||
if (Reflect.has(data, 'title'))
|
||||
this.title = new Text(data.title);
|
||||
|
||||
if (!Reflect.has(data, 'groups'))
|
||||
if (Reflect.has(data, 'groups'))
|
||||
this.groups = Parser.parseArray(data.groups, SearchFilterGroup);
|
||||
|
||||
if (Reflect.has(data, 'button'))
|
||||
|
||||
@@ -5,14 +5,24 @@ import HorizontalCardList from './HorizontalCardList.js';
|
||||
import VideoDescriptionHeader from './VideoDescriptionHeader.js';
|
||||
import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js';
|
||||
import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
|
||||
import VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js';
|
||||
import VideoDescriptionCourseSection from './VideoDescriptionCourseSection.js';
|
||||
|
||||
export default class StructuredDescriptionContent extends YTNode {
|
||||
static type = 'StructuredDescriptionContent';
|
||||
|
||||
items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection | HorizontalCardList>;
|
||||
items: ObservedArray<
|
||||
VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection |
|
||||
VideoDescriptionInfocardsSection | VideoDescriptionTranscriptSection | VideoDescriptionTranscriptSection |
|
||||
VideoDescriptionCourseSection | HorizontalCardList
|
||||
>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]);
|
||||
this.items = Parser.parseArray(data.items, [
|
||||
VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection,
|
||||
VideoDescriptionInfocardsSection, VideoDescriptionCourseSection, VideoDescriptionTranscriptSection,
|
||||
VideoDescriptionTranscriptSection, HorizontalCardList
|
||||
]);
|
||||
}
|
||||
}
|
||||
34
src/parser/classes/StructuredDescriptionPlaylistLockup.ts
Normal file
34
src/parser/classes/StructuredDescriptionPlaylistLockup.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
export default class StructuredDescriptionPlaylistLockup extends YTNode {
|
||||
static type = 'StructuredDescriptionPlaylistLockup';
|
||||
|
||||
thumbnail: Thumbnail[];
|
||||
title: Text;
|
||||
short_byline_text: Text;
|
||||
video_count_short_text: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
thumbnail_width: number;
|
||||
aspect_ratio: number;
|
||||
max_lines_title: number;
|
||||
max_lines_short_byline_text: number;
|
||||
overlay_position: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.title = new Text(data.title);
|
||||
this.short_byline_text = new Text(data.shortBylineText);
|
||||
this.video_count_short_text = new Text(data.videoCountShortText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.thumbnail_width = data.thumbnailWidth;
|
||||
this.aspect_ratio = data.aspectRatio;
|
||||
this.max_lines_title = data.maxLinesTitle;
|
||||
this.max_lines_short_byline_text = data.maxLinesShortBylineText;
|
||||
this.overlay_position = data.overlayPosition;
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/Transcript.ts
Normal file
15
src/parser/classes/Transcript.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import TranscriptSearchPanel from './TranscriptSearchPanel.js';
|
||||
|
||||
export default class Transcript extends YTNode {
|
||||
static type = 'Transcript';
|
||||
|
||||
content: TranscriptSearchPanel | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.content = Parser.parseItem(data.content, TranscriptSearchPanel);
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/TranscriptFooter.ts
Normal file
15
src/parser/classes/TranscriptFooter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import SortFilterSubMenu from './SortFilterSubMenu.js';
|
||||
|
||||
export default class TranscriptFooter extends YTNode {
|
||||
static type = 'TranscriptFooter';
|
||||
|
||||
language_menu: SortFilterSubMenu | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.language_menu = Parser.parseItem(data.languageMenu, SortFilterSubMenu);
|
||||
}
|
||||
}
|
||||
23
src/parser/classes/TranscriptSearchBox.ts
Normal file
23
src/parser/classes/TranscriptSearchBox.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export default class TranscriptSearchBox extends YTNode {
|
||||
static type = 'TranscriptSearchBox';
|
||||
|
||||
formatted_placeholder: Text;
|
||||
clear_button: Button | null;
|
||||
endpoint: NavigationEndpoint;
|
||||
search_button: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.formatted_placeholder = new Text(data.formattedPlaceholder);
|
||||
this.clear_button = Parser.parseItem(data.clearButton, Button);
|
||||
this.endpoint = new NavigationEndpoint(data.onTextChangeCommand);
|
||||
this.search_button = Parser.parseItem(data.searchButton, Button);
|
||||
}
|
||||
}
|
||||
23
src/parser/classes/TranscriptSearchPanel.ts
Normal file
23
src/parser/classes/TranscriptSearchPanel.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import TranscriptFooter from './TranscriptFooter.js';
|
||||
import TranscriptSearchBox from './TranscriptSearchBox.js';
|
||||
import TranscriptSegmentList from './TranscriptSegmentList.js';
|
||||
|
||||
export default class TranscriptSearchPanel extends YTNode {
|
||||
static type = 'TranscriptSearchPanel';
|
||||
|
||||
header: TranscriptSearchBox | null;
|
||||
body: TranscriptSegmentList | null;
|
||||
footer: TranscriptFooter | null;
|
||||
target_id: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.header = Parser.parseItem(data.header, TranscriptSearchBox);
|
||||
this.body = Parser.parseItem(data.body, TranscriptSegmentList);
|
||||
this.footer = Parser.parseItem(data.footer, TranscriptFooter);
|
||||
this.target_id = data.targetId;
|
||||
}
|
||||
}
|
||||
18
src/parser/classes/TranscriptSectionHeader.ts
Normal file
18
src/parser/classes/TranscriptSectionHeader.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class TranscriptSectionHeader extends YTNode {
|
||||
static type = 'TranscriptSectionHeader';
|
||||
|
||||
start_ms: string;
|
||||
end_ms: string;
|
||||
snippet: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.start_ms = data.startMs;
|
||||
this.end_ms = data.endMs;
|
||||
this.snippet = new Text(data.snippet);
|
||||
}
|
||||
}
|
||||
22
src/parser/classes/TranscriptSegment.ts
Normal file
22
src/parser/classes/TranscriptSegment.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
|
||||
export default class TranscriptSegment extends YTNode {
|
||||
static type = 'TranscriptSegment';
|
||||
|
||||
start_ms: string;
|
||||
end_ms: string;
|
||||
snippet: Text;
|
||||
start_time_text: Text;
|
||||
target_id: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.start_ms = data.startMs;
|
||||
this.end_ms = data.endMs;
|
||||
this.snippet = new Text(data.snippet);
|
||||
this.start_time_text = new Text(data.startTimeText);
|
||||
this.target_id = data.targetId;
|
||||
}
|
||||
}
|
||||
24
src/parser/classes/TranscriptSegmentList.ts
Normal file
24
src/parser/classes/TranscriptSegmentList.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
import TranscriptSectionHeader from './TranscriptSectionHeader.js';
|
||||
import TranscriptSegment from './TranscriptSegment.js';
|
||||
|
||||
export default class TranscriptSegmentList extends YTNode {
|
||||
static type = 'TranscriptSegmentList';
|
||||
|
||||
initial_segments: ObservedArray<TranscriptSegment | TranscriptSectionHeader>;
|
||||
no_result_label: Text;
|
||||
retry_label: Text;
|
||||
touch_captions_enabled: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.initial_segments = Parser.parseArray(data.initialSegments, [ TranscriptSegment, TranscriptSectionHeader ]);
|
||||
this.no_result_label = new Text(data.noResultLabel);
|
||||
this.retry_label = new Text(data.retryLabel);
|
||||
this.touch_captions_enabled = data.touchCaptionsEnabled;
|
||||
}
|
||||
}
|
||||
15
src/parser/classes/UploadTimeFactoid.ts
Normal file
15
src/parser/classes/UploadTimeFactoid.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import Factoid from './Factoid.js';
|
||||
|
||||
export default class UploadTimeFactoid extends YTNode {
|
||||
static type = 'UploadTimeFactoid';
|
||||
|
||||
factoid: Factoid | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.factoid = Parser.parseItem(data.factoid, Factoid);
|
||||
}
|
||||
}
|
||||
19
src/parser/classes/VideoDescriptionCourseSection.ts
Normal file
19
src/parser/classes/VideoDescriptionCourseSection.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ObservedArray} from '../helpers.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import StructuredDescriptionPlaylistLockup from './StructuredDescriptionPlaylistLockup.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
export default class VideoDescriptionCourseSection extends YTNode {
|
||||
static type = 'VideoDescriptionCourseSection';
|
||||
|
||||
section_title: Text;
|
||||
media_lockups: ObservedArray<StructuredDescriptionPlaylistLockup>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.section_title = new Text(data.sectionTitle);
|
||||
this.media_lockups = Parser.parseArray(data.mediaLockups, [ StructuredDescriptionPlaylistLockup ]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import Parser, { type RawNode } from '../index.js';
|
||||
import { Text, Thumbnail } from '../misc.js';
|
||||
import Factoid from './Factoid.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import UploadTimeFactoid from './UploadTimeFactoid.js';
|
||||
import ViewCountFactoid from './ViewCountFactoid.js';
|
||||
|
||||
export default class VideoDescriptionHeader extends YTNode {
|
||||
static type = 'VideoDescriptionHeader';
|
||||
@@ -10,7 +12,7 @@ export default class VideoDescriptionHeader extends YTNode {
|
||||
channel: Text;
|
||||
channel_navigation_endpoint?: NavigationEndpoint;
|
||||
channel_thumbnail: Thumbnail[];
|
||||
factoids: ObservedArray<Factoid>;
|
||||
factoids: ObservedArray<Factoid | ViewCountFactoid | UploadTimeFactoid>;
|
||||
publish_date: Text;
|
||||
title: Text;
|
||||
views: Text;
|
||||
@@ -23,6 +25,6 @@ export default class VideoDescriptionHeader extends YTNode {
|
||||
this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail);
|
||||
this.publish_date = new Text(data.publishDate);
|
||||
this.views = new Text(data.views);
|
||||
this.factoids = Parser.parseArray(data.factoid, Factoid);
|
||||
this.factoids = Parser.parseArray(data.factoid, [ Factoid, ViewCountFactoid, UploadTimeFactoid ]);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/parser/classes/VideoDescriptionTranscriptSection.ts
Normal file
20
src/parser/classes/VideoDescriptionTranscriptSection.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import { Text } from '../misc.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
export default class VideoDescriptionTranscriptSection extends YTNode {
|
||||
static type = 'VideoDescriptionTranscriptSection';
|
||||
|
||||
section_title: Text;
|
||||
sub_header_text: Text;
|
||||
primary_button: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.section_title = new Text(data.sectionTitle);
|
||||
this.sub_header_text = new Text(data.subHeaderText);
|
||||
this.primary_button = Parser.parseItem(data.primaryButton, Button);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export default class VideoSecondaryInfo extends YTNode {
|
||||
this.description = new Text(data.description);
|
||||
|
||||
if (Reflect.has(data, 'attributedDescription')) {
|
||||
this.description = new Text(this.#convertAttributedDescriptionToRuns(data.attributedDescription));
|
||||
this.description = Text.fromAttributed(data.attributedDescription);
|
||||
}
|
||||
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
@@ -34,72 +34,4 @@ export default class VideoSecondaryInfo extends YTNode {
|
||||
this.default_expanded = data.defaultExpanded;
|
||||
this.description_collapsed_lines = data.descriptionCollapsedLines;
|
||||
}
|
||||
|
||||
#convertAttributedDescriptionToRuns(description: RawNode) {
|
||||
const runs: {
|
||||
text: string,
|
||||
navigationEndpoint?: RawNode,
|
||||
attachment?: RawNode
|
||||
}[] = [];
|
||||
|
||||
const content = description.content;
|
||||
const command_runs = description.commandRuns;
|
||||
|
||||
let last_end_index = 0;
|
||||
|
||||
if (command_runs) {
|
||||
for (const item of command_runs) {
|
||||
const length: number = item.length;
|
||||
const start_index: number = item.startIndex;
|
||||
|
||||
if (start_index > last_end_index) {
|
||||
runs.push({
|
||||
text: content.slice(last_end_index, start_index)
|
||||
});
|
||||
}
|
||||
|
||||
if (Reflect.has(item, 'onTap')) {
|
||||
let attachment = null;
|
||||
|
||||
if (Reflect.has(description, 'attachmentRuns')) {
|
||||
const attachment_runs = description.attachmentRuns;
|
||||
|
||||
for (const attatchment_run of attachment_runs) {
|
||||
if ((attatchment_run.startIndex - 2) == start_index) {
|
||||
attachment = attatchment_run;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
runs.push({
|
||||
text: content.slice(start_index, start_index + length),
|
||||
navigationEndpoint: item.onTap,
|
||||
attachment
|
||||
});
|
||||
} else {
|
||||
runs.push({
|
||||
text: content.slice(start_index, start_index + length),
|
||||
navigationEndpoint: item.onTap
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
last_end_index = start_index + length;
|
||||
}
|
||||
|
||||
if (last_end_index < content.length) {
|
||||
runs.push({
|
||||
text: content.slice(last_end_index)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
runs.push({
|
||||
text: content
|
||||
});
|
||||
}
|
||||
|
||||
return { runs };
|
||||
}
|
||||
}
|
||||
19
src/parser/classes/ViewCountFactoid.ts
Normal file
19
src/parser/classes/ViewCountFactoid.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import Factoid from './Factoid.js';
|
||||
|
||||
export default class ViewCountFactoid extends YTNode {
|
||||
static type = 'ViewCountFactoid';
|
||||
|
||||
view_count_entity_key: string;
|
||||
factoid: Factoid | null;
|
||||
view_count_type: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.view_count_entity_key = data.viewCountEntityKey;
|
||||
this.factoid = Parser.parseItem(data.factoid, [ Factoid ]);
|
||||
this.view_count_type = data.viewCountType;
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/actions/UpdateEngagementPanelAction.ts
Normal file
17
src/parser/classes/actions/UpdateEngagementPanelAction.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
import Parser from '../../index.js';
|
||||
import Transcript from '../Transcript.js';
|
||||
|
||||
export default class UpdateEngagementPanelAction extends YTNode {
|
||||
static type = 'UpdateEngagementPanelAction';
|
||||
|
||||
target_id: string;
|
||||
content: Transcript | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.target_id = data.targetId;
|
||||
this.content = Parser.parseItem(data.content, Transcript);
|
||||
}
|
||||
}
|
||||
17
src/parser/classes/menus/MenuPopup.ts
Normal file
17
src/parser/classes/menus/MenuPopup.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ObservedArray} from '../../helpers.js';
|
||||
import { YTNode } from '../../helpers.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
import Parser from '../../index.js';
|
||||
import MenuNavigationItem from './MenuNavigationItem.js';
|
||||
import MenuServiceItem from './MenuServiceItem.js';
|
||||
|
||||
export default class MenuPopup extends YTNode {
|
||||
static type = 'MenuPopup';
|
||||
|
||||
items: ObservedArray<MenuNavigationItem | MenuServiceItem>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parseArray(data.items, [ MenuNavigationItem, MenuServiceItem ]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { InnertubeError } from '../../../utils/Utils.js';
|
||||
import type { RawNode } from '../../index.js';
|
||||
|
||||
export default class Format {
|
||||
#this_response_nsig_cache?: Map<string, string>;
|
||||
|
||||
itag: number;
|
||||
mime_type: string;
|
||||
is_type_otf: boolean;
|
||||
@@ -52,7 +54,11 @@ export default class Format {
|
||||
matrix_coefficients?: string;
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
constructor(data: RawNode, this_response_nsig_cache?: Map<string, string>) {
|
||||
if (this_response_nsig_cache) {
|
||||
this.#this_response_nsig_cache = this_response_nsig_cache;
|
||||
}
|
||||
|
||||
this.itag = data.itag;
|
||||
this.mime_type = data.mimeType;
|
||||
this.is_type_otf = data.type === 'FORMAT_STREAM_TYPE_OTF';
|
||||
@@ -104,7 +110,7 @@ export default class Format {
|
||||
this.language = xtags?.find((x: string) => x.startsWith('lang='))?.split('=')[1] || null;
|
||||
this.is_dubbed = audio_content === 'dubbed';
|
||||
this.is_descriptive = audio_content === 'descriptive';
|
||||
this.is_original = audio_content === 'original' || !this.is_dubbed || !this.is_descriptive;
|
||||
this.is_original = audio_content === 'original' || (!this.is_dubbed && !this.is_descriptive);
|
||||
|
||||
if (Reflect.has(data, 'audioTrack')) {
|
||||
this.audio_track = {
|
||||
@@ -122,6 +128,6 @@ export default class Format {
|
||||
*/
|
||||
decipher(player: Player | undefined): string {
|
||||
if (!player) throw new InnertubeError('Cannot decipher format, this session appears to have no valid player.');
|
||||
return player.decipher(this.url, this.signature_cipher, this.cipher);
|
||||
return player.decipher(this.url, this.signature_cipher, this.cipher, this.#this_response_nsig_cache);
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,74 @@ export default class Text {
|
||||
}
|
||||
}
|
||||
|
||||
static fromAttributed(data: RawNode): Text {
|
||||
const runs: {
|
||||
text: string,
|
||||
navigationEndpoint?: RawNode,
|
||||
attachment?: RawNode
|
||||
}[] = [];
|
||||
|
||||
const content = data.content;
|
||||
const command_runs = data.commandRuns;
|
||||
|
||||
let last_end_index = 0;
|
||||
|
||||
if (command_runs) {
|
||||
for (const item of command_runs) {
|
||||
const length: number = item.length;
|
||||
const start_index: number = item.startIndex;
|
||||
|
||||
if (start_index > last_end_index) {
|
||||
runs.push({
|
||||
text: content.slice(last_end_index, start_index)
|
||||
});
|
||||
}
|
||||
|
||||
if (Reflect.has(item, 'onTap')) {
|
||||
let attachment = null;
|
||||
|
||||
if (Reflect.has(data, 'attachmentRuns')) {
|
||||
const attachment_runs = data.attachmentRuns;
|
||||
|
||||
for (const attatchment_run of attachment_runs) {
|
||||
if ((attatchment_run.startIndex - 2) == start_index) {
|
||||
attachment = attatchment_run;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
runs.push({
|
||||
text: content.slice(start_index, start_index + length),
|
||||
navigationEndpoint: item.onTap,
|
||||
attachment
|
||||
});
|
||||
} else {
|
||||
runs.push({
|
||||
text: content.slice(start_index, start_index + length),
|
||||
navigationEndpoint: item.onTap
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
last_end_index = start_index + length;
|
||||
}
|
||||
|
||||
if (last_end_index < content.length) {
|
||||
runs.push({
|
||||
text: content.slice(last_end_index)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
runs.push({
|
||||
text: content
|
||||
});
|
||||
}
|
||||
|
||||
return new Text({ runs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the text to HTML.
|
||||
* @returns The HTML.
|
||||
|
||||
@@ -134,33 +134,35 @@ export function isRendererList(value: unknown) {
|
||||
* @returns If it is a misc type, return the InferenceType. Otherwise, return false.
|
||||
*/
|
||||
export function isMiscType(key: string, value: unknown): MiscInferenceType | false {
|
||||
// NavigationEndpoint
|
||||
if ((key.endsWith('Endpoint') || key.endsWith('Command') || key === 'endpoint') && typeof value === 'object' && value !== null) {
|
||||
return {
|
||||
type: 'misc',
|
||||
endpoint: new NavigationEndpoint(value),
|
||||
optional: false,
|
||||
misc_type: 'NavigationEndpoint'
|
||||
};
|
||||
}
|
||||
// Text
|
||||
if (typeof value === 'object' && value !== null && (Reflect.has(value, 'simpleText') || Reflect.has(value, 'runs'))) {
|
||||
const textNode = new Text(value);
|
||||
return {
|
||||
type: 'misc',
|
||||
misc_type: 'Text',
|
||||
optional: false,
|
||||
endpoint: textNode.endpoint,
|
||||
text: textNode.toString()
|
||||
};
|
||||
}
|
||||
// Thumbnail
|
||||
if (typeof value === 'object' && value !== null && Reflect.has(value, 'thumbnails') && Array.isArray(Reflect.get(value, 'thumbnails'))) {
|
||||
return {
|
||||
type: 'misc',
|
||||
misc_type: 'Thumbnail',
|
||||
optional: false
|
||||
};
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// NavigationEndpoint
|
||||
if (key.endsWith('Endpoint') || key.endsWith('Command') || key === 'endpoint') {
|
||||
return {
|
||||
type: 'misc',
|
||||
endpoint: new NavigationEndpoint(value),
|
||||
optional: false,
|
||||
misc_type: 'NavigationEndpoint'
|
||||
};
|
||||
}
|
||||
// Text
|
||||
if (Reflect.has(value, 'simpleText') || Reflect.has(value, 'runs')) {
|
||||
const textNode = new Text(value);
|
||||
return {
|
||||
type: 'misc',
|
||||
misc_type: 'Text',
|
||||
optional: false,
|
||||
endpoint: textNode.endpoint,
|
||||
text: textNode.toString()
|
||||
};
|
||||
}
|
||||
// Thumbnail
|
||||
if (Reflect.has(value, 'thumbnails') && Array.isArray(Reflect.get(value, 'thumbnails'))) {
|
||||
return {
|
||||
type: 'misc',
|
||||
misc_type: 'Thumbnail',
|
||||
optional: false
|
||||
};
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ export { default as AccountItemSectionHeader } from './classes/AccountItemSectio
|
||||
export { default as AccountSectionList } from './classes/AccountSectionList.js';
|
||||
export { default as AppendContinuationItemsAction } from './classes/actions/AppendContinuationItemsAction.js';
|
||||
export { default as OpenPopupAction } from './classes/actions/OpenPopupAction.js';
|
||||
export { default as UpdateEngagementPanelAction } from './classes/actions/UpdateEngagementPanelAction.js';
|
||||
export { default as Alert } from './classes/Alert.js';
|
||||
export { default as AlertWithButton } from './classes/AlertWithButton.js';
|
||||
export { default as AnalyticsMainAppKeyMetrics } from './classes/analytics/AnalyticsMainAppKeyMetrics.js';
|
||||
export { default as AnalyticsRoot } from './classes/analytics/AnalyticsRoot.js';
|
||||
export { default as AnalyticsShortsCarouselCard } from './classes/analytics/AnalyticsShortsCarouselCard.js';
|
||||
@@ -36,6 +38,7 @@ export { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullM
|
||||
export { default as ChannelAgeGate } from './classes/ChannelAgeGate.js';
|
||||
export { default as ChannelFeaturedContent } from './classes/ChannelFeaturedContent.js';
|
||||
export { default as ChannelHeaderLinks } from './classes/ChannelHeaderLinks.js';
|
||||
export { default as ChannelHeaderLinksView } from './classes/ChannelHeaderLinksView.js';
|
||||
export { default as ChannelMetadata } from './classes/ChannelMetadata.js';
|
||||
export { default as ChannelMobileHeader } from './classes/ChannelMobileHeader.js';
|
||||
export { default as ChannelOptions } from './classes/ChannelOptions.js';
|
||||
@@ -67,6 +70,7 @@ export { default as SponsorCommentBadge } from './classes/comments/SponsorCommen
|
||||
export { default as CompactChannel } from './classes/CompactChannel.js';
|
||||
export { default as CompactLink } from './classes/CompactLink.js';
|
||||
export { default as CompactMix } from './classes/CompactMix.js';
|
||||
export { default as CompactMovie } from './classes/CompactMovie.js';
|
||||
export { default as CompactPlaylist } from './classes/CompactPlaylist.js';
|
||||
export { default as CompactStation } from './classes/CompactStation.js';
|
||||
export { default as CompactVideo } from './classes/CompactVideo.js';
|
||||
@@ -99,6 +103,7 @@ export { default as ExpandableTab } from './classes/ExpandableTab.js';
|
||||
export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js';
|
||||
export { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.js';
|
||||
export { default as Factoid } from './classes/Factoid.js';
|
||||
export { default as FancyDismissibleDialog } from './classes/FancyDismissibleDialog.js';
|
||||
export { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.js';
|
||||
export { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.js';
|
||||
export { default as GameCard } from './classes/GameCard.js';
|
||||
@@ -188,6 +193,7 @@ export { default as MacroMarkersList } from './classes/MacroMarkersList.js';
|
||||
export { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.js';
|
||||
export { default as Menu } from './classes/menus/Menu.js';
|
||||
export { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem.js';
|
||||
export { default as MenuPopup } from './classes/menus/MenuPopup.js';
|
||||
export { default as MenuServiceItem } from './classes/menus/MenuServiceItem.js';
|
||||
export { default as MenuServiceItemDownload } from './classes/menus/MenuServiceItemDownload.js';
|
||||
export { default as MultiPageMenu } from './classes/menus/MultiPageMenu.js';
|
||||
@@ -271,6 +277,9 @@ export { default as PlaylistVideoThumbnail } from './classes/PlaylistVideoThumbn
|
||||
export { default as Poll } from './classes/Poll.js';
|
||||
export { default as Post } from './classes/Post.js';
|
||||
export { default as PostMultiImage } from './classes/PostMultiImage.js';
|
||||
export { default as ProductList } from './classes/ProductList.js';
|
||||
export { default as ProductListHeader } from './classes/ProductListHeader.js';
|
||||
export { default as ProductListItem } from './classes/ProductListItem.js';
|
||||
export { default as ProfileColumn } from './classes/ProfileColumn.js';
|
||||
export { default as ProfileColumnStats } from './classes/ProfileColumnStats.js';
|
||||
export { default as ProfileColumnStatsEntry } from './classes/ProfileColumnStatsEntry.js';
|
||||
@@ -319,6 +328,7 @@ export { default as SlimOwner } from './classes/SlimOwner.js';
|
||||
export { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.js';
|
||||
export { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.js';
|
||||
export { default as StructuredDescriptionContent } from './classes/StructuredDescriptionContent.js';
|
||||
export { default as StructuredDescriptionPlaylistLockup } from './classes/StructuredDescriptionPlaylistLockup.js';
|
||||
export { default as SubFeedOption } from './classes/SubFeedOption.js';
|
||||
export { default as SubFeedSelector } from './classes/SubFeedSelector.js';
|
||||
export { default as SubscribeButton } from './classes/SubscribeButton.js';
|
||||
@@ -346,22 +356,33 @@ export { default as ToggleButton } from './classes/ToggleButton.js';
|
||||
export { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem.js';
|
||||
export { default as Tooltip } from './classes/Tooltip.js';
|
||||
export { default as TopicChannelDetails } from './classes/TopicChannelDetails.js';
|
||||
export { default as Transcript } from './classes/Transcript.js';
|
||||
export { default as TranscriptFooter } from './classes/TranscriptFooter.js';
|
||||
export { default as TranscriptSearchBox } from './classes/TranscriptSearchBox.js';
|
||||
export { default as TranscriptSearchPanel } from './classes/TranscriptSearchPanel.js';
|
||||
export { default as TranscriptSectionHeader } from './classes/TranscriptSectionHeader.js';
|
||||
export { default as TranscriptSegment } from './classes/TranscriptSegment.js';
|
||||
export { default as TranscriptSegmentList } from './classes/TranscriptSegmentList.js';
|
||||
export { default as TwoColumnBrowseResults } from './classes/TwoColumnBrowseResults.js';
|
||||
export { default as TwoColumnSearchResults } from './classes/TwoColumnSearchResults.js';
|
||||
export { default as TwoColumnWatchNextResults } from './classes/TwoColumnWatchNextResults.js';
|
||||
export { default as UniversalWatchCard } from './classes/UniversalWatchCard.js';
|
||||
export { default as UploadTimeFactoid } from './classes/UploadTimeFactoid.js';
|
||||
export { default as UpsellDialog } from './classes/UpsellDialog.js';
|
||||
export { default as VerticalList } from './classes/VerticalList.js';
|
||||
export { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.js';
|
||||
export { default as Video } from './classes/Video.js';
|
||||
export { default as VideoCard } from './classes/VideoCard.js';
|
||||
export { default as VideoDescriptionCourseSection } from './classes/VideoDescriptionCourseSection.js';
|
||||
export { default as VideoDescriptionHeader } from './classes/VideoDescriptionHeader.js';
|
||||
export { default as VideoDescriptionInfocardsSection } from './classes/VideoDescriptionInfocardsSection.js';
|
||||
export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.js';
|
||||
export { default as VideoDescriptionTranscriptSection } from './classes/VideoDescriptionTranscriptSection.js';
|
||||
export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js';
|
||||
export { default as VideoOwner } from './classes/VideoOwner.js';
|
||||
export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js';
|
||||
export { default as VideoSecondaryInfo } from './classes/VideoSecondaryInfo.js';
|
||||
export { default as ViewCountFactoid } from './classes/ViewCountFactoid.js';
|
||||
export { default as WatchCardCompactVideo } from './classes/WatchCardCompactVideo.js';
|
||||
export { default as WatchCardHeroVideo } from './classes/WatchCardHeroVideo.js';
|
||||
export { default as WatchCardRichHeader } from './classes/WatchCardRichHeader.js';
|
||||
|
||||
@@ -6,6 +6,8 @@ import PlayerCaptionsTracklist from './classes/PlayerCaptionsTracklist.js';
|
||||
import PlayerLiveStoryboardSpec from './classes/PlayerLiveStoryboardSpec.js';
|
||||
import PlayerStoryboardSpec from './classes/PlayerStoryboardSpec.js';
|
||||
import Alert from './classes/Alert.js';
|
||||
import AlertWithButton from './classes/AlertWithButton.js';
|
||||
import EngagementPanelSectionList from './classes/EngagementPanelSectionList.js';
|
||||
|
||||
import type { IParsedResponse, IRawResponse, RawData, RawNode } from './types/index.js';
|
||||
|
||||
@@ -20,7 +22,13 @@ import { Memo, observe, SuperParsedResult } from './helpers.js';
|
||||
import * as YTNodes from './nodes.js';
|
||||
import type { KeyInfo } from './generator.js';
|
||||
import { camelToSnake, generateRuntimeClass, generateTypescriptClass } from './generator.js';
|
||||
import { Continuation, ItemSectionContinuation, SectionListContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand, ReloadContinuationItemsCommand } from './continuations.js';
|
||||
|
||||
import {
|
||||
Continuation, ItemSectionContinuation, SectionListContinuation,
|
||||
LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation,
|
||||
GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand,
|
||||
ReloadContinuationItemsCommand
|
||||
} from './continuations.js';
|
||||
|
||||
export type ParserError = {
|
||||
classname: string,
|
||||
@@ -288,6 +296,14 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
}
|
||||
_clearMemo();
|
||||
|
||||
_createMemo();
|
||||
const items = parse(data.items);
|
||||
if (items) {
|
||||
parsed_data.items = items;
|
||||
parsed_data.items_memo = _getMemo();
|
||||
}
|
||||
_clearMemo();
|
||||
|
||||
applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
|
||||
|
||||
const continuation = data.continuation ? parseC(data.continuation) : null;
|
||||
@@ -310,7 +326,7 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
parsed_data.overlay = overlay;
|
||||
}
|
||||
|
||||
const alerts = parseArray(data.alerts, Alert);
|
||||
const alerts = parseArray(data.alerts, [ Alert, AlertWithButton ]);
|
||||
if (alerts.length) {
|
||||
parsed_data.alerts = alerts;
|
||||
}
|
||||
@@ -351,15 +367,21 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
parsed_data.playability_status = playability_status;
|
||||
}
|
||||
|
||||
const streaming_data = data.streamingData ? {
|
||||
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
|
||||
formats: parseFormats(data.streamingData.formats),
|
||||
adaptive_formats: parseFormats(data.streamingData.adaptiveFormats),
|
||||
dash_manifest_url: data.streamingData.dashManifestUrl || null,
|
||||
hls_manifest_url: data.streamingData.hlsManifestUrl || null
|
||||
} : undefined;
|
||||
if (data.streamingData) {
|
||||
// Currently each response with streaming data only has two n param values
|
||||
// One for the adaptive formats and another for the combined formats
|
||||
// As they are the same for a response, we only need to decipher them once
|
||||
// For all futher deciphering calls on formats from that response, we can use the cached output, given the same input n param
|
||||
const this_response_nsig_cache = new Map<string, string>();
|
||||
|
||||
const streaming_data = {
|
||||
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
|
||||
formats: parseFormats(data.streamingData.formats, this_response_nsig_cache),
|
||||
adaptive_formats: parseFormats(data.streamingData.adaptiveFormats, this_response_nsig_cache),
|
||||
dash_manifest_url: data.streamingData.dashManifestUrl || null,
|
||||
hls_manifest_url: data.streamingData.hlsManifestUrl || null
|
||||
};
|
||||
|
||||
if (streaming_data) {
|
||||
parsed_data.streaming_data = streaming_data;
|
||||
}
|
||||
|
||||
@@ -403,20 +425,10 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
parsed_data.cards = cards;
|
||||
}
|
||||
|
||||
const engagement_panels = data.engagementPanels?.map((e) => {
|
||||
const item = parseItem(e, YTNodes.EngagementPanelSectionList) as YTNodes.EngagementPanelSectionList;
|
||||
return item;
|
||||
});
|
||||
if (engagement_panels) {
|
||||
const engagement_panels = parseArray(data.engagementPanels, EngagementPanelSectionList);
|
||||
if (engagement_panels.length) {
|
||||
parsed_data.engagement_panels = engagement_panels;
|
||||
}
|
||||
_createMemo();
|
||||
const items = parse(data.items);
|
||||
if (items) {
|
||||
parsed_data.items = items;
|
||||
parsed_data.items_memo = _getMemo();
|
||||
}
|
||||
_clearMemo();
|
||||
|
||||
return parsed_data;
|
||||
}
|
||||
@@ -428,7 +440,7 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
*/
|
||||
export function parseItem<T extends YTNode, K extends YTNodeConstructor<T>[]>(data: RawNode | undefined, validTypes: K): InstanceType<K[number]> | null;
|
||||
export function parseItem<T extends YTNode>(data: RawNode | undefined, validTypes: YTNodeConstructor<T>): T | null;
|
||||
export function parseItem(data?: RawNode) : YTNode;
|
||||
export function parseItem(data?: RawNode): YTNode;
|
||||
export function parseItem(data?: RawNode, validTypes?: YTNodeConstructor | YTNodeConstructor[]) {
|
||||
if (!data) return null;
|
||||
|
||||
@@ -592,8 +604,8 @@ export function parseActions(data: RawData) {
|
||||
return new SuperParsedResult(parseItem(data));
|
||||
}
|
||||
|
||||
export function parseFormats(formats: RawNode[]): Format[] {
|
||||
return formats?.map((format) => new Format(format)) || [];
|
||||
export function parseFormats(formats: RawNode[], this_response_nsig_cache: Map<string, string>): Format[] {
|
||||
return formats?.map((format) => new Format(format, this_response_nsig_cache)) || [];
|
||||
}
|
||||
|
||||
export function applyMutations(memo: Memo, mutations: RawNode[]) {
|
||||
|
||||
@@ -15,10 +15,12 @@ import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.j
|
||||
import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.js';
|
||||
import type VideoDetails from '../classes/misc/VideoDetails.js';
|
||||
import type Alert from '../classes/Alert.js';
|
||||
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';
|
||||
|
||||
export interface IParsedResponse {
|
||||
actions?: SuperParsedResult<YTNode>;
|
||||
actions_memo?: Memo;
|
||||
@@ -44,7 +46,7 @@ export interface IParsedResponse {
|
||||
metadata?: SuperParsedResult<YTNode>;
|
||||
microformat?: YTNode;
|
||||
overlay?: YTNode;
|
||||
alerts?: ObservedArray<Alert>;
|
||||
alerts?: ObservedArray<Alert | AlertWithButton>;
|
||||
refinements?: string[];
|
||||
estimated_results?: number;
|
||||
player_overlays?: SuperParsedResult<YTNode>;
|
||||
@@ -62,7 +64,7 @@ export interface IParsedResponse {
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
endscreen?: Endscreen;
|
||||
cards?: CardCollection;
|
||||
engagement_panels?: EngagementPanelSectionList[];
|
||||
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
|
||||
items?: SuperParsedResult<YTNode>;
|
||||
}
|
||||
|
||||
@@ -105,7 +107,7 @@ export interface INextResponse {
|
||||
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_endpoints_memo?: Memo;
|
||||
player_overlays?: SuperParsedResult<YTNode>;
|
||||
engagement_panels?: EngagementPanelSectionList[];
|
||||
engagement_panels?: ObservedArray<EngagementPanelSectionList>;
|
||||
}
|
||||
|
||||
export interface IBrowseResponse {
|
||||
@@ -122,7 +124,7 @@ export interface IBrowseResponse {
|
||||
header_memo?: Memo;
|
||||
metadata?: SuperParsedResult<YTNode>;
|
||||
microformat?: YTNode;
|
||||
alerts?: ObservedArray<Alert>;
|
||||
alerts?: ObservedArray<Alert | AlertWithButton>;
|
||||
sidebar?: YTNode;
|
||||
sidebar_memo?: Memo;
|
||||
}
|
||||
@@ -144,6 +146,11 @@ export interface IResolveURLResponse {
|
||||
endpoint: NavigationEndpoint;
|
||||
}
|
||||
|
||||
export interface IGetTranscriptResponse {
|
||||
actions: SuperParsedResult<YTNode>;
|
||||
actions_memo: Memo;
|
||||
}
|
||||
|
||||
export interface IGetNotificationsMenuResponse {
|
||||
actions: SuperParsedResult<YTNode>;
|
||||
actions_memo: Memo;
|
||||
|
||||
@@ -9,6 +9,7 @@ import PlaylistSidebarPrimaryInfo from '../classes/PlaylistSidebarPrimaryInfo.js
|
||||
import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInfo.js';
|
||||
import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail.js';
|
||||
import VideoOwner from '../classes/VideoOwner.js';
|
||||
import Alert from '../classes/Alert.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
@@ -17,7 +18,7 @@ import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Playlist extends Feed<IBrowseResponse> {
|
||||
export default class Playlist extends Feed<IBrowseResponse> {
|
||||
info;
|
||||
menu;
|
||||
endpoint?: NavigationEndpoint;
|
||||
@@ -29,9 +30,13 @@ class Playlist extends Feed<IBrowseResponse> {
|
||||
const header = this.memo.getType(PlaylistHeader).first();
|
||||
const primary_info = this.memo.getType(PlaylistSidebarPrimaryInfo).first();
|
||||
const secondary_info = this.memo.getType(PlaylistSidebarSecondaryInfo).first();
|
||||
const alert = this.page.alerts?.firstOfType(Alert);
|
||||
|
||||
if (!primary_info && !secondary_info)
|
||||
throw new InnertubeError('This playlist does not exist');
|
||||
if (alert && alert.alert_type === 'ERROR')
|
||||
throw new InnertubeError(alert.text.toString(), alert);
|
||||
|
||||
if (!primary_info && !secondary_info && Object.keys(this.page).length === 0)
|
||||
throw new InnertubeError('Got empty continuation response. This is likely the end of the playlist.');
|
||||
|
||||
this.info = {
|
||||
...this.page.metadata?.item().as(PlaylistMetadata),
|
||||
@@ -69,6 +74,4 @@ class Playlist extends Feed<IBrowseResponse> {
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new Playlist(this.actions, page, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default Playlist;
|
||||
}
|
||||
55
src/parser/youtube/TranscriptInfo.ts
Normal file
55
src/parser/youtube/TranscriptInfo.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import { type ApiResponse } from '../../core/Actions.js';
|
||||
import type { IGetTranscriptResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import Transcript from '../classes/Transcript.js';
|
||||
|
||||
export default class TranscriptInfo {
|
||||
#page: IGetTranscriptResponse;
|
||||
#actions: Actions;
|
||||
transcript: Transcript;
|
||||
|
||||
constructor(actions: Actions, response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#actions = actions;
|
||||
this.transcript = this.#page.actions_memo.getType(Transcript).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a language from the language menu and returns the updated transcript.
|
||||
* @param language - Language to select.
|
||||
*/
|
||||
async selectLanguage(language: string): Promise<TranscriptInfo> {
|
||||
const target_menu_item = this.transcript.content?.footer?.language_menu?.sub_menu_items?.find((item) => item.title.toString() === language);
|
||||
|
||||
if (!target_menu_item)
|
||||
throw new Error(`Language not found: ${language}`);
|
||||
|
||||
if (target_menu_item.selected)
|
||||
return this;
|
||||
|
||||
const response = await this.#actions.execute('/get_transcript', {
|
||||
params: target_menu_item.continuation
|
||||
});
|
||||
|
||||
return new TranscriptInfo(this.#actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns available languages.
|
||||
*/
|
||||
get languages(): string[] {
|
||||
return this.transcript.content?.footer?.language_menu?.sub_menu_items?.map((item) => item.title.toString()) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently selected language.
|
||||
*/
|
||||
get selectedLanguage(): string {
|
||||
return this.transcript.content?.footer?.language_menu?.sub_menu_items?.find((item) => item.selected)?.title.toString() || '';
|
||||
}
|
||||
|
||||
get page() {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -334,18 +334,22 @@ export type EditPlaylistEndpointOptions = {
|
||||
* The changes to make to the playlist.
|
||||
*/
|
||||
actions: {
|
||||
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER';
|
||||
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER' | 'ACTION_SET_PLAYLIST_DESCRIPTION' | 'ACTION_SET_PLAYLIST_NAME';
|
||||
added_video_id?: string;
|
||||
set_video_id?: string;
|
||||
moved_set_video_id_predecessor?: string;
|
||||
playlist_description?: string;
|
||||
playlist_name?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IEditPlaylistRequest extends ObjectSnakeToCamel<EditPlaylistEndpointOptions> {
|
||||
actions: {
|
||||
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER';
|
||||
action: 'ACTION_ADD_VIDEO' | 'ACTION_REMOVE_VIDEO' | 'ACTION_MOVE_VIDEO_AFTER' | 'ACTION_SET_PLAYLIST_DESCRIPTION' | 'ACTION_SET_PLAYLIST_NAME';
|
||||
addedVideoId?: string;
|
||||
setVideoId?: string;
|
||||
movedSetVideoIdPredecessor?: string;
|
||||
playlistDescription?: string;
|
||||
playlistName?: string;
|
||||
}[];
|
||||
}
|
||||
@@ -90,7 +90,7 @@ function DashManifest({
|
||||
{
|
||||
audio_sets.map((set, index) => (
|
||||
<adaptation-set
|
||||
id={`audio_${index}`}
|
||||
id={index}
|
||||
mimeType={set.mime_type}
|
||||
startWithSAP="1"
|
||||
subsegmentAlignment="true"
|
||||
@@ -107,7 +107,7 @@ function DashManifest({
|
||||
}
|
||||
{
|
||||
set.track_name &&
|
||||
<label id={`audio_${index}`}>
|
||||
<label id={index}>
|
||||
{set.track_name}
|
||||
</label>
|
||||
}
|
||||
@@ -143,7 +143,7 @@ function DashManifest({
|
||||
{
|
||||
video_sets.map((set, index) => (
|
||||
<adaptation-set
|
||||
id={`video_${index}`}
|
||||
id={index + audio_sets.length}
|
||||
mimeType={set.mime_type}
|
||||
startWithSAP="1"
|
||||
subsegmentAlignment="true"
|
||||
@@ -192,7 +192,7 @@ function DashManifest({
|
||||
{
|
||||
image_sets.map(async (set, index) => {
|
||||
return <adaptation-set
|
||||
id={`thumbs_${index}`}
|
||||
id={index + audio_sets.length + video_sets.length}
|
||||
mimeType={await set.getMimeType()}
|
||||
contentType="image"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user