mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-16 19:12:24 +00:00
Compare commits
28 Commits
v4.2.0-den
...
v8.2.0-den
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944f821d9a | ||
|
|
bd9ae29de3 | ||
|
|
1b159e5162 | ||
|
|
2b58b3887c | ||
|
|
2294bf7065 | ||
|
|
f2d673ac95 | ||
|
|
1b557f25e3 | ||
|
|
20f055b7ea | ||
|
|
09321408e5 | ||
|
|
848c7e68e2 | ||
|
|
bc437015e2 | ||
|
|
96e337a1d0 | ||
|
|
8d47c81318 | ||
|
|
5b9febc7bf | ||
|
|
55d47e1fe6 | ||
|
|
a40abda80c | ||
|
|
e147a01ba5 | ||
|
|
499937fe3a | ||
|
|
0582bcb677 | ||
|
|
50fdef7c07 | ||
|
|
5f69f10e85 | ||
|
|
dc69b7437e | ||
|
|
7d71f3d2cd | ||
|
|
37e9987afd | ||
|
|
6b1d5f0655 | ||
|
|
2d1972bee0 | ||
|
|
48dc99e28b | ||
|
|
b19d687eed |
15
README.md
15
README.md
@@ -31,8 +31,8 @@
|
||||
<sup>Special thanks to:</sup>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://serpapi.com#gh-light-mode-only" target="_blank">
|
||||
<img width="80" alt="SerpApi" src="https://luanrt.is-a.dev/assets/img/serpapi.svg" />
|
||||
<a href="https://serpapi.com" target="_blank">
|
||||
<img width="80" alt="SerpApi" src="https://luanrt.github.io/assets/img/serpapi.svg" />
|
||||
<br>
|
||||
<sub>
|
||||
API to get search engine results with ease.
|
||||
@@ -171,7 +171,7 @@ import dashjs from 'dashjs';
|
||||
|
||||
const youtube = await Innertube.create({ /* setup - see above */ });
|
||||
|
||||
// get the video info
|
||||
// Get the video info
|
||||
const videoInfo = await youtube.getInfo('videoId');
|
||||
|
||||
// now convert to a dash manifest
|
||||
@@ -191,7 +191,7 @@ const player = dashjs.MediaPlayer().create();
|
||||
player.initialize(videoElement, uri, true);
|
||||
```
|
||||
|
||||
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web). Alternatively, you can view it live at [ytjsexample.pages.dev](https://ytjsexample.pages.dev/).
|
||||
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web).
|
||||
|
||||
<a name="custom-fetch"></a>
|
||||
|
||||
@@ -325,6 +325,9 @@ Retrieves video info.
|
||||
- `<info>#download(options)`
|
||||
- Downloads the video. See [download](#download).
|
||||
|
||||
- `<info>#getTranscript()`
|
||||
- Retrieves the video's transcript.
|
||||
|
||||
- `<info>#filters`
|
||||
- Returns filters that can be applied to the watch next feed.
|
||||
|
||||
@@ -542,6 +545,8 @@ Retrieves contents for a given channel.
|
||||
- `<channel>#getVideos()`
|
||||
- `<channel>#getShorts()`
|
||||
- `<channel>#getLiveStreams()`
|
||||
- `<channel>#getReleases()`
|
||||
- `<channel>#getPodcasts()`
|
||||
- `<channel>#getPlaylists()`
|
||||
- `<channel>#getHome()`
|
||||
- `<channel>#getCommunity()`
|
||||
@@ -786,7 +791,7 @@ We are immensely grateful to all the wonderful people who have contributed to th
|
||||
|
||||
## Contact
|
||||
|
||||
LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
|
||||
LuanRT - [@thesciencephile][twitter] - luanrt@thatsciencephile.com
|
||||
|
||||
Project Link: [https://github.com/LuanRT/YouTube.js][project]
|
||||
|
||||
|
||||
2
deno.ts
2
deno.ts
@@ -1,3 +1,3 @@
|
||||
export * from './deno/src/platform/deno.ts';
|
||||
import Innertube from './deno/src/platform/deno.ts';
|
||||
export default Innertube;
|
||||
export default Innertube;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.2.0",
|
||||
"version": "8.2.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",
|
||||
@@ -56,7 +56,8 @@
|
||||
"Wykerd (https://github.com/wykerd/)",
|
||||
"MasterOfBob777 (https://github.com/MasterOfBob777)",
|
||||
"patrickkfkan (https://github.com/patrickkfkan)",
|
||||
"akkadaska (https://github.com/akkadaska)"
|
||||
"akkadaska (https://github.com/akkadaska)",
|
||||
"Absidue (https://github.com/absidue)"
|
||||
],
|
||||
"directories": {
|
||||
"test": "./test",
|
||||
@@ -68,12 +69,12 @@
|
||||
"lint": "npx eslint ./src",
|
||||
"lint:fix": "npx eslint --fix ./src",
|
||||
"build": "npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod",
|
||||
"build:parser-map": "node ./scripts/build-parser-map.cjs",
|
||||
"build:parser-map": "node ./scripts/gen-parser-map.mjs",
|
||||
"build:proto": "npx pb-gen-ts --entry-path=\"src/proto\" --out-dir=\"src/proto/generated\" --ext-in-import=\".js\"",
|
||||
"build:esm": "npx tsc",
|
||||
"build:deno": "npx cpy ./src ./deno && npx cpy ./package.json ./deno && npx replace \".ts';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'https://esm.sh/linkedom';\" \"'https://esm.sh/linkedom';\" ./deno -r && npx replace \"'https://esm.sh/jintr';\" \"'https://esm.sh/jintr';\" ./deno -r && npx replace \"new Jinter\" \"new Jinter\" ./deno -r",
|
||||
"build:esm": "npx tspc",
|
||||
"build:deno": "npx cpy ./src ./deno && npx esbuild ./src/utils/DashManifest.tsx --keep-names --format=esm --platform=neutral --target=es2020 --outfile=./deno/src/utils/DashManifest.js && npx cpy ./package.json ./deno && npx replace \".ts';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'./DashManifest.js';\" \"'./DashManifest.js';\" ./deno -r && npx replace \"'https://esm.sh/jintr';\" \"'https://esm.sh/jintr';\" ./deno -r",
|
||||
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --external:tslib --sourcemap --banner:js=\"/* eslint-disable */\"",
|
||||
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser",
|
||||
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --conditions=module --outfile=./bundle/browser.js --platform=browser",
|
||||
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
|
||||
"prepare": "npm run build",
|
||||
"watch": "npx tsc --watch"
|
||||
@@ -84,12 +85,15 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jintr": "^1.0.0",
|
||||
"linkedom": "^0.14.12",
|
||||
"jintr": "^1.1.0",
|
||||
"tslib": "^2.5.0",
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/jest": "^28.1.7",
|
||||
"@types/node": "^17.0.45",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
@@ -103,7 +107,9 @@
|
||||
"pbkit": "^0.0.59",
|
||||
"replace": "^1.2.2",
|
||||
"ts-jest": "^28.0.8",
|
||||
"typescript": "^4.9.5"
|
||||
"ts-patch": "^3.0.2",
|
||||
"ts-transformer-inline-file": "^0.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/LuanRT/YouTube.js/issues"
|
||||
|
||||
@@ -1,49 +1,59 @@
|
||||
|
||||
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 ShortsVideoInfo from './parser/ytshorts/VideoInfo.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 Proto from './proto/index.ts';
|
||||
import Constants from './utils/Constants.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 type Actions from './core/Actions.ts';
|
||||
import type Format from './parser/classes/misc/Format.ts';
|
||||
import * as Proto from './proto/index.ts';
|
||||
import * as Constants from './utils/Constants.ts';
|
||||
import { InnertubeError, generateRandomString, throwIfMissing } from './utils/Utils.ts';
|
||||
|
||||
import {
|
||||
BrowseEndpoint,
|
||||
GetNotificationMenuEndpoint,
|
||||
GuideEndpoint,
|
||||
NextEndpoint,
|
||||
PlayerEndpoint,
|
||||
ResolveURLEndpoint,
|
||||
SearchEndpoint,
|
||||
Reel
|
||||
} 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 { DownloadOptions, FormatOptions } from './utils/FormatUtils.ts';
|
||||
import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.ts';
|
||||
import { type IBrowseResponse, type IParsedResponse } from './parser/types/index.ts';
|
||||
import type { INextRequest } from './types/index.ts';
|
||||
import type { DownloadOptions, FormatOptions } from './types/FormatUtils.ts';
|
||||
import { encodeReelSequence } from './proto/index.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' | 'iOS' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS';
|
||||
|
||||
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 +77,39 @@ export default class Innertube {
|
||||
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
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,12 +121,45 @@ export default class Innertube {
|
||||
async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves shorts info.
|
||||
* @param short_id - The short id.
|
||||
* @param client - The client to use.
|
||||
*/
|
||||
async getShortsWatchItem(short_id: string, client?: InnerTubeClient): Promise<ShortsVideoInfo> {
|
||||
throwIfMissing({ short_id });
|
||||
|
||||
const watchResponse = this.actions.execute(
|
||||
Reel.WatchEndpoint.PATH, Reel.WatchEndpoint.build({
|
||||
short_id: short_id,
|
||||
client: client
|
||||
})
|
||||
);
|
||||
|
||||
const sequenceResponse = this.actions.execute(
|
||||
Reel.WatchSequenceEndpoint.PATH, Reel.WatchSequenceEndpoint.build({
|
||||
sequenceParams: encodeReelSequence(short_id)
|
||||
})
|
||||
);
|
||||
|
||||
const response = await Promise.all([ watchResponse, sequenceResponse ]);
|
||||
|
||||
return new ShortsVideoInfo(response, this.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a given query.
|
||||
* @param query - The search query.
|
||||
@@ -134,14 +168,11 @@ export default class Innertube {
|
||||
async search(query: string, filters: SearchFilters = {}): Promise<Search> {
|
||||
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 +210,13 @@ export default class Innertube {
|
||||
async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments> {
|
||||
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 +225,9 @@ export default class Innertube {
|
||||
* Retrieves YouTube's home feed (aka recommendations).
|
||||
*/
|
||||
async getHomeFeed(): Promise<HomeFeed> {
|
||||
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 +235,7 @@ export default class Innertube {
|
||||
* Retrieves YouTube's content guide.
|
||||
*/
|
||||
async getGuide(): Promise<Guide> {
|
||||
const response = await this.actions.execute('/guide');
|
||||
const response = await this.actions.execute(GuideEndpoint.PATH);
|
||||
return new Guide(response.data);
|
||||
}
|
||||
|
||||
@@ -208,7 +243,9 @@ export default class Innertube {
|
||||
* Returns the account's library.
|
||||
*/
|
||||
async getLibrary(): Promise<Library> {
|
||||
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 +254,9 @@ export default class Innertube {
|
||||
* Which can also be achieved with {@link getLibrary}.
|
||||
*/
|
||||
async getHistory(): Promise<History> {
|
||||
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 +264,9 @@ export default class Innertube {
|
||||
* Retrieves trending content.
|
||||
*/
|
||||
async getTrending(): Promise<TabbedFeed<IBrowseResponse>> {
|
||||
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 +274,19 @@ export default class Innertube {
|
||||
* Retrieves subscriptions feed.
|
||||
*/
|
||||
async getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>> {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves channels feed.
|
||||
*/
|
||||
async getChannelsFeed(): Promise<Feed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute(
|
||||
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEchannels' }), parse: true }
|
||||
);
|
||||
return new Feed(this.actions, response);
|
||||
}
|
||||
|
||||
@@ -243,7 +296,9 @@ export default class Innertube {
|
||||
*/
|
||||
async getChannel(id: string): Promise<Channel> {
|
||||
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 +306,11 @@ export default class Innertube {
|
||||
* Retrieves notifications.
|
||||
*/
|
||||
async getNotifications(): Promise<NotificationsMenu> {
|
||||
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 +318,7 @@ export default class Innertube {
|
||||
* Retrieves unseen notifications count.
|
||||
*/
|
||||
async getUnseenNotificationsCount(): Promise<number> {
|
||||
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 +334,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 +348,12 @@ export default class Innertube {
|
||||
async getHashtag(hashtag: string): Promise<HashtagFeed> {
|
||||
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 +387,9 @@ export default class Innertube {
|
||||
* @param url - The URL.
|
||||
*/
|
||||
async resolveURL(url: string): Promise<NavigationEndpoint> {
|
||||
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 +405,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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { NavigateAction } from '../parser/index.ts';
|
||||
import { Parser, NavigateAction } from '../parser/index.ts';
|
||||
import { InnertubeError } from '../utils/Utils.ts';
|
||||
|
||||
import type Session from './Session.ts';
|
||||
@@ -16,7 +16,7 @@ export interface ApiResponse {
|
||||
data: IRawResponse;
|
||||
}
|
||||
|
||||
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/updated_metadata' | '/notification/get_notification_menu' | string;
|
||||
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/reel' | '/updated_metadata' | '/notification/get_notification_menu' | string;
|
||||
|
||||
export type ParsedResponse<T> =
|
||||
T extends '/player' ? IPlayerResponse :
|
||||
@@ -28,7 +28,7 @@ export type ParsedResponse<T> =
|
||||
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<ApiResponse> {
|
||||
const data: Record<string, any> = {
|
||||
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.
|
||||
@@ -218,6 +167,7 @@ class Actions {
|
||||
'FElibrary',
|
||||
'FEhistory',
|
||||
'FEsubscriptions',
|
||||
'FEchannels',
|
||||
'FEmusic_listening_review',
|
||||
'FEmusic_library_landing',
|
||||
'SPaccount_overview',
|
||||
@@ -226,6 +176,4 @@ class Actions {
|
||||
'SPtime_watched'
|
||||
].includes(id);
|
||||
}
|
||||
}
|
||||
|
||||
export default Actions;
|
||||
}
|
||||
@@ -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<Search> {
|
||||
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<VideoInfo> {
|
||||
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<Channel> {
|
||||
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<HomeFeed> {
|
||||
const response = await this.#session.actions.execute('/browse', { browseId: 'FEkids_home', client: 'YTKIDS' });
|
||||
return new HomeFeed(this.#session.actions, response);
|
||||
}
|
||||
}
|
||||
|
||||
export default Kids;
|
||||
@@ -1,103 +0,0 @@
|
||||
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';
|
||||
|
||||
export class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
#actions: Actions;
|
||||
#cpn: string;
|
||||
#playback_tracking;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
this.#cpn = cpn;
|
||||
|
||||
if (info.playability_status?.status === 'ERROR')
|
||||
throw new InnertubeError('This video is unavailable', info.playability_status);
|
||||
|
||||
this.streaming_data = info.streaming_data;
|
||||
this.playability_status = info.playability_status;
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a DASH manifest from the streaming data.
|
||||
* @param url_transformer - Function to transform the URLs.
|
||||
* @param format_filter - Function to filter the formats.
|
||||
* @returns DASH manifest
|
||||
*/
|
||||
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter): Promise<string> {
|
||||
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the format that best matches the given options.
|
||||
* @param options - Options
|
||||
*/
|
||||
chooseFormat(options: FormatOptions): Format {
|
||||
return FormatUtils.chooseFormat(options, this.streaming_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the video.
|
||||
* @param options - Download options.
|
||||
*/
|
||||
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
||||
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds video to the watch history.
|
||||
*/
|
||||
async addToWatchHistory(client_name = Constants.CLIENTS.WEB.NAME, client_version = Constants.CLIENTS.WEB.VERSION, replacement = 'https://www.'): Promise<Response> {
|
||||
if (!this.#playback_tracking)
|
||||
throw new InnertubeError('Playback tracking not available');
|
||||
|
||||
const url_params = {
|
||||
cpn: this.#cpn,
|
||||
fmt: 251,
|
||||
rtn: 0,
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', replacement);
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name,
|
||||
client_version
|
||||
}, url_params);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions instance.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Playback Nonce.
|
||||
*/
|
||||
get cpn(): string {
|
||||
return this.#cpn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original parsed InnerTube response.
|
||||
*/
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import Constants from '../utils/Constants.ts';
|
||||
import * as Constants from '../utils/Constants.ts';
|
||||
import { OAuthError, Platform } from '../utils/Utils.ts';
|
||||
import type Session from './Session.ts';
|
||||
|
||||
/**
|
||||
* Represents the credentials used for authentication.
|
||||
*/
|
||||
export interface Credentials {
|
||||
/**
|
||||
* Token used to sign in.
|
||||
@@ -15,6 +18,14 @@ export interface Credentials {
|
||||
* Access token's expiration date, which is usually 24hrs-ish.
|
||||
*/
|
||||
expires: Date;
|
||||
/**
|
||||
* Optional client ID.
|
||||
*/
|
||||
client_id?: string;
|
||||
/**
|
||||
* Optional client secret.
|
||||
*/
|
||||
client_secret?: string;
|
||||
}
|
||||
|
||||
// TODO: actual type info for this.
|
||||
@@ -28,7 +39,12 @@ export type OAuthAuthEventHandler = (data: {
|
||||
export type OAuthAuthPendingEventHandler = (data: OAuthAuthPendingData) => any;
|
||||
export type OAuthAuthErrorEventHandler = (err: OAuthError) => any;
|
||||
|
||||
class OAuth {
|
||||
export type OAuthClientIdentity = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
};
|
||||
|
||||
export default class OAuth {
|
||||
#identity?: Record<string, string>;
|
||||
#session: Session;
|
||||
#credentials?: Credentials;
|
||||
@@ -71,6 +87,8 @@ class OAuth {
|
||||
this.#credentials = {
|
||||
access_token: credentials.access_token,
|
||||
refresh_token: credentials.refresh_token,
|
||||
client_id: credentials.client_id,
|
||||
client_secret: credentials.client_secret,
|
||||
expires: new Date(credentials.expires)
|
||||
};
|
||||
|
||||
@@ -96,7 +114,7 @@ class OAuth {
|
||||
client_id: this.#identity.client_id,
|
||||
scope: Constants.OAUTH.SCOPE,
|
||||
device_id: Platform.shim.uuidv4(),
|
||||
model_name: Constants.OAUTH.MODEL_NAME
|
||||
device_model: Constants.OAUTH.MODEL_NAME
|
||||
};
|
||||
|
||||
const response = await this.#session.http.fetch_function(new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE), {
|
||||
@@ -157,6 +175,8 @@ class OAuth {
|
||||
this.#credentials = {
|
||||
access_token: response_data.access_token,
|
||||
refresh_token: response_data.refresh_token,
|
||||
client_id: this.#identity?.client_id,
|
||||
client_secret: this.#identity?.client_secret,
|
||||
expires: expiration_date
|
||||
};
|
||||
|
||||
@@ -206,6 +226,8 @@ class OAuth {
|
||||
this.#credentials = {
|
||||
access_token: response_data.access_token,
|
||||
refresh_token: response_data.refresh_token || this.#credentials.refresh_token,
|
||||
client_id: this.#identity.client_id,
|
||||
client_secret: this.#identity.client_secret,
|
||||
expires: expiration_date
|
||||
};
|
||||
|
||||
@@ -226,7 +248,14 @@ class OAuth {
|
||||
/**
|
||||
* Retrieves client identity from YouTube TV.
|
||||
*/
|
||||
async #getClientIdentity(): Promise<{ [key: string]: string; }> {
|
||||
async #getClientIdentity(): Promise<OAuthClientIdentity> {
|
||||
if (this.#credentials?.client_id && this.credentials?.client_secret) {
|
||||
return {
|
||||
client_id: this.#credentials.client_id,
|
||||
client_secret: this.credentials.client_secret
|
||||
};
|
||||
}
|
||||
|
||||
const response = await this.#session.http.fetch_function(new URL('/tv', Constants.URLS.YT_BASE), { headers: Constants.OAUTH.HEADERS });
|
||||
|
||||
const response_data = await response.text();
|
||||
@@ -241,7 +270,7 @@ class OAuth {
|
||||
.replace(/\n/g, '')
|
||||
.match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);
|
||||
|
||||
const groups = client_identity?.groups;
|
||||
const groups = client_identity?.groups as OAuthClientIdentity | null;
|
||||
|
||||
if (!groups)
|
||||
throw new OAuthError('Could not obtain client identity.', { status: 'FAILED' });
|
||||
@@ -264,6 +293,4 @@ class OAuth {
|
||||
Reflect.has(this.#credentials, 'refresh_token') &&
|
||||
Reflect.has(this.#credentials, 'expires') || false;
|
||||
}
|
||||
}
|
||||
|
||||
export default OAuth;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.ts';
|
||||
|
||||
import Constants from '../utils/Constants.ts';
|
||||
import * as 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;
|
||||
@@ -63,7 +66,7 @@ export default class Player {
|
||||
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
|
||||
}
|
||||
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string): string {
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string, this_response_nsig_cache?: Map<string, string>): string {
|
||||
url = url || signature_cipher || cipher;
|
||||
|
||||
if (!url)
|
||||
@@ -90,20 +93,51 @@ export default class Player {
|
||||
const n = url_components.searchParams.get('n');
|
||||
|
||||
if (n) {
|
||||
const nsig = Platform.shim.eval(this.#nsig_sc, {
|
||||
nsig: n
|
||||
});
|
||||
let nsig;
|
||||
|
||||
if (typeof nsig !== 'string')
|
||||
throw new PlayerError('Failed to decipher nsig');
|
||||
if (this_response_nsig_cache && this_response_nsig_cache.has(n)) {
|
||||
nsig = this_response_nsig_cache.get(n) as string;
|
||||
} else {
|
||||
nsig = Platform.shim.eval(this.#nsig_sc, {
|
||||
nsig: n
|
||||
});
|
||||
|
||||
if (nsig.startsWith('enhanced_except_')) {
|
||||
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
|
||||
if (typeof nsig !== 'string')
|
||||
throw new PlayerError('Failed to decipher nsig');
|
||||
|
||||
if (nsig.startsWith('enhanced_except_')) {
|
||||
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
|
||||
} else if (this_response_nsig_cache) {
|
||||
this_response_nsig_cache.set(n, nsig);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import Constants, { CLIENTS } from '../utils/Constants.ts';
|
||||
import * as Constants from '../utils/Constants.ts';
|
||||
import EventEmitterLike from '../utils/EventEmitterLike.ts';
|
||||
import Actions from './Actions.ts';
|
||||
import Player from './Player.ts';
|
||||
|
||||
import Proto from '../proto/index.ts';
|
||||
import { ICache } from '../types/Cache.ts';
|
||||
import { FetchFunction } from '../types/PlatformShim.ts';
|
||||
import * as Proto from '../proto/index.ts';
|
||||
import type { ICache } from '../types/Cache.ts';
|
||||
import type { FetchFunction } from '../types/PlatformShim.ts';
|
||||
import HTTPClient from '../utils/HTTPClient.ts';
|
||||
import { 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 { generateRandomString, 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',
|
||||
KIDS = 'WEB_KIDS',
|
||||
MUSIC = 'WEB_REMIX',
|
||||
IOS = 'iOS',
|
||||
ANDROID = 'ANDROID',
|
||||
ANDROID_MUSIC = 'ANDROID_MUSIC',
|
||||
ANDROID_CREATOR = 'ANDROID_CREATOR',
|
||||
@@ -30,7 +33,6 @@ export interface Context {
|
||||
screenPixelDensity: number;
|
||||
screenWidthPoints: number;
|
||||
visitorData: string;
|
||||
userAgent: string;
|
||||
clientName: string;
|
||||
clientVersion: string;
|
||||
clientScreen?: string,
|
||||
@@ -41,6 +43,7 @@ export interface Context {
|
||||
clientFormFactor: string;
|
||||
userInterfaceTheme: string;
|
||||
timeZone: string;
|
||||
userAgent?: string;
|
||||
browserName?: string;
|
||||
browserVersion?: string;
|
||||
originalUrl: string;
|
||||
@@ -60,13 +63,11 @@ export interface Context {
|
||||
user: {
|
||||
enableSafetyMode: boolean;
|
||||
lockedSafetyMode: boolean;
|
||||
onBehalfOfUser?: string;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
};
|
||||
request: {
|
||||
useSsl: true;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
@@ -84,6 +85,10 @@ export interface SessionOptions {
|
||||
* Only works if you are signed in with cookies.
|
||||
*/
|
||||
account_index?: number;
|
||||
/**
|
||||
* Specify the Page ID of the YouTube profile/channel to use, if the logged-in account has multiple profiles.
|
||||
*/
|
||||
on_behalf_of_user?: string;
|
||||
/**
|
||||
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
|
||||
* **NOTE:** Deciphering formats is not possible without the JS player.
|
||||
@@ -135,6 +140,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;
|
||||
@@ -190,7 +198,8 @@ export default class Session extends EventEmitterLike {
|
||||
options.device_category,
|
||||
options.client_type,
|
||||
options.timezone,
|
||||
options.fetch
|
||||
options.fetch,
|
||||
options.on_behalf_of_user
|
||||
);
|
||||
|
||||
return new Session(
|
||||
@@ -210,14 +219,22 @@ export default class Session extends EventEmitterLike {
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
client_name: ClientType = ClientType.WEB,
|
||||
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
fetch: FetchFunction = Platform.shim.fetch
|
||||
fetch: FetchFunction = Platform.shim.fetch,
|
||||
on_behalf_of_user?: string
|
||||
) {
|
||||
let session_data: SessionData;
|
||||
|
||||
const session_args = { lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data, on_behalf_of_user };
|
||||
|
||||
if (generate_session_locally) {
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
|
||||
session_data = this.#generateSessionData(session_args);
|
||||
} else {
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
|
||||
try {
|
||||
// This can fail if the data changes or the request is blocked for some reason.
|
||||
session_data = await this.#retrieveSessionData(session_args, fetch);
|
||||
} catch (err) {
|
||||
session_data = this.#generateSessionData(session_args);
|
||||
}
|
||||
}
|
||||
|
||||
return { ...session_data, account_index };
|
||||
@@ -231,10 +248,11 @@ export default class Session extends EventEmitterLike {
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
on_behalf_of_user?: string;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
let visitor_id = generateRandomString(11);
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
@@ -273,7 +291,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 +304,12 @@ 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
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
@@ -309,8 +324,9 @@ export default class Session extends EventEmitterLike {
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
on_behalf_of_user?: string;
|
||||
}): SessionData {
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
let visitor_id = generateRandomString(11);
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
@@ -326,9 +342,8 @@ 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,
|
||||
clientVersion: Constants.CLIENTS.WEB.VERSION,
|
||||
osName: 'Windows',
|
||||
osVersion: '10.0',
|
||||
platform: options.device_category.toUpperCase(),
|
||||
@@ -338,18 +353,16 @@ 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
|
||||
lockedSafetyMode: false,
|
||||
onBehalfOfUser: options.on_behalf_of_user
|
||||
}
|
||||
};
|
||||
|
||||
return { context, api_key: CLIENTS.WEB.API_KEY, api_version: CLIENTS.WEB.API_VERSION };
|
||||
return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION };
|
||||
}
|
||||
|
||||
async signIn(credentials?: Credentials): Promise<void> {
|
||||
|
||||
123
deno/src/core/clients/Kids.ts
Normal file
123
deno/src/core/clients/Kids.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Parser } from '../../parser/index.ts';
|
||||
import Channel from '../../parser/ytkids/Channel.ts';
|
||||
import HomeFeed from '../../parser/ytkids/HomeFeed.ts';
|
||||
import Search from '../../parser/ytkids/Search.ts';
|
||||
import VideoInfo from '../../parser/ytkids/VideoInfo.ts';
|
||||
import type Session from '../Session.ts';
|
||||
import { type ApiResponse } from '../Actions.ts';
|
||||
|
||||
import { InnertubeError, generateRandomString } from '../../utils/Utils.ts';
|
||||
|
||||
import {
|
||||
BrowseEndpoint, NextEndpoint,
|
||||
PlayerEndpoint, SearchEndpoint
|
||||
} from '../endpoints/index.ts';
|
||||
|
||||
import { BlocklistPickerEndpoint } from '../endpoints/kids/index.ts';
|
||||
|
||||
import KidsBlocklistPickerItem from '../../parser/classes/ytkids/KidsBlocklistPickerItem.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<Search> {
|
||||
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<VideoInfo> {
|
||||
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<Channel> {
|
||||
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<HomeFeed> {
|
||||
const response = await this.#session.actions.execute(
|
||||
BrowseEndpoint.PATH, BrowseEndpoint.build({
|
||||
browse_id: 'FEkids_home',
|
||||
client: 'YTKIDS'
|
||||
})
|
||||
);
|
||||
return new HomeFeed(this.#session.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of supervised accounts that the signed-in user has
|
||||
* access to, and blocks the given channel for each of them.
|
||||
* @param channel_id - The channel id to block.
|
||||
* @returns A list of API responses.
|
||||
*/
|
||||
async blockChannel(channel_id: string): Promise<ApiResponse[]> {
|
||||
if (!this.#session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const blocklist_payload = BlocklistPickerEndpoint.build({ channel_id: channel_id });
|
||||
const response = await this.#session.actions.execute(BlocklistPickerEndpoint.PATH, blocklist_payload );
|
||||
const popup = response.data.command.confirmDialogEndpoint;
|
||||
const popup_fragment = { contents: popup.content, engagementPanels: [] };
|
||||
const kid_picker = Parser.parseResponse(popup_fragment);
|
||||
const kids = kid_picker.contents_memo?.getType(KidsBlocklistPickerItem);
|
||||
|
||||
if (!kids)
|
||||
throw new InnertubeError('Could not find any kids profiles or supervised accounts.');
|
||||
|
||||
// Iterate through the kids and block the channel if not already blocked.
|
||||
const responses: ApiResponse[] = [];
|
||||
|
||||
for (const kid of kids) {
|
||||
if (!kid.block_button?.is_toggled) {
|
||||
kid.setActions(this.#session.actions);
|
||||
// Block channel and add to the response list.
|
||||
responses.push(await kid.blockChannel());
|
||||
}
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
}
|
||||
@@ -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 * as 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 } 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<TrackInfo> {
|
||||
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<Search> {
|
||||
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<HomeFeed> {
|
||||
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<Explore> {
|
||||
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<Library> {
|
||||
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<PlaylistPanel> {
|
||||
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<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
|
||||
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<MusicDescriptionShelf | undefined> {
|
||||
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');
|
||||
|
||||
@@ -326,8 +329,8 @@ class Music {
|
||||
if (!page.contents)
|
||||
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);
|
||||
if (page.contents.item().type === 'Message')
|
||||
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<Recap> {
|
||||
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);
|
||||
}
|
||||
@@ -350,20 +355,17 @@ class Music {
|
||||
* Retrieves search suggestions for the given query.
|
||||
* @param query - The query.
|
||||
*/
|
||||
async getSearchSuggestions(query: string): Promise<ObservedArray<YTNode>> {
|
||||
const response = await this.#actions.execute('/music/get_search_suggestions', {
|
||||
parse: true,
|
||||
input: query,
|
||||
client: 'YTMUSIC'
|
||||
});
|
||||
async getSearchSuggestions(query: string): Promise<ObservedArray<SearchSuggestionsSection>> {
|
||||
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)
|
||||
return [] as unknown as ObservedArray<SearchSuggestionsSection>;
|
||||
|
||||
if (!search_suggestions_section?.contents.is_array)
|
||||
return observe([] as YTNode[]);
|
||||
const search_suggestions_sections = response.contents_memo.getType(SearchSuggestionsSection);
|
||||
|
||||
return search_suggestions_section?.contents.array();
|
||||
return search_suggestions_sections;
|
||||
}
|
||||
}
|
||||
|
||||
export default Music;
|
||||
}
|
||||
@@ -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 * as Proto from '../../proto/index.ts';
|
||||
import * as Constants from '../../utils/Constants.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<ApiResponse> {
|
||||
async updateVideoMetadata(video_id: string, metadata: UpdateVideoMetadataOptions): Promise<ApiResponse> {
|
||||
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<ApiResponse> {
|
||||
async upload(file: BodyInit, metadata: UploadedVideoMetadataOptions = {}): Promise<ApiResponse> {
|
||||
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;
|
||||
}
|
||||
3
deno/src/core/clients/index.ts
Normal file
3
deno/src/core/clients/index.ts
Normal file
@@ -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';
|
||||
19
deno/src/core/endpoints/BrowseEndpoint.ts
Normal file
19
deno/src/core/endpoints/BrowseEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
16
deno/src/core/endpoints/GetNotificationMenuEndpoint.ts
Normal file
16
deno/src/core/endpoints/GetNotificationMenuEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
1
deno/src/core/endpoints/GuideEndpoint.ts
Normal file
1
deno/src/core/endpoints/GuideEndpoint.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PATH = '/guide';
|
||||
21
deno/src/core/endpoints/NextEndpoint.ts
Normal file
21
deno/src/core/endpoints/NextEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
49
deno/src/core/endpoints/PlayerEndpoint.ts
Normal file
49
deno/src/core/endpoints/PlayerEndpoint.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
attestationRequest: {
|
||||
omitBotguardData: true
|
||||
},
|
||||
racyCheckOk: true,
|
||||
contentCheckOk: true,
|
||||
videoId: opts.video_id,
|
||||
...{
|
||||
client: opts.client,
|
||||
playlistId: opts.playlist_id,
|
||||
// Workaround streaming URLs returning 403 or getting throttled when using Android based clients.
|
||||
params: is_android ? '2AMBCgIQBg' : opts.params
|
||||
}
|
||||
};
|
||||
}
|
||||
16
deno/src/core/endpoints/ResolveURLEndpoint.ts
Normal file
16
deno/src/core/endpoints/ResolveURLEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
19
deno/src/core/endpoints/SearchEndpoint.ts
Normal file
19
deno/src/core/endpoints/SearchEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
13
deno/src/core/endpoints/account/AccountListEndpoint.ts
Normal file
13
deno/src/core/endpoints/account/AccountListEndpoint.ts
Normal file
@@ -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'
|
||||
};
|
||||
}
|
||||
1
deno/src/core/endpoints/account/index.ts
Normal file
1
deno/src/core/endpoints/account/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as AccountListEndpoint from './AccountListEndpoint.ts';
|
||||
24
deno/src/core/endpoints/browse/EditPlaylistEndpoint.ts
Normal file
24
deno/src/core/endpoints/browse/EditPlaylistEndpoint.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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,
|
||||
playlistDescription: action.playlist_description,
|
||||
playlistName: action.playlist_name
|
||||
}
|
||||
}))
|
||||
};
|
||||
}
|
||||
1
deno/src/core/endpoints/browse/index.ts
Normal file
1
deno/src/core/endpoints/browse/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as EditPlaylistEndpoint from './EditPlaylistEndpoint.ts';
|
||||
15
deno/src/core/endpoints/channel/EditDescriptionEndpoint.ts
Normal file
15
deno/src/core/endpoints/channel/EditDescriptionEndpoint.ts
Normal file
@@ -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'
|
||||
};
|
||||
}
|
||||
15
deno/src/core/endpoints/channel/EditNameEndpoint.ts
Normal file
15
deno/src/core/endpoints/channel/EditNameEndpoint.ts
Normal file
@@ -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'
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/channel/index.ts
Normal file
2
deno/src/core/endpoints/channel/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as EditNameEndpoint from './EditNameEndpoint.ts';
|
||||
export * as EditDescriptionEndpoint from './EditDescriptionEndpoint.ts';
|
||||
18
deno/src/core/endpoints/comment/CreateCommentEndpoint.ts
Normal file
18
deno/src/core/endpoints/comment/CreateCommentEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/comment/index.ts
Normal file
2
deno/src/core/endpoints/comment/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as PerformCommentActionEndpoint from './PerformCommentActionEndpoint.ts';
|
||||
export * as CreateCommentEndpoint from './CreateCommentEndpoint.ts';
|
||||
20
deno/src/core/endpoints/index.ts
Normal file
20
deno/src/core/endpoints/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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 Reel from './reel/index.ts';
|
||||
export * as Upload from './upload/index.ts';
|
||||
export * as Kids from './kids/index.ts';
|
||||
12
deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Normal file
12
deno/src/core/endpoints/kids/BlocklistPickerEndpoint.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { IBlocklistPickerRequest, BlocklistPickerRequestEndpointOptions } from '../../../types/index.ts';
|
||||
|
||||
export const PATH = '/kids/get_kids_blocklist_picker';
|
||||
|
||||
/**
|
||||
* Builds a `/kids/get_kids_blocklist_picker` request payload.
|
||||
* @param options - The options to use.
|
||||
* @returns The payload.
|
||||
*/
|
||||
export function build(options: BlocklistPickerRequestEndpointOptions): IBlocklistPickerRequest {
|
||||
return { blockedForKidsContent: { external_channel_id: options.channel_id } };
|
||||
}
|
||||
1
deno/src/core/endpoints/kids/index.ts
Normal file
1
deno/src/core/endpoints/kids/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as BlocklistPickerEndpoint from './BlocklistPickerEndpoint.ts';
|
||||
19
deno/src/core/endpoints/like/DislikeEndpoint.ts
Normal file
19
deno/src/core/endpoints/like/DislikeEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
19
deno/src/core/endpoints/like/LikeEndpoint.ts
Normal file
19
deno/src/core/endpoints/like/LikeEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
19
deno/src/core/endpoints/like/RemoveLikeEndpoint.ts
Normal file
19
deno/src/core/endpoints/like/RemoveLikeEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
3
deno/src/core/endpoints/like/index.ts
Normal file
3
deno/src/core/endpoints/like/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * as LikeEndpoint from './LikeEndpoint.ts';
|
||||
export * as DislikeEndpoint from './DislikeEndpoint.ts';
|
||||
export * as RemoveLikeEndpoint from './RemoveLikeEndpoint.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'
|
||||
};
|
||||
}
|
||||
1
deno/src/core/endpoints/music/index.ts
Normal file
1
deno/src/core/endpoints/music/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as GetSearchSuggestionsEndpoint from './GetSearchSuggestionsEndpoint.ts';
|
||||
@@ -0,0 +1 @@
|
||||
export const PATH = '/notification/get_unseen_count';
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/notification/index.ts
Normal file
2
deno/src/core/endpoints/notification/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as GetUnseenCountEndpoint from './GetUnseenCountEndpoint.ts';
|
||||
export * as ModifyChannelPreferenceEndpoint from './ModifyChannelPreferenceEndpoint.ts';
|
||||
15
deno/src/core/endpoints/playlist/CreateEndpoint.ts
Normal file
15
deno/src/core/endpoints/playlist/CreateEndpoint.ts
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
14
deno/src/core/endpoints/playlist/DeleteEndpoint.ts
Normal file
14
deno/src/core/endpoints/playlist/DeleteEndpoint.ts
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/playlist/index.ts
Normal file
2
deno/src/core/endpoints/playlist/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as CreateEndpoint from './CreateEndpoint.ts';
|
||||
export * as DeleteEndpoint from './DeleteEndpoint.ts';
|
||||
18
deno/src/core/endpoints/reel/WatchEndpoint.ts
Normal file
18
deno/src/core/endpoints/reel/WatchEndpoint.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IReelWatchRequest, ReelWatchEndpointOptions } from '../../../types/index.ts';
|
||||
|
||||
export const PATH = '/reel/reel_item_watch';
|
||||
|
||||
/**
|
||||
* Builds a `/reel/reel_watch_sequence` request payload.
|
||||
* @param opts - The options to use.
|
||||
* @returns The payload.
|
||||
*/
|
||||
export function build(opts: ReelWatchEndpointOptions): IReelWatchRequest {
|
||||
return {
|
||||
playerRequest: {
|
||||
videoId: opts.short_id,
|
||||
params: opts.params ?? 'CAUwAg%3D%3D'
|
||||
},
|
||||
params: opts.params ?? 'CAUwAg%3D%3D'
|
||||
};
|
||||
}
|
||||
14
deno/src/core/endpoints/reel/WatchSequenceEndpoint.ts
Normal file
14
deno/src/core/endpoints/reel/WatchSequenceEndpoint.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IReelSequenceRequest, ReelWatchSequenceEndpointOptions } from '../../../types/index.ts';
|
||||
|
||||
export const PATH = '/reel/reel_watch_sequence';
|
||||
|
||||
/**
|
||||
* Builds a `/reel/reel_watch_sequence` request payload.
|
||||
* @param opts - The options to use.
|
||||
* @returns The payload.
|
||||
*/
|
||||
export function build(opts: ReelWatchSequenceEndpointOptions): IReelSequenceRequest {
|
||||
return {
|
||||
sequenceParams: opts.sequenceParams
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/reel/index.ts
Normal file
2
deno/src/core/endpoints/reel/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as WatchEndpoint from './WatchEndpoint.ts';
|
||||
export * as WatchSequenceEndpoint from './WatchSequenceEndpoint.ts';
|
||||
18
deno/src/core/endpoints/subscription/SubscribeEndpoint.ts
Normal file
18
deno/src/core/endpoints/subscription/SubscribeEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
18
deno/src/core/endpoints/subscription/UnsubscribeEndpoint.ts
Normal file
18
deno/src/core/endpoints/subscription/UnsubscribeEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
2
deno/src/core/endpoints/subscription/index.ts
Normal file
2
deno/src/core/endpoints/subscription/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as SubscribeEndpoint from './SubscribeEndpoint.ts';
|
||||
export * as UnsubscribeEndpoint from './UnsubscribeEndpoint.ts';
|
||||
37
deno/src/core/endpoints/upload/CreateVideoEndpoint.ts
Normal file
37
deno/src/core/endpoints/upload/CreateVideoEndpoint.ts
Normal file
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
1
deno/src/core/endpoints/upload/index.ts
Normal file
1
deno/src/core/endpoints/upload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as CreateVideoEndpoint from './CreateVideoEndpoint.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';
|
||||
@@ -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 * as 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<TimeWatched> {
|
||||
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<Settings> {
|
||||
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<Analytics> {
|
||||
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;
|
||||
}
|
||||
@@ -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 * as 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<ApiResponse>{
|
||||
async unsubscribe(channel_id: string): Promise<ApiResponse> {
|
||||
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;
|
||||
}
|
||||
@@ -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<void> => {
|
||||
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,73 @@ 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;
|
||||
/**
|
||||
* Sets the name (title) for the given playlist.
|
||||
* @param playlist_id - The playlist ID.
|
||||
* @param name - The name / title to use for the playlist.
|
||||
*/
|
||||
async setName(playlist_id: string, name: string): Promise<{ playlist_id: string; action_result: any; }> {
|
||||
throwIfMissing({ playlist_id, name });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
|
||||
|
||||
payload.actions.push({
|
||||
action: 'ACTION_SET_PLAYLIST_NAME',
|
||||
playlist_name: name
|
||||
});
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
|
||||
);
|
||||
|
||||
return {
|
||||
playlist_id,
|
||||
action_result: response.data.actions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description for the given playlist.
|
||||
* @param playlist_id - The playlist ID.
|
||||
* @param description - The description to use for the playlist.
|
||||
*/
|
||||
async setDescription(playlist_id: string, description: string): Promise<{ playlist_id: string; action_result: any; }> {
|
||||
throwIfMissing({ playlist_id, description });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
const payload: EditPlaylistEndpointOptions = { playlist_id, actions: [] };
|
||||
|
||||
payload.actions.push({
|
||||
action: 'ACTION_SET_PLAYLIST_DESCRIPTION',
|
||||
playlist_description: description
|
||||
});
|
||||
|
||||
const response = await this.#actions.execute(
|
||||
EditPlaylistEndpoint.PATH, EditPlaylistEndpoint.build(payload)
|
||||
);
|
||||
|
||||
return {
|
||||
playlist_id,
|
||||
action_result: response.data.actions
|
||||
};
|
||||
}
|
||||
}
|
||||
3
deno/src/core/managers/index.ts
Normal file
3
deno/src/core/managers/index.ts
Normal file
@@ -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';
|
||||
@@ -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<T extends IParsedResponse = IParsedResponse> {
|
||||
export default class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
#page: T;
|
||||
#continuation?: ObservedArray<ContinuationItem>;
|
||||
#actions: Actions;
|
||||
@@ -177,7 +177,7 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
* Checks if the feed has continuation.
|
||||
*/
|
||||
get has_continuation(): boolean {
|
||||
return (this.#memo.get('ContinuationItem') || []).length > 0;
|
||||
return this.#getBodyContinuations().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,17 +185,15 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
*/
|
||||
async getContinuationData(): Promise<T | undefined> {
|
||||
if (this.#continuation) {
|
||||
if (this.#continuation.length > 1)
|
||||
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
|
||||
if (this.#continuation.length === 0)
|
||||
throw new InnertubeError('There are no continuations');
|
||||
throw new InnertubeError('There are no continuations.');
|
||||
|
||||
const response = await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
this.#continuation = this.#memo.getType(ContinuationItem);
|
||||
this.#continuation = this.#getBodyContinuations();
|
||||
|
||||
if (this.#continuation)
|
||||
return this.getContinuationData();
|
||||
@@ -210,6 +208,14 @@ class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new Feed<T>(this.actions, continuation_data, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default Feed;
|
||||
#getBodyContinuations(): ObservedArray<ContinuationItem> {
|
||||
if (this.#page.header_memo) {
|
||||
const header_continuations = this.#page.header_memo.getType(ContinuationItem);
|
||||
|
||||
return this.#memo.getType(ContinuationItem).filter((continuation) => !header_continuations.includes(continuation)) as ObservedArray<ContinuationItem>;
|
||||
}
|
||||
|
||||
return this.#memo.getType(ContinuationItem);
|
||||
}
|
||||
}
|
||||
@@ -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<T extends IParsedResponse> extends Feed<T> {
|
||||
export default class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#chips?: ObservedArray<ChipCloudChip>;
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | T, already_parsed = false) {
|
||||
@@ -69,6 +69,4 @@ class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
|
||||
return new Feed(this.actions, response, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterableFeed;
|
||||
}
|
||||
177
deno/src/core/mixins/MediaInfo.ts
Normal file
177
deno/src/core/mixins/MediaInfo.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { ApiResponse } from '../Actions.ts';
|
||||
import type Actions from '../Actions.ts';
|
||||
import * as Constants from '../../utils/Constants.ts';
|
||||
import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../types/FormatUtils.ts';
|
||||
import * as FormatUtils from '../../utils/FormatUtils.ts';
|
||||
import { InnertubeError } from '../../utils/Utils.ts';
|
||||
import type Format from '../../parser/classes/misc/Format.ts';
|
||||
import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.ts';
|
||||
import { Parser } from '../../parser/index.ts';
|
||||
import type { DashOptions } from '../../types/DashOptions.ts';
|
||||
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.ts';
|
||||
import { getStreamingInfo } from '../../utils/StreamingInfo.ts';
|
||||
import ContinuationItem from '../../parser/classes/ContinuationItem.ts';
|
||||
import TranscriptInfo from '../../parser/youtube/TranscriptInfo.ts';
|
||||
|
||||
export default class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
#actions: Actions;
|
||||
#cpn: string;
|
||||
#playback_tracking;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
player_config: IPlayerConfig;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
this.#cpn = cpn;
|
||||
|
||||
if (info.playability_status?.status === 'ERROR')
|
||||
throw new InnertubeError('This video is unavailable', info.playability_status);
|
||||
|
||||
this.streaming_data = info.streaming_data;
|
||||
this.playability_status = info.playability_status;
|
||||
this.player_config = info.player_config;
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a DASH manifest from the streaming data.
|
||||
* @param url_transformer - Function to transform the URLs.
|
||||
* @param format_filter - Function to filter the formats.
|
||||
* @param options - Additional options to customise the manifest generation
|
||||
* @returns DASH manifest
|
||||
*/
|
||||
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
|
||||
const player_response = this.#page[0];
|
||||
|
||||
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
|
||||
throw new InnertubeError('Generating DASH manifests for live and Post-Live-DVR videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
|
||||
}
|
||||
|
||||
let storyboards;
|
||||
|
||||
if (options.include_thumbnails && player_response.storyboards?.is(PlayerStoryboardSpec)) {
|
||||
storyboards = player_response.storyboards;
|
||||
}
|
||||
|
||||
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cleaned up representation of the adaptive_formats
|
||||
*/
|
||||
getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter) {
|
||||
return getStreamingInfo(
|
||||
this.streaming_data,
|
||||
url_transformer,
|
||||
format_filter,
|
||||
this.cpn,
|
||||
this.#actions.session.player,
|
||||
this.#actions,
|
||||
this.#page[0].storyboards?.is(PlayerStoryboardSpec) ? this.#page[0].storyboards : undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the format that best matches the given options.
|
||||
* @param options - Options
|
||||
*/
|
||||
chooseFormat(options: FormatOptions): Format {
|
||||
return FormatUtils.chooseFormat(options, this.streaming_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the video.
|
||||
* @param options - Download options.
|
||||
*/
|
||||
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
||||
const player_response = this.#page[0];
|
||||
|
||||
if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
|
||||
throw new InnertubeError('Downloading is not supported for live and Post-Live-DVR videos, as they are split up into 5 second segments that are individual files, which require using a tool such as ffmpeg to stitch them together, so they cannot be returned in a single stream.');
|
||||
}
|
||||
|
||||
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the video's transcript.
|
||||
* @param video_id - The video id.
|
||||
*/
|
||||
async getTranscript(): Promise<TranscriptInfo> {
|
||||
const next_response = this.page[1];
|
||||
|
||||
if (!next_response)
|
||||
throw new InnertubeError('Cannot get transcript from basic video info.');
|
||||
|
||||
if (!next_response.engagement_panels)
|
||||
throw new InnertubeError('Engagement panels not found. Video likely has no transcript.');
|
||||
|
||||
const transcript_panel = next_response.engagement_panels.get({
|
||||
panel_identifier: 'engagement-panel-searchable-transcript'
|
||||
});
|
||||
|
||||
if (!transcript_panel)
|
||||
throw new InnertubeError('Transcript panel not found. Video likely has no transcript.');
|
||||
|
||||
const transcript_continuation = transcript_panel.content?.as(ContinuationItem);
|
||||
|
||||
if (!transcript_continuation)
|
||||
throw new InnertubeError('Transcript continuation not found.');
|
||||
|
||||
const response = await transcript_continuation.endpoint.call(this.actions);
|
||||
|
||||
return new TranscriptInfo(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds video to the watch history.
|
||||
*/
|
||||
async addToWatchHistory(client_name = Constants.CLIENTS.WEB.NAME, client_version = Constants.CLIENTS.WEB.VERSION, replacement = 'https://www.'): Promise<Response> {
|
||||
if (!this.#playback_tracking)
|
||||
throw new InnertubeError('Playback tracking not available');
|
||||
|
||||
const url_params = {
|
||||
cpn: this.#cpn,
|
||||
fmt: 251,
|
||||
rtn: 0,
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', replacement);
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name,
|
||||
client_version
|
||||
}, url_params);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions instance.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Playback Nonce.
|
||||
*/
|
||||
get cpn(): string {
|
||||
return this.#cpn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original parsed InnerTube response.
|
||||
*/
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -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<T extends IParsedResponse> extends Feed<T> {
|
||||
export default class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#tabs?: ObservedArray<Tab>;
|
||||
#actions: Actions;
|
||||
|
||||
@@ -56,6 +56,4 @@ class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
get title(): string | undefined {
|
||||
return this.page.contents_memo?.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export default TabbedFeed;
|
||||
}
|
||||
4
deno/src/core/mixins/index.ts
Normal file
4
deno/src/core/mixins/index.ts
Normal file
@@ -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';
|
||||
@@ -310,7 +310,7 @@ const example_data = {
|
||||
|
||||
// The first argument is the name of the class, the second is the data you have for the node.
|
||||
// It will return a class that extends YTNode.
|
||||
const Example = Generator.YTNodeGenerator.generateRuntimeClass('Example', example_data);
|
||||
const Example = Generator.generateRuntimeClass('Example', example_data);
|
||||
|
||||
// You may now use this class as you would any other node.
|
||||
const example = new Example(example_data);
|
||||
|
||||
18
deno/src/parser/classes/AboutChannel.ts
Normal file
18
deno/src/parser/classes/AboutChannel.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import AboutChannelView from './AboutChannelView.ts';
|
||||
import Button from './Button.ts';
|
||||
|
||||
export default class AboutChannel extends YTNode {
|
||||
static type = 'AboutChannel';
|
||||
|
||||
metadata: AboutChannelView | null;
|
||||
share_channel: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.metadata = Parser.parseItem(data.metadata, AboutChannelView);
|
||||
this.share_channel = Parser.parseItem(data.shareChannel, Button);
|
||||
}
|
||||
}
|
||||
87
deno/src/parser/classes/AboutChannelView.ts
Normal file
87
deno/src/parser/classes/AboutChannelView.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ObservedArray } from '../helpers.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import ChannelExternalLinkView from './ChannelExternalLinkView.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
export default class AboutChannelView extends YTNode {
|
||||
static type = 'AboutChannelView';
|
||||
|
||||
description?: string;
|
||||
description_label?: Text;
|
||||
country?: string;
|
||||
custom_links_label?: Text;
|
||||
subscriber_count?: string;
|
||||
view_count?: string;
|
||||
joined_date?: Text;
|
||||
canonical_channel_url?: string;
|
||||
channel_id?: string;
|
||||
additional_info_label?: Text;
|
||||
custom_url_on_tap?: NavigationEndpoint;
|
||||
video_count?: string;
|
||||
sign_in_for_business_email?: Text;
|
||||
links: ObservedArray<ChannelExternalLinkView>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
if (Reflect.has(data, 'description')) {
|
||||
this.description = data.description;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'descriptionLabel')) {
|
||||
this.description_label = Text.fromAttributed(data.descriptionLabel);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'country')) {
|
||||
this.country = data.country;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'customLinksLabel')) {
|
||||
this.custom_links_label = Text.fromAttributed(data.customLinksLabel);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'subscriberCountText')) {
|
||||
this.subscriber_count = data.subscriberCountText;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'viewCountText')) {
|
||||
this.view_count = data.viewCountText;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'joinedDateText')) {
|
||||
this.joined_date = Text.fromAttributed(data.joinedDateText);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'canonicalChannelUrl')) {
|
||||
this.canonical_channel_url = data.canonicalChannelUrl;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'channelId')) {
|
||||
this.channel_id = data.channelId;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'additionalInfoLabel')) {
|
||||
this.additional_info_label = Text.fromAttributed(data.additionalInfoLabel);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'customUrlOnTap')) {
|
||||
this.custom_url_on_tap = new NavigationEndpoint(data.customUrlOnTap);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'videoCountText')) {
|
||||
this.video_count = data.videoCountText;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'signInForBusinessEmail')) {
|
||||
this.sign_in_for_business_email = Text.fromAttributed(data.signInForBusinessEmail);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'links')) {
|
||||
this.links = Parser.parseArray(data.links, ChannelExternalLinkView);
|
||||
} else {
|
||||
this.links = [] as unknown as ObservedArray<ChannelExternalLinkView>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import Parser from '../index.ts';
|
||||
|
||||
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<AccountItem>;
|
||||
header: AccountItemSectionHeader | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = data.contents.map((ac: any) => new AccountItem(ac.accountItem));
|
||||
this.contents = observe<AccountItem>(data.contents.map((ac: RawNode) => new AccountItem(ac.accountItem)));
|
||||
this.header = Parser.parseItem(data.header, AccountItemSectionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountItemSection;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import Parser from '../index.ts';
|
||||
import { Parser } from '../index.ts';
|
||||
import AccountChannel from './AccountChannel.ts';
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
19
deno/src/parser/classes/AlertWithButton.ts
Normal file
19
deno/src/parser/classes/AlertWithButton.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Button from './Button.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
|
||||
export default class AlertWithButton extends YTNode {
|
||||
static type = 'AlertWithButton';
|
||||
|
||||
text: Text;
|
||||
alert_type: string;
|
||||
dismiss_button: Button | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.text = new Text(data.text);
|
||||
this.alert_type = data.type;
|
||||
this.dismiss_button = Parser.parseItem(data.dismissButton, Button);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
30
deno/src/parser/classes/AvatarView.ts
Normal file
30
deno/src/parser/classes/AvatarView.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { type RawNode } from '../index.ts';
|
||||
import { Thumbnail } from '../misc.ts';
|
||||
|
||||
export default class AvatarView extends YTNode {
|
||||
static type = 'AvatarView';
|
||||
|
||||
image: {
|
||||
sources: Thumbnail[],
|
||||
processor: {
|
||||
border_image_processor: {
|
||||
circular: boolean
|
||||
}
|
||||
}
|
||||
};
|
||||
avatar_image_size: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.image = {
|
||||
sources: data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width),
|
||||
processor: {
|
||||
border_image_processor: {
|
||||
circular: data.image.processor.borderImageProcessor.circular
|
||||
}
|
||||
}
|
||||
};
|
||||
this.avatar_image_size = data.avatarImageSize;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
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 Button from './Button.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 +18,13 @@ class BackstagePost extends YTNode {
|
||||
vote_status?: string;
|
||||
vote_count?: Text;
|
||||
menu?: Menu | null;
|
||||
action_buttons;
|
||||
vote_button;
|
||||
action_buttons?: CommentActionButtons | null;
|
||||
vote_button?: Button | null;
|
||||
surface: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
attachment;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.postId;
|
||||
|
||||
@@ -36,40 +36,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) {
|
||||
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
|
||||
if (Reflect.has(data, 'voteButton')) {
|
||||
this.vote_button = Parser.parseItem(data.voteButton, Button);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<YTNode>;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
}
|
||||
}
|
||||
|
||||
export default BrowseFeedActions;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,45 +1,35 @@
|
||||
import Text from './misc/Text.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
class Button extends YTNode {
|
||||
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: any) {
|
||||
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')) {
|
||||
if (Reflect.has(data, 'isDisabled'))
|
||||
this.is_disabled = data.isDisabled;
|
||||
}
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
}
|
||||
28
deno/src/parser/classes/ButtonView.ts
Normal file
28
deno/src/parser/classes/ButtonView.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
|
||||
export default class ButtonView extends YTNode {
|
||||
static type = 'ButtonView';
|
||||
|
||||
icon_name: string;
|
||||
title: string;
|
||||
accessibility_text: string;
|
||||
style: string;
|
||||
is_full_width: boolean;
|
||||
type: string;
|
||||
button_size: string;
|
||||
on_tap: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.icon_name = data.iconName;
|
||||
this.title = data.title;
|
||||
this.accessibility_text = data.accessibilityText;
|
||||
this.style = data.style;
|
||||
this.is_full_width = data.isFullWidth;
|
||||
this.type = data.type;
|
||||
this.button_size = data.buttonSize;
|
||||
this.on_tap = new NavigationEndpoint(data.onTap);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
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 ChannelHeaderLinksView from './ChannelHeaderLinksView.ts';
|
||||
import ChannelTagline from './ChannelTagline.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;
|
||||
@@ -20,57 +20,60 @@ class C4TabbedHeader extends YTNode {
|
||||
videos_count?: Text;
|
||||
sponsor_button?: Button | null;
|
||||
subscribe_button?: SubscribeButton | Button | null;
|
||||
header_links?: ChannelHeaderLinks | null;
|
||||
header_links?: ChannelHeaderLinks | ChannelHeaderLinksView | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
tagline?: ChannelTagline | null;
|
||||
|
||||
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) {
|
||||
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
|
||||
if (Reflect.has(data, 'headerLinks')) {
|
||||
this.header_links = Parser.parseItem(data.headerLinks, [ ChannelHeaderLinks, ChannelHeaderLinksView ]);
|
||||
}
|
||||
|
||||
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;
|
||||
if (Reflect.has(data, 'tagline')) {
|
||||
this.tagline = Parser.parseItem(data.tagline, ChannelTagline);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<YTNode>;
|
||||
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;
|
||||
}
|
||||
@@ -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<YTNode>;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
}
|
||||
}
|
||||
|
||||
export default CarouselHeader;
|
||||
}
|
||||
@@ -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<YTNode>;
|
||||
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;
|
||||
// XXX: For consistency.
|
||||
get contents() {
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
17
deno/src/parser/classes/CarouselLockup.ts
Normal file
17
deno/src/parser/classes/CarouselLockup.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type ObservedArray, YTNode } from '../helpers.ts';
|
||||
import InfoRow from './InfoRow.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import CompactVideo from './CompactVideo.ts';
|
||||
|
||||
export default class CarouselLockup extends YTNode {
|
||||
static type = 'CarouselLockup';
|
||||
|
||||
info_rows: ObservedArray<InfoRow>;
|
||||
video_lockup?: CompactVideo | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.info_rows = Parser.parseArray(data.infoRows, InfoRow);
|
||||
this.video_lockup = Parser.parseItem(data.videoLockup, CompactVideo);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Button>;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.id = data.channelId;
|
||||
this.name = new Text(data.title);
|
||||
@@ -43,14 +40,30 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
title: new Text(link.title)
|
||||
})) ?? [];
|
||||
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.joined = new Text(data.joinedDateText);
|
||||
this.view_count = new Text(data.viewCountText);
|
||||
this.joined_date = new Text(data.joinedDateText);
|
||||
this.description = new Text(data.description);
|
||||
this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand);
|
||||
this.can_reveal_email = !data.signInForBusinessEmail;
|
||||
this.country = new Text(data.country);
|
||||
this.buttons = Parser.parseArray(data.actionButtons, Button);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelAboutFullMetadata;
|
||||
/**
|
||||
* @deprecated
|
||||
* This will be removed in a future release.
|
||||
* Please use {@link Channel.view_count} instead.
|
||||
*/
|
||||
get views() {
|
||||
return this.view_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* This will be removed in a future release.
|
||||
* Please use {@link Channel.joined_date} instead.
|
||||
*/
|
||||
get joined(): Text {
|
||||
return this.joined_date;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import { Parser } from '../index.ts';
|
||||
import Button from './Button.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
class ChannelAgeGate extends YTNode {
|
||||
export default class ChannelAgeGate extends YTNode {
|
||||
static type = 'ChannelAgeGate';
|
||||
|
||||
channel_title: string;
|
||||
@@ -25,6 +24,4 @@ class ChannelAgeGate extends YTNode {
|
||||
this.sign_in_button = Parser.parseItem(data.signInButton, Button);
|
||||
this.secondary_text = new Text(data.secondaryText);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelAgeGate;
|
||||
}
|
||||
20
deno/src/parser/classes/ChannelExternalLinkView.ts
Normal file
20
deno/src/parser/classes/ChannelExternalLinkView.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
|
||||
export default class ChannelExternalLinkView extends YTNode {
|
||||
static type = 'ChannelExternalLinkView';
|
||||
|
||||
title: Text;
|
||||
link: Text;
|
||||
favicon: Thumbnail[];
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
this.title = Text.fromAttributed(data.title);
|
||||
this.link = Text.fromAttributed(data.link);
|
||||
this.favicon = Thumbnail.fromResponse(data.favicon);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import Parser, { RawNode } from '../index.ts';
|
||||
import { type ObservedArray, YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
class ChannelFeaturedContent extends YTNode {
|
||||
export default class ChannelFeaturedContent extends YTNode {
|
||||
static type = 'ChannelFeaturedContent';
|
||||
|
||||
title: Text;
|
||||
items;
|
||||
items: ObservedArray<YTNode>;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelFeaturedContent;
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
import { YTNode, observe, type ObservedArray } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
|
||||
class HeaderLink {
|
||||
// XXX (LuanRT): This is not a real YTNode, but we treat it as one to keep things clean.
|
||||
export class HeaderLink extends YTNode {
|
||||
static type = 'HeaderLink';
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
icon: Thumbnail[];
|
||||
title: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.icon = Thumbnail.fromResponse(data.icon);
|
||||
this.title = new Text(data.title);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelHeaderLinks extends YTNode {
|
||||
export default class ChannelHeaderLinks extends YTNode {
|
||||
static type = 'ChannelHeaderLinks';
|
||||
|
||||
primary: HeaderLink[];
|
||||
secondary: HeaderLink[];
|
||||
primary: ObservedArray<HeaderLink>;
|
||||
secondary: ObservedArray<HeaderLink>;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.primary = data.primaryLinks?.map((link: any) => new HeaderLink(link)) || [];
|
||||
this.secondary = data.secondaryLinks?.map((link: any) => new HeaderLink(link)) || [];
|
||||
this.primary = observe(data.primaryLinks?.map((link: RawNode) => new HeaderLink(link)) || []);
|
||||
this.secondary = observe(data.secondaryLinks?.map((link: RawNode) => new HeaderLink(link)) || []);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelHeaderLinks;
|
||||
}
|
||||
22
deno/src/parser/classes/ChannelHeaderLinksView.ts
Normal file
22
deno/src/parser/classes/ChannelHeaderLinksView.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
export default class ChannelHeaderLinksView extends YTNode {
|
||||
static type = 'ChannelHeaderLinksView';
|
||||
|
||||
first_link?: Text;
|
||||
more?: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
|
||||
if (Reflect.has(data, 'firstLink')) {
|
||||
this.first_link = Text.fromAttributed(data.firstLink);
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'more')) {
|
||||
this.more = Text.fromAttributed(data.more);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,41 @@
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
class ChannelMetadata extends YTNode {
|
||||
export default class ChannelMetadata extends YTNode {
|
||||
static type = 'ChannelMetadata';
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
rss_urls: any; // Array?
|
||||
rss_url: string;
|
||||
vanity_channel_url: string;
|
||||
external_id: string;
|
||||
is_family_safe: boolean;
|
||||
keywords: string[];
|
||||
avatar: Thumbnail[];
|
||||
music_artist_name?: string;
|
||||
available_countries: string[];
|
||||
android_deep_link: string;
|
||||
android_appindexing_link: string;
|
||||
ios_appindexing_link: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = data.title;
|
||||
this.description = data.description;
|
||||
this.url = data.channelUrl;
|
||||
this.rss_urls = data.rssUrl;
|
||||
this.rss_url = data.rssUrl;
|
||||
this.vanity_channel_url = data.vanityChannelUrl;
|
||||
this.external_id = data.externalId;
|
||||
this.is_family_safe = data.isFamilySafe;
|
||||
this.keywords = data.keywords;
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
// Can be an empty string sometimes, so we need the extra length check
|
||||
this.music_artist_name = typeof data.musicArtistName === 'string' && data.musicArtistName.length > 0 ? data.musicArtistName : undefined;
|
||||
this.available_countries = data.availableCountryCodes;
|
||||
this.android_deep_link = data.androidDeepLink;
|
||||
this.android_appindexing_link = data.androidAppindexingLink;
|
||||
this.ios_appindexing_link = data.iosAppindexingLink;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelMetadata;
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import Text from './misc/Text.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
class ChannelMobileHeader extends YTNode {
|
||||
export default class ChannelMobileHeader extends YTNode {
|
||||
static type = 'ChannelMobileHeader';
|
||||
|
||||
title: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelMobileHeader;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import Text from './misc/Text.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
class ChannelOptions extends YTNode {
|
||||
export default class ChannelOptions extends YTNode {
|
||||
static type = 'ChannelOptions';
|
||||
|
||||
avatar: Thumbnail[];
|
||||
@@ -12,13 +12,11 @@ class ChannelOptions extends YTNode {
|
||||
name: string;
|
||||
links: Text[];
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.endpoint = new NavigationEndpoint(data.avatarEndpoint);
|
||||
this.name = data.name;
|
||||
this.links = data.links.map((link: any) => new Text(link));
|
||||
this.links = data.links.map((link: RawNode) => new Text(link));
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelOptions;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user