mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 01:22:11 +00:00
refactor: Move transcript logic to MediaInfo (#511)
* refactor: Move transcript logic to `MediaInfo` + Add support for retrieving different languages. * docs: Update and add examples
This commit is contained in:
14
README.md
14
README.md
@@ -278,7 +278,6 @@ const yt = await Innertube.create({
|
||||
* [.getPlaylist(id)](#getplaylist)
|
||||
* [.getHashtag(hashtag)](#gethashtag)
|
||||
* [.getStreamingData(video_id, options)](#getstreamingdata)
|
||||
* [.getTranscript(video_id)](#gettranscript)
|
||||
* [.download(video_id, options?)](#download)
|
||||
* [.resolveURL(url)](#resolveurl)
|
||||
* [.call(endpoint, args?)](#call)
|
||||
@@ -326,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.
|
||||
|
||||
@@ -661,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.
|
||||
|
||||
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.`);
|
||||
})();
|
||||
@@ -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,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 ]);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
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