mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-15 02:22:11 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aefecd061e | ||
|
|
7485726f1e | ||
|
|
9e703abe3a | ||
|
|
affbe84284 | ||
|
|
fcbdae3e34 | ||
|
|
059c858021 | ||
|
|
4ecd3360e0 | ||
|
|
08e9527931 | ||
|
|
a9f03a1523 | ||
|
|
c8980c7985 | ||
|
|
2e5688f235 | ||
|
|
dcf2b720a0 | ||
|
|
a90f5eb853 | ||
|
|
c6482e07b9 | ||
|
|
2de77c8f2c | ||
|
|
2aaa209906 | ||
|
|
ab028ba1ec | ||
|
|
f2f48af1bc | ||
|
|
3a7da21fd1 | ||
|
|
89794d65da | ||
|
|
91847ae3cc | ||
|
|
eb44b71939 | ||
|
|
88ebb5e2ae | ||
|
|
b237b6af4e | ||
|
|
9e618cc576 | ||
|
|
daf95cfe87 | ||
|
|
bc03c91df9 | ||
|
|
e00be25bf4 | ||
|
|
c9856a8359 | ||
|
|
4b29ad74de | ||
|
|
60730a5531 |
@@ -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
|
||||
```
|
||||
```
|
||||
20
README.md
20
README.md
@@ -40,9 +40,11 @@
|
||||
[][actions]
|
||||
[][versions]
|
||||
[][codefactor]
|
||||
[][npm]
|
||||
[][npm]
|
||||
[][say-thanks]
|
||||
|
||||
<br>
|
||||
[][github-sponsors]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SPONSORS -->
|
||||
@@ -292,7 +294,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 +349,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?)
|
||||
@@ -527,6 +529,14 @@ Retrieves playlist contents.
|
||||
### getStreamingData(video_id, options)
|
||||
Returns deciphered streaming data.
|
||||
|
||||
**Note:**
|
||||
It is recommended to retrieve streaming data from a `VideoInfo`/`TrackInfo` object instead if you want to select formats manually, example:
|
||||
```ts
|
||||
const info = await yt.getBasicInfo('somevideoid');
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
```
|
||||
|
||||
**Returns**: `Promise.<object>`
|
||||
|
||||
| Param | Type | Description |
|
||||
@@ -569,7 +579,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`
|
||||
|
||||
@@ -6,17 +6,52 @@ YouTube.js works in the browser!
|
||||
|
||||
To use YouTube.js in the browser you must proxy requests through your own server. You can see our simple reference implementation in Deno in `examples/browser/proxy/deno.ts`.
|
||||
|
||||
Once the proxy is set up you need to tell Innertube about it when instantiating it.
|
||||
We'll use our own fetch implementation to proxy requests through our server. This is a simple example, but you can use any fetch implementation you want.
|
||||
|
||||
```ts
|
||||
import { Innertube } from "youtubei.js/build/browser";
|
||||
|
||||
const yt = await Innertube.create({
|
||||
browser_proxy: {
|
||||
host: "localhost",
|
||||
schema: 'http',
|
||||
}
|
||||
})
|
||||
fetch: async (input, init) => {
|
||||
// url
|
||||
const url = typeof input === 'string'
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL(input.url);
|
||||
|
||||
// transform the url for use with our proxy
|
||||
url.searchParams.set('__host', url.host);
|
||||
url.host = 'localhost:8080';
|
||||
url.protocol = 'http';
|
||||
|
||||
const headers = init?.headers
|
||||
? new Headers(init.headers)
|
||||
: input instanceof Request
|
||||
? input.headers
|
||||
: new Headers();
|
||||
|
||||
// now serialize the headers
|
||||
url.searchParams.set('__headers', JSON.stringify([...headers]));
|
||||
|
||||
// copy over the request
|
||||
const request = new Request(
|
||||
url,
|
||||
input instanceof Request ? input : undefined,
|
||||
);
|
||||
|
||||
headers.delete('user-agent');
|
||||
|
||||
// fetch the url
|
||||
return fetch(request, init ? {
|
||||
...init,
|
||||
headers
|
||||
} : {
|
||||
headers
|
||||
});
|
||||
},
|
||||
cache: new UniversalCache(),
|
||||
});
|
||||
```
|
||||
|
||||
after that you can use the library as normal.
|
||||
|
||||
@@ -18,7 +18,7 @@ const handler = async (request: Request): Promise<Response> => {
|
||||
'Access-Control-Allow-Origin': request.headers.get('origin') || '*',
|
||||
'Access-Control-Allow-Methods': '*',
|
||||
'Access-Control-Allow-Headers':
|
||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, Accept-Language, Range',
|
||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, Accept-Language, Range, Referer',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
}),
|
||||
@@ -45,7 +45,7 @@ const handler = async (request: Request): Promise<Response> => {
|
||||
JSON.parse(url.searchParams.get('__headers') || '{}'),
|
||||
);
|
||||
copyHeader('range', request_headers, request.headers);
|
||||
copyHeader('user-agent', request_headers, request.headers);
|
||||
!request_headers.has('user-agent') && copyHeader('user-agent', request_headers, request.headers);
|
||||
url.searchParams.delete('__headers');
|
||||
|
||||
// Make the request to YouTube
|
||||
@@ -62,6 +62,8 @@ const handler = async (request: Request): Promise<Response> => {
|
||||
copyHeader('content-length', headers, fetchRes.headers);
|
||||
copyHeader('content-type', headers, fetchRes.headers);
|
||||
copyHeader('content-disposition', headers, fetchRes.headers);
|
||||
copyHeader('accept-ranges', headers, fetchRes.headers);
|
||||
copyHeader('content-range', headers, fetchRes.headers);
|
||||
|
||||
// add cors headers
|
||||
headers.set(
|
||||
|
||||
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.0",
|
||||
"version": "2.2.3"
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.3",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.7.0",
|
||||
"jintr": "^0.1.9",
|
||||
"jintr": "^0.3.1",
|
||||
"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.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.3.1.tgz",
|
||||
"integrity": "sha512-AUcq8fKL4BE9jDx8TizZmJ9UOvk1CHKFW0nQcWaOaqk9tkLS9S10fNmusTWGEYTncn7U43nXrCbhYko/ylqrSg==",
|
||||
"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.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.3.1.tgz",
|
||||
"integrity": "sha512-AUcq8fKL4BE9jDx8TizZmJ9UOvk1CHKFW0nQcWaOaqk9tkLS9S10fNmusTWGEYTncn7U43nXrCbhYko/ylqrSg==",
|
||||
"requires": {
|
||||
"acorn": "^8.8.0"
|
||||
}
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.3",
|
||||
"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.3.1",
|
||||
"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",
|
||||
@@ -69,17 +69,18 @@
|
||||
"youtubedl",
|
||||
"youtube-dl",
|
||||
"youtube-downloader",
|
||||
"innertube",
|
||||
"youtube-music",
|
||||
"innertubeapi",
|
||||
"innertube",
|
||||
"unofficial",
|
||||
"downloader",
|
||||
"livechat",
|
||||
"studio",
|
||||
"upload",
|
||||
"ytmusic",
|
||||
"dislike",
|
||||
"search",
|
||||
"comment",
|
||||
"music",
|
||||
"like",
|
||||
"api"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,10 +141,10 @@ 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'))
|
||||
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.browse(artist_id, { client: 'YTMUSIC' });
|
||||
@@ -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;
|
||||
@@ -42,6 +43,9 @@ export interface Context {
|
||||
user: {
|
||||
lockedSafetyMode: false;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
};
|
||||
request: {
|
||||
useSsl: true;
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -225,7 +225,7 @@ const videos = response.contents_memo.getType(Video);
|
||||
|
||||
If you decompile a YouTube client and analize it for a while you will notice that it has classes named `protos/youtube/api/innertube/MusicItemRenderer`, `protos/youtube/api/innertube/SectionListRenderer`, etc.
|
||||
|
||||
These classes are used to parse objects from the response (which consists of protobuf messages) and also build requests. The website works in a similar way, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
|
||||
These classes are used to parse objects from the response, map them into models and generate the UI. The website works in a similar way, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
|
||||
|
||||
Here we're taking a similar approach, the parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, and on top of that it also parses navigation endpoints which allows us to make an API call with all required parameters in one line and even emulate client actions (eg; clicking a button).
|
||||
|
||||
|
||||
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;
|
||||
@@ -6,8 +6,8 @@ class Card extends YTNode {
|
||||
|
||||
teaser;
|
||||
content;
|
||||
card_id: string;
|
||||
feature: string;
|
||||
card_id: string | null;
|
||||
feature: string | null;
|
||||
|
||||
cue_ranges: {
|
||||
start_card_active_ms: string;
|
||||
@@ -18,10 +18,10 @@ class Card extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.teaser = Parser.parse(data.teaser);
|
||||
this.content = Parser.parse(data.content);
|
||||
this.card_id = data.cardId;
|
||||
this.feature = data.feature;
|
||||
this.teaser = Parser.parseItem(data.teaser);
|
||||
this.content = Parser.parseItem(data.content);
|
||||
this.card_id = data.cardId || null;
|
||||
this.feature = data.feature || null;
|
||||
|
||||
this.cue_ranges = data.cueRanges.map((cr: any) => ({
|
||||
start_card_active_ms: cr.startCardActiveMs,
|
||||
@@ -32,4 +32,4 @@ class Card extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
export default Card;
|
||||
export default Card;
|
||||
|
||||
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;
|
||||
@@ -8,7 +8,7 @@ class DropdownItem extends YTNode {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
value?: number | string;
|
||||
iconType?: string;
|
||||
icon_type?: string;
|
||||
description?: string;
|
||||
endpoint?: NavigationEndpoint;
|
||||
|
||||
@@ -29,7 +29,7 @@ class DropdownItem extends YTNode {
|
||||
}
|
||||
|
||||
if (data.icon?.iconType) {
|
||||
this.iconType = data.icon?.iconType;
|
||||
this.icon_type = data.icon?.iconType;
|
||||
}
|
||||
|
||||
if (data.descriptionText) {
|
||||
|
||||
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;
|
||||
@@ -8,7 +8,7 @@ class ProfileColumnStats extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
|
||||
// XXX: alias for consistency
|
||||
@@ -17,4 +17,4 @@ class ProfileColumnStats extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileColumnStats;
|
||||
export default ProfileColumnStats;
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
15
src/parser/classes/TitleAndButtonListHeader.ts
Normal file
15
src/parser/classes/TitleAndButtonListHeader.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class TitleAndButtonListHeader extends YTNode {
|
||||
static type = 'TitleAndButtonListHeader';
|
||||
|
||||
title: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
}
|
||||
}
|
||||
|
||||
export default TitleAndButtonListHeader;
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Author from './misc/Author';
|
||||
import Menu from './menus/Menu';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
@@ -53,8 +54,8 @@ class Video extends YTNode {
|
||||
})) || [];
|
||||
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail);
|
||||
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
|
||||
this.rich_thumbnail = data.richThumbnail ? Parser.parse(data.richThumbnail) : null;
|
||||
this.author = new Author(data.ownerText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
@@ -73,7 +74,7 @@ class Video extends YTNode {
|
||||
|
||||
this.show_action_menu = data.showActionMenu;
|
||||
this.is_watched = data.isWatched || false;
|
||||
this.menu = Parser.parse(data.menu);
|
||||
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
|
||||
@@ -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';
|
||||
@@ -244,6 +254,7 @@ import { default as ThumbnailOverlayResumePlayback } from './classes/ThumbnailOv
|
||||
import { default as ThumbnailOverlaySidePanel } from './classes/ThumbnailOverlaySidePanel';
|
||||
import { default as ThumbnailOverlayTimeStatus } from './classes/ThumbnailOverlayTimeStatus';
|
||||
import { default as ThumbnailOverlayToggleButton } from './classes/ThumbnailOverlayToggleButton';
|
||||
import { default as TitleAndButtonListHeader } from './classes/TitleAndButtonListHeader';
|
||||
import { default as ToggleButton } from './classes/ToggleButton';
|
||||
import { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem';
|
||||
import { default as Tooltip } from './classes/Tooltip';
|
||||
@@ -280,11 +291,13 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CtaGoToCreatorStudio,
|
||||
DataModelSection,
|
||||
StatRow,
|
||||
AudioOnlyPlayability,
|
||||
AutomixPreviewVideo,
|
||||
BackstageImage,
|
||||
BackstagePost,
|
||||
BackstagePostThread,
|
||||
BrowseFeedActions,
|
||||
BrowserMediaSession,
|
||||
Button,
|
||||
C4TabbedHeader,
|
||||
CallToActionButton,
|
||||
@@ -302,6 +315,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
ChildVideo,
|
||||
ChipCloud,
|
||||
ChipCloudChip,
|
||||
CollaboratorInfoCardContent,
|
||||
CollageHeroImage,
|
||||
AuthorCommentBadge,
|
||||
Comment,
|
||||
@@ -316,6 +330,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CompactMix,
|
||||
CompactPlaylist,
|
||||
CompactVideo,
|
||||
ConfirmDialog,
|
||||
ContinuationItem,
|
||||
CopyLink,
|
||||
CreatePlaylistDialog,
|
||||
@@ -352,6 +367,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
AddBannerToLiveChatCommand,
|
||||
AddChatItemAction,
|
||||
AddLiveChatTickerItemAction,
|
||||
LiveChatAutoModMessage,
|
||||
LiveChatBanner,
|
||||
LiveChatBannerHeader,
|
||||
LiveChatBannerPoll,
|
||||
@@ -380,6 +396,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
UpdateToggleButtonTextAction,
|
||||
UpdateViewershipAction,
|
||||
LiveChatAuthorBadge,
|
||||
LiveChatDialog,
|
||||
LiveChatHeader,
|
||||
LiveChatItemList,
|
||||
LiveChatMessageInput,
|
||||
@@ -402,6 +419,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MetadataRow,
|
||||
MetadataRowContainer,
|
||||
MetadataRowHeader,
|
||||
MetadataScreen,
|
||||
MicroformatData,
|
||||
Mix,
|
||||
Movie,
|
||||
@@ -410,6 +428,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MusicCarouselShelfBasicHeader,
|
||||
MusicDescriptionShelf,
|
||||
MusicDetailHeader,
|
||||
MusicDownloadStateBadge,
|
||||
MusicEditablePlaylistDetailHeader,
|
||||
MusicElementHeader,
|
||||
MusicHeader,
|
||||
@@ -447,6 +466,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
PlaylistMetadata,
|
||||
PlaylistPanel,
|
||||
PlaylistPanelVideo,
|
||||
PlaylistPanelVideoWrapper,
|
||||
PlaylistSidebar,
|
||||
PlaylistSidebarPrimaryInfo,
|
||||
PlaylistSidebarSecondaryInfo,
|
||||
@@ -473,6 +493,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
SearchSuggestionsSection,
|
||||
SecondarySearchContainer,
|
||||
SectionList,
|
||||
SegmentedLikeDislikeButton,
|
||||
SettingBoolean,
|
||||
SettingsCheckbox,
|
||||
SettingsOptions,
|
||||
@@ -508,6 +529,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
ThumbnailOverlaySidePanel,
|
||||
ThumbnailOverlayTimeStatus,
|
||||
ThumbnailOverlayToggleButton,
|
||||
TitleAndButtonListHeader,
|
||||
ToggleButton,
|
||||
ToggleMenuServiceItem,
|
||||
Tooltip,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import Actions from '../../core/Actions';
|
||||
import Feed from '../../core/Feed';
|
||||
import { observe, ObservedArray, YTNode } from '../helpers';
|
||||
import { InnertubeError } from '../../utils/Utils';
|
||||
import HorizontalCardList from '../classes/HorizontalCardList';
|
||||
|
||||
import Feed from '../../core/Feed';
|
||||
import SectionList from '../classes/SectionList';
|
||||
import ItemSection from '../classes/ItemSection';
|
||||
import HorizontalCardList from '../classes/HorizontalCardList';
|
||||
import RichListHeader from '../classes/RichListHeader';
|
||||
import SearchRefinementCard from '../classes/SearchRefinementCard';
|
||||
import TwoColumnSearchResults from '../classes/TwoColumnSearchResults';
|
||||
import UniversalWatchCard from '../classes/UniversalWatchCard';
|
||||
import WatchCardHeroVideo from '../classes/WatchCardHeroVideo';
|
||||
import WatchCardSectionSequence from '../classes/WatchCardSectionSequence';
|
||||
import { observe, ObservedArray, YTNode } from '../helpers';
|
||||
|
||||
class Search extends Feed {
|
||||
results: ObservedArray<YTNode> | null | undefined;
|
||||
@@ -22,11 +24,11 @@ class Search extends Feed {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents.item().as(TwoColumnSearchResults).primary_contents.item().key('contents').parsed().array() ||
|
||||
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents?.item().as(SectionList).contents.array() ||
|
||||
this.page.on_response_received_commands?.[0].contents;
|
||||
|
||||
const secondary_contents_maybe = this.page.contents.item().key('secondary_contents');
|
||||
const secondary_contents = secondary_contents_maybe.isParsed() ? secondary_contents_maybe.parsed().item().key('contents').parsed().array() : undefined;
|
||||
const secondary_contents_maybe = this.page.contents?.item().key('secondary_contents');
|
||||
const secondary_contents = secondary_contents_maybe?.isParsed() ? secondary_contents_maybe.parsed().item().key('contents').parsed().array() : undefined;
|
||||
|
||||
this.results = contents.firstOfType(ItemSection)?.contents;
|
||||
|
||||
@@ -57,14 +59,14 @@ class Search extends Feed {
|
||||
if (typeof card === 'string') {
|
||||
target_card = this.refinement_cards.cards.get({ query: card });
|
||||
if (!target_card)
|
||||
throw new InnertubeError('Refinement card not found!', { available_cards: this.refinement_card_queries });
|
||||
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
|
||||
} else if (card.type === 'SearchRefinementCard') {
|
||||
target_card = card;
|
||||
} else {
|
||||
throw new InnertubeError('Invalid refinement card!');
|
||||
}
|
||||
|
||||
const page = await target_card.endpoint.call(this.actions);
|
||||
const page = await target_card.endpoint.call(this.actions, undefined, true);
|
||||
|
||||
return new Search(this.actions, page, true);
|
||||
}
|
||||
@@ -81,4 +83,5 @@ class Search extends Feed {
|
||||
return new Search(this.actions, continuation, true);
|
||||
}
|
||||
}
|
||||
export default Search;
|
||||
|
||||
export default Search;
|
||||
@@ -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 });
|
||||
|
||||
@@ -7,6 +7,7 @@ import MusicCarouselShelf from '../classes/MusicCarouselShelf';
|
||||
import MusicPlaylistShelf from '../classes/MusicPlaylistShelf';
|
||||
import MusicImmersiveHeader from '../classes/MusicImmersiveHeader';
|
||||
import MusicVisualHeader from '../classes/MusicVisualHeader';
|
||||
import MusicHeader from '../classes/MusicHeader';
|
||||
|
||||
class Artist {
|
||||
#page;
|
||||
@@ -19,7 +20,7 @@ class Artist {
|
||||
this.#page = Parser.parseResponse((response as AxioslikeResponse).data);
|
||||
this.#actions = actions;
|
||||
|
||||
this.header = this.page.header.item().as(MusicImmersiveHeader, MusicVisualHeader);
|
||||
this.header = this.page.header.item().as(MusicImmersiveHeader, MusicVisualHeader, MusicHeader);
|
||||
|
||||
const music_shelf = this.#page.contents_memo.get('MusicShelf') as MusicShelf[] || [];
|
||||
const music_carousel_shelf = this.#page.contents_memo.get('MusicCarouselShelf') as MusicCarouselShelf[] || [];
|
||||
|
||||
@@ -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,12 @@ 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';
|
||||
ctx.thirdParty = { embedUrl: Constants.URLS.YT_BASE };
|
||||
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,20 +20,20 @@ describe('YouTube.js Tests', () => {
|
||||
expect(search.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 search continuation', async () => {
|
||||
const search = await yt.search(VIDEOS[0].QUERY);
|
||||
const next = await search.getContinuation()
|
||||
expect(next.results?.length).toBeLessThanOrEqual(35);
|
||||
expect(next.videos.length).toBeLessThanOrEqual(35);
|
||||
expect(next.playlists.length).toBeLessThanOrEqual(35);
|
||||
expect(next.channels.length).toBeLessThanOrEqual(35);
|
||||
expect(next.has_continuation).toBe(true);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -77,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();
|
||||
@@ -102,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