Skip to content

Commit

Permalink
feat: Add support for retrieving transcripts (#500)
Browse files Browse the repository at this point in the history
* feat: Add support for retrieving transcripts

* chore: lint

* chore: update docs

* chore: Do not include nodes in errors thrown

* chore: Improve error messages

* fix(ExpandableMetadata): `expanded_content` type mismatch

* chore: lint
  • Loading branch information
LuanRT authored Sep 10, 2023
1 parent 86fb33e commit f94ea6c
Show file tree
Hide file tree
Showing 24 changed files with 387 additions and 25 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,16 @@ 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.
Expand Down
33 changes: 32 additions & 1 deletion src/Innertube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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';
Expand All @@ -36,7 +38,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 IGetTranscriptResponse, type IBrowseResponse, type IParsedResponse } from './parser/types/index.js';
import type { INextRequest } from './types/index.js';
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.js';

Expand Down Expand Up @@ -332,6 +334,35 @@ 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}.
Expand Down
7 changes: 4 additions & 3 deletions src/parser/classes/EngagementPanelSectionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ 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';

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;

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;
}
}
}
5 changes: 3 additions & 2 deletions src/parser/classes/ExpandableMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

Expand All @@ -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);
}
Expand Down
16 changes: 16 additions & 0 deletions src/parser/classes/FancyDismissibleDialog.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions src/parser/classes/ProductList.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions src/parser/classes/ProductListHeader.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions src/parser/classes/ProductListItem.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 5 additions & 1 deletion src/parser/classes/StructuredDescriptionContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ 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';

export default class StructuredDescriptionContent extends YTNode {
static type = 'StructuredDescriptionContent';

items: ObservedArray<VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection | VideoDescriptionInfocardsSection | HorizontalCardList>;
items: ObservedArray<
VideoDescriptionHeader | ExpandableVideoDescriptionBody | VideoDescriptionMusicSection |
VideoDescriptionInfocardsSection | VideoDescriptionTranscriptSection | HorizontalCardList
>;

constructor(data: RawNode) {
super();
Expand Down
15 changes: 15 additions & 0 deletions src/parser/classes/Transcript.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions src/parser/classes/TranscriptFooter.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions src/parser/classes/TranscriptSearchBox.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions src/parser/classes/TranscriptSearchPanel.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
22 changes: 22 additions & 0 deletions src/parser/classes/TranscriptSegment.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
23 changes: 23 additions & 0 deletions src/parser/classes/TranscriptSegmentList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 TranscriptSegment from './TranscriptSegment.js';

export default class TranscriptSegmentList extends YTNode {
static type = 'TranscriptSegmentList';

initial_segments: ObservedArray<TranscriptSegment>;
no_result_label: Text;
retry_label: Text;
touch_captions_enabled: boolean;

constructor(data: RawNode) {
super();
this.initial_segments = Parser.parseArray(data.initialSegments, TranscriptSegment);
this.no_result_label = new Text(data.noResultLabel);
this.retry_label = new Text(data.retryLabel);
this.touch_captions_enabled = data.touchCaptionsEnabled;
}
}
15 changes: 15 additions & 0 deletions src/parser/classes/UploadTimeFactoid.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit f94ea6c

Please sign in to comment.