mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ecd3360e0 | ||
|
|
08e9527931 | ||
|
|
a9f03a1523 | ||
|
|
c8980c7985 | ||
|
|
2e5688f235 | ||
|
|
dcf2b720a0 | ||
|
|
a90f5eb853 | ||
|
|
c6482e07b9 | ||
|
|
2de77c8f2c | ||
|
|
2aaa209906 | ||
|
|
ab028ba1ec | ||
|
|
f2f48af1bc | ||
|
|
3a7da21fd1 | ||
|
|
89794d65da | ||
|
|
91847ae3cc | ||
|
|
eb44b71939 | ||
|
|
88ebb5e2ae | ||
|
|
b237b6af4e | ||
|
|
9e618cc576 | ||
|
|
daf95cfe87 | ||
|
|
bc03c91df9 | ||
|
|
e00be25bf4 |
@@ -12,10 +12,7 @@ ___
|
||||
* [Create a PR](#changes-2)
|
||||
* [Run tests](#test)
|
||||
* [Lint your code](#lint)
|
||||
* [Build for node](#build-1)
|
||||
* [Bundle for browsers](#build-2)
|
||||
* [Compile proto file](#build-3)
|
||||
* [Build parser map](#build-4)
|
||||
* [Build](#build)
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -62,42 +59,23 @@ npm run test
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Or
|
||||
|
||||
```bash
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
<a id="build-1"></a>
|
||||
#### Build for Node
|
||||
<a id="build"></a>
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
# Node
|
||||
npm run build:node
|
||||
```
|
||||
|
||||
<a id="build-2"></a>
|
||||
#### Build for browsers
|
||||
|
||||
```bash
|
||||
# Browser
|
||||
npm run build:browser
|
||||
```
|
||||
Or:
|
||||
```bash
|
||||
npm run build:browser:prod
|
||||
```
|
||||
|
||||
<a id="build-3"></a>
|
||||
#### Compile proto file
|
||||
# Protobuf
|
||||
npm run build:proto
|
||||
|
||||
```bash
|
||||
// TODO
|
||||
```
|
||||
|
||||
<a id="build-4"></a>
|
||||
#### Build parser map
|
||||
|
||||
```bash
|
||||
# Parser map
|
||||
npm run build:parser-map
|
||||
```
|
||||
```
|
||||
@@ -40,7 +40,7 @@
|
||||
[][actions]
|
||||
[][versions]
|
||||
[][codefactor]
|
||||
[][npm]
|
||||
[][npm]
|
||||
[][say-thanks]
|
||||
|
||||
</div>
|
||||
@@ -292,7 +292,7 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID` or `YTMUSIC` |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -347,7 +347,7 @@ Suitable for cases where you only need basic video metadata. Also, it is faster
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID` or `YTMUSIC` |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC_ANDROID`, `YTMUSIC`, `TV_EMBEDDED` |
|
||||
|
||||
<a name="search"></a>
|
||||
### search(query, filters?)
|
||||
@@ -569,7 +569,7 @@ For example, you may want to call an endpoint directly, that can be achieved wit
|
||||
|
||||
const payload = {
|
||||
videoId: 'jLTOuvBTLxA',
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, WEB
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB or TV_EMBEDDED
|
||||
parse: true // tells YouTube.js to parse the response, this is not sent to InnerTube.
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ YouTube Music class.
|
||||
## API
|
||||
|
||||
* Music
|
||||
* [.getInfo(video_id)](#getinfo)
|
||||
* [.getInfo(target)](#getinfo)
|
||||
* [.search(query, filters?)](#search)
|
||||
* [.getHomeFeed()](#gethomefeed)
|
||||
* [.getExplore()](#getexplore)
|
||||
@@ -14,13 +14,13 @@ YouTube Music class.
|
||||
* [.getAlbum(album_id)](#getalbum)
|
||||
* [.getPlaylist(playlist_id)](#getplaylist)
|
||||
* [.getLyrics(video_id)](#getlyrics)
|
||||
* [.getUpNext(video_id)](#getupnext)
|
||||
* [.getUpNext(video_id, automix?)](#getupnext)
|
||||
* [.getRelated(video_id)](#getrelated)
|
||||
* [.getRecap()](#getrecap)
|
||||
* [.getSearchSuggestions(query)](#getsearchsuggestions)
|
||||
|
||||
<a name="getinfo"></a>
|
||||
### getInfo(video_id)
|
||||
### getInfo(target)
|
||||
|
||||
Retrieves track info.
|
||||
|
||||
@@ -28,7 +28,29 @@ Retrieves track info.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | Video id |
|
||||
| target | `string` or `MusicTwoRowItem` | video id or list item |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
|
||||
- `<info>#getTab(title)`
|
||||
- Retrieves contents of the given tab.
|
||||
|
||||
- `<info>#getUpNext(automix?)`
|
||||
- Retrieves up next.
|
||||
|
||||
- `<info>#getRelated()`
|
||||
- Retrieves related content.
|
||||
|
||||
- `<info>#getLyrics()`
|
||||
- Retrieves song lyrics.
|
||||
|
||||
- `<info>#available_tabs`
|
||||
- Returns available tabs.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="search"></a>
|
||||
### search(query, filters?)
|
||||
@@ -211,14 +233,14 @@ Retrieves given playlist.
|
||||
|
||||
Retrieves song lyrics.
|
||||
|
||||
**Returns:** `Promise.<{ text: string; footer: object; }>`
|
||||
**Returns:** `Promise.<MusicDescriptionShelf | undefined>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | Video id |
|
||||
|
||||
<a name="getupnext"></a>
|
||||
### getUpNext(video_id)
|
||||
### getUpNext(video_id, automix?)
|
||||
|
||||
Retrieves up next content.
|
||||
|
||||
@@ -227,6 +249,7 @@ Retrieves up next content.
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | Video id |
|
||||
| automix? | `boolean` | if automix should be fetched |
|
||||
|
||||
<a name="getrelated"></a>
|
||||
### getRelated(video_id)
|
||||
|
||||
@@ -41,7 +41,7 @@ InnerTube API key.
|
||||
**Returns:** `string`
|
||||
|
||||
<a name="api_version"></a>
|
||||
### key
|
||||
### api_version
|
||||
|
||||
InnerTube API version.
|
||||
|
||||
@@ -80,4 +80,4 @@ Player script object.
|
||||
|
||||
Client language.
|
||||
|
||||
**Returns:** `string`
|
||||
**Returns:** `string`
|
||||
|
||||
5
index.ts
5
index.ts
@@ -10,6 +10,11 @@ if (getRuntime() === 'node') {
|
||||
Reflect.set(globalThis, 'Response', undici.Response);
|
||||
Reflect.set(globalThis, 'FormData', undici.FormData);
|
||||
Reflect.set(globalThis, 'File', undici.File);
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const { ReadableStream } = require('node:stream/web');
|
||||
Reflect.set(globalThis, 'ReadableStream', ReadableStream);
|
||||
} catch { /* do nothing */ }
|
||||
}
|
||||
|
||||
import Innertube from './src/Innertube';
|
||||
|
||||
44
package-lock.json
generated
44
package-lock.json
generated
@@ -1,28 +1,26 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.2",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.2",
|
||||
"version": "2.2.1",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.7.0",
|
||||
"jintr": "^0.1.9",
|
||||
"jintr": "^0.2.0",
|
||||
"linkedom": "^0.14.12",
|
||||
"undici": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@protobuf-ts/plugin": "^2.7.0",
|
||||
"@types/flat": "^5.0.2",
|
||||
"@types/jest": "^28.1.7",
|
||||
"@types/node": "^17.0.45",
|
||||
"@types/user-agents": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"esbuild": "^0.14.49",
|
||||
@@ -1263,12 +1261,6 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
@@ -1336,12 +1328,6 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/user-agents": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/user-agents/-/user-agents-1.0.2.tgz",
|
||||
"integrity": "sha512-WOoL2UJTI6RxV8RB2kS3ZhxjjijI5G1i7mgU7mtlm4LsC1XGCfiV56h+GV4VZnAUkkkLQ4gbFGR/dggT01n0RA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
|
||||
@@ -3974,9 +3960,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jintr": {
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.1.9.tgz",
|
||||
"integrity": "sha512-0fB9LtzM/Pf7rGI8TViLM2cfLQTRWAdRjwmYWpMamHPAmX083zR2kj6cXTqtJ4v90+//r+SdZY9RSwTQsNetQw==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.2.0.tgz",
|
||||
"integrity": "sha512-b1TmzzCnVivT6ftmRFtvT/pPu/t/pCDH/GA8yzXKfOGOVG6X/pmrRU9vdWgV1I/EJTKt/i/cvx6MfmGMjlhWMA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
@@ -6294,12 +6280,6 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
@@ -6367,12 +6347,6 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/user-agents": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/user-agents/-/user-agents-1.0.2.tgz",
|
||||
"integrity": "sha512-WOoL2UJTI6RxV8RB2kS3ZhxjjijI5G1i7mgU7mtlm4LsC1XGCfiV56h+GV4VZnAUkkkLQ4gbFGR/dggT01n0RA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
|
||||
@@ -8206,9 +8180,9 @@
|
||||
}
|
||||
},
|
||||
"jintr": {
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.1.9.tgz",
|
||||
"integrity": "sha512-0fB9LtzM/Pf7rGI8TViLM2cfLQTRWAdRjwmYWpMamHPAmX083zR2kj6cXTqtJ4v90+//r+SdZY9RSwTQsNetQw==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.2.0.tgz",
|
||||
"integrity": "sha512-b1TmzzCnVivT6ftmRFtvT/pPu/t/pCDH/GA8yzXKfOGOVG6X/pmrRU9vdWgV1I/EJTKt/i/cvx6MfmGMjlhWMA==",
|
||||
"requires": {
|
||||
"acorn": "^8.8.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.2",
|
||||
"version": "2.2.1",
|
||||
"description": "Full-featured wrapper around YouTube's private API.",
|
||||
"main": "./dist/index.js",
|
||||
"browser": "./bundle/browser.js",
|
||||
@@ -39,16 +39,16 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.7.0",
|
||||
"jintr": "^0.1.9",
|
||||
"jintr": "^0.2.0",
|
||||
"linkedom": "^0.14.12",
|
||||
"undici": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"@protobuf-ts/plugin": "^2.7.0",
|
||||
"@types/jest": "^28.1.7",
|
||||
"@types/node": "^17.0.45",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"esbuild": "^0.14.49",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface SearchFilters {
|
||||
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
|
||||
}
|
||||
|
||||
export type InnerTubeClient = 'ANDROID' | 'YTMUSIC_ANDROID' | 'WEB' | 'YTMUSIC';
|
||||
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'TV_EMBEDDED';
|
||||
|
||||
class Innertube {
|
||||
session;
|
||||
|
||||
@@ -64,11 +64,11 @@ class Actions {
|
||||
/**
|
||||
* Mimmics the Axios API using Fetch's Response object.
|
||||
*/
|
||||
async #wrap(response: Response, protobuf?: boolean) {
|
||||
async #wrap(response: Response) {
|
||||
return {
|
||||
success: response.ok,
|
||||
status_code: response.status,
|
||||
data: protobuf ? await response.text() : JSON.parse(await response.text())
|
||||
data: JSON.parse(await response.text())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -618,7 +618,7 @@ class Actions {
|
||||
/**
|
||||
* Used to retrieve video info.
|
||||
*/
|
||||
async getVideoInfo(id: string, cpn?: string, client?: string) {
|
||||
async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string) {
|
||||
const data: Record<string, any> = {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
@@ -647,6 +647,10 @@ class Actions {
|
||||
data.cpn = cpn;
|
||||
}
|
||||
|
||||
if (playlist_id) {
|
||||
data.playlistId = playlist_id;
|
||||
}
|
||||
|
||||
const response = await this.#session.http.fetch('/player', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
@@ -719,6 +723,9 @@ class Actions {
|
||||
throw new InnertubeError('You are not signed in');
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'override_endpoint'))
|
||||
delete data.override_endpoint;
|
||||
|
||||
if (Reflect.has(data, 'parse'))
|
||||
delete data.parse;
|
||||
|
||||
@@ -745,11 +752,17 @@ class Actions {
|
||||
data.continuation = data.token;
|
||||
delete data.token;
|
||||
}
|
||||
|
||||
if (data?.client === 'YTMUSIC') {
|
||||
data.isAudioOnly = true;
|
||||
}
|
||||
} else {
|
||||
data = args.serialized_data;
|
||||
}
|
||||
|
||||
const response = await this.#session.http.fetch(action, {
|
||||
const endpoint = Reflect.has(args, 'override_endpoint') ? args.override_endpoint : action;
|
||||
|
||||
const response = await this.#session.http.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: args.protobuf ? data : JSON.stringify(data),
|
||||
headers: {
|
||||
@@ -763,7 +776,7 @@ class Actions {
|
||||
return Parser.parseResponse(await response.json());
|
||||
}
|
||||
|
||||
return this.#wrap(response, args.protobuf);
|
||||
return this.#wrap(response);
|
||||
}
|
||||
|
||||
#needsLogin(id: string) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Session from './Session';
|
||||
|
||||
import TrackInfo from '../parser/ytmusic/TrackInfo';
|
||||
|
||||
import Search from '../parser/ytmusic/Search';
|
||||
import HomeFeed from '../parser/ytmusic/HomeFeed';
|
||||
import Explore from '../parser/ytmusic/Explore';
|
||||
@@ -11,39 +10,94 @@ import Album from '../parser/ytmusic/Album';
|
||||
import Playlist from '../parser/ytmusic/Playlist';
|
||||
import Recap from '../parser/ytmusic/Recap';
|
||||
|
||||
import Parser from '../parser/index';
|
||||
import { observe, YTNode } from '../parser/helpers';
|
||||
|
||||
import Tab from '../parser/classes/Tab';
|
||||
import Tabbed from '../parser/classes/Tabbed';
|
||||
import SingleColumnMusicWatchNextResults from '../parser/classes/SingleColumnMusicWatchNextResults';
|
||||
import WatchNextTabbedResults from '../parser/classes/WatchNextTabbedResults';
|
||||
import SectionList from '../parser/classes/SectionList';
|
||||
|
||||
import Message from '../parser/classes/Message';
|
||||
import MusicQueue from '../parser/classes/MusicQueue';
|
||||
import PlaylistPanel from '../parser/classes/PlaylistPanel';
|
||||
import Message from '../parser/classes/Message';
|
||||
import MusicDescriptionShelf from '../parser/classes/MusicDescriptionShelf';
|
||||
import MusicCarouselShelf from '../parser/classes/MusicCarouselShelf';
|
||||
import SearchSuggestionsSection from '../parser/classes/SearchSuggestionsSection';
|
||||
import AutomixPreviewVideo from '../parser/classes/AutomixPreviewVideo';
|
||||
import MusicTwoRowItem from '../parser/classes/MusicTwoRowItem';
|
||||
|
||||
import { observe, ObservedArray, YTNode } from '../parser/helpers';
|
||||
import { InnertubeError, throwIfMissing, generateRandomString } from '../utils/Utils';
|
||||
|
||||
class Music {
|
||||
#session;
|
||||
#actions;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.#session = session;
|
||||
this.#actions = session.actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves track info.
|
||||
* Retrieves track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
|
||||
* @param target - video id or a list item.
|
||||
*/
|
||||
async getInfo(video_id: string) {
|
||||
getInfo(target: string | MusicTwoRowItem): Promise<TrackInfo> {
|
||||
if (target instanceof MusicTwoRowItem) {
|
||||
return this.#fetchInfoFromListItem(target);
|
||||
} else if (typeof target === 'string') {
|
||||
return this.#fetchInfoFromVideoId(target);
|
||||
}
|
||||
|
||||
throw new InnertubeError('Invalid target, expected either a video id or a valid MusicTwoRowItem', target);
|
||||
}
|
||||
|
||||
async #fetchInfoFromVideoId(video_id: string) {
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = await this.#actions.getVideoInfo(video_id, cpn, 'YTMUSIC');
|
||||
const continuation = this.#actions.execute('/next', { client: 'YTMUSIC', videoId: video_id });
|
||||
const initial_info = this.#actions.execute('/player', {
|
||||
cpn,
|
||||
client: 'YTMUSIC',
|
||||
videoId: video_id,
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
signatureTimestamp: this.#session.player.sts
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async #fetchInfoFromListItem(list_item: MusicTwoRowItem | undefined) {
|
||||
if (!list_item)
|
||||
throw new InnertubeError('List item cannot be undefined');
|
||||
|
||||
if (!list_item.endpoint)
|
||||
throw new Error('This item does not have an endpoint.');
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = list_item.endpoint.callTest(this.#actions, {
|
||||
cpn,
|
||||
client: 'YTMUSIC',
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
signatureTimestamp: this.#session.player.sts
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const continuation = list_item.endpoint.callTest(this.#actions, {
|
||||
client: 'YTMUSIC',
|
||||
enablePersistentPlaylistPanel: true,
|
||||
override_endpoint: '/next'
|
||||
});
|
||||
|
||||
const response = await Promise.all([ initial_info, continuation ]);
|
||||
return new TrackInfo(response, this.#actions, cpn);
|
||||
@@ -54,7 +108,7 @@ class Music {
|
||||
*/
|
||||
async search(query: string, filters: {
|
||||
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
|
||||
} = {}) {
|
||||
} = {}): Promise<Search> {
|
||||
throwIfMissing({ query });
|
||||
const response = await this.#actions.search({ query, filters, client: 'YTMUSIC' });
|
||||
return new Search(response, this.#actions, { is_filtered: Reflect.has(filters, 'type') && filters.type !== 'all' });
|
||||
@@ -63,7 +117,7 @@ class Music {
|
||||
/**
|
||||
* Retrieves the home feed.
|
||||
*/
|
||||
async getHomeFeed() {
|
||||
async getHomeFeed(): Promise<HomeFeed> {
|
||||
const response = await this.#actions.browse('FEmusic_home', { client: 'YTMUSIC' });
|
||||
return new HomeFeed(response, this.#actions);
|
||||
}
|
||||
@@ -71,7 +125,7 @@ class Music {
|
||||
/**
|
||||
* Retrieves the Explore feed.
|
||||
*/
|
||||
async getExplore() {
|
||||
async getExplore(): Promise<Explore> {
|
||||
const response = await this.#actions.browse('FEmusic_explore', { client: 'YTMUSIC' });
|
||||
return new Explore(response);
|
||||
// TODO: return new Explore(response, this.#actions);
|
||||
@@ -87,7 +141,7 @@ class Music {
|
||||
/**
|
||||
* Retrieves artist's info & content.
|
||||
*/
|
||||
async getArtist(artist_id: string) {
|
||||
async getArtist(artist_id: string): Promise<Artist> {
|
||||
throwIfMissing({ artist_id });
|
||||
|
||||
if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist'))
|
||||
@@ -100,7 +154,7 @@ class Music {
|
||||
/**
|
||||
* Retrieves album.
|
||||
*/
|
||||
async getAlbum(album_id: string) {
|
||||
async getAlbum(album_id: string): Promise<Album> {
|
||||
throwIfMissing({ album_id });
|
||||
|
||||
if (!album_id.startsWith('MPR') && !album_id.startsWith('FEmusic_library_privately_owned_release'))
|
||||
@@ -113,7 +167,7 @@ class Music {
|
||||
/**
|
||||
* Retrieves playlist.
|
||||
*/
|
||||
async getPlaylist(playlist_id: string) {
|
||||
async getPlaylist(playlist_id: string): Promise<Playlist> {
|
||||
throwIfMissing({ playlist_id });
|
||||
|
||||
if (!playlist_id.startsWith('VL')) {
|
||||
@@ -124,53 +178,17 @@ class Music {
|
||||
return new Playlist(response, this.#actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves song lyrics.
|
||||
*/
|
||||
async getLyrics(video_id: string) {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Lyrics' });
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, 'YTMUSIC', true);
|
||||
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not retrieve tab contents, the given id may be invalid or is not a song.');
|
||||
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
|
||||
|
||||
const section_list = page.contents.item().as(SectionList).contents.array();
|
||||
const description_shelf = section_list.firstOfType(MusicDescriptionShelf);
|
||||
|
||||
return {
|
||||
text: description_shelf?.description.toString(),
|
||||
footer: description_shelf?.footer
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves up next.
|
||||
*/
|
||||
async getUpNext(video_id: string) {
|
||||
async getUpNext(video_id: string, automix = true): Promise<PlaylistPanel> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
const data = await this.#actions.execute('/next', {
|
||||
videoId: video_id,
|
||||
client: 'YTMUSIC',
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
@@ -188,7 +206,25 @@ class Music {
|
||||
if (!music_queue || !music_queue.content)
|
||||
throw new InnertubeError('Music queue was empty, the given id is probably invalid.', music_queue);
|
||||
|
||||
const playlist_panel = music_queue.content.item().as(PlaylistPanel);
|
||||
const playlist_panel = music_queue.content.as(PlaylistPanel);
|
||||
|
||||
if (!playlist_panel.playlist_id && automix) {
|
||||
const automix_preview_video = playlist_panel.contents.firstOfType(AutomixPreviewVideo);
|
||||
|
||||
if (!automix_preview_video)
|
||||
throw new InnertubeError('Automix item not found');
|
||||
|
||||
const page = await automix_preview_video.playlist_video?.endpoint.callTest(this.#actions, {
|
||||
videoId: video_id,
|
||||
client: 'YTMUSIC',
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not fetch automix');
|
||||
|
||||
return page.contents_memo.getType(PlaylistPanel)?.[0];
|
||||
}
|
||||
|
||||
return playlist_panel;
|
||||
}
|
||||
@@ -196,12 +232,14 @@ class Music {
|
||||
/**
|
||||
* Retrieves related content.
|
||||
*/
|
||||
async getRelated(video_id: string) {
|
||||
async getRelated(video_id: string): Promise<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const response = await this.#actions.next({ video_id, client: 'YTMUSIC' });
|
||||
|
||||
const data = Parser.parseResponse(response.data);
|
||||
const data = await this.#actions.execute('/next', {
|
||||
videoId: video_id,
|
||||
client: 'YTMUSIC',
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
@@ -224,7 +262,45 @@ class Music {
|
||||
return shelves;
|
||||
}
|
||||
|
||||
async getRecap() {
|
||||
/**
|
||||
* Retrieves song lyrics.
|
||||
*/
|
||||
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 tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Lyrics' });
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, 'YTMUSIC', true);
|
||||
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not retrieve tab contents, the given id may be invalid or is not a song.');
|
||||
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
|
||||
|
||||
const section_list = page.contents.item().as(SectionList).contents.array();
|
||||
return section_list.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves recap.
|
||||
*/
|
||||
async getRecap(): Promise<Recap> {
|
||||
const response = await this.#actions.execute('/browse', {
|
||||
browseId: 'FEmusic_listening_review',
|
||||
client: 'YTMUSIC_ANDROID'
|
||||
|
||||
@@ -166,20 +166,21 @@ export default class Player {
|
||||
}
|
||||
|
||||
static extractSigSourceCode(data: string) {
|
||||
const funcs = getStringBetweenStrings(data, 'this.audioTracks};var', '};');
|
||||
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
|
||||
const obj_name = calls?.split('.')?.[0]?.replace(';', '');
|
||||
const functions = getStringBetweenStrings(data, `var ${obj_name}=`, '};');
|
||||
|
||||
if (!funcs || !calls)
|
||||
throw new PlayerError('Failed to extract signature decipher algorithm');
|
||||
if (!functions || !calls)
|
||||
console.warn(new PlayerError('Failed to extract signature decipher algorithm'));
|
||||
|
||||
return `function descramble_sig(a) { a = a.split(""); ${funcs}}${calls} return a.join("") } descramble_sig(sig);`;
|
||||
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}=${functions}}${calls} return a.join("") } descramble_sig(sig);`;
|
||||
}
|
||||
|
||||
static extractNSigSourceCode(data: string) {
|
||||
const sc = `function descramble_nsig(a) { let b=a.split("")${getStringBetweenStrings(data, 'b=a.split("")', '}return b.join("")}')}} return b.join(""); } descramble_nsig(nsig)`;
|
||||
|
||||
if (!sc)
|
||||
throw new PlayerError('Failed to extract n-token decipher algorithm');
|
||||
console.warn(new PlayerError('Failed to extract n-token decipher algorithm'));
|
||||
|
||||
return sc;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Context {
|
||||
userAgent: string;
|
||||
clientName: string;
|
||||
clientVersion: string;
|
||||
clientScreen?: string,
|
||||
androidSdkVersion?: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
|
||||
@@ -89,7 +89,7 @@ const is_null = response.is_null;
|
||||
## YTNode
|
||||
All renderers returned by InnerTube are converted to this generic class and then extended for the specific renderers.
|
||||
|
||||
This class is allows us a typesafe way to use data returned by the InnerTube API.
|
||||
This class is what allows us a typesafe way to use data returned by the InnerTube API.
|
||||
|
||||
Here's how to use this class to access returned data:
|
||||
|
||||
|
||||
14
src/parser/classes/AudioOnlyPlayability.ts
Normal file
14
src/parser/classes/AudioOnlyPlayability.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class AudioOnlyPlayability extends YTNode {
|
||||
static type = 'AudioOnlyPlayability';
|
||||
|
||||
audio_only_availability: string;
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.audio_only_availability = data.audioOnlyAvailability;
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioOnlyPlayability;
|
||||
18
src/parser/classes/BrowserMediaSession.ts
Normal file
18
src/parser/classes/BrowserMediaSession.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class BrowserMediaSession extends YTNode {
|
||||
static type = 'BrowserMediaSession';
|
||||
|
||||
album;
|
||||
thumbnails;
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.album = new Text(data.album);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnailDetails);
|
||||
}
|
||||
}
|
||||
|
||||
export default BrowserMediaSession;
|
||||
26
src/parser/classes/CollaboratorInfoCardContent.ts
Normal file
26
src/parser/classes/CollaboratorInfoCardContent.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class CollaboratorInfoCardContent extends YTNode {
|
||||
static type = 'CollaboratorInfoCardContent';
|
||||
|
||||
channel_avatar: Thumbnail[];
|
||||
custom_text: Text;
|
||||
channel_name: Text;
|
||||
subscriber_count: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.channel_avatar = Thumbnail.fromResponse(data.channelAvatar);
|
||||
this.custom_text = new Text(data.customText);
|
||||
this.channel_name = new Text(data.channelName);
|
||||
this.subscriber_count = new Text(data.subscriberCountText);
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollaboratorInfoCardContent;
|
||||
24
src/parser/classes/ConfirmDialog.ts
Normal file
24
src/parser/classes/ConfirmDialog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Parser from '..';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class ConfirmDialog extends YTNode {
|
||||
static type = 'ConfirmDialog';
|
||||
|
||||
title: Text;
|
||||
confirm_button: Button | null;
|
||||
cancel_button: Button | null;
|
||||
dialog_messages: Text[];
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.confirm_button = Parser.parseItem<Button>(data.confirmButton, Button);
|
||||
this.cancel_button = Parser.parseItem<Button>(data.cancelButton, Button);
|
||||
this.dialog_messages = data.dialogMessages.map((txt: any) => new Text(txt));
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
19
src/parser/classes/LiveChatDialog.ts
Normal file
19
src/parser/classes/LiveChatDialog.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Parser from '..';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class LiveChatDialog extends YTNode {
|
||||
static type = 'LiveChatDialog';
|
||||
|
||||
confirm_button: Button | null;
|
||||
dialog_messages: Text[];
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.confirm_button = Parser.parseItem<Button>(data.confirmButton, Button);
|
||||
this.dialog_messages = data.dialogMessages.map((el: any) => new Text(el));
|
||||
}
|
||||
}
|
||||
|
||||
export default LiveChatDialog;
|
||||
15
src/parser/classes/MetadataScreen.ts
Normal file
15
src/parser/classes/MetadataScreen.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MetadataScreen extends YTNode {
|
||||
static type = 'MetadataScreen';
|
||||
|
||||
section_list;
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.section_list = Parser.parseItem(data);
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataScreen;
|
||||
16
src/parser/classes/MusicDownloadStateBadge.ts
Normal file
16
src/parser/classes/MusicDownloadStateBadge.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MusicDownloadStateBadge extends YTNode {
|
||||
static type = 'MusicDownloadStateBadge';
|
||||
|
||||
playlist_id: string;
|
||||
supported_download_states: string[];
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.playlist_id = data.playlistId;
|
||||
this.supported_download_states = data.supportedDownloadStates;
|
||||
}
|
||||
}
|
||||
|
||||
export default MusicDownloadStateBadge;
|
||||
@@ -1,4 +1,5 @@
|
||||
import Parser from '../index';
|
||||
import MusicPlayButton from './MusicPlayButton';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MusicItemThumbnailOverlay extends YTNode {
|
||||
@@ -10,7 +11,7 @@ class MusicItemThumbnailOverlay extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.content = Parser.parse(data.content);
|
||||
this.content = Parser.parseItem<MusicPlayButton>(data.content, MusicPlayButton);
|
||||
this.content_position = data.contentPosition;
|
||||
this.display_style = data.displayStyle;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Parser from '../index';
|
||||
import PlaylistPanel from './PlaylistPanel';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MusicQueue extends YTNode {
|
||||
static type = 'MusicQueue';
|
||||
|
||||
content;
|
||||
content: PlaylistPanel | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.content = Parser.parse(data.content);
|
||||
this.content = Parser.parseItem<PlaylistPanel>(data.content, PlaylistPanel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
import TextRun from './misc/TextRun';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay';
|
||||
import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn';
|
||||
import MusicResponsiveListItemFixedColumn from './MusicResponsiveListItemFixedColumn';
|
||||
import Menu from './menus/Menu';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
import { YTNode } from '../helpers';
|
||||
import TextRun from './misc/TextRun';
|
||||
|
||||
class MusicResponsiveListItem extends YTNode {
|
||||
static type = 'MusicResponsiveListItem';
|
||||
@@ -66,8 +70,8 @@ class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.#flex_columns = Parser.parseArray(data.flexColumns);
|
||||
this.#fixed_columns = Parser.parseArray(data.fixedColumns);
|
||||
this.#flex_columns = Parser.parseArray<MusicResponsiveListItemFlexColumn>(data.flexColumns, MusicResponsiveListItemFlexColumn);
|
||||
this.#fixed_columns = Parser.parseArray<MusicResponsiveListItemFixedColumn>(data.fixedColumns, MusicResponsiveListItemFixedColumn);
|
||||
|
||||
this.#playlist_item_data = {
|
||||
video_id: data?.playlistItemData?.videoId || null,
|
||||
@@ -109,8 +113,8 @@ class MusicResponsiveListItem extends YTNode {
|
||||
|
||||
this.thumbnails = data.thumbnail ? Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer?.thumbnail) : [];
|
||||
this.badges = Parser.parseArray(data.badges);
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.overlay = Parser.parse(data.overlay);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
this.overlay = Parser.parseItem<MusicItemThumbnailOverlay>(data.overlay, MusicItemThumbnailOverlay);
|
||||
}
|
||||
|
||||
#parseOther() {
|
||||
|
||||
@@ -5,6 +5,9 @@ import Text from './misc/Text';
|
||||
import TextRun from './misc/TextRun';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay';
|
||||
import Menu from './menus/Menu';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MusicTwoRowItem extends YTNode {
|
||||
@@ -118,8 +121,8 @@ class MusicTwoRowItem extends YTNode {
|
||||
}
|
||||
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnailRenderer.musicThumbnailRenderer.thumbnail);
|
||||
this.thumbnail_overlay = Parser.parse(data.thumbnailOverlay);
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.thumbnail_overlay = Parser.parseItem<MusicItemThumbnailOverlay>(data.thumbnailOverlay, MusicItemThumbnailOverlay);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,10 @@ class NavigationEndpoint extends YTNode {
|
||||
switch (name) {
|
||||
case 'browseEndpoint':
|
||||
return '/browse';
|
||||
case 'watchEndpoint':
|
||||
return '/player';
|
||||
case 'watchPlaylistEndpoint':
|
||||
return '/next';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class PlayerErrorMessage extends YTNode {
|
||||
@@ -8,17 +10,17 @@ class PlayerErrorMessage extends YTNode {
|
||||
|
||||
subreason: Text;
|
||||
reason: Text;
|
||||
proceed_button;
|
||||
proceed_button: Button | null;
|
||||
thumbnails: Thumbnail[];
|
||||
icon_type: string;
|
||||
icon_type: string | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.subreason = new Text(data.subreason);
|
||||
this.reason = new Text(data.reason);
|
||||
this.proceed_button = Parser.parse(data.proceedButton);
|
||||
this.proceed_button = Parser.parseItem<Button>(data.proceedButton, Button);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.icon_type = data.icon.iconType;
|
||||
this.icon_type = data.icon?.iconType || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class PlayerOverlay extends YTNode {
|
||||
share_button;
|
||||
add_to_menu;
|
||||
fullscreen_engagement;
|
||||
actions;
|
||||
browser_media_session;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -22,6 +24,8 @@ class PlayerOverlay extends YTNode {
|
||||
this.share_button = Parser.parseItem<Button>(data.shareButton, Button);
|
||||
this.add_to_menu = Parser.parseItem<Menu>(data.addToMenu, Menu);
|
||||
this.fullscreen_engagement = Parser.parse(data.fullscreenEngagement);
|
||||
this.actions = Parser.parseArray(data.actions);
|
||||
this.browser_media_session = Parser.parseItem(data.browserMediaSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import PlaylistPanelVideo from './PlaylistPanelVideo';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import AutomixPreviewVideo from './AutomixPreviewVideo';
|
||||
import PlaylistPanelVideoWrapper from './PlaylistPanelVideoWrapper';
|
||||
|
||||
class PlaylistPanel extends YTNode {
|
||||
static type = 'PlaylistPanel';
|
||||
@@ -22,7 +23,7 @@ class PlaylistPanel extends YTNode {
|
||||
super();
|
||||
this.title = data.title;
|
||||
this.title_text = new Text(data.titleText);
|
||||
this.contents = Parser.parseArray<PlaylistPanelVideo | AutomixPreviewVideo>(data.contents, [ PlaylistPanelVideo, AutomixPreviewVideo ]);
|
||||
this.contents = Parser.parseArray<PlaylistPanelVideoWrapper | PlaylistPanelVideo | AutomixPreviewVideo>(data.contents);
|
||||
this.playlist_id = data.playlistId;
|
||||
this.is_infinite = data.isInfinite;
|
||||
this.continuation = data.continuations?.[0]?.nextRadioContinuationData?.continuation || data.continuations?.[0]?.nextContinuationData?.continuation;
|
||||
|
||||
18
src/parser/classes/PlaylistPanelVideoWrapper.ts
Normal file
18
src/parser/classes/PlaylistPanelVideoWrapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
import PlaylistPanelVideo from './PlaylistPanelVideo';
|
||||
|
||||
class PlaylistPanelVideoWrapper extends YTNode {
|
||||
static type = 'PlaylistPanelVideoWrapper';
|
||||
|
||||
primary: PlaylistPanelVideo | null;
|
||||
counterpart: Array<PlaylistPanelVideo | null>;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.primary = Parser.parseItem<PlaylistPanelVideo>(data.primaryRenderer);
|
||||
this.counterpart = data.counterpart?.map((item: any) => Parser.parseItem<PlaylistPanelVideo>(item.counterpartRenderer)) || [];
|
||||
}
|
||||
}
|
||||
|
||||
export default PlaylistPanelVideoWrapper;
|
||||
18
src/parser/classes/SegmentedLikeDislikeButton.ts
Normal file
18
src/parser/classes/SegmentedLikeDislikeButton.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import Parser from '..';
|
||||
import ToggleButton from './ToggleButton';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SegmentedLikeDislikeButton extends YTNode {
|
||||
static type = 'SegmentedLikeDislikeButton';
|
||||
|
||||
like_button: ToggleButton | null;
|
||||
dislike_button: ToggleButton | null;
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.like_button = Parser.parseItem<ToggleButton>(data.likeButton, ToggleButton);
|
||||
this.dislike_button = Parser.parseItem<ToggleButton>(data.dislikeButton, ToggleButton);
|
||||
}
|
||||
}
|
||||
|
||||
export default SegmentedLikeDislikeButton;
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import SubscriptionNotificationToggleButton from './SubscriptionNotificationToggleButton';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SubscribeButton extends YTNode {
|
||||
@@ -14,7 +15,7 @@ class SubscribeButton extends YTNode {
|
||||
show_preferences: boolean;
|
||||
subscribed_text: Text;
|
||||
unsubscribed_text: Text;
|
||||
notification_preference_button;
|
||||
notification_preference_button: SubscriptionNotificationToggleButton | null;
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -27,7 +28,7 @@ class SubscribeButton extends YTNode {
|
||||
this.show_preferences = data.showPreferences;
|
||||
this.subscribed_text = new Text(data.subscribedButtonText);
|
||||
this.unsubscribed_text = new Text(data.unsubscribedButtonText);
|
||||
this.notification_preference_button = Parser.parse(data.notificationPreferenceButton);
|
||||
this.notification_preference_button = Parser.parseItem<SubscriptionNotificationToggleButton>(data.notificationPreferenceButton, SubscriptionNotificationToggleButton);
|
||||
this.endpoint = new NavigationEndpoint(data.serviceEndpoints?.[0] || data.onSubscribeEndpoints?.[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
import VideoOwner from './VideoOwner';
|
||||
import SubscribeButton from './SubscribeButton';
|
||||
import MetadataRowContainer from './MetadataRowContainer';
|
||||
import { YTNode } from '../helpers';
|
||||
@@ -8,7 +9,7 @@ import { YTNode } from '../helpers';
|
||||
class VideoSecondaryInfo extends YTNode {
|
||||
static type = 'VideoSecondaryInfo';
|
||||
|
||||
owner; // TODO: VideoOwner?
|
||||
owner: VideoOwner | null;// TODO: VideoOwner?
|
||||
description: Text;
|
||||
subscribe_button;
|
||||
metadata: MetadataRowContainer | null;
|
||||
@@ -19,7 +20,7 @@ class VideoSecondaryInfo extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.owner = Parser.parse(data.owner);
|
||||
this.owner = Parser.parseItem<VideoOwner>(data.owner);
|
||||
this.description = new Text(data.description);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton | Button>(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.metadata = Parser.parseItem<MetadataRowContainer>(data.metadataRowContainer, MetadataRowContainer);
|
||||
|
||||
24
src/parser/classes/livechat/items/LiveChatAutoModMessage.ts
Normal file
24
src/parser/classes/livechat/items/LiveChatAutoModMessage.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Text from '../../misc/Text';
|
||||
import Parser from '../../../index';
|
||||
import { YTNode } from '../../../helpers';
|
||||
|
||||
class LiveChatAutoModMessage extends YTNode {
|
||||
static type = 'LiveChatAutoModMessage';
|
||||
|
||||
auto_moderated_item;
|
||||
header_text: Text;
|
||||
|
||||
timestamp: number;
|
||||
id: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.auto_moderated_item = Parser.parse(data.autoModeratedItem);
|
||||
this.header_text = new Text(data.headerText);
|
||||
this.timestamp = Math.floor(parseInt(data.timestampUsec) / 1000);
|
||||
this.id = data.id;
|
||||
}
|
||||
}
|
||||
|
||||
export default LiveChatAutoModMessage;
|
||||
@@ -2,6 +2,7 @@ import Text from '../../misc/Text';
|
||||
import Thumbnail from '../../misc/Thumbnail';
|
||||
import NavigationEndpoint from '../../NavigationEndpoint';
|
||||
import MetadataBadge from '../../MetadataBadge';
|
||||
import LiveChatAuthorBadge from '../../LiveChatAuthorBadge';
|
||||
import Parser from '../../../index';
|
||||
import { YTNode } from '../../../helpers';
|
||||
|
||||
@@ -14,7 +15,7 @@ class LiveChatPaidMessage extends YTNode {
|
||||
id: string;
|
||||
name: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
badges: MetadataBadge[];
|
||||
badges: LiveChatAuthorBadge[] | MetadataBadge[];
|
||||
is_moderator: boolean | null;
|
||||
is_verified: boolean | null;
|
||||
is_verified_artist: boolean | null;
|
||||
@@ -38,13 +39,13 @@ class LiveChatPaidMessage extends YTNode {
|
||||
id: data.authorExternalChannelId,
|
||||
name: new Text(data.authorName),
|
||||
thumbnails: Thumbnail.fromResponse(data.authorPhoto),
|
||||
badges: Parser.parseArray<MetadataBadge>(data.authorBadges, MetadataBadge),
|
||||
badges: Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]),
|
||||
is_moderator: null,
|
||||
is_verified: null,
|
||||
is_verified_artist: null
|
||||
};
|
||||
|
||||
const badges = Parser.parseArray<MetadataBadge>(data.authorBadges, MetadataBadge);
|
||||
const badges = Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]);
|
||||
|
||||
this.author.badges = badges;
|
||||
this.author.is_moderator = badges?.some((badge: any) => badge.icon_type == 'MODERATOR') || null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import Text from '../../misc/Text';
|
||||
import Thumbnail from '../../misc/Thumbnail';
|
||||
import NavigationEndpoint from '../../NavigationEndpoint';
|
||||
import MetadataBadge from '../../MetadataBadge';
|
||||
import LiveChatAuthorBadge from '../../LiveChatAuthorBadge';
|
||||
import Parser from '../../../index';
|
||||
|
||||
import { YTNode } from '../../../helpers';
|
||||
@@ -14,7 +15,7 @@ class LiveChatTextMessage extends YTNode {
|
||||
id: string;
|
||||
name: Text;
|
||||
thumbnails: Thumbnail[];
|
||||
badges: MetadataBadge[];
|
||||
badges: LiveChatAuthorBadge[] | MetadataBadge[];
|
||||
is_moderator: boolean | null;
|
||||
is_verified: boolean | null;
|
||||
is_verified_artist: boolean | null;
|
||||
@@ -32,13 +33,13 @@ class LiveChatTextMessage extends YTNode {
|
||||
id: data.authorExternalChannelId,
|
||||
name: new Text(data.authorName),
|
||||
thumbnails: Thumbnail.fromResponse(data.authorPhoto),
|
||||
badges: [] as MetadataBadge[],
|
||||
badges: [] as LiveChatAuthorBadge[] | [] as MetadataBadge[],
|
||||
is_moderator: null,
|
||||
is_verified: null,
|
||||
is_verified_artist: null
|
||||
};
|
||||
|
||||
const badges = Parser.parseArray<MetadataBadge>(data.authorBadges, MetadataBadge);
|
||||
const badges = Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]);
|
||||
|
||||
this.author.badges = badges;
|
||||
this.author.is_moderator = badges ? badges.some((badge) => badge.icon_type == 'MODERATOR') : null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import Text from '../../misc/Text';
|
||||
import Thumbnail from '../../misc/Thumbnail';
|
||||
import NavigationEndpoint from '../../NavigationEndpoint';
|
||||
import MetadataBadge from '../../MetadataBadge';
|
||||
import LiveChatAuthorBadge from '../../LiveChatAuthorBadge';
|
||||
import Parser from '../../../index';
|
||||
|
||||
import { YTNode } from '../../../helpers';
|
||||
@@ -12,7 +13,7 @@ class LiveChatTickerPaidMessageItem extends YTNode {
|
||||
author: {
|
||||
id: string;
|
||||
thumbnails: Thumbnail[];
|
||||
badges: MetadataBadge[];
|
||||
badges: LiveChatAuthorBadge[] | MetadataBadge[];
|
||||
is_moderator: boolean | null;
|
||||
is_verified: boolean | null;
|
||||
is_verified_artist: boolean | null;
|
||||
@@ -31,13 +32,13 @@ class LiveChatTickerPaidMessageItem extends YTNode {
|
||||
this.author = {
|
||||
id: data.authorExternalChannelId,
|
||||
thumbnails: Thumbnail.fromResponse(data.authorPhoto),
|
||||
badges: Parser.parseArray<MetadataBadge>(data.authorBadges, MetadataBadge),
|
||||
badges: Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]),
|
||||
is_moderator: null,
|
||||
is_verified: null,
|
||||
is_verified_artist: null
|
||||
};
|
||||
|
||||
const badges = Parser.parseArray<MetadataBadge>(data.authorBadges, MetadataBadge);
|
||||
const badges = Parser.parseArray<LiveChatAuthorBadge | MetadataBadge>(data.authorBadges, [ MetadataBadge, LiveChatAuthorBadge ]);
|
||||
|
||||
this.author.badges = badges;
|
||||
this.author.is_moderator = badges?.some((badge) => badge.icon_type == 'MODERATOR') || null;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { YTNode, YTNodeConstructor, SuperParsedResult, ObservedArray, observe, M
|
||||
|
||||
import package_json from '../../package.json';
|
||||
import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem';
|
||||
import AudioOnlyPlayability from './classes/AudioOnlyPlayability';
|
||||
|
||||
export class AppendContinuationItemsAction extends YTNode {
|
||||
static readonly type = 'appendContinuationItemsAction';
|
||||
@@ -258,7 +259,8 @@ export default class Parser {
|
||||
} : null,
|
||||
playability_status: data.playabilityStatus ? {
|
||||
status: data.playabilityStatus.status as string,
|
||||
error_screen: Parser.parse(data.playabilityStatus.errorScreen),
|
||||
error_screen: Parser.parseItem(data.playabilityStatus.errorScreen),
|
||||
audio_only_playablility: Parser.parseItem<AudioOnlyPlayability>(data.playabilityStatus.audioOnlyPlayability, AudioOnlyPlayability),
|
||||
embeddable: !!data.playabilityStatus.playableInEmbed || false,
|
||||
reason: data.playabilityStatus?.reason || ''
|
||||
} : undefined,
|
||||
@@ -373,10 +375,6 @@ export default class Parser {
|
||||
|
||||
static parse<T extends YTNode = YTNode>(data: any, requireArray: true, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) : ObservedArray<T> | null;
|
||||
static parse<T extends YTNode = YTNode>(data: any, requireArray?: false | undefined, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) : SuperParsedResult<T>;
|
||||
|
||||
/**
|
||||
* Parses the `contents` property of the response as well as its nodes.
|
||||
*/
|
||||
static parse<T extends YTNode = YTNode>(data: any, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
if (!data) return null;
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ import { default as AnalyticsVodCarouselCard } from './classes/analytics/Analyti
|
||||
import { default as CtaGoToCreatorStudio } from './classes/analytics/CtaGoToCreatorStudio';
|
||||
import { default as DataModelSection } from './classes/analytics/DataModelSection';
|
||||
import { default as StatRow } from './classes/analytics/StatRow';
|
||||
import { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability';
|
||||
import { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo';
|
||||
import { default as BackstageImage } from './classes/BackstageImage';
|
||||
import { default as BackstagePost } from './classes/BackstagePost';
|
||||
import { default as BackstagePostThread } from './classes/BackstagePostThread';
|
||||
import { default as BrowseFeedActions } from './classes/BrowseFeedActions';
|
||||
import { default as BrowserMediaSession } from './classes/BrowserMediaSession';
|
||||
import { default as Button } from './classes/Button';
|
||||
import { default as C4TabbedHeader } from './classes/C4TabbedHeader';
|
||||
import { default as CallToActionButton } from './classes/CallToActionButton';
|
||||
@@ -38,6 +40,7 @@ import { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer';
|
||||
import { default as ChildVideo } from './classes/ChildVideo';
|
||||
import { default as ChipCloud } from './classes/ChipCloud';
|
||||
import { default as ChipCloudChip } from './classes/ChipCloudChip';
|
||||
import { default as CollaboratorInfoCardContent } from './classes/CollaboratorInfoCardContent';
|
||||
import { default as CollageHeroImage } from './classes/CollageHeroImage';
|
||||
import { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge';
|
||||
import { default as Comment } from './classes/comments/Comment';
|
||||
@@ -52,6 +55,7 @@ import { default as CompactLink } from './classes/CompactLink';
|
||||
import { default as CompactMix } from './classes/CompactMix';
|
||||
import { default as CompactPlaylist } from './classes/CompactPlaylist';
|
||||
import { default as CompactVideo } from './classes/CompactVideo';
|
||||
import { default as ConfirmDialog } from './classes/ConfirmDialog';
|
||||
import { default as ContinuationItem } from './classes/ContinuationItem';
|
||||
import { default as CopyLink } from './classes/CopyLink';
|
||||
import { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog';
|
||||
@@ -88,6 +92,7 @@ import { default as LiveChat } from './classes/LiveChat';
|
||||
import { default as AddBannerToLiveChatCommand } from './classes/livechat/AddBannerToLiveChatCommand';
|
||||
import { default as AddChatItemAction } from './classes/livechat/AddChatItemAction';
|
||||
import { default as AddLiveChatTickerItemAction } from './classes/livechat/AddLiveChatTickerItemAction';
|
||||
import { default as LiveChatAutoModMessage } from './classes/livechat/items/LiveChatAutoModMessage';
|
||||
import { default as LiveChatBanner } from './classes/livechat/items/LiveChatBanner';
|
||||
import { default as LiveChatBannerHeader } from './classes/livechat/items/LiveChatBannerHeader';
|
||||
import { default as LiveChatBannerPoll } from './classes/livechat/items/LiveChatBannerPoll';
|
||||
@@ -116,6 +121,7 @@ import { default as UpdateTitleAction } from './classes/livechat/UpdateTitleActi
|
||||
import { default as UpdateToggleButtonTextAction } from './classes/livechat/UpdateToggleButtonTextAction';
|
||||
import { default as UpdateViewershipAction } from './classes/livechat/UpdateViewershipAction';
|
||||
import { default as LiveChatAuthorBadge } from './classes/LiveChatAuthorBadge';
|
||||
import { default as LiveChatDialog } from './classes/LiveChatDialog';
|
||||
import { default as LiveChatHeader } from './classes/LiveChatHeader';
|
||||
import { default as LiveChatItemList } from './classes/LiveChatItemList';
|
||||
import { default as LiveChatMessageInput } from './classes/LiveChatMessageInput';
|
||||
@@ -138,6 +144,7 @@ import { default as MetadataBadge } from './classes/MetadataBadge';
|
||||
import { default as MetadataRow } from './classes/MetadataRow';
|
||||
import { default as MetadataRowContainer } from './classes/MetadataRowContainer';
|
||||
import { default as MetadataRowHeader } from './classes/MetadataRowHeader';
|
||||
import { default as MetadataScreen } from './classes/MetadataScreen';
|
||||
import { default as MicroformatData } from './classes/MicroformatData';
|
||||
import { default as Mix } from './classes/Mix';
|
||||
import { default as Movie } from './classes/Movie';
|
||||
@@ -146,6 +153,7 @@ import { default as MusicCarouselShelf } from './classes/MusicCarouselShelf';
|
||||
import { default as MusicCarouselShelfBasicHeader } from './classes/MusicCarouselShelfBasicHeader';
|
||||
import { default as MusicDescriptionShelf } from './classes/MusicDescriptionShelf';
|
||||
import { default as MusicDetailHeader } from './classes/MusicDetailHeader';
|
||||
import { default as MusicDownloadStateBadge } from './classes/MusicDownloadStateBadge';
|
||||
import { default as MusicEditablePlaylistDetailHeader } from './classes/MusicEditablePlaylistDetailHeader';
|
||||
import { default as MusicElementHeader } from './classes/MusicElementHeader';
|
||||
import { default as MusicHeader } from './classes/MusicHeader';
|
||||
@@ -183,6 +191,7 @@ import { default as PlaylistInfoCardContent } from './classes/PlaylistInfoCardCo
|
||||
import { default as PlaylistMetadata } from './classes/PlaylistMetadata';
|
||||
import { default as PlaylistPanel } from './classes/PlaylistPanel';
|
||||
import { default as PlaylistPanelVideo } from './classes/PlaylistPanelVideo';
|
||||
import { default as PlaylistPanelVideoWrapper } from './classes/PlaylistPanelVideoWrapper';
|
||||
import { default as PlaylistSidebar } from './classes/PlaylistSidebar';
|
||||
import { default as PlaylistSidebarPrimaryInfo } from './classes/PlaylistSidebarPrimaryInfo';
|
||||
import { default as PlaylistSidebarSecondaryInfo } from './classes/PlaylistSidebarSecondaryInfo';
|
||||
@@ -209,6 +218,7 @@ import { default as SearchSuggestion } from './classes/SearchSuggestion';
|
||||
import { default as SearchSuggestionsSection } from './classes/SearchSuggestionsSection';
|
||||
import { default as SecondarySearchContainer } from './classes/SecondarySearchContainer';
|
||||
import { default as SectionList } from './classes/SectionList';
|
||||
import { default as SegmentedLikeDislikeButton } from './classes/SegmentedLikeDislikeButton';
|
||||
import { default as SettingBoolean } from './classes/SettingBoolean';
|
||||
import { default as SettingsCheckbox } from './classes/SettingsCheckbox';
|
||||
import { default as SettingsOptions } from './classes/SettingsOptions';
|
||||
@@ -281,11 +291,13 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CtaGoToCreatorStudio,
|
||||
DataModelSection,
|
||||
StatRow,
|
||||
AudioOnlyPlayability,
|
||||
AutomixPreviewVideo,
|
||||
BackstageImage,
|
||||
BackstagePost,
|
||||
BackstagePostThread,
|
||||
BrowseFeedActions,
|
||||
BrowserMediaSession,
|
||||
Button,
|
||||
C4TabbedHeader,
|
||||
CallToActionButton,
|
||||
@@ -303,6 +315,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
ChildVideo,
|
||||
ChipCloud,
|
||||
ChipCloudChip,
|
||||
CollaboratorInfoCardContent,
|
||||
CollageHeroImage,
|
||||
AuthorCommentBadge,
|
||||
Comment,
|
||||
@@ -317,6 +330,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CompactMix,
|
||||
CompactPlaylist,
|
||||
CompactVideo,
|
||||
ConfirmDialog,
|
||||
ContinuationItem,
|
||||
CopyLink,
|
||||
CreatePlaylistDialog,
|
||||
@@ -353,6 +367,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
AddBannerToLiveChatCommand,
|
||||
AddChatItemAction,
|
||||
AddLiveChatTickerItemAction,
|
||||
LiveChatAutoModMessage,
|
||||
LiveChatBanner,
|
||||
LiveChatBannerHeader,
|
||||
LiveChatBannerPoll,
|
||||
@@ -381,6 +396,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
UpdateToggleButtonTextAction,
|
||||
UpdateViewershipAction,
|
||||
LiveChatAuthorBadge,
|
||||
LiveChatDialog,
|
||||
LiveChatHeader,
|
||||
LiveChatItemList,
|
||||
LiveChatMessageInput,
|
||||
@@ -403,6 +419,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MetadataRow,
|
||||
MetadataRowContainer,
|
||||
MetadataRowHeader,
|
||||
MetadataScreen,
|
||||
MicroformatData,
|
||||
Mix,
|
||||
Movie,
|
||||
@@ -411,6 +428,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MusicCarouselShelfBasicHeader,
|
||||
MusicDescriptionShelf,
|
||||
MusicDetailHeader,
|
||||
MusicDownloadStateBadge,
|
||||
MusicEditablePlaylistDetailHeader,
|
||||
MusicElementHeader,
|
||||
MusicHeader,
|
||||
@@ -448,6 +466,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
PlaylistMetadata,
|
||||
PlaylistPanel,
|
||||
PlaylistPanelVideo,
|
||||
PlaylistPanelVideoWrapper,
|
||||
PlaylistSidebar,
|
||||
PlaylistSidebarPrimaryInfo,
|
||||
PlaylistSidebarSecondaryInfo,
|
||||
@@ -474,6 +493,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
SearchSuggestionsSection,
|
||||
SecondarySearchContainer,
|
||||
SectionList,
|
||||
SegmentedLikeDislikeButton,
|
||||
SettingBoolean,
|
||||
SettingsCheckbox,
|
||||
SettingsOptions,
|
||||
|
||||
@@ -16,6 +16,7 @@ import ItemSection from '../classes/ItemSection';
|
||||
import PlayerOverlay from '../classes/PlayerOverlay';
|
||||
import ToggleButton from '../classes/ToggleButton';
|
||||
import CommentsEntryPointHeader from '../classes/comments/CommentsEntryPointHeader';
|
||||
import SegmentedLikeDislikeButton from '../classes/SegmentedLikeDislikeButton';
|
||||
import ContinuationItem from '../classes/ContinuationItem';
|
||||
import PlayerMicroformat from '../classes/PlayerMicroformat';
|
||||
import MicroformatData from '../classes/MicroformatData';
|
||||
@@ -45,9 +46,9 @@ export interface FormatOptions {
|
||||
*/
|
||||
format?: string;
|
||||
/**
|
||||
* InnerTube client, can be ANDROID, WEB or YTMUSIC
|
||||
* InnerTube client, can be ANDROID, WEB, YTMUSIC, YTMUSIC_ANDROID or TV_EMBEDDED
|
||||
*/
|
||||
client?: 'ANDROID' | 'WEB' | 'YTMUSIC'
|
||||
client?: 'ANDROID' | 'WEB' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'TV_EMBEDDED'
|
||||
}
|
||||
|
||||
export interface DownloadOptions extends FormatOptions {
|
||||
@@ -153,9 +154,11 @@ class VideoInfo {
|
||||
|
||||
this.player_overlays = next?.player_overlays.item().as(PlayerOverlay);
|
||||
|
||||
this.basic_info.like_count = this.primary_info?.menu?.top_level_buttons?.get({ icon_type: 'LIKE' })?.as(ToggleButton)?.like_count;
|
||||
this.basic_info.is_liked = this.primary_info?.menu?.top_level_buttons?.get({ icon_type: 'LIKE' })?.as(ToggleButton)?.is_toggled;
|
||||
this.basic_info.is_disliked = this.primary_info?.menu?.top_level_buttons?.get({ icon_type: 'DISLIKE' })?.as(ToggleButton)?.is_toggled;
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
|
||||
this.basic_info.like_count = segmented_like_dislike_button?.like_button?.as(ToggleButton)?.like_count;
|
||||
this.basic_info.is_liked = segmented_like_dislike_button?.like_button?.as(ToggleButton)?.is_toggled;
|
||||
this.basic_info.is_disliked = segmented_like_dislike_button?.dislike_button?.as(ToggleButton)?.is_toggled;
|
||||
|
||||
const comments_entry_point = results.get({ target_id: 'comments-entry-point' })?.as(ItemSection);
|
||||
|
||||
@@ -196,7 +199,9 @@ class VideoInfo {
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const response = await this.#actions.stats(this.#playback_tracking.videostats_playback_url, {
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', 'https://www.');
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name: Constants.CLIENTS.WEB.NAME,
|
||||
client_version: Constants.CLIENTS.WEB.VERSION
|
||||
}, url_params);
|
||||
@@ -224,7 +229,8 @@ class VideoInfo {
|
||||
* Likes the video.
|
||||
*/
|
||||
async like() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ button_id: 'TOGGLE_BUTTON_ID_TYPE_LIKE' })?.as(ToggleButton);
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.like_button?.as(ToggleButton);
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Like button not found', { video_id: this.basic_info.id });
|
||||
@@ -241,7 +247,8 @@ class VideoInfo {
|
||||
* Dislikes the video.
|
||||
*/
|
||||
async dislike() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ button_id: 'TOGGLE_BUTTON_ID_TYPE_DISLIKE' })?.as(ToggleButton);
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.dislike_button?.as(ToggleButton);
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Dislike button not found', { video_id: this.basic_info.id });
|
||||
@@ -258,7 +265,17 @@ class VideoInfo {
|
||||
* Removes like/dislike.
|
||||
*/
|
||||
async removeLike() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ is_toggled: true })?.as(ToggleButton);
|
||||
let button;
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const like_button = segmented_like_dislike_button?.like_button?.as(ToggleButton);
|
||||
const dislike_button = segmented_like_dislike_button?.dislike_button?.as(ToggleButton);
|
||||
|
||||
if (like_button?.is_toggled) {
|
||||
button = like_button;
|
||||
} else if (dislike_button?.is_toggled) {
|
||||
button = dislike_button;
|
||||
}
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('This video is not liked/disliked', { video_id: this.basic_info.id });
|
||||
|
||||
@@ -9,8 +9,16 @@ import WatchNextTabbedResults from '../classes/WatchNextTabbedResults';
|
||||
import SingleColumnMusicWatchNextResults from '../classes/SingleColumnMusicWatchNextResults';
|
||||
import MicroformatData from '../classes/MicroformatData';
|
||||
import PlayerOverlay from '../classes/PlayerOverlay';
|
||||
import PlaylistPanel from '../classes/PlaylistPanel';
|
||||
import SectionList from '../classes/SectionList';
|
||||
import MusicQueue from '../classes/MusicQueue';
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf';
|
||||
import MusicDescriptionShelf from '../classes/MusicDescriptionShelf';
|
||||
import AutomixPreviewVideo from '../classes/AutomixPreviewVideo';
|
||||
import Message from '../classes/Message';
|
||||
|
||||
import { ObservedArray } from '../helpers';
|
||||
|
||||
// TODO: add a way to get specific tabs
|
||||
class TrackInfo {
|
||||
#page: [ ParsedResponse, ParsedResponse? ];
|
||||
#actions: Actions;
|
||||
@@ -73,6 +81,77 @@ class TrackInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves contents of the given tab.
|
||||
*/
|
||||
async getTab(title: string) {
|
||||
if (!this.tabs)
|
||||
throw new InnertubeError('Could not find any tab');
|
||||
|
||||
const target_tab = this.tabs.get({ title });
|
||||
|
||||
if (!target_tab)
|
||||
throw new InnertubeError(`Tab "${title}" not found`, { available_tabs: this.available_tabs });
|
||||
|
||||
if (target_tab.content)
|
||||
return target_tab.content;
|
||||
|
||||
const page = await target_tab.endpoint.callTest(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
return page.contents.item().as(Message);
|
||||
|
||||
return page.contents.item().as(SectionList).contents.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves up next.
|
||||
*/
|
||||
async getUpNext(automix = true): Promise<PlaylistPanel> {
|
||||
const music_queue = await this.getTab('Up next') as MusicQueue;
|
||||
|
||||
if (!music_queue || !music_queue.content)
|
||||
throw new InnertubeError('Music queue was empty, the video id is probably invalid.', music_queue);
|
||||
|
||||
const playlist_panel = music_queue.content.as(PlaylistPanel);
|
||||
|
||||
if (!playlist_panel.playlist_id && automix) {
|
||||
const automix_preview_video = playlist_panel.contents.firstOfType(AutomixPreviewVideo);
|
||||
|
||||
if (!automix_preview_video)
|
||||
throw new InnertubeError('Automix item not found');
|
||||
|
||||
const page = await automix_preview_video.playlist_video?.endpoint.callTest(this.#actions, {
|
||||
videoId: this.basic_info.id,
|
||||
client: 'YTMUSIC',
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not fetch automix');
|
||||
|
||||
return page.contents_memo.getType(PlaylistPanel)?.[0];
|
||||
}
|
||||
|
||||
return playlist_panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves related content.
|
||||
*/
|
||||
async getRelated(): Promise<ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>> {
|
||||
const tab = await this.getTab('Related') as ObservedArray<MusicDescriptionShelf | MusicDescriptionShelf>;
|
||||
return tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves lyrics.
|
||||
*/
|
||||
async getLyrics(): Promise<MusicDescriptionShelf | undefined> {
|
||||
const tab = await this.getTab('Lyrics') as ObservedArray<MusicCarouselShelf | MusicDescriptionShelf>;
|
||||
return tab.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the song to the watch history.
|
||||
*/
|
||||
@@ -87,7 +166,9 @@ class TrackInfo {
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const response = await this.#actions.stats(this.#playback_tracking.videostats_playback_url, {
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', 'https://music.');
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name: Constants.CLIENTS.YTMUSIC.NAME,
|
||||
client_version: Constants.CLIENTS.YTMUSIC.VERSION
|
||||
}, url_params);
|
||||
@@ -95,6 +176,10 @@ class TrackInfo {
|
||||
return response;
|
||||
}
|
||||
|
||||
get available_tabs(): string[] {
|
||||
return this.tabs ? this.tabs.map((tab) => tab.title) : [];
|
||||
}
|
||||
|
||||
get page() {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protobuf-ts 2.8.0
|
||||
// @generated by protobuf-ts 2.7.0
|
||||
// @generated from protobuf file "youtube.proto" (package "youtube", syntax proto2)
|
||||
// tslint:disable
|
||||
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||
|
||||
@@ -6,7 +6,8 @@ export const URLS = Object.freeze({
|
||||
YT_UPLOAD: 'https://upload.youtube.com/',
|
||||
API: Object.freeze({
|
||||
BASE: 'https://youtubei.googleapis.com',
|
||||
PRODUCTION: 'https://youtubei.googleapis.com/youtubei/',
|
||||
PRODUCTION_1: 'https://www.youtube.com/youtubei/',
|
||||
PRODUCTION_2: 'https://youtubei.googleapis.com/youtubei/',
|
||||
STAGING: 'https://green-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
RELEASE: 'https://release-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
TEST: 'https://test-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
@@ -48,6 +49,10 @@ export const CLIENTS = Object.freeze({
|
||||
YTMUSIC_ANDROID: {
|
||||
NAME: 'ANDROID_MUSIC',
|
||||
VERSION: '5.17.51'
|
||||
},
|
||||
TV_EMBEDDED: {
|
||||
NAME: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
|
||||
VERSION: '2.0'
|
||||
}
|
||||
});
|
||||
export const STREAM_HEADERS = Object.freeze({
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class HTTPClient {
|
||||
input: URL | Request | string,
|
||||
init?: RequestInit & HTTPClientInit
|
||||
) {
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION + this.#session.api_version;
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION_1 + this.#session.api_version;
|
||||
const baseURL = init?.baseURL || innertube_url;
|
||||
|
||||
const request_url =
|
||||
@@ -143,6 +143,11 @@ export default class HTTPClient {
|
||||
ctx.client.clientName = Constants.CLIENTS.YTMUSIC_ANDROID.NAME;
|
||||
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
|
||||
break;
|
||||
case 'TV_EMBEDDED':
|
||||
ctx.client.clientVersion = Constants.CLIENTS.TV_EMBEDDED.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.TV_EMBEDDED.NAME;
|
||||
ctx.client.clientScreen = 'EMBED';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('YouTube.js Tests', () => {
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
it('should search on YouTube', async () => {
|
||||
it('should search', async () => {
|
||||
const search = await yt.search(VIDEOS[0].QUERY);
|
||||
expect(search.results?.length).toBeLessThanOrEqual(35);
|
||||
expect(search.videos.length).toBeLessThanOrEqual(35);
|
||||
@@ -20,7 +20,7 @@ describe('YouTube.js Tests', () => {
|
||||
expect(search.has_continuation).toBe(true);
|
||||
});
|
||||
|
||||
it('should retrieve YouTube search continuation', async () => {
|
||||
it('should retrieve search continuation', async () => {
|
||||
const search = await yt.search(VIDEOS[0].QUERY);
|
||||
const next = await search.getContinuation()
|
||||
expect(next.results?.length).toBeLessThanOrEqual(35);
|
||||
@@ -30,20 +30,10 @@ describe('YouTube.js Tests', () => {
|
||||
expect(next.has_continuation).toBe(true);
|
||||
});
|
||||
|
||||
it('should search on YouTube Music', async () => {
|
||||
const search = await yt.music.search(VIDEOS[1].QUERY);
|
||||
expect(search.songs?.contents.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should retrieve YouTube search suggestions', async () => {
|
||||
it('should retrieve search suggestions', async () => {
|
||||
const suggestions = await yt.getSearchSuggestions(VIDEOS[0].QUERY);
|
||||
expect(suggestions.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
|
||||
it('should retrieve YouTube Music search suggestions', async () => {
|
||||
const suggestions = await yt.music.getSearchSuggestions(VIDEOS[1].QUERY);
|
||||
expect(suggestions.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comments', () => {
|
||||
@@ -87,15 +77,10 @@ describe('YouTube.js Tests', () => {
|
||||
});
|
||||
|
||||
describe('General', () => {
|
||||
it('should retrieve playlist with YouTube', async () => {
|
||||
it('should retrieve playlist', async () => {
|
||||
const playlist = await yt.getPlaylist('PLLw0AzOz95FU7w2juhPECP9NyGhbZmz_t');
|
||||
expect(playlist.items.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('should retrieve playlist with YouTube Music', async () => {
|
||||
const playlist = await yt.music.getPlaylist('PLVbEymL-83SyVXXqT7fYX5sEvELvyGjL7');
|
||||
expect(playlist.items?.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it('should retrieve home feed', async () => {
|
||||
const homefeed = await yt.getHomeFeed();
|
||||
@@ -112,6 +97,46 @@ describe('YouTube.js Tests', () => {
|
||||
expect(result).toBeTruthy();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('YouTube Music', () => {
|
||||
let search: any;
|
||||
|
||||
it('should search', async () => {
|
||||
search = await yt.music.search(VIDEOS[1].QUERY);
|
||||
expect(search.songs?.contents.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should retrieve search suggestions', async () => {
|
||||
const suggestions = await yt.music.getSearchSuggestions(VIDEOS[1].QUERY);
|
||||
expect(suggestions.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
|
||||
it('should retrieve track info', async () => {
|
||||
const info = await yt.music.getInfo(VIDEOS[1].ID);
|
||||
expect(info.basic_info.id).toBe(VIDEOS[1].ID);
|
||||
});
|
||||
|
||||
it('should retrieve the "Related" tab', async () => {
|
||||
const info = await yt.music.getInfo(VIDEOS[1].ID);
|
||||
const related = await info.getRelated();
|
||||
expect((related as any).length).toBeGreaterThan(3);
|
||||
});
|
||||
|
||||
it('should retrieve albums', async () => {
|
||||
const album = await yt.music.getAlbum(search.albums?.contents[0]?.id);
|
||||
expect(album.contents).toBeDefined();
|
||||
});
|
||||
|
||||
it('should retrieve artists', async () => {
|
||||
const artist = await yt.music.getArtist(search.artists?.contents[0]?.id);
|
||||
expect(artist.sections).toBeDefined();
|
||||
});
|
||||
|
||||
it('should retrieve playlists', async () => {
|
||||
const playlist = await yt.music.getPlaylist(search.playlists?.contents[0]?.id);
|
||||
expect(playlist.items).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function download(id: string, yt: Innertube): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user