From d6c5a9b971444d0cd746aaf5310d3389793680ea Mon Sep 17 00:00:00 2001 From: LuanRT Date: Fri, 27 Jan 2023 00:42:20 -0300 Subject: [PATCH] feat: improve support for dubbed content (#293) * feat(Format): add `language`, `is_dubbed` and `is_original` * feat: add a format filtering option to the DASH function > And a simple language option to VideoInfo's download method. * chore: update docs * feat: improve audio track info parsing * feat(Format): parse `audioTrack` prop --- README.md | 2 +- docs/API/kids.md | 2 +- docs/API/music.md | 2 +- src/parser/classes/PlayerCaptionsTracklist.ts | 14 ++++++ src/parser/classes/misc/Format.ts | 25 +++++++++++ src/parser/youtube/VideoInfo.ts | 8 ++-- src/parser/ytkids/VideoInfo.ts | 7 +-- src/parser/ytmusic/TrackInfo.ts | 7 +-- src/utils/FormatUtils.ts | 44 +++++++++++++++---- 9 files changed, 90 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index dae71bbd..542f4d70 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Retrieves video info, including playback data and even layout elements such as m - `#chooseFormat(options)` - Used to choose streaming data formats. -- `#toDash(url_transformer)` +- `#toDash(url_transformer?, format_filter?)` - Converts streaming data to an MPEG-DASH manifest. - `#download(options)` diff --git a/docs/API/kids.md b/docs/API/kids.md index 42913ea1..4a02e6f5 100644 --- a/docs/API/kids.md +++ b/docs/API/kids.md @@ -47,7 +47,7 @@ Retrieves video info. Methods & Getters

-- `#toDash(url_transformer?)` +- `#toDash(url_transformer?, format_filter?)` - Generates a DASH manifest from the streaming data. - `#chooseFormat(options)` diff --git a/docs/API/music.md b/docs/API/music.md index 8368b900..94fe4e45 100644 --- a/docs/API/music.md +++ b/docs/API/music.md @@ -49,7 +49,7 @@ Retrieves track info. - `#available_tabs` - Returns available tabs. -- `#toDash(url_transformer?)` +- `#toDash(url_transformer?, format_filter?)` - Generates a DASH manifest from the streaming data. - `#chooseFormat(options)` diff --git a/src/parser/classes/PlayerCaptionsTracklist.ts b/src/parser/classes/PlayerCaptionsTracklist.ts index 99d089e2..0efea8c8 100644 --- a/src/parser/classes/PlayerCaptionsTracklist.ts +++ b/src/parser/classes/PlayerCaptionsTracklist.ts @@ -14,9 +14,16 @@ class PlayerCaptionsTracklist extends YTNode { }[]; audio_tracks: { + audio_track_id: string; + captions_initial_state: string; + default_caption_track_index: number; + has_default_track: boolean; + visibility: string; caption_track_indices: number; }[]; + default_audio_track_index: number; + translation_languages: { language_code: string; language_name: Text; @@ -34,9 +41,16 @@ class PlayerCaptionsTracklist extends YTNode { })); this.audio_tracks = data.audioTracks.map((at: any) => ({ + audio_track_id: at.audioTrackId, + captions_initial_state: at.captionsInitialState, + default_caption_track_index: at.defaultCaptionTrackIndex, + has_default_track: at.hasDefaultTrack, + visibility: at.visibility, caption_track_indices: at.captionTrackIndices })); + this.default_audio_track_index = data.defaultAudioTrackIndex; + this.translation_languages = data.translationLanguages.map((tl: any) => ({ language_code: tl.languageCode, language_name: new Text(tl.languageName) diff --git a/src/parser/classes/misc/Format.ts b/src/parser/classes/misc/Format.ts index b3760603..4e215450 100644 --- a/src/parser/classes/misc/Format.ts +++ b/src/parser/classes/misc/Format.ts @@ -28,12 +28,20 @@ class Format { cipher: string | undefined; signature_cipher: string | undefined; audio_quality: string | undefined; + audio_track?: { + audio_is_default: boolean; + display_name: string; + id: string; + }; approx_duration_ms: number; audio_sample_rate: number; audio_channels: number; loudness_db: number; has_audio: boolean; has_video: boolean; + language?: string | null; + is_dubbed?: boolean; + is_original?: boolean; constructor(data: any) { this.itag = data.itag; @@ -68,6 +76,23 @@ class Format { this.loudness_db = data.loudnessDb; this.has_audio = !!data.audioBitrate || !!data.audioQuality; this.has_video = !!data.qualityLabel; + + if (this.has_audio) { + const args = new URLSearchParams(this.cipher || this.signature_cipher); + const url_components = new URLSearchParams(args.get('url') || this.url); + + this.language = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('lang='))?.split('=').at(1) || null; + this.is_dubbed = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'dubbed'; + this.is_original = url_components.get('xtags')?.split(':').find((x: string) => x.startsWith('acont='))?.split('=').at(1) === 'original' || !this.is_dubbed; + + if (data.audioTrack) { + this.audio_track = { + audio_is_default: data.audioTrack.audioIsDefault, + display_name: data.audioTrack.displayName, + id: data.audioTrack.id + }; + } + } } /** diff --git a/src/parser/youtube/VideoInfo.ts b/src/parser/youtube/VideoInfo.ts index 920267b3..3509b3d8 100644 --- a/src/parser/youtube/VideoInfo.ts +++ b/src/parser/youtube/VideoInfo.ts @@ -34,7 +34,8 @@ import type Actions from '../../core/Actions'; import type { ApiResponse } from '../../core/Actions'; import type { ObservedArray, YTNode } from '../helpers'; -import FormatUtils, { FormatOptions, DownloadOptions, URLTransformer } from '../../utils/FormatUtils'; +import FormatUtils, { FormatOptions, DownloadOptions, URLTransformer, FormatFilter } from '../../utils/FormatUtils'; + import { InnertubeError } from '../../utils/Utils'; class VideoInfo { @@ -308,10 +309,11 @@ class VideoInfo { /** * Generates a DASH manifest from the streaming data. * @param url_transformer - Function to transform the URLs. + * @param format_filter - Function to filter the formats. * @returns DASH manifest */ - toDash(url_transformer: URLTransformer = (url) => url): string { - return FormatUtils.toDash(this.streaming_data, url_transformer); + toDash(url_transformer: URLTransformer = (url) => url, format_filter: FormatFilter): string { + return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#player); } /** diff --git a/src/parser/ytkids/VideoInfo.ts b/src/parser/ytkids/VideoInfo.ts index 16c945cc..fe880499 100644 --- a/src/parser/ytkids/VideoInfo.ts +++ b/src/parser/ytkids/VideoInfo.ts @@ -14,7 +14,7 @@ import type { ObservedArray, YTNode } from '../helpers'; import { Constants } from '../../utils'; import { InnertubeError } from '../../utils/Utils'; -import FormatUtils, { DownloadOptions, FormatOptions, URLTransformer } from '../../utils/FormatUtils'; +import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils'; class VideoInfo { #page: [ParsedResponse, ParsedResponse?]; @@ -69,10 +69,11 @@ class VideoInfo { /** * Generates a DASH manifest from the streaming data. * @param url_transformer - Function to transform the URLs. + * @param format_filter - Function to filter the formats. * @returns DASH manifest */ - toDash(url_transformer: URLTransformer = (url) => url): string { - return FormatUtils.toDash(this.streaming_data, url_transformer, this.#cpn, this.#actions.session.player); + toDash(url_transformer: URLTransformer = (url) => url, format_filter: FormatFilter): string { + return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player); } /** diff --git a/src/parser/ytmusic/TrackInfo.ts b/src/parser/ytmusic/TrackInfo.ts index 26e0402f..27e43eeb 100644 --- a/src/parser/ytmusic/TrackInfo.ts +++ b/src/parser/ytmusic/TrackInfo.ts @@ -25,7 +25,7 @@ import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec'; import type Format from '../classes/misc/Format'; import type { ObservedArray, YTNode } from '../helpers'; -import FormatUtils, { URLTransformer, FormatOptions, DownloadOptions } from '../../utils/FormatUtils'; +import FormatUtils, { URLTransformer, FormatOptions, DownloadOptions, FormatFilter } from '../../utils/FormatUtils'; class TrackInfo { #page: [ ParsedResponse, ParsedResponse? ]; @@ -91,10 +91,11 @@ class TrackInfo { /** * Generates a DASH manifest from the streaming data. * @param url_transformer - Function to transform the URLs. + * @param format_filter - Function to filter the formats. * @returns DASH manifest */ - toDash(url_transformer: URLTransformer = (url) => url): string { - return FormatUtils.toDash(this.streaming_data, url_transformer, this.#cpn, this.#actions.session.player); + toDash(url_transformer: URLTransformer = (url) => url, format_filter: FormatFilter): string { + return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player); } /** diff --git a/src/utils/FormatUtils.ts b/src/utils/FormatUtils.ts index e3fae07e..d081ed62 100644 --- a/src/utils/FormatUtils.ts +++ b/src/utils/FormatUtils.ts @@ -14,6 +14,7 @@ import { Constants } from '.'; import { getStringBetweenStrings, InnertubeError, streamToIterable } from './Utils'; export type URLTransformer = (url: URL) => URL; +export type FormatFilter = (format: Format) => boolean; export interface FormatOptions { /** @@ -24,6 +25,10 @@ export interface FormatOptions { * Download type, can be: video, audio or video+audio */ type?: 'video' | 'audio' | 'video+audio'; + /** + * Language code, defaults to 'original'. + */ + language?: string; /** * File format, use 'any' to download any format */ @@ -187,7 +192,8 @@ class FormatUtils { const requires_audio = options.type ? options.type.includes('audio') : true; const requires_video = options.type ? options.type.includes('video') : true; - const quality = options.quality || '360p'; + const language = options.language || 'original'; + const quality = options.quality || 'best'; let best_width = -1; @@ -208,17 +214,20 @@ class FormatUtils { return true; }); - if (!candidates.length) { - throw new InnertubeError('No matching formats found', { - options - }); - } + if (!candidates.length) + throw new InnertubeError('No matching formats found', { options }); if (is_best && requires_video) candidates = candidates.filter((format) => format.width === best_width); if (requires_audio && !requires_video) { - const audio_only = candidates.filter((format) => !format.has_video); + const audio_only = candidates.filter((format) => { + if (language !== 'original') { + return !format.has_video && format.language === language; + } + return !format.has_video && format.is_original; + + }); if (audio_only.length > 0) { candidates = audio_only; } @@ -241,11 +250,28 @@ class FormatUtils { adaptive_formats: Format[]; dash_manifest_url: string | null; hls_manifest_url: string | null; - }, url_transformer: URLTransformer = (url) => url, cpn?: string, player?: Player): string { + }, url_transformer: URLTransformer = (url) => url, format_filter?: FormatFilter, cpn?: string, player?: Player): string { if (!streaming_data) throw new InnertubeError('Streaming data not available'); - const { adaptive_formats } = streaming_data; + let filtered_streaming_data; + + if (format_filter) { + filtered_streaming_data = { + formats: streaming_data.formats.filter((fmt: Format) => !(format_filter(fmt))), + adaptive_formats: streaming_data.adaptive_formats.filter((fmt: Format) => !(format_filter(fmt))), + expires: streaming_data.expires, + dash_manifest_url: streaming_data.dash_manifest_url, + hls_manifest_url: streaming_data.hls_manifest_url + }; + } else { + filtered_streaming_data = streaming_data; + } + + const { adaptive_formats } = filtered_streaming_data; + + if (!adaptive_formats.length) + throw new InnertubeError('No adaptive formats found'); const length = adaptive_formats[0].approx_duration_ms / 1000;