diff --git a/deno/package.json b/deno/package.json index 215a0805..76e0bc6c 100644 --- a/deno/package.json +++ b/deno/package.json @@ -1,6 +1,6 @@ { "name": "youtubei.js", - "version": "4.3.0", + "version": "5.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", @@ -103,7 +103,7 @@ "pbkit": "^0.0.59", "replace": "^1.2.2", "ts-jest": "^28.0.8", - "typescript": "^4.9.5" + "typescript": "^5.0.0" }, "bugs": { "url": "https://github.com/LuanRT/YouTube.js/issues" diff --git a/deno/src/Innertube.ts b/deno/src/Innertube.ts index 91f88cd4..dde5b2fc 100644 --- a/deno/src/Innertube.ts +++ b/deno/src/Innertube.ts @@ -1,50 +1,57 @@ - -import Session, { SessionOptions } from './core/Session.ts'; +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 HashtagFeed from './parser/youtube/HashtagFeed.ts'; -import AccountManager from './core/AccountManager.ts'; -import Feed from './core/Feed.ts'; -import InteractionManager from './core/InteractionManager.ts'; -import YTKids from './core/Kids.ts'; -import YTMusic from './core/Music.ts'; -import PlaylistManager from './core/PlaylistManager.ts'; -import YTStudio from './core/Studio.ts'; -import TabbedFeed from './core/TabbedFeed.ts'; -import HomeFeed from './parser/youtube/HomeFeed.ts'; -import Guide from './parser/youtube/Guide.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 Proto from './proto/index.ts'; import Constants from './utils/Constants.ts'; +import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts'; -import type Actions from './core/Actions.ts'; -import type Format from './parser/classes/misc/Format.ts'; +import { + BrowseEndpoint, + GetNotificationMenuEndpoint, + GuideEndpoint, + NextEndpoint, + PlayerEndpoint, + ResolveURLEndpoint, + SearchEndpoint +} from './core/endpoints/index.ts'; + +import { GetUnseenCountEndpoint } from './core/endpoints/notification/index.ts'; import type { ApiResponse } from './core/Actions.ts'; import type { IBrowseResponse, IParsedResponse } from './parser/types/index.ts'; +import type { INextRequest } from './types/index.ts'; import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.ts'; -import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.ts'; export type InnertubeConfig = SessionOptions; -export interface SearchFilters { - upload_date?: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year'; - type?: 'all' | 'video' | 'channel' | 'playlist' | 'movie'; - duration?: 'all' | 'short' | 'medium' | 'long'; - sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'; - features?: ('hd' | 'subtitles' | 'creative_commons' | '3d' | 'live' | 'purchased' | '4k' | '360' | 'location' | 'hdr' | 'vr180')[]; -} - export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS' +export type SearchFilters = Partial<{ + upload_date: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year'; + type: 'all' | 'video' | 'channel' | 'playlist' | 'movie'; + duration: 'all' | 'short' | 'medium' | 'long'; + sort_by: 'relevance' | 'rating' | 'upload_date' | 'view_count'; + features: ('hd' | 'subtitles' | 'creative_commons' | '3d' | 'live' | 'purchased' | '4k' | '360' | 'location' | 'hdr' | 'vr180')[]; +}>; + /** * Provides access to various services and modules in the YouTube API. */ @@ -67,48 +74,39 @@ export default class Innertube { async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise { throwIfMissing({ target }); - let payload: { - videoId: string, - playlistId?: string, - params?: string, - playlistIndex?: number - }; + let next_payload: INextRequest; if (target instanceof NavigationEndpoint) { - const video_id = target.payload?.videoId; - - if (!video_id) - throw new InnertubeError('Missing video id in endpoint payload.', target); - - payload = { - videoId: video_id - }; - - if (target.payload.playlistId) { - payload.playlistId = target.payload.playlistId; - } - - if (target.payload.params) { - payload.params = target.payload.params; - } - - if (target.payload.index) { - payload.playlistIndex = target.payload.index; - } + next_payload = NextEndpoint.build({ + video_id: target.payload?.videoId, + playlist_id: target.payload?.playlistId, + params: target.payload?.params, + playlist_index: target.payload?.index + }); } else if (typeof target === 'string') { - payload = { - videoId: target - }; + next_payload = NextEndpoint.build({ + video_id: target + }); } else { throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target); } + if (!next_payload.videoId) + throw new InnertubeError('Video id cannot be empty', next_payload); + + const player_payload = PlayerEndpoint.build({ + video_id: next_payload.videoId, + playlist_id: next_payload?.playlistId, + client: client, + sts: this.#session.player?.sts + }); + + const player_response = this.actions.execute(PlayerEndpoint.PATH, player_payload); + const next_response = this.actions.execute(NextEndpoint.PATH, next_payload); + const response = await Promise.all([ player_response, next_response ]); + const cpn = generateRandomString(16); - const initial_info = this.actions.getVideoInfo(payload.videoId, cpn, client); - const continuation = this.actions.execute('/next', payload); - - const response = await Promise.all([ initial_info, continuation ]); return new VideoInfo(response, this.actions, cpn); } @@ -120,8 +118,15 @@ export default class Innertube { async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise { throwIfMissing({ video_id }); + const response = await this.actions.execute( + PlayerEndpoint.PATH, PlayerEndpoint.build({ + video_id: video_id, + client: client, + sts: this.#session.player?.sts + }) + ); + const cpn = generateRandomString(16); - const response = await this.actions.getVideoInfo(video_id, cpn, client); return new VideoInfo([ response ], this.actions, cpn); } @@ -134,14 +139,11 @@ export default class Innertube { async search(query: string, filters: SearchFilters = {}): Promise { throwIfMissing({ query }); - const args = { - query, - ...{ - params: filters ? Proto.encodeSearchFilters(filters) : undefined - } - }; - - const response = await this.actions.execute('/search', args); + const response = await this.actions.execute( + SearchEndpoint.PATH, SearchEndpoint.build({ + query, params: filters ? Proto.encodeSearchFilters(filters) : undefined + }) + ); return new Search(this.actions, response); } @@ -179,11 +181,13 @@ export default class Innertube { async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise { throwIfMissing({ video_id }); - const payload = Proto.encodeCommentsSectionParams(video_id, { - sort_by: sort_by || 'TOP_COMMENTS' - }); - - const response = await this.actions.execute('/next', { continuation: payload }); + const response = await this.actions.execute( + NextEndpoint.PATH, NextEndpoint.build({ + continuation: Proto.encodeCommentsSectionParams(video_id, { + sort_by: sort_by || 'TOP_COMMENTS' + }) + }) + ); return new Comments(this.actions, response.data); } @@ -192,7 +196,9 @@ export default class Innertube { * Retrieves YouTube's home feed (aka recommendations). */ async getHomeFeed(): Promise { - const response = await this.actions.execute('/browse', { browseId: 'FEwhat_to_watch' }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEwhat_to_watch' }) + ); return new HomeFeed(this.actions, response); } @@ -200,7 +206,7 @@ export default class Innertube { * Retrieves YouTube's content guide. */ async getGuide(): Promise { - const response = await this.actions.execute('/guide'); + const response = await this.actions.execute(GuideEndpoint.PATH); return new Guide(response.data); } @@ -208,7 +214,9 @@ export default class Innertube { * Returns the account's library. */ async getLibrary(): Promise { - const response = await this.actions.execute('/browse', { browseId: 'FElibrary' }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FElibrary' }) + ); return new Library(this.actions, response); } @@ -217,7 +225,9 @@ export default class Innertube { * Which can also be achieved with {@link getLibrary}. */ async getHistory(): Promise { - const response = await this.actions.execute('/browse', { browseId: 'FEhistory' }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: 'FEhistory' }) + ); return new History(this.actions, response); } @@ -225,7 +235,9 @@ export default class Innertube { * Retrieves trending content. */ async getTrending(): Promise> { - const response = await this.actions.execute('/browse', { browseId: 'FEtrending', parse: true }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEtrending' }), parse: true } + ); return new TabbedFeed(this.actions, response); } @@ -233,7 +245,9 @@ export default class Innertube { * Retrieves subscriptions feed. */ async getSubscriptionsFeed(): Promise> { - const response = await this.actions.execute('/browse', { browseId: 'FEsubscriptions', parse: true }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEsubscriptions' }), parse: true } + ); return new Feed(this.actions, response); } @@ -243,7 +257,9 @@ export default class Innertube { */ async getChannel(id: string): Promise { throwIfMissing({ id }); - const response = await this.actions.execute('/browse', { browseId: id }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: id }) + ); return new Channel(this.actions, response); } @@ -251,7 +267,11 @@ export default class Innertube { * Retrieves notifications. */ async getNotifications(): Promise { - const response = await this.actions.execute('/notification/get_notification_menu', { notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' }); + const response = await this.actions.execute( + GetNotificationMenuEndpoint.PATH, GetNotificationMenuEndpoint.build({ + notifications_menu_request_type: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' + }) + ); return new NotificationsMenu(this.actions, response); } @@ -259,7 +279,7 @@ export default class Innertube { * Retrieves unseen notifications count. */ async getUnseenNotificationsCount(): Promise { - const response = await this.actions.execute('/notification/get_unseen_count'); + const response = await this.actions.execute(GetUnseenCountEndpoint.PATH); // TODO: properly parse this return response.data?.unseenCount || response.data?.actions?.[0].updateNotificationsUnseenCountAction?.unseenCount || 0; } @@ -275,7 +295,9 @@ export default class Innertube { id = `VL${id}`; } - const response = await this.actions.execute('/browse', { browseId: id }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ browse_id: id }) + ); return new Playlist(this.actions, response); } @@ -287,8 +309,12 @@ export default class Innertube { async getHashtag(hashtag: string): Promise { throwIfMissing({ hashtag }); - const params = Proto.encodeHashtag(hashtag); - const response = await this.actions.execute('/browse', { browseId: 'FEhashtag', params }); + const response = await this.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: 'FEhashtag', + params: Proto.encodeHashtag(hashtag) + }) + ); return new HashtagFeed(this.actions, response); } @@ -322,7 +348,9 @@ export default class Innertube { * @param url - The URL. */ async resolveURL(url: string): Promise { - const response = await this.actions.execute('/navigation/resolve_url', { url, parse: true }); + const response = await this.actions.execute( + ResolveURLEndpoint.PATH, { ...ResolveURLEndpoint.build({ url }), parse: true } + ); return response.endpoint; } @@ -338,58 +366,58 @@ export default class Innertube { } /** - * An instance of YTMusic for interacting with the YouTube Music service. + * An interface for interacting with YouTube Music. */ - get music(): YTMusic { - return new YTMusic(this.#session); + get music() { + return new Music(this.#session); } /** - * An instance of YTStudio for interacting with the YouTube Studio service. + * An interface for interacting with YouTube Studio. */ - get studio(): YTStudio { - return new YTStudio(this.#session); + get studio() { + return new Studio(this.#session); } /** - * An instance of YTKids for interacting with the YouTube Kids service. + * An interface for interacting with YouTube Kids. */ - get kids(): YTKids { - return new YTKids(this.#session); + get kids() { + return new Kids(this.#session); } /** - * An instance of AccountManager for managing a user's account. + * An interface for managing and retrieving account information. */ - get account(): AccountManager { + get account() { return new AccountManager(this.#session.actions); } /** - * An instance of PlaylistManager for managing playlists. + * An interface for managing playlists. */ - get playlist(): PlaylistManager { + get playlist() { return new PlaylistManager(this.#session.actions); } /** - * An instance of InteractionManager for interacting with contents in YouTube. + * An interface for directly interacting with certain YouTube features. */ - get interact(): InteractionManager { + get interact() { return new InteractionManager(this.#session.actions); } /** - * An instance of Actions. + * An internal class used to dispatch requests. */ - get actions(): Actions { + get actions() { return this.#session.actions; } /** - * Returns the InnerTube session instance. + * The session used by this instance. */ - get session(): Session { + get session() { return this.#session; } } \ No newline at end of file diff --git a/deno/src/core/Actions.ts b/deno/src/core/Actions.ts index 83ec2e8c..58a23070 100644 --- a/deno/src/core/Actions.ts +++ b/deno/src/core/Actions.ts @@ -28,7 +28,7 @@ export type ParsedResponse = T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse : IParsedResponse; -class Actions { +export default class Actions { #session: Session; constructor(session: Session) { @@ -51,57 +51,6 @@ class Actions { }; } - /** - * Used to retrieve video info. - * @param id - The video ID. - * @param cpn - Content Playback Nonce. - * @param client - The client to use. - * @param playlist_id - The playlist ID. - */ - async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string): Promise { - const data: Record = { - playbackContext: { - contentPlaybackContext: { - vis: 0, - splay: false, - referer: 'https://www.youtube.com', - currentUrl: `/watch?v=${id}`, - autonavState: 'STATE_NONE', - signatureTimestamp: this.#session.player?.sts || 0, - autoCaptionsDefaultOn: false, - html5Preference: 'HTML5_PREF_WANTS', - lactMilliseconds: '-1' - } - }, - attestationRequest: { - omitBotguardData: true - }, - videoId: id - }; - - if (client) { - data.client = client; - } - - if (cpn) { - data.cpn = cpn; - } - - if (playlist_id) { - data.playlistId = playlist_id; - } - - const response = await this.#session.http.fetch('/player', { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json' - } - }); - - return this.#wrap(response); - } - /** * Makes calls to the playback tracking API. * @param url - The URL to call. @@ -226,6 +175,4 @@ class Actions { 'SPtime_watched' ].includes(id); } -} - -export default Actions; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/Kids.ts b/deno/src/core/Kids.ts deleted file mode 100644 index 38429884..00000000 --- a/deno/src/core/Kids.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Search from '../parser/ytkids/Search.ts'; -import HomeFeed from '../parser/ytkids/HomeFeed.ts'; -import VideoInfo from '../parser/ytkids/VideoInfo.ts'; -import Channel from '../parser/ytkids/Channel.ts'; -import type Session from './Session.ts'; - -import { generateRandomString } from '../utils/Utils.ts'; - -class Kids { - #session: Session; - - constructor(session: Session) { - this.#session = session; - } - - /** - * Searches the given query. - * @param query - The query. - */ - async search(query: string): Promise { - const response = await this.#session.actions.execute('/search', { query, client: 'YTKIDS' }); - return new Search(this.#session.actions, response); - } - - /** - * Retrieves video info. - * @param video_id - The video id. - */ - async getInfo(video_id: string): Promise { - const cpn = generateRandomString(16); - - const initial_info = this.#session.actions.execute('/player', { - cpn, - client: 'YTKIDS', - videoId: video_id, - playbackContext: { - contentPlaybackContext: { - signatureTimestamp: this.#session.player?.sts || 0 - } - } - }); - - const continuation = this.#session.actions.execute('/next', { videoId: video_id, client: 'YTKIDS' }); - - const response = await Promise.all([ initial_info, continuation ]); - - return new VideoInfo(response, this.#session.actions, cpn); - } - - /** - * Retrieves the contents of the given channel. - * @param channel_id - The channel id. - */ - async getChannel(channel_id: string): Promise { - const response = await this.#session.actions.execute('/browse', { browseId: channel_id, client: 'YTKIDS' }); - return new Channel(this.#session.actions, response); - } - - /** - * Retrieves the home feed. - */ - async getHomeFeed(): Promise { - const response = await this.#session.actions.execute('/browse', { browseId: 'FEkids_home', client: 'YTKIDS' }); - return new HomeFeed(this.#session.actions, response); - } -} - -export default Kids; \ No newline at end of file diff --git a/deno/src/core/OAuth.ts b/deno/src/core/OAuth.ts index 0a36e2df..38e945e3 100644 --- a/deno/src/core/OAuth.ts +++ b/deno/src/core/OAuth.ts @@ -28,7 +28,7 @@ export type OAuthAuthEventHandler = (data: { export type OAuthAuthPendingEventHandler = (data: OAuthAuthPendingData) => any; export type OAuthAuthErrorEventHandler = (err: OAuthError) => any; -class OAuth { +export default class OAuth { #identity?: Record; #session: Session; #credentials?: Credentials; @@ -264,6 +264,4 @@ class OAuth { Reflect.has(this.#credentials, 'refresh_token') && Reflect.has(this.#credentials, 'expires') || false; } -} - -export default OAuth; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/Player.ts b/deno/src/core/Player.ts index 257e4f8e..d77cf1a6 100644 --- a/deno/src/core/Player.ts +++ b/deno/src/core/Player.ts @@ -2,9 +2,12 @@ import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } fr import Constants from '../utils/Constants.ts'; -import { ICache } from '../types/Cache.ts'; -import { FetchFunction } from '../types/PlatformShim.ts'; +import type { ICache } from '../types/Cache.ts'; +import type { FetchFunction } from '../types/PlatformShim.ts'; +/** + * Represents YouTube's player script. This is required to decipher signatures. + */ export default class Player { #nsig_sc; #sig_sc; @@ -104,6 +107,29 @@ export default class Player { url_components.searchParams.set('n', nsig); } + const client = url_components.searchParams.get('c'); + + switch (client) { + case 'WEB': + url_components.searchParams.set('cver', Constants.CLIENTS.WEB.VERSION); + break; + case 'WEB_REMIX': + url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC.VERSION); + break; + case 'WEB_KIDS': + url_components.searchParams.set('cver', Constants.CLIENTS.WEB_KIDS.VERSION); + break; + case 'ANDROID': + url_components.searchParams.set('cver', Constants.CLIENTS.ANDROID.VERSION); + break; + case 'ANDROID_MUSIC': + url_components.searchParams.set('cver', Constants.CLIENTS.YTMUSIC_ANDROID.VERSION); + break; + case 'TVHTML5_SIMPLY_EMBEDDED_PLAYER': + url_components.searchParams.set('cver', Constants.CLIENTS.TV_EMBEDDED.VERSION); + break; + } + return url_components.toString(); } diff --git a/deno/src/core/Session.ts b/deno/src/core/Session.ts index a11d2ba1..40bd1db4 100644 --- a/deno/src/core/Session.ts +++ b/deno/src/core/Session.ts @@ -4,11 +4,13 @@ import Actions from './Actions.ts'; import Player from './Player.ts'; import Proto from '../proto/index.ts'; -import { ICache } from '../types/Cache.ts'; -import { FetchFunction } from '../types/PlatformShim.ts'; +import type { ICache } from '../types/Cache.ts'; +import type { FetchFunction } from '../types/PlatformShim.ts'; import HTTPClient from '../utils/HTTPClient.ts'; -import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.ts'; -import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.ts'; +import type { DeviceCategory} from '../utils/Utils.ts'; +import { getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.ts'; +import type { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.ts'; +import OAuth from './OAuth.ts'; export enum ClientType { WEB = 'WEB', @@ -30,7 +32,6 @@ export interface Context { screenPixelDensity: number; screenWidthPoints: number; visitorData: string; - userAgent: string; clientName: string; clientVersion: string; clientScreen?: string, @@ -41,6 +42,7 @@ export interface Context { clientFormFactor: string; userInterfaceTheme: string; timeZone: string; + userAgent?: string; browserName?: string; browserVersion?: string; originalUrl: string; @@ -64,9 +66,6 @@ export interface Context { thirdParty?: { embedUrl: string; }; - request: { - useSsl: true; - }; } export interface SessionOptions { @@ -135,6 +134,9 @@ export interface SessionData { api_version: string; } +/** + * Represents an InnerTube session. This holds all the data needed to make requests to YouTube. + */ export default class Session extends EventEmitterLike { #api_version: string; #key: string; @@ -273,7 +275,6 @@ export default class Session extends EventEmitterLike { screenPixelDensity: 1, screenWidthPoints: 1920, visitorData: device_info[13], - userAgent: device_info[14], clientName: options.client_name, clientVersion: device_info[16], osName: device_info[17], @@ -287,14 +288,11 @@ export default class Session extends EventEmitterLike { originalUrl: Constants.URLS.YT_BASE, deviceMake: device_info[11], deviceModel: device_info[12], - utcOffsetMinutes: new Date().getTimezoneOffset() + utcOffsetMinutes: -new Date().getTimezoneOffset() }, user: { enableSafetyMode: options.enable_safety_mode, lockedSafetyMode: false - }, - request: { - useSsl: true } }; @@ -326,7 +324,6 @@ export default class Session extends EventEmitterLike { screenPixelDensity: 1, screenWidthPoints: 1920, visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)), - userAgent: getRandomUserAgent('desktop'), clientName: options.client_name, clientVersion: CLIENTS.WEB.VERSION, osName: 'Windows', @@ -338,14 +335,11 @@ export default class Session extends EventEmitterLike { originalUrl: Constants.URLS.YT_BASE, deviceMake: '', deviceModel: '', - utcOffsetMinutes: new Date().getTimezoneOffset() + utcOffsetMinutes: -new Date().getTimezoneOffset() }, user: { enableSafetyMode: options.enable_safety_mode, lockedSafetyMode: false - }, - request: { - useSsl: true } }; diff --git a/deno/src/core/clients/Kids.ts b/deno/src/core/clients/Kids.ts new file mode 100644 index 00000000..ddd8ddbe --- /dev/null +++ b/deno/src/core/clients/Kids.ts @@ -0,0 +1,83 @@ +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 { generateRandomString } from '../../utils/Utils.ts'; + +import { + BrowseEndpoint, NextEndpoint, + PlayerEndpoint, SearchEndpoint +} from '../endpoints/index.ts'; + +export default class Kids { + #session: Session; + + constructor(session: Session) { + this.#session = session; + } + + /** + * Searches the given query. + * @param query - The query. + */ + async search(query: string): Promise { + const response = await this.#session.actions.execute( + SearchEndpoint.PATH, SearchEndpoint.build({ client: 'YTKIDS', query }) + ); + return new Search(this.#session.actions, response); + } + + /** + * Retrieves video info. + * @param video_id - The video id. + */ + async getInfo(video_id: string): Promise { + const player_payload = PlayerEndpoint.build({ + sts: this.#session.player?.sts, + client: 'YTKIDS', + video_id + }); + + const next_payload = NextEndpoint.build({ + video_id, + client: 'YTKIDS' + }); + + const player_response = this.#session.actions.execute(PlayerEndpoint.PATH, player_payload); + const next_response = this.#session.actions.execute(NextEndpoint.PATH, next_payload); + const response = await Promise.all([ player_response, next_response ]); + + const cpn = generateRandomString(16); + + return new VideoInfo(response, this.#session.actions, cpn); + } + + /** + * Retrieves the contents of the given channel. + * @param channel_id - The channel id. + */ + async getChannel(channel_id: string): Promise { + const response = await this.#session.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: channel_id, + client: 'YTKIDS' + }) + ); + return new Channel(this.#session.actions, response); + } + + /** + * Retrieves the home feed. + */ + async getHomeFeed(): Promise { + const response = await this.#session.actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: 'FEkids_home', + client: 'YTKIDS' + }) + ); + return new HomeFeed(this.#session.actions, response); + } +} \ No newline at end of file diff --git a/deno/src/core/Music.ts b/deno/src/core/clients/Music.ts similarity index 56% rename from deno/src/core/Music.ts rename to deno/src/core/clients/Music.ts index 52ead9b0..dafd97f4 100644 --- a/deno/src/core/Music.ts +++ b/deno/src/core/clients/Music.ts @@ -1,38 +1,41 @@ +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 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 AutomixPreviewVideo from '../../parser/classes/AutomixPreviewVideo.ts'; +import Message from '../../parser/classes/Message.ts'; +import MusicCarouselShelf from '../../parser/classes/MusicCarouselShelf.ts'; +import MusicDescriptionShelf from '../../parser/classes/MusicDescriptionShelf.ts'; +import MusicQueue from '../../parser/classes/MusicQueue.ts'; +import MusicTwoRowItem from '../../parser/classes/MusicTwoRowItem.ts'; +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 Proto from '../../proto/index.ts'; -import AutomixPreviewVideo from '../parser/classes/AutomixPreviewVideo.ts'; -import Message from '../parser/classes/Message.ts'; -import MusicCarouselShelf from '../parser/classes/MusicCarouselShelf.ts'; -import MusicDescriptionShelf from '../parser/classes/MusicDescriptionShelf.ts'; -import MusicQueue from '../parser/classes/MusicQueue.ts'; -import MusicTwoRowItem from '../parser/classes/MusicTwoRowItem.ts'; -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 type { ObservedArray, YTNode } 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 { observe } from '../parser/helpers.ts'; -import Proto from '../proto/index.ts'; -import { generateRandomString, InnertubeError, throwIfMissing } from '../utils/Utils.ts'; +import { + BrowseEndpoint, + NextEndpoint, + PlayerEndpoint, + SearchEndpoint +} from '../endpoints/index.ts'; -import type { ObservedArray, YTNode } from '../parser/helpers.ts'; -import type Actions from './Actions.ts'; -import type Session from './Session.ts'; +import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.ts'; -export interface MusicSearchFilters { - type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist'; -} - -class Music { +export default class Music { #session: Session; #actions: Actions; @@ -56,25 +59,23 @@ class Music { } async #fetchInfoFromVideoId(video_id: string): Promise { + const player_payload = PlayerEndpoint.build({ + video_id, + sts: this.#session.player?.sts, + client: 'YTMUSIC' + }); + + const next_payload = NextEndpoint.build({ + video_id, + client: 'YTMUSIC' + }); + + const player_response = this.#actions.execute(PlayerEndpoint.PATH, player_payload); + const next_response = this.#actions.execute(NextEndpoint.PATH, next_payload); + const response = await Promise.all([ player_response, next_response ]); + const cpn = generateRandomString(16); - const initial_info = this.#actions.execute('/player', { - cpn, - client: 'YTMUSIC', - videoId: video_id, - playbackContext: { - contentPlaybackContext: { - signatureTimestamp: this.#session.player?.sts || 0 - } - } - }); - - const continuation = this.#actions.execute('/next', { - client: 'YTMUSIC', - videoId: video_id - }); - - const response = await Promise.all([ initial_info, continuation ]); return new TrackInfo(response, this.#actions, cpn); } @@ -85,25 +86,26 @@ class Music { if (!list_item.endpoint) throw new Error('This item does not have an endpoint.'); - const cpn = generateRandomString(16); - - const initial_info = list_item.endpoint.call(this.#actions, { - cpn, + const player_response = list_item.endpoint.call(this.#actions, { client: 'YTMUSIC', playbackContext: { contentPlaybackContext: { - signatureTimestamp: this.#session.player?.sts || 0 + ...{ + signatureTimestamp: this.#session.player?.sts + } } } }); - const continuation = list_item.endpoint.call(this.#actions, { + const next_response = list_item.endpoint.call(this.#actions, { client: 'YTMUSIC', enablePersistentPlaylistPanel: true, override_endpoint: '/next' }); - const response = await Promise.all([ initial_info, continuation ]); + const cpn = generateRandomString(16); + + const response = await Promise.all([ player_response, next_response ]); return new TrackInfo(response, this.#actions, cpn); } @@ -115,17 +117,12 @@ class Music { async search(query: string, filters: MusicSearchFilters = {}): Promise { throwIfMissing({ query }); - const payload: { - query: string; - client: string; - params?: string; - } = { query, client: 'YTMUSIC' }; - - if (filters.type && filters.type !== 'all') { - payload.params = Proto.encodeMusicSearchFilters(filters); - } - - const response = await this.#actions.execute('/search', payload); + const response = await this.#actions.execute( + SearchEndpoint.PATH, SearchEndpoint.build({ + query, client: 'YTMUSIC', + params: filters.type && filters.type !== 'all' ? Proto.encodeMusicSearchFilters(filters) : undefined + }) + ); return new Search(response, this.#actions, Reflect.has(filters, 'type') && filters.type !== 'all'); } @@ -134,10 +131,12 @@ class Music { * Retrieves the home feed. */ async getHomeFeed(): Promise { - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - browseId: 'FEmusic_home' - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: 'FEmusic_home', + client: 'YTMUSIC' + }) + ); return new HomeFeed(response, this.#actions); } @@ -146,10 +145,12 @@ class Music { * Retrieves the Explore feed. */ async getExplore(): Promise { - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - browseId: 'FEmusic_explore' - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: 'FEmusic_explore' + }) + ); return new Explore(response); // TODO: return new Explore(response, this.#actions); @@ -159,10 +160,12 @@ class Music { * Retrieves the library. */ async getLibrary(): Promise { - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - browseId: 'FEmusic_library_landing' - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: 'FEmusic_library_landing' + }) + ); return new Library(response, this.#actions); } @@ -177,10 +180,12 @@ class Music { if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist')) throw new InnertubeError('Invalid artist id', artist_id); - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - browseId: artist_id - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: artist_id + }) + ); return new Artist(response, this.#actions); } @@ -195,10 +200,12 @@ class Music { if (!album_id.startsWith('MPR') && !album_id.startsWith('FEmusic_library_privately_owned_release')) throw new InnertubeError('Invalid album id', album_id); - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - browseId: album_id - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: album_id + }) + ); return new Album(response); } @@ -214,10 +221,12 @@ class Music { playlist_id = `VL${playlist_id}`; } - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - browseId: playlist_id - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: playlist_id + }) + ); return new Playlist(response, this.#actions); } @@ -230,13 +239,11 @@ class Music { async getUpNext(video_id: string, automix = true): Promise { throwIfMissing({ video_id }); - const data = await this.#actions.execute('/next', { - videoId: video_id, - client: 'YTMUSIC', - parse: true - }); + const response = await this.#actions.execute( + NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true } + ); - const tabs = data.contents_memo?.getType(Tab); + const tabs = response.contents_memo?.getType(Tab); const tab = tabs?.first(); @@ -278,13 +285,11 @@ class Music { async getRelated(video_id: string): Promise> { throwIfMissing({ video_id }); - const data = await this.#actions.execute('/next', { - videoId: video_id, - client: 'YTMUSIC', - parse: true - }); + const response = await this.#actions.execute( + NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true } + ); - const tabs = data.contents_memo?.getType(Tab); + const tabs = response.contents_memo?.getType(Tab); const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED'); @@ -308,13 +313,11 @@ class Music { async getLyrics(video_id: string): Promise { throwIfMissing({ video_id }); - const data = await this.#actions.execute('/next', { - videoId: video_id, - client: 'YTMUSIC', - parse: true - }); + const response = await this.#actions.execute( + NextEndpoint.PATH, { ...NextEndpoint.build({ video_id, client: 'YTMUSIC' }), parse: true } + ); - const tabs = data.contents_memo?.getType(Tab); + const tabs = response.contents_memo?.getType(Tab); const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS'); @@ -327,7 +330,7 @@ class Music { throw new InnertubeError('Unexpected response', page); if (page.contents.item().key('type').string() === 'Message') - throw new InnertubeError(page.contents.item().as(Message).text, video_id); + throw new InnertubeError(page.contents.item().as(Message).text.toString(), video_id); const section_list = page.contents.item().as(SectionList).contents; @@ -338,10 +341,12 @@ class Music { * Retrieves recap. */ async getRecap(): Promise { - const response = await this.#actions.execute('/browse', { - browseId: 'FEmusic_listening_review', - client: 'YTMUSIC_ANDROID' - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC_ANDROID', + browse_id: 'FEmusic_listening_review' + }) + ); return new Recap(response, this.#actions); } @@ -351,19 +356,16 @@ class Music { * @param query - The query. */ async getSearchSuggestions(query: string): Promise> { - const response = await this.#actions.execute('/music/get_search_suggestions', { - parse: true, - input: query, - client: 'YTMUSIC' - }); + const response = await this.#actions.execute( + GetSearchSuggestionsEndpoint.PATH, + { ...GetSearchSuggestionsEndpoint.build({ input: query }), parse: true } + ); - const search_suggestions_section = response.contents_memo?.getType(SearchSuggestionsSection)?.[0]; + if (!response.contents_memo) + throw new InnertubeError('Unexpected response', response); - if (!search_suggestions_section?.contents.is_array) - return observe([] as YTNode[]); + const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection).first(); - return search_suggestions_section?.contents.array(); + return search_suggestions_section.contents; } -} - -export default Music; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/Studio.ts b/deno/src/core/clients/Studio.ts similarity index 73% rename from deno/src/core/Studio.ts rename to deno/src/core/clients/Studio.ts index 15decff3..080054a7 100644 --- a/deno/src/core/Studio.ts +++ b/deno/src/core/clients/Studio.ts @@ -1,9 +1,12 @@ -import Proto from '../proto/index.ts'; -import { Constants } from '../utils/index.ts'; -import { InnertubeError, MissingParamError, Platform } from '../utils/Utils.ts'; +import Proto from '../../proto/index.ts'; +import { Constants } from '../../utils/index.ts'; +import { InnertubeError, MissingParamError, Platform } from '../../utils/Utils.ts'; -import type { ApiResponse } from './Actions.ts'; -import type Session from './Session.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'; interface UploadResult { status: string; @@ -18,25 +21,7 @@ interface InitialUploadData { chunk_granularity: string; } -export interface VideoMetadata { - title?: string; - description?: string; - tags?: string[]; - category?: number; - license?: string; - age_restricted?: boolean; - made_for_kids?: boolean; - privacy?: 'PUBLIC' | 'PRIVATE' | 'UNLISTED'; -} - -export interface UploadedVideoMetadata { - title?: string; - description?: string; - privacy?: 'PUBLIC' | 'PRIVATE' | 'UNLISTED'; - is_draft?: boolean; -} - -class Studio { +export default class Studio { #session: Session; constructor(session: Session) { @@ -69,7 +54,7 @@ class Studio { } /** - * Updates given video's metadata. + * Updates a given video's metadata. * @example * ```ts * const response = await yt.studio.updateVideoMetadata('videoid', { @@ -82,7 +67,7 @@ class Studio { * }); * ``` */ - async updateVideoMetadata(video_id: string, metadata: VideoMetadata): Promise { + async updateVideoMetadata(video_id: string, metadata: UpdateVideoMetadataOptions): Promise { if (!this.#session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); @@ -104,7 +89,7 @@ class Studio { * const response = await yt.studio.upload(file.buffer, { title: 'Wow!' }); * ``` */ - async upload(file: BodyInit, metadata: UploadedVideoMetadata = {}): Promise { + async upload(file: BodyInit, metadata: UploadedVideoMetadataOptions = {}): Promise { if (!this.#session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); @@ -174,38 +159,34 @@ class Studio { return data; } - async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: UploadedVideoMetadata) { - const metadata_payload = { - resourceId: { - scottyResourceId: { - id: upload_result.scottyResourceId - } - }, - frontendUploadId: initial_data.frontend_upload_id, - initialMetadata: { - title: { - newTitle: metadata.title || new Date().toDateString() + async #setVideoMetadata(initial_data: InitialUploadData, upload_result: UploadResult, metadata: UploadedVideoMetadataOptions) { + const response = await this.#session.actions.execute( + CreateVideoEndpoint.PATH, CreateVideoEndpoint.build({ + resource_id: { + scotty_resource_id: { + id: upload_result.scottyResourceId + } }, - description: { - newDescription: metadata.description || '', - shouldSegment: true + frontend_upload_id: initial_data.frontend_upload_id, + initial_metadata: { + title: { + new_title: metadata.title || new Date().toDateString() + }, + description: { + new_description: metadata.description || '', + should_segment: true + }, + privacy: { + new_privacy: metadata.privacy || 'PRIVATE' + }, + draft_state: { + is_draft: metadata.is_draft + } }, - privacy: { - newPrivacy: metadata.privacy || 'PRIVATE' - }, - draftState: { - isDraft: metadata.is_draft || false - } - } - }; - - const response = await this.#session.actions.execute('/upload/createvideo', { - client: 'ANDROID', - ...metadata_payload - }); + client: 'ANDROID' + }) + ); return response; } -} - -export default Studio; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/clients/index.ts b/deno/src/core/clients/index.ts new file mode 100644 index 00000000..d4c7f9ac --- /dev/null +++ b/deno/src/core/clients/index.ts @@ -0,0 +1,3 @@ +export { default as Kids } from './Kids.ts'; +export { default as Music } from './Music.ts'; +export { default as Studio } from './Studio.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/BrowseEndpoint.ts b/deno/src/core/endpoints/BrowseEndpoint.ts new file mode 100644 index 00000000..38bf8a52 --- /dev/null +++ b/deno/src/core/endpoints/BrowseEndpoint.ts @@ -0,0 +1,19 @@ +import type { IBrowseRequest, BrowseEndpointOptions } from '../../types/index.ts'; + +export const PATH = '/browse'; + +/** + * Builds a `/browse` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: BrowseEndpointOptions): IBrowseRequest { + return { + ...{ + browseId: opts.browse_id, + params: opts.params, + continuation: opts.continuation, + client: opts.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/GetNotificationMenuEndpoint.ts b/deno/src/core/endpoints/GetNotificationMenuEndpoint.ts new file mode 100644 index 00000000..0e6c4317 --- /dev/null +++ b/deno/src/core/endpoints/GetNotificationMenuEndpoint.ts @@ -0,0 +1,16 @@ +import type { IGetNotificationMenuRequest, GetNotificationMenuEndpointOptions } from '../../types/index.ts'; + +export const PATH = '/notification/get_notification_menu'; + +/** + * Builds a `/get_notification_menu` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: GetNotificationMenuEndpointOptions): IGetNotificationMenuRequest { + return { + ...{ + notificationsMenuRequestType: opts.notifications_menu_request_type + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/GuideEndpoint.ts b/deno/src/core/endpoints/GuideEndpoint.ts new file mode 100644 index 00000000..04198267 --- /dev/null +++ b/deno/src/core/endpoints/GuideEndpoint.ts @@ -0,0 +1 @@ +export const PATH = '/guide'; \ No newline at end of file diff --git a/deno/src/core/endpoints/NextEndpoint.ts b/deno/src/core/endpoints/NextEndpoint.ts new file mode 100644 index 00000000..992aa8aa --- /dev/null +++ b/deno/src/core/endpoints/NextEndpoint.ts @@ -0,0 +1,21 @@ +import type { INextRequest, NextEndpointOptions } from '../../types/index.ts'; + +export const PATH = '/next'; + +/** + * Builds a `/next` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: NextEndpointOptions): INextRequest { + return { + ...{ + videoId: opts.video_id, + playlistId: opts.playlist_id, + params: opts.params, + playlistIndex: opts.playlist_index, + client: opts.client, + continuation: opts.continuation + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/PlayerEndpoint.ts b/deno/src/core/endpoints/PlayerEndpoint.ts new file mode 100644 index 00000000..b72083a1 --- /dev/null +++ b/deno/src/core/endpoints/PlayerEndpoint.ts @@ -0,0 +1,46 @@ +import type { IPlayerRequest, PlayerEndpointOptions } from '../../types/index.ts'; + +export const PATH = '/player'; + +/** + * Builds a `/player` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: PlayerEndpointOptions): IPlayerRequest { + const is_android = + opts.client === 'ANDROID' || + opts.client === 'YTMUSIC_ANDROID' || + opts.client === 'YTSTUDIO_ANDROID'; + + return { + playbackContext: { + contentPlaybackContext: { + vis: 0, + splay: false, + referer: opts.playlist_id ? + `https://www.youtube.com/watch?v=${opts.video_id}&list=${opts.playlist_id}` : + `https://www.youtube.com/watch?v=${opts.video_id}`, + currentUrl: opts.playlist_id ? + `/watch?v=${opts.video_id}&list=${opts.playlist_id}` : + `/watch?v=${opts.video_id}`, + autonavState: 'STATE_ON', + autoCaptionsDefaultOn: false, + html5Preference: 'HTML5_PREF_WANTS', + lactMilliseconds: '-1', + ...{ + signatureTimestamp: opts.sts + } + } + }, + racyCheckOk: true, + contentCheckOk: true, + videoId: opts.video_id, + ...{ + client: opts.client, + playlistId: opts.playlist_id, + // Workaround streaming URLs returning 403 when using Android clients. + params: is_android ? '8AEB' : opts.params + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/ResolveURLEndpoint.ts b/deno/src/core/endpoints/ResolveURLEndpoint.ts new file mode 100644 index 00000000..b2e3c510 --- /dev/null +++ b/deno/src/core/endpoints/ResolveURLEndpoint.ts @@ -0,0 +1,16 @@ +import type { IResolveURLRequest, ResolveURLEndpointOptions } from '../../types/index.ts'; + +export const PATH = '/navigation/resolve_url'; + +/** + * Builds a `/resolve_url` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: ResolveURLEndpointOptions): IResolveURLRequest { + return { + ...{ + url: opts.url + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/SearchEndpoint.ts b/deno/src/core/endpoints/SearchEndpoint.ts new file mode 100644 index 00000000..c8a37836 --- /dev/null +++ b/deno/src/core/endpoints/SearchEndpoint.ts @@ -0,0 +1,19 @@ +import type { ISearchRequest, SearchEndpointOptions } from '../../types/index.ts'; + +export const PATH = '/search'; + +/** + * Builds a `/search` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: SearchEndpointOptions): ISearchRequest { + return { + ...{ + query: opts.query, + params: opts.params, + continuation: opts.continuation, + client: opts.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/account/AccountListEndpoint.ts b/deno/src/core/endpoints/account/AccountListEndpoint.ts new file mode 100644 index 00000000..587253ed --- /dev/null +++ b/deno/src/core/endpoints/account/AccountListEndpoint.ts @@ -0,0 +1,13 @@ +import type { IAccountListRequest } from '../../../types/index.ts'; + +export const PATH = '/account/accounts_list'; + +/** + * Builds a `/account/accounts_list` request payload. + * @returns The payload. + */ +export function build(): IAccountListRequest { + return { + client: 'ANDROID' + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/account/index.ts b/deno/src/core/endpoints/account/index.ts new file mode 100644 index 00000000..a32a5e5c --- /dev/null +++ b/deno/src/core/endpoints/account/index.ts @@ -0,0 +1 @@ +export * as AccountListEndpoint from './AccountListEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/browse/EditPlaylistEndpoint.ts b/deno/src/core/endpoints/browse/EditPlaylistEndpoint.ts new file mode 100644 index 00000000..c9d6d418 --- /dev/null +++ b/deno/src/core/endpoints/browse/EditPlaylistEndpoint.ts @@ -0,0 +1,22 @@ +import type { IEditPlaylistRequest, EditPlaylistEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/browse/edit_playlist'; + +/** + * Builds a `/browse/edit_playlist` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: EditPlaylistEndpointOptions): IEditPlaylistRequest { + return { + playlistId: opts.playlist_id, + actions: opts.actions.map((action) => ({ + action: action.action, + ...{ + addedVideoId: action.added_video_id, + setVideoId: action.set_video_id, + movedSetVideoIdPredecessor: action.moved_set_video_id_predecessor + } + })) + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/browse/index.ts b/deno/src/core/endpoints/browse/index.ts new file mode 100644 index 00000000..e9dab932 --- /dev/null +++ b/deno/src/core/endpoints/browse/index.ts @@ -0,0 +1 @@ +export * as EditPlaylistEndpoint from './EditPlaylistEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/channel/EditDescriptionEndpoint.ts b/deno/src/core/endpoints/channel/EditDescriptionEndpoint.ts new file mode 100644 index 00000000..f73be21a --- /dev/null +++ b/deno/src/core/endpoints/channel/EditDescriptionEndpoint.ts @@ -0,0 +1,15 @@ +import type { IChannelEditDescriptionRequest, ChannelEditDescriptionEndpointOptions } from '../../../types/Endpoints.ts'; + +export const PATH = '/channel/edit_description'; + +/** + * Builds a `/channel/edit_description` request payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: ChannelEditDescriptionEndpointOptions): IChannelEditDescriptionRequest { + return { + givenDescription: options.given_description, + client: 'ANDROID' + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/channel/EditNameEndpoint.ts b/deno/src/core/endpoints/channel/EditNameEndpoint.ts new file mode 100644 index 00000000..c80fc5de --- /dev/null +++ b/deno/src/core/endpoints/channel/EditNameEndpoint.ts @@ -0,0 +1,15 @@ +import type { IChannelEditNameRequest, ChannelEditNameEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/channel/edit_name'; + +/** + * Builds a `/channel/edit_name` request payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: ChannelEditNameEndpointOptions): IChannelEditNameRequest { + return { + givenName: options.given_name, + client: 'ANDROID' + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/channel/index.ts b/deno/src/core/endpoints/channel/index.ts new file mode 100644 index 00000000..de4ec5c5 --- /dev/null +++ b/deno/src/core/endpoints/channel/index.ts @@ -0,0 +1,2 @@ +export * as EditNameEndpoint from './EditNameEndpoint.ts'; +export * as EditDescriptionEndpoint from './EditDescriptionEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/comment/CreateCommentEndpoint.ts b/deno/src/core/endpoints/comment/CreateCommentEndpoint.ts new file mode 100644 index 00000000..7147acf1 --- /dev/null +++ b/deno/src/core/endpoints/comment/CreateCommentEndpoint.ts @@ -0,0 +1,18 @@ +import type { ICreateCommentRequest, CreateCommentEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/comment/create_comment'; + +/** + * Builds a `/comment/create_comment` request payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: CreateCommentEndpointOptions): ICreateCommentRequest { + return { + commentText: options.comment_text, + createCommentParams: options.create_comment_params, + ...{ + client: options.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/comment/PerformCommentActionEndpoint.ts b/deno/src/core/endpoints/comment/PerformCommentActionEndpoint.ts new file mode 100644 index 00000000..25b2c59b --- /dev/null +++ b/deno/src/core/endpoints/comment/PerformCommentActionEndpoint.ts @@ -0,0 +1,17 @@ +import type { IPerformCommentActionRequest, PerformCommentActionEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/comment/perform_comment_action'; + +/** + * Builds a `/comment/perform_comment_action` request payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: PerformCommentActionEndpointOptions): IPerformCommentActionRequest { + return { + actions: options.actions, + ...{ + client: options.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/comment/index.ts b/deno/src/core/endpoints/comment/index.ts new file mode 100644 index 00000000..be0fd04b --- /dev/null +++ b/deno/src/core/endpoints/comment/index.ts @@ -0,0 +1,2 @@ +export * as PerformCommentActionEndpoint from './PerformCommentActionEndpoint.ts'; +export * as CreateCommentEndpoint from './CreateCommentEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/index.ts b/deno/src/core/endpoints/index.ts new file mode 100644 index 00000000..e8ff4e2d --- /dev/null +++ b/deno/src/core/endpoints/index.ts @@ -0,0 +1,18 @@ +export * as BrowseEndpoint from './BrowseEndpoint.ts'; +export * as GetNotificationMenuEndpoint from './GetNotificationMenuEndpoint.ts'; +export * as GuideEndpoint from './GuideEndpoint.ts'; +export * as NextEndpoint from './NextEndpoint.ts'; +export * as PlayerEndpoint from './PlayerEndpoint.ts'; +export * as ResolveURLEndpoint from './ResolveURLEndpoint.ts'; +export * as SearchEndpoint from './SearchEndpoint.ts'; + +export * as Account from './account/index.ts'; +export * as Browse from './browse/index.ts'; +export * as Channel from './channel/index.ts'; +export * as Comment from './comment/index.ts'; +export * as Like from './like/index.ts'; +export * as Music from './music/index.ts'; +export * as Notification from './notification/index.ts'; +export * as Playlist from './playlist/index.ts'; +export * as Subscription from './subscription/index.ts'; +export * as Upload from './upload/index.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/like/DislikeEndpoint.ts b/deno/src/core/endpoints/like/DislikeEndpoint.ts new file mode 100644 index 00000000..a9f1afa5 --- /dev/null +++ b/deno/src/core/endpoints/like/DislikeEndpoint.ts @@ -0,0 +1,19 @@ +import type { IDislikeRequest, DislikeEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/like/dislike'; + +/** + * Builds a `/like/dislike` endpoint payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: DislikeEndpointOptions): IDislikeRequest { + return { + target: { + videoId: options.target.video_id + }, + ...{ + client: options.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/like/LikeEndpoint.ts b/deno/src/core/endpoints/like/LikeEndpoint.ts new file mode 100644 index 00000000..f14e1269 --- /dev/null +++ b/deno/src/core/endpoints/like/LikeEndpoint.ts @@ -0,0 +1,19 @@ +import type { ILikeRequest, LikeEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/like/like'; + +/** + * Builds a `/like/like` endpoint payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: LikeEndpointOptions): ILikeRequest { + return { + target: { + videoId: options.target.video_id + }, + ...{ + client: options.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/like/RemoveLikeEndpoint.ts b/deno/src/core/endpoints/like/RemoveLikeEndpoint.ts new file mode 100644 index 00000000..d310f677 --- /dev/null +++ b/deno/src/core/endpoints/like/RemoveLikeEndpoint.ts @@ -0,0 +1,19 @@ +import type { IRemoveLikeRequest, RemoveLikeEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/like/removelike'; + +/** + * Builds a `/like/removelike` endpoint payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: RemoveLikeEndpointOptions): IRemoveLikeRequest { + return { + target: { + videoId: options.target.video_id + }, + ...{ + client: options.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/like/index.ts b/deno/src/core/endpoints/like/index.ts new file mode 100644 index 00000000..85de64da --- /dev/null +++ b/deno/src/core/endpoints/like/index.ts @@ -0,0 +1,3 @@ +export * as LikeEndpoint from './LikeEndpoint.ts'; +export * as DislikeEndpoint from './DislikeEndpoint.ts'; +export * as RemoveLikeEndpoint from './RemoveLikeEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts b/deno/src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts new file mode 100644 index 00000000..9ec10aab --- /dev/null +++ b/deno/src/core/endpoints/music/GetSearchSuggestionsEndpoint.ts @@ -0,0 +1,16 @@ +import type { IMusicGetSearchSuggestionsRequest, MusicGetSearchSuggestionsEndpointOptions } from '../../../types/index.ts'; + + +export const PATH = '/music/get_search_suggestions'; + +/** + * Builds a `/music/get_search_suggestions` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: MusicGetSearchSuggestionsEndpointOptions): IMusicGetSearchSuggestionsRequest { + return { + input: opts.input, + client: 'YTMUSIC' + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/music/index.ts b/deno/src/core/endpoints/music/index.ts new file mode 100644 index 00000000..e445aba4 --- /dev/null +++ b/deno/src/core/endpoints/music/index.ts @@ -0,0 +1 @@ +export * as GetSearchSuggestionsEndpoint from './GetSearchSuggestionsEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/notification/GetUnseenCountEndpoint.ts b/deno/src/core/endpoints/notification/GetUnseenCountEndpoint.ts new file mode 100644 index 00000000..3d3bd902 --- /dev/null +++ b/deno/src/core/endpoints/notification/GetUnseenCountEndpoint.ts @@ -0,0 +1 @@ +export const PATH = '/notification/get_unseen_count'; \ No newline at end of file diff --git a/deno/src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts b/deno/src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts new file mode 100644 index 00000000..fd5c7cea --- /dev/null +++ b/deno/src/core/endpoints/notification/ModifyChannelPreferenceEndpoint.ts @@ -0,0 +1,17 @@ +import type { IModifyChannelPreferenceRequest, ModifyChannelPreferenceEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/notification/modify_channel_preference'; + +/** + * Builds a `/notification/modify_channel_preference` request payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: ModifyChannelPreferenceEndpointOptions): IModifyChannelPreferenceRequest { + return { + params: options.params, + ...{ + client: options.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/notification/index.ts b/deno/src/core/endpoints/notification/index.ts new file mode 100644 index 00000000..733c6bfd --- /dev/null +++ b/deno/src/core/endpoints/notification/index.ts @@ -0,0 +1,2 @@ +export * as GetUnseenCountEndpoint from './GetUnseenCountEndpoint.ts'; +export * as ModifyChannelPreferenceEndpoint from './ModifyChannelPreferenceEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/playlist/CreateEndpoint.ts b/deno/src/core/endpoints/playlist/CreateEndpoint.ts new file mode 100644 index 00000000..f2e64e97 --- /dev/null +++ b/deno/src/core/endpoints/playlist/CreateEndpoint.ts @@ -0,0 +1,15 @@ +import type { ICreatePlaylistRequest, CreatePlaylistEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/playlist/create'; + +/** + * Builds a `/playlist/create` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: CreatePlaylistEndpointOptions): ICreatePlaylistRequest { + return { + title: opts.title, + ids: opts.ids + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/playlist/DeleteEndpoint.ts b/deno/src/core/endpoints/playlist/DeleteEndpoint.ts new file mode 100644 index 00000000..65397f5b --- /dev/null +++ b/deno/src/core/endpoints/playlist/DeleteEndpoint.ts @@ -0,0 +1,14 @@ +import type { IDeletePlaylistRequest, DeletePlaylistEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/playlist/delete'; + +/** + * Builds a `/playlist/delete` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: DeletePlaylistEndpointOptions): IDeletePlaylistRequest { + return { + playlistId: opts.playlist_id + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/playlist/index.ts b/deno/src/core/endpoints/playlist/index.ts new file mode 100644 index 00000000..765f46b0 --- /dev/null +++ b/deno/src/core/endpoints/playlist/index.ts @@ -0,0 +1,2 @@ +export * as CreateEndpoint from './CreateEndpoint.ts'; +export * as DeleteEndpoint from './DeleteEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/subscription/SubscribeEndpoint.ts b/deno/src/core/endpoints/subscription/SubscribeEndpoint.ts new file mode 100644 index 00000000..c532c9c8 --- /dev/null +++ b/deno/src/core/endpoints/subscription/SubscribeEndpoint.ts @@ -0,0 +1,18 @@ +import type { ISubscribeRequest, SubscribeEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/subscription/subscribe'; + +/** + * Builds a `/subscription/subscribe` endpoint payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: SubscribeEndpointOptions): ISubscribeRequest { + return { + channelIds: options.channel_ids, + ...{ + client: options.client, + params: options.params + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/subscription/UnsubscribeEndpoint.ts b/deno/src/core/endpoints/subscription/UnsubscribeEndpoint.ts new file mode 100644 index 00000000..43bbdccd --- /dev/null +++ b/deno/src/core/endpoints/subscription/UnsubscribeEndpoint.ts @@ -0,0 +1,18 @@ +import type { IUnsubscribeRequest, UnsubscribeEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/subscription/unsubscribe'; + +/** + * Builds a `/subscription/unsubscribe` endpoint payload. + * @param options - The options to use. + * @returns The payload. + */ +export function build(options: UnsubscribeEndpointOptions): IUnsubscribeRequest { + return { + channelIds: options.channel_ids, + ...{ + client: options.client, + params: options.params + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/subscription/index.ts b/deno/src/core/endpoints/subscription/index.ts new file mode 100644 index 00000000..5e69b811 --- /dev/null +++ b/deno/src/core/endpoints/subscription/index.ts @@ -0,0 +1,2 @@ +export * as SubscribeEndpoint from './SubscribeEndpoint.ts'; +export * as UnsubscribeEndpoint from './UnsubscribeEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/endpoints/upload/CreateVideoEndpoint.ts b/deno/src/core/endpoints/upload/CreateVideoEndpoint.ts new file mode 100644 index 00000000..4d537a18 --- /dev/null +++ b/deno/src/core/endpoints/upload/CreateVideoEndpoint.ts @@ -0,0 +1,37 @@ +import type { ICreateVideoRequest, CreateVideoEndpointOptions } from '../../../types/index.ts'; + +export const PATH = '/upload/createvideo'; + +/** + * Builds a `/upload/createvideo` request payload. + * @param opts - The options to use. + * @returns The payload. + */ +export function build(opts: CreateVideoEndpointOptions): ICreateVideoRequest { + return { + resourceId: { + scottyResourceId: { + id: opts.resource_id.scotty_resource_id.id + } + }, + frontendUploadId: opts.frontend_upload_id, + initialMetadata: { + title: { + newTitle: opts.initial_metadata.title.new_title + }, + description: { + newDescription: opts.initial_metadata.description.new_description, + shouldSegment: opts.initial_metadata.description.should_segment + }, + privacy: { + newPrivacy: opts.initial_metadata.privacy.new_privacy + }, + draftState: { + isDraft: !!opts.initial_metadata.draft_state.is_draft + } + }, + ...{ + client: opts.client + } + }; +} \ No newline at end of file diff --git a/deno/src/core/endpoints/upload/index.ts b/deno/src/core/endpoints/upload/index.ts new file mode 100644 index 00000000..148fef43 --- /dev/null +++ b/deno/src/core/endpoints/upload/index.ts @@ -0,0 +1 @@ +export * as CreateVideoEndpoint from './CreateVideoEndpoint.ts'; \ No newline at end of file diff --git a/deno/src/core/index.ts b/deno/src/core/index.ts index f35a7671..65d1982a 100644 --- a/deno/src/core/index.ts +++ b/deno/src/core/index.ts @@ -1,38 +1,16 @@ -export { default as AccountManager } from './AccountManager.ts'; -export * from './AccountManager.ts'; +export { default as Session } from './Session.ts'; +export * from './Session.ts'; export { default as Actions } from './Actions.ts'; export * from './Actions.ts'; -export { default as Feed } from './Feed.ts'; -export * from './Feed.ts'; - -export { default as FilterableFeed } from './FilterableFeed.ts'; -export * from './FilterableFeed.ts'; - -export { default as InteractionManager } from './InteractionManager.ts'; -export * from './InteractionManager.ts'; - -export { default as Kids } from './Kids.ts'; -export * from './Kids.ts'; - -export { default as Music } from './Music.ts'; -export * from './Music.ts'; +export { default as Player } from './Player.ts'; +export * from './Player.ts'; export { default as OAuth } from './OAuth.ts'; export * from './OAuth.ts'; -export { default as Player } from './Player.ts'; -export * from './Player.ts'; - -export { default as PlaylistManager } from './PlaylistManager.ts'; -export * from './PlaylistManager.ts'; - -export { default as Session } from './Session.ts'; -export * from './Session.ts'; - -export { default as Studio } from './Studio.ts'; -export * from './Studio.ts'; - -export { default as TabbedFeed } from './TabbedFeed.ts'; -export * from './TabbedFeed.ts'; +export * as Clients from './clients/index.ts'; +export * as Endpoints from './endpoints/index.ts'; +export * as Managers from './managers/index.ts'; +export * as Mixins from './mixins/index.ts'; \ No newline at end of file diff --git a/deno/src/core/AccountManager.ts b/deno/src/core/managers/AccountManager.ts similarity index 50% rename from deno/src/core/AccountManager.ts rename to deno/src/core/managers/AccountManager.ts index 9be44183..4e08ed08 100644 --- a/deno/src/core/AccountManager.ts +++ b/deno/src/core/managers/AccountManager.ts @@ -1,15 +1,16 @@ -import Proto from '../proto/index.ts'; -import type Actions from './Actions.ts'; -import type { ApiResponse } from './Actions.ts'; +import AccountInfo from '../../parser/youtube/AccountInfo.ts'; +import Analytics from '../../parser/youtube/Analytics.ts'; +import Settings from '../../parser/youtube/Settings.ts'; +import TimeWatched from '../../parser/youtube/TimeWatched.ts'; -import Analytics from '../parser/youtube/Analytics.ts'; -import TimeWatched from '../parser/youtube/TimeWatched.ts'; -import AccountInfo from '../parser/youtube/AccountInfo.ts'; -import Settings from '../parser/youtube/Settings.ts'; +import Proto from '../../proto/index.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; +import { Account, BrowseEndpoint, Channel } from '../endpoints/index.ts'; -import { InnertubeError } from '../utils/Utils.ts'; +import type Actions from '../Actions.ts'; +import type { ApiResponse } from '../Actions.ts'; -class AccountManager { +export default class AccountManager { #actions: Actions; channel: { @@ -30,10 +31,12 @@ class AccountManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - return this.#actions.execute('/channel/edit_name', { - givenName: new_name, - client: 'ANDROID' - }); + return this.#actions.execute( + Channel.EditNameEndpoint.PATH, + Channel.EditNameEndpoint.build({ + given_name: new_name + }) + ); }, /** * Edits channel description. @@ -43,10 +46,12 @@ class AccountManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - return this.#actions.execute('/channel/edit_description', { - givenDescription: new_description, - client: 'ANDROID' - }); + return this.#actions.execute( + Channel.EditDescriptionEndpoint.PATH, + Channel.EditDescriptionEndpoint.build({ + given_description: new_description + }) + ); }, /** * Retrieves basic channel analytics. @@ -62,7 +67,11 @@ class AccountManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute('/account/accounts_list', { client: 'ANDROID' }); + const response = await this.#actions.execute( + Account.AccountListEndpoint.PATH, + Account.AccountListEndpoint.build() + ); + return new AccountInfo(response); } @@ -70,10 +79,12 @@ class AccountManager { * Retrieves time watched statistics. */ async getTimeWatched(): Promise { - const response = await this.#actions.execute('/browse', { - browseId: 'SPtime_watched', - client: 'ANDROID' - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: 'SPtime_watched', + client: 'ANDROID' + }) + ); return new TimeWatched(response); } @@ -82,10 +93,11 @@ class AccountManager { * Opens YouTube settings. */ async getSettings(): Promise { - const response = await this.#actions.execute('/browse', { - browseId: 'SPaccount_overview' - }); - + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: 'SPaccount_overview' + }) + ); return new Settings(this.#actions, response); } @@ -95,16 +107,14 @@ class AccountManager { async getAnalytics(): Promise { const info = await this.getInfo(); - const params = Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId); - - const response = await this.#actions.execute('/browse', { - browseId: 'FEanalytics_screen', - client: 'ANDROID', - params - }); + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + browse_id: 'FEanalytics_screen', + params: Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId), + client: 'ANDROID' + }) + ); return new Analytics(response); } -} - -export default AccountManager; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/InteractionManager.ts b/deno/src/core/managers/InteractionManager.ts similarity index 59% rename from deno/src/core/InteractionManager.ts rename to deno/src/core/managers/InteractionManager.ts index aa87b5ca..41b433df 100644 --- a/deno/src/core/InteractionManager.ts +++ b/deno/src/core/managers/InteractionManager.ts @@ -1,9 +1,14 @@ -import Proto from '../proto/index.ts'; -import type Actions from './Actions.ts'; -import type { ApiResponse } from './Actions.ts'; -import { throwIfMissing } from '../utils/Utils.ts'; +import Proto from '../../proto/index.ts'; +import type Actions from '../Actions.ts'; +import type { ApiResponse } from '../Actions.ts'; -class InteractionManager { +import { throwIfMissing } from '../../utils/Utils.ts'; +import { LikeEndpoint, DislikeEndpoint, RemoveLikeEndpoint } from '../endpoints/like/index.ts'; +import { SubscribeEndpoint, UnsubscribeEndpoint } from '../endpoints/subscription/index.ts'; +import { CreateCommentEndpoint, PerformCommentActionEndpoint } from '../endpoints/comment/index.ts'; +import { ModifyChannelPreferenceEndpoint } from '../endpoints/notification/index.ts'; + +export default class InteractionManager { #actions: Actions; constructor(actions: Actions) { @@ -20,12 +25,12 @@ class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute('/like/like', { - client: 'ANDROID', - target: { - videoId: video_id - } - }); + const action = await this.#actions.execute( + LikeEndpoint.PATH, LikeEndpoint.build({ + client: 'ANDROID', + target: { video_id } + }) + ); return action; } @@ -40,12 +45,12 @@ class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute('/like/dislike', { - client: 'ANDROID', - target: { - videoId: video_id - } - }); + const action = await this.#actions.execute( + DislikeEndpoint.PATH, DislikeEndpoint.build({ + client: 'ANDROID', + target: { video_id } + }) + ); return action; } @@ -60,12 +65,12 @@ class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute('/like/removelike', { - client: 'ANDROID', - target: { - videoId: video_id - } - }); + const action = await this.#actions.execute( + RemoveLikeEndpoint.PATH, RemoveLikeEndpoint.build({ + client: 'ANDROID', + target: { video_id } + }) + ); return action; } @@ -80,11 +85,13 @@ class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute('/subscription/subscribe', { - client: 'ANDROID', - channelIds: [ channel_id ], - params: 'EgIIAhgA' - }); + const action = await this.#actions.execute( + SubscribeEndpoint.PATH, SubscribeEndpoint.build({ + client: 'ANDROID', + channel_ids: [ channel_id ], + params: 'EgIIAhgA' + }) + ); return action; } @@ -93,17 +100,19 @@ class InteractionManager { * Unsubscribes from a given channel. * @param channel_id - The channel ID */ - async unsubscribe(channel_id: string): Promise{ + async unsubscribe(channel_id: string): Promise { throwIfMissing({ channel_id }); if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute('/subscription/unsubscribe', { - client: 'ANDROID', - channelIds: [ channel_id ], - params: 'CgIIAhgA' - }); + const action = await this.#actions.execute( + UnsubscribeEndpoint.PATH, UnsubscribeEndpoint.build({ + client: 'ANDROID', + channel_ids: [ channel_id ], + params: 'CgIIAhgA' + }) + ); return action; } @@ -119,11 +128,13 @@ class InteractionManager { if (!this.#actions.session.logged_in) throw new Error('You must be signed in to perform this operation.'); - const action = await this.#actions.execute('/comment/create_comment', { - client: 'ANDROID', - commentText: text, - createCommentParams: Proto.encodeCommentParams(video_id) - }); + const action = await this.#actions.execute( + CreateCommentEndpoint.PATH, CreateCommentEndpoint.build({ + comment_text: text, + create_comment_params: Proto.encodeCommentParams(video_id), + client: 'ANDROID' + }) + ); return action; } @@ -139,10 +150,12 @@ class InteractionManager { const target_action = Proto.encodeCommentActionParams(22, { text, target_language, ...args }); - const response = await this.#actions.execute('/comment/perform_comment_action', { - client: 'ANDROID', - actions: [ target_action ] - }); + const response = await this.#actions.execute( + PerformCommentActionEndpoint.PATH, PerformCommentActionEndpoint.build({ + client: 'ANDROID', + actions: [ target_action ] + }) + ); const mutation = response.data.frameworkUpdates.entityBatchUpdate.mutations[0].payload.commentEntityPayload; @@ -175,13 +188,13 @@ class InteractionManager { if (!Object.keys(pref_types).includes(type.toUpperCase())) throw new Error(`Invalid notification preference type: ${type}`); - const action = await this.#actions.execute('/notification/modify_channel_preference', { - client: 'WEB', - params: Proto.encodeNotificationPref(channel_id, pref_types[type.toUpperCase() as keyof typeof pref_types]) - }); + const action = await this.#actions.execute( + ModifyChannelPreferenceEndpoint.PATH, ModifyChannelPreferenceEndpoint.build({ + client: 'WEB', + params: Proto.encodeNotificationPref(channel_id, pref_types[type.toUpperCase() as keyof typeof pref_types]) + }) + ); return action; } -} - -export default InteractionManager; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/PlaylistManager.ts b/deno/src/core/managers/PlaylistManager.ts similarity index 71% rename from deno/src/core/PlaylistManager.ts rename to deno/src/core/managers/PlaylistManager.ts index 2648933a..131301af 100644 --- a/deno/src/core/PlaylistManager.ts +++ b/deno/src/core/managers/PlaylistManager.ts @@ -1,10 +1,14 @@ -import type Feed from './Feed.ts'; -import type Actions from './Actions.ts'; -import Playlist from '../parser/youtube/Playlist.ts'; +import Playlist from '../../parser/youtube/Playlist.ts'; +import type Actions from '../Actions.ts'; +import type Feed from '../mixins/Feed.ts'; -import { InnertubeError, throwIfMissing } from '../utils/Utils.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'; -class PlaylistManager { +export default class PlaylistManager { #actions: Actions; constructor(actions: Actions) { @@ -22,11 +26,12 @@ class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute('/playlist/create', { - title, - ids: video_ids, - parse: false - }); + const response = await this.#actions.execute( + CreateEndpoint.PATH, CreateEndpoint.build({ + ids: video_ids, + title + }) + ); return { success: response.success, @@ -46,7 +51,11 @@ class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute('playlist/delete', { playlistId: playlist_id }); + const response = await this.#actions.execute( + DeleteEndpoint.PATH, DeleteEndpoint.build({ + playlist_id + }) + ); return { playlist_id, @@ -67,14 +76,15 @@ class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const response = await this.#actions.execute('/browse/edit_playlist', { - playlistId: playlist_id, - actions: video_ids.map((id) => ({ - action: 'ACTION_ADD_VIDEO', - addedVideoId: id - })), - parse: false - }); + const response = await this.#actions.execute( + EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build({ + actions: video_ids.map((id) => ({ + action: 'ACTION_ADD_VIDEO', + added_video_id: id + })), + playlist_id + }) + ); return { playlist_id, @@ -93,23 +103,16 @@ class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const info = await this.#actions.execute('/browse', { - browseId: `VL${playlist_id}`, - parse: true - }); + const info = await this.#actions.execute( + BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true } + ); const playlist = new Playlist(this.#actions, info, true); if (!playlist.info.is_editable) throw new InnertubeError('This playlist cannot be edited.', playlist_id); - const payload = { - playlistId: playlist_id, - actions: [] as { - action: string; - setVideoId: string; - }[] - }; + const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; const getSetVideoIds = async (pl: Feed): Promise => { const videos = pl.videos.filter((video) => video_ids.includes(video.key('id').string())); @@ -117,7 +120,7 @@ class PlaylistManager { videos.forEach((video) => payload.actions.push({ action: 'ACTION_REMOVE_VIDEO', - setVideoId: video.key('set_video_id').string() + set_video_id: video.key('set_video_id').string() }) ); @@ -132,7 +135,9 @@ class PlaylistManager { if (!payload.actions.length) throw new InnertubeError('Given video ids were not found in this playlist.', video_ids); - const response = await this.#actions.execute('/browse/edit_playlist', { ...payload, parse: false }); + const response = await this.#actions.execute( + EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload) + ); return { playlist_id, @@ -152,24 +157,16 @@ class PlaylistManager { if (!this.#actions.session.logged_in) throw new InnertubeError('You must be signed in to perform this operation.'); - const info = await this.#actions.execute('/browse', { - browseId: `VL${playlist_id}`, - parse: true - }); + const info = await this.#actions.execute( + BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: `VL${playlist_id}` }), parse: true } + ); const playlist = new Playlist(this.#actions, info, true); if (!playlist.info.is_editable) throw new InnertubeError('This playlist cannot be edited.', playlist_id); - const payload = { - playlistId: playlist_id, - actions: [] as { - action: string, - setVideoId?: string, - movedSetVideoIdPredecessor?: string - }[] - }; + const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] }; let set_video_id_0: string | undefined, set_video_id_1: string | undefined; @@ -190,20 +187,17 @@ class PlaylistManager { payload.actions.push({ action: 'ACTION_MOVE_VIDEO_AFTER', - setVideoId: set_video_id_0, - movedSetVideoIdPredecessor: set_video_id_1 + set_video_id: set_video_id_0, + moved_set_video_id_predecessor: set_video_id_1 }); - const response = await this.#actions.execute('/browse/edit_playlist', { - ...payload, - parse: false - }); + const response = await this.#actions.execute( + EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload) + ); return { playlist_id, action_result: response.data.actions // TODO: implement actions in the parser }; } -} - -export default PlaylistManager; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/managers/index.ts b/deno/src/core/managers/index.ts new file mode 100644 index 00000000..b17b4583 --- /dev/null +++ b/deno/src/core/managers/index.ts @@ -0,0 +1,3 @@ +export { default as AccountManager } from './AccountManager.ts'; +export { default as PlaylistManager } from './PlaylistManager.ts'; +export { default as InteractionManager } from './InteractionManager.ts'; \ No newline at end of file diff --git a/deno/src/core/Feed.ts b/deno/src/core/mixins/Feed.ts similarity index 68% rename from deno/src/core/Feed.ts rename to deno/src/core/mixins/Feed.ts index 8045d150..0e5a1210 100644 --- a/deno/src/core/Feed.ts +++ b/deno/src/core/mixins/Feed.ts @@ -1,40 +1,40 @@ -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 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'; -import Channel from '../parser/classes/Channel.ts'; -import CompactVideo from '../parser/classes/CompactVideo.ts'; -import GridChannel from '../parser/classes/GridChannel.ts'; -import GridPlaylist from '../parser/classes/GridPlaylist.ts'; -import GridVideo from '../parser/classes/GridVideo.ts'; -import Playlist from '../parser/classes/Playlist.ts'; -import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo.ts'; -import PlaylistVideo from '../parser/classes/PlaylistVideo.ts'; -import Post from '../parser/classes/Post.ts'; -import ReelItem from '../parser/classes/ReelItem.ts'; -import ReelShelf from '../parser/classes/ReelShelf.ts'; -import RichShelf from '../parser/classes/RichShelf.ts'; -import Shelf from '../parser/classes/Shelf.ts'; -import Tab from '../parser/classes/Tab.ts'; -import Video from '../parser/classes/Video.ts'; +import BackstagePost from '../../parser/classes/BackstagePost.ts'; +import SharedPost from '../../parser/classes/SharedPost.ts'; +import Channel from '../../parser/classes/Channel.ts'; +import CompactVideo from '../../parser/classes/CompactVideo.ts'; +import GridChannel from '../../parser/classes/GridChannel.ts'; +import GridPlaylist from '../../parser/classes/GridPlaylist.ts'; +import GridVideo from '../../parser/classes/GridVideo.ts'; +import Playlist from '../../parser/classes/Playlist.ts'; +import PlaylistPanelVideo from '../../parser/classes/PlaylistPanelVideo.ts'; +import PlaylistVideo from '../../parser/classes/PlaylistVideo.ts'; +import Post from '../../parser/classes/Post.ts'; +import ReelItem from '../../parser/classes/ReelItem.ts'; +import ReelShelf from '../../parser/classes/ReelShelf.ts'; +import RichShelf from '../../parser/classes/RichShelf.ts'; +import Shelf from '../../parser/classes/Shelf.ts'; +import Tab from '../../parser/classes/Tab.ts'; +import Video from '../../parser/classes/Video.ts'; -import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction.ts'; -import ContinuationItem from '../parser/classes/ContinuationItem.ts'; -import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults.ts'; -import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults.ts'; -import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo.ts'; +import AppendContinuationItemsAction from '../../parser/classes/actions/AppendContinuationItemsAction.ts'; +import ContinuationItem from '../../parser/classes/ContinuationItem.ts'; +import TwoColumnBrowseResults from '../../parser/classes/TwoColumnBrowseResults.ts'; +import TwoColumnSearchResults from '../../parser/classes/TwoColumnSearchResults.ts'; +import WatchCardCompactVideo from '../../parser/classes/WatchCardCompactVideo.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 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'; +import type { IParsedResponse } from '../../parser/types/index.ts'; +import type { ApiResponse } from '../Actions.ts'; -class Feed { +export default class Feed { #page: T; #continuation?: ObservedArray; #actions: Actions; @@ -210,6 +210,4 @@ class Feed { throw new InnertubeError('Could not get continuation data'); return new Feed(this.actions, continuation_data, true); } -} - -export default Feed; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/FilterableFeed.ts b/deno/src/core/mixins/FilterableFeed.ts similarity index 78% rename from deno/src/core/FilterableFeed.ts rename to deno/src/core/mixins/FilterableFeed.ts index 021ea9f6..c5672c87 100644 --- a/deno/src/core/FilterableFeed.ts +++ b/deno/src/core/mixins/FilterableFeed.ts @@ -1,14 +1,14 @@ -import ChipCloudChip from '../parser/classes/ChipCloudChip.ts'; -import FeedFilterChipBar from '../parser/classes/FeedFilterChipBar.ts'; -import { InnertubeError } from '../utils/Utils.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 { 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'; -class FilterableFeed extends Feed { +export default class FilterableFeed extends Feed { #chips?: ObservedArray; constructor(actions: Actions, data: ApiResponse | T, already_parsed = false) { @@ -69,6 +69,4 @@ class FilterableFeed extends Feed { return new Feed(this.actions, response, true); } -} - -export default FilterableFeed; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/MediaInfo.ts b/deno/src/core/mixins/MediaInfo.ts similarity index 82% rename from deno/src/core/MediaInfo.ts rename to deno/src/core/mixins/MediaInfo.ts index cdd5becf..58b83812 100644 --- a/deno/src/core/MediaInfo.ts +++ b/deno/src/core/mixins/MediaInfo.ts @@ -1,11 +1,14 @@ -import Actions, { ApiResponse } from './Actions.ts'; -import Constants from '../utils/Constants.ts'; -import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../utils/FormatUtils.ts'; -import { InnertubeError } from '../utils/Utils.ts'; -import Format from '../parser/classes/misc/Format.ts'; -import Parser, { INextResponse, IPlayerResponse } from '../parser/index.ts'; +import type { ApiResponse } from '../Actions.ts'; +import type Actions from '../Actions.ts'; +import Constants from '../../utils/Constants.ts'; +import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.ts'; +import FormatUtils from '../../utils/FormatUtils.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; +import type Format from '../../parser/classes/misc/Format.ts'; +import type { INextResponse, IPlayerResponse } from '../../parser/index.ts'; +import Parser from '../../parser/index.ts'; -export class MediaInfo { +export default class MediaInfo { #page: [IPlayerResponse, INextResponse?]; #actions: Actions; #cpn: string; diff --git a/deno/src/core/TabbedFeed.ts b/deno/src/core/mixins/TabbedFeed.ts similarity index 78% rename from deno/src/core/TabbedFeed.ts rename to deno/src/core/mixins/TabbedFeed.ts index 11adafa1..436f91e8 100644 --- a/deno/src/core/TabbedFeed.ts +++ b/deno/src/core/mixins/TabbedFeed.ts @@ -1,13 +1,13 @@ -import Tab from '../parser/classes/Tab.ts'; +import Tab from '../../parser/classes/Tab.ts'; import Feed from './Feed.ts'; -import { InnertubeError } from '../utils/Utils.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; -import type Actions from './Actions.ts'; -import type { ObservedArray } from '../parser/helpers.ts'; -import type { IParsedResponse } from '../parser/types/ParsedResponse.ts'; -import type { ApiResponse } from './Actions.ts'; +import type Actions from '../Actions.ts'; +import type { ObservedArray } from '../../parser/helpers.ts'; +import type { IParsedResponse } from '../../parser/types/ParsedResponse.ts'; +import type { ApiResponse } from '../Actions.ts'; -class TabbedFeed extends Feed { +export default class TabbedFeed extends Feed { #tabs?: ObservedArray; #actions: Actions; @@ -56,6 +56,4 @@ class TabbedFeed extends Feed { get title(): string | undefined { return this.page.contents_memo?.getType(Tab)?.find((tab) => tab.selected)?.title.toString(); } -} - -export default TabbedFeed; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/core/mixins/index.ts b/deno/src/core/mixins/index.ts new file mode 100644 index 00000000..2f411197 --- /dev/null +++ b/deno/src/core/mixins/index.ts @@ -0,0 +1,4 @@ +export { default as Feed } from './Feed.ts'; +export { default as FilterableFeed } from './FilterableFeed.ts'; +export { default as TabbedFeed } from './TabbedFeed.ts'; +export { default as MediaInfo } from './MediaInfo.ts'; \ No newline at end of file diff --git a/deno/src/parser/classes/AccountChannel.ts b/deno/src/parser/classes/AccountChannel.ts index 6815976e..3ed199cb 100644 --- a/deno/src/parser/classes/AccountChannel.ts +++ b/deno/src/parser/classes/AccountChannel.ts @@ -3,7 +3,7 @@ import NavigationEndpoint from './NavigationEndpoint.ts'; import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; -class AccountChannel extends YTNode { +export default class AccountChannel extends YTNode { static type = 'AccountChannel'; title: Text; @@ -14,6 +14,4 @@ class AccountChannel extends YTNode { this.title = new Text(data.title); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); } -} - -export default AccountChannel; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/AccountItemSection.ts b/deno/src/parser/classes/AccountItemSection.ts index 80ccdae0..6c591b69 100644 --- a/deno/src/parser/classes/AccountItemSection.ts +++ b/deno/src/parser/classes/AccountItemSection.ts @@ -1,14 +1,16 @@ import Parser from '../index.ts'; - +import AccountItemSectionHeader from './AccountItemSectionHeader.ts'; +import NavigationEndpoint from './NavigationEndpoint.ts'; import Text from './misc/Text.ts'; import Thumbnail from './misc/Thumbnail.ts'; -import NavigationEndpoint from './NavigationEndpoint.ts'; -import AccountItemSectionHeader from './AccountItemSectionHeader.ts'; -import { YTNode } from '../helpers.ts'; +import { YTNode, observe, type ObservedArray } from '../helpers.ts'; import type { RawNode } from '../index.ts'; -class AccountItem { +/** + * Not a real renderer but we treat it as one to keep things organized. + */ +export class AccountItem extends YTNode { static type = 'AccountItem'; account_name: Text; @@ -20,27 +22,26 @@ class AccountItem { account_byline: Text; constructor(data: RawNode) { + super(); this.account_name = new Text(data.accountName); this.account_photo = Thumbnail.fromResponse(data.accountPhoto); - this.is_selected = data.isSelected; - this.is_disabled = data.isDisabled; - this.has_channel = data.hasChannel; + this.is_selected = !!data.isSelected; + this.is_disabled = !!data.isDisabled; + this.has_channel = !!data.hasChannel; this.endpoint = new NavigationEndpoint(data.serviceEndpoint); this.account_byline = new Text(data.accountByline); } } -class AccountItemSection extends YTNode { +export default class AccountItemSection extends YTNode { static type = 'AccountItemSection'; - contents; - header; + contents: ObservedArray; + header: AccountItemSectionHeader | null; constructor(data: RawNode) { super(); - this.contents = data.contents.map((ac: any) => new AccountItem(ac.accountItem)); + this.contents = observe(data.contents.map((ac: RawNode) => new AccountItem(ac.accountItem))); this.header = Parser.parseItem(data.header, AccountItemSectionHeader); } -} - -export default AccountItemSection; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/AccountItemSectionHeader.ts b/deno/src/parser/classes/AccountItemSectionHeader.ts index 1355c5ab..f680f5b0 100644 --- a/deno/src/parser/classes/AccountItemSectionHeader.ts +++ b/deno/src/parser/classes/AccountItemSectionHeader.ts @@ -1,7 +1,8 @@ import Text from './misc/Text.ts'; import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; -class AccountItemSectionHeader extends YTNode { + +export default class AccountItemSectionHeader extends YTNode { static type = 'AccountItemSectionHeader'; title: Text; @@ -10,6 +11,4 @@ class AccountItemSectionHeader extends YTNode { super(); this.title = new Text(data.title); } -} - -export default AccountItemSectionHeader; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/AccountSectionList.ts b/deno/src/parser/classes/AccountSectionList.ts index 4e8bb7b9..faec5a61 100644 --- a/deno/src/parser/classes/AccountSectionList.ts +++ b/deno/src/parser/classes/AccountSectionList.ts @@ -4,7 +4,8 @@ import AccountItemSection from './AccountItemSection.ts'; import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; -class AccountSectionList extends YTNode { + +export default class AccountSectionList extends YTNode { static type = 'AccountSectionList'; contents; @@ -15,6 +16,4 @@ class AccountSectionList extends YTNode { this.contents = Parser.parseItem(data.contents[0], AccountItemSection); this.footers = Parser.parseItem(data.footers[0], AccountChannel); } -} - -export default AccountSectionList; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/Alert.ts b/deno/src/parser/classes/Alert.ts index 2c8aac67..ced95aa2 100644 --- a/deno/src/parser/classes/Alert.ts +++ b/deno/src/parser/classes/Alert.ts @@ -1,7 +1,8 @@ import Text from './misc/Text.ts'; import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; -class Alert extends YTNode { + +export default class Alert extends YTNode { static type = 'Alert'; text: Text; @@ -12,6 +13,4 @@ class Alert extends YTNode { this.text = new Text(data.text); this.alert_type = data.type; } -} - -export default Alert; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/AudioOnlyPlayability.ts b/deno/src/parser/classes/AudioOnlyPlayability.ts index 7b6071f5..83d4dba2 100644 --- a/deno/src/parser/classes/AudioOnlyPlayability.ts +++ b/deno/src/parser/classes/AudioOnlyPlayability.ts @@ -1,6 +1,7 @@ import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; -class AudioOnlyPlayability extends YTNode { + +export default class AudioOnlyPlayability extends YTNode { static type = 'AudioOnlyPlayability'; audio_only_availability: string; @@ -9,6 +10,4 @@ class AudioOnlyPlayability extends YTNode { super(); this.audio_only_availability = data.audioOnlyAvailability; } -} - -export default AudioOnlyPlayability; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/AutomixPreviewVideo.ts b/deno/src/parser/classes/AutomixPreviewVideo.ts index b3eda2b3..ad392612 100644 --- a/deno/src/parser/classes/AutomixPreviewVideo.ts +++ b/deno/src/parser/classes/AutomixPreviewVideo.ts @@ -1,7 +1,8 @@ import { YTNode } from '../helpers.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; import type { RawNode } from '../index.ts'; -class AutomixPreviewVideo extends YTNode { + +export default class AutomixPreviewVideo extends YTNode { static type = 'AutomixPreviewVideo'; playlist_video?: { endpoint: NavigationEndpoint }; @@ -14,6 +15,4 @@ class AutomixPreviewVideo extends YTNode { }; } } -} - -export default AutomixPreviewVideo; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/BackstageImage.ts b/deno/src/parser/classes/BackstageImage.ts index 61e877e7..1ce46353 100644 --- a/deno/src/parser/classes/BackstageImage.ts +++ b/deno/src/parser/classes/BackstageImage.ts @@ -1,18 +1,17 @@ import Thumbnail from './misc/Thumbnail.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class BackstageImage extends YTNode { +export default class BackstageImage extends YTNode { static type = 'BackstageImage'; image: Thumbnail[]; endpoint: NavigationEndpoint; - constructor(data: any) { + constructor(data: RawNode) { super(); this.image = Thumbnail.fromResponse(data.image); this.endpoint = new NavigationEndpoint(data.command); } -} - -export default BackstageImage; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/BackstagePost.ts b/deno/src/parser/classes/BackstagePost.ts index f45a1064..e9449893 100644 --- a/deno/src/parser/classes/BackstagePost.ts +++ b/deno/src/parser/classes/BackstagePost.ts @@ -1,13 +1,12 @@ -import Parser from '../index.ts'; -import Author from './misc/Author.ts'; -import Text from './misc/Text.ts'; +import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; import CommentActionButtons from './comments/CommentActionButtons.ts'; import Menu from './menus/Menu.ts'; +import Author from './misc/Author.ts'; +import Text from './misc/Text.ts'; -import { YTNode } from '../helpers.ts'; - -class BackstagePost extends YTNode { +export default class BackstagePost extends YTNode { static type = 'BackstagePost'; id: string; @@ -18,13 +17,13 @@ class BackstagePost extends YTNode { vote_status?: string; vote_count?: Text; menu?: Menu | null; - action_buttons; - vote_button; + action_buttons?: CommentActionButtons | null; + vote_button?: CommentActionButtons | null; surface: string; endpoint?: NavigationEndpoint; attachment; - constructor(data: any) { + constructor(data: RawNode) { super(); this.id = data.postId; @@ -36,40 +35,38 @@ class BackstagePost extends YTNode { this.content = new Text(data.contentText); this.published = new Text(data.publishedTimeText); - if (data.pollStatus) { + if (Reflect.has(data, 'pollStatus')) { this.poll_status = data.pollStatus; } - if (data.voteStatus) { + if (Reflect.has(data, 'voteStatus')) { this.vote_status = data.voteStatus; } - if (data.voteCount) { + if (Reflect.has(data, 'voteCount')) { this.vote_count = new Text(data.voteCount); } - if (data.actionMenu) { + if (Reflect.has(data, 'actionMenu')) { this.menu = Parser.parseItem(data.actionMenu, Menu); } - if (data.actionButtons) { + if (Reflect.has(data, 'actionButtons')) { this.action_buttons = Parser.parseItem(data.actionButtons, CommentActionButtons); } - if (data.voteButton) { + if (Reflect.has(data, 'voteButton')) { this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons); } - if (data.navigationEndpoint) { + if (Reflect.has(data, 'navigationEndpoint')) { this.endpoint = new NavigationEndpoint(data.navigationEndpoint); } - if (data.backstageAttachment) { + if (Reflect.has(data, 'backstageAttachment')) { this.attachment = Parser.parseItem(data.backstageAttachment); } this.surface = data.surface; } -} - -export default BackstagePost; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/BackstagePostThread.ts b/deno/src/parser/classes/BackstagePostThread.ts index ab1c67b1..ea20e0fd 100644 --- a/deno/src/parser/classes/BackstagePostThread.ts +++ b/deno/src/parser/classes/BackstagePostThread.ts @@ -1,15 +1,13 @@ -import Parser from '../index.ts'; +import Parser, { type RawNode } from '../index.ts'; import { YTNode } from '../helpers.ts'; -class BackstagePostThread extends YTNode { +export default class BackstagePostThread extends YTNode { static type = 'BackstagePostThread'; - post; + post: YTNode; - constructor(data: any) { + constructor(data: RawNode) { super(); this.post = Parser.parseItem(data.post); } -} - -export default BackstagePostThread; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/BrowseFeedActions.ts b/deno/src/parser/classes/BrowseFeedActions.ts index 59e7ef0a..03113935 100644 --- a/deno/src/parser/classes/BrowseFeedActions.ts +++ b/deno/src/parser/classes/BrowseFeedActions.ts @@ -1,15 +1,13 @@ -import Parser from '../index.ts'; -import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import { type ObservedArray, YTNode } from '../helpers.ts'; -class BrowseFeedActions extends YTNode { +export default class BrowseFeedActions extends YTNode { static type = 'BrowseFeedActions'; - contents; + contents: ObservedArray; - constructor(data: any) { + constructor(data: RawNode) { super(); this.contents = Parser.parseArray(data.contents); } -} - -export default BrowseFeedActions; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/BrowserMediaSession.ts b/deno/src/parser/classes/BrowserMediaSession.ts index bbfc0464..dca0b81f 100644 --- a/deno/src/parser/classes/BrowserMediaSession.ts +++ b/deno/src/parser/classes/BrowserMediaSession.ts @@ -1,18 +1,17 @@ import Text from './misc/Text.ts'; import Thumbnail from './misc/Thumbnail.ts'; import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class BrowserMediaSession extends YTNode { +export default class BrowserMediaSession extends YTNode { static type = 'BrowserMediaSession'; - album; - thumbnails; + album: Text; + thumbnails: Thumbnail[]; - constructor (data: any) { + constructor (data: RawNode) { super(); this.album = new Text(data.album); this.thumbnails = Thumbnail.fromResponse(data.thumbnailDetails); } -} - -export default BrowserMediaSession; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/Button.ts b/deno/src/parser/classes/Button.ts index 8c3fc848..31af5b1e 100644 --- a/deno/src/parser/classes/Button.ts +++ b/deno/src/parser/classes/Button.ts @@ -1,6 +1,5 @@ import Text from './misc/Text.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; - import { YTNode } from '../helpers.ts'; import type { RawNode } from '../index.ts'; @@ -8,31 +7,29 @@ export default class Button extends YTNode { static type = 'Button'; text?: string; - label?: string; tooltip?: string; icon_type?: string; is_disabled?: boolean; - endpoint: NavigationEndpoint; constructor(data: RawNode) { super(); - if (data.text) { + if (Reflect.has(data, 'text')) { this.text = new Text(data.text).toString(); } - if (data.accessibility?.label) { - this.label = data.accessibility?.label; + if (Reflect.has(data, 'accessibility') && Reflect.has(data.accessibility, 'label')) { + this.label = data.accessibility.label; } - if (data.tooltip) { + if (Reflect.has(data, 'tooltip')) { this.tooltip = data.tooltip; } - if (data.icon?.iconType) { - this.icon_type = data.icon?.iconType; + if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType')) { + this.icon_type = data.icon.iconType; } if (Reflect.has(data, 'isDisabled')) { diff --git a/deno/src/parser/classes/C4TabbedHeader.ts b/deno/src/parser/classes/C4TabbedHeader.ts index 8de20d02..4e3dc71d 100644 --- a/deno/src/parser/classes/C4TabbedHeader.ts +++ b/deno/src/parser/classes/C4TabbedHeader.ts @@ -1,15 +1,13 @@ -import Parser from '../index.ts'; +import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import Button from './Button.ts'; +import ChannelHeaderLinks from './ChannelHeaderLinks.ts'; +import SubscribeButton from './SubscribeButton.ts'; import Author from './misc/Author.ts'; import Text from './misc/Text.ts'; import Thumbnail from './misc/Thumbnail.ts'; -import Button from './Button.ts'; -import ChannelHeaderLinks from './ChannelHeaderLinks.ts'; -import SubscribeButton from './SubscribeButton.ts'; - -import { YTNode } from '../helpers.ts'; - -class C4TabbedHeader extends YTNode { +export default class C4TabbedHeader extends YTNode { static type = 'C4TabbedHeader'; author: Author; @@ -24,53 +22,51 @@ class C4TabbedHeader extends YTNode { channel_handle?: Text; channel_id?: string; - constructor(data: any) { + constructor(data: RawNode) { super(); this.author = new Author({ simpleText: data.title, navigationEndpoint: data.navigationEndpoint }, data.badges, data.avatar); - if (data.banner) { + if (Reflect.has(data, 'banner')) { this.banner = Thumbnail.fromResponse(data.banner); } - if (data.tv_banner) { + if (Reflect.has(data, 'tv_banner')) { this.tv_banner = Thumbnail.fromResponse(data.tvBanner); } - if (data.mobile_banner) { + if (Reflect.has(data, 'mobile_banner')) { this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner); } - if (data.subscriberCountText) { + if (Reflect.has(data, 'subscriberCountText')) { this.subscribers = new Text(data.subscriberCountText); } - if (data.videosCountText) { + if (Reflect.has(data, 'videosCountText')) { this.videos_count = new Text(data.videosCountText); } - if (data.sponsorButton) { + if (Reflect.has(data, 'sponsorButton')) { this.sponsor_button = Parser.parseItem(data.sponsorButton, Button); } - if (data.subscribeButton) { + if (Reflect.has(data, 'subscribeButton')) { this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]); } - if (data.headerLinks) { + if (Reflect.has(data, 'headerLinks')) { this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks); } - if (data.channelHandleText) { + if (Reflect.has(data, 'channelHandleText')) { this.channel_handle = new Text(data.channelHandleText); } - if (data.channelId) { + if (Reflect.has(data, 'channelId')) { this.channel_id = data.channelId; } } -} - -export default C4TabbedHeader; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/CallToActionButton.ts b/deno/src/parser/classes/CallToActionButton.ts index 6213e4d9..42d4ee88 100644 --- a/deno/src/parser/classes/CallToActionButton.ts +++ b/deno/src/parser/classes/CallToActionButton.ts @@ -1,19 +1,18 @@ import Text from './misc/Text.ts'; import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class CallToActionButton extends YTNode { +export default class CallToActionButton extends YTNode { static type = 'CallToActionButton'; label: Text; icon_type: string; style: string; - constructor(data: any) { + constructor(data: RawNode) { super(); this.label = new Text(data.label); this.icon_type = data.icon.iconType; this.style = data.style; } -} - -export default CallToActionButton; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/Card.ts b/deno/src/parser/classes/Card.ts index 77b4d366..f6a59913 100644 --- a/deno/src/parser/classes/Card.ts +++ b/deno/src/parser/classes/Card.ts @@ -1,13 +1,13 @@ -import Parser from '../index.ts'; +import Parser, { type RawNode } from '../index.ts'; import { YTNode } from '../helpers.ts'; -class Card extends YTNode { +export default class Card extends YTNode { static type = 'Card'; - teaser; - content; - card_id: string | null; - feature: string | null; + teaser: YTNode; + content: YTNode; + card_id?: string; + feature?: string; cue_ranges: { start_card_active_ms: string; @@ -16,12 +16,18 @@ class Card extends YTNode { icon_after_teaser_ms: string; }[]; - constructor(data: any) { + constructor(data: RawNode) { super(); this.teaser = Parser.parseItem(data.teaser); this.content = Parser.parseItem(data.content); - this.card_id = data.cardId || null; - this.feature = data.feature || null; + + if (Reflect.has(data, 'cardId')) { + this.card_id = data.cardId; + } + + if (Reflect.has(data, 'feature')) { + this.feature = data.feature; + } this.cue_ranges = data.cueRanges.map((cr: any) => ({ start_card_active_ms: cr.startCardActiveMs, @@ -30,6 +36,4 @@ class Card extends YTNode { icon_after_teaser_ms: cr.iconAfterTeaserMs })); } -} - -export default Card; +} \ No newline at end of file diff --git a/deno/src/parser/classes/CardCollection.ts b/deno/src/parser/classes/CardCollection.ts index 4e39a6b5..e3a64004 100644 --- a/deno/src/parser/classes/CardCollection.ts +++ b/deno/src/parser/classes/CardCollection.ts @@ -1,20 +1,18 @@ -import Parser from '../index.ts'; +import { YTNode, type ObservedArray } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; import Text from './misc/Text.ts'; -import { YTNode } from '../helpers.ts'; -class CardCollection extends YTNode { +export default class CardCollection extends YTNode { static type = 'CardCollection'; - cards; + cards: ObservedArray; header: Text; allow_teaser_dismiss: boolean; - constructor(data: any) { + constructor(data: RawNode) { super(); this.cards = Parser.parseArray(data.cards); this.header = new Text(data.headerText); this.allow_teaser_dismiss = data.allowTeaserDismiss; } -} - -export default CardCollection; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/CarouselHeader.ts b/deno/src/parser/classes/CarouselHeader.ts index 8e443e30..d55dff1b 100644 --- a/deno/src/parser/classes/CarouselHeader.ts +++ b/deno/src/parser/classes/CarouselHeader.ts @@ -1,15 +1,13 @@ -import Parser from '../index.ts'; -import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import { type ObservedArray, YTNode } from '../helpers.ts'; -class CarouselHeader extends YTNode { +export default class CarouselHeader extends YTNode { static type = 'CarouselHeader'; - contents: YTNode[]; + contents: ObservedArray; - constructor(data: any) { + constructor(data: RawNode) { super(); this.contents = Parser.parseArray(data.contents); } -} - -export default CarouselHeader; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/CarouselItem.ts b/deno/src/parser/classes/CarouselItem.ts index 725a2ac5..0723a50c 100644 --- a/deno/src/parser/classes/CarouselItem.ts +++ b/deno/src/parser/classes/CarouselItem.ts @@ -1,18 +1,17 @@ -import Parser from '../index.ts'; -import { YTNode } from '../helpers.ts'; - +import Parser, { type RawNode } from '../index.ts'; +import { type ObservedArray, YTNode } from '../helpers.ts'; import Thumbnail from './misc/Thumbnail.ts'; -class CarouselItem extends YTNode { +export default class CarouselItem extends YTNode { static type = 'CarouselItem'; - items: YTNode[]; + items: ObservedArray; background_color: string; layout_style: string; pagination_thumbnails: Thumbnail[]; paginator_alignment: string; - constructor (data: any) { + constructor (data: RawNode) { super(); this.items = Parser.parseArray(data.carouselItems); this.background_color = data.backgroundColor; @@ -20,6 +19,9 @@ class CarouselItem extends YTNode { this.pagination_thumbnails = Thumbnail.fromResponse(data.paginationThumbnails); this.paginator_alignment = data.paginatorAlignment; } -} -export default CarouselItem; \ No newline at end of file + // XXX: For consistency. + get contents() { + return this.items; + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/Channel.ts b/deno/src/parser/classes/Channel.ts index 2510fa30..efe92936 100644 --- a/deno/src/parser/classes/Channel.ts +++ b/deno/src/parser/classes/Channel.ts @@ -1,28 +1,25 @@ -import Parser from '../index.ts'; - -import Text from './misc/Text.ts'; -import Author from './misc/Author.ts'; -import NavigationEndpoint from './NavigationEndpoint.ts'; - -import SubscribeButton from './SubscribeButton.ts'; -import Button from './Button.ts'; - import { YTNode } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import Button from './Button.ts'; +import NavigationEndpoint from './NavigationEndpoint.ts'; +import SubscribeButton from './SubscribeButton.ts'; +import Author from './misc/Author.ts'; +import Text from './misc/Text.ts'; -class Channel extends YTNode { +export default class Channel extends YTNode { static type = 'Channel'; id: string; author: Author; - subscribers: Text; - videos: Text; + subscriber_count: Text; + video_count: Text; long_byline: Text; short_byline: Text; endpoint: NavigationEndpoint; subscribe_button: SubscribeButton | Button | null; description_snippet: Text; - constructor(data: any) { + constructor(data: RawNode) { super(); this.id = data.channelId; @@ -31,15 +28,31 @@ class Channel extends YTNode { navigationEndpoint: data.navigationEndpoint }, data.ownerBadges, data.thumbnail); - // TODO: subscriberCountText is now the channel's handle and videoCountText is the subscriber count. Why haven't they renamed the properties? - this.subscribers = new Text(data.subscriberCountText); - this.videos = new Text(data.videoCountText); + // XXX: `subscriberCountText` is now the channel's handle and `videoCountText` is the subscriber count. + this.subscriber_count = new Text(data.subscriberCountText); + this.video_count = new Text(data.videoCountText); this.long_byline = new Text(data.longBylineText); this.short_byline = new Text(data.shortBylineText); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]); this.description_snippet = new Text(data.descriptionSnippet); } -} -export default Channel; + /** + * @deprecated + * This will be removed in a future release. + * Please use {@link Channel.subscriber_count} instead. + */ + get subscribers(): Text { + return this.subscriber_count; + } + + /** + * @deprecated + * This will be removed in a future release. + * Please use {@link Channel.video_count} instead. + */ + get videos(): Text { + 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 0a094f49..5e037fdd 100644 --- a/deno/src/parser/classes/ChannelAboutFullMetadata.ts +++ b/deno/src/parser/classes/ChannelAboutFullMetadata.ts @@ -1,14 +1,11 @@ -import Parser from '../index.ts'; - +import { YTNode, type ObservedArray } from '../helpers.ts'; +import Parser, { type RawNode } from '../index.ts'; +import Button from './Button.ts'; +import NavigationEndpoint from './NavigationEndpoint.ts'; import Text from './misc/Text.ts'; import Thumbnail from './misc/Thumbnail.ts'; -import NavigationEndpoint from './NavigationEndpoint.ts'; -import Button from './Button.ts'; - -import { YTNode } from '../helpers.ts'; - -class ChannelAboutFullMetadata extends YTNode { +export default class ChannelAboutFullMetadata extends YTNode { static type = 'ChannelAboutFullMetadata'; id: string; @@ -22,15 +19,15 @@ class ChannelAboutFullMetadata extends YTNode { title: Text; }[]; - views: Text; - joined: Text; + view_count: Text; + joined_date: Text; description: Text; email_reveal: NavigationEndpoint; can_reveal_email: boolean; country: Text; - buttons: Button[]; + buttons: ObservedArray