mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82c843928 | ||
|
|
be71d7c937 | ||
|
|
470d8d9406 | ||
|
|
2c5907f80f | ||
|
|
ade5feb31c | ||
|
|
13ebf0a039 | ||
|
|
cb8fafe94b | ||
|
|
bd35faa597 | ||
|
|
a8b507ee65 | ||
|
|
e7eacd9742 | ||
|
|
1c72a41675 | ||
|
|
62a68b207c | ||
|
|
1d9587e8c1 | ||
|
|
a90e5e0d07 | ||
|
|
955c8010a6 | ||
|
|
b2269deb79 | ||
|
|
573c8643aa | ||
|
|
e21542c227 | ||
|
|
9d912e5938 | ||
|
|
7ca0607004 | ||
|
|
20d84265b5 | ||
|
|
b13bf6e992 | ||
|
|
3d3436472f | ||
|
|
1a2fc3abd7 | ||
|
|
8ef4b42d44 | ||
|
|
b71f03caf2 | ||
|
|
dae7d6e40c | ||
|
|
2cee59024c | ||
|
|
ffd7d79308 | ||
|
|
9b005d62d6 | ||
|
|
a8e7e644ec | ||
|
|
ad1d3dbf91 | ||
|
|
3df3261488 | ||
|
|
1b1ce41c00 | ||
|
|
b82b720e4b | ||
|
|
4784dfa563 | ||
|
|
3e4d41bf06 | ||
|
|
9f1c31d7a0 | ||
|
|
9cb4530299 | ||
|
|
cb9a0c5410 | ||
|
|
427db5bbc2 | ||
|
|
2b29244b41 | ||
|
|
f9754f5ac6 | ||
|
|
b2253df802 | ||
|
|
f3517708ff | ||
|
|
0d35fe0ca5 | ||
|
|
3e3dc351bb | ||
|
|
197bb759cd | ||
|
|
c76b24b3f4 | ||
|
|
574b67a1f7 | ||
|
|
9b2738f128 | ||
|
|
95f1d4077f | ||
|
|
a511608f18 | ||
|
|
cf8a33c79f | ||
|
|
cfc1a183e0 | ||
|
|
95033e723e | ||
|
|
2cc7b8bcd6 | ||
|
|
2d774e26aa | ||
|
|
214aa147ce | ||
|
|
ce53ac1843 | ||
|
|
0ad26f28d9 | ||
|
|
4c7b8a3403 | ||
|
|
33a6e740d7 | ||
|
|
0b1840a62c | ||
|
|
f4e0f30e6e | ||
|
|
200632f374 | ||
|
|
f933cb45bc | ||
|
|
a0e6cef00f | ||
|
|
a0bfe16427 | ||
|
|
9d352b58eb | ||
|
|
6b6c80ddf1 | ||
|
|
58a6c84121 | ||
|
|
63b1261b7c | ||
|
|
d2eff3bfb8 | ||
|
|
b668ba8cfb | ||
|
|
0b88575614 | ||
|
|
bed0ff4154 | ||
|
|
27a50a2a7e | ||
|
|
d4f2d704bb | ||
|
|
97f181b212 | ||
|
|
251ed74bba | ||
|
|
1cdf701c84 | ||
|
|
bf12740333 | ||
|
|
0d77b59945 | ||
|
|
6e30309f56 | ||
|
|
e37cf62732 | ||
|
|
567fdbaf52 | ||
|
|
0a22319d9e | ||
|
|
eb72c2f6ef | ||
|
|
2ccbe2ce62 | ||
|
|
a69e43bf3a | ||
|
|
b2900f48a7 | ||
|
|
d612590530 | ||
|
|
e82e23dfbb | ||
|
|
f62c66db39 | ||
|
|
de61782f1a | ||
|
|
ceefbed98c | ||
|
|
315d89f84a | ||
|
|
2ea3602b61 | ||
|
|
b7df3d6df4 | ||
|
|
2acb7da019 | ||
|
|
0b991800a5 | ||
|
|
50ef71284d | ||
|
|
d6c5a9b971 | ||
|
|
0fc29f0bbf | ||
|
|
2bbefefbb7 | ||
|
|
13ad3774c9 | ||
|
|
8051a7dee6 | ||
|
|
2842b1d917 | ||
|
|
870b2811d9 | ||
|
|
1aedbd3ea6 | ||
|
|
e8af2a603d | ||
|
|
8e37efa575 | ||
|
|
5a362a0bd5 | ||
|
|
89ee68b084 | ||
|
|
dca61c3a22 | ||
|
|
56e6e23453 | ||
|
|
00fa514b03 | ||
|
|
d36389c865 | ||
|
|
55ca986888 | ||
|
|
b04df7e119 | ||
|
|
d8d92866d1 | ||
|
|
b4b0731589 | ||
|
|
d69d701869 | ||
|
|
cd4d28c951 | ||
|
|
22b9c174bb | ||
|
|
b704c8e78c | ||
|
|
bbfeb99f55 | ||
|
|
f2adeeeab4 | ||
|
|
3756e63996 | ||
|
|
a27807b6c1 | ||
|
|
5cfb969e33 | ||
|
|
1163125f5c | ||
|
|
9ac5043309 | ||
|
|
6a4b4f3359 | ||
|
|
2b3642ba63 | ||
|
|
fb2e237284 | ||
|
|
6f3deaf16a | ||
|
|
d4382e81c3 | ||
|
|
89956cab46 | ||
|
|
ac9341c769 | ||
|
|
cac762569a | ||
|
|
9978ebf085 | ||
|
|
b036e2fcdc | ||
|
|
e37f42f41b | ||
|
|
883a023624 | ||
|
|
506834b253 |
@@ -5,4 +5,5 @@ cache/
|
||||
src/proto/youtube.ts
|
||||
coverage/
|
||||
node_modules/
|
||||
dist/
|
||||
dist/
|
||||
src/proto/generated/
|
||||
8
.github/labeler_config.yml
vendored
8
.github/labeler_config.yml
vendored
@@ -2,13 +2,7 @@ version: 1
|
||||
labels:
|
||||
- label: "breaking-change"
|
||||
title: "^refactor!:.*"
|
||||
|
||||
- label: "enhancement"
|
||||
title: "^feat:.*"
|
||||
|
||||
- label: "bug"
|
||||
title: "^fix:.*"
|
||||
|
||||
|
||||
- label: "github"
|
||||
files:
|
||||
- ".github/.*"
|
||||
|
||||
20
.github/release.yml
vendored
20
.github/release.yml
vendored
@@ -1,20 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- octocat
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
- Semver-Major
|
||||
- breaking-change
|
||||
- title: New Features
|
||||
labels:
|
||||
- Semver-Minor
|
||||
- enhancement
|
||||
- title: Bug Fixes
|
||||
- bug
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -5,9 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: srvaroa/labeler@master
|
||||
with:
|
||||
|
||||
25
.github/workflows/lint.yml
vendored
25
.github/workflows/lint.yml
vendored
@@ -1,17 +1,18 @@
|
||||
name: Lint
|
||||
name: lint
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
name: Lint
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
|
||||
- name: npm install and lint
|
||||
run: |
|
||||
npm install
|
||||
npm run lint
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
26
.github/workflows/node.js.yml
vendored
26
.github/workflows/node.js.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 16.x, 18.x ]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run test
|
||||
62
.github/workflows/release-please.yml
vendored
Normal file
62
.github/workflows/release-please.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: release-please
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
id: release
|
||||
with:
|
||||
release-type: node
|
||||
package-name: youtubei.js
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "16.x"
|
||||
- name: Build for Deno
|
||||
run: |
|
||||
npm ci
|
||||
npm run build:deno
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
- name: Move Deno files
|
||||
run: |
|
||||
mkdir build
|
||||
mv deno build/deno
|
||||
cp deno.ts build/deno.ts
|
||||
cp {LICENSE,README.md} build
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
- name: Push to the Deno branch
|
||||
uses: s0/git-publish-subdir-action@develop
|
||||
env:
|
||||
REPO: self
|
||||
BRANCH: deno
|
||||
FOLDER: ./build
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SKIP_EMPTY_COMMITS: true
|
||||
MESSAGE: "chore: ${{ steps.release.outputs.tag_name }} release"
|
||||
TAG: ${{ steps.release.outputs.tag_name }}-deno
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
- name: Remove Deno folder
|
||||
run: rm -rf build
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
- name: Publish package to npmjs
|
||||
run: |
|
||||
npm ci
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
if: ${{ steps.release.outputs.release_created }}
|
||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -11,9 +11,7 @@ jobs:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.'
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. Remove the stale label or comment or this will be closed in 2 days'
|
||||
days-before-stale: 60
|
||||
days-before-close: 4
|
||||
18
.github/workflows/test.yml
vendored
Normal file
18
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
- run: npm run test
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -66,6 +66,12 @@ tmp/
|
||||
dist/
|
||||
bundle/*.js.*
|
||||
bundle/*.js
|
||||
bundle/*.cjs
|
||||
bundle/*.cjs.*
|
||||
deno/
|
||||
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
151
CHANGELOG.md
Normal file
151
CHANGELOG.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Changelog
|
||||
|
||||
## [4.1.1](https://github.com/LuanRT/YouTube.js/compare/v4.1.0...v4.1.1) (2023-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **PlayerCaptionsTracklist:** parse props only if they exist in the node ([470d8d9](https://github.com/LuanRT/YouTube.js/commit/470d8d94063f0159cd005c9eb15fd1a4a175bea0)), closes [#372](https://github.com/LuanRT/YouTube.js/issues/372)
|
||||
* **Search:** Return search results even if there are ads ([#373](https://github.com/LuanRT/YouTube.js/issues/373)) ([2c5907f](https://github.com/LuanRT/YouTube.js/commit/2c5907f80fd76452afe87d1722fe35a4f45a22e0))
|
||||
|
||||
## [4.1.0](https://github.com/LuanRT/YouTube.js/compare/v4.0.1...v4.1.0) (2023-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Session:** allow setting a custom visitor data token ([#371](https://github.com/LuanRT/YouTube.js/issues/371)) ([13ebf0a](https://github.com/LuanRT/YouTube.js/commit/13ebf0a03973e7ba7b65e9f72c4333927e4254f6))
|
||||
* **ShowingResultsFor:** parse all props ([1d9587e](https://github.com/LuanRT/YouTube.js/commit/1d9587e8c1ee0b11bb0e444c3d1e98162e9e1059))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **http:** android tv http client missing `clientName` ([#370](https://github.com/LuanRT/YouTube.js/issues/370)) ([cb8fafe](https://github.com/LuanRT/YouTube.js/commit/cb8fafe94b8ab330ae58211a892923321d65d890))
|
||||
* **node:** Electron apps crashing ([#367](https://github.com/LuanRT/YouTube.js/issues/367)) ([e7eacd9](https://github.com/LuanRT/YouTube.js/commit/e7eacd974211c90e7fbddfbf8019388cda3dfa5a))
|
||||
* **parser:** Make Video.is_live work on channel pages ([#368](https://github.com/LuanRT/YouTube.js/issues/368)) ([bd35faa](https://github.com/LuanRT/YouTube.js/commit/bd35faa5978f0b822e98d019523be1303374ddc0))
|
||||
* **toDash:** Generate unique Representation ids ([#366](https://github.com/LuanRT/YouTube.js/issues/366)) ([a8b507e](https://github.com/LuanRT/YouTube.js/commit/a8b507ee65ccd8edc9aea2aef8a908fa272bb23c))
|
||||
* **Utils:** Properly parse timestamps with thousands separators ([#363](https://github.com/LuanRT/YouTube.js/issues/363)) ([1c72a41](https://github.com/LuanRT/YouTube.js/commit/1c72a41675e47f711bd61ebb898bca5527406a79))
|
||||
|
||||
## [4.0.1](https://github.com/LuanRT/YouTube.js/compare/v4.0.0...v4.0.1) (2023-03-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Channel:** type mismatch in `subscribe_button` prop ([573c864](https://github.com/LuanRT/YouTube.js/commit/573c8643aae16ec7b6be5b333619a5d8c91ca5c1))
|
||||
|
||||
## [4.0.0](https://github.com/LuanRT/YouTube.js/compare/v3.3.0...v4.0.0) (2023-03-15)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **Parser:** general refactoring of parsers ([#344](https://github.com/LuanRT/YouTube.js/issues/344))
|
||||
* The `toDash` functions are now asynchronous, they now return a `Promise<string>` instead of a `string`, as we need to fetch the first sequence of the OTF format streams while building the manifest.
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for OTF format streams ([3e4d41b](https://github.com/LuanRT/YouTube.js/commit/3e4d41bf06ba16232979977c705444f2032bcde6))
|
||||
* **parser:** add `GridMix` ([#356](https://github.com/LuanRT/YouTube.js/issues/356)) ([a8e7e64](https://github.com/LuanRT/YouTube.js/commit/a8e7e644ec6df3b3c98a313f0321da27b4ca456e))
|
||||
* **parser:** add `GridShow` and `ShowCustomThumbnail` ([8ef4b42](https://github.com/LuanRT/YouTube.js/commit/8ef4b42d444c4fbe5cd65a55c0e0e7aa31738755)), closes [#459](https://github.com/LuanRT/YouTube.js/issues/459)
|
||||
* **parser:** add `MusicCardShelf` ([#358](https://github.com/LuanRT/YouTube.js/issues/358)) ([9b005d6](https://github.com/LuanRT/YouTube.js/commit/9b005d62d6590a2ddf6848dabfa33fce36e8df9c))
|
||||
* **parser:** Add `play_all_button` to `Shelf` ([#345](https://github.com/LuanRT/YouTube.js/issues/345)) ([427db5b](https://github.com/LuanRT/YouTube.js/commit/427db5bbc2bf3e8ec60371d504c2ab1cdae6e918))
|
||||
* **parser:** add `view_playlist` to `Playlist` ([#348](https://github.com/LuanRT/YouTube.js/issues/348)) ([9cb4530](https://github.com/LuanRT/YouTube.js/commit/9cb45302997771d909487b1ecba6f38655abef48))
|
||||
* **parser:** add InfoPanelContent and InfoPanelContainer nodes ([4784dfa](https://github.com/LuanRT/YouTube.js/commit/4784dfa563a4dbeaee31811824d5aa37a67f5557)), closes [#326](https://github.com/LuanRT/YouTube.js/issues/326)
|
||||
* **Parser:** just-in-time YTNode generation ([#310](https://github.com/LuanRT/YouTube.js/issues/310)) ([2cee590](https://github.com/LuanRT/YouTube.js/commit/2cee59024c730c34aa06052849ed6fb3f862ef33))
|
||||
* **yt:** add support for movie items and trailers ([#349](https://github.com/LuanRT/YouTube.js/issues/349)) ([9f1c31d](https://github.com/LuanRT/YouTube.js/commit/9f1c31d7a09532e80a187b14acceff31c22579bf))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **Parser:** general refactoring of parsers ([#344](https://github.com/LuanRT/YouTube.js/issues/344)) ([b13bf6e](https://github.com/LuanRT/YouTube.js/commit/b13bf6e9926c19a1939e0f4b69cbd53d1af0f7c8))
|
||||
|
||||
## [3.3.0](https://github.com/LuanRT/YouTube.js/compare/v3.2.0...v3.3.0) (2023-03-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **parser:** add `ConversationBar` node ([b2253df](https://github.com/LuanRT/YouTube.js/commit/b2253df8022217dc486155d8cacbf22db04dd9c2))
|
||||
* **VideoInfo:** support get by endpoint + more info ([#342](https://github.com/LuanRT/YouTube.js/issues/342)) ([0d35fe0](https://github.com/LuanRT/YouTube.js/commit/0d35fe0ca5e87a877b76cbb6cf3c92843eac5a99))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MultiMarkersPlayerBar:** avoid observing undefined objects ([f351770](https://github.com/LuanRT/YouTube.js/commit/f3517708ff34093a544c09d6f5f1ec806130d5cc))
|
||||
* **SharedPost:** import `Menu` node directly (oops) ([3e3dc35](https://github.com/LuanRT/YouTube.js/commit/3e3dc351bb44faec87616d9b922924d14a95f29f))
|
||||
* **ytmusic:** use static visitor id to avoid empty API responses ([f9754f5](https://github.com/LuanRT/YouTube.js/commit/f9754f5ac61d0f11b025f37f93783f971560268b)), closes [#279](https://github.com/LuanRT/YouTube.js/issues/279)
|
||||
|
||||
## [3.2.0](https://github.com/LuanRT/YouTube.js/compare/v3.1.1...v3.2.0) (2023-03-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for descriptive audio tracks ([#338](https://github.com/LuanRT/YouTube.js/issues/338)) ([574b67a](https://github.com/LuanRT/YouTube.js/commit/574b67a1f707a32378586dd2fe7b2f36f4ab6ddb))
|
||||
* export `FormatUtils`' types ([2d774e2](https://github.com/LuanRT/YouTube.js/commit/2d774e26aae79f3d1b115e0e85c148ae80985529))
|
||||
* **parser:** add `banner` to `PlaylistHeader` ([#337](https://github.com/LuanRT/YouTube.js/issues/337)) ([95033e7](https://github.com/LuanRT/YouTube.js/commit/95033e723ef912706e4d176de6b2760f017184e1))
|
||||
* **parser:** SharedPost ([#332](https://github.com/LuanRT/YouTube.js/issues/332)) ([ce53ac1](https://github.com/LuanRT/YouTube.js/commit/ce53ac18435cbcb20d6d4c4ab52fd156091e7592))
|
||||
* **VideoInfo:** add `game_info` and `category` ([#333](https://github.com/LuanRT/YouTube.js/issues/333)) ([214aa14](https://github.com/LuanRT/YouTube.js/commit/214aa147ce6306e37a6bf860a7bed5635db4797e))
|
||||
* **YouTube/Search:** add `SearchSubMenu` node ([#340](https://github.com/LuanRT/YouTube.js/issues/340)) ([a511608](https://github.com/LuanRT/YouTube.js/commit/a511608f18b37b0d9f2c7958ed5128330fabcfa0))
|
||||
* **yt:** add `getGuide()` ([#335](https://github.com/LuanRT/YouTube.js/issues/335)) ([2cc7b8b](https://github.com/LuanRT/YouTube.js/commit/2cc7b8bcd6938c7fb3af4f854a1d78b86d153873))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SegmentedLikeDislikeButton:** like/dislike buttons can also be a simple `Button` ([9b2738f](https://github.com/LuanRT/YouTube.js/commit/9b2738f1285b278c3e83541857651be9a6248288))
|
||||
* **YouTube:** fix warnings when retrieving members-only content ([#341](https://github.com/LuanRT/YouTube.js/issues/341)) ([95f1d40](https://github.com/LuanRT/YouTube.js/commit/95f1d4077ff3775f36967dca786139a09e2830a2))
|
||||
* **ytmusic:** export search filters type ([cf8a33c](https://github.com/LuanRT/YouTube.js/commit/cf8a33c79f5432136b865d535fd0ecedc2393382))
|
||||
|
||||
## [3.1.1](https://github.com/LuanRT/YouTube.js/compare/v3.1.0...v3.1.1) (2023-03-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Channel:** getting community continuations ([#329](https://github.com/LuanRT/YouTube.js/issues/329)) ([4c7b8a3](https://github.com/LuanRT/YouTube.js/commit/4c7b8a34030effa26c4ea186d3e9509128aec31c))
|
||||
|
||||
## [3.1.0](https://github.com/LuanRT/YouTube.js/compare/v3.0.0...v3.1.0) (2023-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add upcoming and live info to playlist videos ([#317](https://github.com/LuanRT/YouTube.js/issues/317)) ([a0bfe16](https://github.com/LuanRT/YouTube.js/commit/a0bfe164279ec27b0c49c6b0c32222c1a92df5c3))
|
||||
* **VideoSecondaryInfo:** add support for attributed descriptions ([#325](https://github.com/LuanRT/YouTube.js/issues/325)) ([f933cb4](https://github.com/LuanRT/YouTube.js/commit/f933cb45bcb92c07b3bc063d63869a51cbff4eb0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **parser:** export YTNodes individually so they can be used as types ([200632f](https://github.com/LuanRT/YouTube.js/commit/200632f374d5e0e105b600d579a2665a6fb36e38)), closes [#321](https://github.com/LuanRT/YouTube.js/issues/321)
|
||||
* **PlayerMicroformat:** Make the embed field optional ([#320](https://github.com/LuanRT/YouTube.js/issues/320)) ([a0e6cef](https://github.com/LuanRT/YouTube.js/commit/a0e6cef00fb9e3f52593cec22704f7ddc1f7553e))
|
||||
* send correct UA for Android requests ([f4e0f30](https://github.com/LuanRT/YouTube.js/commit/f4e0f30e6e94b347b28d67d9a86284ea2d23ee15)), closes [#322](https://github.com/LuanRT/YouTube.js/issues/322)
|
||||
|
||||
## [3.0.0](https://github.com/LuanRT/YouTube.js/compare/v2.9.0...v3.0.0) (2023-02-17)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* cleanup platform support ([#306](https://github.com/LuanRT/YouTube.js/issues/306))
|
||||
|
||||
### Features
|
||||
|
||||
* add parser support for MultiImage community posts ([#298](https://github.com/LuanRT/YouTube.js/issues/298)) ([de61782](https://github.com/LuanRT/YouTube.js/commit/de61782f1a673cbe66ae9b410341e39b7501ba84))
|
||||
* add support for hashtag feeds ([#312](https://github.com/LuanRT/YouTube.js/issues/312)) ([bf12740](https://github.com/LuanRT/YouTube.js/commit/bf12740333a82c26fe84e7c702c2fbb8859814fc))
|
||||
* add support for YouTube Kids ([#291](https://github.com/LuanRT/YouTube.js/issues/291)) ([2bbefef](https://github.com/LuanRT/YouTube.js/commit/2bbefefbb7cb061f3e7b686158b7568c32f0da5d))
|
||||
* allow checking whether a channel has optional tabs ([#296](https://github.com/LuanRT/YouTube.js/issues/296)) ([ceefbed](https://github.com/LuanRT/YouTube.js/commit/ceefbed98c70bb936e2d2df58c02834842acfdfc))
|
||||
* **Channel:** Add getters for all optional tabs ([#303](https://github.com/LuanRT/YouTube.js/issues/303)) ([b2900f4](https://github.com/LuanRT/YouTube.js/commit/b2900f48a7aa4c22635e1819ba9f636e81964f2c))
|
||||
* **Channel:** add support for sorting the playlist tab ([#295](https://github.com/LuanRT/YouTube.js/issues/295)) ([50ef712](https://github.com/LuanRT/YouTube.js/commit/50ef71284db41e5f94bb511892651d22a1d363a0))
|
||||
* extract channel error alert ([0b99180](https://github.com/LuanRT/YouTube.js/commit/0b991800a5c67f0e702251982b52eb8531f36f19))
|
||||
* **FormatUtils:** support multiple audio tracks in the DASH manifest ([#308](https://github.com/LuanRT/YouTube.js/issues/308)) ([a69e43b](https://github.com/LuanRT/YouTube.js/commit/a69e43bf3ae02f2428c4aa86f647e3e5e0db5ba6))
|
||||
* improve support for dubbed content ([#293](https://github.com/LuanRT/YouTube.js/issues/293)) ([d6c5a9b](https://github.com/LuanRT/YouTube.js/commit/d6c5a9b971444d0cd746aaf5310d3389793680ea))
|
||||
* parse isLive in CompactVideo ([#294](https://github.com/LuanRT/YouTube.js/issues/294)) ([2acb7da](https://github.com/LuanRT/YouTube.js/commit/2acb7da0198bfeca6ff911cf95cf06a220fccaa5))
|
||||
* **parser:** add `ChannelAgeGate` node ([1cdf701](https://github.com/LuanRT/YouTube.js/commit/1cdf701c8403db6b681a26ecb1df2daa51add454))
|
||||
* **parser:** Text#toHTML ([#300](https://github.com/LuanRT/YouTube.js/issues/300)) ([e82e23d](https://github.com/LuanRT/YouTube.js/commit/e82e23dfbb24dff3ddf45754c7319d783990e254))
|
||||
* **ytkids:** add `getChannel()` ([#292](https://github.com/LuanRT/YouTube.js/issues/292)) ([0fc29f0](https://github.com/LuanRT/YouTube.js/commit/0fc29f0bbf965215146a6ae192494c74e6cefcbb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* assign MetadataBadge's label ([#311](https://github.com/LuanRT/YouTube.js/issues/311)) ([e37cf62](https://github.com/LuanRT/YouTube.js/commit/e37cf627322f688fcef18d41345f77cbccd58829))
|
||||
* **ChannelAboutFullMetadata:** fix error when there are no primary links ([#299](https://github.com/LuanRT/YouTube.js/issues/299)) ([f62c66d](https://github.com/LuanRT/YouTube.js/commit/f62c66db396ba7d2f93007414101112b49d8375f))
|
||||
* **TopicChannelDetails:** avatar and subtitle parsing ([#302](https://github.com/LuanRT/YouTube.js/issues/302)) ([d612590](https://github.com/LuanRT/YouTube.js/commit/d612590530f5fe590fee969810b1dd44c37f0457))
|
||||
* **VideoInfo:** Gracefully handle missing watch next continuation ([#288](https://github.com/LuanRT/YouTube.js/issues/288)) ([13ad377](https://github.com/LuanRT/YouTube.js/commit/13ad3774c9783ed2a9f286aeee88110bd43b3a73))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* cleanup platform support ([#306](https://github.com/LuanRT/YouTube.js/issues/306)) ([2ccbe2c](https://github.com/LuanRT/YouTube.js/commit/2ccbe2ce6260ace3bfac8b4b391e583fbcc4e286))
|
||||
18
COLLABORATORS.md
Normal file
18
COLLABORATORS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Collaborators
|
||||
|
||||
This page lists the collaborators who have contributed to the development and success of the project.
|
||||
|
||||
## [LuanRT](https://github.com/LuanRT)
|
||||
[](https://github.com/sponsors/LuanRT)
|
||||
[](https://ko-fi.com/luanrt)
|
||||
|
||||
Owner and maintainer.
|
||||
|
||||
## [Wykerd](https://github.com/wykerd/)
|
||||
Initial parser implementation, several bug fixes, major refactorings and general maintenance.
|
||||
|
||||
## [MasterOfBob777](https://github.com/MasterOfBob777)
|
||||
Bug fixes and TypeScript support.
|
||||
|
||||
## [patrickkfkan](https://github.com/patrickkfkan)
|
||||
Major refactorings, improved YouTube Music support, and bug fixes.
|
||||
101
CONTRIBUTING.md
101
CONTRIBUTING.md
@@ -1,81 +1,66 @@
|
||||
# Contributing to YouTube.js
|
||||
Welcome to YouTube.js! We're thrilled to have you interested in contributing to our project. As a community-driven project, we believe in the power of collaboration and look forward to working with you. To get started, please follow our easy-to-follow guidelines:
|
||||
|
||||
Thank you for taking the time to contribute!
|
||||
The following is a set of guidelines for contributing to YouTube.js.
|
||||
___
|
||||
* [Issues](#issues)
|
||||
* [Create a new issue](#issue-1)
|
||||
* [Solve an issue](#issue-2)
|
||||
|
||||
* [Make changes](#changes)
|
||||
* [Commit your updates](#changes-1)
|
||||
* [Create a PR](#changes-2)
|
||||
* [Run tests](#test)
|
||||
* [Lint your code](#lint)
|
||||
* [Build](#build)
|
||||
|
||||
## Issues
|
||||
|
||||
<a id="issue-1"></a>
|
||||
#### Create a new issue
|
||||
If you find a problem, search if an issue already exists. If a related issue doesn't exist, you can open a new issue using a relevant issue form.
|
||||
### Creating a new issue
|
||||
Before creating a new issue, we recommend searching for similar or related issues to avoid duplication efforts. However, if you can't find one, you're more than welcome to create a new issue using a relevant issue form. Please make sure to describe the issue as clearly and concisely as possible.
|
||||
|
||||
<a id="issue-2"></a>
|
||||
#### Solve an issue
|
||||
Scan through the existing issues to find one that interests you. You can narrow down the search using labels as filters. If you find an issue to work on, you are welcome to open a PR with a fix. Documentation updates and grammar fixes are also appreciated!
|
||||
### Solving an issue
|
||||
If you want to lend a hand by solving an issue, it's always good to browse existing issues to find one that grabs your attention. You can narrow down the search using tags as filters. If you find an issue you'd like to help with, please feel free to open a Pull Request with a fix. We appreciate documentation updates and grammar fixes too!
|
||||
|
||||
<a id="changes"></a>
|
||||
## Make changes
|
||||
## Making Changes
|
||||
|
||||
1. Fork the repository
|
||||
2. Install or update to **Node.js v16**
|
||||
3. Create a working branch and start with your changes!
|
||||
1. Fork the repository on GitHub.
|
||||
2. Ensure that you have the latest Node.js v16 version installed.
|
||||
3. Create a working branch and start making your changes and improvements!
|
||||
|
||||
<a id="changes-1"></a>
|
||||
#### Commit your updates
|
||||
### Committing updates
|
||||
When you're done with the changes, make sure to commit them. Don't forget to write a clear, descriptive commit message. We recommend following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
Commit the changes once you're happy with them.
|
||||
### Creating a Pull Request
|
||||
Once you're happy with your updates, create a pull request on GitHub. This is the most efficient way to get your contribution reviewed and eventually merged into our codebase.
|
||||
|
||||
<a id="changes-2"></a>
|
||||
#### Pull Request
|
||||
- Use the pull request template to fill in the necessary details.
|
||||
- If you're solving an issue, link the pull request to that issue.
|
||||
- Enable the checkbox to allow maintainers to edit the branch and update it for merging.
|
||||
- Changes may be required before we can merge your changes, and we'll let you know what needs to be done.
|
||||
|
||||
When you think the code is ready for review a pull request should be created on Github. Owners of the repository will watch out for new PR‘s and review them in regular intervals.
|
||||
### Testing, Linting, and Building
|
||||
We have some automated processes set up for testing, linting, and building. Please run the following commands to test, lint, and build your code before submitting it:
|
||||
|
||||
- Fill the template.
|
||||
- Link the PR to an issue, if you are solving one.
|
||||
- Enable the checkbox to allow maintainer edits so the branch can be updated for a merge.
|
||||
- Changes may be requested before a PR can be merged.
|
||||
- As you update your PR and apply changes, mark each conversation as resolved.
|
||||
|
||||
<a id="test"></a>
|
||||
#### Test
|
||||
|
||||
```bash
|
||||
Testing:
|
||||
```sh
|
||||
npm run test
|
||||
```
|
||||
|
||||
<a id="lint"></a>
|
||||
#### Lint
|
||||
|
||||
```bash
|
||||
Linting:
|
||||
```sh
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
<a id="build"></a>
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
# Node
|
||||
npm run build:node
|
||||
|
||||
# Browser
|
||||
npm run build:browser
|
||||
npm run build:browser:prod
|
||||
Building:
|
||||
```sh
|
||||
# Build all
|
||||
npm run build
|
||||
|
||||
# Protobuf
|
||||
npm run build:proto
|
||||
|
||||
# Parser map
|
||||
npm run build:parser-map
|
||||
```
|
||||
|
||||
# Deno
|
||||
npm run build:deno
|
||||
|
||||
# ES Module
|
||||
npm run build:esm
|
||||
|
||||
# Node
|
||||
npm run bundle:node
|
||||
|
||||
# Browser
|
||||
npm run bundle:browser
|
||||
npm run bundle:browser:prod
|
||||
```
|
||||
|
||||
We appreciate your efforts and contributions to YouTube.js! Together, we can make this project even better.
|
||||
363
README.md
363
README.md
@@ -4,7 +4,7 @@
|
||||
[codefactor]: https://www.codefactor.io/repository/github/luanrt/youtube.js
|
||||
[actions]: https://github.com/LuanRT/YouTube.js/actions
|
||||
[say-thanks]: https://saythanks.io/to/LuanRT
|
||||
[github-sponsors]:https://github.com/sponsors/LuanRT
|
||||
[collaborators]: https://github.com/LuanRT/YouTube.js/blob/main/COLLABORATORS.md
|
||||
|
||||
<!-- OTHER LINKS -->
|
||||
[project]: https://github.com/LuanRT/YouTube.js
|
||||
@@ -14,18 +14,18 @@
|
||||
|
||||
<h1 align=center>YouTube.js</h1>
|
||||
|
||||
<p align=center>A full-featured wrapper around the InnerTube API, which is what YouTube itself uses</p>
|
||||
<p align=center>A full-featured wrapper around the InnerTube API</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[][actions]
|
||||
[][actions]
|
||||
[][versions]
|
||||
[][codefactor]
|
||||
[][npm]
|
||||
[][discord]
|
||||
[][say-thanks]
|
||||
<br>
|
||||
[][github-sponsors]
|
||||
[][collaborators]
|
||||
|
||||
</div>
|
||||
|
||||
@@ -51,40 +51,36 @@
|
||||
</body>
|
||||
</table>
|
||||
|
||||
___
|
||||
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#about">About</a>
|
||||
</li>
|
||||
<li>
|
||||
## Table of Contents
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#description">Description</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#getting-started">Getting Started</a>
|
||||
<ul>
|
||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||
<li><a href="#installation">Installation</a></li>
|
||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||
<li><a href="#installation">Installation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#usage">Usage</a>
|
||||
<ul>
|
||||
<li><a href="#browser-usage">Browser Usage</a></li>
|
||||
<li><a href="#caching">Caching</a></li>
|
||||
<li><a href="#api">API</a></li>
|
||||
<li><a href="#browser-usage">Browser Usage</a></li>
|
||||
<li><a href="#caching">Caching</a></li>
|
||||
<li><a href="#api">API</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#extending-the-library">Extending the library</a></li>
|
||||
<li><a href="#contributing">Contributing</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
<li><a href="#disclaimer">Disclaimer</a></li>
|
||||
<li><a href="#license">License</a></li>
|
||||
</ol>
|
||||
</details>
|
||||
</li>
|
||||
<li><a href="#extending-the-library">Extending the library</a></li>
|
||||
<li><a href="#contributing">Contributing</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
<li><a href="#disclaimer">Disclaimer</a></li>
|
||||
<li><a href="#license">License</a></li>
|
||||
</ol>
|
||||
|
||||
## Description
|
||||
|
||||
InnerTube is an API used across all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform[^1]. This library handles all the low-level communication with InnerTube, providing a simple, and efficient way to interact with YouTube programmatically. It is designed to emulate an actual client as closely as possible, including how API responses are parsed.
|
||||
InnerTube is an API used by all YouTube clients. It was created to simplify the deployment of new features and experiments across the platform [^1]. This library manages all low-level communication with InnerTube, providing a simple and efficient way to interact with YouTube programmatically. Its design aims to closely emulate an actual client, including the parsing of API responses.
|
||||
|
||||
If you have any questions or need help, feel free to reach out to us on our [Discord server][discord] or open an issue [here](https://github.com/LuanRT/YouTube.js/issues).
|
||||
|
||||
@@ -95,7 +91,7 @@ YouTube.js runs on Node.js, Deno, and modern browsers.
|
||||
|
||||
It requires a runtime with the following features:
|
||||
- [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
||||
- On Node we use [undici]()'s fetch implementation which requires Node.js 16.8+. You may provide your fetch implementation if you need to use an older version. See [providing your own fetch implementation](#custom-fetch) for more information.
|
||||
- On Node, we use [undici](https://github.com/nodejs/undici)'s fetch implementation, which requires Node.js 16.8+. If you need to use an older version, you may provide your own fetch implementation. See [providing your own fetch implementation](#custom-fetch) for more information.
|
||||
- The `Response` object returned by fetch must thus be spec compliant and return a `ReadableStream` object if you want to use the `VideoInfo#download` method. (Implementations like `node-fetch` returns a non-standard `Readable` object.)
|
||||
- [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) and [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) are required.
|
||||
|
||||
@@ -112,24 +108,54 @@ yarn add youtubei.js@latest
|
||||
npm install github:LuanRT/YouTube.js
|
||||
```
|
||||
|
||||
**TODO:** Deno install instructions (esm.sh possibly?)
|
||||
When using Deno, you can import YouTube.js directly from deno.land:
|
||||
```ts
|
||||
import { Innertube } from 'https://deno.land/x/youtubei/deno.ts';
|
||||
```
|
||||
|
||||
## Usage
|
||||
Create an InnerTube instance:
|
||||
```ts
|
||||
// const { Innertube } = require('youtubei.js');
|
||||
import { Innertube } from 'youtubei.js';
|
||||
const youtube = await Innertube.create();
|
||||
```****
|
||||
const youtube = await Innertube.create(/* options */);
|
||||
```
|
||||
|
||||
### Initialization Options
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
| Option | Type | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| `lang` | `string` | Language. | `en` |
|
||||
| `location` | `string` | Geolocation. | `US` |
|
||||
| `account_index` | `number` | The account index to use. This is useful if you have multiple accounts logged in. **NOTE:** Only works if you are signed in with cookies. | `0` |
|
||||
| `visitor_data` | `string` | Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint. | `undefined` |
|
||||
| `retrieve_player` | `boolean` | Specifies whether to retrieve the JS player. Disabling this will make session creation faster. **NOTE:** Deciphering formats is not possible without the JS player. | `true` |
|
||||
| `enable_safety_mode` | `boolean` | Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. | `false` |
|
||||
| `generate_session_locally` | `boolean` | Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. | `false` |
|
||||
| `device_category` | `DeviceCategory` | Platform to use for the session. | `DESKTOP` |
|
||||
| `client_type` | `ClientType` | InnerTube client type. | `WEB` |
|
||||
| `timezone` | `string` | The time zone. | `*` |
|
||||
| `cache` | `ICache` | Used to cache the deciphering functions from the JS player. | `undefined` |
|
||||
| `cookie` | `string` | YouTube cookies. | `undefined` |
|
||||
| `fetch` | `FetchFunction` | Fetch function to use. | `fetch` |
|
||||
|
||||
</details>
|
||||
|
||||
## Browser Usage
|
||||
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`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts).
|
||||
To use YouTube.js in the browser, you must proxy requests through your own server. You can see our simple reference implementation in Deno at [`examples/browser/proxy/deno.ts`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts).
|
||||
|
||||
You may provide your own fetch implementation to be used by YouTube.js. Which we will use here to modify and send the requests through our proxy. See [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/web) for a simple example using [Vite](https://vitejs.dev/).
|
||||
You may provide your own fetch implementation to be used by YouTube.js, which we will use to modify and send the requests through a proxy. See [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/web) for a simple example using [Vite](https://vitejs.dev/).
|
||||
|
||||
```ts
|
||||
// Pre-bundled version for the web
|
||||
import { Innertube } from 'youtubei.js/bundle/browser';
|
||||
// Multiple exports are available for the web.
|
||||
// Unbundled ESM version
|
||||
import { Innertube } from 'youtubei.js/web';
|
||||
// Bundled ESM version
|
||||
// import { Innertube } from 'youtubei.js/web.bundle';
|
||||
// Production Bundled ESM version
|
||||
// import { Innertube } from 'youtubei.js/web.bundle.min';
|
||||
await Innertube.create({
|
||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
// Modify the request
|
||||
@@ -147,7 +173,7 @@ YouTube.js supports streaming of videos in the browser by converting YouTube's s
|
||||
The example below uses [`dash.js`](https://github.com/Dash-Industry-Forum/dash.js) to play the video.
|
||||
|
||||
```ts
|
||||
import { Innertube } from 'youtubei.js';
|
||||
import { Innertube } from 'youtubei.js/web';
|
||||
import dashjs from 'dashjs';
|
||||
|
||||
const youtube = await Innertube.create({ /* setup - see above */ });
|
||||
@@ -158,7 +184,7 @@ const videoInfo = await youtube.getInfo('videoId');
|
||||
// now convert to a dash manifest
|
||||
// again - to be able to stream the video in the browser - we must proxy the requests through our own server
|
||||
// to do this, we provide a method to transform the URLs before writing them to the manifest
|
||||
const manifest = videoInfo.toDash(url => {
|
||||
const manifest = await videoInfo.toDash(url => {
|
||||
// modify the url
|
||||
// and return it
|
||||
return url;
|
||||
@@ -172,7 +198,8 @@ const player = dashjs.MediaPlayer().create();
|
||||
player.initialize(videoElement, uri, true);
|
||||
```
|
||||
|
||||
Our browser example in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web) provides a fully working example.
|
||||
A fully working example can be found in [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/blob/main/examples/browser/web). Alternatively, you can view it live at [ytjsexample.pages.dev](https://ytjsexample.pages.dev/).
|
||||
|
||||
<a name="custom-fetch"></a>
|
||||
|
||||
## Providing your own fetch implementation
|
||||
@@ -193,23 +220,25 @@ const yt = await Innertube.create({
|
||||
<a name="caching"></a>
|
||||
|
||||
## Caching
|
||||
To improve performance, you may wish to cache the transformed player instance which we use to decode the streaming urls.
|
||||
Caching the transformed player instance can greatly improve the performance. Our `UniversalCache` implementation uses different caching methods depending on the environment.
|
||||
|
||||
Our cache uses the `node:fs` module in Node-like environments, `Deno.writeFile` in Deno, and `indexedDB` in browsers.
|
||||
In Node.js, we use the `node:fs` module, `Deno.writeFile()` in Deno, and `indexedDB` in browsers.
|
||||
|
||||
By default, the cache stores data in the operating system's temporary directory (or `indexedDB` in browsers). You can make this cache persistent by specifying the path to the cache directory, which will be created if it doesn't exist.
|
||||
|
||||
```ts
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
// By default, cache stores files in the OS temp directory (or indexedDB in browsers).
|
||||
// Create a cache that stores files in the OS temp directory (or indexedDB in browsers) by default.
|
||||
const yt = await Innertube.create({
|
||||
cache: new UniversalCache()
|
||||
cache: new UniversalCache(false)
|
||||
});
|
||||
|
||||
// You may wish to make the cache persistent (on Node and Deno)
|
||||
// You may want to create a persistent cache instead (on Node and Deno).
|
||||
const yt = await Innertube.create({
|
||||
cache: new UniversalCache(
|
||||
// Enables persistent caching
|
||||
true,
|
||||
// Path to the cache directory will create the directory if it doesn't exist
|
||||
// Path to the cache directory. The directory will be created if it doesn't exist
|
||||
'./.cache'
|
||||
)
|
||||
});
|
||||
@@ -229,6 +258,7 @@ const yt = await Innertube.create({
|
||||
* [.playlist](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/playlist.md)
|
||||
* [.music](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/music.md)
|
||||
* [.studio](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/studio.md)
|
||||
* [.kids](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/kids.md)
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -238,12 +268,13 @@ const yt = await Innertube.create({
|
||||
<summary>Methods</summary>
|
||||
<p>
|
||||
|
||||
* [.getInfo(video_id, client?)](#getinfo)
|
||||
* [.getInfo(target, client?)](#getinfo)
|
||||
* [.getBasicInfo(video_id, client?)](#getbasicinfo)
|
||||
* [.search(query, filters?)](#search)
|
||||
* [.getSearchSuggestions(query)](#getsearchsuggestions)
|
||||
* [.getComments(video_id, sort_by?)](#getcomments)
|
||||
* [.getHomeFeed()](#gethomefeed)
|
||||
* [.getGuide()](#getguide)
|
||||
* [.getLibrary()](#getlibrary)
|
||||
* [.getHistory()](#gethistory)
|
||||
* [.getTrending()](#gettrending)
|
||||
@@ -252,23 +283,25 @@ const yt = await Innertube.create({
|
||||
* [.getNotifications()](#getnotifications)
|
||||
* [.getUnseenNotificationsCount()](#getunseennotificationscount)
|
||||
* [.getPlaylist(id)](#getplaylist)
|
||||
* [.getHashtag(hashtag)](#gethashtag)
|
||||
* [.getStreamingData(video_id, options)](#getstreamingdata)
|
||||
* [.download(video_id, options?)](#download)
|
||||
* [.resolveURL(url)](#resolveurl)
|
||||
* [.call(endpoint, args?)](#call)
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="getinfo"></a>
|
||||
### getInfo(video_id, client?)
|
||||
### getInfo(target, client?)
|
||||
|
||||
Retrieves video info, including playback data and even layout elements such as menus, buttons, etc — all nicely parsed.
|
||||
Retrieves video info.
|
||||
|
||||
**Returns**: `Promise.<VideoInfo>`
|
||||
**Returns**: `Promise<VideoInfo>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| target | `string` \| `NavigationEndpoint` | If `string`, the id of the video. If `NavigationEndpoint`, the endpoint of watchable elements such as `Video`, `Mix` and `Playlist`. To clarify, valid endpoints have payloads containing at least `videoId` and optionally `playlistId`, `params` and `index`. |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
|
||||
|
||||
<details>
|
||||
@@ -281,16 +314,19 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<info>#dislike()`
|
||||
- Dislikes the video.
|
||||
|
||||
- `<info>#removeLike()`
|
||||
- `<info>#removeRating()`
|
||||
- Removes like/dislike.
|
||||
|
||||
- `<info>#getLiveChat()`
|
||||
- Returns a LiveChat instance.
|
||||
|
||||
- `<info>#getTrailerInfo()`
|
||||
- Returns trailer info in a new `VideoInfo` instance, or `null` if none. Typically available for non-purchased movies or films.
|
||||
|
||||
- `<info>#chooseFormat(options)`
|
||||
- Used to choose streaming data formats.
|
||||
|
||||
- `<info>#toDash(url_transformer)`
|
||||
- `<info>#toDash(url_transformer?, format_filter?)`
|
||||
- Converts streaming data to an MPEG-DASH manifest.
|
||||
|
||||
- `<info>#download(options)`
|
||||
@@ -308,6 +344,12 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<info>#addToWatchHistory()`
|
||||
- Adds the video to the watch history.
|
||||
|
||||
- `<info>#autoplay_video_endpoint`
|
||||
- Returns the endpoint of the video for Autoplay.
|
||||
|
||||
- `<info>#has_trailer`
|
||||
- Checks if trailer is available.
|
||||
|
||||
- `<info>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
|
||||
@@ -319,7 +361,7 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
|
||||
Suitable for cases where you only need basic video metadata. Also, it is faster than [`getInfo()`](#getinfo).
|
||||
|
||||
**Returns**: `Promise.<VideoInfo>`
|
||||
**Returns**: `Promise<VideoInfo>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -331,13 +373,30 @@ Suitable for cases where you only need basic video metadata. Also, it is faster
|
||||
|
||||
Searches the given query on YouTube.
|
||||
|
||||
**Returns**: `Promise.<Search>`
|
||||
**Returns**: `Promise<Search>`
|
||||
|
||||
> **Note**
|
||||
> `Search` extends the [`Feed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/feed.md) class.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| query | `string` | The search query |
|
||||
| filters? | `SearchFilters` | Search filters |
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Search Filters</summary>
|
||||
|
||||
| Filter | Type | Value | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| upload_date | `string` | `all` \| `hour` \| `today` \| `week` \| `month` \| `year` | Filter by upload date |
|
||||
| type | `string` | `all` \| `video` \| `channel` \| `playlist` \| `movie` | Filter by type |
|
||||
| duration | `string` | `all` \| `short` \| `medium` \| `long` | Filter by duration |
|
||||
| sort_by | `string` | `relevance` \| `rating` \| `upload_date` \| `view_count` | Sort by |
|
||||
| features | `string[]` | `hd` \| `subtitles` \| `creative_commons` \| `3d` \| `live` \| `purchased` \| `4k` \| `360` \| `location` \| `hdr` \| `vr180` | Filter by features |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
@@ -358,7 +417,7 @@ Searches the given query on YouTube.
|
||||
### getSearchSuggestions(query)
|
||||
Retrieves search suggestions for given query.
|
||||
|
||||
**Returns**: `Promise.<string[]>`
|
||||
**Returns**: `Promise<string[]>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -368,7 +427,7 @@ Retrieves search suggestions for given query.
|
||||
### getComments(video_id, sort_by?)
|
||||
Retrieves comments for given video.
|
||||
|
||||
**Returns**: `Promise.<Comments>`
|
||||
**Returns**: `Promise<Comments>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -381,7 +440,10 @@ See [`./examples/comments`](https://github.com/LuanRT/YouTube.js/blob/main/examp
|
||||
### getHomeFeed()
|
||||
Retrieves YouTube's home feed.
|
||||
|
||||
**Returns**: `Promise.<HomeFeed>`
|
||||
**Returns**: `Promise<HomeFeed>`
|
||||
|
||||
> **Note**
|
||||
> `HomeFeed` extends the [`FilterableFeed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/filterable-feed.md) class.
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -408,11 +470,20 @@ Retrieves YouTube's home feed.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="getguide"></a>
|
||||
### getGuide()
|
||||
Retrieves YouTube's content guide.
|
||||
|
||||
**Returns**: `Promise<Guide>`
|
||||
|
||||
<a name="getlibrary"></a>
|
||||
### getLibrary()
|
||||
Retrieves the account's library.
|
||||
|
||||
**Returns**: `Promise.<Library>`
|
||||
**Returns**: `Promise<Library>`
|
||||
|
||||
> **Note**
|
||||
> `Library` extends the [`Feed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/feed.md) class.
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -421,10 +492,8 @@ Retrieves the account's library.
|
||||
- `<library>#history`
|
||||
- `<library>#watch_later`
|
||||
- `<library>#liked_videos`
|
||||
- `<library>#playlists`
|
||||
- `<library>#playlists_section`
|
||||
- `<library>#clips`
|
||||
- `<library>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -433,7 +502,10 @@ Retrieves the account's library.
|
||||
### getHistory()
|
||||
Retrieves watch history.
|
||||
|
||||
**Returns**: `Promise.<History>`
|
||||
**Returns**: `Promise<History>`
|
||||
|
||||
> **Note**
|
||||
> `History` extends the [`Feed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/feed.md) class.
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -449,19 +521,22 @@ Retrieves watch history.
|
||||
### getTrending()
|
||||
Retrieves trending content.
|
||||
|
||||
**Returns**: `Promise.<TabbedFeed>`
|
||||
**Returns**: `Promise<TabbedFeed<IBrowseResponse>>`
|
||||
|
||||
<a name="getsubscriptionsfeed"></a>
|
||||
### getSubscriptionsFeed()
|
||||
Retrieves subscriptions feed.
|
||||
Retrieves the subscriptions feed.
|
||||
|
||||
**Returns**: `Promise.<Feed>`
|
||||
**Returns**: `Promise<Feed<IBrowseResponse>>`
|
||||
|
||||
<a name="getchannel"></a>
|
||||
### getChannel(id)
|
||||
Retrieves contents for a given channel.
|
||||
|
||||
**Returns**: `Promise.<Channel>`
|
||||
**Returns**: `Promise<Channel>`
|
||||
|
||||
> **Note**
|
||||
> `Channel` extends the [`TabbedFeed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/tabbed-feed.md) class.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -479,8 +554,14 @@ Retrieves contents for a given channel.
|
||||
- `<channel>#getCommunity()`
|
||||
- `<channel>#getChannels()`
|
||||
- `<channel>#getAbout()`
|
||||
- `<channel>#search(query)`
|
||||
- `<channel>#applyFilter(filter)`
|
||||
- `<channel>#applyContentTypeFilter(content_type_filter)`
|
||||
- `<channel>#applySort(sort)`
|
||||
- `<channel>#getContinuation()`
|
||||
- `<channel>#filters`
|
||||
- `<channel>#content_type_filters`
|
||||
- `<channel>#sort_filters`
|
||||
- `<channel>#page`
|
||||
|
||||
</p>
|
||||
@@ -492,7 +573,7 @@ See [`./examples/channel`](https://github.com/LuanRT/YouTube.js/blob/main/exampl
|
||||
### getNotifications()
|
||||
Retrieves notifications.
|
||||
|
||||
**Returns**: `Promise.<NotificationsMenu>`
|
||||
**Returns**: `Promise<NotificationsMenu>`
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getter</summary>
|
||||
@@ -508,13 +589,16 @@ Retrieves notifications.
|
||||
### getUnseenNotificationsCount()
|
||||
Retrieves unseen notifications count.
|
||||
|
||||
**Returns**: `Promise.<number>`
|
||||
**Returns**: `Promise<number>`
|
||||
|
||||
<a name="getplaylist"></a>
|
||||
### getPlaylist(id)
|
||||
Retrieves playlist contents.
|
||||
|
||||
**Returns**: `Promise.<Playlist>`
|
||||
**Returns**: `Promise<Playlist>`
|
||||
|
||||
> **Note**
|
||||
> `Playlist` extends the [`Feed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/feed.md) class.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -530,19 +614,51 @@ Retrieves playlist contents.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="gethashtag"></a>
|
||||
### getHashtag(hashtag)
|
||||
Retrieves a given hashtag's page.
|
||||
|
||||
**Returns**: `Promise<HashtagFeed>`
|
||||
|
||||
> **Note**
|
||||
> `HashtagFeed` extends the [`FilterableFeed`](https://github.com/LuanRT/YouTube.js/blob/main/docs/API/filterable-feed.md) class.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| hashtag | `string` | The hashtag |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getter</summary>
|
||||
<p>
|
||||
|
||||
- `<hashtag>#applyFilter(filter)`
|
||||
- Applies given filter and returns a new `HashtagFeed` instance.
|
||||
- `<hashtag>#getContinuation()`
|
||||
- Retrieves next batch of contents.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="getstreamingdata"></a>
|
||||
### 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:
|
||||
> **Note**
|
||||
> This method will be deprecated in the future. We recommend retrieving streaming data from a `VideoInfo` or `TrackInfo` object instead if you want to select formats manually. Please refer to the following example:
|
||||
|
||||
```ts
|
||||
const info = await yt.getBasicInfo('somevideoid');
|
||||
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
|
||||
// or:
|
||||
const format = info.chooseFormat({ type: 'audio', quality: 'best' });
|
||||
const url = format?.decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
```
|
||||
|
||||
**Returns**: `Promise.<object>`
|
||||
**Returns**: `Promise<object>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -553,7 +669,7 @@ console.info('Playback url:', url);
|
||||
### download(video_id, options?)
|
||||
Downloads a given video.
|
||||
|
||||
**Returns**: `Promise.<ReadableStream<Uint8Array>>`
|
||||
**Returns**: `Promise<ReadableStream<Uint8Array>>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -562,11 +678,21 @@ Downloads a given video.
|
||||
|
||||
See [`./examples/download`](https://github.com/LuanRT/YouTube.js/blob/main/examples/download) for examples.
|
||||
|
||||
<a name="resolveurl"></a>
|
||||
### resolveURL(url)
|
||||
Resolves a given url.
|
||||
|
||||
**Returns**: `Promise<NavigationEndpoint>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| url | `string` | Url to resolve |
|
||||
|
||||
<a name="call"></a>
|
||||
### call(endpoint, args?)
|
||||
Utility to call navigation endpoints.
|
||||
|
||||
**Returns**: `Promise.<ActionsResponse | ParsedResponse>`
|
||||
**Returns**: `Promise<T extends IParsedResponse | IParsedResponse | ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -575,64 +701,64 @@ Utility to call navigation endpoints.
|
||||
|
||||
## Extending the library
|
||||
|
||||
YouTube.js is completely modular and easy to extend. Almost all methods, classes, and utilities used internally are exposed and can be used to implement your own extensions without having to modify the library's source code.
|
||||
|
||||
For example, let's say we want to implement a method to retrieve video info manually. We can do that by using an instance of the `Actions` class:
|
||||
YouTube.js is modular and easy to extend. Most of the methods, classes, and utilities used internally are exposed and can be used to implement your own extensions without having to modify the library's source code.
|
||||
|
||||
For example, let's say we want to implement a method to retrieve video info. We can do that by using an instance of the `Actions` class:
|
||||
```ts
|
||||
import { Innertube } from 'youtubei.js';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
(async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
async function getVideoInfo(videoId: string) {
|
||||
const payload = {
|
||||
// anything added here will be merged with the default payload and sent to InnerTube.
|
||||
videoId,
|
||||
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.
|
||||
};
|
||||
async function getVideoInfo(videoId: string) {
|
||||
const videoInfo = await yt.actions.execute('/player', {
|
||||
// You can add any additional payloads here, and they'll merge with the default payload sent to InnerTube.
|
||||
videoId,
|
||||
client: 'YTMUSIC', // InnerTube client options: ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB, or TV_EMBEDDED.
|
||||
parse: true // tells YouTube.js to parse the response (not sent to InnerTube).
|
||||
});
|
||||
|
||||
const videoInfo = await yt.actions.execute('/player', payload);
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
return videoInfo;
|
||||
}
|
||||
|
||||
const videoInfo = await getVideoInfo('jLTOuvBTLxA');
|
||||
console.info(videoInfo);
|
||||
const videoInfo = await getVideoInfo('jLTOuvBTLxA');
|
||||
console.info(videoInfo);
|
||||
})();
|
||||
```
|
||||
|
||||
Or perhaps there's a `NavigationEndpoint` in a parsed response and we want to call it to see what happens:
|
||||
|
||||
Alternatively, suppose we locate a `NavigationEndpoint` in a parsed response and want to see what happens when we call it:
|
||||
```ts
|
||||
import { Innertube, YTNodes } from 'youtubei.js';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
(async () => {
|
||||
const yt = await Innertube.create();
|
||||
|
||||
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
|
||||
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
|
||||
const artist = await yt.music.getArtist('UC52ZqHVQz5OoGhvbWiRal6g');
|
||||
const albums = artist.sections[1].as(YTNodes.MusicCarouselShelf);
|
||||
|
||||
// Say we want to click the “More” button:
|
||||
const button = albums.as(YTNodes.MusicCarouselShelf).header?.more_content;
|
||||
// Let's imagine that we wish to click on the “More” button:
|
||||
const button = albums.as(YTNodes.MusicCarouselShelf).header?.more_content;
|
||||
|
||||
if (button) {
|
||||
// After making sure it exists, we can call its navigation endpoint:
|
||||
const page = await button.endpoint.call(yt.actions);
|
||||
console.info(page);
|
||||
}
|
||||
if (button) {
|
||||
// Having ensured that it exists, we can then call its navigation endpoint using the following code:
|
||||
const page = await button.endpoint.call(yt.actions, { parse: true });
|
||||
console.info(page);
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
### Parser
|
||||
|
||||
YouTube.js' parser allows you to parse InnerTube responses and turn their nodes into strongly typed objects that can be easily manipulated. It also provides a set of utility methods that make working with InnerTube much easier.
|
||||
YouTube.js' parser enables you to parse InnerTube responses and convert their nodes into strongly-typed objects that are simple to manipulate. Additionally, it provides numerous utility methods that make working with InnerTube a breeze.
|
||||
|
||||
Example:
|
||||
Here's an example of its usage:
|
||||
```ts
|
||||
// See ./examples/parser
|
||||
|
||||
import { Parser, YTNodes } from 'youtubei.js';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Artist page response from YouTube Music
|
||||
// YouTube Music's artist page response
|
||||
const data = readFileSync('./artist.json').toString();
|
||||
|
||||
const page = Parser.parseResponse(JSON.parse(data));
|
||||
@@ -641,14 +767,8 @@ const header = page.header?.item().as(YTNodes.MusicImmersiveHeader, YTNodes.Musi
|
||||
|
||||
console.info('Header:', header);
|
||||
|
||||
/**
|
||||
* The parser encapsulates all arrays in a proxy object.
|
||||
* A proxy intercepts access to the actual data, allowing
|
||||
* the parser to add type safety and many utility methods
|
||||
* that make working with InnerTube much easier.
|
||||
*/
|
||||
const tab = page.contents.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
|
||||
// The parser uses a proxy object to add type safety and utility methods for working with InnerTube's data arrays:
|
||||
const tab = page.contents?.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
|
||||
if (!tab)
|
||||
throw new Error('Target tab not found');
|
||||
@@ -656,19 +776,17 @@ if (!tab)
|
||||
if (!tab.content)
|
||||
throw new Error('Target tab appears to be empty');
|
||||
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.array().as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
|
||||
console.info('Sections:', sections);
|
||||
```
|
||||
|
||||
Documentation for the parser can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/src/parser).
|
||||
|
||||
<!-- CONTRIBUTING -->
|
||||
## Contributing
|
||||
Contributions, issues, and feature requests are welcome.
|
||||
Feel free to check the [issues page](https://github.com/LuanRT/YouTube.js/issues) and our [guidelines](https://github.com/LuanRT/YouTube.js/blob/main/CONTRIBUTING.md) if you want to contribute.
|
||||
We welcome all contributions, issues and feature requests, whether small or large. If you want to contribute, feel free to check out our [issues page](https://github.com/LuanRT/YouTube.js/issues) and our [guidelines](https://github.com/LuanRT/YouTube.js/blob/main/CONTRIBUTING.md).
|
||||
|
||||
Thank you to all the wonderful people who have contributed to this project:
|
||||
We are immensely grateful to all the wonderful people who have contributed to this project. A special shoutout to all our contributors! 🎉
|
||||
<a href="https://github.com/LuanRT/YouTube.js/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=LuanRT/YouTube.js" />
|
||||
</a>
|
||||
@@ -680,10 +798,9 @@ LuanRT - [@thesciencephile][twitter] - luan.lrt4@gmail.com
|
||||
Project Link: [https://github.com/LuanRT/YouTube.js][project]
|
||||
|
||||
## Disclaimer
|
||||
This project is not affiliated with, endorsed, or sponsored by YouTube or any of its affiliates or subsidiaries.
|
||||
All trademarks, logos, and brand names are the property of their respective owners and are used only to directly describe the services being provided, as such, any usage of trademarks to refer to such services is considered nominative use.
|
||||
This project is not affiliated with, endorsed, or sponsored by YouTube or any of its affiliates or subsidiaries. All trademarks, logos, and brand names used in this project are the property of their respective owners and are used solely to describe the services provided.
|
||||
|
||||
Should you have any questions or concerns please contact me directly via email.
|
||||
As such, any usage of trademarks to refer to such services is considered nominative use. If you have any questions or concerns, please contact me directly via email.
|
||||
|
||||
[^1]: https://gizmodo.com/how-project-innertube-helped-pull-youtube-out-of-the-gu-1704946491
|
||||
|
||||
|
||||
11
browser.ts
11
browser.ts
@@ -1,11 +0,0 @@
|
||||
// Deno and browser runtimes
|
||||
|
||||
import Innertube from './src/Innertube';
|
||||
|
||||
export * from './src/utils';
|
||||
export { YTNodes } from './src/parser/map';
|
||||
export { default as Parser } from './src/parser';
|
||||
export { default as Innertube } from './src/Innertube';
|
||||
export { default as Session } from './src/core/Session';
|
||||
export { default as Player } from './src/core/Player';
|
||||
export default Innertube;
|
||||
2
bundle/browser.d.ts
vendored
2
bundle/browser.d.ts
vendored
@@ -1 +1 @@
|
||||
export * from '../dist/browser';
|
||||
export * from '../dist/src/platform/lib.js';
|
||||
1
bundle/node.d.cts
Normal file
1
bundle/node.d.cts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '../dist/src/platform/lib.js';
|
||||
3
deno.ts
Normal file
3
deno.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './deno/src/platform/deno.ts';
|
||||
import Innertube from './deno/src/platform/deno.ts';
|
||||
export default Innertube;
|
||||
@@ -46,7 +46,7 @@ Retrieves account information.
|
||||
<p>
|
||||
|
||||
- `<accountinfo>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -63,7 +63,7 @@ Retrieves time watched statistics.
|
||||
<p>
|
||||
|
||||
- `<timewatched>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -91,6 +91,9 @@ Retrieves YouTube settings.
|
||||
- `<settings>#sidebar_items`
|
||||
- Returns options available in the sidebar menu.
|
||||
|
||||
- `<settings>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -106,7 +109,7 @@ Retrieves basic channel analytics.
|
||||
<p>
|
||||
|
||||
- `<analytics>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
115
docs/API/feed.md
Normal file
115
docs/API/feed.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Feed
|
||||
|
||||
Represents a YouTube feed. This class provides a set of utility methods for parsing and interacting with feeds.
|
||||
|
||||
## API
|
||||
|
||||
* Feed
|
||||
* [.videos](#videos)
|
||||
* [.posts](#posts)
|
||||
* [.channels](#channels)
|
||||
* [.playlists](#playlists)
|
||||
* [.shelves](#shelves)
|
||||
* [.memo](#memo)
|
||||
* [.page_contents](#page_contents)
|
||||
* [.secondary_contents](#secondary_contents)
|
||||
* [.page](#page)
|
||||
* [.has_continuation](#has_continuation)
|
||||
* [.getContinuationData()](#getcontinuationdata)
|
||||
* [.getContinuation()](#getcontinuation)
|
||||
* [.getShelf(title)](#getshelf)
|
||||
|
||||
<a name="videos"></a>
|
||||
### videos
|
||||
|
||||
Returns all videos in the feed.
|
||||
|
||||
**Returns:** `ObservedArray<Video | GridVideo | ReelItem | CompactVideo | PlaylistVideo | PlaylistPanelVideo | WatchCardCompactVideo>`
|
||||
|
||||
<a name="posts"></a>
|
||||
### posts
|
||||
|
||||
Returns all posts in the feed.
|
||||
|
||||
**Returns:** `ObservedArray<Post | BackstagePost>`
|
||||
|
||||
<a name="channels"></a>
|
||||
### channels
|
||||
|
||||
Returns all channels in the feed.
|
||||
|
||||
**Returns:** `ObservedArray<Channel | GridChannel>`
|
||||
|
||||
<a name="playlists"></a>
|
||||
### playlists
|
||||
|
||||
Returns all playlists in the feed.
|
||||
|
||||
**Returns:** `ObservedArray<Playlist | GridPlaylist>`
|
||||
|
||||
<a name="shelves"></a>
|
||||
### shelves
|
||||
|
||||
Returns all shelves in the feed.
|
||||
|
||||
**Returns:** `ObservedArray<Shelf | RichShelf | ReelShelf>`
|
||||
|
||||
<a name="memo"></a>
|
||||
### memo
|
||||
|
||||
Returns the memoized feed contents.
|
||||
|
||||
**Returns:** `Memo`
|
||||
|
||||
<a name="page_contents"></a>
|
||||
### page_contents
|
||||
|
||||
Returns the page contents.
|
||||
|
||||
**Returns:** `SectionList | MusicQueue | RichGrid | ReloadContinuationItemsCommand`
|
||||
|
||||
<a name="secondary_contents"></a>
|
||||
### secondary_contents
|
||||
|
||||
Returns the secondary contents node.
|
||||
|
||||
**Returns:** `SuperParsedResult<YTNode> | undefined `
|
||||
|
||||
<a name="page"></a>
|
||||
### page
|
||||
|
||||
Returns the original InnerTube response, parsed and sanitized.
|
||||
|
||||
**Returns:** `T extends IParsedResponse = IParsedResponse`
|
||||
|
||||
<a name="has_continuation"></a>
|
||||
### has_continuation
|
||||
|
||||
Returns whether the feed has a continuation.
|
||||
|
||||
**Returns:** `boolean`
|
||||
|
||||
<a name="getcontinuationdata"></a>
|
||||
### getContinuationData()
|
||||
|
||||
Returns the continuation data.
|
||||
|
||||
**Returns:** `Promise<T | undefined>`
|
||||
|
||||
<a name="getcontinuation"></a>
|
||||
### getContinuation()
|
||||
|
||||
Retrieves the feed's continuation.
|
||||
|
||||
**Returns:** `Promise<Feed<T>>`
|
||||
|
||||
<a name="getshelf"></a>
|
||||
### getShelf(title)
|
||||
|
||||
Gets a shelf by its title.
|
||||
|
||||
**Returns:** `Shelf | RichShelf | ReelShelf | undefined`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| title | `string` | The title of the shelf to get |
|
||||
38
docs/API/filterable-feed.md
Normal file
38
docs/API/filterable-feed.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# FilterableFeed
|
||||
|
||||
Represents a feed that can be filtered.
|
||||
|
||||
> **Note**
|
||||
> This class extends the [Feed](feed.md) class.
|
||||
|
||||
## API
|
||||
|
||||
* FilterableFeed
|
||||
* [.filter_chips](#filter_chips)
|
||||
* [.filters](#filters)
|
||||
* [.getFilteredFeed(filter: string | ChipCloudChip)](#getfilteredfeed)
|
||||
|
||||
<a name="filter_chips"></a>
|
||||
### filter_chips
|
||||
|
||||
Returns the feed's filter chips.
|
||||
|
||||
**Returns:** `ObservedArray<ChipCloudChip>`
|
||||
|
||||
<a name="filters"></a>
|
||||
### filters
|
||||
|
||||
Returns the feed's filter chips as an array of strings.
|
||||
|
||||
**Returns:** `string[]`
|
||||
|
||||
<a name="getfilteredfeed"></a>
|
||||
### getFilteredFeed(filter: string | ChipCloudChip)
|
||||
|
||||
Returns a new [Feed](feed.md) with the given filter applied.
|
||||
|
||||
**Returns:** `Promise<Feed<T>>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| filter | `string` \| `ChipCloudChip` | The filter to apply |
|
||||
@@ -7,7 +7,7 @@ Handles direct interactions.
|
||||
* InteractionManager
|
||||
* [.like(video_id)](#like)
|
||||
* [.dislike(video_id)](#dislike)
|
||||
* [.removeLike(video_id)](#removelike)
|
||||
* [.removeRating(video_id)](#removerating)
|
||||
* [.subscribe(video_id)](#subscribe)
|
||||
* [.unsubscribe(video_id)](#unsubscribe)
|
||||
* [.comment(video_id, text)](#comment)
|
||||
@@ -19,7 +19,7 @@ Handles direct interactions.
|
||||
|
||||
Likes given video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -30,18 +30,18 @@ Likes given video.
|
||||
|
||||
Dislikes given video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | Video id |
|
||||
|
||||
<a name="removelike"></a>
|
||||
<a name="removerating"></a>
|
||||
### removeLike(video_id)
|
||||
|
||||
Remover like/dislike.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -52,7 +52,7 @@ Remover like/dislike.
|
||||
|
||||
Subscribes to given channel.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -63,7 +63,7 @@ Subscribes to given channel.
|
||||
|
||||
Unsubscribes from given channel.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -74,7 +74,7 @@ Unsubscribes from given channel.
|
||||
|
||||
Posts a comment on given video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -86,7 +86,7 @@ Posts a comment on given video.
|
||||
|
||||
Translates given text using YouTube's comment translation feature.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -100,7 +100,7 @@ Translates given text using YouTube's comment translation feature.
|
||||
Changes notification preferences for a given channel.
|
||||
Only works with channels you are subscribed to.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
||||
113
docs/API/kids.md
Normal file
113
docs/API/kids.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# YouTube Kids
|
||||
|
||||
YouTube Kids is a modified version of the YouTube app, with a simplified interface and curated content. This class allows you to interact with its API.
|
||||
|
||||
## API
|
||||
|
||||
* Kids
|
||||
* [.search(query)](#search)
|
||||
* [.getInfo(video_id)](#getinfo)
|
||||
* [.getChannel(channel_id)](#getchannel)
|
||||
* [.getHomeFeed()](#gethomefeed)
|
||||
|
||||
<a name="search"></a>
|
||||
### search(query)
|
||||
|
||||
Searches the given query on YouTube Kids.
|
||||
|
||||
**Returns:** `Promise.<Search>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| query | `string` | The query to search |
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
|
||||
- `<search>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="getinfo"></a>
|
||||
### getInfo(video_id)
|
||||
|
||||
Retrieves video info.
|
||||
|
||||
**Returns:** `Promise.<VideoInfo>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The video id |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
|
||||
- `<info>#toDash(url_transformer?, format_filter?)`
|
||||
- Generates a DASH manifest from the streaming data.
|
||||
|
||||
- `<info>#chooseFormat(options)`
|
||||
- Selects the format that best matches the given options. This method is used internally by `#download`.
|
||||
|
||||
- `<info>#download(options?)`
|
||||
- Downloads the video.
|
||||
|
||||
- `<info>#addToWatchHistory()`
|
||||
- Adds the video to the watch history.
|
||||
|
||||
- `<info>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="getchannel"></a>
|
||||
### getChannel(channel_id)
|
||||
|
||||
Retrieves channel info.
|
||||
|
||||
**Returns:** `Promise.<Channel>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| channel_id | `string` | The channel id |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
|
||||
- `<channel>#getContinuation()`
|
||||
- Retrieves next batch of videos.
|
||||
|
||||
- `<channel>#has_continuation`
|
||||
- Returns whether there are more videos to retrieve.
|
||||
|
||||
- `<channel>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<a name="gethomefeed"></a>
|
||||
### getHomeFeed()
|
||||
|
||||
Retrieves the home feed.
|
||||
|
||||
**Returns:** `Promise.<HomeFeed>`
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
<p>
|
||||
|
||||
- `<feed>#selectCategoryTab(tab: string | KidsCategoryTab)`
|
||||
- Selects the given category tab.
|
||||
|
||||
- `<feed>#categories`
|
||||
- Returns available categories.
|
||||
|
||||
- `<feed>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Music
|
||||
# YouTube Music
|
||||
|
||||
YouTube Music class.
|
||||
YouTube Music is a music streaming service developed by YouTube, a subsidiary of Google. It provides a tailored interface for the service oriented towards music streaming, with a greater emphasis on browsing and discovery compared to its main service. This class allows you to interact with its API.
|
||||
|
||||
## API
|
||||
|
||||
@@ -49,6 +49,21 @@ Retrieves track info.
|
||||
- `<info>#available_tabs`
|
||||
- Returns available tabs.
|
||||
|
||||
- `<info>#toDash(url_transformer?, format_filter?)`
|
||||
- Generates a DASH manifest from the streaming data.
|
||||
|
||||
- `<info>#chooseFormat(options)`
|
||||
- Selects the format that best matches the given options. This method is used internally by `#download`.
|
||||
|
||||
- `<info>#download(options?)`
|
||||
- Downloads the track.
|
||||
|
||||
- `<info>#addToWatchHistory()`
|
||||
- Adds the song to the watch history.
|
||||
|
||||
- `<info>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -62,7 +77,16 @@ Searches on YouTube Music.
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| query | `string` | Search query |
|
||||
| filters? | `object` | Search filters |
|
||||
| filters? | `MusicSearchFilters` | Search filters |
|
||||
|
||||
<details>
|
||||
<summary>Search Filters</summary>
|
||||
|
||||
| Filter | Type | Value | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| type | `string` | `all`, `song`, `video`, `album`, `playlist`, `artist` | Search type |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -99,7 +123,7 @@ Searches on YouTube Music.
|
||||
- Returns songs shelf.
|
||||
|
||||
- `<search>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -124,6 +148,9 @@ Retrieves home feed.
|
||||
- `<homefeed>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
|
||||
- `<homefeed>#page`
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -139,7 +166,7 @@ Retrieves “Explore” feed.
|
||||
<p>
|
||||
|
||||
- `<explore>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -158,8 +185,8 @@ Retrieves library.
|
||||
- `<library>#applyFilter(filter)`
|
||||
- Applies given filter to the library.
|
||||
|
||||
- `<library>#applySortFilter(filter)`
|
||||
- Applies given sort filter to the library items.
|
||||
- `<library>#applySort(sort_by)`
|
||||
- Applies given sort option to the library items.
|
||||
|
||||
- `<library>#getContinuation()`
|
||||
- Retrieves continuation of the library items.
|
||||
@@ -170,11 +197,11 @@ Retrieves library.
|
||||
- `<library>#filters`
|
||||
- Returns available filters.
|
||||
|
||||
- `<library>#sort_filters`
|
||||
- Returns available sort filters.
|
||||
- `<library>#sort_options`
|
||||
- Returns available sort options.
|
||||
|
||||
- `<library>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -195,7 +222,7 @@ Retrieves artist's info & content.
|
||||
<p>
|
||||
|
||||
- `<artist>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -216,7 +243,7 @@ Retrieves given album.
|
||||
<p>
|
||||
|
||||
- `<album>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -249,7 +276,7 @@ Retrieves given playlist.
|
||||
- Checks if continuation is available.
|
||||
|
||||
- `<playlist>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -303,7 +330,7 @@ Retrieves your YouTube Music recap.
|
||||
- Retrieves recap playlist.
|
||||
|
||||
- `<recap>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
- Returns the original InnerTube response(s), parsed and sanitized.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -16,7 +16,7 @@ Playlist management class.
|
||||
|
||||
Creates a playlist.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -28,7 +28,7 @@ Creates a playlist.
|
||||
|
||||
Deletes given playlist.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -14,7 +14,7 @@ YouTube Studio class (WIP).
|
||||
|
||||
Uploads a custom thumbnail and sets it for a video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -26,7 +26,7 @@ Uploads a custom thumbnail and sets it for a video.
|
||||
|
||||
Updates given video's metadata.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -38,7 +38,7 @@ Updates given video's metadata.
|
||||
|
||||
Uploads a video to YouTube.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
||||
62
docs/API/tabbed-feed.md
Normal file
62
docs/API/tabbed-feed.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# TabbedFeed
|
||||
|
||||
Represents a feed with tabs.
|
||||
|
||||
> **Note**
|
||||
> This class extends the [Feed](feed.md) class.
|
||||
|
||||
## API
|
||||
|
||||
* TabbedFeed
|
||||
* [.tabs](#tabs)
|
||||
* [.getTabByName(title: string)](#gettabbyname)
|
||||
* [.getTabByURL(url: string)](#gettabbyurl)
|
||||
* [.hasTabWithURL(url: string)](#hastabwithurl)
|
||||
* [.title](#title)
|
||||
|
||||
<a name="tabs"></a>
|
||||
### tabs
|
||||
|
||||
Returns the feed's tabs as an array of strings.
|
||||
|
||||
**Returns:** `string[]`
|
||||
|
||||
<a name="gettabbyname"></a>
|
||||
### getTabByName(title: string)
|
||||
|
||||
Fetches a tab by its title.
|
||||
|
||||
**Returns:** `Promise<TabbedFeed<T>>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| title | `string` | The title of the tab to get |
|
||||
|
||||
<a name="gettabbyurl"></a>
|
||||
### getTabByURL(url: string)
|
||||
|
||||
Fetches a tab by its URL.
|
||||
|
||||
**Returns:** `Promise<TabbedFeed<T>>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| url | `string` | The URL of the tab to get |
|
||||
|
||||
<a name="hastabwithurl"></a>
|
||||
### hasTabWithURL(url: string)
|
||||
|
||||
Returns whether the feed has a tab with the given URL.
|
||||
|
||||
**Returns:** `boolean`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| url | `string` | The URL to check |
|
||||
|
||||
<a name="title"></a>
|
||||
### title
|
||||
|
||||
Returns the currently selected tab's title.
|
||||
|
||||
**Returns:** `string | undefined`
|
||||
@@ -1,42 +1,47 @@
|
||||
# Updating the parser
|
||||
# Updating the Parser
|
||||
|
||||
YouTube is constantly changing, so it is not rare to see YouTube crawlers/scrapers breaking every now and then.
|
||||
YouTube is constantly changing, so it is not uncommon to see YouTube crawlers/scrapers breaking every now and then.
|
||||
|
||||
Our parser, on the other hand, was written so that it behaves similarly to an official client, parsing and mapping renderers (also known as YTNodes) dynamically without hard-coding their path in the response. This way, whenever a new renderer pops up (e.g., when YouTube adds a new feature or makes a minor UI change), the library will print a warning similar to this:
|
||||
|
||||
Our parser, on the other hand, was written so that it behaves similarly to an official client, parsing and mapping renderers (a.k.a YTNodes) dynamically without hard-coding their path in the response. This way, whenever a new renderer pops up (e.g; YouTube adds a new feature / minor UI changes) the library will print a warning like this in the console:
|
||||
```
|
||||
InnertubeError: SomeRenderer not found!
|
||||
SomeRenderer not found!
|
||||
This is a bug, want to help us fix it? Follow the instructions at https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md or report it at https://github.com/LuanRT/YouTube.js/issues!
|
||||
at Parser.printError (...)
|
||||
at Parser.parseItem (...)
|
||||
at Parser.parseArray (...) {
|
||||
info: {
|
||||
// renderer data, can be used as a reference to implement the renderer parser
|
||||
},
|
||||
date: 2022-05-22T22:16:06.831Z,
|
||||
version: '2.2.3'
|
||||
Introspected and JIT generated this class in the meantime:
|
||||
class SomeRenderer extends YTNode {
|
||||
static type = 'SomeRenderer';
|
||||
|
||||
// ...
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This warning, however, **does not** throw an error. The parser itself will continue working normally, even if a parsing error occurs in an existing renderer parser.
|
||||
This warning **does not** throw an error. The parser itself will continue working normally, even if a parsing error occurs in an existing renderer parser.
|
||||
|
||||
## Adding a new renderer parser
|
||||
## Adding a New Renderer Parser
|
||||
|
||||
Thanks to the modularity of the parser, a renderer can be implemented by simply adding a new file anywhere in the [classes directory](../src/parser/classes)!
|
||||
|
||||
For example, say we found a new renderer named `verticalListRenderer`, to let the parser know it exists we would have to create a file with the following structure:
|
||||
For example, suppose we have found a new renderer named `verticalListRenderer`. In that case, to let the parser know it exists at compile-time, we would have to create a file with the following structure:
|
||||
|
||||
> `../classes/VerticalList.ts`
|
||||
|
||||
```ts
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class VerticalList extends YTNode {
|
||||
static type = 'VerticalList';
|
||||
|
||||
|
||||
header;
|
||||
contents;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
// parse the data here, ex;
|
||||
this.header = Parser.parseItem(data.header);
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
@@ -46,9 +51,12 @@ class VerticalList extends YTNode {
|
||||
export default VerticalList;
|
||||
```
|
||||
|
||||
You may use the parser's generated class for the new renderer as a starting point for your own implementation.
|
||||
|
||||
Then update the parser map:
|
||||
|
||||
```bash
|
||||
npm run build:parser-map
|
||||
```
|
||||
|
||||
And that's it!
|
||||
And that's it!
|
||||
|
||||
@@ -3,7 +3,7 @@ const { Innertube, UniversalCache } = require('youtubei.js');
|
||||
(async () => {
|
||||
const yt = await Innertube.create({
|
||||
// required if you wish to use OAuth#cacheCredentials
|
||||
cache: new UniversalCache()
|
||||
cache: new UniversalCache(false)
|
||||
});
|
||||
|
||||
// 'auth-pending' is fired with the info needed to sign in via OAuth.
|
||||
@@ -17,8 +17,9 @@ const { Innertube, UniversalCache } = require('youtubei.js');
|
||||
});
|
||||
|
||||
// 'update-credentials' is fired when the access token expires, if you do not save the updated credentials any subsequent request will fail
|
||||
yt.session.on('update-credentials', ({ credentials }) => {
|
||||
yt.session.on('update-credentials', async ({ credentials }) => {
|
||||
console.log('Credentials updated:', credentials);
|
||||
await yt.session.oauth.cacheCredentials();
|
||||
});
|
||||
|
||||
// Attempt to sign in
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
YouTube.js works in the browser!
|
||||
|
||||
## How to use
|
||||
## Usage
|
||||
|
||||
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`.
|
||||
|
||||
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";
|
||||
import { Innertube } from "youtubei.js/web.bundle.min";
|
||||
|
||||
const yt = await Innertube.create({
|
||||
fetch: async (input, init) => {
|
||||
@@ -54,8 +54,10 @@ const yt = await Innertube.create({
|
||||
});
|
||||
```
|
||||
|
||||
after that you can use the library as normal.
|
||||
After that, you can use the library as normal.
|
||||
|
||||
## Example
|
||||
|
||||
We've got a full example in `examples/browser/web` using vite.
|
||||
|
||||
If you don't want to run the example yourself, you can see it in action here: [ytjsexample.pages.dev](https://ytjsexample.pages.dev/).
|
||||
@@ -1,20 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<form>
|
||||
<input type="text" name="id" placeholder="Video ID" />
|
||||
<input type="submit" value="Play" />
|
||||
</form>
|
||||
<span id="video_name">
|
||||
Library is loading...
|
||||
</span>
|
||||
<video></video>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>YouTube.js Example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form>
|
||||
<input type="text" name="id" placeholder="Video ID or URL" />
|
||||
<input type="submit" value="Play" />
|
||||
</form>
|
||||
<div id="loader"></div>
|
||||
<div id="video_container">
|
||||
<video id="video"></video>
|
||||
<h2 id="title"></h2>
|
||||
<div id="metadata"></div>
|
||||
<hr />
|
||||
<div id="description"></div>
|
||||
</div>
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/LuanRT/YouTube.js">YouTube.js</a></p>
|
||||
</footer>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -2,15 +2,24 @@ import './style.css';
|
||||
import { Innertube, UniversalCache } from '../../../../bundle/browser';
|
||||
import dashjs from 'dashjs';
|
||||
|
||||
const description = document.getElementById('description') as HTMLDivElement;
|
||||
const form = document.querySelector('form') as HTMLFormElement;
|
||||
const title = document.getElementById('title') as HTMLHeadingElement;
|
||||
const metadata = document.getElementById('metadata') as HTMLDivElement;
|
||||
const loader = document.getElementById('loader') as HTMLDivElement;
|
||||
const video = document.getElementById('video') as HTMLVideoElement;
|
||||
const video_container = document.getElementById('video_container') as HTMLDivElement;
|
||||
|
||||
async function main() {
|
||||
const yt = await Innertube.create({
|
||||
generate_session_locally: true,
|
||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
// url
|
||||
const url = typeof input === 'string'
|
||||
? new URL(input)
|
||||
: input instanceof URL
|
||||
? input
|
||||
: new URL(input.url);
|
||||
? input
|
||||
: new URL(input.url);
|
||||
|
||||
// transform the url for use with our proxy
|
||||
url.searchParams.set('__host', url.host);
|
||||
@@ -20,12 +29,15 @@ async function main() {
|
||||
const headers = init?.headers
|
||||
? new Headers(init.headers)
|
||||
: input instanceof Request
|
||||
? input.headers
|
||||
: new Headers();
|
||||
? input.headers
|
||||
: new Headers();
|
||||
|
||||
// now serialize the headers
|
||||
url.searchParams.set('__headers', JSON.stringify([...headers]));
|
||||
|
||||
// @ts-ignore
|
||||
input.duplex = 'half';
|
||||
|
||||
// copy over the request
|
||||
const request = new Request(
|
||||
url,
|
||||
@@ -42,58 +54,105 @@ async function main() {
|
||||
headers
|
||||
});
|
||||
},
|
||||
cache: new UniversalCache(),
|
||||
cache: new UniversalCache(false),
|
||||
});
|
||||
|
||||
const span = document.getElementById('video_name') as HTMLSpanElement;
|
||||
const form = document.querySelector('form') as HTMLFormElement;
|
||||
form.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' });
|
||||
form.style.display = 'block';
|
||||
|
||||
span.textContent = 'Library ready';
|
||||
showUI(false);
|
||||
|
||||
let player: dashjs.MediaPlayerClass | undefined;
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
span.textContent = 'Loading...';
|
||||
if (player) {
|
||||
player.reset();
|
||||
}
|
||||
|
||||
const video_id = document.querySelector<HTMLInputElement>(
|
||||
'input[type=text]',
|
||||
)?.value;
|
||||
if (!video_id) {
|
||||
span.textContent = 'No video id';
|
||||
hideUI();
|
||||
|
||||
let video_id;
|
||||
|
||||
const video_id_or_url = document.querySelector<HTMLInputElement>('input[type=text]')?.value;
|
||||
|
||||
if (!video_id_or_url) {
|
||||
title.textContent = 'No video id or URL provided';
|
||||
showUI(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const video = await yt.getInfo(video_id);
|
||||
if (video_id_or_url.match(/(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/)) {
|
||||
const endpoint = await yt.resolveURL(video_id_or_url);
|
||||
|
||||
console.log(video);
|
||||
span.textContent = video.basic_info.title || null;
|
||||
if (!endpoint.payload.videoId) {
|
||||
title.textContent = 'Could not resolve URL';
|
||||
showUI(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const dash = video.toDash((url) => {
|
||||
video_id = endpoint.payload.videoId;
|
||||
} else {
|
||||
video_id = video_id_or_url;
|
||||
}
|
||||
|
||||
const info = await yt.getInfo(video_id);
|
||||
|
||||
title.textContent = info.basic_info.title || null;
|
||||
description.innerHTML = info.secondary_info?.description.toHTML() || '';
|
||||
title.textContent = info.basic_info.title || null;
|
||||
|
||||
document.title = info.basic_info.title || '';
|
||||
|
||||
metadata!.innerHTML = '';
|
||||
metadata!.innerHTML += `<div class="metadata_item">${info.primary_info?.published.toHTML()}</div>`;
|
||||
metadata!.innerHTML += `<div class="metadata_item">${info.primary_info?.view_count.toHTML()}</div>`;
|
||||
metadata!.innerHTML += `<div class="metadata_item">${info.basic_info.like_count} likes</div>`;
|
||||
|
||||
showUI(true);
|
||||
|
||||
const dash = await info.toDash((url) => {
|
||||
url.searchParams.set('__host', url.host);
|
||||
url.host = 'localhost:8080';
|
||||
url.protocol = 'http';
|
||||
return url;
|
||||
});
|
||||
|
||||
const uri = 'data:application/dash+xml;charset=utf-8;base64,' +
|
||||
btoa(dash);
|
||||
const uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash);
|
||||
|
||||
// create and append video element
|
||||
const video_element = document.querySelector('video') as HTMLVideoElement;
|
||||
video_element.setAttribute('controls', 'true');
|
||||
video_element.poster = info.basic_info.thumbnail![0].url;
|
||||
|
||||
// use dash.js to parse the manifest
|
||||
if (player) {
|
||||
player.destroy();
|
||||
}
|
||||
|
||||
player = dashjs.MediaPlayer().create();
|
||||
player.initialize(video_element, uri, true);
|
||||
player.setInitialMediaSettingsFor('audio', { lang: 'en-US' });
|
||||
} catch (error) {
|
||||
span.textContent = 'An error occurred (see console)';
|
||||
title.textContent = 'An error occurred (see console)';
|
||||
showUI(false);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
function showUI(with_video = true) {
|
||||
loader.style.display = 'none';
|
||||
video.style.display = with_video ? 'block' : 'none';
|
||||
video_container.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' });
|
||||
video_container.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideUI() {
|
||||
video_container.style.display = 'none';
|
||||
loader.style.display = 'block';
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -3,10 +3,88 @@ body {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background-color: rgb(32, 32, 32);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
background-color: rgb(68, 68, 68);
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0.5rem 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: block;
|
||||
border: 10px solid rgb(68, 68, 68);
|
||||
border-top: 10px solid rgb(255, 255, 255);
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
align-self: center;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#video_container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 70vw !important;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
#metadata {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-self: left;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
#metadata > .metadata_item {
|
||||
margin: 0 0.3rem;
|
||||
background-color: beige;
|
||||
color: black;
|
||||
font: 1em bold;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
#video_container > #description {
|
||||
align-self: left;
|
||||
margin-left: 0.5rem;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: calc(100vw - 1rem);
|
||||
width: fit-content;
|
||||
max-height: calc(90vh - 12rem);
|
||||
width: 100%;
|
||||
height: 40vw;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
video {
|
||||
height: auto;
|
||||
}
|
||||
#video_container {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
|
||||
|
||||
const channel = await yt.getChannel('UCX6OQ3DkcsbYNE6H8uQQuVA');
|
||||
|
||||
@@ -14,28 +14,34 @@ import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';
|
||||
|
||||
console.info('Country:', about.country.toString());
|
||||
|
||||
console.info('\nLists the following videos:');
|
||||
console.info('\nVideos:');
|
||||
const videos = await channel.getVideos();
|
||||
|
||||
for (const video of videos.videos) {
|
||||
console.info('Video:', video.title.toString());
|
||||
}
|
||||
|
||||
console.info('\nLists the following playlists:');
|
||||
console.info('\nPopular videos:');
|
||||
const popular_videos = await videos.applyFilter('Popular');
|
||||
for (const video of popular_videos.videos) {
|
||||
console.info('Video:', video.title.toString());
|
||||
}
|
||||
|
||||
console.info('\nPlaylists:');
|
||||
const playlists = await channel.getPlaylists();
|
||||
|
||||
for (const playlist of playlists.playlists) {
|
||||
console.info('Playlist:', playlist.title.toString());
|
||||
}
|
||||
|
||||
console.info('\nLists the following channels:');
|
||||
console.info('\nChannels:');
|
||||
const channels = await channel.getChannels();
|
||||
|
||||
for (const channel of channels.channels) {
|
||||
console.info('Channel:', channel.author.name);
|
||||
}
|
||||
|
||||
console.info('\nLists the following community posts:');
|
||||
console.info('\nCommunity posts:');
|
||||
const posts = await channel.getCommunity();
|
||||
|
||||
for (const post of posts.posts) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
## Comment
|
||||
Contains information about a single comment. A [`Comment`](../../lib/parser/contents/classes/Comment.js) can be a top-level comment or a reply to a top-level comment.
|
||||
Contains information about a single comment. A [`Comment`](../../src/parser/classes/comments/Comment.ts) can be a top-level comment or a reply to a top-level comment.
|
||||
|
||||
## API
|
||||
|
||||
|
||||
@@ -9,27 +9,42 @@ A `CommentThread` represents a top-level comment and its replies.
|
||||
* [.replies](#replies) ⇒ `Comment[]`
|
||||
* [.getReplies](#getreplies) ⇒ `function`
|
||||
* [.getContinuation](#getcontinuation) ⇒ `function`
|
||||
* [.has_continuation](#hascontinuation) ⇒ `boolean`
|
||||
* [.has_replies](#hasreplies) ⇒ `boolean`
|
||||
|
||||
<a name="comment"></a>
|
||||
### comment
|
||||
The top-level comment. **Note:** More about `Comment` [here](./Comment.md).
|
||||
|
||||
**Type:** [`Comment`](../../lib/parser/contents/classes/Comment.js)
|
||||
**Type:** [`Comment`](../../src/parser/classes/comments/Comment.ts)
|
||||
|
||||
<a name="replies"></a>
|
||||
### replies
|
||||
An array of replies to the top-level comment. (not populated until [`getReplies()`](#getreplies) is called).
|
||||
|
||||
**Type:** [`Comment[]`](../../lib/parser/contents/classes/Comment.js)
|
||||
**Type:** [`Comment[]`](../../src/parser/classes/comments/Comment.ts)
|
||||
|
||||
<a name="getreplies"></a>
|
||||
### getReplies()
|
||||
Retrieves replies to the top-level comment and attaches a [`replies`](#replies) array to the original `CommentThread` object and returns it.
|
||||
|
||||
**Returns:** [`Promise.<CommentThread>`](../../lib/parser/contents/classes/CommentThread.js)
|
||||
**Returns:** [`Promise.<CommentThread>`](../../src/parser/classes/comments/CommentThread.ts)
|
||||
|
||||
<a name="getcontinuation"></a>
|
||||
### getContinuation()
|
||||
Retrieves next batch of replies and adds them to the [`replies`](#replies) array. **Note:** [`getReplies()`](#getreplies) must be called before using this.
|
||||
|
||||
**Returns:** [`Promise.<CommentThread>`](../../lib/parser/contents/classes/CommentThread.js)
|
||||
**Returns:** [`Promise.<CommentThread>`](../../src/parser/classes/comments/CommentThread.ts)
|
||||
|
||||
<a name="hascontinuation"></a>
|
||||
### has_continuation
|
||||
Whether there are more replies to be retrieved.
|
||||
|
||||
**Type:** `boolean`
|
||||
|
||||
<a name="hasreplies"></a>
|
||||
### has_replies
|
||||
|
||||
Whether there are replies to the top-level comment.
|
||||
|
||||
**Type:** `boolean`
|
||||
@@ -1,8 +1,8 @@
|
||||
## Comments
|
||||
YouTube.js has full support for comments, including comment actions such as liking, disliking, replying etc.
|
||||
YouTube.js has full support for comments, including comment actions such as translating, liking, disliking and replying.
|
||||
|
||||
## Usage
|
||||
Get a [`Comments`](../../lib/parser/youtube/Comments.js) instance:
|
||||
Get a [`Comments`](../../src/parser/youtube/Comments.ts) instance:
|
||||
|
||||
```js
|
||||
const comments = await yt.getComments(VIDEO_ID);
|
||||
@@ -11,15 +11,27 @@ const comments = await yt.getComments(VIDEO_ID);
|
||||
## API
|
||||
* Comments
|
||||
* [.contents](#commentthread) ⇒ `CommentThread[]`
|
||||
* [.applySort](#applysort) ⇒ `function`
|
||||
* [.createComment](#createComment) ⇒ `function`
|
||||
* [.getContinuation](#getc) ⇒ `function`
|
||||
* [.has_continuation](#has_continuation) ⇒ `getter`
|
||||
* [.page](#page) ⇒ `getter`
|
||||
|
||||
<a name="commentthread"></a>
|
||||
### contents
|
||||
A list of comment threads. **Note:** More about comment threads [**here**](./CommentThread.md).
|
||||
|
||||
**Type:** [`CommentThread[]`](../../lib/parser/contents/classes/CommentThread.js)
|
||||
**Type:** [`CommentThread[]`](../../src/parser/classes/comments/CommentThread.ts)
|
||||
|
||||
<a name="applysort"></a>
|
||||
### applySort(sort)
|
||||
Applies given sort option to the comments.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| sort | `string` | Sort option. Can be `TOP_COMMENTS`, `NEWEST_FIRST` |
|
||||
|
||||
**Returns:** [`Promise.<Comments>`](../../src/parser/youtube/Comments.ts)
|
||||
|
||||
<a name="createComment"></a>
|
||||
### createComment(text)
|
||||
@@ -35,7 +47,13 @@ Creates a top-level comment.
|
||||
### getContinuation()
|
||||
Retrieves next batch of comment threads.
|
||||
|
||||
**Returns:** [`Promise.<Comments>`](../../lib/parser/youtube/Comments.ts)
|
||||
**Returns:** [`Promise.<Comments>`](../../src/parser/youtube/Comments.ts)
|
||||
|
||||
<a name="has_continuation"></a>
|
||||
### has_continuation
|
||||
Returns whether there are more comments to be fetched.
|
||||
|
||||
**Type:** `boolean`
|
||||
|
||||
<a name="page"></a>
|
||||
### page
|
||||
|
||||
@@ -1,39 +1,45 @@
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
|
||||
|
||||
const comments = await yt.getComments('a-rqu-hjobc');
|
||||
|
||||
console.info(`This video has ${comments.header?.comments_count.toString() || 'N/A'} comments.\n`);
|
||||
const comment_section = await yt.getComments('a-rqu-hjobc');
|
||||
|
||||
for (const thread of comments.contents) {
|
||||
console.info(`This video has ${comment_section.header?.comments_count.toString() || 'N/A'} comments.\n`);
|
||||
|
||||
for (const thread of comment_section.contents) {
|
||||
const comment = thread.comment;
|
||||
|
||||
|
||||
if (comment) {
|
||||
console.info(
|
||||
`${comment.is_pinned ? '[Pinned]' : ''}`,
|
||||
`${comment.is_member ? `${comment.sponsor_comment_badge?.tooltip}` : ''}`,
|
||||
`${comment.author.name} • ${comment.published}\n`,
|
||||
`${comment.content.toString()}`, '\n',
|
||||
`Likes: ${comment.vote_count.short_text}`, '\n'
|
||||
`Likes: ${comment.vote_count}`, '\n'
|
||||
);
|
||||
|
||||
if (comment.reply_count > 0) {
|
||||
|
||||
if (thread.has_replies) {
|
||||
console.info('Replies:', '\n');
|
||||
|
||||
const comment_thread = await thread.getReplies();
|
||||
|
||||
if (comment_thread.replies) {
|
||||
for (const reply of comment_thread.replies) {
|
||||
let comment_thread = await thread.getReplies();
|
||||
|
||||
while (true) {
|
||||
for (const reply of comment_thread?.replies || []) {
|
||||
console.info(
|
||||
`> ${reply.author.name} • ${reply.published}\n`,
|
||||
`${reply.content.toString()}`, '\n',
|
||||
`Likes: ${reply.vote_count.short_text}`, '\n'
|
||||
`Likes: ${reply.vote_count}`, '\n'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
comment_thread = await comment_thread.getContinuation();
|
||||
} catch { break; };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('\n');
|
||||
}
|
||||
})();
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Innertube } from '../../bundle/browser.js';
|
||||
import { Innertube } from 'https://deno.land/x/youtubei/deno.ts';
|
||||
|
||||
const yt = await Innertube.create();
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
import { readFileSync, existsSync, mkdirSync, createWriteStream } from 'fs';
|
||||
import { streamToIterable } from 'youtubei.js/dist/src/utils/Utils';
|
||||
import { Innertube, UniversalCache, Utils } from 'youtubei.js';
|
||||
import { existsSync, mkdirSync, createWriteStream } from 'fs';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
|
||||
|
||||
const search = await yt.music.search('No Copyright Background Music', { type: 'album' });
|
||||
|
||||
@@ -19,7 +18,7 @@ import { streamToIterable } from 'youtubei.js/dist/src/utils/Utils';
|
||||
|
||||
for (const song of album.contents) {
|
||||
const stream = await yt.download(song.id as string, {
|
||||
type: 'audio', // audio, video or audio+video
|
||||
type: 'audio', // audio, video or video+audio
|
||||
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'mp4' // media container format
|
||||
});
|
||||
@@ -34,7 +33,7 @@ import { streamToIterable } from 'youtubei.js/dist/src/utils/Utils';
|
||||
|
||||
const file = createWriteStream(`${dir}/${song.title?.replace(/\//g, '')}.m4a`);
|
||||
|
||||
for await (const chunk of streamToIterable(stream)) {
|
||||
for await (const chunk of Utils.streamToIterable(stream)) {
|
||||
file.write(chunk);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
## Live Chat
|
||||
|
||||
The library's Live Chat parser and poller were heavily based on YouTube's original compiled code, this makes it behave in a similar if not identical way to YouTube's Live Chat. Here you can do all sorts of funny things, ex; track messages, donations, polls, and much more.
|
||||
Represents a livestream chat.
|
||||
|
||||
## Usage
|
||||
|
||||
Before fetching a Live Chat, you have to retrieve the target livestream's info:
|
||||
Before fetching a live chat, you have to retrieve the target livestream's info:
|
||||
|
||||
```js
|
||||
const info = await yt.getInfo('video_id');
|
||||
```
|
||||
|
||||
Then you may request a Live Chat instance:
|
||||
Then you may request a live chat instance:
|
||||
```js
|
||||
const livechat = await info.getLiveChat();
|
||||
```
|
||||
@@ -21,6 +21,7 @@ const livechat = await info.getLiveChat();
|
||||
* [.ev](#ev) ⇒ `EventEmitter`
|
||||
* [.start](#start) ⇒ `function`
|
||||
* [.stop](#stop) ⇒ `function`
|
||||
* [.applyFilter](#applyfilter) ⇒ `function`
|
||||
* [.getItemMenu](#getitemmenu) ⇒ `function`
|
||||
* [.sendMessage](#sendmessage) ⇒ `function`
|
||||
|
||||
@@ -31,6 +32,8 @@ Live Chat's EventEmitter.
|
||||
**Events:**
|
||||
|
||||
- `start`
|
||||
|
||||
Fired when the live chat is started.
|
||||
|
||||
Arguments:
|
||||
| Type | Description |
|
||||
@@ -38,18 +41,35 @@ Live Chat's EventEmitter.
|
||||
| `LiveChatContinuation` | Initial chat data, actions, info, etc. |
|
||||
|
||||
- `chat-update`
|
||||
|
||||
|
||||
Fired when a new chat action is received.
|
||||
|
||||
Arguments:
|
||||
| Type | Description |
|
||||
| --- | --- |
|
||||
| `ChatAction` | Chat Action |
|
||||
| `ChatAction` | Chat action |
|
||||
|
||||
- `metadata-update`
|
||||
|
||||
Fired when the livestream's metadata is updated.
|
||||
|
||||
Arguments:
|
||||
| Type | Description |
|
||||
| --- | --- |
|
||||
| `LiveMetadata` | LiveStream Metadata |
|
||||
| `LiveMetadata` | Livestream metadata |
|
||||
|
||||
- `error`
|
||||
|
||||
Fired when an error occurs.
|
||||
|
||||
Arguments:
|
||||
| Type | Description |
|
||||
| --- | --- |
|
||||
| `Error` | Details about the error |
|
||||
|
||||
- `end`
|
||||
|
||||
Fired when the livestream ends.
|
||||
|
||||
<a name="start"></a>
|
||||
### start()
|
||||
@@ -59,6 +79,15 @@ Starts the Live Chat.
|
||||
### stop()
|
||||
Stops the Live Chat.
|
||||
|
||||
<a name="applyfilter"></a>
|
||||
### applyFilter(filter)
|
||||
|
||||
Applies given filter to the live chat.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| filter | `string` | Can be `TOP_CHAT` or `LIVE_CHAT` |
|
||||
|
||||
<a name="getitemmenu"></a>
|
||||
### getItemMenu(item)
|
||||
Retrieves given chat item's menu.
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';
|
||||
|
||||
import { LiveChatContinuation } from 'youtubei.js/dist/src/parser';
|
||||
import { Innertube, UniversalCache, YTNodes, LiveChatContinuation } from 'youtubei.js';
|
||||
import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/LiveChat';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
|
||||
|
||||
|
||||
const search = await yt.search('Lofi girl live');
|
||||
const search = await yt.search('lofi hip hop radio - beats to relax/study to');
|
||||
const info = await yt.getInfo(search.videos[0].as(YTNodes.Video).id);
|
||||
|
||||
const livechat = info.getLiveChat();
|
||||
|
||||
livechat.on('start', (initial_data: LiveChatContinuation) => {
|
||||
/**
|
||||
* Initial info is what you see when you first open a Live Chat — this is; inital actions (pinned messages, top donations..), account's info and so on.
|
||||
* Initial info is what you see when you first open a a live chat — this is; initial actions (pinned messages, top donations..), account's info and so forth.
|
||||
*/
|
||||
console.info(`Hey ${initial_data.viewer_name || 'Guest'}, welcome to Live Chat!`);
|
||||
|
||||
console.info(`Hey ${initial_data.viewer_name || 'N/A'}, welcome to Live Chat!`);
|
||||
const pinned_action = initial_data.actions.firstOfType(YTNodes.AddBannerToLiveChatCommand);
|
||||
|
||||
if (pinned_action) {
|
||||
if (pinned_action.banner?.contents?.is(YTNodes.LiveChatTextMessage)) {
|
||||
console.info(
|
||||
'\n', 'Pinned message:\n',
|
||||
pinned_action.banner.contents.author?.name.toString(), '-', pinned_action?.banner.contents.message.toString(),
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
livechat.on('error', (error: Error) => console.info('Live chat error:', error));
|
||||
|
||||
livechat.on('end', () => console.info('This live stream has ended.'));
|
||||
|
||||
livechat.on('chat-update', (action: ChatAction) => {
|
||||
/**
|
||||
* An action represents what is being added to
|
||||
@@ -43,24 +55,42 @@ import { ChatAction, LiveMetadata } from 'youtubei.js/dist/src/parser/youtube/Li
|
||||
switch (item.type) {
|
||||
case 'LiveChatTextMessage':
|
||||
console.info(
|
||||
`${item.as(YTNodes.LiveChatTextMessage).author?.is_moderator ? '[MOD]' : ''}`,
|
||||
`${hours} - ${item.as(YTNodes.LiveChatTextMessage).author?.name.toString()}:\n` +
|
||||
`${item.as(YTNodes.LiveChatTextMessage).message.toString()}\n`
|
||||
);
|
||||
break;
|
||||
case 'LiveChatPaidMessage':
|
||||
console.info(
|
||||
`${item.as(YTNodes.LiveChatPaidMessage).author?.is_moderator ? '[MOD]' : ''}`,
|
||||
`${hours} - ${item.as(YTNodes.LiveChatPaidMessage).author.name.toString()}:\n` +
|
||||
`${item.as(YTNodes.LiveChatPaidMessage).message.toString()}\n`,
|
||||
`${item.as(YTNodes.LiveChatPaidMessage).purchase_amount}\n`
|
||||
);
|
||||
break;
|
||||
case 'LiveChatPaidSticker':
|
||||
console.info(
|
||||
`${item.as(YTNodes.LiveChatPaidSticker).author?.is_moderator ? '[MOD]' : ''}`,
|
||||
`${hours} - ${item.as(YTNodes.LiveChatPaidSticker).author.name.toString()}:\n` +
|
||||
`${item.as(YTNodes.LiveChatPaidSticker).purchase_amount}\n`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.debug(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.is(YTNodes.MarkChatItemAsDeletedAction)) {
|
||||
console.warn(`Message ${action.target_item_id} just got deleted and should be replaced with ${action.deleted_state_message.toString()}!`, '\n');
|
||||
if (action.is(YTNodes.AddBannerToLiveChatCommand)) {
|
||||
console.info('Message pinned:', action.banner?.contents);
|
||||
}
|
||||
|
||||
if (action.is(YTNodes.RemoveBannerForLiveChatCommand)) {
|
||||
console.info(`Message with action id ${action.target_action_id} was unpinned.`);
|
||||
}
|
||||
|
||||
if (action.is(YTNodes.RemoveChatItemAction)) {
|
||||
console.warn(`Message with action id ${action.target_item_id} just got deleted!`, '\n');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ console.info('Header:', header);
|
||||
// A proxy intercepts access to the actual data, allowing
|
||||
// the parser to add type safety and many utility methods
|
||||
// that make working with InnerTube much easier.
|
||||
const tab = page.contents.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
const tab = page.contents?.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
|
||||
if (!tab)
|
||||
throw new Error('Target tab not found');
|
||||
@@ -21,6 +21,6 @@ if (!tab)
|
||||
if (!tab.content)
|
||||
throw new Error('Target tab appears to be empty');
|
||||
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.array().as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
|
||||
console.info('Sections:', sections);
|
||||
@@ -5,7 +5,7 @@ const creds_path = './my_yt_creds.json';
|
||||
const creds = existsSync(creds_path) ? JSON.parse(readFileSync(creds_path).toString()) : undefined;
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(false) });
|
||||
|
||||
yt.session.on('auth-pending', (data: any) => {
|
||||
console.info(`Hello!\nOn your phone or computer, go to ${data.verification_url} and enter the code ${data.user_code}`);
|
||||
@@ -31,5 +31,5 @@ const creds = existsSync(creds_path) ? JSON.parse(readFileSync(creds_path).toStr
|
||||
privacy: 'UNLISTED'
|
||||
});
|
||||
|
||||
console.info('Done!');
|
||||
console.info('Done!', upload);
|
||||
})();
|
||||
|
||||
28
index.ts
28
index.ts
@@ -1,28 +0,0 @@
|
||||
import { getRuntime } from './src/utils/Utils';
|
||||
|
||||
// Polyfill fetch for node
|
||||
if (getRuntime() === 'node') {
|
||||
// eslint-disable-next-line
|
||||
const undici = require('undici');
|
||||
Reflect.set(globalThis, 'fetch', undici.fetch);
|
||||
Reflect.set(globalThis, 'Headers', undici.Headers);
|
||||
Reflect.set(globalThis, 'Request', undici.Request);
|
||||
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';
|
||||
|
||||
export * from './src/utils';
|
||||
export { YTNodes } from './src/parser/map';
|
||||
export { default as Parser } from './src/parser';
|
||||
export { default as Innertube } from './src/Innertube';
|
||||
export { default as Session } from './src/core/Session';
|
||||
export { default as Player } from './src/core/Player';
|
||||
export default Innertube;
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
projects: [
|
||||
{
|
||||
displayName: 'node',
|
||||
|
||||
2511
package-lock.json
generated
2511
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -1,10 +1,53 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.5.2",
|
||||
"description": "Full-featured wrapper around YouTube's private API. Supports YouTube, YouTube Music and YouTube Studio (WIP).",
|
||||
"main": "./dist/index.js",
|
||||
"browser": "./bundle/browser.js",
|
||||
"types": "./dist",
|
||||
"version": "4.1.1",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"agnostic": [
|
||||
"./dist/src/platform/lib.d.ts"
|
||||
],
|
||||
"web": [
|
||||
"./dist/src/platform/lib.d.ts"
|
||||
],
|
||||
"web.bundle": [
|
||||
"./dist/src/platform/lib.d.ts"
|
||||
],
|
||||
"web.bundle.min": [
|
||||
"./dist/src/platform/lib.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"node": {
|
||||
"import": "./dist/src/platform/node.js",
|
||||
"require": "./bundle/node.cjs"
|
||||
},
|
||||
"deno": "./dist/src/platform/deno.js",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"browser": "./dist/src/platform/web.js",
|
||||
"default": "./dist/src/platform/web.js"
|
||||
},
|
||||
"./agnostic": {
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"default": "./dist/src/platform/lib.js"
|
||||
},
|
||||
"./web": {
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"default": "./dist/src/platform/web.js"
|
||||
},
|
||||
"./web.bundle": {
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"default": "./bundle/browser.js"
|
||||
},
|
||||
"./web.bundle.min": {
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"default": "./bundle/browser.min.js"
|
||||
}
|
||||
},
|
||||
"author": "LuanRT <luan.lrt4@gmail.com> (https://github.com/LuanRT)",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
@@ -24,12 +67,14 @@
|
||||
"test": "npx jest --verbose",
|
||||
"lint": "npx eslint ./src",
|
||||
"lint:fix": "npx eslint --fix ./src",
|
||||
"build": "npm run build:parser-map && npm run build:proto && npm run bundle:browser && npm run bundle:browser:prod && npm run build:node",
|
||||
"build:node": "npx tsc",
|
||||
"bundle:browser": "npx tsc --module esnext && npx esbuild ./dist/browser.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser",
|
||||
"build": "npm run build:parser-map && npm run build:proto && npm run build:esm && npm run bundle:node && npm run bundle:browser && npm run bundle:browser:prod",
|
||||
"build:parser-map": "node ./scripts/build-parser-map.cjs",
|
||||
"build:proto": "npx pb-gen-ts --entry-path=\"src/proto\" --out-dir=\"src/proto/generated\" --ext-in-import=\".js\"",
|
||||
"build:esm": "npx tsc",
|
||||
"build:deno": "npx cpy ./src ./deno && npx cpy ./package.json ./deno && npx replace \".js';\" \".ts';\" ./deno -r && npx replace '.js\";' '.ts\";' ./deno -r && npx replace \"'linkedom';\" \"'https://esm.sh/linkedom';\" ./deno -r && npx replace \"'jintr';\" \"'https://esm.sh/jintr';\" ./deno -r && npx replace \"new Jinter.default\" \"new Jinter\" ./deno -r",
|
||||
"bundle:node": "npx esbuild ./dist/src/platform/node.js --bundle --target=node10 --keep-names --format=cjs --platform=node --outfile=./bundle/node.cjs --external:jintr --external:undici --external:linkedom --sourcemap --banner:js=\"/* eslint-disable */\"",
|
||||
"bundle:browser": "npx esbuild ./dist/src/platform/web.js --banner:js=\"/* eslint-disable */\" --bundle --target=chrome58 --keep-names --format=esm --sourcemap --define:global=globalThis --outfile=./bundle/browser.js --platform=browser",
|
||||
"bundle:browser:prod": "npm run bundle:browser -- --outfile=./bundle/browser.min.js --minify",
|
||||
"build:parser-map": "node ./scripts/build-parser-map.js",
|
||||
"build:proto": "npx protoc --ts_out ./src/proto --proto_path ./src/proto ./src/proto/youtube.proto",
|
||||
"prepare": "npm run build",
|
||||
"watch": "npx tsc --watch"
|
||||
},
|
||||
@@ -39,24 +84,25 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.7.0",
|
||||
"jintr": "^0.3.1",
|
||||
"jintr": "^0.4.1",
|
||||
"linkedom": "^0.14.12",
|
||||
"undici": "^5.7.0"
|
||||
"undici": "^5.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"cpy-cli": "^4.2.0",
|
||||
"esbuild": "^0.14.49",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
"glob": "^8.0.3",
|
||||
"jest": "^28.1.3",
|
||||
"pbkit": "^0.0.59",
|
||||
"replace": "^1.2.2",
|
||||
"ts-jest": "^28.0.8",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/LuanRT/YouTube.js/issues"
|
||||
@@ -72,7 +118,6 @@
|
||||
"youtube-downloader",
|
||||
"youtube-music",
|
||||
"youtube-studio",
|
||||
"innertubeapi",
|
||||
"innertube",
|
||||
"unofficial",
|
||||
"downloader",
|
||||
@@ -81,7 +126,6 @@
|
||||
"upload",
|
||||
"ytmusic",
|
||||
"search",
|
||||
"comment",
|
||||
"music",
|
||||
"api"
|
||||
]
|
||||
|
||||
39
scripts/build-parser-map.cjs
Normal file
39
scripts/build-parser-map.cjs
Normal file
@@ -0,0 +1,39 @@
|
||||
const glob = require('glob');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const import_list = [];
|
||||
const misc_imports = [];
|
||||
|
||||
glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname })
|
||||
.forEach((file) => {
|
||||
// Trim path
|
||||
const is_misc = file.includes('/misc/');
|
||||
file = file.replace('../src/parser/classes/', '').replace('.js', '').replace('.ts', '');
|
||||
const import_name = file.split('/').pop();
|
||||
|
||||
if (is_misc) {
|
||||
const class_name = file.split('/').pop().replace('.js', '').replace('.ts', '');
|
||||
misc_imports.push(`export { default as ${class_name} } from './classes/${file}.js';`);
|
||||
} else {
|
||||
import_list.push(`export { default as ${import_name} } from './classes/${file}.js';`);
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../src/parser/nodes.ts'),
|
||||
`// This file was auto generated, do not edit.
|
||||
// See ./scripts/build-parser-map.js
|
||||
|
||||
${import_list.join('\n')}
|
||||
`
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../src/parser/misc.ts'),
|
||||
`// This file was auto generated, do not edit.
|
||||
// See ./scripts/build-parser-map.js
|
||||
|
||||
${misc_imports.join('\n')}
|
||||
`
|
||||
);
|
||||
@@ -1,48 +0,0 @@
|
||||
const glob = require('glob');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const import_list = [];
|
||||
|
||||
const json = [];
|
||||
|
||||
glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname })
|
||||
.forEach((file) => {
|
||||
if (file.includes('/misc/')) return;
|
||||
// Trim path
|
||||
file = file.replace('../src/parser/classes/', '').replace('.js', '').replace('.ts', '');
|
||||
const import_name = file.split('/').pop();
|
||||
import_list.push(`import { default as ${import_name} } from './classes/${file}';`);
|
||||
json.push(import_name);
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../src/parser/map.ts'),
|
||||
`// This file was auto generated, do not edit.
|
||||
// See ./scripts/build-parser-map.js
|
||||
import { YTNodeConstructor } from './helpers';
|
||||
|
||||
${import_list.join('\n')}
|
||||
|
||||
export const YTNodes = {
|
||||
${json.join(',\n ')}
|
||||
};
|
||||
|
||||
const map: Record<string, YTNodeConstructor> = YTNodes;
|
||||
|
||||
/**
|
||||
* @param name - Name of the node to be parsed
|
||||
*/
|
||||
export default function GetParserByName(name: string) {
|
||||
const ParserConstructor = map[name];
|
||||
|
||||
if (!ParserConstructor) {
|
||||
const error = new Error(\`Module not found: \${name}\`);
|
||||
(error as any).code = 'MODULE_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
return ParserConstructor;
|
||||
}
|
||||
`
|
||||
);
|
||||
52
scripts/get-agents.js
Normal file
52
scripts/get-agents.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { fetch } from 'undici';
|
||||
import { gunzip } from 'zlib';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { writeFile } from 'fs/promises';
|
||||
|
||||
(async () => {
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const buf = await (await fetch('https://github.com/intoli/user-agents/blob/master/src/user-agents.json.gz?raw=true')).arrayBuffer();
|
||||
const bytes = new Uint8Array(buf);
|
||||
|
||||
// Only get desktop and mobile agents
|
||||
const allowed_agents = new Set([
|
||||
'desktop',
|
||||
'mobile'
|
||||
]);
|
||||
|
||||
const decompressed = await new Promise((resolve, reject) => {
|
||||
gunzip(bytes, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result.buffer);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const contents = new TextDecoder().decode(decompressed);
|
||||
|
||||
const agents = JSON.parse(contents);
|
||||
|
||||
if (!Array.isArray(agents)) {
|
||||
throw new Error('Invalid user-agents.json');
|
||||
}
|
||||
|
||||
const agentsByDevice = agents.reduce((acc, agent) => {
|
||||
const device = agent.deviceCategory;
|
||||
if (!allowed_agents.has(device))
|
||||
return acc;
|
||||
if (!acc[device]) {
|
||||
acc[device] = [];
|
||||
}
|
||||
// We dont want to massive of a list of agents for each device
|
||||
if (acc[device].length <= 25) acc[device].push(agent.userAgent);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
await writeFile(resolve(__dirname, '..', 'src', 'utils', 'user-agents.ts'), `/* eslint-disable */\n/* Generated file do not edit */\nexport default ${JSON.stringify(agentsByDevice, null, 2)} as { desktop: string[], mobile: string[] };`);
|
||||
|
||||
})();
|
||||
@@ -1,52 +0,0 @@
|
||||
import { fetch } from "undici";
|
||||
import { gunzip } from "zlib";
|
||||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { writeFile } from "fs/promises";
|
||||
|
||||
(async () => {
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const buf = await (await fetch('https://github.com/intoli/user-agents/blob/master/src/user-agents.json.gz?raw=true')).arrayBuffer();
|
||||
const bytes = new Uint8Array(buf);
|
||||
|
||||
// Only get desktop and mobile agents
|
||||
const allowed_agents = new Set([
|
||||
'desktop',
|
||||
'mobile',
|
||||
])
|
||||
|
||||
const decompressed = await new Promise((resolve, reject) => {
|
||||
gunzip(bytes, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result.buffer);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const contents = new TextDecoder().decode(decompressed);
|
||||
|
||||
const agents = JSON.parse(contents);
|
||||
|
||||
if (!Array.isArray(agents)) {
|
||||
throw new Error('Invalid user-agents.json');
|
||||
}
|
||||
|
||||
const agentsByDevice = agents.reduce((acc, agent) => {
|
||||
const device = agent.deviceCategory;
|
||||
if (!allowed_agents.has(device))
|
||||
return acc;
|
||||
if (!acc[device]) {
|
||||
acc[device] = [];
|
||||
}
|
||||
// we dont want to massive of a list of agents for each device
|
||||
if (acc[device].length <= 25) acc[device].push(agent.userAgent);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
await writeFile(resolve(__dirname, '..', 'src', 'utils', 'user-agents.json'), JSON.stringify(agentsByDevice, null, 2));
|
||||
|
||||
})();
|
||||
293
src/Innertube.ts
293
src/Innertube.ts
@@ -1,79 +1,115 @@
|
||||
|
||||
import Session, { SessionOptions } from './core/Session';
|
||||
import Session, { SessionOptions } from './core/Session.js';
|
||||
|
||||
import Search from './parser/youtube/Search';
|
||||
import Channel from './parser/youtube/Channel';
|
||||
import Playlist from './parser/youtube/Playlist';
|
||||
import Library from './parser/youtube/Library';
|
||||
import History from './parser/youtube/History';
|
||||
import Comments from './parser/youtube/Comments';
|
||||
import NotificationsMenu from './parser/youtube/NotificationsMenu';
|
||||
import VideoInfo, { DownloadOptions, FormatOptions } from './parser/youtube/VideoInfo';
|
||||
import NavigationEndpoint from './parser/classes/NavigationEndpoint';
|
||||
import NavigationEndpoint from './parser/classes/NavigationEndpoint.js';
|
||||
import Channel from './parser/youtube/Channel.js';
|
||||
import Comments from './parser/youtube/Comments.js';
|
||||
import History from './parser/youtube/History.js';
|
||||
import Library from './parser/youtube/Library.js';
|
||||
import NotificationsMenu from './parser/youtube/NotificationsMenu.js';
|
||||
import Playlist from './parser/youtube/Playlist.js';
|
||||
import Search from './parser/youtube/Search.js';
|
||||
import VideoInfo from './parser/youtube/VideoInfo.js';
|
||||
import HashtagFeed from './parser/youtube/HashtagFeed.js';
|
||||
|
||||
import { ParsedResponse } from './parser';
|
||||
import { ActionsResponse } from './core/Actions';
|
||||
import AccountManager from './core/AccountManager.js';
|
||||
import Feed from './core/Feed.js';
|
||||
import InteractionManager from './core/InteractionManager.js';
|
||||
import YTKids from './core/Kids.js';
|
||||
import YTMusic from './core/Music.js';
|
||||
import PlaylistManager from './core/PlaylistManager.js';
|
||||
import YTStudio from './core/Studio.js';
|
||||
import TabbedFeed from './core/TabbedFeed.js';
|
||||
import HomeFeed from './parser/youtube/HomeFeed.js';
|
||||
import Guide from './parser/youtube/Guide.js';
|
||||
import Proto from './proto/index.js';
|
||||
import Constants from './utils/Constants.js';
|
||||
|
||||
import Feed from './core/Feed';
|
||||
import YTMusic from './core/Music';
|
||||
import Studio from './core/Studio';
|
||||
import HomeFeed from './parser/youtube/HomeFeed';
|
||||
import AccountManager from './core/AccountManager';
|
||||
import PlaylistManager from './core/PlaylistManager';
|
||||
import InteractionManager from './core/InteractionManager';
|
||||
import TabbedFeed from './core/TabbedFeed';
|
||||
import Constants from './utils/Constants';
|
||||
import Proto from './proto/index';
|
||||
import type Actions from './core/Actions.js';
|
||||
import type Format from './parser/classes/misc/Format.js';
|
||||
|
||||
import { throwIfMissing, generateRandomString } from './utils/Utils';
|
||||
import type { ApiResponse } from './core/Actions.js';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
|
||||
import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.js';
|
||||
import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.js';
|
||||
|
||||
export type InnertubeConfig = SessionOptions;
|
||||
|
||||
export interface SearchFilters {
|
||||
upload_date?: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year',
|
||||
type?: 'all' | 'video' | 'channel' | 'playlist' | 'movie',
|
||||
duration?: 'all' | 'short' | 'medium' | 'long',
|
||||
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count'
|
||||
upload_date?: 'all' | 'hour' | 'today' | 'week' | 'month' | 'year';
|
||||
type?: 'all' | 'video' | 'channel' | 'playlist' | 'movie';
|
||||
duration?: 'all' | 'short' | 'medium' | 'long';
|
||||
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
|
||||
features?: ('hd' | 'subtitles' | 'creative_commons' | '3d' | 'live' | 'purchased' | '4k' | '360' | 'location' | 'hdr' | 'vr180')[];
|
||||
}
|
||||
|
||||
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'TV_EMBEDDED';
|
||||
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'YTSTUDIO_ANDROID' | 'TV_EMBEDDED' | 'YTKIDS'
|
||||
|
||||
class Innertube {
|
||||
session;
|
||||
account;
|
||||
playlist;
|
||||
interact;
|
||||
music;
|
||||
studio;
|
||||
actions;
|
||||
/**
|
||||
* Provides access to various services and modules in the YouTube API.
|
||||
*/
|
||||
export default class Innertube {
|
||||
#session: Session;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.session = session;
|
||||
this.account = new AccountManager(this.session.actions);
|
||||
this.playlist = new PlaylistManager(this.session.actions);
|
||||
this.interact = new InteractionManager(this.session.actions);
|
||||
this.music = new YTMusic(this.session);
|
||||
this.studio = new Studio(this.session);
|
||||
this.actions = this.session.actions;
|
||||
this.#session = session;
|
||||
}
|
||||
|
||||
static async create(config: InnertubeConfig = {}) {
|
||||
static async create(config: InnertubeConfig = {}): Promise<Innertube> {
|
||||
return new Innertube(await Session.create(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves video info.
|
||||
* @param video_id - The video id.
|
||||
* @param target - The video id or `NavigationEndpoint`.
|
||||
* @param client - The client to use.
|
||||
*/
|
||||
async getInfo(video_id: string, client?: InnerTubeClient) {
|
||||
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ target });
|
||||
|
||||
let payload: {
|
||||
videoId: string,
|
||||
playlistId?: string,
|
||||
params?: string,
|
||||
playlistIndex?: number
|
||||
};
|
||||
|
||||
if (target instanceof NavigationEndpoint) {
|
||||
const video_id = target.payload?.videoId;
|
||||
|
||||
if (!video_id)
|
||||
throw new InnertubeError('Missing video id in endpoint payload.', target);
|
||||
|
||||
payload = {
|
||||
videoId: video_id
|
||||
};
|
||||
|
||||
if (target.payload.playlistId) {
|
||||
payload.playlistId = target.payload.playlistId;
|
||||
}
|
||||
|
||||
if (target.payload.params) {
|
||||
payload.params = target.payload.params;
|
||||
}
|
||||
|
||||
if (target.payload.index) {
|
||||
payload.playlistIndex = target.payload.index;
|
||||
}
|
||||
} else if (typeof target === 'string') {
|
||||
payload = {
|
||||
videoId: target
|
||||
};
|
||||
} else {
|
||||
throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target);
|
||||
}
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.actions.getVideoInfo(video_id, cpn, client);
|
||||
const continuation = this.actions.execute('/next', { videoId: video_id });
|
||||
const initial_info = this.actions.getVideoInfo(payload.videoId, cpn, client);
|
||||
const continuation = this.actions.execute('/next', payload);
|
||||
|
||||
const response = await Promise.all([ initial_info, continuation ]);
|
||||
return new VideoInfo(response, this.actions, this.session.player, cpn);
|
||||
return new VideoInfo(response, this.actions, cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,11 +117,13 @@ class Innertube {
|
||||
* @param video_id - The video id.
|
||||
* @param client - The client to use.
|
||||
*/
|
||||
async getBasicInfo(video_id: string, client?: InnerTubeClient) {
|
||||
async getBasicInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
const response = await this.actions.getVideoInfo(video_id, cpn, client);
|
||||
|
||||
return new VideoInfo([ response ], this.actions, this.session.player, cpn);
|
||||
return new VideoInfo([ response ], this.actions, cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +131,7 @@ class Innertube {
|
||||
* @param query - The search query.
|
||||
* @param filters - Search filters.
|
||||
*/
|
||||
async search(query: string, filters: SearchFilters = {}) {
|
||||
async search(query: string, filters: SearchFilters = {}): Promise<Search> {
|
||||
throwIfMissing({ query });
|
||||
|
||||
const args = {
|
||||
@@ -105,7 +143,7 @@ class Innertube {
|
||||
|
||||
const response = await this.actions.execute('/search', args);
|
||||
|
||||
return new Search(this.actions, response.data);
|
||||
return new Search(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,14 +155,14 @@ class Innertube {
|
||||
|
||||
const url = new URL(`${Constants.URLS.YT_SUGGESTIONS}search`);
|
||||
url.searchParams.set('q', query);
|
||||
url.searchParams.set('hl', this.session.context.client.hl);
|
||||
url.searchParams.set('gl', this.session.context.client.gl);
|
||||
url.searchParams.set('hl', this.#session.context.client.hl);
|
||||
url.searchParams.set('gl', this.#session.context.client.gl);
|
||||
url.searchParams.set('ds', 'yt');
|
||||
url.searchParams.set('client', 'youtube');
|
||||
url.searchParams.set('xssi', 't');
|
||||
url.searchParams.set('oe', 'UTF');
|
||||
|
||||
const response = await this.session.http.fetch(url);
|
||||
const response = await this.#session.http.fetch(url);
|
||||
const response_data = await response.text();
|
||||
|
||||
const data = JSON.parse(response_data.replace(')]}\'', ''));
|
||||
@@ -138,7 +176,7 @@ class Innertube {
|
||||
* @param video_id - The video id.
|
||||
* @param sort_by - Sorting options.
|
||||
*/
|
||||
async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST') {
|
||||
async getComments(video_id: string, sort_by?: 'TOP_COMMENTS' | 'NEWEST_FIRST'): Promise<Comments> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const payload = Proto.encodeCommentsSectionParams(video_id, {
|
||||
@@ -153,58 +191,66 @@ class Innertube {
|
||||
/**
|
||||
* Retrieves YouTube's home feed (aka recommendations).
|
||||
*/
|
||||
async getHomeFeed() {
|
||||
async getHomeFeed(): Promise<HomeFeed> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEwhat_to_watch' });
|
||||
return new HomeFeed(this.actions, response.data);
|
||||
return new HomeFeed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves YouTube's content guide.
|
||||
*/
|
||||
async getGuide(): Promise<Guide> {
|
||||
const response = await this.actions.execute('/guide');
|
||||
return new Guide(response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the account's library.
|
||||
*/
|
||||
async getLibrary() {
|
||||
async getLibrary(): Promise<Library> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FElibrary' });
|
||||
return new Library(response.data, this.actions);
|
||||
return new Library(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves watch history.
|
||||
* Which can also be achieved with {@link getLibrary}.
|
||||
*/
|
||||
async getHistory() {
|
||||
async getHistory(): Promise<History> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEhistory' });
|
||||
return new History(this.actions, response.data);
|
||||
return new History(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves trending content.
|
||||
*/
|
||||
async getTrending() {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEtrending' });
|
||||
return new TabbedFeed(this.actions, response.data);
|
||||
async getTrending(): Promise<TabbedFeed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEtrending', parse: true });
|
||||
return new TabbedFeed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves subscriptions feed.
|
||||
*/
|
||||
async getSubscriptionsFeed() {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEsubscriptions' });
|
||||
return new Feed(this.actions, response.data);
|
||||
async getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEsubscriptions', parse: true });
|
||||
return new Feed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves contents for a given channel.
|
||||
* @param id - channel id
|
||||
* @param id - Channel id
|
||||
*/
|
||||
async getChannel(id: string) {
|
||||
async getChannel(id: string): Promise<Channel> {
|
||||
throwIfMissing({ id });
|
||||
const response = await this.actions.execute('/browse', { browseId: id });
|
||||
return new Channel(this.actions, response.data);
|
||||
return new Channel(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves notifications.
|
||||
*/
|
||||
async getNotifications() {
|
||||
async getNotifications(): Promise<NotificationsMenu> {
|
||||
const response = await this.actions.execute('/notification/get_notification_menu', { notificationsMenuRequestType: 'NOTIFICATIONS_MENU_REQUEST_TYPE_INBOX' });
|
||||
return new NotificationsMenu(this.actions, response);
|
||||
}
|
||||
@@ -220,8 +266,9 @@ class Innertube {
|
||||
|
||||
/**
|
||||
* Retrieves playlist contents.
|
||||
* @param id - Playlist id
|
||||
*/
|
||||
async getPlaylist(id: string) {
|
||||
async getPlaylist(id: string): Promise<Playlist> {
|
||||
throwIfMissing({ id });
|
||||
|
||||
if (!id.startsWith('VL')) {
|
||||
@@ -229,7 +276,21 @@ class Innertube {
|
||||
}
|
||||
|
||||
const response = await this.actions.execute('/browse', { browseId: id });
|
||||
return new Playlist(this.actions, response.data);
|
||||
|
||||
return new Playlist(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a given hashtag's page.
|
||||
* @param hashtag - The hashtag to fetch.
|
||||
*/
|
||||
async getHashtag(hashtag: string): Promise<HashtagFeed> {
|
||||
throwIfMissing({ hashtag });
|
||||
|
||||
const params = Proto.encodeHashtag(hashtag);
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEhashtag', params });
|
||||
|
||||
return new HashtagFeed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,32 +298,98 @@ class Innertube {
|
||||
* Returns deciphered streaming data.
|
||||
*
|
||||
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
|
||||
* @param video_id - The video id.
|
||||
* @param options - Format options.
|
||||
*/
|
||||
async getStreamingData(video_id: string, options: FormatOptions = {}) {
|
||||
async getStreamingData(video_id: string, options: FormatOptions = {}): Promise<Format> {
|
||||
const info = await this.getBasicInfo(video_id);
|
||||
return info.chooseFormat(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a given video. If you only need the direct download link see {@link getStreamingData}.
|
||||
*
|
||||
* If you wish to retrieve the video info too, have a look at {@link getBasicInfo} or {@link getInfo}.
|
||||
* @param video_id - The video id.
|
||||
* @param options - Download options.
|
||||
*/
|
||||
async download(video_id: string, options?: DownloadOptions) {
|
||||
async download(video_id: string, options?: DownloadOptions): Promise<ReadableStream<Uint8Array>> {
|
||||
const info = await this.getBasicInfo(video_id, options?.client);
|
||||
return info.download(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given URL.
|
||||
* @param url - The URL.
|
||||
*/
|
||||
async resolveURL(url: string): Promise<NavigationEndpoint> {
|
||||
const response = await this.actions.execute('/navigation/resolve_url', { url, parse: true });
|
||||
return response.endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to call an endpoint without having to use {@link Actions}.
|
||||
* @param endpoint -The endpoint to call.
|
||||
* @param args - Call arguments.
|
||||
*/
|
||||
call(endpoint: NavigationEndpoint, args: { [ key: string ]: any; parse: true }): Promise<ParsedResponse>;
|
||||
call(endpoint: NavigationEndpoint, args?: { [ key: string ]: any; parse?: false }): Promise<ActionsResponse>;
|
||||
call(endpoint: NavigationEndpoint, args?: object): Promise<ActionsResponse | ParsedResponse> {
|
||||
call<T extends IParsedResponse>(endpoint: NavigationEndpoint, args: { [key: string]: any; parse: true }): Promise<T>;
|
||||
call(endpoint: NavigationEndpoint, args?: { [key: string]: any; parse?: false }): Promise<ApiResponse>;
|
||||
call(endpoint: NavigationEndpoint, args?: object): Promise<IParsedResponse | ApiResponse> {
|
||||
return endpoint.call(this.actions, args);
|
||||
}
|
||||
}
|
||||
|
||||
export default Innertube;
|
||||
/**
|
||||
* An instance of YTMusic for interacting with the YouTube Music service.
|
||||
*/
|
||||
get music(): YTMusic {
|
||||
return new YTMusic(this.#session);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of YTStudio for interacting with the YouTube Studio service.
|
||||
*/
|
||||
get studio(): YTStudio {
|
||||
return new YTStudio(this.#session);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of YTKids for interacting with the YouTube Kids service.
|
||||
*/
|
||||
get kids(): YTKids {
|
||||
return new YTKids(this.#session);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of AccountManager for managing a user's account.
|
||||
*/
|
||||
get account(): AccountManager {
|
||||
return new AccountManager(this.#session.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of PlaylistManager for managing playlists.
|
||||
*/
|
||||
get playlist(): PlaylistManager {
|
||||
return new PlaylistManager(this.#session.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of InteractionManager for interacting with contents in YouTube.
|
||||
*/
|
||||
get interact(): InteractionManager {
|
||||
return new InteractionManager(this.#session.actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of Actions.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#session.actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the InnerTube session instance.
|
||||
*/
|
||||
get session(): Session {
|
||||
return this.#session;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
import Proto from '../proto/index';
|
||||
import Actions from './Actions';
|
||||
import Proto from '../proto/index.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
import Analytics from '../parser/youtube/Analytics';
|
||||
import TimeWatched from '../parser/youtube/TimeWatched';
|
||||
import AccountInfo from '../parser/youtube/AccountInfo';
|
||||
import Settings from '../parser/youtube/Settings';
|
||||
import { InnertubeError } from '../utils/Utils';
|
||||
import Analytics from '../parser/youtube/Analytics.js';
|
||||
import TimeWatched from '../parser/youtube/TimeWatched.js';
|
||||
import AccountInfo from '../parser/youtube/AccountInfo.js';
|
||||
import Settings from '../parser/youtube/Settings.js';
|
||||
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
|
||||
class AccountManager {
|
||||
#actions;
|
||||
channel;
|
||||
#actions: Actions;
|
||||
|
||||
channel: {
|
||||
editName: (new_name: string) => Promise<ApiResponse>;
|
||||
editDescription: (new_description: string) => Promise<ApiResponse>;
|
||||
getBasicAnalytics: () => Promise<Analytics>;
|
||||
};
|
||||
|
||||
constructor(actions: Actions) {
|
||||
this.#actions = actions;
|
||||
@@ -51,7 +58,7 @@ class AccountManager {
|
||||
/**
|
||||
* Retrieves channel info.
|
||||
*/
|
||||
async getInfo() {
|
||||
async getInfo(): Promise<AccountInfo> {
|
||||
if (!this.#actions.session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
@@ -62,7 +69,7 @@ class AccountManager {
|
||||
/**
|
||||
* Retrieves time watched statistics.
|
||||
*/
|
||||
async getTimeWatched() {
|
||||
async getTimeWatched(): Promise<TimeWatched> {
|
||||
const response = await this.#actions.execute('/browse', {
|
||||
browseId: 'SPtime_watched',
|
||||
client: 'ANDROID'
|
||||
@@ -74,7 +81,7 @@ class AccountManager {
|
||||
/**
|
||||
* Opens YouTube settings.
|
||||
*/
|
||||
async getSettings() {
|
||||
async getSettings(): Promise<Settings> {
|
||||
const response = await this.#actions.execute('/browse', {
|
||||
browseId: 'SPaccount_overview'
|
||||
});
|
||||
@@ -85,7 +92,7 @@ class AccountManager {
|
||||
/**
|
||||
* Retrieves basic channel analytics.
|
||||
*/
|
||||
async getAnalytics() {
|
||||
async getAnalytics(): Promise<Analytics> {
|
||||
const info = await this.getInfo();
|
||||
|
||||
const params = Proto.encodeChannelAnalyticsParams(info.footers?.endpoint.payload.browseId);
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
import Session from './Session';
|
||||
import Parser, { ParsedResponse } from '../parser/index';
|
||||
import { InnertubeError } from '../utils/Utils';
|
||||
import Parser, { NavigateAction } from '../parser/index.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
|
||||
import type Session from './Session.js';
|
||||
|
||||
import type {
|
||||
IBrowseResponse, IGetNotificationsMenuResponse,
|
||||
INextResponse, IPlayerResponse, IResolveURLResponse,
|
||||
ISearchResponse, IUpdatedMetadataResponse,
|
||||
IParsedResponse, IRawResponse
|
||||
} from '../parser/types/index.js';
|
||||
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: any;
|
||||
data: IRawResponse;
|
||||
}
|
||||
|
||||
export type ActionsResponse = Promise<ApiResponse>;
|
||||
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/updated_metadata' | '/notification/get_notification_menu' | string;
|
||||
|
||||
export type ParsedResponse<T> =
|
||||
T extends '/player' ? IPlayerResponse :
|
||||
T extends '/search' ? ISearchResponse :
|
||||
T extends '/browse' ? IBrowseResponse :
|
||||
T extends '/next' ? INextResponse :
|
||||
T extends '/updated_metadata' ? IUpdatedMetadataResponse :
|
||||
T extends '/navigation/resolve_url' ? IResolveURLResponse :
|
||||
T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
|
||||
IParsedResponse;
|
||||
|
||||
class Actions {
|
||||
#session;
|
||||
#session: Session;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.#session = session;
|
||||
}
|
||||
|
||||
get session() {
|
||||
get session(): Session {
|
||||
return this.#session;
|
||||
}
|
||||
|
||||
@@ -25,7 +43,7 @@ class Actions {
|
||||
* Mimmics the Axios API using Fetch's Response object.
|
||||
* @param response - The response object.
|
||||
*/
|
||||
async #wrap(response: Response) {
|
||||
async #wrap(response: Response): Promise<ApiResponse> {
|
||||
return {
|
||||
success: response.ok,
|
||||
status_code: response.status,
|
||||
@@ -40,7 +58,7 @@ class Actions {
|
||||
* @param client - The client to use.
|
||||
* @param playlist_id - The playlist ID.
|
||||
*/
|
||||
async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string) {
|
||||
async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string): Promise<ApiResponse> {
|
||||
const data: Record<string, any> = {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
@@ -48,7 +66,7 @@ class Actions {
|
||||
splay: false,
|
||||
referer: 'https://www.youtube.com',
|
||||
currentUrl: `/watch?v=${id}`,
|
||||
autonavState: 'STATE_OFF',
|
||||
autonavState: 'STATE_NONE',
|
||||
signatureTimestamp: this.#session.player?.sts || 0,
|
||||
autoCaptionsDefaultOn: false,
|
||||
html5Preference: 'HTML5_PREF_WANTS',
|
||||
@@ -90,7 +108,7 @@ class Actions {
|
||||
* @param client - The client to use.
|
||||
* @param params - Call parameters.
|
||||
*/
|
||||
async stats(url: string, client: { client_name: string; client_version: string }, params: { [key: string]: any }) {
|
||||
async stats(url: string, client: { client_name: string; client_version: string }, params: { [key: string]: any }): Promise<Response> {
|
||||
const s_url = new URL(url);
|
||||
|
||||
s_url.searchParams.set('ver', '2');
|
||||
@@ -109,12 +127,12 @@ class Actions {
|
||||
|
||||
/**
|
||||
* Executes an API call.
|
||||
* @param action - The endpoint to call.
|
||||
* @param endpoint - The endpoint to call.
|
||||
* @param args - Call arguments
|
||||
*/
|
||||
async execute(action: string, args: { [key: string]: any; parse: true; protobuf?: false; serialized_data?: any }) : Promise<ParsedResponse>;
|
||||
async execute(action: string, args?: { [key: string]: any; parse?: false; protobuf?: true; serialized_data?: any }) : Promise<ActionsResponse>;
|
||||
async execute(action: string, args?: { [key: string]: any; parse?: boolean; protobuf?: boolean; serialized_data?: any }): Promise<ParsedResponse | ActionsResponse> {
|
||||
async execute<T extends InnertubeEndpoint>(endpoint: T, args: { [key: string]: any; parse: true; protobuf?: false; serialized_data?: any }): Promise<ParsedResponse<T>>;
|
||||
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: { [key: string]: any; parse?: false; protobuf?: true; serialized_data?: any }): Promise<ApiResponse>;
|
||||
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: { [key: string]: any; parse?: boolean; protobuf?: boolean; serialized_data?: any }): Promise<ParsedResponse<T> | ApiResponse> {
|
||||
let data;
|
||||
|
||||
if (args && !args.protobuf) {
|
||||
@@ -162,9 +180,9 @@ class Actions {
|
||||
data = args.serialized_data;
|
||||
}
|
||||
|
||||
const endpoint = Reflect.has(args || {}, 'override_endpoint') ? args?.override_endpoint : action;
|
||||
const target_endpoint = Reflect.has(args || {}, 'override_endpoint') ? args?.override_endpoint : endpoint;
|
||||
|
||||
const response = await this.#session.http.fetch(endpoint, {
|
||||
const response = await this.#session.http.fetch(target_endpoint, {
|
||||
method: 'POST',
|
||||
body: args?.protobuf ? data : JSON.stringify((data || {})),
|
||||
headers: {
|
||||
@@ -175,12 +193,26 @@ class Actions {
|
||||
});
|
||||
|
||||
if (args?.parse) {
|
||||
return Parser.parseResponse(await response.json());
|
||||
let parsed_response = Parser.parseResponse<ParsedResponse<T>>(await response.json());
|
||||
|
||||
// Handle redirects
|
||||
if (this.#isBrowse(parsed_response) && parsed_response.on_response_received_actions?.first()?.type === 'navigateAction') {
|
||||
const navigate_action = parsed_response.on_response_received_actions.firstOfType(NavigateAction);
|
||||
if (navigate_action) {
|
||||
parsed_response = await navigate_action.endpoint.call(this, { parse: true });
|
||||
}
|
||||
}
|
||||
|
||||
return parsed_response;
|
||||
}
|
||||
|
||||
return this.#wrap(response);
|
||||
}
|
||||
|
||||
#isBrowse(response: IParsedResponse): response is IBrowseResponse {
|
||||
return 'on_response_received_actions' in response;
|
||||
}
|
||||
|
||||
#needsLogin(id: string) {
|
||||
return [
|
||||
'FElibrary',
|
||||
|
||||
128
src/core/Feed.ts
128
src/core/Feed.ts
@@ -1,57 +1,61 @@
|
||||
import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index';
|
||||
import { Memo, ObservedArray } from '../parser/helpers';
|
||||
import { concatMemos, InnertubeError } from '../utils/Utils';
|
||||
import Actions from './Actions';
|
||||
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../parser/helpers.js';
|
||||
import Parser, { ReloadContinuationItemsCommand } from '../parser/index.js';
|
||||
import { concatMemos, InnertubeError } from '../utils/Utils.js';
|
||||
import type Actions from './Actions.js';
|
||||
|
||||
import Post from '../parser/classes/Post';
|
||||
import BackstagePost from '../parser/classes/BackstagePost';
|
||||
import BackstagePost from '../parser/classes/BackstagePost.js';
|
||||
import SharedPost from '../parser/classes/SharedPost.js';
|
||||
import Channel from '../parser/classes/Channel.js';
|
||||
import CompactVideo from '../parser/classes/CompactVideo.js';
|
||||
import GridChannel from '../parser/classes/GridChannel.js';
|
||||
import GridPlaylist from '../parser/classes/GridPlaylist.js';
|
||||
import GridVideo from '../parser/classes/GridVideo.js';
|
||||
import Playlist from '../parser/classes/Playlist.js';
|
||||
import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo.js';
|
||||
import PlaylistVideo from '../parser/classes/PlaylistVideo.js';
|
||||
import Post from '../parser/classes/Post.js';
|
||||
import ReelItem from '../parser/classes/ReelItem.js';
|
||||
import ReelShelf from '../parser/classes/ReelShelf.js';
|
||||
import RichShelf from '../parser/classes/RichShelf.js';
|
||||
import Shelf from '../parser/classes/Shelf.js';
|
||||
import Tab from '../parser/classes/Tab.js';
|
||||
import Video from '../parser/classes/Video.js';
|
||||
|
||||
import Channel from '../parser/classes/Channel';
|
||||
import CompactVideo from '../parser/classes/CompactVideo';
|
||||
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction.js';
|
||||
import ContinuationItem from '../parser/classes/ContinuationItem.js';
|
||||
import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults.js';
|
||||
import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults.js';
|
||||
import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo.js';
|
||||
|
||||
import GridChannel from '../parser/classes/GridChannel';
|
||||
import GridPlaylist from '../parser/classes/GridPlaylist';
|
||||
import GridVideo from '../parser/classes/GridVideo';
|
||||
import type MusicQueue from '../parser/classes/MusicQueue.js';
|
||||
import type RichGrid from '../parser/classes/RichGrid.js';
|
||||
import type SectionList from '../parser/classes/SectionList.js';
|
||||
|
||||
import Playlist from '../parser/classes/Playlist';
|
||||
import PlaylistPanelVideo from '../parser/classes/PlaylistPanelVideo';
|
||||
import PlaylistVideo from '../parser/classes/PlaylistVideo';
|
||||
import type { IParsedResponse } from '../parser/types/index.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
import Tab from '../parser/classes/Tab';
|
||||
import ReelShelf from '../parser/classes/ReelShelf';
|
||||
import RichShelf from '../parser/classes/RichShelf';
|
||||
import Shelf from '../parser/classes/Shelf';
|
||||
|
||||
import TwoColumnBrowseResults from '../parser/classes/TwoColumnBrowseResults';
|
||||
import TwoColumnSearchResults from '../parser/classes/TwoColumnSearchResults';
|
||||
import WatchCardCompactVideo from '../parser/classes/WatchCardCompactVideo';
|
||||
import AppendContinuationItemsAction from '../parser/classes/actions/AppendContinuationItemsAction';
|
||||
import ContinuationItem from '../parser/classes/ContinuationItem';
|
||||
|
||||
import Video from '../parser/classes/Video';
|
||||
import ReelItem from '../parser/classes/ReelItem';
|
||||
|
||||
class Feed {
|
||||
#page: ParsedResponse;
|
||||
class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
#page: T;
|
||||
#continuation?: ObservedArray<ContinuationItem>;
|
||||
#actions;
|
||||
#memo;
|
||||
#actions: Actions;
|
||||
#memo: Memo;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed) {
|
||||
this.#page = data;
|
||||
constructor(actions: Actions, response: ApiResponse | IParsedResponse, already_parsed = false) {
|
||||
if (this.#isParsed(response) || already_parsed) {
|
||||
this.#page = response as T;
|
||||
} else {
|
||||
this.#page = Parser.parseResponse(data);
|
||||
this.#page = Parser.parseResponse<T>(response.data);
|
||||
}
|
||||
|
||||
const memo = concatMemos(
|
||||
const memo = concatMemos(...[
|
||||
this.#page.contents_memo,
|
||||
this.#page.continuation_contents_memo,
|
||||
this.#page.on_response_received_commands_memo,
|
||||
this.#page.on_response_received_endpoints_memo,
|
||||
this.#page.on_response_received_actions_memo,
|
||||
this.#page.sidebar_memo,
|
||||
this.#page.header_memo
|
||||
);
|
||||
]);
|
||||
|
||||
if (!memo)
|
||||
throw new InnertubeError('No memo found in feed');
|
||||
@@ -60,11 +64,15 @@ class Feed {
|
||||
this.#actions = actions;
|
||||
}
|
||||
|
||||
#isParsed(response: IParsedResponse | ApiResponse): response is IParsedResponse {
|
||||
return !('data' in response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all videos on a given page via memo
|
||||
*/
|
||||
static getVideosFromMemo(memo: Memo) {
|
||||
return memo.getType<Video | GridVideo | ReelItem | CompactVideo | PlaylistVideo | PlaylistPanelVideo | WatchCardCompactVideo>([
|
||||
return memo.getType(
|
||||
Video,
|
||||
GridVideo,
|
||||
ReelItem,
|
||||
@@ -72,14 +80,14 @@ class Feed {
|
||||
PlaylistVideo,
|
||||
PlaylistPanelVideo,
|
||||
WatchCardCompactVideo
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all playlists on a given page via memo
|
||||
*/
|
||||
static getPlaylistsFromMemo(memo: Memo) {
|
||||
return memo.getType<Playlist | GridPlaylist>([ Playlist, GridPlaylist ]);
|
||||
return memo.getType(Playlist, GridPlaylist);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,14 +101,14 @@ class Feed {
|
||||
* Get all the community posts in the feed
|
||||
*/
|
||||
get posts() {
|
||||
return this.#memo.getType<Post | BackstagePost>([ BackstagePost, Post ]);
|
||||
return this.#memo.getType(BackstagePost, Post, SharedPost);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the channels in the feed
|
||||
*/
|
||||
get channels() {
|
||||
return this.#memo.getType<Channel | GridChannel>([ Channel, GridChannel ]);
|
||||
return this.#memo.getType(Channel, GridChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,10 +125,10 @@ class Feed {
|
||||
/**
|
||||
* Returns contents from the page.
|
||||
*/
|
||||
get page_contents() {
|
||||
const tab_content = this.#memo.getType(Tab)?.[0]?.content;
|
||||
const reload_continuation_items = this.#memo.getType(ReloadContinuationItemsCommand)?.[0];
|
||||
const append_continuation_items = this.#memo.getType(AppendContinuationItemsAction)?.[0];
|
||||
get page_contents(): SectionList | MusicQueue | RichGrid | ReloadContinuationItemsCommand {
|
||||
const tab_content = this.#memo.getType(Tab)?.first().content;
|
||||
const reload_continuation_items = this.#memo.getType(ReloadContinuationItemsCommand).first();
|
||||
const append_continuation_items = this.#memo.getType(AppendContinuationItemsAction).first();
|
||||
|
||||
return tab_content || reload_continuation_items || append_continuation_items;
|
||||
}
|
||||
@@ -129,24 +137,24 @@ class Feed {
|
||||
* Returns all segments/sections from the page.
|
||||
*/
|
||||
get shelves() {
|
||||
return this.#memo.getType<Shelf | RichShelf | ReelShelf>([ Shelf, RichShelf, ReelShelf ]);
|
||||
return this.#memo.getType(Shelf, RichShelf, ReelShelf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds shelf by title.
|
||||
*/
|
||||
getShelf(title: string) {
|
||||
return this.shelves.find((shelf) => shelf.title.toString() === title);
|
||||
return this.shelves.get({ title });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns secondary contents from the page.
|
||||
*/
|
||||
get secondary_contents() {
|
||||
if (!this.#page.contents.is_node)
|
||||
get secondary_contents(): SuperParsedResult<YTNode> | undefined {
|
||||
if (!this.#page.contents?.is_node)
|
||||
return undefined;
|
||||
|
||||
const node = this.#page.contents.item();
|
||||
const node = this.#page.contents?.item();
|
||||
|
||||
if (!node.is(TwoColumnBrowseResults, TwoColumnSearchResults))
|
||||
return undefined;
|
||||
@@ -154,35 +162,35 @@ class Feed {
|
||||
return node.secondary_contents;
|
||||
}
|
||||
|
||||
get actions() {
|
||||
get actions(): Actions {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original page data
|
||||
*/
|
||||
get page() {
|
||||
get page(): T {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the feed has continuation.
|
||||
*/
|
||||
get has_continuation() {
|
||||
get has_continuation(): boolean {
|
||||
return (this.#memo.get('ContinuationItem') || []).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves continuation data as it is.
|
||||
*/
|
||||
async getContinuationData(): Promise<ParsedResponse | undefined> {
|
||||
async getContinuationData(): Promise<T | undefined> {
|
||||
if (this.#continuation) {
|
||||
if (this.#continuation.length > 1)
|
||||
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
|
||||
if (this.#continuation.length === 0)
|
||||
throw new InnertubeError('There are no continuations');
|
||||
|
||||
const response = await this.#continuation[0].endpoint.call(this.#actions, { parse: true });
|
||||
const response = await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -196,9 +204,11 @@ class Feed {
|
||||
/**
|
||||
* Retrieves next batch of contents and returns a new {@link Feed} object.
|
||||
*/
|
||||
async getContinuation() {
|
||||
async getContinuation(): Promise<Feed<T>> {
|
||||
const continuation_data = await this.getContinuationData();
|
||||
return new Feed(this.actions, continuation_data, true);
|
||||
if (!continuation_data)
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new Feed<T>(this.actions, continuation_data, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import ChipCloudChip from '../parser/classes/ChipCloudChip';
|
||||
import FeedFilterChipBar from '../parser/classes/FeedFilterChipBar';
|
||||
import { ObservedArray } from '../parser/helpers';
|
||||
import { InnertubeError } from '../utils/Utils';
|
||||
import Actions from './Actions';
|
||||
import Feed from './Feed';
|
||||
import ChipCloudChip from '../parser/classes/ChipCloudChip.js';
|
||||
import FeedFilterChipBar from '../parser/classes/FeedFilterChipBar.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
import Feed from './Feed.js';
|
||||
|
||||
class FilterableFeed extends Feed {
|
||||
import type { ObservedArray } from '../parser/helpers.js';
|
||||
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#chips?: ObservedArray<ChipCloudChip>;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | T, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filter chips.
|
||||
*/
|
||||
get filter_chips() {
|
||||
get filter_chips(): ObservedArray<ChipCloudChip> {
|
||||
if (this.#chips)
|
||||
return this.#chips || [];
|
||||
|
||||
@@ -33,14 +36,14 @@ class FilterableFeed extends Feed {
|
||||
/**
|
||||
* Returns available filters.
|
||||
*/
|
||||
get filters() {
|
||||
get filters(): string[] {
|
||||
return this.filter_chips.map((chip) => chip.text.toString()) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given filter and returns a new {@link Feed} object.
|
||||
*/
|
||||
async getFilteredFeed(filter: string | ChipCloudChip) {
|
||||
async getFilteredFeed(filter: string | ChipCloudChip): Promise<Feed<T>> {
|
||||
let target_filter: ChipCloudChip | undefined;
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
@@ -61,6 +64,9 @@ class FilterableFeed extends Feed {
|
||||
|
||||
const response = await target_filter.endpoint?.call(this.actions, { parse: true });
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Failed to get filtered feed');
|
||||
|
||||
return new Feed(this.actions, response, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Proto from '../proto';
|
||||
import Actions from './Actions';
|
||||
import { throwIfMissing } from '../utils/Utils';
|
||||
import Proto from '../proto/index.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
import { throwIfMissing } from '../utils/Utils.js';
|
||||
|
||||
class InteractionManager {
|
||||
#actions;
|
||||
#actions: Actions;
|
||||
|
||||
constructor(actions: Actions) {
|
||||
this.#actions = actions;
|
||||
@@ -13,7 +14,7 @@ class InteractionManager {
|
||||
* Likes a given video.
|
||||
* @param video_id - The video ID
|
||||
*/
|
||||
async like(video_id: string) {
|
||||
async like(video_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -33,7 +34,7 @@ class InteractionManager {
|
||||
* Dislikes a given video.
|
||||
* @param video_id - The video ID
|
||||
*/
|
||||
async dislike(video_id: string) {
|
||||
async dislike(video_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -53,7 +54,7 @@ class InteractionManager {
|
||||
* Removes a like/dislike.
|
||||
* @param video_id - The video ID
|
||||
*/
|
||||
async removeRating(video_id: string) {
|
||||
async removeRating(video_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -73,7 +74,7 @@ class InteractionManager {
|
||||
* Subscribes to a given channel.
|
||||
* @param channel_id - The channel ID
|
||||
*/
|
||||
async subscribe(channel_id: string) {
|
||||
async subscribe(channel_id: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ channel_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -92,7 +93,7 @@ class InteractionManager {
|
||||
* Unsubscribes from a given channel.
|
||||
* @param channel_id - The channel ID
|
||||
*/
|
||||
async unsubscribe(channel_id: string) {
|
||||
async unsubscribe(channel_id: string): Promise<ApiResponse>{
|
||||
throwIfMissing({ channel_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -112,7 +113,7 @@ class InteractionManager {
|
||||
* @param video_id - The video ID
|
||||
* @param text - The comment text
|
||||
*/
|
||||
async comment(video_id: string, text: string) {
|
||||
async comment(video_id: string, text: string): Promise<ApiResponse> {
|
||||
throwIfMissing({ video_id, text });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -159,7 +160,7 @@ class InteractionManager {
|
||||
* @param channel_id - The channel ID.
|
||||
* @param type - The notification type.
|
||||
*/
|
||||
async setNotificationPreferences(channel_id: string, type: 'PERSONALIZED' | 'ALL' | 'NONE') {
|
||||
async setNotificationPreferences(channel_id: string, type: 'PERSONALIZED' | 'ALL' | 'NONE'): Promise<ApiResponse> {
|
||||
throwIfMissing({ channel_id, type });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -175,7 +176,7 @@ class InteractionManager {
|
||||
throw new Error(`Invalid notification preference type: ${type}`);
|
||||
|
||||
const action = await this.#actions.execute('/notification/modify_channel_preference', {
|
||||
client: 'ANDROID',
|
||||
client: 'WEB',
|
||||
params: Proto.encodeNotificationPref(channel_id, pref_types[type.toUpperCase() as keyof typeof pref_types])
|
||||
});
|
||||
|
||||
|
||||
68
src/core/Kids.ts
Normal file
68
src/core/Kids.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import Search from '../parser/ytkids/Search.js';
|
||||
import HomeFeed from '../parser/ytkids/HomeFeed.js';
|
||||
import VideoInfo from '../parser/ytkids/VideoInfo.js';
|
||||
import Channel from '../parser/ytkids/Channel.js';
|
||||
import type Session from './Session.js';
|
||||
|
||||
import { generateRandomString } from '../utils/Utils.js';
|
||||
|
||||
class Kids {
|
||||
#session: Session;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.#session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the given query.
|
||||
* @param query - The query.
|
||||
*/
|
||||
async search(query: string): Promise<Search> {
|
||||
const response = await this.#session.actions.execute('/search', { query, client: 'YTKIDS' });
|
||||
return new Search(this.#session.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves video info.
|
||||
* @param video_id - The video id.
|
||||
*/
|
||||
async getInfo(video_id: string): Promise<VideoInfo> {
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.#session.actions.execute('/player', {
|
||||
cpn,
|
||||
client: 'YTKIDS',
|
||||
videoId: video_id,
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
signatureTimestamp: this.#session.player?.sts || 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const continuation = this.#session.actions.execute('/next', { videoId: video_id, client: 'YTKIDS' });
|
||||
|
||||
const response = await Promise.all([ initial_info, continuation ]);
|
||||
|
||||
return new VideoInfo(response, this.#session.actions, cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contents of the given channel.
|
||||
* @param channel_id - The channel id.
|
||||
*/
|
||||
async getChannel(channel_id: string): Promise<Channel> {
|
||||
const response = await this.#session.actions.execute('/browse', { browseId: channel_id, client: 'YTKIDS' });
|
||||
return new Channel(this.#session.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the home feed.
|
||||
*/
|
||||
async getHomeFeed(): Promise<HomeFeed> {
|
||||
const response = await this.#session.actions.execute('/browse', { browseId: 'FEkids_home', client: 'YTKIDS' });
|
||||
return new HomeFeed(this.#session.actions, response);
|
||||
}
|
||||
}
|
||||
|
||||
export default Kids;
|
||||
103
src/core/MediaInfo.ts
Normal file
103
src/core/MediaInfo.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import Actions, { ApiResponse } from './Actions.js';
|
||||
import Constants from '../utils/Constants.js';
|
||||
import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../utils/FormatUtils.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
import Format from '../parser/classes/misc/Format.js';
|
||||
import Parser, { INextResponse, IPlayerResponse } from '../parser/index.js';
|
||||
|
||||
export class MediaInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
#actions: Actions;
|
||||
#cpn: string;
|
||||
#playback_tracking;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
this.#cpn = cpn;
|
||||
|
||||
if (info.playability_status?.status === 'ERROR')
|
||||
throw new InnertubeError('This video is unavailable', info.playability_status);
|
||||
|
||||
this.streaming_data = info.streaming_data;
|
||||
this.playability_status = info.playability_status;
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a DASH manifest from the streaming data.
|
||||
* @param url_transformer - Function to transform the URLs.
|
||||
* @param format_filter - Function to filter the formats.
|
||||
* @returns DASH manifest
|
||||
*/
|
||||
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter): Promise<string> {
|
||||
return FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the format that best matches the given options.
|
||||
* @param options - Options
|
||||
*/
|
||||
chooseFormat(options: FormatOptions): Format {
|
||||
return FormatUtils.chooseFormat(options, this.streaming_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the video.
|
||||
* @param options - Download options.
|
||||
*/
|
||||
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
||||
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds video to the watch history.
|
||||
*/
|
||||
async addToWatchHistory(client_name = Constants.CLIENTS.WEB.NAME, client_version = Constants.CLIENTS.WEB.VERSION, replacement = 'https://www.'): Promise<Response> {
|
||||
if (!this.#playback_tracking)
|
||||
throw new InnertubeError('Playback tracking not available');
|
||||
|
||||
const url_params = {
|
||||
cpn: this.#cpn,
|
||||
fmt: 251,
|
||||
rtn: 0,
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', replacement);
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name,
|
||||
client_version
|
||||
}, url_params);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions instance.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Playback Nonce.
|
||||
*/
|
||||
get cpn(): string {
|
||||
return this.#cpn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Original parsed InnerTube response.
|
||||
*/
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,40 @@
|
||||
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';
|
||||
import Library from '../parser/ytmusic/Library';
|
||||
import Artist from '../parser/ytmusic/Artist';
|
||||
import Album from '../parser/ytmusic/Album';
|
||||
import Playlist from '../parser/ytmusic/Playlist';
|
||||
import Recap from '../parser/ytmusic/Recap';
|
||||
import Album from '../parser/ytmusic/Album.js';
|
||||
import Artist from '../parser/ytmusic/Artist.js';
|
||||
import Explore from '../parser/ytmusic/Explore.js';
|
||||
import HomeFeed from '../parser/ytmusic/HomeFeed.js';
|
||||
import Library from '../parser/ytmusic/Library.js';
|
||||
import Playlist from '../parser/ytmusic/Playlist.js';
|
||||
import Recap from '../parser/ytmusic/Recap.js';
|
||||
import Search from '../parser/ytmusic/Search.js';
|
||||
import TrackInfo from '../parser/ytmusic/TrackInfo.js';
|
||||
|
||||
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 AutomixPreviewVideo from '../parser/classes/AutomixPreviewVideo.js';
|
||||
import Message from '../parser/classes/Message.js';
|
||||
import MusicCarouselShelf from '../parser/classes/MusicCarouselShelf.js';
|
||||
import MusicDescriptionShelf from '../parser/classes/MusicDescriptionShelf.js';
|
||||
import MusicQueue from '../parser/classes/MusicQueue.js';
|
||||
import MusicTwoRowItem from '../parser/classes/MusicTwoRowItem.js';
|
||||
import PlaylistPanel from '../parser/classes/PlaylistPanel.js';
|
||||
import SearchSuggestionsSection from '../parser/classes/SearchSuggestionsSection.js';
|
||||
import SectionList from '../parser/classes/SectionList.js';
|
||||
import Tab from '../parser/classes/Tab.js';
|
||||
|
||||
import Message from '../parser/classes/Message';
|
||||
import MusicQueue from '../parser/classes/MusicQueue';
|
||||
import PlaylistPanel from '../parser/classes/PlaylistPanel';
|
||||
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 } from '../parser/helpers.js';
|
||||
import Proto from '../proto/index.js';
|
||||
import { generateRandomString, InnertubeError, throwIfMissing } from '../utils/Utils.js';
|
||||
|
||||
import { observe, ObservedArray, YTNode } from '../parser/helpers';
|
||||
import { InnertubeError, throwIfMissing, generateRandomString } from '../utils/Utils';
|
||||
import Proto from '../proto';
|
||||
import type { ObservedArray, YTNode } from '../parser/helpers.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type Session from './Session.js';
|
||||
|
||||
export interface MusicSearchFilters {
|
||||
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
|
||||
}
|
||||
|
||||
class Music {
|
||||
#session;
|
||||
#actions;
|
||||
#session: Session;
|
||||
#actions: Actions;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.#session = session;
|
||||
@@ -52,7 +55,7 @@ class Music {
|
||||
throw new InnertubeError('Invalid target, expected either a video id or a valid MusicTwoRowItem', target);
|
||||
}
|
||||
|
||||
async #fetchInfoFromVideoId(video_id: string) {
|
||||
async #fetchInfoFromVideoId(video_id: string): Promise<TrackInfo> {
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.#actions.execute('/player', {
|
||||
@@ -75,7 +78,7 @@ class Music {
|
||||
return new TrackInfo(response, this.#actions, cpn);
|
||||
}
|
||||
|
||||
async #fetchInfoFromListItem(list_item: MusicTwoRowItem | undefined) {
|
||||
async #fetchInfoFromListItem(list_item: MusicTwoRowItem | undefined): Promise<TrackInfo> {
|
||||
if (!list_item)
|
||||
throw new InnertubeError('List item cannot be undefined');
|
||||
|
||||
@@ -109,9 +112,7 @@ class Music {
|
||||
* @param query - Search query.
|
||||
* @param filters - Search filters.
|
||||
*/
|
||||
async search(query: string, filters: {
|
||||
type?: 'all' | 'song' | 'video' | 'album' | 'playlist' | 'artist';
|
||||
} = {}): Promise<Search> {
|
||||
async search(query: string, filters: MusicSearchFilters = {}): Promise<Search> {
|
||||
throwIfMissing({ query });
|
||||
|
||||
const payload: {
|
||||
@@ -126,7 +127,7 @@ class Music {
|
||||
|
||||
const response = await this.#actions.execute('/search', payload);
|
||||
|
||||
return new Search(response, this.#actions, { is_filtered: Reflect.has(filters, 'type') && filters.type !== 'all' });
|
||||
return new Search(response, this.#actions, Reflect.has(filters, 'type') && filters.type !== 'all');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +200,7 @@ class Music {
|
||||
browseId: album_id
|
||||
});
|
||||
|
||||
return new Album(response, this.#actions);
|
||||
return new Album(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,13 +236,9 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Up next' });
|
||||
const tab = tabs?.first();
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -265,10 +262,10 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (!page)
|
||||
if (!page || !page.contents_memo)
|
||||
throw new InnertubeError('Could not fetch automix');
|
||||
|
||||
return page.contents_memo.getType(PlaylistPanel)?.[0];
|
||||
return page.contents_memo.getType(PlaylistPanel).first();
|
||||
}
|
||||
|
||||
return playlist_panel;
|
||||
@@ -287,20 +284,19 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Related' });
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
const shelves = page.contents.item().as(SectionList).contents.array().as(MusicCarouselShelf, MusicDescriptionShelf);
|
||||
if (!page.contents)
|
||||
throw new InnertubeError('Unexpected response', page);
|
||||
|
||||
const shelves = page.contents.item().as(SectionList).contents.as(MusicCarouselShelf, MusicDescriptionShelf);
|
||||
|
||||
return shelves;
|
||||
}
|
||||
@@ -318,23 +314,23 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents.item()
|
||||
.as(SingleColumnMusicWatchNextResults).contents.item()
|
||||
.as(Tabbed).contents.item()
|
||||
.as(WatchNextTabbedResults)
|
||||
.tabs.array().as(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs.get({ title: 'Lyrics' });
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
if (!page.contents)
|
||||
throw new InnertubeError('Unexpected response', page);
|
||||
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
|
||||
|
||||
const section_list = page.contents.item().as(SectionList).contents.array();
|
||||
const section_list = page.contents.item().as(SectionList).contents;
|
||||
|
||||
return section_list.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
@@ -354,14 +350,14 @@ class Music {
|
||||
* Retrieves search suggestions for the given query.
|
||||
* @param query - The query.
|
||||
*/
|
||||
async getSearchSuggestions(query: string) {
|
||||
async getSearchSuggestions(query: string): Promise<ObservedArray<YTNode>> {
|
||||
const response = await this.#actions.execute('/music/get_search_suggestions', {
|
||||
parse: true,
|
||||
input: query,
|
||||
client: 'YTMUSIC'
|
||||
});
|
||||
|
||||
const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection)?.[0];
|
||||
const search_suggestions_section = response.contents_memo?.getType(SearchSuggestionsSection)?.[0];
|
||||
|
||||
if (!search_suggestions_section?.contents.is_array)
|
||||
return observe([] as YTNode[]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Session from './Session';
|
||||
import Constants from '../utils/Constants';
|
||||
import { OAuthError, uuidv4 } from '../utils/Utils';
|
||||
import Constants from '../utils/Constants.js';
|
||||
import { OAuthError, Platform } from '../utils/Utils.js';
|
||||
import type Session from './Session.js';
|
||||
|
||||
export interface Credentials {
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ class OAuth {
|
||||
/**
|
||||
* Starts the auth flow in case no valid credentials are available.
|
||||
*/
|
||||
async init(credentials?: Credentials) {
|
||||
async init(credentials?: Credentials): Promise<void> {
|
||||
this.#credentials = credentials;
|
||||
|
||||
if (this.validateCredentials()) {
|
||||
@@ -55,13 +55,13 @@ class OAuth {
|
||||
}
|
||||
}
|
||||
|
||||
async cacheCredentials() {
|
||||
async cacheCredentials(): Promise<void> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(JSON.stringify(this.#credentials));
|
||||
await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer);
|
||||
}
|
||||
|
||||
async #loadCachedCredentials() {
|
||||
async #loadCachedCredentials(): Promise<boolean> {
|
||||
const data = await this.#session.cache?.get('youtubei_oauth_credentials');
|
||||
if (!data) return false;
|
||||
|
||||
@@ -82,20 +82,20 @@ class OAuth {
|
||||
return true;
|
||||
}
|
||||
|
||||
async removeCache() {
|
||||
async removeCache(): Promise<void> {
|
||||
await this.#session.cache?.remove('youtubei_oauth_credentials');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the server for a user code and verification URL.
|
||||
*/
|
||||
async #getUserCode() {
|
||||
async #getUserCode(): Promise<void> {
|
||||
this.#identity = await this.#getClientIdentity();
|
||||
|
||||
const data = {
|
||||
client_id: this.#identity.client_id,
|
||||
scope: Constants.OAUTH.SCOPE,
|
||||
device_id: uuidv4(),
|
||||
device_id: Platform.shim.uuidv4(),
|
||||
model_name: Constants.OAUTH.MODEL_NAME
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ class OAuth {
|
||||
/**
|
||||
* Polls the authorization server until access is granted by the user.
|
||||
*/
|
||||
#startPolling(device_code: string) {
|
||||
#startPolling(device_code: string): void {
|
||||
const poller = setInterval(async () => {
|
||||
const data = {
|
||||
...this.#identity,
|
||||
@@ -176,13 +176,13 @@ class OAuth {
|
||||
/**
|
||||
* Refresh access token if the same has expired.
|
||||
*/
|
||||
async refreshIfRequired() {
|
||||
async refreshIfRequired(): Promise<void> {
|
||||
if (this.has_access_token_expired) {
|
||||
await this.#refreshAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
async #refreshAccessToken() {
|
||||
async #refreshAccessToken(): Promise<void> {
|
||||
if (!this.#credentials) return;
|
||||
this.#identity = await this.#getClientIdentity();
|
||||
|
||||
@@ -215,7 +215,7 @@ class OAuth {
|
||||
});
|
||||
}
|
||||
|
||||
async revokeCredentials() {
|
||||
async revokeCredentials(): Promise<Response | undefined> {
|
||||
if (!this.#credentials) return;
|
||||
await this.removeCache();
|
||||
return this.#session.http.fetch_function(new URL(`/o/oauth2/revoke?token=${encodeURIComponent(this.#credentials.access_token)}`, Constants.URLS.YT_BASE), {
|
||||
@@ -226,7 +226,7 @@ class OAuth {
|
||||
/**
|
||||
* Retrieves client identity from YouTube TV.
|
||||
*/
|
||||
async #getClientIdentity() {
|
||||
async #getClientIdentity(): Promise<{ [key: string]: string; }> {
|
||||
const response = await this.#session.http.fetch_function(new URL('/tv', Constants.URLS.YT_BASE), { headers: Constants.OAUTH.HEADERS });
|
||||
|
||||
const response_data = await response.text();
|
||||
@@ -249,7 +249,7 @@ class OAuth {
|
||||
return groups;
|
||||
}
|
||||
|
||||
get credentials() {
|
||||
get credentials(): Credentials | undefined {
|
||||
return this.#credentials;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Platform, getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils.js';
|
||||
|
||||
import { FetchFunction } from '../utils/HTTPClient';
|
||||
import { getRandomUserAgent, getStringBetweenStrings, PlayerError } from '../utils/Utils';
|
||||
import Constants from '../utils/Constants.js';
|
||||
|
||||
import Constants from '../utils/Constants';
|
||||
import UniversalCache from '../utils/Cache';
|
||||
|
||||
// See https://github.com/LuanRT/Jinter
|
||||
import Jinter from 'jintr';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
import { FetchFunction } from '../types/PlatformShim.js';
|
||||
|
||||
export default class Player {
|
||||
#nsig_sc;
|
||||
@@ -23,7 +20,7 @@ export default class Player {
|
||||
this.#player_id = player_id;
|
||||
}
|
||||
|
||||
static async create(cache: UniversalCache | undefined, fetch: FetchFunction = globalThis.fetch) {
|
||||
static async create(cache: ICache | undefined, fetch: FetchFunction = Platform.shim.fetch): Promise<Player> {
|
||||
const url = new URL('/iframe_api', Constants.URLS.YT_BASE);
|
||||
const res = await fetch(url);
|
||||
|
||||
@@ -66,7 +63,7 @@ export default class Player {
|
||||
return await Player.fromSource(cache, sig_timestamp, sig_sc, nsig_sc, player_id);
|
||||
}
|
||||
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string) {
|
||||
decipher(url?: string, signature_cipher?: string, cipher?: string): string {
|
||||
url = url || signature_cipher || cipher;
|
||||
|
||||
if (!url)
|
||||
@@ -75,13 +72,13 @@ export default class Player {
|
||||
const args = new URLSearchParams(url);
|
||||
const url_components = new URL(args.get('url') || url);
|
||||
|
||||
url_components.searchParams.set('ratebypass', 'yes');
|
||||
|
||||
if (signature_cipher || cipher) {
|
||||
const sig_decipher = new Jinter(this.#sig_sc);
|
||||
sig_decipher.scope.set('sig', args.get('s'));
|
||||
const signature = Platform.shim.eval(this.#sig_sc, {
|
||||
sig: args.get('s')
|
||||
});
|
||||
|
||||
const signature = sig_decipher.interpret();
|
||||
if (typeof signature !== 'string')
|
||||
throw new PlayerError('Failed to decipher signature');
|
||||
|
||||
const sp = args.get('sp');
|
||||
|
||||
@@ -93,10 +90,12 @@ export default class Player {
|
||||
const n = url_components.searchParams.get('n');
|
||||
|
||||
if (n) {
|
||||
const nsig_decipher = new Jinter(this.#nsig_sc);
|
||||
nsig_decipher.scope.set('nsig', n);
|
||||
const nsig = Platform.shim.eval(this.#nsig_sc, {
|
||||
nsig: n
|
||||
});
|
||||
|
||||
const nsig = nsig_decipher.interpret();
|
||||
if (typeof nsig !== 'string')
|
||||
throw new PlayerError('Failed to decipher nsig');
|
||||
|
||||
if (nsig.startsWith('enhanced_except_')) {
|
||||
console.warn('Warning:\nCould not transform nsig, download may be throttled.\nChanging the InnerTube client to "ANDROID" might help!');
|
||||
@@ -108,7 +107,7 @@ export default class Player {
|
||||
return url_components.toString();
|
||||
}
|
||||
|
||||
static async fromCache(cache: UniversalCache, player_id: string) {
|
||||
static async fromCache(cache: ICache, player_id: string): Promise<Player | null> {
|
||||
const buffer = await cache.get(player_id);
|
||||
|
||||
if (!buffer)
|
||||
@@ -134,13 +133,13 @@ export default class Player {
|
||||
return new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
|
||||
}
|
||||
|
||||
static async fromSource(cache: UniversalCache | undefined, sig_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string) {
|
||||
static async fromSource(cache: ICache | undefined, sig_timestamp: number, sig_sc: string, nsig_sc: string, player_id: string): Promise<Player> {
|
||||
const player = new Player(sig_timestamp, sig_sc, nsig_sc, player_id);
|
||||
await player.cache(cache);
|
||||
return player;
|
||||
}
|
||||
|
||||
async cache(cache?: UniversalCache) {
|
||||
async cache(cache?: ICache): Promise<void> {
|
||||
if (!cache) return;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
@@ -161,11 +160,11 @@ export default class Player {
|
||||
await cache.set(this.#player_id, new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
static extractSigTimestamp(data: string) {
|
||||
static extractSigTimestamp(data: string): number {
|
||||
return parseInt(getStringBetweenStrings(data, 'signatureTimestamp:', ',') || '0');
|
||||
}
|
||||
|
||||
static extractSigSourceCode(data: string) {
|
||||
static extractSigSourceCode(data: string): string {
|
||||
const calls = getStringBetweenStrings(data, 'function(a){a=a.split("")', 'return a.join("")}');
|
||||
const obj_name = calls?.split(/\.|\[/)?.[0]?.replace(';', '')?.trim();
|
||||
const functions = getStringBetweenStrings(data, `var ${obj_name}={`, '};');
|
||||
@@ -176,7 +175,7 @@ export default class Player {
|
||||
return `function descramble_sig(a) { a = a.split(""); let ${obj_name}={${functions}}${calls} return a.join("") } descramble_sig(sig);`;
|
||||
}
|
||||
|
||||
static extractNSigSourceCode(data: string) {
|
||||
static extractNSigSourceCode(data: string): 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)
|
||||
@@ -185,23 +184,23 @@ export default class Player {
|
||||
return sc;
|
||||
}
|
||||
|
||||
get url() {
|
||||
get url(): string {
|
||||
return new URL(`/s/player/${this.#player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE).toString();
|
||||
}
|
||||
|
||||
get sts() {
|
||||
get sts(): number {
|
||||
return this.#sig_sc_timestamp;
|
||||
}
|
||||
|
||||
get nsig_sc() {
|
||||
get nsig_sc(): string {
|
||||
return this.#nsig_sc;
|
||||
}
|
||||
|
||||
get sig_sc() {
|
||||
get sig_sc(): string {
|
||||
return this.#sig_sc;
|
||||
}
|
||||
|
||||
static get LIBRARY_VERSION() {
|
||||
static get LIBRARY_VERSION(): number {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import Playlist from '../parser/youtube/Playlist';
|
||||
import Actions from './Actions';
|
||||
import Feed from './Feed';
|
||||
import type Feed from './Feed.js';
|
||||
import type Actions from './Actions.js';
|
||||
import Playlist from '../parser/youtube/Playlist.js';
|
||||
|
||||
import { InnertubeError, throwIfMissing } from '../utils/Utils';
|
||||
import { InnertubeError, throwIfMissing } from '../utils/Utils.js';
|
||||
|
||||
class PlaylistManager {
|
||||
#actions;
|
||||
#actions: Actions;
|
||||
|
||||
constructor(actions: Actions) {
|
||||
this.#actions = actions;
|
||||
@@ -16,7 +16,7 @@ class PlaylistManager {
|
||||
* @param title - The title of the playlist.
|
||||
* @param video_ids - An array of video IDs to add to the playlist.
|
||||
*/
|
||||
async create(title: string, video_ids: string[]) {
|
||||
async create(title: string, video_ids: string[]): Promise<{ success: boolean; status_code: number; playlist_id?: string; data: any }> {
|
||||
throwIfMissing({ title, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -40,7 +40,7 @@ class PlaylistManager {
|
||||
* Deletes a given playlist.
|
||||
* @param playlist_id - The playlist ID.
|
||||
*/
|
||||
async delete(playlist_id: string) {
|
||||
async delete(playlist_id: string): Promise<{ playlist_id: string; success: boolean; status_code: number; data: any }> {
|
||||
throwIfMissing({ playlist_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -61,7 +61,7 @@ class PlaylistManager {
|
||||
* @param playlist_id - The playlist ID.
|
||||
* @param video_ids - An array of video IDs to add to the playlist.
|
||||
*/
|
||||
async addVideos(playlist_id: string, video_ids: string[]) {
|
||||
async addVideos(playlist_id: string, video_ids: string[]): Promise<{ playlist_id: string; action_result: any }> {
|
||||
throwIfMissing({ playlist_id, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -87,7 +87,7 @@ class PlaylistManager {
|
||||
* @param playlist_id - The playlist ID.
|
||||
* @param video_ids - An array of video IDs to remove from the playlist.
|
||||
*/
|
||||
async removeVideos(playlist_id: string, video_ids: string[]) {
|
||||
async removeVideos(playlist_id: string, video_ids: string[]): Promise<{ playlist_id: string; action_result: any }> {
|
||||
throwIfMissing({ playlist_id, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
@@ -146,7 +146,7 @@ class PlaylistManager {
|
||||
* @param moved_video_id - The video ID to move.
|
||||
* @param predecessor_video_id - The video ID to move the moved video before.
|
||||
*/
|
||||
async moveVideo(playlist_id: string, moved_video_id: string, predecessor_video_id: string) {
|
||||
async moveVideo(playlist_id: string, moved_video_id: string, predecessor_video_id: string): Promise<{ playlist_id: string; action_result: any; }> {
|
||||
throwIfMissing({ playlist_id, moved_video_id, predecessor_video_id });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import Player from './Player';
|
||||
import Actions from './Actions';
|
||||
import Constants from '../utils/Constants';
|
||||
import UniversalCache from '../utils/Cache';
|
||||
import EventEmitterLike from '../utils/EventEmitterLike';
|
||||
import Constants, { CLIENTS } from '../utils/Constants.js';
|
||||
import EventEmitterLike from '../utils/EventEmitterLike.js';
|
||||
import Actions from './Actions.js';
|
||||
import Player from './Player.js';
|
||||
|
||||
import HTTPClient, { FetchFunction } from '../utils/HTTPClient';
|
||||
import { DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth';
|
||||
import Proto from '../proto/index.js';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
import { FetchFunction } from '../types/PlatformShim.js';
|
||||
import HTTPClient from '../utils/HTTPClient.js';
|
||||
import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
KIDS = 'WEB_KIDS',
|
||||
MUSIC = 'WEB_REMIX',
|
||||
ANDROID = 'ANDROID',
|
||||
ANDROID_MUSIC = 'ANDROID_MUSIC'
|
||||
ANDROID_MUSIC = 'ANDROID_MUSIC',
|
||||
ANDROID_CREATOR = 'ANDROID_CREATOR',
|
||||
TV_EMBEDDED = 'TVHTML5_SIMPLY_EMBEDDED_PLAYER'
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
client: {
|
||||
hl: string;
|
||||
gl: string;
|
||||
remoteHost: string;
|
||||
remoteHost?: string;
|
||||
screenDensityFloat: number;
|
||||
screenHeightPoints: number;
|
||||
screenPixelDensity: number;
|
||||
@@ -36,15 +41,25 @@ export interface Context {
|
||||
clientFormFactor: string;
|
||||
userInterfaceTheme: string;
|
||||
timeZone: string;
|
||||
browserName: string;
|
||||
browserVersion: string;
|
||||
browserName?: string;
|
||||
browserVersion?: string;
|
||||
originalUrl: string;
|
||||
deviceMake: string;
|
||||
deviceModel: string;
|
||||
utcOffsetMinutes: number;
|
||||
kidsAppInfo?: {
|
||||
categorySettings: {
|
||||
enabledCategories: string[];
|
||||
};
|
||||
contentSettings: {
|
||||
corpusPreference: string;
|
||||
kidsNoSearchMode: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
user: {
|
||||
lockedSafetyMode: false;
|
||||
enableSafetyMode: boolean;
|
||||
lockedSafetyMode: boolean;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
@@ -55,30 +70,85 @@ export interface Context {
|
||||
}
|
||||
|
||||
export interface SessionOptions {
|
||||
/**
|
||||
* Language.
|
||||
*/
|
||||
lang?: string;
|
||||
/**
|
||||
* Geolocation.
|
||||
*/
|
||||
location?: string;
|
||||
/**
|
||||
* The account index to use. This is useful if you have multiple accounts logged in.
|
||||
* **NOTE:**
|
||||
* Only works if you are signed in with cookies.
|
||||
*/
|
||||
account_index?: number;
|
||||
/**
|
||||
* Specifies whether to retrieve the JS player. Disabling this will make session creation faster.
|
||||
* **NOTE:** Deciphering formats is not possible without the JS player.
|
||||
*/
|
||||
retrieve_player?: boolean;
|
||||
/**
|
||||
* Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content.
|
||||
*/
|
||||
enable_safety_mode?: boolean;
|
||||
/**
|
||||
* Specifies whether to generate the session data locally or retrieve it from YouTube.
|
||||
* This can be useful if you need more performance.
|
||||
*/
|
||||
generate_session_locally?: boolean;
|
||||
/**
|
||||
* Platform to use for the session.
|
||||
*/
|
||||
device_category?: DeviceCategory;
|
||||
/**
|
||||
* InnerTube client type.
|
||||
*/
|
||||
client_type?: ClientType;
|
||||
/**
|
||||
* The time zone.
|
||||
*/
|
||||
timezone?: string;
|
||||
cache?: UniversalCache;
|
||||
/**
|
||||
* Used to cache the deciphering functions from the JS player.
|
||||
*/
|
||||
cache?: ICache;
|
||||
/**
|
||||
* YouTube cookies.
|
||||
*/
|
||||
cookie?: string;
|
||||
/**
|
||||
* Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in.
|
||||
* A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint.
|
||||
*/
|
||||
visitor_data?: string;
|
||||
/**
|
||||
* Fetch function to use.
|
||||
*/
|
||||
fetch?: FetchFunction;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
context: Context;
|
||||
api_key: string;
|
||||
api_version: string;
|
||||
}
|
||||
|
||||
export default class Session extends EventEmitterLike {
|
||||
#api_version;
|
||||
#key;
|
||||
#context;
|
||||
#account_index;
|
||||
#player;
|
||||
#api_version: string;
|
||||
#key: string;
|
||||
#context: Context;
|
||||
#account_index: number;
|
||||
#player?: Player;
|
||||
|
||||
oauth;
|
||||
http;
|
||||
logged_in;
|
||||
actions;
|
||||
cache;
|
||||
oauth: OAuth;
|
||||
http: HTTPClient;
|
||||
logged_in: boolean;
|
||||
actions: Actions;
|
||||
cache?: ICache;
|
||||
|
||||
constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: UniversalCache) {
|
||||
constructor(context: Context, api_key: string, api_version: string, account_index: number, player?: Player, cookie?: string, fetch?: FetchFunction, cache?: ICache) {
|
||||
super();
|
||||
this.#context = context;
|
||||
this.#account_index = account_index;
|
||||
@@ -112,38 +182,77 @@ export default class Session extends EventEmitterLike {
|
||||
static async create(options: SessionOptions = {}) {
|
||||
const { context, api_key, api_version, account_index } = await Session.getSessionData(
|
||||
options.lang,
|
||||
options.location,
|
||||
options.account_index,
|
||||
options.visitor_data,
|
||||
options.enable_safety_mode,
|
||||
options.generate_session_locally,
|
||||
options.device_category,
|
||||
options.client_type,
|
||||
options.timezone,
|
||||
options.fetch
|
||||
);
|
||||
return new Session(context, api_key, api_version, account_index, await Player.create(options.cache, options.fetch), options.cookie, options.fetch, options.cache);
|
||||
|
||||
return new Session(
|
||||
context, api_key, api_version, account_index,
|
||||
options.retrieve_player === false ? undefined : await Player.create(options.cache, options.fetch),
|
||||
options.cookie, options.fetch, options.cache
|
||||
);
|
||||
}
|
||||
|
||||
static async getSessionData(
|
||||
lang = 'en-US',
|
||||
lang = '',
|
||||
location = '',
|
||||
account_index = 0,
|
||||
visitor_data = '',
|
||||
enable_safety_mode = false,
|
||||
generate_session_locally = false,
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
client_name: ClientType = ClientType.WEB,
|
||||
tz: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
fetch: FetchFunction = globalThis.fetch
|
||||
fetch: FetchFunction = Platform.shim.fetch
|
||||
) {
|
||||
let session_data: SessionData;
|
||||
|
||||
if (generate_session_locally) {
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
|
||||
} else {
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
|
||||
}
|
||||
|
||||
return { ...session_data, account_index };
|
||||
}
|
||||
|
||||
static async #retrieveSessionData(options: {
|
||||
lang: string;
|
||||
location: string;
|
||||
time_zone: string;
|
||||
device_category: string;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'accept-language': lang,
|
||||
'accept-language': options.lang || 'en-US',
|
||||
'user-agent': getRandomUserAgent('desktop'),
|
||||
'accept': '*/*',
|
||||
'referer': 'https://www.youtube.com/sw.js',
|
||||
'cookie': `PREF=tz=${tz.replace('/', '.')}`
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};`
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new SessionError(`Failed to get session data: ${res.status}`);
|
||||
}
|
||||
if (!res.ok)
|
||||
throw new SessionError(`Failed to retrieve session data: ${res.status}`);
|
||||
|
||||
const text = await res.text();
|
||||
const data = JSON.parse(text.replace(/^\)\]\}'/, ''));
|
||||
@@ -157,22 +266,22 @@ export default class Session extends EventEmitterLike {
|
||||
const context: Context = {
|
||||
client: {
|
||||
hl: device_info[0],
|
||||
gl: device_info[2],
|
||||
gl: options.location || device_info[2],
|
||||
remoteHost: device_info[3],
|
||||
screenDensityFloat: 1,
|
||||
screenHeightPoints: 720,
|
||||
screenHeightPoints: 1080,
|
||||
screenPixelDensity: 1,
|
||||
screenWidthPoints: 1280,
|
||||
screenWidthPoints: 1920,
|
||||
visitorData: device_info[13],
|
||||
userAgent: device_info[14],
|
||||
clientName: client_name,
|
||||
clientName: options.client_name,
|
||||
clientVersion: device_info[16],
|
||||
osName: device_info[17],
|
||||
osVersion: device_info[18],
|
||||
platform: device_category.toUpperCase(),
|
||||
platform: options.device_category.toUpperCase(),
|
||||
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
|
||||
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
|
||||
timeZone: device_info[79],
|
||||
timeZone: device_info[79] || options.time_zone,
|
||||
browserName: device_info[86],
|
||||
browserVersion: device_info[87],
|
||||
originalUrl: Constants.URLS.YT_BASE,
|
||||
@@ -181,6 +290,7 @@ export default class Session extends EventEmitterLike {
|
||||
utcOffsetMinutes: new Date().getTimezoneOffset()
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
},
|
||||
request: {
|
||||
@@ -188,7 +298,58 @@ export default class Session extends EventEmitterLike {
|
||||
}
|
||||
};
|
||||
|
||||
return { context, api_key, api_version, account_index };
|
||||
return { context, api_key, api_version };
|
||||
}
|
||||
|
||||
static #generateSessionData(options: {
|
||||
lang: string;
|
||||
location: string;
|
||||
time_zone: string;
|
||||
device_category: DeviceCategory;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}): SessionData {
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
const context: Context = {
|
||||
client: {
|
||||
hl: options.lang || 'en',
|
||||
gl: options.location || 'US',
|
||||
screenDensityFloat: 1,
|
||||
screenHeightPoints: 1080,
|
||||
screenPixelDensity: 1,
|
||||
screenWidthPoints: 1920,
|
||||
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
|
||||
userAgent: getRandomUserAgent('desktop'),
|
||||
clientName: options.client_name,
|
||||
clientVersion: CLIENTS.WEB.VERSION,
|
||||
osName: 'Windows',
|
||||
osVersion: '10.0',
|
||||
platform: options.device_category.toUpperCase(),
|
||||
clientFormFactor: 'UNKNOWN_FORM_FACTOR',
|
||||
userInterfaceTheme: 'USER_INTERFACE_THEME_LIGHT',
|
||||
timeZone: options.time_zone,
|
||||
originalUrl: Constants.URLS.YT_BASE,
|
||||
deviceMake: '',
|
||||
deviceModel: '',
|
||||
utcOffsetMinutes: new Date().getTimezoneOffset()
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
},
|
||||
request: {
|
||||
useSsl: true
|
||||
}
|
||||
};
|
||||
|
||||
return { context, api_key: CLIENTS.WEB.API_KEY, api_version: CLIENTS.WEB.API_VERSION };
|
||||
}
|
||||
|
||||
async signIn(credentials?: Credentials): Promise<void> {
|
||||
@@ -222,7 +383,10 @@ export default class Session extends EventEmitterLike {
|
||||
});
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
/**
|
||||
* Signs out of the current account and revokes the credentials.
|
||||
*/
|
||||
async signOut(): Promise<Response | undefined> {
|
||||
if (!this.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
@@ -232,35 +396,41 @@ export default class Session extends EventEmitterLike {
|
||||
return response;
|
||||
}
|
||||
|
||||
get key() {
|
||||
/**
|
||||
* InnerTube API key.
|
||||
*/
|
||||
get key(): string {
|
||||
return this.#key;
|
||||
}
|
||||
|
||||
get api_version() {
|
||||
/**
|
||||
* InnerTube API version.
|
||||
*/
|
||||
get api_version(): string {
|
||||
return this.#api_version;
|
||||
}
|
||||
|
||||
get client_version() {
|
||||
get client_version(): string {
|
||||
return this.#context.client.clientVersion;
|
||||
}
|
||||
|
||||
get client_name() {
|
||||
get client_name(): string {
|
||||
return this.#context.client.clientName;
|
||||
}
|
||||
|
||||
get account_index() {
|
||||
get account_index(): number {
|
||||
return this.#account_index;
|
||||
}
|
||||
|
||||
get context() {
|
||||
get context(): Context {
|
||||
return this.#context;
|
||||
}
|
||||
|
||||
get player() {
|
||||
get player(): Player | undefined {
|
||||
return this.#player;
|
||||
}
|
||||
|
||||
get lang() {
|
||||
get lang(): string {
|
||||
return this.#context.client.hl;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import Proto from '../proto';
|
||||
import Session from './Session';
|
||||
import { ApiResponse } from './Actions';
|
||||
import { InnertubeError, MissingParamError, uuidv4 } from '../utils/Utils';
|
||||
import { Constants } from '../utils';
|
||||
import Proto from '../proto/index.js';
|
||||
import { Constants } from '../utils/index.js';
|
||||
import { InnertubeError, MissingParamError, Platform } from '../utils/Utils.js';
|
||||
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
import type Session from './Session.js';
|
||||
|
||||
interface UploadResult {
|
||||
status: string;
|
||||
@@ -36,7 +37,7 @@ export interface UploadedVideoMetadata {
|
||||
}
|
||||
|
||||
class Studio {
|
||||
#session;
|
||||
#session: Session;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.#session = session;
|
||||
@@ -81,7 +82,7 @@ class Studio {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
async updateVideoMetadata(video_id: string, metadata: VideoMetadata) {
|
||||
async updateVideoMetadata(video_id: string, metadata: VideoMetadata): Promise<ApiResponse> {
|
||||
if (!this.#session.logged_in)
|
||||
throw new InnertubeError('You must be signed in to perform this operation.');
|
||||
|
||||
@@ -119,12 +120,12 @@ class Studio {
|
||||
}
|
||||
|
||||
async #getInitialUploadData(): Promise<InitialUploadData> {
|
||||
const frontend_upload_id = `innertube_android:${uuidv4()}:0:v=3,api=1,cf=3`;
|
||||
const frontend_upload_id = `innertube_android:${Platform.shim.uuidv4()}:0:v=3,api=1,cf=3`;
|
||||
|
||||
const payload = {
|
||||
frontendUploadId: frontend_upload_id,
|
||||
deviceDisplayName: 'Pixel 6 Pro',
|
||||
fileId: `goog-edited-video://generated?videoFileUri=content://media/external/video/media/${uuidv4()}`,
|
||||
fileId: `goog-edited-video://generated?videoFileUri=content://media/external/video/media/${Platform.shim.uuidv4()}`,
|
||||
mp4MoovAtomRelocationStatus: 'UNSUPPORTED',
|
||||
transcodeResult: 'DISABLED',
|
||||
connectionType: 'WIFI'
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import Tab from '../parser/classes/Tab';
|
||||
import { InnertubeError } from '../utils/Utils';
|
||||
import Actions from './Actions';
|
||||
import Feed from './Feed';
|
||||
import Tab from '../parser/classes/Tab.js';
|
||||
import Feed from './Feed.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
|
||||
class TabbedFeed extends Feed {
|
||||
#tabs;
|
||||
#actions;
|
||||
import type Actions from './Actions.js';
|
||||
import type { ObservedArray } from '../parser/helpers.js';
|
||||
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#tabs?: ObservedArray<Tab>;
|
||||
#actions: Actions;
|
||||
|
||||
constructor(actions: Actions, data: ApiResponse | IParsedResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.#actions = actions;
|
||||
this.#tabs = this.page.contents_memo.getType(Tab);
|
||||
this.#tabs = this.page.contents_memo?.getType(Tab);
|
||||
}
|
||||
|
||||
get tabs() {
|
||||
return this.#tabs.map((tab) => tab.title.toString());
|
||||
get tabs(): string[] {
|
||||
return this.#tabs?.map((tab) => tab.title.toString()) ?? [];
|
||||
}
|
||||
|
||||
async getTab(title: string) {
|
||||
const tab = this.#tabs.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
|
||||
async getTabByName(title: string): Promise<TabbedFeed<T>> {
|
||||
const tab = this.#tabs?.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError(`Tab "${title}" not found`);
|
||||
@@ -28,14 +32,29 @@ class TabbedFeed extends Feed {
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Failed to call endpoint');
|
||||
|
||||
return new TabbedFeed(this.#actions, response.data, false);
|
||||
return new TabbedFeed<T>(this.#actions, response, false);
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.page.contents_memo.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
|
||||
async getTabByURL(url: string): Promise<TabbedFeed<T>> {
|
||||
const tab = this.#tabs?.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError(`Tab "${url}" not found`);
|
||||
|
||||
if (tab.selected)
|
||||
return this;
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
return new TabbedFeed<T>(this.#actions, response, false);
|
||||
}
|
||||
|
||||
hasTabWithURL(url: string): boolean {
|
||||
return this.#tabs?.some((tab) => tab.endpoint.metadata.url?.split('/').pop() === url) ?? false;
|
||||
}
|
||||
|
||||
get title(): string | undefined {
|
||||
return this.page.contents_memo?.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
src/core/index.ts
Normal file
38
src/core/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export { default as AccountManager } from './AccountManager.js';
|
||||
export * from './AccountManager.js';
|
||||
|
||||
export { default as Actions } from './Actions.js';
|
||||
export * from './Actions.js';
|
||||
|
||||
export { default as Feed } from './Feed.js';
|
||||
export * from './Feed.js';
|
||||
|
||||
export { default as FilterableFeed } from './FilterableFeed.js';
|
||||
export * from './FilterableFeed.js';
|
||||
|
||||
export { default as InteractionManager } from './InteractionManager.js';
|
||||
export * from './InteractionManager.js';
|
||||
|
||||
export { default as Kids } from './Kids.js';
|
||||
export * from './Kids.js';
|
||||
|
||||
export { default as Music } from './Music.js';
|
||||
export * from './Music.js';
|
||||
|
||||
export { default as OAuth } from './OAuth.js';
|
||||
export * from './OAuth.js';
|
||||
|
||||
export { default as Player } from './Player.js';
|
||||
export * from './Player.js';
|
||||
|
||||
export { default as PlaylistManager } from './PlaylistManager.js';
|
||||
export * from './PlaylistManager.js';
|
||||
|
||||
export { default as Session } from './Session.js';
|
||||
export * from './Session.js';
|
||||
|
||||
export { default as Studio } from './Studio.js';
|
||||
export * from './Studio.js';
|
||||
|
||||
export { default as TabbedFeed } from './TabbedFeed.js';
|
||||
export * from './TabbedFeed.js';
|
||||
@@ -1,25 +1,46 @@
|
||||
# Parser
|
||||
|
||||
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data. Also [drastically improves](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube/Library.ts#L69) how API calls are made and handled.
|
||||
The parser is responsible for sanitizing and standardizing InnerTube responses while preserving the integrity of the data.
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#api">API</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#usage">Usage</a>
|
||||
<ul>
|
||||
<li><a href="#observedarray">ObservedArray</a></li>
|
||||
<li><a href="#superparsedresponse">SuperParsedResponse</a></li>
|
||||
<li><a href="#ytnode">YTNode</a></li>
|
||||
<li><a href="#memo">Memo</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#adding-new-nodes">Adding new nodes</a></li>
|
||||
<li><a href="#how-it-works">How it works</a></li>
|
||||
</ol>
|
||||
## Table of Contents
|
||||
|
||||
___
|
||||
- [Parser](#parser)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Structure](#structure)
|
||||
- [Core](#core)
|
||||
- [Clients](#clients)
|
||||
- [API](#api)
|
||||
- [`parse(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[])`](#parsedata-rawdata-requirearray-boolean-validtypes-ytnodeconstructort--ytnodeconstructort)
|
||||
- [`parseResponse(data: IRawResponse): T`](#parseresponsedata-irawresponse-t)
|
||||
- [Usage](#usage)
|
||||
- [ObservedArray](#observedarray)
|
||||
- [SuperParsedResponse](#superparsedresponse)
|
||||
- [YTNode](#ytnode)
|
||||
- [Type Casting](#type-casting)
|
||||
- [Accessing properties without casting](#accessing-properties-without-casting)
|
||||
- [Memo](#memo)
|
||||
- [Adding new nodes](#adding-new-nodes)
|
||||
- [Generating nodes at runtime](#generating-nodes-at-runtime)
|
||||
- [How it works](#how-it-works)
|
||||
|
||||
## Structure
|
||||
### Core
|
||||
|
||||
* [`/types`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/types) - General response types.
|
||||
* [`/classes`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes) - InnerTube nodes.
|
||||
* [`generator.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/generator.ts) - Used to generate missing nodes at runtime.
|
||||
* [`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
|
||||
* [`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/parser.ts) - The core of the parser.
|
||||
* [`nodes.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/nodes.ts) - Contains a list of all the InnerTube nodes, which is used to determine the appropriate node for a given renderer. It's important to note that this file is automatically generated and should not be edited manually.
|
||||
* [`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes. Also automatically generated.
|
||||
|
||||
### Clients
|
||||
|
||||
The parser itself is not tied to any specific client. Therefore, we have a separate folder for each client that the library supports. These folders are responsible for arranging the parsed data into a format that can be easily consumed and understood. Additionally, the underlying data is also exposed for those who wish to access it.
|
||||
|
||||
* [`/youtube`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube)
|
||||
* [`/ytmusic`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytmusic)
|
||||
* [`/ytkids`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytkids)
|
||||
|
||||
## API
|
||||
|
||||
@@ -31,86 +52,82 @@ ___
|
||||
|
||||
<a name="parse"></a>
|
||||
|
||||
#### parse(data, requireArray, validTypes)
|
||||
### `parse(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[])`
|
||||
|
||||
Responsible for parsing individual nodes.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| data | `any` | The data |
|
||||
| data | `RawData` | The data to parse |
|
||||
| requireArray | `?boolean` | Whether the response should be an array |
|
||||
| validTypes | `YTNodeConstructor<T> \| YTNodeConstructor<T>[] \| undefined` | Types of `YTNode` allowed |
|
||||
|
||||
When `requireArray` is `true`, the response will be an `ObservedArray<YTNodes>`.
|
||||
- If `requireArray` is `true`, the response will be an `ObservedArray<YTNodes>`.
|
||||
- If `validTypes` is `undefined`, the response will be an array of YTNodes.
|
||||
- If `validTypes` is an array, the response will be an array of YTNodes that are of the types specified in the array.
|
||||
- If `validTypes` is a single type, the response will be an array of YTNodes that are of the type specified.
|
||||
|
||||
When `validTypes` is `undefined`, the response will be an array of YTNodes.
|
||||
|
||||
When `validTypes` is an array, the response will be an array of YTNodes that are of the types specified in the array.
|
||||
|
||||
When `validTypes` is a single type, the response will be an array of YTNodes that are of the type specified.
|
||||
|
||||
If you do not specify `requireArray`, the return type of the function will not be known at runtime, and therefore we return the response wrapped in a helper, `SuperParsedResponse`, to gain access to the response.
|
||||
If you do not specify `requireArray`, the return type of the function will not be known at runtime. Therefore, to gain access to the response, we return it wrapped in a helper, `SuperParsedResponse`.
|
||||
|
||||
You may use the `Parser#parseArray` and `Parser#parseItem` methods to parse the response in a deterministic way.
|
||||
|
||||
<a name="parseresponse"></a>
|
||||
#### parseResponse(data)
|
||||
|
||||
### `parseResponse(data: IRawResponse): T`
|
||||
|
||||
Unlike `parse`, this can be used to parse the entire response object.
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| data | `object` | Raw InnerTube response |
|
||||
| data | `IRawResponse` | Raw InnerTube response |
|
||||
|
||||
## Usage
|
||||
|
||||
## ObservedArray
|
||||
You may use `ObservedArray<T extends YTNode>` as a normal array, but it provides additional methods for typesafe access and casting.
|
||||
### ObservedArray
|
||||
You can utilize an `ObservedArray<T extends YTNode>` as a regular array, but it also offers further methods for accessing and casting values in a type-safe manner.
|
||||
|
||||
```ts
|
||||
// For example, we have a feed, and want all the videos:
|
||||
const feed = new ObservedArray<YTNode>([...feed.contents]);
|
||||
const videos = feed.filterType(GridVideo);
|
||||
// This is now a GridVideo[]
|
||||
|
||||
// Or we want only the first video:
|
||||
// Here, we use the filterType method to retrieve only GridVideo items from the feed.
|
||||
const videos = feed.filterType(GridVideo);
|
||||
// `videos` is now a GridVideo[] array.
|
||||
|
||||
// Alternatively, we can use firstOfType to retrieve the first GridVideo item from the feed.
|
||||
const firstVideo = feed.firstOfType(GridVideo);
|
||||
|
||||
// We may cast the whole array to a GridVideo[] and throw if we have any non-GridVideo elements:
|
||||
// If we want to make sure that all elements in the `feed` array are of the `GridVideo` type, we can use the `as` method to cast the entire array to a `GridVideo[]` type. If the cast fails because of non-GridVideo items, an exception is thrown.
|
||||
const allVideos = feed.as(GridVideo);
|
||||
|
||||
// There are some extra methods for ObservedArray<T extends YTNode>
|
||||
// which we use internally but not documented here (yet).
|
||||
// see the source code for more details.
|
||||
// Note that ObservedArray provides additional methods beyond what's shown here, which we use internally. For more information, see the source code or documentation.
|
||||
```
|
||||
|
||||
## SuperParsedResponse
|
||||
Represents a parsed response in an unknown state. Either a `YTNode` or an `ObservedArray<YTNode>` or `null`.
|
||||
|
||||
You will need to assert the type and unwrap the response to get the actual value.
|
||||
### SuperParsedResponse
|
||||
Represents a parsed response in an unknown state. Either a `YTNode`, an `ObservedArray<YTNode>`, or `null`. To extract the actual value, you must first assert the type and unwrap the response.
|
||||
|
||||
```ts
|
||||
// We can assert we have a YTNode:
|
||||
// First, parse the data and store it in `response`.
|
||||
const response = Parser.parse(data);
|
||||
|
||||
// Check whether `response` is a YTNode.
|
||||
if (response.is_item) {
|
||||
// If so, we can assert that it is a YTNode and retrieve it.
|
||||
const node = response.item();
|
||||
}
|
||||
|
||||
// We can assert we have an ObservedArray<YTNode>:
|
||||
const response = Parser.parse(data);
|
||||
// Check whether `response` is an ObservedArray<YTNode>.
|
||||
if (response.is_array) {
|
||||
// If so, we can assert that it is an ObservedArray<YTNode> and retrieve its contents as an array of YTNode objects.
|
||||
const nodes = response.array();
|
||||
}
|
||||
|
||||
// Or lastly a null response:
|
||||
const response = Parser.parse(data);
|
||||
// Finally, to check if `response` is a null value, use the `is_null` getter.
|
||||
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 what allows us a typesafe way to use data returned by the InnerTube API.
|
||||
### YTNode
|
||||
All renderers returned by InnerTube are converted to this generic class and then extended for the specific renderers. This class is what allows us a type-safe way to use data returned by the InnerTube API.
|
||||
|
||||
Here's how to use this class to access returned data:
|
||||
|
||||
@@ -118,10 +135,10 @@ Here's how to use this class to access returned data:
|
||||
```ts
|
||||
// We can cast a YTNode to a child class of YTNode
|
||||
const results = node.as(TwoColumnSearchResults);
|
||||
// This will throw if the node is not a TwoColumnSearchResults
|
||||
// We thus may want to check for the type of the node before casting
|
||||
// This will throw an error if the node is not a TwoColumnSearchResults.
|
||||
// Therefore, we may want to check for the type of the node before casting.
|
||||
if (node.is(TwoColumnSearchResults)) {
|
||||
// We do not need to recast the node, it is already a TwoColumnSearchResults after calling is() and using it in the branch where is() returns true
|
||||
// We do not need to recast the node; it is already a TwoColumnSearchResults after calling is() and using it in the branch where is() returns true.
|
||||
const results = node;
|
||||
}
|
||||
|
||||
@@ -131,21 +148,20 @@ const results = node.as(TwoColumnSearchResults, VideoList);
|
||||
|
||||
// Similarly, we can check if the node is of a certain type.
|
||||
if (node.is(TwoColumnSearchResults, VideoList)) {
|
||||
// Again no casting is needed, the node is already of the correct type.
|
||||
// // Again, no casting is needed; the node is already of the correct type.
|
||||
const results = node;
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing properties without casting
|
||||
Sometimes multiple nodes have the same properties and we don't want to check the type of the node before accessing the property, for example, the property "contents" is used by many node types, and we may add more in the future, as such we want to only assert the property instead of casting to a specific type.
|
||||
Sometimes multiple nodes have the same properties, and we don't want to check the type of the node before accessing the property. For example, the property 'contents' is used by many node types, and we may add more in the future. As such, we want to only assert the property instead of casting to a specific type.
|
||||
|
||||
```ts
|
||||
// Accessing a property on a node which you aren't sure if it exists.
|
||||
// Accessing a property on a node when you aren't sure if it exists.
|
||||
const prop = node.key("contents");
|
||||
// This returns the value wrapped into a `Maybe` type
|
||||
// which you can use to find the type of the value
|
||||
// note, however, this throws an error if the key doesn't exist
|
||||
// we may want to check for the key before accessing it.
|
||||
|
||||
// This returns the value wrapped into a `Maybe` type, which you can use to determine the type of the value.
|
||||
// However, this throws an error if the key doesn't exist, so we may want to check for the key before accessing it.
|
||||
if (node.hasKey("contents")) {
|
||||
const prop = node.key("contents");
|
||||
}
|
||||
@@ -156,19 +172,18 @@ if (prop.isString()) {
|
||||
const value = prop.string();
|
||||
}
|
||||
|
||||
// We can do more complex assertions too,
|
||||
// like checking for instanceof.
|
||||
// We can do more complex assertions, like checking for instanceof.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isInstanceof(Text)) {
|
||||
const text = prop.instanceof(Text);
|
||||
// and then use the value as the given type
|
||||
if (prop.isInstanceOf(Text)) {
|
||||
const text = prop.instanceOf(Text);
|
||||
|
||||
// Then use the value as the given type.
|
||||
text.runs.forEach(run => {
|
||||
console.log(run.text);
|
||||
});
|
||||
}
|
||||
|
||||
// There are some special methods for using with the parser —
|
||||
// such as getting the value as a YTNode.
|
||||
// There are special methods for use with the parser, such as getting the value as a YTNode.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isNode()) {
|
||||
const node = prop.node();
|
||||
@@ -187,13 +202,12 @@ if (prop.isNodeOfType([TwoColumnSearchResults, VideoList])) {
|
||||
}
|
||||
|
||||
// Sometimes an ObservedArray is returned when working with parsed data.
|
||||
// We've got a helper for that too;
|
||||
// We also have a helper for this.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isObserved()) {
|
||||
const array = prop.observed();
|
||||
|
||||
// Now we may use all the ObservedArray methods as normal,
|
||||
// like finding nodes of a certain type for example.
|
||||
// Now we can use all the ObservedArray methods as normal, such as finding nodes of a certain type.
|
||||
const results = array.filterType(GridVideo);
|
||||
}
|
||||
|
||||
@@ -202,8 +216,8 @@ const prop = node.key("contents");
|
||||
if (prop.isParsed()) {
|
||||
const result = prop.parsed();
|
||||
|
||||
// SuperParsedResult is another helper for typesafe access to the parsed data,
|
||||
// it is explained above with the `Parser#parse` method.
|
||||
// SuperParsedResult is another helper for type-safe access to the parsed data.
|
||||
// It is explained above with the `Parser#parse` method.
|
||||
const results = results.array();
|
||||
const videos = results.filterType(Video);
|
||||
}
|
||||
@@ -213,51 +227,106 @@ if (prop.isParsed()) {
|
||||
const prop = node.key("contents");
|
||||
const value = prop.any();
|
||||
|
||||
// Arrays are also a special case as every element may be of a different type,
|
||||
// the `arrayOfMaybe` method will return an array of `Maybe`s.
|
||||
// Arrays are a special case, as every element may be of a different type.
|
||||
// The `arrayOfMaybe` method will return an array of `Maybe`s.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isArray()) {
|
||||
const array = prop.arrayOfMaybe();
|
||||
// This will return Maybe[]
|
||||
// This will return `Maybe[]`.
|
||||
}
|
||||
|
||||
// Or if you want zero type safety you can use the `array` method.
|
||||
// Or, if you don't need type safety, you can use the `array` method.
|
||||
const prop = node.key("contents");
|
||||
if (prop.isArray()) {
|
||||
const array = prop.array();
|
||||
// This will return any[]
|
||||
// This will return any[].
|
||||
}
|
||||
```
|
||||
|
||||
## Memo
|
||||
The `Memo` class is a helper class for memoizing values in the `Parser#parseResponse` method. It is useful for finding nodes after parsing the response.
|
||||
### Memo
|
||||
The `Memo` class is a helper class for memoizing values in the `Parser#parseResponse` method. It can be used to conveniently access nodes after parsing the response.
|
||||
|
||||
Say we want all of the videos in a search result. We can use the `Memo` to find all of them quickly without recursing through the response.
|
||||
For example, if we'd like to obtain all of the videos from a search result, we can use the `Memo#getType` method to find them quickly without needing to traverse the entire response.
|
||||
|
||||
```ts
|
||||
const response = Parser.parseResponse(data);
|
||||
const videos = response.contents_memo.getType(Video);
|
||||
// This returns the nodes as a ObservedArray<Video>.
|
||||
// This returns the nodes as an `ObservedArray<Video>`.
|
||||
```
|
||||
|
||||
`Memo` extends `Map<string, YTNode[]>` and can be used as a normal `Map` too if you want.
|
||||
`Memo` extends `Map<string, YTNode[]>` and can be used as a regular `Map` if desired.
|
||||
|
||||
## Adding new nodes
|
||||
Instructions can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md).
|
||||
|
||||
## Generating nodes at runtime
|
||||
YouTube constantly updates their client, and sometimes they add new nodes to the response. The parser needs to know about these new nodes in order to parse them correctly. Once a new node is dicovered by the parser, it will attempt to generate a new node class for it.
|
||||
|
||||
Using the existing `YTNode` class, you may interact with these new nodes in a type-safe way. However, you will not be able to cast them to the node's specific type, as this requires the node to be defined at compile-time.
|
||||
|
||||
The current implementation recognises the following values:
|
||||
- Renderers
|
||||
- Renderer arrays
|
||||
- Text
|
||||
- Navigation endpoints
|
||||
- Author (does not currently detect the author thumbnails)
|
||||
- Thumbnails
|
||||
- Objects (key-value pairs)
|
||||
- Primatives (string, number, boolean, etc.)
|
||||
|
||||
This may be expanded in the future.
|
||||
|
||||
At runtime, these JIT-generated nodes will revalidate themselves when constructed so that when the types change, the node will be re-generated.
|
||||
|
||||
To access these nodes that have been generated at runtime, you may use the `Parser.getParserByName(name: string)` method. You may also check if a parser has been generated for a node by using the `Parser.hasParser(name: string)` method.
|
||||
|
||||
```ts
|
||||
import { Parser } from "youtubei.js";
|
||||
|
||||
// We may check if we have a parser for a node.
|
||||
if (Parser.hasParser('Example')) {
|
||||
// Then retrieve it.
|
||||
const Example = Parser.getParserByName('Example');
|
||||
// We may then use the parser as normal.
|
||||
const example = new Example(data);
|
||||
}
|
||||
```
|
||||
|
||||
You may also generate your own nodes ahead of time, given you have an example of one of the nodes.
|
||||
|
||||
```ts
|
||||
import { Generator } from "youtubei.js";
|
||||
|
||||
// Provided you have an example of the node `Example`
|
||||
const example_data = {
|
||||
"title": {
|
||||
"runs": [
|
||||
{
|
||||
"text": "Example"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// The first argument is the name of the class, the second is the data you have for the node.
|
||||
// It will return a class that extends YTNode.
|
||||
const Example = Generator.YTNodeGenerator.generateRuntimeClass('Example', example_data);
|
||||
|
||||
// You may now use this class as you would any other node.
|
||||
const example = new Example(example_data);
|
||||
|
||||
const title = example.key('title').instanceof(Text).toString();
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
If you decompile a YouTube client and analyze it for a while you will notice that it has classes named `protos/youtube/api/innertube/MusicItemRenderer`, `protos/youtube/api/innertube/SectionListRenderer`, etc.
|
||||
If you decompile a YouTube client and analyze it, it becomes apparent that it uses classes such as `../youtube/api/innertube/MusicItemRenderer` and `../youtube/api/innertube/SectionListRenderer` to parse objects from the response, map them into models, and generate the UI. The website operates similarly, but instead uses plain JSON. You can think of renderers as components in a web framework.
|
||||
|
||||
These classes are used to parse objects from the response, map them into models and generate the UI. The website works similarly, the difference is that it uses plain JSON (likely converted from protobuf server-side, hence the weird structure of the response).
|
||||
Our approach is similar to YouTube's: our parser goes through all the renderers and parses their inner element(s). The final result is a nicely structured JSON, it even parses navigation endpoints, which allow us to make an API call with all required parameters in one line and emulate client actions, such as clicking a button.
|
||||
|
||||
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 allow us to make an API call with all required parameters in one line and even emulate client actions (eg; clicking a button).
|
||||
|
||||
Here is your average, arguably ugly InnerTube response:
|
||||
<details>
|
||||
<summary>Click to see</summary>
|
||||
<p>
|
||||
To illustrate the transformation we make, let's take an unstructured InnerTube response and parse it into a cleaner format:
|
||||
|
||||
Raw InnerTube Response:
|
||||
```js
|
||||
{
|
||||
sidebar: {
|
||||
@@ -296,14 +365,7 @@ Here is your average, arguably ugly InnerTube response:
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
And what we get after parsing it:
|
||||
<details>
|
||||
<summary>Click to see</summary>
|
||||
<p>
|
||||
|
||||
Clean Parsed Response:
|
||||
```js
|
||||
{
|
||||
sidebar: {
|
||||
@@ -311,21 +373,28 @@ And what we get after parsing it:
|
||||
contents: [
|
||||
{
|
||||
type: 'PlaylistSidebarPrimaryInfo',
|
||||
title: { text: '..' },
|
||||
description: { text: '..' },
|
||||
title: { text: '..', runs: [ { text: '..' } ] },
|
||||
description: { text: '..', runs: [ { text: '..' } ] },
|
||||
stats: [
|
||||
{
|
||||
text: '..'
|
||||
text: '..',
|
||||
runs: [
|
||||
{
|
||||
text: '..'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '..'
|
||||
text: '..',
|
||||
runs: [
|
||||
{
|
||||
text: '..'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
```
|
||||
@@ -1,6 +1,7 @@
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import { YTNode } from '../helpers';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class AccountChannel extends YTNode {
|
||||
static type = 'AccountChannel';
|
||||
@@ -8,7 +9,7 @@ class AccountChannel extends YTNode {
|
||||
title: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Parser from '..';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import AccountItemSectionHeader from './AccountItemSectionHeader';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import AccountItemSectionHeader from './AccountItemSectionHeader.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class AccountItem {
|
||||
static type = 'AccountItem';
|
||||
@@ -18,7 +19,7 @@ class AccountItem {
|
||||
endpoint: NavigationEndpoint;
|
||||
account_byline: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
this.account_name = new Text(data.accountName);
|
||||
this.account_photo = Thumbnail.fromResponse(data.accountPhoto);
|
||||
this.is_selected = data.isSelected;
|
||||
@@ -35,10 +36,10 @@ class AccountItemSection extends YTNode {
|
||||
contents;
|
||||
header;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = data.contents.map((ac: any) => new AccountItem(ac.accountItem));
|
||||
this.header = Parser.parseItem<AccountItemSectionHeader>(data.header, AccountItemSectionHeader);
|
||||
this.header = Parser.parseItem(data.header, AccountItemSectionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
class AccountItemSectionHeader extends YTNode {
|
||||
static type = 'AccountItemSectionHeader';
|
||||
|
||||
title: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import Parser from '..';
|
||||
import AccountChannel from './AccountChannel';
|
||||
import AccountItemSection from './AccountItemSection';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import AccountChannel from './AccountChannel.js';
|
||||
import AccountItemSection from './AccountItemSection.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
class AccountSectionList extends YTNode {
|
||||
static type = 'AccountSectionList';
|
||||
|
||||
contents;
|
||||
footers;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseItem<AccountItemSection>(data.contents[0], AccountItemSection);
|
||||
this.footers = Parser.parseItem<AccountChannel>(data.footers[0], AccountChannel);
|
||||
this.contents = Parser.parseItem(data.contents[0], AccountItemSection);
|
||||
this.footers = Parser.parseItem(data.footers[0], AccountChannel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/parser/classes/Alert.ts
Normal file
17
src/parser/classes/Alert.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
class Alert extends YTNode {
|
||||
static type = 'Alert';
|
||||
|
||||
text: Text;
|
||||
alert_type: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.text = new Text(data.text);
|
||||
this.alert_type = data.type;
|
||||
}
|
||||
}
|
||||
|
||||
export default Alert;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
class AudioOnlyPlayability extends YTNode {
|
||||
static type = 'AudioOnlyPlayability';
|
||||
|
||||
audio_only_availability: string;
|
||||
|
||||
constructor (data: any) {
|
||||
constructor (data: RawNode) {
|
||||
super();
|
||||
this.audio_only_availability = data.audioOnlyAvailability;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { YTNode } from '../helpers';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
class AutomixPreviewVideo extends YTNode {
|
||||
static type = 'AutomixPreviewVideo';
|
||||
|
||||
playlist_video?: { endpoint: NavigationEndpoint };
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
if (data?.content?.automixPlaylistVideoRenderer?.navigationEndpoint) {
|
||||
this.playlist_video = {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import { YTNode } from '../helpers';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class BackstageImage extends YTNode {
|
||||
static type = 'BackstageImage';
|
||||
|
||||
image: Thumbnail[];
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
this.endpoint = new NavigationEndpoint(data.command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Parser from '../index';
|
||||
import Author from './misc/Author';
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Parser from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import CommentActionButtons from './comments/CommentActionButtons.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class BackstagePost extends YTNode {
|
||||
static type = 'BackstagePost';
|
||||
@@ -12,19 +14,18 @@ class BackstagePost extends YTNode {
|
||||
author: Author;
|
||||
content: Text;
|
||||
published: Text;
|
||||
poll_status: string;
|
||||
vote_status: string;
|
||||
likes: Text;
|
||||
menu;
|
||||
actions;
|
||||
poll_status?: string;
|
||||
vote_status?: string;
|
||||
vote_count?: Text;
|
||||
menu?: Menu | null;
|
||||
action_buttons;
|
||||
vote_button;
|
||||
surface: string;
|
||||
endpoint: NavigationEndpoint;
|
||||
endpoint?: NavigationEndpoint;
|
||||
attachment;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.id = data.postId;
|
||||
|
||||
this.author = new Author({
|
||||
@@ -34,15 +35,40 @@ class BackstagePost extends YTNode {
|
||||
|
||||
this.content = new Text(data.contentText);
|
||||
this.published = new Text(data.publishedTimeText);
|
||||
this.poll_status = data.pollStatus;
|
||||
this.vote_status = data.voteStatus;
|
||||
this.likes = new Text(data.voteCount);
|
||||
this.menu = Parser.parse(data.actionMenu) || null;
|
||||
this.actions = Parser.parse(data.actionButtons);
|
||||
this.vote_button = Parser.parse(data.voteButton);
|
||||
|
||||
if (data.pollStatus) {
|
||||
this.poll_status = data.pollStatus;
|
||||
}
|
||||
|
||||
if (data.voteStatus) {
|
||||
this.vote_status = data.voteStatus;
|
||||
}
|
||||
|
||||
if (data.voteCount) {
|
||||
this.vote_count = new Text(data.voteCount);
|
||||
}
|
||||
|
||||
if (data.actionMenu) {
|
||||
this.menu = Parser.parseItem(data.actionMenu, Menu);
|
||||
}
|
||||
|
||||
if (data.actionButtons) {
|
||||
this.action_buttons = Parser.parseItem(data.actionButtons, CommentActionButtons);
|
||||
}
|
||||
|
||||
if (data.voteButton) {
|
||||
this.vote_button = Parser.parseItem(data.voteButton, CommentActionButtons);
|
||||
}
|
||||
|
||||
if (data.navigationEndpoint) {
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
}
|
||||
|
||||
if (data.backstageAttachment) {
|
||||
this.attachment = Parser.parseItem(data.backstageAttachment);
|
||||
}
|
||||
|
||||
this.surface = data.surface;
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.attachment = Parser.parse(data.backstageAttachment) || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class BackstagePostThread extends YTNode {
|
||||
static type = 'BackstagePostThread';
|
||||
@@ -8,7 +8,7 @@ class BackstagePostThread extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.post = Parser.parse(data.post);
|
||||
this.post = Parser.parseItem(data.post);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class BrowseFeedActions extends YTNode {
|
||||
static type = 'BrowseFeedActions';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import { YTNode } from '../helpers';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class BrowserMediaSession extends YTNode {
|
||||
static type = 'BrowserMediaSession';
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Button extends YTNode {
|
||||
static type = 'Button';
|
||||
|
||||
text: string;
|
||||
text?: string;
|
||||
|
||||
label;
|
||||
tooltip;
|
||||
icon_type;
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
icon_type?: string;
|
||||
is_disabled?: boolean;
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.text = new Text(data.text).toString();
|
||||
|
||||
if (data.text) {
|
||||
this.text = new Text(data.text).toString();
|
||||
}
|
||||
|
||||
if (data.accessibility?.label) {
|
||||
this.label = data.accessibility?.label;
|
||||
@@ -30,6 +34,10 @@ class Button extends YTNode {
|
||||
this.icon_type = data.icon?.iconType;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'isDisabled')) {
|
||||
this.is_disabled = data.isDisabled;
|
||||
}
|
||||
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import Parser from '../index';
|
||||
import Author from './misc/Author';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
import Button from './Button.js';
|
||||
import ChannelHeaderLinks from './ChannelHeaderLinks.js';
|
||||
import SubscribeButton from './SubscribeButton.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class C4TabbedHeader extends YTNode {
|
||||
static type = 'C4TabbedHeader';
|
||||
|
||||
author;
|
||||
banner;
|
||||
tv_banner;
|
||||
mobile_banner;
|
||||
subscribers;
|
||||
sponsor_button;
|
||||
subscribe_button;
|
||||
header_links;
|
||||
author: Author;
|
||||
banner?: Thumbnail[];
|
||||
tv_banner?: Thumbnail[];
|
||||
mobile_banner?: Thumbnail[];
|
||||
subscribers?: Text;
|
||||
videos_count?: Text;
|
||||
sponsor_button?: Button | null;
|
||||
subscribe_button?: SubscribeButton | Button | null;
|
||||
header_links?: ChannelHeaderLinks | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -23,13 +31,45 @@ class C4TabbedHeader extends YTNode {
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
}, data.badges, data.avatar);
|
||||
|
||||
this.banner = data.banner ? Thumbnail.fromResponse(data.banner) : [];
|
||||
this.tv_banner = data.tvBanner ? Thumbnail.fromResponse(data.tvBanner) : [];
|
||||
this.mobile_banner = data.mobileBanner ? Thumbnail.fromResponse(data.mobileBanner) : [];
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.sponsor_button = data.sponsorButton ? Parser.parseItem(data.sponsorButton) : undefined;
|
||||
this.subscribe_button = data.subscribeButton ? Parser.parseItem(data.subscribeButton) : undefined;
|
||||
this.header_links = data.headerLinks ? Parser.parse(data.headerLinks) : undefined;
|
||||
if (data.banner) {
|
||||
this.banner = Thumbnail.fromResponse(data.banner);
|
||||
}
|
||||
|
||||
if (data.tv_banner) {
|
||||
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
|
||||
}
|
||||
|
||||
if (data.mobile_banner) {
|
||||
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
|
||||
}
|
||||
|
||||
if (data.subscriberCountText) {
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
}
|
||||
|
||||
if (data.videosCountText) {
|
||||
this.videos_count = new Text(data.videosCountText);
|
||||
}
|
||||
|
||||
if (data.sponsorButton) {
|
||||
this.sponsor_button = Parser.parseItem(data.sponsorButton, Button);
|
||||
}
|
||||
|
||||
if (data.subscribeButton) {
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
}
|
||||
|
||||
if (data.headerLinks) {
|
||||
this.header_links = Parser.parseItem(data.headerLinks, ChannelHeaderLinks);
|
||||
}
|
||||
|
||||
if (data.channelHandleText) {
|
||||
this.channel_handle = new Text(data.channelHandleText);
|
||||
}
|
||||
|
||||
if (data.channelId) {
|
||||
this.channel_id = data.channelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class CallToActionButton extends YTNode {
|
||||
static type = 'CallToActionButton';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '../index';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Card extends YTNode {
|
||||
static type = 'Card';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class CardCollection extends YTNode {
|
||||
static type = 'CardCollection';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class CarouselHeader extends YTNode {
|
||||
static type = 'CarouselHeader';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
class CarouselItem extends YTNode {
|
||||
static type = 'CarouselItem';
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import Author from './misc/Author';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import SubscribeButton from './SubscribeButton.js';
|
||||
import Button from './Button.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Channel extends YTNode {
|
||||
static type = 'Channel';
|
||||
@@ -10,7 +16,10 @@ class Channel extends YTNode {
|
||||
author: Author;
|
||||
subscribers: Text;
|
||||
videos: Text;
|
||||
long_byline: Text;
|
||||
short_byline: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
subscribe_button: SubscribeButton | Button | null;
|
||||
description_snippet: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -22,11 +31,15 @@ class Channel extends YTNode {
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
}, data.ownerBadges, data.thumbnail);
|
||||
|
||||
// TODO: subscriberCountText is now the channel's handle and videoCountText is the subscriber count. Why haven't they renamed the properties?
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.videos = new Text(data.videoCountText);
|
||||
this.long_byline = new Text(data.longBylineText);
|
||||
this.short_byline = new Text(data.shortBylineText);
|
||||
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
|
||||
this.subscribe_button = Parser.parseItem(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.description_snippet = new Text(data.descriptionSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
export default Channel;
|
||||
export default Channel;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text';
|
||||
import Parser from '../index';
|
||||
import { YTNode } from '../helpers';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import Button from './Button.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelAboutFullMetadata extends YTNode {
|
||||
static type = 'ChannelAboutFullMetadata';
|
||||
@@ -11,13 +15,20 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
name: Text;
|
||||
avatar: Thumbnail[];
|
||||
canonical_channel_url: string;
|
||||
|
||||
primary_links: {
|
||||
endpoint: NavigationEndpoint;
|
||||
icon: Thumbnail[];
|
||||
title: Text;
|
||||
}[];
|
||||
|
||||
views: Text;
|
||||
joined: Text;
|
||||
description: Text;
|
||||
email_reveal: NavigationEndpoint;
|
||||
can_reveal_email: boolean;
|
||||
country: Text;
|
||||
buttons;
|
||||
buttons: Button[];
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -25,13 +36,20 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
this.name = new Text(data.title);
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.canonical_channel_url = data.canonicalChannelUrl;
|
||||
|
||||
this.primary_links = data.primaryLinks?.map((link: any) => ({
|
||||
endpoint: new NavigationEndpoint(link.navigationEndpoint),
|
||||
icon: Thumbnail.fromResponse(link.icon),
|
||||
title: new Text(link.title)
|
||||
})) ?? [];
|
||||
|
||||
this.views = new Text(data.viewCountText);
|
||||
this.joined = new Text(data.joinedDateText);
|
||||
this.description = new Text(data.description);
|
||||
this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand);
|
||||
this.can_reveal_email = !data.signInForBusinessEmail;
|
||||
this.country = new Text(data.country);
|
||||
this.buttons = Parser.parse(data.actionButtons);
|
||||
this.buttons = Parser.parseArray(data.actionButtons, Button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
src/parser/classes/ChannelAgeGate.ts
Normal file
30
src/parser/classes/ChannelAgeGate.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Parser } from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class ChannelAgeGate extends YTNode {
|
||||
static type = 'ChannelAgeGate';
|
||||
|
||||
channel_title: string;
|
||||
avatar: Thumbnail[];
|
||||
header: Text;
|
||||
main_text: Text;
|
||||
sign_in_button: Button | null;
|
||||
secondary_text: Text;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.channel_title = data.channelTitle;
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.header = new Text(data.header);
|
||||
this.main_text = new Text(data.mainText);
|
||||
this.sign_in_button = Parser.parseItem(data.signInButton, Button);
|
||||
this.secondary_text = new Text(data.secondaryText);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelAgeGate;
|
||||
@@ -1,6 +1,6 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
class ChannelFeaturedContent extends YTNode {
|
||||
static type = 'ChannelFeaturedContent';
|
||||
@@ -8,10 +8,10 @@ class ChannelFeaturedContent extends YTNode {
|
||||
title: Text;
|
||||
items;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.items = Parser.parse(data.items);
|
||||
this.items = Parser.parseArray(data.items);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import { YTNode } from '../helpers';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class HeaderLink {
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import { YTNode } from '../helpers';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelMetadata extends YTNode {
|
||||
static type = 'ChannelMetadata';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelMobileHeader extends YTNode {
|
||||
static type = 'ChannelMobileHeader';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelOptions extends YTNode {
|
||||
static type = 'ChannelOptions';
|
||||
|
||||
27
src/parser/classes/ChannelSubMenu.ts
Normal file
27
src/parser/classes/ChannelSubMenu.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import Parser from '../index.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelSubMenu extends YTNode {
|
||||
static type = 'ChannelSubMenu';
|
||||
|
||||
content_type_sub_menu_items: {
|
||||
endpoint: NavigationEndpoint;
|
||||
selected: boolean;
|
||||
title: string;
|
||||
}[];
|
||||
|
||||
sort_setting;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.content_type_sub_menu_items = data.contentTypeSubMenuItems.map((item: any) => ({
|
||||
endpoint: new NavigationEndpoint(item.navigationEndpoint || item.endpoint),
|
||||
selected: item.selected,
|
||||
title: item.title
|
||||
}));
|
||||
this.sort_setting = Parser.parseItem(data.sortSetting);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelSubMenu;
|
||||
@@ -1,6 +1,6 @@
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import { YTNode } from '../helpers';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelThumbnailWithLink extends YTNode {
|
||||
static type = 'ChannelThumbnailWithLink';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Text from './misc/Text';
|
||||
import { YTNode } from '../helpers';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelVideoPlayer extends YTNode {
|
||||
static type = 'ChannelVideoPlayer';
|
||||
|
||||
21
src/parser/classes/Chapter.ts
Normal file
21
src/parser/classes/Chapter.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Chapter extends YTNode {
|
||||
static type = 'Chapter';
|
||||
|
||||
title: Text;
|
||||
time_range_start_millis: number;
|
||||
thumbnail: Thumbnail[];
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.time_range_start_millis = data.timeRangeStartMillis;
|
||||
this.thumbnail = Thumbnail.fromResponse(data.thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
export default Chapter;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user