From 648488641d5779c7b67c92b782b84b29e2be984b Mon Sep 17 00:00:00 2001 From: LuanRT Date: Thu, 25 Jan 2024 22:05:21 +0000 Subject: [PATCH] chore: v9.0.0 release --- deno/package.json | 2 +- deno/src/Innertube.ts | 62 +++-- deno/src/core/Actions.ts | 2 +- deno/src/core/OAuth.ts | 9 +- deno/src/core/Player.ts | 35 ++- deno/src/core/Session.ts | 77 +++--- deno/src/core/clients/Kids.ts | 11 +- deno/src/core/clients/Music.ts | 28 +- deno/src/core/clients/Studio.ts | 8 +- deno/src/core/managers/AccountManager.ts | 5 +- deno/src/core/managers/InteractionManager.ts | 4 +- deno/src/core/managers/PlaylistManager.ts | 10 +- deno/src/core/mixins/Feed.ts | 15 +- deno/src/core/mixins/FilterableFeed.ts | 7 +- deno/src/core/mixins/MediaInfo.ts | 33 +-- deno/src/core/mixins/TabbedFeed.ts | 7 +- deno/src/parser/classes/AttributionView.ts | 17 ++ deno/src/parser/classes/AvatarView.ts | 20 +- deno/src/parser/classes/Channel.ts | 3 + .../classes/ChannelAboutFullMetadata.ts | 3 + deno/src/parser/classes/ChannelVideoPlayer.ts | 3 + .../src/parser/classes/DecoratedAvatarView.ts | 12 +- .../parser/classes/DescriptionPreviewView.ts | 47 ++++ deno/src/parser/classes/DynamicTextView.ts | 7 +- deno/src/parser/classes/ImageBannerView.ts | 16 ++ .../parser/classes/ModalWithTitleAndButton.ts | 19 ++ deno/src/parser/classes/NavigationEndpoint.ts | 11 + deno/src/parser/classes/PageHeaderView.ts | 9 + .../parser/classes/PlayerCaptionsTracklist.ts | 2 +- .../classes/PlayerLiveStoryboardSpec.ts | 27 +- deno/src/parser/classes/PlayerMicroformat.ts | 2 + .../parser/classes/PlayerStoryboardSpec.ts | 2 + deno/src/parser/classes/PlaylistVideo.ts | 5 + deno/src/parser/classes/misc/Format.ts | 4 + deno/src/parser/classes/misc/VideoDetails.ts | 6 + deno/src/parser/continuations.ts | 5 +- deno/src/parser/generator.ts | 6 +- deno/src/parser/helpers.ts | 7 +- deno/src/parser/nodes.ts | 4 + deno/src/parser/parser.ts | 50 ++-- deno/src/parser/types/ParsedResponse.ts | 2 +- deno/src/parser/youtube/AccountInfo.ts | 9 +- deno/src/parser/youtube/Analytics.ts | 4 +- deno/src/parser/youtube/Channel.ts | 25 +- deno/src/parser/youtube/Comments.ts | 8 +- deno/src/parser/youtube/Guide.ts | 7 +- deno/src/parser/youtube/HashtagFeed.ts | 9 +- deno/src/parser/youtube/History.ts | 6 +- deno/src/parser/youtube/HomeFeed.ts | 7 +- deno/src/parser/youtube/ItemMenu.ts | 8 +- deno/src/parser/youtube/Library.ts | 6 +- deno/src/parser/youtube/LiveChat.ts | 36 ++- deno/src/parser/youtube/NotificationsMenu.ts | 7 +- deno/src/parser/youtube/Playlist.ts | 50 +++- deno/src/parser/youtube/Search.ts | 6 +- deno/src/parser/youtube/Settings.ts | 6 +- deno/src/parser/youtube/TimeWatched.ts | 4 +- deno/src/parser/youtube/TranscriptInfo.ts | 6 +- deno/src/parser/youtube/VideoInfo.ts | 17 +- deno/src/parser/youtube/index.ts | 1 + deno/src/parser/ytkids/Channel.ts | 6 +- deno/src/parser/ytkids/HomeFeed.ts | 6 +- deno/src/parser/ytkids/Search.ts | 6 +- deno/src/parser/ytkids/VideoInfo.ts | 7 +- deno/src/parser/ytmusic/Album.ts | 6 +- deno/src/parser/ytmusic/Artist.ts | 4 +- deno/src/parser/ytmusic/Explore.ts | 6 +- deno/src/parser/ytmusic/HomeFeed.ts | 11 +- deno/src/parser/ytmusic/Library.ts | 7 +- deno/src/parser/ytmusic/Playlist.ts | 7 +- deno/src/parser/ytmusic/Recap.ts | 10 +- deno/src/parser/ytmusic/Search.ts | 5 +- deno/src/parser/ytmusic/TrackInfo.ts | 18 +- deno/src/parser/ytshorts/VideoInfo.ts | 22 +- deno/src/platform/jsruntime/jinter.ts | 13 +- deno/src/platform/web.ts | 4 +- deno/src/proto/index.ts | 2 +- deno/src/utils/Constants.ts | 2 +- deno/src/utils/DashManifest.js | 24 +- deno/src/utils/DashManifest.tsx | 34 ++- deno/src/utils/FormatUtils.ts | 7 +- deno/src/utils/HTTPClient.ts | 7 +- deno/src/utils/Log.ts | 49 ++++ deno/src/utils/StreamingInfo.ts | 259 ++++++++++++++---- deno/src/utils/Utils.ts | 12 +- deno/src/utils/index.ts | 2 + 86 files changed, 909 insertions(+), 453 deletions(-) create mode 100644 deno/src/parser/classes/AttributionView.ts create mode 100644 deno/src/parser/classes/DescriptionPreviewView.ts create mode 100644 deno/src/parser/classes/ImageBannerView.ts create mode 100644 deno/src/parser/classes/ModalWithTitleAndButton.ts create mode 100644 deno/src/utils/Log.ts diff --git a/deno/package.json b/deno/package.json index e4d224f9..c474205f 100644 --- a/deno/package.json +++ b/deno/package.json @@ -1,6 +1,6 @@ { "name": "youtubei.js", - "version": "8.2.0", + "version": "9.0.0", "description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).", "type": "module", "types": "./dist/src/platform/lib.d.ts", diff --git a/deno/src/Innertube.ts b/deno/src/Innertube.ts index 58e557fc..5ef3bbba 100644 --- a/deno/src/Innertube.ts +++ b/deno/src/Innertube.ts @@ -1,29 +1,8 @@ -import type { SessionOptions } from './core/Session.ts'; import Session from './core/Session.ts'; - -import NavigationEndpoint from './parser/classes/NavigationEndpoint.ts'; -import type Format from './parser/classes/misc/Format.ts'; -import Channel from './parser/youtube/Channel.ts'; -import Comments from './parser/youtube/Comments.ts'; -import Guide from './parser/youtube/Guide.ts'; -import HashtagFeed from './parser/youtube/HashtagFeed.ts'; -import History from './parser/youtube/History.ts'; -import HomeFeed from './parser/youtube/HomeFeed.ts'; -import Library from './parser/youtube/Library.ts'; -import NotificationsMenu from './parser/youtube/NotificationsMenu.ts'; -import Playlist from './parser/youtube/Playlist.ts'; -import Search from './parser/youtube/Search.ts'; -import VideoInfo from './parser/youtube/VideoInfo.ts'; -import ShortsVideoInfo from './parser/ytshorts/VideoInfo.ts'; - import { Kids, Music, Studio } from './core/clients/index.ts'; import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.ts'; import { Feed, TabbedFeed } from './core/mixins/index.ts'; -import * as Proto from './proto/index.ts'; -import * as Constants from './utils/Constants.ts'; -import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts'; - import { BrowseEndpoint, GetNotificationMenuEndpoint, @@ -32,16 +11,39 @@ import { PlayerEndpoint, ResolveURLEndpoint, SearchEndpoint, - Reel + Reel, + Notification } from './core/endpoints/index.ts'; -import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.ts'; +import { + Channel, + Comments, + Guide, + HashtagFeed, + History, + HomeFeed, + Library, + NotificationsMenu, + Playlist, + Search, + VideoInfo +} from './parser/youtube/index.ts'; + +import { VideoInfo as ShortsVideoInfo } from './parser/ytshorts/index.ts'; + +import NavigationEndpoint from './parser/classes/NavigationEndpoint.ts'; + +import * as Proto from './proto/index.ts'; +import * as Constants from './utils/Constants.ts'; +import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts'; + import type { ApiResponse } from './core/Actions.ts'; -import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.ts'; import type { INextRequest } from './types/index.ts'; +import type { IBrowseResponse, IParsedResponse } from './parser/types/index.ts'; import type { DownloadOptions, FormatOptions } from './types/FormatUtils.ts'; -import { encodeReelSequence } from './proto/index.ts'; +import type { SessionOptions } from './core/Session.ts'; +import type Format from './parser/classes/misc/Format.ts'; export type InnertubeConfig = SessionOptions; @@ -151,7 +153,7 @@ export default class Innertube { const sequenceResponse = this.actions.execute( Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({ - sequenceParams: encodeReelSequence(short_id) + sequenceParams: Proto.encodeReelSequence(short_id) }) ); @@ -261,7 +263,7 @@ export default class Innertube { } /** - * Retrieves trending content. + * Retrieves Trending content. */ async getTrending(): Promise> { const response = await this.actions.execute( @@ -271,7 +273,7 @@ export default class Innertube { } /** - * Retrieves subscriptions feed. + * Retrieves Subscriptions feed. */ async getSubscriptionsFeed(): Promise> { const response = await this.actions.execute( @@ -281,7 +283,7 @@ export default class Innertube { } /** - * Retrieves channels feed. + * Retrieves Channels feed. */ async getChannelsFeed(): Promise> { const response = await this.actions.execute( @@ -318,7 +320,7 @@ export default class Innertube { * Retrieves unseen notifications count. */ async getUnseenNotificationsCount(): Promise { - const response = await this.actions.execute(GetUnseenCountEndpoint.PATH); + const response = await this.actions.execute(Notification.GetUnseenCountEndpoint.PATH); // TODO: properly parse this return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0; } diff --git a/deno/src/core/Actions.ts b/deno/src/core/Actions.ts index 165385a5..df74ede9 100644 --- a/deno/src/core/Actions.ts +++ b/deno/src/core/Actions.ts @@ -1,7 +1,7 @@ import { Parser, NavigateAction } from '../parser/index.ts'; import { InnertubeError } from '../utils/Utils.ts'; -import type Session from './Session.ts'; +import type { Session } from './index.ts'; import type { IBrowseResponse, IGetNotificationsMenuResponse, diff --git a/deno/src/core/OAuth.ts b/deno/src/core/OAuth.ts index cb8a9981..0a2dcc5d 100644 --- a/deno/src/core/OAuth.ts +++ b/deno/src/core/OAuth.ts @@ -1,4 +1,4 @@ -import * as Constants from '../utils/Constants.ts'; +import { Log, Constants } from '../utils/index.ts'; import { OAuthError, Platform } from '../utils/Utils.ts'; import type Session from './Session.ts'; @@ -45,6 +45,8 @@ export type OAuthClientIdentity = { }; export default class OAuth { + static TAG = 'OAuth'; + #identity?: Record; #session: Session; #credentials?: Credentials; @@ -250,6 +252,7 @@ export default class OAuth { */ async #getClientIdentity(): Promise { if (this.#credentials?.client_id && this.credentials?.client_secret) { + Log.info(OAuth.TAG, 'Using custom OAuth2 credentials.\n'); return { client_id: this.#credentials.client_id, client_secret: this.credentials.client_secret @@ -264,6 +267,8 @@ export default class OAuth { if (!url_body) throw new OAuthError('Could not obtain script url.', { status: 'FAILED' }); + Log.info(OAuth.TAG, `Got YouTubeTV script URL (${url_body})`); + const script = await this.#session.http.fetch(url_body, { baseURL: Constants.URLS.YT_BASE }); const client_identity = (await script.text()) @@ -275,6 +280,8 @@ export default class OAuth { if (!groups) throw new OAuthError('Could not obtain client identity.', { status: 'FAILED' }); + Log.info(OAuth.TAG, 'OAuth2 credentials retrieved.\n', groups); + return groups; } diff --git a/deno/src/core/Player.ts b/deno/src/core/Player.ts index a9c25fd8..0aac2b97 100644 --- a/deno/src/core/Player.ts +++ b/deno/src/core/Player.ts @@ -1,14 +1,13 @@ +import { Log, Constants } from '../utils/index.ts'; import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.ts'; - -import * as Constants from '../utils/Constants.ts'; - -import type { ICache } from '../types/Cache.ts'; -import type { FetchFunction } from '../types/PlatformShim.ts'; +import type { ICache, FetchFunction } from '../types/index.ts'; /** * Represents YouTube's player script. This is required to decipher signatures. */ export default class Player { + static TAG = 'Player'; + #nsig_sc; #sig_sc; #sig_sc_timestamp; @@ -17,9 +16,7 @@ export default class Player { constructor(signature_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) { this.#nsig_sc = nsig_sc; this.#sig_sc = sig_sc; - this.#sig_sc_timestamp = signature_timestamp; - this.#player_id = player_id; } @@ -34,11 +31,14 @@ export default class Player { const player_id = getStringBetweenStrings(js, 'player\\/', '\\/'); + Log.info(Player.TAG, `Got player id (${player_id}). Checking for cached players..`); + if (!player_id) throw new PlayerError('Failed to get player id'); - // We have the playerID now we can check if we have a cached player + // We have the player id, now we can check if we have a cached player. if (cache) { + Log.info(Player.TAG, 'Found a cached player.'); const cached_player = await Player.fromCache(cache, player_id); if (cached_player) return cached_player; @@ -46,6 +46,8 @@ export default class Player { const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE); + Log.info(Player.TAG, `Could not find any cached player. Will download a new player from ${player_url}.`); + const player_res = await fetch(player_url, { headers: { 'user-agent': getRandomUserAgent('desktop') @@ -59,10 +61,11 @@ export default class Player { const player_js = await player_res.text(); const sig_timestamp = this.extractSigTimestamp(player_js); - const sig_sc = this.extractSigSourceCode(player_js); const nsig_sc = this.extractNSigSourceCode(player_js); + Log.info(Player.TAG, `Got signature timestamp (${sig_timestamp}) and algorithms needed to decipher signatures.`); + return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id); } @@ -80,6 +83,8 @@ export default class Player { sig: args.get('s') }); + Log.info(Player.TAG, `Transformed signature ${args.get('s')} to ${signature}.`); + if (typeof signature !== 'string') throw new PlayerError('Failed to decipher signature'); @@ -102,11 +107,13 @@ export default class Player { nsig: n }); + Log.info(Player.TAG, `Transformed nsig ${n} to ${nsig}.`); + if (typeof nsig !== 'string') throw new PlayerError('Failed to decipher nsig'); if (nsig.startsWith('enhanced_except_')) { - console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!'); + Log.warn(Player.TAG, 'Could not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!'); } else if (this_response_nsig_cache) { this_response_nsig_cache.set(n, nsig); } @@ -138,6 +145,10 @@ export default class Player { break; } + const result = url_components.toString(); + + Log.info(Player.TAG, `Full deciphered URL: ${result}`); + return url_components.toString(); } @@ -204,7 +215,7 @@ export default class Player { const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};'); if (!functions || !calls) - console.warn(new PlayerError('Failed to extract signature decipher algorithm')); + Log.warn(Player.TAG, 'Failed to extract signature decipher algorithm.'); return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`; } @@ -213,7 +224,7 @@ export default class Player { const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`; if (!sc) - console.warn(new PlayerError('Failed to extract n-token decipher algorithm')); + Log.warn(Player.TAG, 'Failed to extract n-token decipher algorithm'); return sc; } diff --git a/deno/src/core/Session.ts b/deno/src/core/Session.ts index 145bc5aa..9889d98f 100644 --- a/deno/src/core/Session.ts +++ b/deno/src/core/Session.ts @@ -1,16 +1,19 @@ +import { Player, OAuth, Actions } from './index.ts'; +import { Log, EventEmitter, HTTPClient } from '../utils/index.ts'; import * as Constants from '../utils/Constants.ts'; -import EventEmitterLike from '../utils/EventEmitterLike.ts'; -import Actions from './Actions.ts'; -import Player from './Player.ts'; - import * as Proto from '../proto/index.ts'; -import type { ICache } from '../types/Cache.ts'; -import type { FetchFunction } from '../types/PlatformShim.ts'; -import HTTPClient from '../utils/HTTPClient.ts'; + +import { + generateRandomString, getRandomUserAgent, + InnertubeError, Platform, SessionError +} from '../utils/Utils.ts'; + import type { DeviceCategory } from '../utils/Utils.ts'; -import { generateRandomString, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.ts'; -import type { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.ts'; -import OAuth from './OAuth.ts'; +import type { FetchFunction, ICache } from '../types/index.ts'; +import type { + Credentials, OAuthAuthErrorEventHandler, + OAuthAuthEventHandler, OAuthAuthPendingEventHandler +} from './OAuth.ts'; export enum ClientType { WEB = 'WEB', @@ -140,10 +143,23 @@ export interface SessionData { api_version: string; } +export type SessionArgs = { + lang: string; + location: string; + time_zone: string; + device_category: DeviceCategory; + client_name: ClientType; + enable_safety_mode: boolean; + visitor_data: string; + on_behalf_of_user: string | undefined; +} + /** * Represents an InnerTube session. This holds all the data needed to make requests to YouTube. */ -export default class Session extends EventEmitterLike { +export default class Session extends EventEmitter { + static TAG = 'Session'; + #api_version: string; #key: string; #context: Context; @@ -226,6 +242,8 @@ export default class Session extends EventEmitterLike { const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user }; + Log.info(Session.TAG, 'Retrieving InnerTube session.'); + if (generate_session_locally) { session_data = this.#generateSessionData(session_args); } else { @@ -233,30 +251,29 @@ export default class Session extends EventEmitterLike { // This can fail if the data changes or the request is blocked for some reason. session_data = await this.#retrieveSessionData(session_args, fetch); } catch (err) { + Log.error(Session.TAG, 'Failed to retrieve session data from server. Will try to generate it locally.'); session_data = this.#generateSessionData(session_args); } } + Log.info(Session.TAG, 'Got session data.\n', session_data); + return { ...session_data, account_index }; } - static async #retrieveSessionData(options: { - lang: string; - location: string; - time_zone: string; - device_category: string; - client_name: string; - enable_safety_mode: boolean; - visitor_data: string; - on_behalf_of_user?: string; - }, fetch: FetchFunction = Platform.shim.fetch): Promise { + static #getVisitorID(visitor_data: string) { + const decoded_visitor_data = Proto.decodeVisitorData(visitor_data); + Log.info(Session.TAG, 'Custom visitor data decoded successfully.\n', decoded_visitor_data); + return decoded_visitor_data.id; + } + + static async #retrieveSessionData(options: SessionArgs, fetch: FetchFunction = Platform.shim.fetch): Promise { const url = new URL('/sw.js_data', Constants.URLS.YT_BASE); let visitor_id = generateRandomString(11); if (options.visitor_data) { - const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data); - visitor_id = decoded_visitor_data.id; + visitor_id = this.#getVisitorID(options.visitor_data); } const res = await fetch(url, { @@ -316,21 +333,11 @@ export default class Session extends EventEmitterLike { return { context, api_key, api_version }; } - static #generateSessionData(options: { - lang: string; - location: string; - time_zone: string; - device_category: DeviceCategory; - client_name: string; - enable_safety_mode: boolean; - visitor_data: string; - on_behalf_of_user?: string; - }): SessionData { + static #generateSessionData(options: SessionArgs): SessionData { let visitor_id = generateRandomString(11); if (options.visitor_data) { - const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data); - visitor_id = decoded_visitor_data.id; + visitor_id = this.#getVisitorID(options.visitor_data); } const context: Context = { diff --git a/deno/src/core/clients/Kids.ts b/deno/src/core/clients/Kids.ts index 170b9bf4..d43ebc54 100644 --- a/deno/src/core/clients/Kids.ts +++ b/deno/src/core/clients/Kids.ts @@ -1,12 +1,7 @@ import { Parser } from '../../parser/index.ts'; -import Channel from '../../parser/ytkids/Channel.ts'; -import HomeFeed from '../../parser/ytkids/HomeFeed.ts'; -import Search from '../../parser/ytkids/Search.ts'; -import VideoInfo from '../../parser/ytkids/VideoInfo.ts'; -import type Session from '../Session.ts'; -import { type ApiResponse } from '../Actions.ts'; - +import { Channel, HomeFeed, Search, VideoInfo } from '../../parser/ytkids/index.ts'; import { InnertubeError, generateRandomString } from '../../utils/Utils.ts'; +import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.ts'; import { BrowseEndpoint, NextEndpoint, @@ -15,7 +10,7 @@ import { import { BlocklistPickerEndpoint } from '../endpoints/kids/index.ts'; -import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.ts'; +import type { Session, ApiResponse } from '../index.ts'; export default class Kids { #session: Session; diff --git a/deno/src/core/clients/Music.ts b/deno/src/core/clients/Music.ts index de1faa9b..ede36a0d 100644 --- a/deno/src/core/clients/Music.ts +++ b/deno/src/core/clients/Music.ts @@ -1,12 +1,11 @@ -import Album from '../../parser/ytmusic/Album.ts'; -import Artist from '../../parser/ytmusic/Artist.ts'; -import Explore from '../../parser/ytmusic/Explore.ts'; -import HomeFeed from '../../parser/ytmusic/HomeFeed.ts'; -import Library from '../../parser/ytmusic/Library.ts'; -import Playlist from '../../parser/ytmusic/Playlist.ts'; -import Recap from '../../parser/ytmusic/Recap.ts'; -import Search from '../../parser/ytmusic/Search.ts'; -import TrackInfo from '../../parser/ytmusic/TrackInfo.ts'; +import * as Proto from '../../proto/index.ts'; +import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.ts'; + +import { + Album, Artist, Explore, + HomeFeed, Library, Playlist, + Recap, Search, TrackInfo +} from '../../parser/ytmusic/index.ts'; import AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.ts'; import Message from '../../parser/classes/Message.ts'; @@ -18,13 +17,6 @@ import PlaylistPanel from '../../parser/classes/PlaylistPanel.ts'; import SearchSuggestionsSection from '../../parser/classes/SearchSuggestionsSection.ts'; import SectionList from '../../parser/classes/SectionList.ts'; import Tab from '../../parser/classes/Tab.ts'; -import * as Proto from '../../proto/index.ts'; - -import type { ObservedArray } from '../../parser/helpers.ts'; -import type { MusicSearchFilters } from '../../types/index.ts'; -import { InnertubeError, generateRandomString, throwIfMissing } from '../../utils/Utils.ts'; -import type Actions from '../Actions.ts'; -import type Session from '../Session.ts'; import { BrowseEndpoint, @@ -35,6 +27,10 @@ import { import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.ts'; +import type { ObservedArray } from '../../parser/helpers.ts'; +import type { MusicSearchFilters } from '../../types/index.ts'; +import type { Actions, Session } from '../index.ts'; + export default class Music { #session: Session; #actions: Actions; diff --git a/deno/src/core/clients/Studio.ts b/deno/src/core/clients/Studio.ts index 32aa32a7..c7e543fe 100644 --- a/deno/src/core/clients/Studio.ts +++ b/deno/src/core/clients/Studio.ts @@ -1,12 +1,10 @@ import * as Proto from '../../proto/index.ts'; -import * as Constants from '../../utils/Constants.ts'; +import { Constants } from '../../utils/index.ts'; import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.ts'; +import { CreateVideoEndpoint } from '../endpoints/upload/index.ts'; import type { UpdateVideoMetadataOptions, UploadedVideoMetadataOptions } from '../../types/Clients.ts'; -import type { ApiResponse } from '../Actions.ts'; -import type Session from '../Session.ts'; - -import { CreateVideoEndpoint } from '../endpoints/upload/index.ts'; +import type { ApiResponse, Session } from '../index.ts'; interface UploadResult { status: string; diff --git a/deno/src/core/managers/AccountManager.ts b/deno/src/core/managers/AccountManager.ts index 801523fa..1c3fa564 100644 --- a/deno/src/core/managers/AccountManager.ts +++ b/deno/src/core/managers/AccountManager.ts @@ -1,3 +1,5 @@ +import type { Actions, ApiResponse } from '../index.ts'; + import AccountInfo from '../../parser/youtube/AccountInfo.ts'; import Analytics from '../../parser/youtube/Analytics.ts'; import Settings from '../../parser/youtube/Settings.ts'; @@ -7,9 +9,6 @@ import * as Proto from '../../proto/index.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import { Account, BrowseEndpoint, Channel } from '../endpoints/index.ts'; -import type Actions from '../Actions.ts'; -import type { ApiResponse } from '../Actions.ts'; - export default class AccountManager { #actions: Actions; diff --git a/deno/src/core/managers/InteractionManager.ts b/deno/src/core/managers/InteractionManager.ts index 854535bb..bce21e77 100644 --- a/deno/src/core/managers/InteractionManager.ts +++ b/deno/src/core/managers/InteractionManager.ts @@ -1,6 +1,4 @@ import * as Proto from '../../proto/index.ts'; -import type Actions from '../Actions.ts'; -import type { ApiResponse } from '../Actions.ts'; import { throwIfMissing } from '../../utils/Utils.ts'; import { LikeEndpoint, DislikeEndpoint, RemoveLikeEndpoint } from '../endpoints/like/index.ts'; @@ -8,6 +6,8 @@ import { SubscribeEndpoint, UnsubscribeEndpoint } from '../endpoints/subscriptio import { CreateCommentEndpoint, PerformCommentActionEndpoint } from '../endpoints/comment/index.ts'; import { ModifyChannelPreferenceEndpoint } from '../endpoints/notification/index.ts'; +import type { Actions, ApiResponse } from '../index.ts'; + export default class InteractionManager { #actions: Actions; diff --git a/deno/src/core/managers/PlaylistManager.ts b/deno/src/core/managers/PlaylistManager.ts index df7f8b4c..51c70b1f 100644 --- a/deno/src/core/managers/PlaylistManager.ts +++ b/deno/src/core/managers/PlaylistManager.ts @@ -1,12 +1,12 @@ -import Playlist from '../../parser/youtube/Playlist.ts'; -import type Actions from '../Actions.ts'; -import type Feed from '../mixins/Feed.ts'; - -import type { EditPlaylistEndpointOptions } from '../../types/index.ts'; import { InnertubeError, throwIfMissing } from '../../utils/Utils.ts'; import { EditPlaylistEndpoint } from '../endpoints/browse/index.ts'; import { BrowseEndpoint } from '../endpoints/index.ts'; import { CreateEndpoint, DeleteEndpoint } from '../endpoints/playlist/index.ts'; +import Playlist from '../../parser/youtube/Playlist.ts'; + +import type { Actions } from '../index.ts'; +import type { Feed } from '../mixins/index.ts'; +import type { EditPlaylistEndpointOptions } from '../../types/index.ts'; export default class PlaylistManager { #actions: Actions; diff --git a/deno/src/core/mixins/Feed.ts b/deno/src/core/mixins/Feed.ts index db8ae75d..9e508fff 100644 --- a/deno/src/core/mixins/Feed.ts +++ b/deno/src/core/mixins/Feed.ts @@ -1,7 +1,5 @@ -import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../../parser/helpers.ts'; import { Parser, ReloadContinuationItemsCommand } from '../../parser/index.ts'; import { concatMemos, InnertubeError } from '../../utils/Utils.ts'; -import type Actions from '../Actions.ts'; import BackstagePost from '../../parser/classes/BackstagePost.ts'; import SharedPost from '../../parser/classes/SharedPost.ts'; @@ -27,12 +25,15 @@ import TwoColumnBrowseResults from '../../parser/classes/TwoColumnBrowseResults. import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.ts'; import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.ts'; +import type { ApiResponse, Actions } from '../index.ts'; +import type { + Memo, ObservedArray, + SuperParsedResult, YTNode +} from '../../parser/helpers.ts'; import type MusicQueue from '../../parser/classes/MusicQueue.ts'; import type RichGrid from '../../parser/classes/RichGrid.ts'; import type SectionList from '../../parser/classes/SectionList.ts'; - import type { IParsedResponse } from '../../parser/types/index.ts'; -import type { ApiResponse } from '../Actions.ts'; export default class Feed { #page: T; @@ -212,10 +213,10 @@ export default class Feed { #getBodyContinuations(): ObservedArray { if (this.#page.header_memo) { const header_continuations = this.#page.header_memo.getType(ContinuationItem); - - return this.#memo.getType(ContinuationItem).filter((continuation) => !header_continuations.includes(continuation)) as ObservedArray; + return this.#memo.getType(ContinuationItem).filter( + (continuation) => !header_continuations.includes(continuation) + ) as ObservedArray; } - return this.#memo.getType(ContinuationItem); } } \ No newline at end of file diff --git a/deno/src/core/mixins/FilterableFeed.ts b/deno/src/core/mixins/FilterableFeed.ts index c5672c87..dfa4af89 100644 --- a/deno/src/core/mixins/FilterableFeed.ts +++ b/deno/src/core/mixins/FilterableFeed.ts @@ -1,12 +1,11 @@ +import Feed from './Feed.ts'; import ChipCloudChip from '../../parser/classes/ChipCloudChip.ts'; import FeedFilterChipBar from '../../parser/classes/FeedFilterChipBar.ts'; import { InnertubeError } from '../../utils/Utils.ts'; -import Feed from './Feed.ts'; import type { ObservedArray } from '../../parser/helpers.ts'; -import type { IParsedResponse } from '../../parser/types/ParsedResponse.ts'; -import type Actions from '../Actions.ts'; -import type { ApiResponse } from '../Actions.ts'; +import type { IParsedResponse } from '../../parser/types/index.ts'; +import type { ApiResponse, Actions } from '../index.ts'; export default class FilterableFeed extends Feed { #chips?: ObservedArray; diff --git a/deno/src/core/mixins/MediaInfo.ts b/deno/src/core/mixins/MediaInfo.ts index cd2bfb30..a2feb14b 100644 --- a/deno/src/core/mixins/MediaInfo.ts +++ b/deno/src/core/mixins/MediaInfo.ts @@ -1,17 +1,17 @@ -import type { ApiResponse } from '../Actions.ts'; -import type Actions from '../Actions.ts'; -import * as Constants from '../../utils/Constants.ts'; -import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.ts'; -import * as FormatUtils from '../../utils/FormatUtils.ts'; +import { Constants, FormatUtils } from '../../utils/index.ts'; import { InnertubeError } from '../../utils/Utils.ts'; -import type Format from '../../parser/classes/misc/Format.ts'; -import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.ts'; -import { Parser } from '../../parser/index.ts'; -import type { DashOptions } from '../../types/DashOptions.ts'; -import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.ts'; import { getStreamingInfo } from '../../utils/StreamingInfo.ts'; + +import { Parser } from '../../parser/index.ts'; +import { TranscriptInfo } from '../../parser/youtube/index.ts'; +import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.ts'; import ContinuationItem from '../../parser/classes/ContinuationItem.ts'; -import TranscriptInfo from '../../parser/youtube/TranscriptInfo.ts'; + +import type { ApiResponse, Actions } from '../index.ts'; +import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.ts'; +import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.ts'; +import type Format from '../../parser/classes/misc/Format.ts'; +import type { DashOptions } from '../../types/DashOptions.ts'; export default class MediaInfo { #page: [IPlayerResponse, INextResponse?]; @@ -50,17 +50,17 @@ export default class MediaInfo { async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise { const player_response = this.#page[0]; - if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) { - throw new InnertubeError('Generating DASH manifests for live and Post-Live-DVR videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.'); + if (player_response.video_details && (player_response.video_details.is_live)) { + throw new InnertubeError('Generating DASH manifests for live videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.'); } let storyboards; - if (options.include_thumbnails && player_response.storyboards?.is(PlayerStoryboardSpec)) { + if (options.include_thumbnails && player_response.storyboards) { storyboards = player_response.storyboards; } - return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards); + return FormatUtils.toDash(this.streaming_data, this.page[0].video_details?.is_post_live_dvr, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards); } /** @@ -69,12 +69,13 @@ export default class MediaInfo { getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter) { return getStreamingInfo( this.streaming_data, + this.page[0].video_details?.is_post_live_dvr, url_transformer, format_filter, this.cpn, this.#actions.session.player, this.#actions, - this.#page[0].storyboards?.is(PlayerStoryboardSpec) ? this.#page[0].storyboards : undefined + this.#page[0].storyboards ? this.#page[0].storyboards : undefined ); } diff --git a/deno/src/core/mixins/TabbedFeed.ts b/deno/src/core/mixins/TabbedFeed.ts index 436f91e8..5be666ca 100644 --- a/deno/src/core/mixins/TabbedFeed.ts +++ b/deno/src/core/mixins/TabbedFeed.ts @@ -1,11 +1,10 @@ -import Tab from '../../parser/classes/Tab.ts'; -import Feed from './Feed.ts'; +import { Feed } from './index.ts'; import { InnertubeError } from '../../utils/Utils.ts'; +import Tab from '../../parser/classes/Tab.ts'; -import type Actions from '../Actions.ts'; +import type { Actions, ApiResponse } from '../index.ts'; import type { ObservedArray } from '../../parser/helpers.ts'; import type { IParsedResponse } from '../../parser/types/ParsedResponse.ts'; -import type { ApiResponse } from '../Actions.ts'; export default class TabbedFeed extends Feed { #tabs?: ObservedArray; diff --git a/deno/src/parser/classes/AttributionView.ts b/deno/src/parser/classes/AttributionView.ts new file mode 100644 index 00000000..e908d4bb --- /dev/null +++ b/deno/src/parser/classes/AttributionView.ts @@ -0,0 +1,17 @@ +import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; +import Text from './misc/Text.ts'; + +export default class AttributionView extends YTNode { + static type = 'AttributionView'; + + text: Text; + suffix: Text; + + constructor(data: RawNode) { + super(); + + this.text = Text.fromAttributed(data.text); + this.suffix = Text.fromAttributed(data.suffix); + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/AvatarView.ts b/deno/src/parser/classes/AvatarView.ts index 7e2025a7..98ca9eff 100644 --- a/deno/src/parser/classes/AvatarView.ts +++ b/deno/src/parser/classes/AvatarView.ts @@ -5,24 +5,20 @@ import { Thumbnail } from '../misc.ts'; export default class AvatarView extends YTNode { static type = 'AvatarView'; - image: { - sources: Thumbnail[], - processor: { - border_image_processor: { - circular: boolean - } + image: Thumbnail[]; + image_processor: { + border_image_processor: { + circular: boolean } }; avatar_image_size: string; constructor(data: RawNode) { super(); - this.image = { - sources: data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width), - processor: { - border_image_processor: { - circular: data.image.processor.borderImageProcessor.circular - } + this.image = Thumbnail.fromResponse(data.image); + this.image_processor = { + border_image_processor: { + circular: data.image.processor.borderImageProcessor.circular } }; this.avatar_image_size = data.avatarImageSize; diff --git a/deno/src/parser/classes/Channel.ts b/deno/src/parser/classes/Channel.ts index 04b709c5..a1a3a48e 100644 --- a/deno/src/parser/classes/Channel.ts +++ b/deno/src/parser/classes/Channel.ts @@ -1,3 +1,4 @@ +import { Log } from '../../utils/index.ts'; import { YTNode } from '../helpers.ts'; import { Parser, type RawNode } from '../index.ts'; import Button from './Button.ts'; @@ -44,6 +45,7 @@ export default class Channel extends YTNode { * Please use {@link Channel.subscriber_count} instead. */ get subscribers(): Text { + Log.warnOnce(Channel.type, 'Channel#subscribers is deprecated. Please use Channel#subscriber_count instead.'); return this.subscriber_count; } @@ -53,6 +55,7 @@ export default class Channel extends YTNode { * Please use {@link Channel.video_count} instead. */ get videos(): Text { + Log.warnOnce(Channel.type, 'Channel#videos is deprecated. Please use Channel#video_count instead.'); return this.video_count; } } \ No newline at end of file diff --git a/deno/src/parser/classes/ChannelAboutFullMetadata.ts b/deno/src/parser/classes/ChannelAboutFullMetadata.ts index 53b70334..c5d02cb6 100644 --- a/deno/src/parser/classes/ChannelAboutFullMetadata.ts +++ b/deno/src/parser/classes/ChannelAboutFullMetadata.ts @@ -1,3 +1,4 @@ +import { Log } from '../../utils/index.ts'; import { YTNode, type ObservedArray } from '../helpers.ts'; import { Parser, type RawNode } from '../index.ts'; import Button from './Button.ts'; @@ -55,6 +56,7 @@ export default class ChannelAboutFullMetadata extends YTNode { * Please use {@link Channel.view_count} instead. */ get views() { + Log.warnOnce(ChannelAboutFullMetadata.type, 'ChannelAboutFullMetadata#views is deprecated. Please use ChannelAboutFullMetadata#view_count instead.'); return this.view_count; } @@ -64,6 +66,7 @@ export default class ChannelAboutFullMetadata extends YTNode { * Please use {@link Channel.joined_date} instead. */ get joined(): Text { + Log.warnOnce(ChannelAboutFullMetadata.type, 'ChannelAboutFullMetadata#joined is deprecated. Please use ChannelAboutFullMetadata#joined_date instead.'); return this.joined_date; } } \ No newline at end of file diff --git a/deno/src/parser/classes/ChannelVideoPlayer.ts b/deno/src/parser/classes/ChannelVideoPlayer.ts index 08229775..ac018d68 100644 --- a/deno/src/parser/classes/ChannelVideoPlayer.ts +++ b/deno/src/parser/classes/ChannelVideoPlayer.ts @@ -1,6 +1,7 @@ import Text from './misc/Text.ts'; import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; +import { Log } from '../../utils/index.ts'; export default class ChannelVideoPlayer extends YTNode { static type = 'ChannelVideoPlayer'; @@ -26,6 +27,7 @@ export default class ChannelVideoPlayer extends YTNode { * Please use {@link ChannelVideoPlayer.view_count} instead. */ get views(): Text { + Log.warnOnce(ChannelVideoPlayer.type, 'ChannelVideoPlayer#views is deprecated. Please use ChannelVideoPlayer#view_count instead.'); return this.view_count; } @@ -35,6 +37,7 @@ export default class ChannelVideoPlayer extends YTNode { * Please use {@link ChannelVideoPlayer.published_time} instead. */ get published(): Text { + Log.warnOnce(ChannelVideoPlayer.type, 'ChannelVideoPlayer#published is deprecated. Please use ChannelVideoPlayer#published_time instead.'); return this.published_time; } } \ No newline at end of file diff --git a/deno/src/parser/classes/DecoratedAvatarView.ts b/deno/src/parser/classes/DecoratedAvatarView.ts index 89458cf2..6b444397 100644 --- a/deno/src/parser/classes/DecoratedAvatarView.ts +++ b/deno/src/parser/classes/DecoratedAvatarView.ts @@ -1,19 +1,21 @@ import { YTNode } from '../helpers.ts'; -import type { RawNode } from '../index.ts'; +import { Parser, type RawNode } from '../index.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; import AvatarView from './AvatarView.ts'; export default class DecoratedAvatarView extends YTNode { static type = 'DecoratedAvatarView'; - avatar: AvatarView; + avatar: AvatarView | null; a11y_label: string; - on_tap_endpoint: NavigationEndpoint; + on_tap_endpoint?: NavigationEndpoint; constructor(data: RawNode) { super(); - this.avatar = new AvatarView(data.avatar.avatarViewModel); + this.avatar = Parser.parseItem(data.avatar, AvatarView); this.a11y_label = data.a11yLabel; - this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap.innertubeCommand); + if (data.rendererContext?.commandContext?.onTap) { + this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap); + } } } \ No newline at end of file diff --git a/deno/src/parser/classes/DescriptionPreviewView.ts b/deno/src/parser/classes/DescriptionPreviewView.ts new file mode 100644 index 00000000..90cf485f --- /dev/null +++ b/deno/src/parser/classes/DescriptionPreviewView.ts @@ -0,0 +1,47 @@ +import { YTNode } from '../helpers.ts'; +import { Parser, type RawNode } from '../index.ts'; +import EngagementPanelSectionList from './EngagementPanelSectionList.ts'; +import Text from './misc/Text.ts'; + +export default class DescriptionPreviewView extends YTNode { + static type = 'DescriptionPreviewView'; + + description: Text; + max_lines: number; + truncation_text: Text; + always_show_truncation_text: boolean; + more_endpoint?: { + show_engagement_panel_endpoint: { + engagement_panel: EngagementPanelSectionList | null, + engagement_panel_popup_type: string; + identifier: { + surface: string, + tag: string + } + } + }; + + constructor(data: RawNode) { + super(); + + this.description = Text.fromAttributed(data.description); + this.max_lines = parseInt(data.maxLines); + this.truncation_text = Text.fromAttributed(data.truncationText); + this.always_show_truncation_text = !!data.alwaysShowTruncationText; + + if (data.rendererContext.commandContext?.onTap?.innertubeCommand?.showEngagementPanelEndpoint) { + const endpoint = data.rendererContext.commandContext?.onTap?.innertubeCommand?.showEngagementPanelEndpoint; + + this.more_endpoint = { + show_engagement_panel_endpoint: { + engagement_panel: Parser.parseItem(endpoint.engagementPanel, EngagementPanelSectionList), + engagement_panel_popup_type: endpoint.engagementPanelPresentationConfigs.engagementPanelPopupPresentationConfig.popupType, + identifier: { + surface: endpoint.identifier.surface, + tag: endpoint.identifier.tag + } + } + }; + } + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/DynamicTextView.ts b/deno/src/parser/classes/DynamicTextView.ts index be212500..45f35641 100644 --- a/deno/src/parser/classes/DynamicTextView.ts +++ b/deno/src/parser/classes/DynamicTextView.ts @@ -1,13 +1,16 @@ import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; +import Text from './misc/Text.ts'; export default class DynamicTextView extends YTNode { static type = 'DynamicTextView'; - text: string; + text: Text; + max_lines: number; constructor(data: RawNode) { super(); - this.text = data.text.content; + this.text = Text.fromAttributed(data.text); + this.max_lines = parseInt(data.maxLines); } } \ No newline at end of file diff --git a/deno/src/parser/classes/ImageBannerView.ts b/deno/src/parser/classes/ImageBannerView.ts new file mode 100644 index 00000000..a9560ba9 --- /dev/null +++ b/deno/src/parser/classes/ImageBannerView.ts @@ -0,0 +1,16 @@ +import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; +import Thumbnail from './misc/Thumbnail.ts'; + +export default class ImageBannerView extends YTNode { + static type = 'ImageBannerView'; + + image: Thumbnail[]; + style: string; + + constructor(data: RawNode) { + super(); + this.image = Thumbnail.fromResponse(data.image); + this.style = data.style; + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/ModalWithTitleAndButton.ts b/deno/src/parser/classes/ModalWithTitleAndButton.ts new file mode 100644 index 00000000..d99c6fa9 --- /dev/null +++ b/deno/src/parser/classes/ModalWithTitleAndButton.ts @@ -0,0 +1,19 @@ +import { YTNode } from '../helpers.ts'; +import { Parser, type RawNode } from '../index.ts'; +import Button from './Button.ts'; +import Text from './misc/Text.ts'; + +export default class ModalWithTitleAndButton extends YTNode { + static type = 'ModalWithTitleAndButton'; + + title: Text; + content: Text; + button: Button | null; + + constructor(data: RawNode) { + super(); + this.title = new Text(data.title); + this.content = new Text(data.content); + this.button = Parser.parseItem(data.button, Button); + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/NavigationEndpoint.ts b/deno/src/parser/classes/NavigationEndpoint.ts index f14efcdd..ff2c91d2 100644 --- a/deno/src/parser/classes/NavigationEndpoint.ts +++ b/deno/src/parser/classes/NavigationEndpoint.ts @@ -4,6 +4,7 @@ import { YTNode } from '../helpers.ts'; import { Parser, type RawNode } from '../index.ts'; import type { IParsedResponse } from '../types/ParsedResponse.ts'; import CreatePlaylistDialog from './CreatePlaylistDialog.ts'; +import type ModalWithTitleAndButton from './ModalWithTitleAndButton.ts'; import OpenPopupAction from './actions/OpenPopupAction.ts'; export default class NavigationEndpoint extends YTNode { @@ -11,8 +12,11 @@ export default class NavigationEndpoint extends YTNode { payload; dialog?: CreatePlaylistDialog | YTNode | null; + modal?: ModalWithTitleAndButton | YTNode | null; open_popup?: OpenPopupAction | null; + next_endpoint?: NavigationEndpoint; + metadata: { url?: string; api_url?: string; @@ -41,6 +45,13 @@ export default class NavigationEndpoint extends YTNode { this.dialog = Parser.parseItem(this.payload.dialog || this.payload.content); } + if (Reflect.has(this.payload, 'modal')) { + this.modal = Parser.parseItem(this.payload.modal); + } + + if (Reflect.has(this.payload, 'nextEndpoint')) { + this.next_endpoint = new NavigationEndpoint(this.payload.nextEndpoint); + } if (data?.serviceEndpoint) { data = data.serviceEndpoint; diff --git a/deno/src/parser/classes/PageHeaderView.ts b/deno/src/parser/classes/PageHeaderView.ts index 837e95c9..df3d1f4d 100644 --- a/deno/src/parser/classes/PageHeaderView.ts +++ b/deno/src/parser/classes/PageHeaderView.ts @@ -5,6 +5,9 @@ import ContentPreviewImageView from './ContentPreviewImageView.ts'; import DecoratedAvatarView from './DecoratedAvatarView.ts'; import DynamicTextView from './DynamicTextView.ts'; import FlexibleActionsView from './FlexibleActionsView.ts'; +import DescriptionPreviewView from './DescriptionPreviewView.ts'; +import AttributionView from './AttributionView.ts'; +import ImageBannerView from './ImageBannerView.ts'; export default class PageHeaderView extends YTNode { static type = 'PageHeaderView'; @@ -13,6 +16,9 @@ export default class PageHeaderView extends YTNode { image: ContentPreviewImageView | DecoratedAvatarView | null; metadata: ContentMetadataView | null; actions: FlexibleActionsView | null; + description: DescriptionPreviewView | null; + attributation: AttributionView | null; + banner: ImageBannerView | null; constructor(data: RawNode) { super(); @@ -20,5 +26,8 @@ export default class PageHeaderView extends YTNode { this.image = Parser.parseItem(data.image, [ ContentPreviewImageView, DecoratedAvatarView ]); this.metadata = Parser.parseItem(data.metadata, ContentMetadataView); this.actions = Parser.parseItem(data.actions, FlexibleActionsView); + this.description = Parser.parseItem(data.description, DescriptionPreviewView); + this.attributation = Parser.parseItem(data.attributation, AttributionView); + this.banner = Parser.parseItem(data.banner, ImageBannerView); } } \ No newline at end of file diff --git a/deno/src/parser/classes/PlayerCaptionsTracklist.ts b/deno/src/parser/classes/PlayerCaptionsTracklist.ts index 96868da2..cc1b7f10 100644 --- a/deno/src/parser/classes/PlayerCaptionsTracklist.ts +++ b/deno/src/parser/classes/PlayerCaptionsTracklist.ts @@ -10,7 +10,7 @@ export default class PlayerCaptionsTracklist extends YTNode { name: Text; vss_id: string; language_code: string; - kind: string; + kind?: 'asr' | 'frc'; is_translatable: boolean; }[]; diff --git a/deno/src/parser/classes/PlayerLiveStoryboardSpec.ts b/deno/src/parser/classes/PlayerLiveStoryboardSpec.ts index 343fdacb..66fc8162 100644 --- a/deno/src/parser/classes/PlayerLiveStoryboardSpec.ts +++ b/deno/src/parser/classes/PlayerLiveStoryboardSpec.ts @@ -1,11 +1,32 @@ import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; + +export interface LiveStoryboardData { + type: 'live', + template_url: string, + thumbnail_width: number, + thumbnail_height: number, + columns: number, + rows: number +} export default class PlayerLiveStoryboardSpec extends YTNode { static type = 'PlayerLiveStoryboardSpec'; - constructor() { + board: LiveStoryboardData; + + constructor(data: RawNode) { super(); - // TODO: A little bit different from PlayerLiveStoryboardSpec - // https://i.ytimg.com/sb/5qap5aO4i9A/storyboard_live_90_2x2_b2/M$M.jpg?rs=AOn4CLC9s6IeOsw_gKvEbsbU9y-e2FVRTw#159#90#2#2 + + const [ template_url, thumbnail_width, thumbnail_height, columns, rows ] = data.spec.split('#'); + + this.board = { + type: 'live', + template_url, + thumbnail_width: parseInt(thumbnail_width, 10), + thumbnail_height: parseInt(thumbnail_height, 10), + columns: parseInt(columns, 10), + rows: parseInt(rows, 10) + }; } } \ No newline at end of file diff --git a/deno/src/parser/classes/PlayerMicroformat.ts b/deno/src/parser/classes/PlayerMicroformat.ts index 42ed826c..2c122b7f 100644 --- a/deno/src/parser/classes/PlayerMicroformat.ts +++ b/deno/src/parser/classes/PlayerMicroformat.ts @@ -36,6 +36,7 @@ export default class PlayerMicroformat extends YTNode { upload_date: string; available_countries: string[]; start_timestamp: Date | null; + end_timestamp: Date | null; constructor(data: RawNode) { super(); @@ -70,5 +71,6 @@ export default class PlayerMicroformat extends YTNode { this.upload_date = data.uploadDate; this.available_countries = data.availableCountries; this.start_timestamp = data.liveBroadcastDetails?.startTimestamp ? new Date(data.liveBroadcastDetails.startTimestamp) : null; + this.end_timestamp = data.liveBroadcastDetails?.endTimestamp ? new Date(data.liveBroadcastDetails.endTimestamp) : null; } } \ No newline at end of file diff --git a/deno/src/parser/classes/PlayerStoryboardSpec.ts b/deno/src/parser/classes/PlayerStoryboardSpec.ts index d5cd20ed..b19404ca 100644 --- a/deno/src/parser/classes/PlayerStoryboardSpec.ts +++ b/deno/src/parser/classes/PlayerStoryboardSpec.ts @@ -2,6 +2,7 @@ import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; export interface StoryboardData { + type: 'vod' template_url: string; thumbnail_width: number; thumbnail_height: number; @@ -31,6 +32,7 @@ export default class PlayerStoryboardSpec extends YTNode { const storyboard_count = Math.ceil(parseInt(thumbnail_count, 10) / (parseInt(columns, 10) * parseInt(rows, 10))); return { + type: 'vod', template_url: url.toString().replace('$L', i).replace('$N', name), thumbnail_width: parseInt(thumbnail_width, 10), thumbnail_height: parseInt(thumbnail_height, 10), diff --git a/deno/src/parser/classes/PlaylistVideo.ts b/deno/src/parser/classes/PlaylistVideo.ts index 164e551b..731bdf8d 100644 --- a/deno/src/parser/classes/PlaylistVideo.ts +++ b/deno/src/parser/classes/PlaylistVideo.ts @@ -23,6 +23,7 @@ export default class PlaylistVideo extends YTNode { upcoming?: Date; video_info: Text; accessibility_label?: string; + style?: string; duration: { text: string; @@ -44,6 +45,10 @@ export default class PlaylistVideo extends YTNode { this.video_info = new Text(data.videoInfo); this.accessibility_label = data.title.accessibility.accessibilityData.label; + if (Reflect.has(data, 'style')) { + this.style = data.style; + } + const upcoming = data.upcomingEventData && Number(`${data.upcomingEventData.startTime}000`); if (upcoming) { this.upcoming = new Date(upcoming); diff --git a/deno/src/parser/classes/misc/Format.ts b/deno/src/parser/classes/misc/Format.ts index 4f2df35d..c971236b 100644 --- a/deno/src/parser/classes/misc/Format.ts +++ b/deno/src/parser/classes/misc/Format.ts @@ -41,6 +41,8 @@ export default class Format { audio_sample_rate?: number; audio_channels?: number; loudness_db?: number; + max_dvr_duration_sec?: number; + target_duration_dec?: number; has_audio: boolean; has_video: boolean; language?: string | null; @@ -90,6 +92,8 @@ export default class Format { this.audio_sample_rate = parseInt(data.audioSampleRate); this.audio_channels = data.audioChannels; this.loudness_db = data.loudnessDb; + this.max_dvr_duration_sec = data.maxDvrDurationSec; + this.target_duration_dec = data.targetDurationSec; this.has_audio = !!data.audioBitrate || !!data.audioQuality; this.has_video = !!data.qualityLabel; diff --git a/deno/src/parser/classes/misc/VideoDetails.ts b/deno/src/parser/classes/misc/VideoDetails.ts index c2f0a4b0..5dfe4ce9 100644 --- a/deno/src/parser/classes/misc/VideoDetails.ts +++ b/deno/src/parser/classes/misc/VideoDetails.ts @@ -16,9 +16,12 @@ export default class VideoDetails { is_private: boolean; is_live: boolean; is_live_content: boolean; + is_live_dvr_enabled: boolean; is_upcoming: boolean; is_crawlable: boolean; is_post_live_dvr: boolean; + is_low_latency_live_stream: boolean; + live_chunk_readahead?: number; constructor(data: RawNode) { this.id = data.videoId; @@ -35,8 +38,11 @@ export default class VideoDetails { this.is_private = !!data.isPrivate; this.is_live = !!data.isLive; this.is_live_content = !!data.isLiveContent; + this.is_live_dvr_enabled = !!data.isLiveDvrEnabled; + this.is_low_latency_live_stream = !!data.isLowLatencyLiveStream; this.is_upcoming = !!data.isUpcoming; this.is_post_live_dvr = !!data.isPostLiveDvr; this.is_crawlable = !!data.isCrawlable; + this.live_chunk_readahead = data.liveChunkReadahead; } } \ No newline at end of file diff --git a/deno/src/parser/continuations.ts b/deno/src/parser/continuations.ts index 2d04d1b8..5ae0b8c2 100644 --- a/deno/src/parser/continuations.ts +++ b/deno/src/parser/continuations.ts @@ -1,10 +1,11 @@ -import type { ObservedArray } from './helpers.ts'; import { YTNode, observe } from './helpers.ts'; -import type { RawNode } from './index.ts'; import { Thumbnail } from './misc.ts'; import { NavigationEndpoint, LiveChatItemList, LiveChatHeader, LiveChatParticipantsList, Message } from './nodes.ts'; import * as Parser from './parser.ts'; +import type { RawNode } from './index.ts'; +import type { ObservedArray } from './helpers.ts'; + export class ItemSectionContinuation extends YTNode { static readonly type = 'itemSectionContinuation'; diff --git a/deno/src/parser/generator.ts b/deno/src/parser/generator.ts index a59995c1..bba80b41 100644 --- a/deno/src/parser/generator.ts +++ b/deno/src/parser/generator.ts @@ -1,12 +1,14 @@ /* eslint-disable no-cond-assign */ +import { YTNode } from './helpers.ts'; +import { Parser } from './index.ts'; import { InnertubeError } from '../utils/Utils.ts'; + import Author from './classes/misc/Author.ts'; import Text from './classes/misc/Text.ts'; import Thumbnail from './classes/misc/Thumbnail.ts'; import NavigationEndpoint from './classes/NavigationEndpoint.ts'; + import type { YTNodeConstructor } from './helpers.ts'; -import { YTNode } from './helpers.ts'; -import * as Parser from './parser.ts'; export type MiscInferenceType = { type: 'misc', diff --git a/deno/src/parser/helpers.ts b/deno/src/parser/helpers.ts index 670f20b8..4ced39c6 100644 --- a/deno/src/parser/helpers.ts +++ b/deno/src/parser/helpers.ts @@ -1,3 +1,4 @@ +import Log from '../utils/Log.ts'; import { deepCompare, ParsingError } from '../utils/Utils.ts'; const isObserved = Symbol('ObservedArray.isObserved'); @@ -62,6 +63,7 @@ export class YTNode { } export class Maybe { + #TAG = 'Maybe'; #value; constructor (value: any) { @@ -275,10 +277,11 @@ export class Maybe { } /** - * @deprecated This call is not meant to be used outside of debugging. Please use the specific type getter instead. + * @deprecated + * This call is not meant to be used outside of debugging. Please use the specific type getter instead. */ any(): any { - console.warn('This call is not meant to be used outside of debugging. Please use the specific type getter instead.'); + Log.warn(this.#TAG, 'This call is not meant to be used outside of debugging. Please use the specific type getter instead.'); return this.#value; } diff --git a/deno/src/parser/nodes.ts b/deno/src/parser/nodes.ts index 1d194b1b..f54bf2f7 100644 --- a/deno/src/parser/nodes.ts +++ b/deno/src/parser/nodes.ts @@ -20,6 +20,7 @@ export { default as AnalyticsVodCarouselCard } from './classes/analytics/Analyti export { default as CtaGoToCreatorStudio } from './classes/analytics/CtaGoToCreatorStudio.ts'; export { default as DataModelSection } from './classes/analytics/DataModelSection.ts'; export { default as StatRow } from './classes/analytics/StatRow.ts'; +export { default as AttributionView } from './classes/AttributionView.ts'; export { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability.ts'; export { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo.ts'; export { default as AvatarView } from './classes/AvatarView.ts'; @@ -97,6 +98,7 @@ export { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog. export { default as DecoratedAvatarView } from './classes/DecoratedAvatarView.ts'; export { default as DecoratedPlayerBar } from './classes/DecoratedPlayerBar.ts'; export { default as DefaultPromoPanel } from './classes/DefaultPromoPanel.ts'; +export { default as DescriptionPreviewView } from './classes/DescriptionPreviewView.ts'; export { default as DidYouMean } from './classes/DidYouMean.ts'; export { default as DislikeButtonView } from './classes/DislikeButtonView.ts'; export { default as DownloadButton } from './classes/DownloadButton.ts'; @@ -151,6 +153,7 @@ export { default as HorizontalCardList } from './classes/HorizontalCardList.ts'; export { default as HorizontalList } from './classes/HorizontalList.ts'; export { default as HorizontalMovieList } from './classes/HorizontalMovieList.ts'; export { default as IconLink } from './classes/IconLink.ts'; +export { default as ImageBannerView } from './classes/ImageBannerView.ts'; export { default as IncludingResultsFor } from './classes/IncludingResultsFor.ts'; export { default as InfoPanelContainer } from './classes/InfoPanelContainer.ts'; export { default as InfoPanelContent } from './classes/InfoPanelContent.ts'; @@ -231,6 +234,7 @@ export { default as MetadataRowHeader } from './classes/MetadataRowHeader.ts'; export { default as MetadataScreen } from './classes/MetadataScreen.ts'; export { default as MicroformatData } from './classes/MicroformatData.ts'; export { default as Mix } from './classes/Mix.ts'; +export { default as ModalWithTitleAndButton } from './classes/ModalWithTitleAndButton.ts'; export { default as Movie } from './classes/Movie.ts'; export { default as MovingThumbnail } from './classes/MovingThumbnail.ts'; export { default as MultiMarkersPlayerBar } from './classes/MultiMarkersPlayerBar.ts'; diff --git a/deno/src/parser/parser.ts b/deno/src/parser/parser.ts index 9b2ac649..4b90979a 100644 --- a/deno/src/parser/parser.ts +++ b/deno/src/parser/parser.ts @@ -1,3 +1,16 @@ +import { YTNodes } from './index.ts'; +import { InnertubeError, ParsingError, Platform } from '../utils/Utils.ts'; +import { Memo, observe, SuperParsedResult } from './helpers.ts'; +import { camelToSnake, generateRuntimeClass, generateTypescriptClass } from './generator.ts'; +import { Log } from '../utils/index.ts'; + +import { + Continuation, ItemSectionContinuation, SectionListContinuation, + LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, + GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand, + ReloadContinuationItemsCommand, ContinuationCommand +} from './continuations.ts'; + import AudioOnlyPlayability from './classes/AudioOnlyPlayability.ts'; import CardCollection from './classes/CardCollection.ts'; import Endscreen from './classes/Endscreen.ts'; @@ -8,27 +21,16 @@ import PlayerStoryboardSpec from './classes/PlayerStoryboardSpec.ts'; import Alert from './classes/Alert.ts'; import AlertWithButton from './classes/AlertWithButton.ts'; import EngagementPanelSectionList from './classes/EngagementPanelSectionList.ts'; - -import type { IParsedResponse, IRawResponse, RawData, RawNode } from './types/index.ts'; - import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem.ts'; import Format from './classes/misc/Format.ts'; import VideoDetails from './classes/misc/VideoDetails.ts'; import NavigationEndpoint from './classes/NavigationEndpoint.ts'; -import { InnertubeError, ParsingError, Platform } from '../utils/Utils.ts'; -import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.ts'; -import { Memo, observe, SuperParsedResult } from './helpers.ts'; -import * as YTNodes from './nodes.ts'; import type { KeyInfo } from './generator.ts'; -import { camelToSnake, generateRuntimeClass, generateTypescriptClass } from './generator.ts'; +import type { ObservedArray, YTNodeConstructor, YTNode } from './helpers.ts'; +import type { IParsedResponse, IRawResponse, RawData, RawNode } from './types/index.ts'; -import { - Continuation, ItemSectionContinuation, SectionListContinuation, - LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, - GridContinuation, PlaylistPanelContinuation, NavigateAction, ShowMiniplayerCommand, - ReloadContinuationItemsCommand, ContinuationCommand -} from './continuations.ts'; +const TAG = 'Parser'; export type ParserError = { classname: string, @@ -85,7 +87,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) switch (context.error_type) { case 'parse': if (context.error instanceof Error) { - console.warn( + Log.warn(TAG, new InnertubeError( `Something went wrong at ${classname}!\n` + `This is a bug, please report it at ${Platform.shim.info.bugs_url}`, { @@ -96,7 +98,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) } break; case 'typecheck': - console.warn( + Log.warn(TAG, new ParsingError( `Type mismatch, got ${classname} expected ${Array.isArray(context.expected) ? context.expected.join(' | ') : context.expected}.`, context.classdata @@ -104,7 +106,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) ); break; case 'mutation_data_missing': - console.warn( + Log.warn(TAG, new InnertubeError( 'Mutation data required for processing MusicMultiSelectMenuItems, but none found.\n' + `This is a bug, please report it at ${Platform.shim.info.bugs_url}` @@ -112,7 +114,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) ); break; case 'mutation_data_invalid': - console.warn( + Log.warn(TAG, new InnertubeError( `Mutation data missing or invalid for ${context.failed} out of ${context.total} MusicMultiSelectMenuItems. ` + `The titles of the failed items are: ${context.titles.join(', ')}.\n` + @@ -121,7 +123,7 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) ); break; case 'class_not_found': - console.warn( + Log.warn(TAG, new InnertubeError( `${classname} not found!\n` + `This is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\n` + @@ -130,14 +132,14 @@ let ERROR_HANDLER: ParserErrorHandler = ({ classname, ...context }: ParserError) ); break; case 'class_changed': - console.warn( + Log.warn(TAG, `${classname} changed!\n` + `The following keys where altered: ${context.changed_keys.map(([ key ]) => camelToSnake(key)).join(', ')}\n` + `The class has changed to:\n${generateTypescriptClass(classname, context.key_info)}` ); break; default: - console.warn( + Log.warn(TAG, 'Unreachable code reached at ParserErrorHandler' ); break; @@ -319,9 +321,9 @@ export function parseResponse(data: parsed_data.continuation = continuation; } - const continuationEndpoint = data.continuationEndpoint ? parseLC(data.continuationEndpoint) : null; - if (continuationEndpoint) { - parsed_data.continuationEndpoint = continuationEndpoint; + const continuation_endpoint = data.continuationEndpoint ? parseLC(data.continuationEndpoint) : null; + if (continuation_endpoint) { + parsed_data.continuation_endpoint = continuation_endpoint; } const metadata = parse(data.metadata); diff --git a/deno/src/parser/types/ParsedResponse.ts b/deno/src/parser/types/ParsedResponse.ts index 309b1235..8226a066 100644 --- a/deno/src/parser/types/ParsedResponse.ts +++ b/deno/src/parser/types/ParsedResponse.ts @@ -69,7 +69,7 @@ export interface IParsedResponse { items?: SuperParsedResult; entries?: SuperParsedResult; entries_memo?: Memo; - continuationEndpoint?: YTNode; + continuation_endpoint?: YTNode; } export interface IPlayerConfig { diff --git a/deno/src/parser/youtube/AccountInfo.ts b/deno/src/parser/youtube/AccountInfo.ts index 833fcfd3..b6e3c53d 100644 --- a/deno/src/parser/youtube/AccountInfo.ts +++ b/deno/src/parser/youtube/AccountInfo.ts @@ -1,13 +1,12 @@ import { Parser } from '../index.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { IParsedResponse } from '../types/ParsedResponse.ts'; - +import { InnertubeError } from '../../utils/Utils.ts'; import AccountSectionList from '../classes/AccountSectionList.ts'; + +import type { ApiResponse } from '../../core/index.ts'; +import type { IParsedResponse } from '../types/index.ts'; import type AccountItemSection from '../classes/AccountItemSection.ts'; import type AccountChannel from '../classes/AccountChannel.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; - class AccountInfo { #page: IParsedResponse; diff --git a/deno/src/parser/youtube/Analytics.ts b/deno/src/parser/youtube/Analytics.ts index 2b13f1e1..9926abec 100644 --- a/deno/src/parser/youtube/Analytics.ts +++ b/deno/src/parser/youtube/Analytics.ts @@ -1,7 +1,7 @@ import { Parser } from '../index.ts'; import Element from '../classes/Element.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { ApiResponse } from '../../core/index.ts'; +import type { IBrowseResponse } from '../types/index.ts'; class Analytics { #page: IBrowseResponse; diff --git a/deno/src/parser/youtube/Channel.ts b/deno/src/parser/youtube/Channel.ts index a903016d..a33373a0 100644 --- a/deno/src/parser/youtube/Channel.ts +++ b/deno/src/parser/youtube/Channel.ts @@ -1,3 +1,7 @@ +import Feed from '../../core/mixins/Feed.ts'; +import FilterableFeed from '../../core/mixins/FilterableFeed.ts'; +import { ChannelError, InnertubeError } from '../../utils/Utils.ts'; + import TabbedFeed from '../../core/mixins/TabbedFeed.ts'; import C4TabbedHeader from '../classes/C4TabbedHeader.ts'; import CarouselHeader from '../classes/CarouselHeader.ts'; @@ -12,9 +16,6 @@ import SectionList from '../classes/SectionList.ts'; import Tab from '../classes/Tab.ts'; import PageHeader from '../classes/PageHeader.ts'; import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.ts'; - -import Feed from '../../core/mixins/Feed.ts'; -import FilterableFeed from '../../core/mixins/FilterableFeed.ts'; import ChipCloudChip from '../classes/ChipCloudChip.ts'; import FeedFilterChipBar from '../classes/FeedFilterChipBar.ts'; import ChannelSubMenu from '../classes/ChannelSubMenu.ts'; @@ -22,11 +23,8 @@ import SortFilterSubMenu from '../classes/SortFilterSubMenu.ts'; import ContinuationItem from '../classes/ContinuationItem.ts'; import NavigationEndpoint from '../classes/NavigationEndpoint.ts'; -import { ChannelError, InnertubeError } from '../../utils/Utils.ts'; - import type { AppendContinuationItemsAction, ReloadContinuationItemsCommand } from '../index.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { IBrowseResponse } from '../types/index.ts'; export default class Channel extends TabbedFeed { @@ -196,10 +194,13 @@ export default class Channel extends TabbedFeed { if (this.hasTabWithURL('about')) { const tab = await this.getTabByURL('about'); return tab.memo.getType(ChannelAboutFullMetadata)[0]; - } else if (this.header?.is(C4TabbedHeader) && this.header.tagline) { + } - if (this.header.tagline.more_endpoint instanceof NavigationEndpoint) { - const response = await this.header.tagline.more_endpoint.call(this.actions); + const tagline = this.header?.is(C4TabbedHeader) && this.header.tagline; + + if (tagline || this.header?.is(PageHeader) && this.header.content?.description) { + if (tagline && tagline.more_endpoint instanceof NavigationEndpoint) { + const response = await tagline.more_endpoint.call(this.actions); const tab = new TabbedFeed(this.actions, response, false); return tab.memo.getType(ChannelAboutFullMetadata)[0]; @@ -271,7 +272,9 @@ export default class Channel extends TabbedFeed { get has_about(): boolean { // Game topic channels still have an about tab, user channels have switched to the popup - return this.hasTabWithURL('about') || !!(this.header?.is(C4TabbedHeader) && this.header.tagline?.more_endpoint); + return this.hasTabWithURL('about') || + !!(this.header?.is(C4TabbedHeader) && this.header.tagline?.more_endpoint) || + !!(this.header?.is(PageHeader) && this.header.content?.description?.more_endpoint); } get has_search(): boolean { diff --git a/deno/src/parser/youtube/Comments.ts b/deno/src/parser/youtube/Comments.ts index ecaae8a9..8479eab2 100644 --- a/deno/src/parser/youtube/Comments.ts +++ b/deno/src/parser/youtube/Comments.ts @@ -1,16 +1,16 @@ import { Parser } from '../index.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; import { InnertubeError } from '../../utils/Utils.ts'; -import type { ObservedArray } from '../helpers.ts'; import { observe } from '../helpers.ts'; -import type { INextResponse } from '../types/ParsedResponse.ts'; import CommentsHeader from '../classes/comments/CommentsHeader.ts'; import CommentSimplebox from '../classes/comments/CommentSimplebox.ts'; import CommentThread from '../classes/comments/CommentThread.ts'; import ContinuationItem from '../classes/ContinuationItem.ts'; +import type { Actions, ApiResponse } from '../../core/index.ts'; +import type { ObservedArray } from '../helpers.ts'; +import type { INextResponse } from '../types/index.ts'; + class Comments { #page: INextResponse; #actions: Actions; diff --git a/deno/src/parser/youtube/Guide.ts b/deno/src/parser/youtube/Guide.ts index 37ffc864..c4144f11 100644 --- a/deno/src/parser/youtube/Guide.ts +++ b/deno/src/parser/youtube/Guide.ts @@ -1,10 +1,11 @@ -import type { IGuideResponse } from '../types/ParsedResponse.ts'; -import type { IRawResponse } from '../index.ts'; import { Parser } from '../index.ts'; -import type { ObservedArray } from '../helpers.ts'; import GuideSection from '../classes/GuideSection.ts'; import GuideSubscriptionsSection from '../classes/GuideSubscriptionsSection.ts'; +import type { ObservedArray } from '../helpers.ts'; +import type { IGuideResponse } from '../types/index.ts'; +import type { IRawResponse } from '../index.ts'; + export default class Guide { #page: IGuideResponse; diff --git a/deno/src/parser/youtube/HashtagFeed.ts b/deno/src/parser/youtube/HashtagFeed.ts index d7f3319b..cfb18c2a 100644 --- a/deno/src/parser/youtube/HashtagFeed.ts +++ b/deno/src/parser/youtube/HashtagFeed.ts @@ -1,14 +1,13 @@ -import FilterableFeed from '../../core/mixins/FilterableFeed.ts'; import { InnertubeError } from '../../utils/Utils.ts'; +import FilterableFeed from '../../core/mixins/FilterableFeed.ts'; import HashtagHeader from '../classes/HashtagHeader.ts'; import RichGrid from '../classes/RichGrid.ts'; +import PageHeader from '../classes/PageHeader.ts'; import Tab from '../classes/Tab.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type ChipCloudChip from '../classes/ChipCloudChip.ts'; +import type { Actions, ApiResponse } from '../../core/index.ts'; import type { IBrowseResponse } from '../index.ts'; -import { PageHeader } from '../nodes.ts'; +import type ChipCloudChip from '../classes/ChipCloudChip.ts'; export default class HashtagFeed extends FilterableFeed { header?: HashtagHeader | PageHeader; diff --git a/deno/src/parser/youtube/History.ts b/deno/src/parser/youtube/History.ts index 747f2cb4..d6786efa 100644 --- a/deno/src/parser/youtube/History.ts +++ b/deno/src/parser/youtube/History.ts @@ -1,9 +1,9 @@ -import type Actions from '../../core/Actions.ts'; import Feed from '../../core/mixins/Feed.ts'; import ItemSection from '../classes/ItemSection.ts'; import BrowseFeedActions from '../classes/BrowseFeedActions.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; + +import type { Actions, ApiResponse } from '../../core/index.ts'; +import type { IBrowseResponse } from '../types/index.ts'; // TODO: make feed actions usable class History extends Feed { diff --git a/deno/src/parser/youtube/HomeFeed.ts b/deno/src/parser/youtube/HomeFeed.ts index 61c6d7c0..32b83ad1 100644 --- a/deno/src/parser/youtube/HomeFeed.ts +++ b/deno/src/parser/youtube/HomeFeed.ts @@ -1,12 +1,11 @@ -import type Actions from '../../core/Actions.ts'; import FilterableFeed from '../../core/mixins/FilterableFeed.ts'; -import type ChipCloudChip from '../classes/ChipCloudChip.ts'; import FeedTabbedHeader from '../classes/FeedTabbedHeader.ts'; import RichGrid from '../classes/RichGrid.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { IBrowseResponse } from '../types/index.ts'; import type { AppendContinuationItemsAction, ReloadContinuationItemsCommand } from '../index.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type ChipCloudChip from '../classes/ChipCloudChip.ts'; export default class HomeFeed extends FilterableFeed { contents: RichGrid | AppendContinuationItemsAction | ReloadContinuationItemsCommand; diff --git a/deno/src/parser/youtube/ItemMenu.ts b/deno/src/parser/youtube/ItemMenu.ts index bc9aa72d..a249581b 100644 --- a/deno/src/parser/youtube/ItemMenu.ts +++ b/deno/src/parser/youtube/ItemMenu.ts @@ -1,12 +1,12 @@ -import Button from '../classes/Button.ts'; import Menu from '../classes/menus/Menu.ts'; +import Button from '../classes/Button.ts'; import MenuServiceItem from '../classes/menus/MenuServiceItem.ts'; -import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; -import type Actions from '../../core/Actions.ts'; +import type { Actions } from '../../core/index.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import type { IParsedResponse } from '../types/ParsedResponse.ts'; +import type { IParsedResponse } from '../types/index.ts'; +import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; class ItemMenu { #page: IParsedResponse; diff --git a/deno/src/parser/youtube/Library.ts b/deno/src/parser/youtube/Library.ts index 5e5ed3bc..a2e28787 100644 --- a/deno/src/parser/youtube/Library.ts +++ b/deno/src/parser/youtube/Library.ts @@ -1,6 +1,4 @@ -import type Actions from '../../core/Actions.ts'; import { InnertubeError } from '../../utils/Utils.ts'; - import Feed from '../../core/mixins/Feed.ts'; import History from './History.ts'; import Playlist from './Playlist.ts'; @@ -9,8 +7,8 @@ import Shelf from '../classes/Shelf.ts'; import Button from '../classes/Button.ts'; import PageHeader from '../classes/PageHeader.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { Actions, ApiResponse } from '../../core/index.ts'; +import type { IBrowseResponse } from '../types/index.ts'; class Library extends Feed { header: PageHeader | null; diff --git a/deno/src/parser/youtube/LiveChat.ts b/deno/src/parser/youtube/LiveChat.ts index 84a035c6..7e37b49d 100644 --- a/deno/src/parser/youtube/LiveChat.ts +++ b/deno/src/parser/youtube/LiveChat.ts @@ -1,42 +1,40 @@ -import EventEmitter from '../../utils/EventEmitterLike.ts'; +import * as Proto from '../../proto/index.ts'; +import { EventEmitter } from '../../utils/index.ts'; +import { InnertubeError, Platform } from '../../utils/Utils.ts'; import { Parser, LiveChatContinuation } from '../index.ts'; -import type VideoInfo from './VideoInfo.ts'; import SmoothedQueue from './SmoothedQueue.ts'; import AddChatItemAction from '../classes/livechat/AddChatItemAction.ts'; -import type AddLiveChatTickerItemAction from '../classes/livechat/AddLiveChatTickerItemAction.ts'; -import type MarkChatItemAsDeletedAction from '../classes/livechat/MarkChatItemAsDeletedAction.ts'; -import type MarkChatItemsByAuthorAsDeletedAction from '../classes/livechat/MarkChatItemsByAuthorAsDeletedAction.ts'; -import type ReplaceChatItemAction from '../classes/livechat/ReplaceChatItemAction.ts'; -import type ReplayChatItemAction from '../classes/livechat/ReplayChatItemAction.ts'; -import type ShowLiveChatActionPanelAction from '../classes/livechat/ShowLiveChatActionPanelAction.ts'; - import UpdateDateTextAction from '../classes/livechat/UpdateDateTextAction.ts'; import UpdateDescriptionAction from '../classes/livechat/UpdateDescriptionAction.ts'; import UpdateTitleAction from '../classes/livechat/UpdateTitleAction.ts'; import UpdateToggleButtonTextAction from '../classes/livechat/UpdateToggleButtonTextAction.ts'; import UpdateViewershipAction from '../classes/livechat/UpdateViewershipAction.ts'; +import NavigationEndpoint from '../classes/NavigationEndpoint.ts'; +import ItemMenu from './ItemMenu.ts'; +import type { ObservedArray, YTNode } from '../helpers.ts'; + +import type VideoInfo from './VideoInfo.ts'; import type AddBannerToLiveChatCommand from '../classes/livechat/AddBannerToLiveChatCommand.ts'; import type RemoveBannerForLiveChatCommand from '../classes/livechat/RemoveBannerForLiveChatCommand.ts'; import type ShowLiveChatTooltipCommand from '../classes/livechat/ShowLiveChatTooltipCommand.ts'; - -import * as Proto from '../../proto/index.ts'; -import { InnertubeError, Platform } from '../../utils/Utils.ts'; -import type { ObservedArray, YTNode } from '../helpers.ts'; - -import type Button from '../classes/Button.ts'; import type LiveChatAutoModMessage from '../classes/livechat/items/LiveChatAutoModMessage.ts'; import type LiveChatMembershipItem from '../classes/livechat/items/LiveChatMembershipItem.ts'; import type LiveChatPaidMessage from '../classes/livechat/items/LiveChatPaidMessage.ts'; import type LiveChatPaidSticker from '../classes/livechat/items/LiveChatPaidSticker.ts'; import type LiveChatTextMessage from '../classes/livechat/items/LiveChatTextMessage.ts'; import type LiveChatViewerEngagementMessage from '../classes/livechat/items/LiveChatViewerEngagementMessage.ts'; -import ItemMenu from './ItemMenu.ts'; +import type AddLiveChatTickerItemAction from '../classes/livechat/AddLiveChatTickerItemAction.ts'; +import type MarkChatItemAsDeletedAction from '../classes/livechat/MarkChatItemAsDeletedAction.ts'; +import type MarkChatItemsByAuthorAsDeletedAction from '../classes/livechat/MarkChatItemsByAuthorAsDeletedAction.ts'; +import type ReplaceChatItemAction from '../classes/livechat/ReplaceChatItemAction.ts'; +import type ReplayChatItemAction from '../classes/livechat/ReplayChatItemAction.ts'; +import type ShowLiveChatActionPanelAction from '../classes/livechat/ShowLiveChatActionPanelAction.ts'; +import type Button from '../classes/Button.ts'; -import type Actions from '../../core/Actions.ts'; -import type { IParsedResponse, IUpdatedMetadataResponse } from '../types/ParsedResponse.ts'; -import { NavigationEndpoint } from '../nodes.ts'; +import type { Actions } from '../../core/index.ts'; +import type { IParsedResponse, IUpdatedMetadataResponse } from '../types/index.ts'; export type ChatAction = AddChatItemAction | AddBannerToLiveChatCommand | AddLiveChatTickerItemAction | diff --git a/deno/src/parser/youtube/NotificationsMenu.ts b/deno/src/parser/youtube/NotificationsMenu.ts index 5095e688..5ca4b89c 100644 --- a/deno/src/parser/youtube/NotificationsMenu.ts +++ b/deno/src/parser/youtube/NotificationsMenu.ts @@ -1,13 +1,12 @@ import { Parser } from '../index.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; import ContinuationItem from '../classes/ContinuationItem.ts'; import SimpleMenuHeader from '../classes/menus/SimpleMenuHeader.ts'; import Notification from '../classes/Notification.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { IGetNotificationsMenuResponse } from '../types/ParsedResponse.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type { IGetNotificationsMenuResponse } from '../types/index.ts'; class NotificationsMenu { #page: IGetNotificationsMenuResponse; diff --git a/deno/src/parser/youtube/Playlist.ts b/deno/src/parser/youtube/Playlist.ts index 024f518e..a83d6dc0 100644 --- a/deno/src/parser/youtube/Playlist.ts +++ b/deno/src/parser/youtube/Playlist.ts @@ -1,7 +1,7 @@ +import { InnertubeError } from '../../utils/Utils.ts'; + import Feed from '../../core/mixins/Feed.ts'; import Message from '../classes/Message.ts'; -import type Thumbnail from '../classes/misc/Thumbnail.ts'; -import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; import PlaylistCustomThumbnail from '../classes/PlaylistCustomThumbnail.ts'; import PlaylistHeader from '../classes/PlaylistHeader.ts'; import PlaylistMetadata from '../classes/PlaylistMetadata.ts'; @@ -10,13 +10,15 @@ import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInf import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail.ts'; import VideoOwner from '../classes/VideoOwner.ts'; import Alert from '../classes/Alert.ts'; +import ContinuationItem from '../classes/ContinuationItem.ts'; +import PlaylistVideo from '../classes/PlaylistVideo.ts'; +import SectionList from '../classes/SectionList.ts'; +import { observe, type ObservedArray } from '../helpers.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; -import type { ObservedArray } from '../helpers.ts'; - -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type Thumbnail from '../classes/misc/Thumbnail.ts'; +import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; export default class Playlist extends Feed { info; @@ -64,8 +66,38 @@ export default class Playlist extends Feed { return primary_info.stats[index]?.toString() || 'N/A'; } - get items() { - return this.videos; + get items(): ObservedArray { + return observe(this.videos.as(PlaylistVideo).filter((video) => video.style !== 'PLAYLIST_VIDEO_RENDERER_STYLE_RECOMMENDED_VIDEO')); + } + + get has_continuation() { + const section_list = this.memo.getType(SectionList).first(); + + if (!section_list) + return super.has_continuation; + + return !!this.memo.getType(ContinuationItem).find((node) => !section_list.contents.includes(node)); + } + + async getContinuationData(): Promise { + const section_list = this.memo.getType(SectionList).first(); + + /** + * No section list means there can't be additional continuation nodes here, + * so no need to check. + */ + if (!section_list) + return await super.getContinuationData(); + + const playlist_contents_continuation = this.memo.getType(ContinuationItem) + .find((node) => !section_list.contents.includes(node)); + + if (!playlist_contents_continuation) + throw new InnertubeError('There are no continuations.'); + + const response = await playlist_contents_continuation.endpoint.call(this.actions, { parse: true }); + + return response; } async getContinuation(): Promise { diff --git a/deno/src/parser/youtube/Search.ts b/deno/src/parser/youtube/Search.ts index c5a2ac66..8a7e5848 100644 --- a/deno/src/parser/youtube/Search.ts +++ b/deno/src/parser/youtube/Search.ts @@ -8,10 +8,10 @@ import SearchSubMenu from '../classes/SearchSubMenu.ts'; import SectionList from '../classes/SectionList.ts'; import UniversalWatchCard from '../classes/UniversalWatchCard.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import type { ISearchResponse } from '../types/ParsedResponse.ts'; +import type { ISearchResponse } from '../types/index.ts'; + class Search extends Feed { header?: SearchHeader; results?: ObservedArray | null; diff --git a/deno/src/parser/youtube/Settings.ts b/deno/src/parser/youtube/Settings.ts index 823c6a6a..11d1265b 100644 --- a/deno/src/parser/youtube/Settings.ts +++ b/deno/src/parser/youtube/Settings.ts @@ -13,9 +13,9 @@ import ItemSectionHeader from '../classes/ItemSectionHeader.ts'; import ItemSectionTabbedHeader from '../classes/ItemSectionTabbedHeader.ts'; import Tab from '../classes/Tab.ts'; import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; + +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type { IBrowseResponse } from '../types/index.ts'; class Settings { #page: IBrowseResponse; diff --git a/deno/src/parser/youtube/TimeWatched.ts b/deno/src/parser/youtube/TimeWatched.ts index cf38cf57..0bc18132 100644 --- a/deno/src/parser/youtube/TimeWatched.ts +++ b/deno/src/parser/youtube/TimeWatched.ts @@ -4,9 +4,9 @@ import SectionList from '../classes/SectionList.ts'; import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.ts'; import { InnertubeError } from '../../utils/Utils.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse } from '../../core/index.ts'; import type { ObservedArray } from '../helpers.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { IBrowseResponse } from '../types/index.ts'; class TimeWatched { #page: IBrowseResponse; diff --git a/deno/src/parser/youtube/TranscriptInfo.ts b/deno/src/parser/youtube/TranscriptInfo.ts index 574586e3..00168eaf 100644 --- a/deno/src/parser/youtube/TranscriptInfo.ts +++ b/deno/src/parser/youtube/TranscriptInfo.ts @@ -1,9 +1,9 @@ -import type Actions from '../../core/Actions.ts'; -import { type ApiResponse } from '../../core/Actions.ts'; -import type { IGetTranscriptResponse } from '../index.ts'; import { Parser } from '../index.ts'; import Transcript from '../classes/Transcript.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type { IGetTranscriptResponse } from '../index.ts'; + export default class TranscriptInfo { #page: IGetTranscriptResponse; #actions: Actions; diff --git a/deno/src/parser/youtube/VideoInfo.ts b/deno/src/parser/youtube/VideoInfo.ts index dedc8c2d..2a15489b 100644 --- a/deno/src/parser/youtube/VideoInfo.ts +++ b/deno/src/parser/youtube/VideoInfo.ts @@ -1,3 +1,6 @@ +import { InnertubeError } from '../../utils/Utils.ts'; +import { MediaInfo } from '../../core/mixins/index.ts'; + import ChipCloud from '../classes/ChipCloud.ts'; import ChipCloudChip from '../classes/ChipCloudChip.ts'; import CommentsEntryPointHeader from '../classes/comments/CommentsEntryPointHeader.ts'; @@ -19,8 +22,11 @@ import VideoPrimaryInfo from '../classes/VideoPrimaryInfo.ts'; import VideoSecondaryInfo from '../classes/VideoSecondaryInfo.ts'; import NavigationEndpoint from '../classes/NavigationEndpoint.ts'; import PlayerLegacyDesktopYpcTrailer from '../classes/PlayerLegacyDesktopYpcTrailer.ts'; +import StructuredDescriptionContent from '../classes/StructuredDescriptionContent.ts'; +import VideoDescriptionMusicSection from '../classes/VideoDescriptionMusicSection.ts'; import LiveChatWrap from './LiveChat.ts'; +import type { RawNode } from '../index.ts'; import type CardCollection from '../classes/CardCollection.ts'; import type Endscreen from '../classes/Endscreen.ts'; import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.ts'; @@ -28,16 +34,9 @@ import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.ts' import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.ts'; import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; -import { MediaInfo } from '../../core/mixins/index.ts'; -import StructuredDescriptionContent from '../classes/StructuredDescriptionContent.ts'; -import { VideoDescriptionMusicSection } from '../nodes.ts'; -import type { RawNode } from '../index.ts'; - class VideoInfo extends MediaInfo { #watch_next_continuation?: ContinuationItem; @@ -88,6 +87,7 @@ class VideoInfo extends MediaInfo { category: info.microformat?.is(PlayerMicroformat) ? info.microformat?.category : null, has_ypc_metadata: info.microformat?.is(PlayerMicroformat) ? info.microformat?.has_ypc_metadata : null, start_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.start_timestamp : null, + end_timestamp: info.microformat?.is(PlayerMicroformat) ? info.microformat.end_timestamp : null, view_count: info.microformat?.is(PlayerMicroformat) && isNaN(info.video_details?.view_count as number) ? info.microformat.view_count : info.video_details?.view_count }, like_count: undefined as number | undefined, @@ -223,7 +223,6 @@ class VideoInfo extends MediaInfo { return super.addToWatchHistory(); } - /** * Retrieves watch next feed continuation. */ diff --git a/deno/src/parser/youtube/index.ts b/deno/src/parser/youtube/index.ts index 47547752..a0754b33 100644 --- a/deno/src/parser/youtube/index.ts +++ b/deno/src/parser/youtube/index.ts @@ -17,3 +17,4 @@ export { default as Settings } from './Settings.ts'; export { default as SmoothedQueue } from './SmoothedQueue.ts'; export { default as TimeWatched } from './TimeWatched.ts'; export { default as VideoInfo } from './VideoInfo.ts'; +export { default as TranscriptInfo } from './TranscriptInfo.ts'; \ No newline at end of file diff --git a/deno/src/parser/ytkids/Channel.ts b/deno/src/parser/ytkids/Channel.ts index d3696818..c8039b8c 100644 --- a/deno/src/parser/ytkids/Channel.ts +++ b/deno/src/parser/ytkids/Channel.ts @@ -2,9 +2,9 @@ import Feed from '../../core/mixins/Feed.ts'; import C4TabbedHeader from '../classes/C4TabbedHeader.ts'; import ItemSection from '../classes/ItemSection.ts'; import { ItemSectionContinuation } from '../index.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; + +import type { IBrowseResponse } from '../types/index.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; class Channel extends Feed { header?: C4TabbedHeader; diff --git a/deno/src/parser/ytkids/HomeFeed.ts b/deno/src/parser/ytkids/HomeFeed.ts index 41243763..e3464585 100644 --- a/deno/src/parser/ytkids/HomeFeed.ts +++ b/deno/src/parser/ytkids/HomeFeed.ts @@ -1,11 +1,11 @@ +import { InnertubeError } from '../../utils/Utils.ts'; + import Feed from '../../core/mixins/Feed.ts'; import KidsCategoriesHeader from '../classes/ytkids/KidsCategoriesHeader.ts'; import KidsCategoryTab from '../classes/ytkids/KidsCategoryTab.ts'; import KidsHomeScreen from '../classes/ytkids/KidsHomeScreen.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { IBrowseResponse } from '../types/ParsedResponse.ts'; class HomeFeed extends Feed { diff --git a/deno/src/parser/ytkids/Search.ts b/deno/src/parser/ytkids/Search.ts index c5d315c4..845738e3 100644 --- a/deno/src/parser/ytkids/Search.ts +++ b/deno/src/parser/ytkids/Search.ts @@ -1,10 +1,10 @@ import Feed from '../../core/mixins/Feed.ts'; import ItemSection from '../classes/ItemSection.ts'; import { InnertubeError } from '../../utils/Utils.ts'; -import type Actions from '../../core/Actions.ts'; + +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import type { ISearchResponse } from '../types/ParsedResponse.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ISearchResponse } from '../types/index.ts'; class Search extends Feed { estimated_results: number; diff --git a/deno/src/parser/ytkids/VideoInfo.ts b/deno/src/parser/ytkids/VideoInfo.ts index 2d7df3f9..9ceb8b75 100644 --- a/deno/src/parser/ytkids/VideoInfo.ts +++ b/deno/src/parser/ytkids/VideoInfo.ts @@ -1,13 +1,12 @@ +import { MediaInfo } from '../../core/mixins/index.ts'; import ItemSection from '../classes/ItemSection.ts'; -import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; import PlayerOverlay from '../classes/PlayerOverlay.ts'; import SlimVideoMetadata from '../classes/SlimVideoMetadata.ts'; import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import { MediaInfo } from '../../core/mixins/index.ts'; +import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; class VideoInfo extends MediaInfo { basic_info; diff --git a/deno/src/parser/ytmusic/Album.ts b/deno/src/parser/ytmusic/Album.ts index 4d8dfa67..37eeee89 100644 --- a/deno/src/parser/ytmusic/Album.ts +++ b/deno/src/parser/ytmusic/Album.ts @@ -1,5 +1,3 @@ -import type { ApiResponse } from '../../core/Actions.ts'; -import type { ObservedArray } from '../helpers.ts'; import { Parser } from '../index.ts'; import MicroformatData from '../classes/MicroformatData.ts'; @@ -7,8 +5,10 @@ import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import MusicDetailHeader from '../classes/MusicDetailHeader.ts'; import MusicShelf from '../classes/MusicShelf.ts'; +import type { ApiResponse } from '../../core/index.ts'; +import type { ObservedArray } from '../helpers.ts'; +import type { IBrowseResponse } from '../types/index.ts'; import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; class Album { #page: IBrowseResponse; diff --git a/deno/src/parser/ytmusic/Artist.ts b/deno/src/parser/ytmusic/Artist.ts index fed9da71..45729fa7 100644 --- a/deno/src/parser/ytmusic/Artist.ts +++ b/deno/src/parser/ytmusic/Artist.ts @@ -1,6 +1,4 @@ import { Parser } from '../index.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import MusicShelf from '../classes/MusicShelf.ts'; @@ -9,6 +7,8 @@ import MusicPlaylistShelf from '../classes/MusicPlaylistShelf.ts'; import MusicImmersiveHeader from '../classes/MusicImmersiveHeader.ts'; import MusicVisualHeader from '../classes/MusicVisualHeader.ts'; import MusicHeader from '../classes/MusicHeader.ts'; + +import type { ApiResponse, Actions } from '../../core/index.ts'; import type { IBrowseResponse } from '../types/ParsedResponse.ts'; class Artist { diff --git a/deno/src/parser/ytmusic/Explore.ts b/deno/src/parser/ytmusic/Explore.ts index df4d5c02..2a21e15b 100644 --- a/deno/src/parser/ytmusic/Explore.ts +++ b/deno/src/parser/ytmusic/Explore.ts @@ -1,4 +1,5 @@ import { Parser } from '../index.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; import Grid from '../classes/Grid.ts'; import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; @@ -6,10 +7,9 @@ import MusicNavigationButton from '../classes/MusicNavigationButton.ts'; import SectionList from '../classes/SectionList.ts'; import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; +import type { ApiResponse } from '../../core/index.ts'; import type { ObservedArray } from '../helpers.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { IBrowseResponse } from '../types/index.ts'; class Explore { #page: IBrowseResponse; diff --git a/deno/src/parser/ytmusic/HomeFeed.ts b/deno/src/parser/ytmusic/HomeFeed.ts index 45dc0dad..acd2cc45 100644 --- a/deno/src/parser/ytmusic/HomeFeed.ts +++ b/deno/src/parser/ytmusic/HomeFeed.ts @@ -1,17 +1,16 @@ +import { InnertubeError } from '../../utils/Utils.ts'; import { Parser, SectionListContinuation } from '../index.ts'; import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import SectionList from '../classes/SectionList.ts'; import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.ts'; import MusicTastebuilderShelf from '../classes/MusicTastebuilderShelf.ts'; - -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { ObservedArray } from '../helpers.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; import ChipCloud from '../classes/ChipCloud.ts'; import ChipCloudChip from '../classes/ChipCloudChip.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type { ObservedArray } from '../helpers.ts'; +import type { IBrowseResponse } from '../types/index.ts'; + class HomeFeed { #page: IBrowseResponse; #actions: Actions; diff --git a/deno/src/parser/ytmusic/Library.ts b/deno/src/parser/ytmusic/Library.ts index 88d4cc70..13a7e47a 100644 --- a/deno/src/parser/ytmusic/Library.ts +++ b/deno/src/parser/ytmusic/Library.ts @@ -1,6 +1,4 @@ import { Parser, GridContinuation, MusicShelfContinuation, SectionListContinuation } from '../index.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; import Grid from '../classes/Grid.ts'; import MusicShelf from '../classes/MusicShelf.ts'; @@ -12,11 +10,12 @@ import ChipCloud from '../classes/ChipCloud.ts'; import ChipCloudChip from '../classes/ChipCloudChip.ts'; import MusicMultiSelectMenuItem from '../classes/menus/MusicMultiSelectMenuItem.ts'; import MusicSortFilterButton from '../classes/MusicSortFilterButton.ts'; -import type MusicMenuItemDivider from '../classes/menus/MusicMenuItemDivider.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import type { ObservedArray } from '../helpers.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { IBrowseResponse } from '../types/index.ts'; +import type MusicMenuItemDivider from '../classes/menus/MusicMenuItemDivider.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; class Library { #page: IBrowseResponse; diff --git a/deno/src/parser/ytmusic/Playlist.ts b/deno/src/parser/ytmusic/Playlist.ts index c4d4918d..37c351d8 100644 --- a/deno/src/parser/ytmusic/Playlist.ts +++ b/deno/src/parser/ytmusic/Playlist.ts @@ -4,15 +4,14 @@ import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import MusicDetailHeader from '../classes/MusicDetailHeader.ts'; import MusicEditablePlaylistDetailHeader from '../classes/MusicEditablePlaylistDetailHeader.ts'; import MusicPlaylistShelf from '../classes/MusicPlaylistShelf.ts'; -import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts'; import MusicShelf from '../classes/MusicShelf.ts'; import SectionList from '../classes/SectionList.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type { IBrowseResponse } from '../types/index.ts'; +import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts'; class Playlist { #page: IBrowseResponse; diff --git a/deno/src/parser/ytmusic/Recap.ts b/deno/src/parser/ytmusic/Recap.ts index a509feb7..9abb320f 100644 --- a/deno/src/parser/ytmusic/Recap.ts +++ b/deno/src/parser/ytmusic/Recap.ts @@ -1,22 +1,20 @@ import { Parser } from '../index.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; +import Playlist from './Playlist.ts'; import HighlightsCarousel from '../classes/HighlightsCarousel.ts'; import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import MusicElementHeader from '../classes/MusicElementHeader.ts'; import MusicHeader from '../classes/MusicHeader.ts'; import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.ts'; -import Playlist from './Playlist.ts'; - import ItemSection from '../classes/ItemSection.ts'; import Message from '../classes/Message.ts'; import SectionList from '../classes/SectionList.ts'; import Tab from '../classes/Tab.ts'; -import { InnertubeError } from '../../utils/Utils.ts'; import type { ObservedArray } from '../helpers.ts'; -import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import type { IBrowseResponse } from '../types/index.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; class Recap { #page: IBrowseResponse; diff --git a/deno/src/parser/ytmusic/Search.ts b/deno/src/parser/ytmusic/Search.ts index ab729c77..34dd4f6b 100644 --- a/deno/src/parser/ytmusic/Search.ts +++ b/deno/src/parser/ytmusic/Search.ts @@ -1,4 +1,3 @@ -import type Actions from '../../core/Actions.ts'; import { InnertubeError } from '../../utils/Utils.ts'; import { Parser, MusicShelfContinuation } from '../index.ts'; @@ -9,7 +8,6 @@ import ItemSection from '../classes/ItemSection.ts'; import Message from '../classes/Message.ts'; import MusicCardShelf from '../classes/MusicCardShelf.ts'; import MusicHeader from '../classes/MusicHeader.ts'; -import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts'; import MusicShelf from '../classes/MusicShelf.ts'; import SectionList from '../classes/SectionList.ts'; import ShowingResultsFor from '../classes/ShowingResultsFor.ts'; @@ -17,7 +15,8 @@ import TabbedSearchResults from '../classes/TabbedSearchResults.ts'; import type { ObservedArray } from '../helpers.ts'; import type { ISearchResponse } from '../types/ParsedResponse.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.ts'; export default class Search { #page: ISearchResponse; diff --git a/deno/src/parser/ytmusic/TrackInfo.ts b/deno/src/parser/ytmusic/TrackInfo.ts index 7c8b2497..b2f34d0e 100644 --- a/deno/src/parser/ytmusic/TrackInfo.ts +++ b/deno/src/parser/ytmusic/TrackInfo.ts @@ -1,28 +1,26 @@ -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; - -import * as Constants from '../../utils/Constants.ts'; +import { Constants } from '../../utils/index.ts'; import { InnertubeError } from '../../utils/Utils.ts'; +import { MediaInfo } from '../../core/mixins/index.ts'; +import Tab from '../classes/Tab.ts'; import AutomixPreviewVideo from '../classes/AutomixPreviewVideo.ts'; -import type Endscreen from '../classes/Endscreen.ts'; import Message from '../classes/Message.ts'; import MicroformatData from '../classes/MicroformatData.ts'; -import type MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import MusicDescriptionShelf from '../classes/MusicDescriptionShelf.ts'; -import type MusicQueue from '../classes/MusicQueue.ts'; import PlayerOverlay from '../classes/PlayerOverlay.ts'; import PlaylistPanel from '../classes/PlaylistPanel.ts'; -import type RichGrid from '../classes/RichGrid.ts'; import SectionList from '../classes/SectionList.ts'; -import Tab from '../classes/Tab.ts'; import WatchNextTabbedResults from '../classes/WatchNextTabbedResults.ts'; +import type RichGrid from '../classes/RichGrid.ts'; +import type MusicQueue from '../classes/MusicQueue.ts'; +import type Endscreen from '../classes/Endscreen.ts'; +import type MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.ts'; import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.ts'; import type { ObservedArray, YTNode } from '../helpers.ts'; -import { MediaInfo } from '../../core/mixins/index.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; class TrackInfo extends MediaInfo { basic_info; diff --git a/deno/src/parser/ytshorts/VideoInfo.ts b/deno/src/parser/ytshorts/VideoInfo.ts index e64ae118..67c894d2 100644 --- a/deno/src/parser/ytshorts/VideoInfo.ts +++ b/deno/src/parser/ytshorts/VideoInfo.ts @@ -1,13 +1,11 @@ -import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; -import type PlayerOverlay from '../classes/PlayerOverlay.ts'; -import type Actions from '../../core/Actions.ts'; -import type { ApiResponse } from '../../core/Actions.ts'; -import type { ObservedArray, YTNode } from '../helpers.ts'; import { Parser, ContinuationCommand } from '../index.ts'; import { InnertubeError } from '../../utils/Utils.ts'; -import { - Reel -} from '../../core/endpoints/index.ts'; +import { Reel } from '../../core/endpoints/index.ts'; + +import type NavigationEndpoint from '../classes/NavigationEndpoint.ts'; +import type PlayerOverlay from '../classes/PlayerOverlay.ts'; +import type { ApiResponse, Actions } from '../../core/index.ts'; +import type { ObservedArray, YTNode } from '../helpers.ts'; class VideoInfo { #watch_next_continuation?: ContinuationCommand; @@ -23,12 +21,12 @@ class VideoInfo { const info = Parser.parseResponse(data[0].data); - const watchNext = Parser.parseResponse(data[1].data); + const watch_next = Parser.parseResponse(data[1].data); this.basic_info = info.video_details; - this.watch_next_feed = watchNext.entries?.array(); - this.#watch_next_continuation = watchNext.continuationEndpoint?.as(ContinuationCommand); + this.watch_next_feed = watch_next.entries?.array(); + this.#watch_next_continuation = watch_next.continuation_endpoint?.as(ContinuationCommand); } /** @@ -49,7 +47,7 @@ class VideoInfo { const parsed = Parser.parseResponse(response.data); this.watch_next_feed = parsed.entries?.array(); - this.#watch_next_continuation = parsed.continuationEndpoint?.as(ContinuationCommand); + this.#watch_next_continuation = parsed.continuation_endpoint?.as(ContinuationCommand); return this; } diff --git a/deno/src/platform/jsruntime/jinter.ts b/deno/src/platform/jsruntime/jinter.ts index f3fa55d8..69234de6 100644 --- a/deno/src/platform/jsruntime/jinter.ts +++ b/deno/src/platform/jsruntime/jinter.ts @@ -1,10 +1,21 @@ import { Jinter } from 'https://esm.sh/jintr'; import type { VMPrimative } from '../../types/PlatformShim.ts'; +import { Log } from '../lib.ts'; + +const TAG = 'JsRuntime'; export default function evaluate(code: string, env: Record) { + Log.info(TAG, 'Evaluating JavaScript.\n', code); + const runtime = new Jinter(code); + for (const [ key, value ] of Object.entries(env)) { runtime.scope.set(key, value); } - return runtime.interpret(); + + const result = runtime.interpret(); + + Log.info(TAG, 'Done. Result:', result); + + return result; } \ No newline at end of file diff --git a/deno/src/platform/web.ts b/deno/src/platform/web.ts index a40c0b82..11233d9d 100644 --- a/deno/src/platform/web.ts +++ b/deno/src/platform/web.ts @@ -4,8 +4,10 @@ import { Platform } from '../utils/Utils.ts'; import sha1Hash from './polyfills/web-crypto.ts'; import package_json from '../../package.json' assert { type: 'json' }; import evaluate from './jsruntime/jinter.ts'; +import Log from '../utils/Log.ts'; class Cache implements ICache { + #TAG = 'Cache'; #persistent_directory: string; #persistent: boolean; @@ -21,7 +23,7 @@ class Cache implements ICache { #getBrowserDB() { const indexedDB: IDBFactory = Reflect.get(globalThis, 'indexedDB') || Reflect.get(globalThis, 'webkitIndexedDB') || Reflect.get(globalThis, 'mozIndexedDB') || Reflect.get(globalThis, 'msIndexedDB'); - if (!indexedDB) return console.log('IndexedDB is not supported. No cache will be used.'); + if (!indexedDB) return Log.warn(this.#TAG, 'IndexedDB is not supported. No cache will be used.'); return new Promise((resolve, reject) => { const request = indexedDB.open('youtubei.js', 1); diff --git a/deno/src/proto/index.ts b/deno/src/proto/index.ts index e59235c0..f12ba57f 100644 --- a/deno/src/proto/index.ts +++ b/deno/src/proto/index.ts @@ -22,7 +22,7 @@ export function encodeVisitorData(id: string, timestamp: number): string { } export function decodeVisitorData(visitor_data: string): VisitorData.Type { - const data = VisitorData.decodeBinary(base64ToU8(decodeURIComponent(visitor_data))); + const data = VisitorData.decodeBinary(base64ToU8(decodeURIComponent(visitor_data).replace(/-/g, '+').replace(/_/g, '/'))); return data; } diff --git a/deno/src/utils/Constants.ts b/deno/src/utils/Constants.ts index aae19d9c..869d2106 100644 --- a/deno/src/utils/Constants.ts +++ b/deno/src/utils/Constants.ts @@ -41,7 +41,7 @@ export const CLIENTS = Object.freeze({ }, WEB: { NAME: 'WEB', - VERSION: '2.20230622.06.00', + VERSION: '2.20240111.09.00', API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', API_VERSION: 'v1', STATIC_VISITOR_ID: '6zpwvWUNAco' diff --git a/deno/src/utils/DashManifest.js b/deno/src/utils/DashManifest.js index 47377bb1..5a7f8e97 100644 --- a/deno/src/utils/DashManifest.js +++ b/deno/src/utils/DashManifest.js @@ -3,12 +3,12 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable: import * as DashUtils from "./DashUtils.ts"; import { getStreamingInfo } from "./StreamingInfo.ts"; import { InnertubeError } from "./Utils.ts"; -async function OTFSegmentInfo({ info }) { - if (!info.is_oft) +async function OTFPostLiveDvrSegmentInfo({ info }) { + if (!info.is_oft && !info.is_post_live_dvr) return null; const template = await info.getSegmentTemplate(); return /* @__PURE__ */ DashUtils.createElement("segment-template", { - startNumber: "1", + startNumber: template.init_url ? "1" : "0", timescale: "1000", initialization: template.init_url, media: template.media_url @@ -17,10 +17,10 @@ async function OTFSegmentInfo({ info }) { r: segment_duration.repeat_count })))); } -__name(OTFSegmentInfo, "OTFSegmentInfo"); +__name(OTFPostLiveDvrSegmentInfo, "OTFPostLiveDvrSegmentInfo"); function SegmentInfo({ info }) { - if (info.is_oft) { - return /* @__PURE__ */ DashUtils.createElement(OTFSegmentInfo, { + if (info.is_oft || info.is_post_live_dvr) { + return /* @__PURE__ */ DashUtils.createElement(OTFPostLiveDvrSegmentInfo, { info }); } @@ -31,8 +31,9 @@ function SegmentInfo({ info }) { }))); } __name(SegmentInfo, "SegmentInfo"); -function DashManifest({ +async function DashManifest({ streamingData, + isPostLiveDvr, transformURL, rejectFormat, cpn, @@ -41,17 +42,17 @@ function DashManifest({ storyboards }) { const { - duration, + getDuration, audio_sets, video_sets, image_sets - } = getStreamingInfo(streamingData, transformURL, rejectFormat, cpn, player, actions, storyboards); + } = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards); return /* @__PURE__ */ DashUtils.createElement("mpd", { xmlns: "urn:mpeg:dash:schema:mpd:2011", minBufferTime: "PT1.500S", profiles: "urn:mpeg:dash:profile:isoff-main:2011", type: "static", - mediaPresentationDuration: `PT${duration}S`, + mediaPresentationDuration: `PT${await getDuration()}S`, "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation": "urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd" }, /* @__PURE__ */ DashUtils.createElement("period", null, audio_sets.map((set, index) => /* @__PURE__ */ DashUtils.createElement("adaptation-set", { @@ -129,12 +130,13 @@ function DashManifest({ }))); } __name(DashManifest, "DashManifest"); -function toDash(streaming_data, url_transformer = (url) => url, format_filter, cpn, player, actions, storyboards) { +function toDash(streaming_data, is_post_live_dvr = false, url_transformer = (url) => url, format_filter, cpn, player, actions, storyboards) { if (!streaming_data) throw new InnertubeError("Streaming data not available"); return DashUtils.renderToString( /* @__PURE__ */ DashUtils.createElement(DashManifest, { streamingData: streaming_data, + isPostLiveDvr: is_post_live_dvr, transformURL: url_transformer, rejectFormat: format_filter, cpn, diff --git a/deno/src/utils/DashManifest.tsx b/deno/src/utils/DashManifest.tsx index 32183b63..20b17bbb 100644 --- a/deno/src/utils/DashManifest.tsx +++ b/deno/src/utils/DashManifest.tsx @@ -1,33 +1,36 @@ /* eslint-disable tsdoc/syntax */ /** @jsxFactory DashUtils.createElement */ /** @jsxFragmentFactory DashUtils.Fragment */ +import * as DashUtils from './DashUtils.ts'; +import { getStreamingInfo } from './StreamingInfo.ts'; +import { InnertubeError } from './Utils.ts'; + import type Actions from '../core/Actions.ts'; import type Player from '../core/Player.ts'; import type { IStreamingData } from '../parser/index.ts'; import type { PlayerStoryboardSpec } from '../parser/nodes.ts'; -import * as DashUtils from './DashUtils.ts'; import type { SegmentInfo as FSegmentInfo } from './StreamingInfo.ts'; -import { getStreamingInfo } from './StreamingInfo.ts'; import type { FormatFilter, URLTransformer } from '../types/FormatUtils.ts'; -import { InnertubeError } from './Utils.ts'; +import type PlayerLiveStoryboardSpec from '../parser/classes/PlayerLiveStoryboardSpec.ts'; interface DashManifestProps { streamingData: IStreamingData; + isPostLiveDvr: boolean; transformURL?: URLTransformer; rejectFormat?: FormatFilter; cpn?: string; player?: Player; actions?: Actions; - storyboards?: PlayerStoryboardSpec; + storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec; } -async function OTFSegmentInfo({ info }: { info: FSegmentInfo }) { - if (!info.is_oft) return null; +async function OTFPostLiveDvrSegmentInfo({ info }: { info: FSegmentInfo }) { + if (!info.is_oft && !info.is_post_live_dvr) return null; const template = await info.getSegmentTemplate(); return ; + if (info.is_oft || info.is_post_live_dvr) { + return ; } return <> @@ -59,8 +62,9 @@ function SegmentInfo({ info }: { info: FSegmentInfo }) { ; } -function DashManifest({ +async function DashManifest({ streamingData, + isPostLiveDvr, transformURL, rejectFormat, cpn, @@ -69,11 +73,11 @@ function DashManifest({ storyboards }: DashManifestProps) { const { - duration, + getDuration, audio_sets, video_sets, image_sets - } = getStreamingInfo(streamingData, transformURL, rejectFormat, cpn, player, actions, storyboards); + } = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards); // XXX: DASH spec: https://standards.iso.org/ittf/PubliclyAvailableStandards/c083314_ISO_IEC%2023009-1_2022(en).zip @@ -82,7 +86,7 @@ function DashManifest({ minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="static" - mediaPresentationDuration={`PT${duration}S`} + mediaPresentationDuration={`PT${await getDuration()}S`} xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd" > @@ -227,12 +231,13 @@ function DashManifest({ export function toDash( streaming_data?: IStreamingData, + is_post_live_dvr = false, url_transformer: URLTransformer = (url) => url, format_filter?: FormatFilter, cpn?: string, player?: Player, actions?: Actions, - storyboards?: PlayerStoryboardSpec + storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec ) { if (!streaming_data) throw new InnertubeError('Streaming data not available'); @@ -240,6 +245,7 @@ export function toDash( return DashUtils.renderToString( console.error(...args), + [Log.Level.WARNING]: (...args: any[]) => console.warn(...args), + [Log.Level.INFO]: (...args: any[]) => console.info(...args), + [Log.Level.DEBUG]: (...args: any[]) => console.debug(...args) + }; + + private static log_level_ = [ Log.Level.WARNING ]; + private static one_time_warnings_issued_ = new Set(); + + static warnOnce = (id: string, ...args: any[]) => { + if (this.one_time_warnings_issued_.has(id)) + return; + this.doLog(Log.Level.WARNING, id, args); + this.one_time_warnings_issued_.add(id); + }; + + static warn = (tag?: string, ...args: any[]) => this.doLog(Log.Level.WARNING, tag, args); + static error = (tag?: string, ...args: any[]) => this.doLog(Log.Level.ERROR, tag, args); + static info = (tag?: string, ...args: any[]) => this.doLog(Log.Level.INFO, tag, args); + static debug = (tag?: string, ...args: any[]) => this.doLog(Log.Level.DEBUG, tag, args); + + private static doLog(level: number, tag?: string, args?: any[]) { + if (!this.log_map_[level] || !this.log_level_.includes(level)) + return; + + const tags = [ `[${this.YTJS_TAG}]` ]; + + if (tag) + tags.push(`[${tag}]`); + + this.log_map_[level](`${tags.join('')}:`, ...(args || [])); + } + + static setLevel(...args: number[]) { + this.log_level_ = args; + } +} \ No newline at end of file diff --git a/deno/src/utils/StreamingInfo.ts b/deno/src/utils/StreamingInfo.ts index 2e0603dd..402c6430 100644 --- a/deno/src/utils/StreamingInfo.ts +++ b/deno/src/utils/StreamingInfo.ts @@ -1,15 +1,19 @@ import type Actions from '../core/Actions.ts'; import type Player from '../core/Player.ts'; +import type { LiveStoryboardData } from '../parser/classes/PlayerLiveStoryboardSpec.ts'; import type { StoryboardData } from '../parser/classes/PlayerStoryboardSpec.ts'; import type { IStreamingData } from '../parser/index.ts'; import type { Format } from '../parser/misc.ts'; -import type { PlayerStoryboardSpec } from '../parser/nodes.ts'; +import type { PlayerLiveStoryboardSpec } from '../parser/nodes.ts'; import type { FormatFilter, URLTransformer } from '../types/FormatUtils.ts'; +import PlayerStoryboardSpec from '../parser/classes/PlayerStoryboardSpec.ts'; import { InnertubeError, Platform, getStringBetweenStrings } from './Utils.ts'; -import { Constants } from './index.ts'; +import { Constants, Log } from './index.ts'; + +const TAG_ = 'StreamingInfo'; export interface StreamingInfo { - duration: number; + getDuration(): Promise; audio_sets: AudioSet[]; video_sets: VideoSet[]; image_sets: ImageSet[]; @@ -33,11 +37,17 @@ export interface Range { export type SegmentInfo = { is_oft: false, + is_post_live_dvr: false base_url: string; index_range: Range; init_range: Range; } | { is_oft: true, + is_post_live_dvr: false + getSegmentTemplate(): Promise +} | { + is_oft: false, + is_post_live_dvr: true, getSegmentTemplate(): Promise } @@ -47,7 +57,7 @@ export interface Segment { } export interface SegmentTemplate { - init_url: string, + init_url?: string, media_url: string, timeline: Segment[] } @@ -109,13 +119,22 @@ export interface ImageRepresentation { getURL(n: number): string; } -function getFormatGroupings(formats: Format[]) { +interface PostLiveDvrInfo { + duration: number, + segment_count: number +} + +interface SharedPostLiveDvrInfo { + item?: PostLiveDvrInfo +} + +function getFormatGroupings(formats: Format[], is_post_live_dvr: boolean) { const group_info = new Map(); const has_multiple_audio_tracks = formats.some((fmt) => !!fmt.audio_track); for (const format of formats) { - if ((!format.index_range || !format.init_range) && !format.is_type_otf) { + if ((!format.index_range || !format.init_range) && !format.is_type_otf && !is_post_live_dvr) { continue; } const mime_type = format.mime_type.split(';')[0]; @@ -224,12 +243,53 @@ async function getOTFSegmentTemplate(url: string, actions: Actions): Promise { + const response = await actions.session.http.fetch_function(`${transformed_url}&rn=0&sq=0`, { + method: 'HEAD', + headers: Constants.STREAM_HEADERS, + redirect: 'follow' + }); + + const duration_ms = parseInt(response.headers.get('X-Head-Time-Millis') || ''); + const segment_count = parseInt(response.headers.get('X-Head-Seqnum') || ''); + + if (isNaN(duration_ms) || isNaN(segment_count)) { + throw new InnertubeError('Failed to extract the duration or segment count for this Post Live DVR video'); + } + + return { + duration: duration_ms / 1000, + segment_count + }; +} + +async function getPostLiveDvrDuration( + shared_post_live_dvr_info: SharedPostLiveDvrInfo, + format: Format, + url_transformer: URLTransformer, + actions: Actions, + player?: Player, + cpn?: string +) { + if (!shared_post_live_dvr_info.item) { + const url = new URL(format.decipher(player)); + url.searchParams.set('cpn', cpn || ''); + + const transformed_url = url_transformer(url).toString(); + + shared_post_live_dvr_info.item = await getPostLiveDvrInfo(transformed_url, actions); + } + + return shared_post_live_dvr_info.item.duration; +} + function getSegmentInfo( format: Format, url_transformer: URLTransformer, actions?: Actions, player?: Player, - cpn?: string + cpn?: string, + shared_post_live_dvr_info?: SharedPostLiveDvrInfo ) { const url = new URL(format.decipher(player)); url.searchParams.set('cpn', cpn || ''); @@ -242,6 +302,7 @@ function getSegmentInfo( const info: SegmentInfo = { is_oft: true, + is_post_live_dvr: false, getSegmentTemplate() { return getOTFSegmentTemplate(transformed_url, actions); } @@ -250,11 +311,46 @@ function getSegmentInfo( return info; } + if (shared_post_live_dvr_info) { + if (!actions) { + throw new InnertubeError('Unable to get segment count for this Post Live DVR video without an Actions instance', { format }); + } + + const target_duration_dec = format.target_duration_dec; + + if (typeof target_duration_dec !== 'number') { + throw new InnertubeError('Format is missing target_duration_dec', { format }); + } + + const info: SegmentInfo = { + is_oft: false, + is_post_live_dvr: true, + async getSegmentTemplate(): Promise { + if (!shared_post_live_dvr_info.item) { + shared_post_live_dvr_info.item = await getPostLiveDvrInfo(transformed_url, actions); + } + + return { + media_url: `${transformed_url}&sq=$Number$`, + timeline: [ + { + duration: target_duration_dec * 1000, + repeat_count: shared_post_live_dvr_info.item.segment_count + } + ] + }; + } + }; + + return info; + } + if (!format.index_range || !format.init_range) throw new InnertubeError('Index and init ranges not available', { format }); const info: SegmentInfo = { is_oft: false, + is_post_live_dvr: false, base_url: transformed_url, index_range: format.index_range, init_range: format.init_range @@ -269,7 +365,8 @@ function getAudioRepresentation( url_transformer: URLTransformer, actions?: Actions, player?: Player, - cpn?: string + cpn?: string, + shared_post_live_dvr_info?: SharedPostLiveDvrInfo ) { const url = new URL(format.decipher(player)); url.searchParams.set('cpn', cpn || ''); @@ -280,7 +377,7 @@ function getAudioRepresentation( codecs: !hoisted.includes('codecs') ? getStringBetweenStrings(format.mime_type, 'codecs="', '"') : undefined, audio_sample_rate: !hoisted.includes('audio_sample_rate') ? format.audio_sample_rate : undefined, channels: !hoisted.includes('AudioChannelConfiguration') ? format.audio_channels || 2 : undefined, - segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn) + segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn, shared_post_live_dvr_info) }; return rep; @@ -309,7 +406,8 @@ function getAudioSet( url_transformer: URLTransformer, actions?: Actions, player?: Player, - cpn?: string + cpn?: string, + shared_post_live_dvr_info?: SharedPostLiveDvrInfo ) { const first_format = formats[0]; const { audio_track } = first_format; @@ -323,7 +421,7 @@ function getAudioSet( track_name: audio_track?.display_name, track_role: getTrackRole(first_format), channels: hoistAudioChannelsIfPossible(formats, hoisted), - representations: formats.map((format) => getAudioRepresentation(format, hoisted, url_transformer, actions, player, cpn)) + representations: formats.map((format) => getAudioRepresentation(format, hoisted, url_transformer, actions, player, cpn, shared_post_live_dvr_info)) }; return set; @@ -354,26 +452,38 @@ function getColorInfo(format: Format) { // The player.js file was also helpful const color_info = format.color_info; - const primaries = - color_info?.primaries ? COLOR_PRIMARIES[color_info.primaries] : undefined; + let primaries; + let transfer_characteristics; + let matrix_coefficients; - const transfer_characteristics = - color_info?.transfer_characteristics ? COLOR_TRANSFER_CHARACTERISTICS[color_info.transfer_characteristics] : undefined; + if (color_info) { + if (color_info.primaries) { + primaries = COLOR_PRIMARIES[color_info.primaries]; + } - const matrix_coefficients = - color_info?.matrix_coefficients ? COLOR_MATRIX_COEFFICIENTS[color_info.matrix_coefficients] : undefined; + if (color_info.transfer_characteristics) { + transfer_characteristics = COLOR_TRANSFER_CHARACTERISTICS[color_info.transfer_characteristics]; + } else if (getStringBetweenStrings(format.mime_type, 'codecs="', '"')?.startsWith('avc1')) { + // YouTube's h264 streams always seem to be SDR, so this is a pretty safe bet. + transfer_characteristics = COLOR_TRANSFER_CHARACTERISTICS.BT709; + } - if (color_info?.matrix_coefficients && !matrix_coefficients) { - const url = new URL(format.url as string); + if (color_info.matrix_coefficients) { + matrix_coefficients = COLOR_MATRIX_COEFFICIENTS[color_info.matrix_coefficients]; - const anonymisedFormat = JSON.parse(JSON.stringify(format)); - anonymisedFormat.url = 'REDACTED'; - anonymisedFormat.signature_cipher = 'REDACTED'; - anonymisedFormat.cipher = 'REDACTED'; + if (!matrix_coefficients) { + const url = new URL(format.url as string); - console.warn(`YouTube.js toDash(): Unknown matrix coefficients "${color_info.matrix_coefficients}", the DASH manifest is still usuable without this.\n` - + `Please report it at ${Platform.shim.info.bugs_url} so we can add support for it.\n` - + `Innertube client: ${url.searchParams.get('c')}\nformat:`, anonymisedFormat); + const anonymisedFormat = JSON.parse(JSON.stringify(format)); + anonymisedFormat.url = 'REDACTED'; + anonymisedFormat.signature_cipher = 'REDACTED'; + anonymisedFormat.cipher = 'REDACTED'; + + Log.warn(TAG_, `Unknown matrix coefficients "${color_info.matrix_coefficients}", the DASH manifest is still usuable without this.\n` + + `Please report it at ${Platform.shim.info.bugs_url} so we can add support for it.\n` + + `InnerTube client: ${url.searchParams.get('c')}\nformat:`, anonymisedFormat); + } + } } const info: ColorInfo = { @@ -391,7 +501,8 @@ function getVideoRepresentation( hoisted: string[], player?: Player, actions?: Actions, - cpn?: string + cpn?: string, + shared_post_live_dvr_info?: SharedPostLiveDvrInfo ) { const rep: VideoRepresentation = { uid: format.itag.toString(), @@ -400,7 +511,7 @@ function getVideoRepresentation( height: format.height, codecs: !hoisted.includes('codecs') ? getStringBetweenStrings(format.mime_type, 'codecs="', '"') : undefined, fps: !hoisted.includes('fps') ? format.fps : undefined, - segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn) + segment_info: getSegmentInfo(format, url_transformer, actions, player, cpn, shared_post_live_dvr_info) }; return rep; @@ -411,7 +522,8 @@ function getVideoSet( url_transformer: URLTransformer, player?: Player, actions?: Actions, - cpn?: string + cpn?: string, + shared_post_live_dvr_info?: SharedPostLiveDvrInfo ) { const first_format = formats[0]; const color_info = getColorInfo(first_format); @@ -422,18 +534,23 @@ function getVideoSet( color_info, codecs: hoistCodecsIfPossible(formats, hoisted), fps: hoistNumberAttributeIfPossible(formats, 'fps', hoisted), - representations: formats.map((format) => getVideoRepresentation(format, url_transformer, hoisted, player, actions, cpn)) + representations: formats.map((format) => getVideoRepresentation(format, url_transformer, hoisted, player, actions, cpn, shared_post_live_dvr_info)) }; return set; } function getStoryboardInfo( - storyboards: PlayerStoryboardSpec + storyboards: PlayerStoryboardSpec | PlayerLiveStoryboardSpec ) { - const mime_info = new Map(); + // Can't seem to combine the types in the Map, so create an alias here + type AnyStoryboardData = StoryboardData | LiveStoryboardData - for (const storyboard of storyboards.boards) { + const mime_info = new Map(); + + const boards = storyboards.is(PlayerStoryboardSpec) ? storyboards.boards : [ storyboards.board ]; + + for (const storyboard of boards) { const extension = new URL(storyboard.template_url).pathname.split('.').pop(); const mime_type = `image/${extension === 'jpg' ? 'jpeg' : extension}`; @@ -453,7 +570,7 @@ interface SharedStoryboardResponse { async function getStoryboardMimeType( actions: Actions, - board: StoryboardData, + board: StoryboardData | LiveStoryboardData, transform_url: URLTransformer, probable_mime_type: string, shared_response: SharedStoryboardResponse @@ -476,7 +593,7 @@ async function getStoryboardMimeType( async function getStoryboardBitrate( actions: Actions, - board: StoryboardData, + board: StoryboardData | LiveStoryboardData, shared_response: SharedStoryboardResponse ) { const url = board.template_url; @@ -484,7 +601,7 @@ async function getStoryboardBitrate( const response_promises: Promise[] = []; // Set a limit so we don't take forever for long videos - const request_limit = Math.min(board.storyboard_count, 10); + const request_limit = Math.min(board.type === 'vod' ? board.storyboard_count : 5, 10); for (let i = 0; i < request_limit; i++) { const req_url = new URL(url.replace('$M', i.toString())); @@ -521,13 +638,24 @@ async function getStoryboardBitrate( function getImageRepresentation( duration: number, actions: Actions, - board: StoryboardData, + board: StoryboardData | LiveStoryboardData, transform_url: URLTransformer, shared_response: SharedStoryboardResponse ) { const url = board.template_url; const template_url = new URL(url.replace('$M', '$Number$')); + let template_duration; + + if (board.type === 'vod') { + // Here duration is the duration of the video + template_duration = duration / board.storyboard_count; + } else { + // Here duration is the duration of one of the video/audio segments, + // As there is one tile per segment, we need to multiple it by the number of tiles + template_duration = duration * board.columns * board.rows; + } + const rep: ImageRepresentation = { uid: `thumbnails_${board.thumbnail_width}x${board.thumbnail_height}`, getBitrate() { @@ -539,7 +667,7 @@ function getImageRepresentation( thumbnail_width: board.thumbnail_width, rows: board.rows, columns: board.columns, - template_duration: duration / board.storyboard_count, + template_duration: template_duration, template_url: transform_url(template_url).toString(), getURL(n) { return template_url.toString().replace('$Number$', n.toString()); @@ -552,7 +680,7 @@ function getImageRepresentation( function getImageSets( duration: number, actions: Actions, - storyboards: PlayerStoryboardSpec, + storyboards: PlayerStoryboardSpec | PlayerLiveStoryboardSpec, transform_url: URLTransformer ) { const mime_info = getStoryboardInfo(storyboards); @@ -570,12 +698,13 @@ function getImageSets( export function getStreamingInfo( streaming_data?: IStreamingData, + is_post_live_dvr = false, url_transformer: URLTransformer = (url) => url, format_filter?: FormatFilter, cpn?: string, player?: Player, actions?: Actions, - storyboards?: PlayerStoryboardSpec + storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec ) { if (!streaming_data) throw new InnertubeError('Streaming data not available'); @@ -584,12 +713,34 @@ export function getStreamingInfo( streaming_data.adaptive_formats.filter((fmt) => !format_filter(fmt)) : streaming_data.adaptive_formats; - const duration = formats[0].approx_duration_ms / 1000; + let getDuration; + let shared_post_live_dvr_info: SharedPostLiveDvrInfo | undefined; + + if (is_post_live_dvr) { + shared_post_live_dvr_info = {}; + + if (!actions) { + throw new InnertubeError('Unable to get duration or segment count for this Post Live DVR video without an Actions instance'); + } + + getDuration = () => { + // Should never happen, as we set it just a few lines above, but this stops TypeScript complaining + if (!shared_post_live_dvr_info) { + return Promise.resolve(0); + } + + return getPostLiveDvrDuration(shared_post_live_dvr_info, formats[0], url_transformer, actions, player, cpn); + }; + } else { + const duration = formats[0].approx_duration_ms / 1000; + + getDuration = () => Promise.resolve(duration); + } const { groups, has_multiple_audio_tracks - } = getFormatGroupings(formats); + } = getFormatGroupings(formats, is_post_live_dvr); const { video_groups, @@ -615,15 +766,31 @@ export function getStreamingInfo( audio_groups: [] as Format[][] }); - const audio_sets = audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn)); + const audio_sets = audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info)); - const video_sets = video_groups.map((formats) => getVideoSet(formats, url_transformer, player, actions, cpn)); + const video_sets = video_groups.map((formats) => getVideoSet(formats, url_transformer, player, actions, cpn, shared_post_live_dvr_info)); + + let image_sets: ImageSet[] = []; // XXX: We need to make requests to get the image sizes, so we'll skip the storyboards if we don't have an Actions instance - const image_sets = storyboards && actions ? getImageSets(duration, actions, storyboards, url_transformer) : []; + if (storyboards && actions) { + let duration; + + if (storyboards.is(PlayerStoryboardSpec)) { + duration = formats[0].approx_duration_ms / 1000; + } else { + const target_duration_dec = formats[0].target_duration_dec; + if (typeof target_duration_dec !== 'number') { + throw new InnertubeError('Format is missing target_duration_dec', { format: formats[0] }); + } + duration = target_duration_dec; + } + + image_sets = getImageSets(duration, actions, storyboards, url_transformer); + } const info : StreamingInfo = { - duration, + getDuration, audio_sets, video_sets, image_sets diff --git a/deno/src/utils/Utils.ts b/deno/src/utils/Utils.ts index 890bfe60..7ef95932 100644 --- a/deno/src/utils/Utils.ts +++ b/deno/src/utils/Utils.ts @@ -1,9 +1,13 @@ import { Memo } from '../parser/helpers.ts'; -import type { EmojiRun, TextRun } from '../parser/misc.ts'; import { Text } from '../parser/misc.ts'; +import Log from './Log.ts'; +import userAgents from './user-agents.ts'; + +import type { EmojiRun, TextRun } from '../parser/misc.ts'; import type { FetchFunction } from '../types/PlatformShim.ts'; import type PlatformShim from '../types/PlatformShim.ts'; -import userAgents from './user-agents.ts'; + +const TAG_ = 'Utils'; export class Platform { static #shim: PlatformShim | undefined; @@ -214,8 +218,8 @@ export const debugFetch: FetchFunction = (input, init) => { `${arr_headers.map(([ key, value ]) => ` ${key}: ${value}`).join('\n')}` : ' (none)'; - console.log( - 'YouTube.js Fetch:\n' + + Log.warn(TAG_, + 'Fetch:\n' + ` url: ${url.toString()}\n` + ` method: ${init?.method || 'GET'}\n` + ` headers:\n${headers_serialized}\n' + diff --git a/deno/src/utils/index.ts b/deno/src/utils/index.ts index 2c5208f3..804333fb 100644 --- a/deno/src/utils/index.ts +++ b/deno/src/utils/index.ts @@ -11,3 +11,5 @@ export * from './HTTPClient.ts'; export { Platform } from './Utils.ts'; export * as Utils from './Utils.ts'; + +export { default as Log } from './Log.ts'; \ No newline at end of file