Compare commits

..

8 Commits

Author SHA1 Message Date
github-actions[bot]
e54c0c4bf1 chore(main): release 6.4.1 (#507)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-10-02 00:04:10 -03:00
LuanRT
8e372d5c67 fix(Feed): Do not throw when multiple continuations are present 2023-10-01 23:39:54 -03:00
LuanRT
987f50604a fix(Playlist): Throw a more helpful error when parsing empty responses 2023-10-01 23:31:05 -03:00
Luan
69702085c6 refactor: Move transcript logic to MediaInfo (#511)
* refactor: Move transcript logic to `MediaInfo`

+ Add support for retrieving different languages.

* docs: Update and add examples
2023-09-17 22:17:14 -03:00
absidue
d2959b3a55 perf: Cache deciphered n-params by info response (#505) 2023-09-17 18:52:32 -03:00
absidue
68df321858 perf(generator): Remove duplicate checks in isMiscType (#506) 2023-09-15 15:25:08 -03:00
Ryan Sandbach
f4bc8508d0 chore(docs): Minor update (#502)
* Updated documentation example to matche syntax of existing parsers.

* Changed from require since package is setup as module.
2023-09-12 03:49:36 -03:00
LuanRT
e216124bb0 chore: update docs 2023-09-10 02:14:14 -03:00
22 changed files with 298 additions and 114 deletions

View File

@@ -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)

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,4 +1,4 @@
const { Innertube, UniversalCache } = require('youtubei.js');
import { Innertube, UniversalCache } from 'youtubei.js';
(async () => {
const yt = await Innertube.create({

View 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
View File

@@ -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"
],

View File

@@ -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",

View File

@@ -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}.

View File

@@ -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);

View File

@@ -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 });

View File

@@ -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.
*/

View File

@@ -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
]);
}
}

View 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;
}
}

View 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);
}
}

View File

@@ -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;

View 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 ]);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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[]) {

View File

@@ -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;
}

View 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;
}
}