mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-25 07:42:11 +00:00
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
This commit is contained in:
@@ -292,7 +292,7 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<info>#chooseFormat(options)`
|
||||
- Used to choose streaming data formats.
|
||||
|
||||
- `<info>#toDash(url_transformer)`
|
||||
- `<info>#toDash(url_transformer?, format_filter?)`
|
||||
- Converts streaming data to an MPEG-DASH manifest.
|
||||
|
||||
- `<info>#download(options)`
|
||||
|
||||
@@ -47,7 +47,7 @@ Retrieves video info.
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
|
||||
- `<info>#toDash(url_transformer?)`
|
||||
- `<info>#toDash(url_transformer?, format_filter?)`
|
||||
- Generates a DASH manifest from the streaming data.
|
||||
|
||||
- `<info>#chooseFormat(options)`
|
||||
|
||||
@@ -49,7 +49,7 @@ Retrieves track info.
|
||||
- `<info>#available_tabs`
|
||||
- Returns available tabs.
|
||||
|
||||
- `<info>#toDash(url_transformer?)`
|
||||
- `<info>#toDash(url_transformer?, format_filter?)`
|
||||
- Generates a DASH manifest from the streaming data.
|
||||
|
||||
- `<info>#chooseFormat(options)`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user