/** * @fileoverview TranscriptList model representing all available transcripts for a video */ import { HttpClient, TranslationLanguage, CaptionsJson } from '../types'; import { Transcript } from './transcript'; import { NoTranscriptFound } from '../errors'; /** * Represents a list of available transcripts for a video. * This object is iterable and provides methods to search for specific transcripts. * * @example * ```typescript * const transcriptList = await api.list('video_id'); * * // Iterate over all transcripts * for (const transcript of transcriptList) { * console.log(transcript.language, transcript.languageCode); * } * * // Find a specific transcript * const transcript = transcriptList.findTranscript(['de', 'en']); * ``` */ export class TranscriptList implements Iterable { /** The video ID this list belongs to */ public readonly videoId: string; private manuallyCreatedTranscripts: Record; private autoGeneratedTranscripts: Record; private availableTranslationLanguages: TranslationLanguage[]; /** * Creates a new TranscriptList instance. * The constructor is only for internal use. Use the static buildFromCaptionsJson method instead. * * @param {string} videoId - The video ID * @param {Record} manuallyCreatedTranscripts - Map of manually created transcripts * @param {Record} autoGeneratedTranscripts - Map of auto-generated transcripts * @param {TranslationLanguage[]} availableTranslationLanguages - Available translation languages */ constructor( videoId: string, manuallyCreatedTranscripts: Record, autoGeneratedTranscripts: Record, availableTranslationLanguages: TranslationLanguage[] ) { this.videoId = videoId; this.manuallyCreatedTranscripts = manuallyCreatedTranscripts; this.autoGeneratedTranscripts = autoGeneratedTranscripts; this.availableTranslationLanguages = availableTranslationLanguages; } /** * Factory method to build a TranscriptList from YouTube's captions JSON data. * * @param {HttpClient} httpClient - HTTP client for making requests * @param {string} videoId - The video ID * @param {CaptionsJson} captionsJson - The JSON parsed from YouTube's response * @returns {TranscriptList} The created TranscriptList */ static buildFromCaptionsJson( httpClient: HttpClient, videoId: string, captionsJson: CaptionsJson ): TranscriptList { const translationLanguages = TranscriptList.extractTranslationLanguages(captionsJson); const { manualTranscripts, generatedTranscripts } = TranscriptList.categorizeTranscripts( httpClient, videoId, captionsJson, translationLanguages ); return new TranscriptList(videoId, manualTranscripts, generatedTranscripts, translationLanguages); } /** * Extracts translation languages from the captions JSON. * * @private * @param {CaptionsJson} captionsJson - The captions JSON data * @returns {TranslationLanguage[]} Array of translation languages */ private static extractTranslationLanguages(captionsJson: CaptionsJson): TranslationLanguage[] { return (captionsJson.translationLanguages || []).map(lang => ({ language: lang.languageName.runs[0].text, language_code: lang.languageCode })); } /** * Categorizes transcripts into manually created and auto-generated. * * @private * @param {HttpClient} httpClient - HTTP client for making requests * @param {string} videoId - The video ID * @param {CaptionsJson} captionsJson - The captions JSON data * @param {TranslationLanguage[]} translationLanguages - Available translation languages * @returns {{manualTranscripts: Record, generatedTranscripts: Record}} */ private static categorizeTranscripts( httpClient: HttpClient, videoId: string, captionsJson: CaptionsJson, translationLanguages: TranslationLanguage[] ): { manualTranscripts: Record; generatedTranscripts: Record; } { const manualTranscripts: Record = {}; const generatedTranscripts: Record = {}; for (const captionTrack of captionsJson.captionTracks) { const isAutoGenerated = captionTrack.kind === 'asr'; const targetMap = isAutoGenerated ? generatedTranscripts : manualTranscripts; // Remove format parameter from URL for better compatibility const cleanedUrl = captionTrack.baseUrl.replace('&fmt=srv3', ''); const transcript = new Transcript( httpClient, videoId, cleanedUrl, captionTrack.name.runs[0].text, captionTrack.languageCode, isAutoGenerated, captionTrack.isTranslatable ? translationLanguages : [] ); targetMap[captionTrack.languageCode] = transcript; } return { manualTranscripts, generatedTranscripts }; } /** * Makes the transcript list iterable, allowing for...of loops. * Manual transcripts are yielded first, followed by auto-generated transcripts. * * @returns {Iterator} Iterator over all transcripts */ [Symbol.iterator](): Iterator { const manualTranscripts = Object.values(this.manuallyCreatedTranscripts); const generatedTranscripts = Object.values(this.autoGeneratedTranscripts); return [...manualTranscripts, ...generatedTranscripts][Symbol.iterator](); } /** * Finds a transcript for given language codes. Manually created transcripts are * returned first and only if none are found, auto-generated transcripts are used. * * @param {string[]} languageCodes - A list of language codes in descending priority * @returns {Transcript} The found transcript * @throws {NoTranscriptFound} If no transcript is found for any of the language codes * * @example * ```typescript * // Try German first, then English * const transcript = transcriptList.findTranscript(['de', 'en']); * ``` */ findTranscript(languageCodes: string[]): Transcript { return this.findTranscriptInMaps(languageCodes, [ this.manuallyCreatedTranscripts, this.autoGeneratedTranscripts ]); } /** * Finds an automatically generated transcript for given language codes. * * @param {string[]} languageCodes - A list of language codes in descending priority * @returns {Transcript} The found auto-generated transcript * @throws {NoTranscriptFound} If no auto-generated transcript is found * * @example * ```typescript * const transcript = transcriptList.findGeneratedTranscript(['en']); * ``` */ findGeneratedTranscript(languageCodes: string[]): Transcript { return this.findTranscriptInMaps(languageCodes, [this.autoGeneratedTranscripts]); } /** * Finds a manually created transcript for given language codes. * * @param {string[]} languageCodes - A list of language codes in descending priority * @returns {Transcript} The found manually created transcript * @throws {NoTranscriptFound} If no manually created transcript is found * * @example * ```typescript * const transcript = transcriptList.findManuallyCreatedTranscript(['en']); * ``` */ findManuallyCreatedTranscript(languageCodes: string[]): Transcript { return this.findTranscriptInMaps(languageCodes, [this.manuallyCreatedTranscripts]); } /** * Internal method to find a transcript by searching through provided maps. * * @private * @param {string[]} languageCodes - Language codes to search for * @param {Record[]} transcriptMaps - Maps to search through * @returns {Transcript} The found transcript * @throws {NoTranscriptFound} If no transcript is found */ private findTranscriptInMaps( languageCodes: string[], transcriptMaps: Record[] ): Transcript { for (const languageCode of languageCodes) { for (const transcriptMap of transcriptMaps) { if (languageCode in transcriptMap) { return transcriptMap[languageCode]; } } } throw new NoTranscriptFound(this.videoId, languageCodes, this); } /** * Returns a formatted string representation of all available transcripts. * * @returns {string} A formatted string showing all available transcripts */ toString(): string { const formatTranscriptMap = (transcripts: Record): string => { const descriptions = Object.values(transcripts).map(t => ` - ${t.toString()}`); return descriptions.length > 0 ? descriptions.join('\n') : 'None'; }; const formatTranslationLanguages = (): string => { const descriptions = this.availableTranslationLanguages.map( lang => ` - ${lang.language_code} ("${lang.language}")` ); return descriptions.length > 0 ? descriptions.join('\n') : 'None'; }; return ( `For this video (${this.videoId}) transcripts are available in the following languages:\n\n` + `(MANUALLY CREATED)\n${formatTranscriptMap(this.manuallyCreatedTranscripts)}\n\n` + `(GENERATED)\n${formatTranscriptMap(this.autoGeneratedTranscripts)}\n\n` + `(TRANSLATION LANGUAGES)\n${formatTranslationLanguages()}` ); } }