mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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
|
||||
|
||||
52
CHANGELOG.md
Normal file
52
CHANGELOG.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## [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))
|
||||
@@ -3,16 +3,16 @@
|
||||
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)
|
||||
- [Contributing to YouTube.js](#contributing-to-youtubejs)
|
||||
- [Issues](#issues)
|
||||
- [Create a new issue](#create-a-new-issue)
|
||||
- [Solve an issue](#solve-an-issue)
|
||||
- [Make changes](#make-changes)
|
||||
- [Commit your updates](#commit-your-updates)
|
||||
- [Pull Request](#pull-request)
|
||||
- [Test](#test)
|
||||
- [Lint](#lint)
|
||||
- [Build](#build)
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -66,16 +66,26 @@ npm run lint:fix
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
# Node
|
||||
npm run build:node
|
||||
|
||||
# Browser
|
||||
npm run build:browser
|
||||
npm run build:browser:prod
|
||||
# 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
|
||||
|
||||
```
|
||||
144
README.md
144
README.md
@@ -14,11 +14,11 @@
|
||||
|
||||
<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]
|
||||
@@ -84,7 +84,7 @@ ___
|
||||
|
||||
## 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 handles all the low-level communication with InnerTube, providing a simple, and efficient way to interact with YouTube programmatically. It is designed to emulate an actual client as closely as possible, including how API responses are parsed.
|
||||
|
||||
If you have any questions or need help, feel free to reach out to us on our [Discord server][discord] or open an issue [here](https://github.com/LuanRT/YouTube.js/issues).
|
||||
|
||||
@@ -112,7 +112,10 @@ 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:
|
||||
@@ -128,8 +131,13 @@ To use YouTube.js in the browser you must proxy requests through your own server
|
||||
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/).
|
||||
|
||||
```ts
|
||||
// Pre-bundled version for the web
|
||||
import { Innertube } from 'youtubei.js/bundle/browser';
|
||||
// We provide multiple exports 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 +155,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 */ });
|
||||
@@ -172,7 +180,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
|
||||
@@ -201,7 +210,7 @@ Our cache uses the `node:fs` module in Node-like environments, `Deno.writeFile`
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
// By default, cache stores files in the OS temp directory (or indexedDB in browsers).
|
||||
const yt = await Innertube.create({
|
||||
cache: new UniversalCache()
|
||||
cache: new UniversalCache(false)
|
||||
});
|
||||
|
||||
// You may wish to make the cache persistent (on Node and Deno)
|
||||
@@ -229,6 +238,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>
|
||||
@@ -252,8 +262,10 @@ 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>
|
||||
@@ -264,7 +276,7 @@ const yt = await Innertube.create({
|
||||
|
||||
Retrieves video info, including playback data and even layout elements such as menus, buttons, etc — all nicely parsed.
|
||||
|
||||
**Returns**: `Promise.<VideoInfo>`
|
||||
**Returns**: `Promise<VideoInfo>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -281,7 +293,7 @@ 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()`
|
||||
@@ -290,7 +302,7 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<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)`
|
||||
@@ -319,7 +331,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,7 +343,10 @@ 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 |
|
||||
| --- | --- | --- |
|
||||
@@ -358,7 +373,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 +383,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 +396,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>
|
||||
@@ -412,7 +430,10 @@ Retrieves YouTube's home feed.
|
||||
### 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 +442,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 +452,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 +471,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 +504,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 +523,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 +539,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 +564,45 @@ 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 will be deprecated in the future. It is recommended to retrieve streaming data from a `VideoInfo`/`TrackInfo` object instead if you want to select formats manually, see the example below.
|
||||
|
||||
```ts
|
||||
const info = await yt.getBasicInfo('somevideoid');
|
||||
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
|
||||
console.info('Playback url:', url);
|
||||
```
|
||||
|
||||
**Returns**: `Promise.<object>`
|
||||
**Returns**: `Promise<object>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -553,7 +613,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 +622,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 |
|
||||
| --- | --- | --- |
|
||||
@@ -617,7 +687,7 @@ import { Innertube, YTNodes } from 'youtubei.js';
|
||||
|
||||
if (button) {
|
||||
// After making sure it exists, we can call its navigation endpoint:
|
||||
const page = await button.endpoint.call(yt.actions);
|
||||
const page = await button.endpoint.call(yt.actions, { parse: true });
|
||||
console.info(page);
|
||||
}
|
||||
})();
|
||||
@@ -649,7 +719,7 @@ console.info('Header:', header);
|
||||
* 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)
|
||||
@@ -658,7 +728,7 @@ 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);
|
||||
```
|
||||
|
||||
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>
|
||||
|
||||
@@ -99,7 +114,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 +139,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 +157,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 +176,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 +188,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 +213,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 +234,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 +267,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 +321,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`
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
YouTube is constantly changing, so it is not rare 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 (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:
|
||||
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 similar to this:
|
||||
```
|
||||
InnertubeError: 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!
|
||||
@@ -17,7 +17,7 @@ This is a bug, want to help us fix it? Follow the instructions at https://github
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = 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.6.0",
|
||||
"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": "3.1.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
"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"
|
||||
]
|
||||
|
||||
@@ -5,30 +5,41 @@ const path = require('path');
|
||||
const import_list = [];
|
||||
|
||||
const json = [];
|
||||
const misc_exports = [];
|
||||
|
||||
glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname })
|
||||
.forEach((file) => {
|
||||
if (file.includes('/misc/')) return;
|
||||
// Trim path
|
||||
const is_misc = file.includes('/misc/');
|
||||
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);
|
||||
|
||||
if (is_misc) {
|
||||
const class_name = file.split('/').pop().replace('.js', '').replace('.ts', '');
|
||||
import_list.push(`import { default as ${class_name} } from './classes/${file}.js';`);
|
||||
misc_exports.push(class_name);
|
||||
} else {
|
||||
import_list.push(`import { default as ${import_name} } from './classes/${file}.js';
|
||||
export { ${import_name} };`);
|
||||
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 { YTNodeConstructor } from './helpers.js';
|
||||
|
||||
${import_list.join('\n')}
|
||||
|
||||
export const YTNodes = {
|
||||
const map: Record<string, YTNodeConstructor> = {
|
||||
${json.join(',\n ')}
|
||||
};
|
||||
|
||||
const map: Record<string, YTNodeConstructor> = YTNodes;
|
||||
export const Misc = {
|
||||
${misc_exports.join(',\n ')}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param name - Name of the node to be parsed
|
||||
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));
|
||||
|
||||
})();
|
||||
171
src/Innertube.ts
171
src/Innertube.ts
@@ -1,51 +1,58 @@
|
||||
|
||||
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 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, 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;
|
||||
session: Session;
|
||||
account: AccountManager;
|
||||
playlist: PlaylistManager;
|
||||
interact: InteractionManager;
|
||||
music: YTMusic;
|
||||
studio: YTStudio;
|
||||
kids: YTKids;
|
||||
actions: Actions;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.session = session;
|
||||
@@ -53,11 +60,12 @@ class Innertube {
|
||||
this.playlist = new PlaylistManager(this.session.actions);
|
||||
this.interact = new InteractionManager(this.session.actions);
|
||||
this.music = new YTMusic(this.session);
|
||||
this.studio = new Studio(this.session);
|
||||
this.studio = new YTStudio(this.session);
|
||||
this.kids = new YTKids(this.session);
|
||||
this.actions = this.session.actions;
|
||||
}
|
||||
|
||||
static async create(config: InnertubeConfig = {}) {
|
||||
static async create(config: InnertubeConfig = {}): Promise<Innertube> {
|
||||
return new Innertube(await Session.create(config));
|
||||
}
|
||||
|
||||
@@ -66,7 +74,9 @@ class Innertube {
|
||||
* @param video_id - The video id.
|
||||
* @param client - The client to use.
|
||||
*/
|
||||
async getInfo(video_id: string, client?: InnerTubeClient) {
|
||||
async getInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ video_id });
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.actions.getVideoInfo(video_id, cpn, client);
|
||||
@@ -81,7 +91,9 @@ 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);
|
||||
|
||||
@@ -93,7 +105,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 +117,7 @@ class Innertube {
|
||||
|
||||
const response = await this.actions.execute('/search', args);
|
||||
|
||||
return new Search(this.actions, response.data);
|
||||
return new Search(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +150,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 +165,58 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +232,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 +242,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,30 +264,42 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
115
src/core/Feed.ts
115
src/core/Feed.ts
@@ -1,57 +1,60 @@
|
||||
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 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,6 +63,10 @@ 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
|
||||
*/
|
||||
@@ -117,10 +124,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;
|
||||
}
|
||||
@@ -136,17 +143,17 @@ class Feed {
|
||||
* 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 +161,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 +203,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;
|
||||
@@ -1,34 +1,36 @@
|
||||
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 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';
|
||||
|
||||
class Music {
|
||||
#session;
|
||||
#actions;
|
||||
#session: Session;
|
||||
#actions: Actions;
|
||||
|
||||
constructor(session: Session) {
|
||||
this.#session = session;
|
||||
@@ -49,7 +51,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', {
|
||||
@@ -72,7 +74,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');
|
||||
|
||||
@@ -123,7 +125,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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +198,7 @@ class Music {
|
||||
browseId: album_id
|
||||
});
|
||||
|
||||
return new Album(response, this.#actions);
|
||||
return new Album(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,9 +234,9 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.[0];
|
||||
const tab = tabs?.first();
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -258,10 +260,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;
|
||||
@@ -280,7 +282,7 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
|
||||
@@ -289,6 +291,9 @@ class Music {
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
if (!page.contents)
|
||||
throw new InnertubeError('Unexpected response', page);
|
||||
|
||||
const shelves = page.contents.item().as(SectionList).contents.as(MusicCarouselShelf, MusicDescriptionShelf);
|
||||
|
||||
return shelves;
|
||||
@@ -307,7 +312,7 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
|
||||
|
||||
@@ -316,10 +321,14 @@ class Music {
|
||||
|
||||
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;
|
||||
|
||||
return section_list.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
@@ -339,14 +348,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 HTTPClient from '../utils/HTTPClient.js';
|
||||
import { Platform, DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
import Proto from '../proto/index.js';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
import { FetchFunction } from '../types/PlatformShim.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,31 +70,80 @@ 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;
|
||||
/**
|
||||
* 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;
|
||||
@@ -115,38 +179,65 @@ export default class Session extends EventEmitterLike {
|
||||
options.lang,
|
||||
options.location,
|
||||
options.account_index,
|
||||
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,
|
||||
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 });
|
||||
} else {
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode }, 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;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
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('/', '.')}`
|
||||
}
|
||||
});
|
||||
|
||||
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(/^\)\]\}'/, ''));
|
||||
@@ -160,22 +251,22 @@ export default class Session extends EventEmitterLike {
|
||||
const context: Context = {
|
||||
client: {
|
||||
hl: device_info[0],
|
||||
gl: location || 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,
|
||||
@@ -184,6 +275,7 @@ export default class Session extends EventEmitterLike {
|
||||
utcOffsetMinutes: new Date().getTimezoneOffset()
|
||||
},
|
||||
user: {
|
||||
enableSafetyMode: options.enable_safety_mode,
|
||||
lockedSafetyMode: false
|
||||
},
|
||||
request: {
|
||||
@@ -191,7 +283,53 @@ 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
|
||||
}): SessionData {
|
||||
const id = generateRandomString(11);
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const context: Context = {
|
||||
client: {
|
||||
hl: options.lang || 'en',
|
||||
gl: options.location || 'US',
|
||||
screenDensityFloat: 1,
|
||||
screenHeightPoints: 1080,
|
||||
screenPixelDensity: 1,
|
||||
screenWidthPoints: 1920,
|
||||
visitorData: Proto.encodeVisitorData(id, timestamp),
|
||||
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> {
|
||||
@@ -225,7 +363,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.');
|
||||
|
||||
@@ -235,35 +376,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 getTabByName(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,11 +32,11 @@ class TabbedFeed extends Feed {
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
return new TabbedFeed(this.#actions, response.data, false);
|
||||
return new TabbedFeed<T>(this.#actions, response, false);
|
||||
}
|
||||
|
||||
async getTabByURL(url: string) {
|
||||
const tab = this.#tabs.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
|
||||
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`);
|
||||
@@ -42,11 +46,15 @@ class TabbedFeed extends Feed {
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
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();
|
||||
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,6 +1,19 @@
|
||||
# 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.
|
||||
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data.
|
||||
|
||||
Structure:
|
||||
|
||||
* [`/classes`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes) - InnerTube nodes.
|
||||
* [`/types`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/types) - General response types.
|
||||
* [`/youtube`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube) - Contains the logic for parsing YouTube responses.
|
||||
* [`/ytmusic`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytmusic) - Contains the logic for parsing YouTube Music responses.
|
||||
* [`/ytkids`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytkids) - Contains the logic for parsing YouTube Kids responses.
|
||||
* [`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.
|
||||
* [`map.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/map.ts) - A list of all InnerTube nodes, it is used to determine which node to use for a given renderer. Note that this file is auto-generated and should not be edited manually.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
@@ -328,4 +341,4 @@ And what we get after parsing it:
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
</details>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
class AccountChannel extends YTNode {
|
||||
static type = 'AccountChannel';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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';
|
||||
|
||||
class AccountItem {
|
||||
static type = 'AccountItem';
|
||||
|
||||
@@ -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 AccountItemSectionHeader extends YTNode {
|
||||
static type = 'AccountItemSectionHeader';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Parser from '..';
|
||||
import AccountChannel from './AccountChannel';
|
||||
import AccountItemSection from './AccountItemSection';
|
||||
import Parser from '../index.js';
|
||||
import AccountChannel from './AccountChannel.js';
|
||||
import AccountItemSection from './AccountItemSection.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class AccountSectionList extends YTNode {
|
||||
static type = 'AccountSectionList';
|
||||
|
||||
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';
|
||||
|
||||
class Alert extends YTNode {
|
||||
static type = 'Alert';
|
||||
|
||||
text: Text;
|
||||
alert_type: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.text = new Text(data.text);
|
||||
this.alert_type = data.type;
|
||||
}
|
||||
}
|
||||
|
||||
export default Alert;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class AudioOnlyPlayability extends YTNode {
|
||||
static type = 'AudioOnlyPlayability';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { YTNode } from '../helpers';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
class AutomixPreviewVideo extends YTNode {
|
||||
static type = 'AutomixPreviewVideo';
|
||||
|
||||
@@ -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 type CommentActionButtons from './comments/CommentActionButtons.js';
|
||||
import type 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<Menu>(data.actionMenu);
|
||||
}
|
||||
|
||||
if (data.actionButtons) {
|
||||
this.action_buttons = Parser.parseItem<CommentActionButtons>(data.actionButtons);
|
||||
}
|
||||
|
||||
if (data.voteButton) {
|
||||
this.vote_button = Parser.parseItem(data.voteButton);
|
||||
}
|
||||
|
||||
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,7 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Parser from '../index';
|
||||
import Author from './misc/Author';
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Parser from '../index.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
import type Button from './Button';
|
||||
import type ChannelHeaderLinks from './ChannelHeaderLinks';
|
||||
import type SubscribeButton from './SubscribeButton';
|
||||
import type Button from './Button.js';
|
||||
import type ChannelHeaderLinks from './ChannelHeaderLinks.js';
|
||||
import type SubscribeButton from './SubscribeButton.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class C4TabbedHeader extends YTNode {
|
||||
static type = 'C4TabbedHeader';
|
||||
|
||||
author: Author;
|
||||
banner: Thumbnail[];
|
||||
tv_banner: Thumbnail[];
|
||||
mobile_banner: Thumbnail[];
|
||||
subscribers: Text;
|
||||
videos_count: Text;
|
||||
sponsor_button: Button | null;
|
||||
subscribe_button: SubscribeButton | null;
|
||||
header_links: ChannelHeaderLinks | null;
|
||||
channel_handle: Text;
|
||||
channel_id: string;
|
||||
banner?: Thumbnail[];
|
||||
tv_banner?: Thumbnail[];
|
||||
mobile_banner?: Thumbnail[];
|
||||
subscribers?: Text;
|
||||
videos_count?: Text;
|
||||
sponsor_button?: Button | null;
|
||||
subscribe_button?: SubscribeButton | null;
|
||||
header_links?: ChannelHeaderLinks | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -31,16 +31,45 @@ class C4TabbedHeader extends YTNode {
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
}, data.badges, data.avatar);
|
||||
|
||||
this.banner = Thumbnail.fromResponse(data.banner);
|
||||
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
|
||||
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.videos_count = new Text(data.videosCountText);
|
||||
this.sponsor_button = Parser.parseItem<Button>(data.sponsorButton);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
|
||||
this.channel_handle = new Text(data.channelHandleText);
|
||||
this.channel_id = data.channelId;
|
||||
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<Button>(data.sponsorButton);
|
||||
}
|
||||
|
||||
if (data.subscribeButton) {
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
}
|
||||
|
||||
if (data.headerLinks) {
|
||||
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
|
||||
}
|
||||
|
||||
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,12 +1,12 @@
|
||||
import Parser from '..';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import Text from './misc/Text';
|
||||
import Author from './misc/Author';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text.js';
|
||||
import Author from './misc/Author.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import type SubscribeButton from './SubscribeButton';
|
||||
import type SubscribeButton from './SubscribeButton.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class Channel extends YTNode {
|
||||
static type = 'Channel';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Parser from '../index';
|
||||
import Parser from '../index.js';
|
||||
|
||||
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 type Button from './Button';
|
||||
import type Button from './Button.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelAboutFullMetadata extends YTNode {
|
||||
static type = 'ChannelAboutFullMetadata';
|
||||
@@ -37,11 +37,11 @@ class ChannelAboutFullMetadata extends YTNode {
|
||||
this.avatar = Thumbnail.fromResponse(data.avatar);
|
||||
this.canonical_channel_url = data.canonicalChannelUrl;
|
||||
|
||||
this.primary_links = data.primaryLinks.map((link: any) => ({
|
||||
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);
|
||||
|
||||
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<Button>(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 Parser from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChannelFeaturedContent extends YTNode {
|
||||
static type = 'ChannelFeaturedContent';
|
||||
|
||||
@@ -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;
|
||||
@@ -1,8 +1,8 @@
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import Text from './misc/Text.js';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils';
|
||||
import { YTNode } from '../helpers';
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChildVideo extends YTNode {
|
||||
static type = 'ChildVideo';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Parser from '../index';
|
||||
import Button from './Button';
|
||||
import ChipCloudChip from './ChipCloudChip';
|
||||
import Parser from '../index.js';
|
||||
import Button from './Button.js';
|
||||
import ChipCloudChip from './ChipCloudChip.js';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
class ChipCloud extends YTNode {
|
||||
static type = 'ChipCloud';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
class ChipCloudChip extends YTNode {
|
||||
static type = 'ChipCloudChip';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user