mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 18:12:10 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e54c0c4bf1 | ||
|
|
8e372d5c67 | ||
|
|
987f50604a | ||
|
|
69702085c6 | ||
|
|
d2959b3a55 | ||
|
|
68df321858 | ||
|
|
f4bc8508d0 | ||
|
|
e216124bb0 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -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.
|
||||
|
||||
@@ -660,16 +663,6 @@ console.info('Playback url:', url);
|
||||
| video_id | `string` | Video id |
|
||||
| options | `FormatOptions` | Format options |
|
||||
|
||||
<a name="gettranscript"></a>
|
||||
### `getTranscript(video_id)`
|
||||
Retrieves a given video's transcript.
|
||||
|
||||
**Returns**: `Promise<Transcript>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | Video id |
|
||||
|
||||
<a name="download"></a>
|
||||
### `download(video_id, options?)`
|
||||
Downloads a given video.
|
||||
|
||||
@@ -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.4.0",
|
||||
"version": "6.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "6.4.0",
|
||||
"version": "6.4.1",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "6.4.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",
|
||||
|
||||
@@ -14,8 +14,6 @@ import NotificationsMenu from './parser/youtube/NotificationsMenu.js';
|
||||
import Playlist from './parser/youtube/Playlist.js';
|
||||
import Search from './parser/youtube/Search.js';
|
||||
import VideoInfo from './parser/youtube/VideoInfo.js';
|
||||
import ContinuationItem from './parser/classes/ContinuationItem.js';
|
||||
import Transcript from './parser/classes/Transcript.js';
|
||||
|
||||
import { Kids, Music, Studio } from './core/clients/index.js';
|
||||
import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js';
|
||||
@@ -38,7 +36,7 @@ import {
|
||||
import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.js';
|
||||
|
||||
import type { ApiResponse } from './core/Actions.js';
|
||||
import { type IGetTranscriptResponse, type IBrowseResponse, type 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';
|
||||
|
||||
@@ -334,35 +332,6 @@ export default class Innertube {
|
||||
return info.chooseFormat(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a video's transcript.
|
||||
* @param video_id - The video id.
|
||||
*/
|
||||
async getTranscript(video_id: string): Promise<Transcript> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const next_response = await this.actions.execute(NextEndpoint.PATH, { ...NextEndpoint.build({ video_id }), parse: true });
|
||||
|
||||
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 transcript_response = await transcript_continuation.endpoint.call<IGetTranscriptResponse>(this.actions, { parse: true });
|
||||
|
||||
return transcript_response.actions_memo.getType(Transcript).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a given video. If you only need the direct download link see {@link getStreamingData}.
|
||||
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -5,18 +5,24 @@ import HorizontalCardList from './HorizontalCardList.js';
|
||||
import VideoDescriptionHeader from './VideoDescriptionHeader.js';
|
||||
import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js';
|
||||
import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js';
|
||||
import type VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.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 | VideoDescriptionTranscriptSection | HorizontalCardList
|
||||
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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
import type { ObservedArray} from '../helpers.js';
|
||||
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>;
|
||||
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);
|
||||
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;
|
||||
|
||||
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 { 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';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -328,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';
|
||||
@@ -359,6 +360,7 @@ 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';
|
||||
@@ -371,6 +373,7 @@ 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';
|
||||
|
||||
@@ -367,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;
|
||||
}
|
||||
|
||||
@@ -598,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[]) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user