diff --git a/README.md b/README.md index 9900e1709..dae71bbde 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

YouTube.js

-

A full-featured wrapper around the InnerTube API, which is what YouTube itself uses

+

A full-featured wrapper around the InnerTube API

@@ -84,7 +84,7 @@ ___ ## Description -InnerTube is an API used by all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform[^1]. This library handles all the low-level communication with InnerTube, providing a simple, and efficient way to interact with YouTube programmatically. It is designed to emulate an actual client as closely as possible, including how API responses are [parsed](https://github.com/LuanRT/YouTube.js/tree/main/src/parser#how-it-works). +InnerTube is an API used by all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform[^1]. This library handles all the low-level communication with InnerTube, providing a simple, and efficient way to interact with YouTube programmatically. It is designed to emulate an actual client as closely as possible, including how API responses are parsed. If you have any questions or need help, feel free to reach out to us on our [Discord server][discord] or open an issue [here](https://github.com/LuanRT/YouTube.js/issues). @@ -229,6 +229,7 @@ const yt = await Innertube.create({ * [.playlist](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/playlist.md) * [.music](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/music.md) * [.studio](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/studio.md) + * [.kids](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/kids.md)

diff --git a/docs/API/account.md b/docs/API/account.md index dbb949868..8161d3893 100644 --- a/docs/API/account.md +++ b/docs/API/account.md @@ -46,7 +46,7 @@ Retrieves account information.

- `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -63,7 +63,7 @@ Retrieves time watched statistics.

- `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -91,6 +91,9 @@ Retrieves YouTube settings. - `#sidebar_items` - Returns options available in the sidebar menu. +- `#page` + - Returns the original InnerTube response(s), parsed and sanitized. +

@@ -106,7 +109,7 @@ Retrieves basic channel analytics.

- `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

\ No newline at end of file diff --git a/docs/API/kids.md b/docs/API/kids.md new file mode 100644 index 000000000..ec1ad7825 --- /dev/null +++ b/docs/API/kids.md @@ -0,0 +1,85 @@ +# YouTube Kids + +YouTube Kids is a modified version of the YouTube app, with a simplified interface and curated content. This class allows you to interact with its API. + +## API + +* Kids + * [.search(query)](#search) + * [.getInfo(video_id)](#getinfo) + * [.getHomeFeed()](#gethomefeed) + + +### search(query) + +Searches the given query on YouTube Kids. + +**Returns:** `Promise.` + +| Param | Type | Description | +| --- | --- | --- | +| query | `string` | The query to search | + + +
+Methods & Getters +

+ +- `#page` + - Returns the original InnerTube response(s), parsed and sanitized. + +

+
+ + +### getInfo(video_id) + +Retrieves video info. + +**Returns:** `Promise.` + +| Param | Type | Description | +| --- | --- | --- | +| video_id | `string` | The video id | + +
+Methods & Getters +

+ +- `#toDash(url_transformer?)` + - Generates a DASH manifest from the streaming data. + +- `#chooseFormat(options)` + - Selects the format that best matches the given options. This method is used internally by `#download`. + +- `#download(options?)` + - Downloads the video. + +- `#addToWatchHistory()` + - Adds the video to the watch history. + +- `#page` + - Returns the original InnerTube response(s), parsed and sanitized. + +

+
+ + +### getHomeFeed() + +Retrieves the home feed. + +**Returns:** `Promise.` + +
+Methods & Getters +

+ +- `#selectCategoryTab(tab: string | KidsCategoryTab)` + - Selects the given category tab. + +- `#categories` + - Returns available categories. + +- `#page` + - Returns the original InnerTube response(s), parsed and sanitized. \ No newline at end of file diff --git a/docs/API/music.md b/docs/API/music.md index 8bad7fb9e..8368b900e 100644 --- a/docs/API/music.md +++ b/docs/API/music.md @@ -1,6 +1,6 @@ -# Music +# YouTube Music -YouTube Music class. +YouTube Music is a music streaming service developed by YouTube, a subsidiary of Google. It provides a tailored interface for the service oriented towards music streaming, with a greater emphasis on browsing and discovery compared to its main service. This class allows you to interact with its API. ## API @@ -49,6 +49,21 @@ Retrieves track info. - `#available_tabs` - Returns available tabs. +- `#toDash(url_transformer?)` + - Generates a DASH manifest from the streaming data. + +- `#chooseFormat(options)` + - Selects the format that best matches the given options. This method is used internally by `#download`. + +- `#download(options?)` + - Downloads the track. + +- `#addToWatchHistory()` + - Adds the song to the watch history. + +- `#page` + - Returns the original InnerTube response(s), parsed and sanitized. +

@@ -99,7 +114,7 @@ Searches on YouTube Music. - Returns songs shelf. - `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -124,6 +139,9 @@ Retrieves home feed. - `#page` - Returns original InnerTube response (sanitized). +- `#page` + - Returns the original InnerTube response(s), parsed and sanitized. +

@@ -139,7 +157,7 @@ Retrieves “Explore” feed.

- `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -174,7 +192,7 @@ Retrieves library. - Returns available sort options. - `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -195,7 +213,7 @@ Retrieves artist's info & content.

- `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -216,7 +234,7 @@ Retrieves given album.

- `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -249,7 +267,7 @@ Retrieves given playlist. - Checks if continuation is available. - `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

@@ -303,7 +321,7 @@ Retrieves your YouTube Music recap. - Retrieves recap playlist. - `#page` - - Returns original InnerTube response (sanitized). + - Returns the original InnerTube response(s), parsed and sanitized.

diff --git a/src/Innertube.ts b/src/Innertube.ts index 94985c3d0..1284d58fa 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -11,14 +11,15 @@ import Library from './parser/youtube/Library'; import NotificationsMenu from './parser/youtube/NotificationsMenu'; import Playlist from './parser/youtube/Playlist'; import Search from './parser/youtube/Search'; -import VideoInfo, { DownloadOptions, FormatOptions } from './parser/youtube/VideoInfo'; +import VideoInfo from './parser/youtube/VideoInfo'; import AccountManager from './core/AccountManager'; import Feed from './core/Feed'; import InteractionManager from './core/InteractionManager'; import YTMusic from './core/Music'; import PlaylistManager from './core/PlaylistManager'; -import Studio from './core/Studio'; +import YTStudio from './core/Studio'; +import YTKids from './core/Kids'; import TabbedFeed from './core/TabbedFeed'; import HomeFeed from './parser/youtube/HomeFeed'; import Proto from './proto/index'; @@ -28,6 +29,7 @@ import type Actions from './core/Actions'; import type Format from './parser/classes/misc/Format'; import { generateRandomString, throwIfMissing } from './utils/Utils'; +import type { FormatOptions, DownloadOptions } from './utils/FormatUtils'; export type InnertubeConfig = SessionOptions; @@ -39,7 +41,7 @@ export interface SearchFilters { 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'; +export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS' class Innertube { session: Session; @@ -47,7 +49,8 @@ class Innertube { playlist: PlaylistManager; interact: InteractionManager; music: YTMusic; - studio: Studio; + studio: YTStudio; + kids: YTKids; actions: Actions; constructor(session: Session) { @@ -56,7 +59,8 @@ class Innertube { this.playlist = new PlaylistManager(this.session.actions); this.interact = new InteractionManager(this.session.actions); this.music = new YTMusic(this.session); - this.studio = new Studio(this.session); + this.studio = new YTStudio(this.session); + this.kids = new YTKids(this.session); this.actions = this.session.actions; } diff --git a/src/core/Kids.ts b/src/core/Kids.ts new file mode 100644 index 000000000..f1c8f7d90 --- /dev/null +++ b/src/core/Kids.ts @@ -0,0 +1,57 @@ +import Search from '../parser/ytkids/Search'; +import HomeFeed from '../parser/ytkids/HomeFeed'; +import VideoInfo from '../parser/ytkids/VideoInfo'; +import type Session from './Session'; +import { generateRandomString } from '../utils/Utils'; + +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.data); + } + + /** + * 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 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.data); + } +} + +export default Kids; \ No newline at end of file diff --git a/src/core/Session.ts b/src/core/Session.ts index 21d50b4f7..f698142d0 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -11,6 +11,7 @@ import Proto from '../proto'; export enum ClientType { WEB = 'WEB', + KIDS = 'WEB_KIDS', MUSIC = 'WEB_REMIX', ANDROID = 'ANDROID', ANDROID_MUSIC = 'ANDROID_MUSIC', @@ -45,6 +46,15 @@ export interface Context { deviceMake: string; deviceModel: string; utcOffsetMinutes: number; + kidsAppInfo?: { + categorySettings: { + enabledCategories: string[]; + }; + contentSettings: { + corpusPreference: string; + kidsNoSearchMode: string; + }; + }; }; user: { enableSafetyMode: boolean; diff --git a/src/parser/classes/CompactChannel.ts b/src/parser/classes/CompactChannel.ts new file mode 100644 index 000000000..d8ca2819e --- /dev/null +++ b/src/parser/classes/CompactChannel.ts @@ -0,0 +1,35 @@ +import Parser from '..'; +import Text from './misc/Text'; +import Thumbnail from './misc/Thumbnail'; +import NavigationEndpoint from './NavigationEndpoint'; +import type Menu from './menus/Menu'; +import { YTNode } from '../helpers'; + +class CompactChannel extends YTNode { + static type = 'CompactChannel'; + + title: Text; + channel_id: string; + thumbnail: Thumbnail[]; + display_name: Text; + video_count: Text; + subscriber_count: Text; + endpoint: NavigationEndpoint; + tv_banner: Thumbnail[]; + menu: Menu | null; + + constructor(data: any) { + super(); + this.title = new Text(data.title); + this.channel_id = data.channelId; + this.thumbnail = Thumbnail.fromResponse(data.thumbnail); + this.display_name = new Text(data.displayName); + this.video_count = new Text(data.videoCountText); + this.subscriber_count = new Text(data.subscriberCountText); + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.tv_banner = Thumbnail.fromResponse(data.tvBanner); + this.menu = Parser.parseItem(data.menu); + } +} + +export default CompactChannel; \ No newline at end of file diff --git a/src/parser/classes/SlimOwner.ts b/src/parser/classes/SlimOwner.ts new file mode 100644 index 000000000..ae7d2ed78 --- /dev/null +++ b/src/parser/classes/SlimOwner.ts @@ -0,0 +1,25 @@ +import Parser from '..'; +import Text from './misc/Text'; +import Thumbnail from './misc/Thumbnail'; +import NavigationEndpoint from './NavigationEndpoint'; +import SubscribeButton from './SubscribeButton'; +import { YTNode } from '../helpers'; + +class SlimOwner extends YTNode { + static type = 'SlimOwner'; + + thumbnail: Thumbnail[]; + title: Text; + endpoint: NavigationEndpoint; + subscribe_button: SubscribeButton | null; + + constructor(data: any) { + super(); + this.thumbnail = Thumbnail.fromResponse(data.thumbnail); + this.title = new Text(data.title); + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.subscribe_button = Parser.parseItem(data.subscribeButton, SubscribeButton); + } +} + +export default SlimOwner; \ No newline at end of file diff --git a/src/parser/classes/SlimVideoMetadata.ts b/src/parser/classes/SlimVideoMetadata.ts new file mode 100644 index 000000000..e256ae7b2 --- /dev/null +++ b/src/parser/classes/SlimVideoMetadata.ts @@ -0,0 +1,28 @@ +import Parser from '..'; +import Text from './misc/Text'; +import { YTNode } from '../helpers'; + +class SlimVideoMetadata extends YTNode { + static type = 'SlimVideoMetadata'; + + title: Text; + collapsed_subtitle: Text; + expanded_subtitle: Text; + owner: any; + description: Text; + video_id: string; + date: Text; + + constructor(data: any) { + super(); + this.title = new Text(data.title); + this.collapsed_subtitle = new Text(data.collapsedSubtitle); + this.expanded_subtitle = new Text(data.expandedSubtitle); + this.owner = Parser.parseItem(data.owner); + this.description = new Text(data.description); + this.video_id = data.videoId; + this.date = new Text(data.dateText); + } +} + +export default SlimVideoMetadata; \ No newline at end of file diff --git a/src/parser/classes/ytkids/AnchoredSection.ts b/src/parser/classes/ytkids/AnchoredSection.ts new file mode 100644 index 000000000..8b038b862 --- /dev/null +++ b/src/parser/classes/ytkids/AnchoredSection.ts @@ -0,0 +1,31 @@ +import Parser from '../..'; +import NavigationEndpoint from '../NavigationEndpoint'; +import type SectionList from '../SectionList'; +import { YTNode } from '../../helpers'; + +class AnchoredSection extends YTNode { + static type = 'AnchoredSection'; + + title: string; + content: SectionList | null; + endpoint: NavigationEndpoint; + category_assets: { + asset_key: string; + background_color: string; + }; + category_type: string; + + constructor(data: any) { + super(); + this.title = data.title; + this.content = Parser.parseItem(data.content); + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.category_assets = { + asset_key: data.categoryAssets?.assetKey, + background_color: data.categoryAssets?.backgroundColor + }; + this.category_type = data.categoryType; + } +} + +export default AnchoredSection; \ No newline at end of file diff --git a/src/parser/classes/ytkids/KidsCategoriesHeader.ts b/src/parser/classes/ytkids/KidsCategoriesHeader.ts new file mode 100644 index 000000000..6806db8d6 --- /dev/null +++ b/src/parser/classes/ytkids/KidsCategoriesHeader.ts @@ -0,0 +1,19 @@ +import Parser from '../..'; +import type Button from '../Button'; +import type KidsCategoryTab from './KidsCategoryTab'; +import { YTNode } from '../../helpers'; + +class KidsCategoriesHeader extends YTNode { + static type = 'kidsCategoriesHeader'; + + category_tabs: KidsCategoryTab[]; + privacy_button: Button | null; + + constructor(data: any) { + super(); + this.category_tabs = Parser.parseArray(data.categoryTabs); + this.privacy_button = Parser.parseItem