Skip to content

Commit

Permalink
feat: improve support for dubbed content (#293)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
LuanRT authored Jan 27, 2023
1 parent 0fc29f0 commit d6c5a9b
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
2 changes: 1 addition & 1 deletion docs/API/kids.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
2 changes: 1 addition & 1 deletion docs/API/music.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
14 changes: 14 additions & 0 deletions src/parser/classes/PlayerCaptionsTracklist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions src/parser/classes/misc/Format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
};
}
}
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/parser/youtube/VideoInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/parser/ytkids/VideoInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?];
Expand Down Expand Up @@ -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);
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/parser/ytmusic/TrackInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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? ];
Expand Down Expand Up @@ -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);
}

/**
Expand Down
44 changes: 35 additions & 9 deletions src/utils/FormatUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;

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

Expand Down

0 comments on commit d6c5a9b

Please sign in to comment.