mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db41fa40d2 | ||
|
|
02ece1ddda | ||
|
|
b175e02f6d | ||
|
|
d3394f846a | ||
|
|
07b73ab78d | ||
|
|
d743b5a088 | ||
|
|
bb206c044c | ||
|
|
d48065405d | ||
|
|
dbc8b62ba2 | ||
|
|
e32981728b | ||
|
|
7b33dcbb79 | ||
|
|
4c6bf49bbe | ||
|
|
4bbc2d50f4 | ||
|
|
440d80063d | ||
|
|
c49147523a | ||
|
|
e221c79448 | ||
|
|
291d04e703 | ||
|
|
12baec0b0d | ||
|
|
b793c61fd8 | ||
|
|
b9e15b5fbd | ||
|
|
d0c54f2b8b | ||
|
|
6ff984df66 | ||
|
|
4fa2e5c127 | ||
|
|
725f186bd9 | ||
|
|
07340931a0 | ||
|
|
46d62bf83f | ||
|
|
c28da62ec1 | ||
|
|
c7fc18b516 |
151
README.md
151
README.md
@@ -66,23 +66,18 @@ And big thanks to [@gatecrasher777](https://github.com/gatecrasher777/ytcog) for
|
||||
|
||||
As of now, this is one of the most advanced & stable YouTube libraries out there, here's a short summary of its features:
|
||||
|
||||
- Search videos, playlists, music, albums etc
|
||||
- Get detailed info about any video or playlist
|
||||
- Fetch live chat & live stats in real time
|
||||
- Change notification preferences for a channel
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment etc
|
||||
- Easily sign in to any Google Account
|
||||
- Change an account's settings.
|
||||
- Get subscriptions/home feed
|
||||
- Get notifications
|
||||
- Get watch history
|
||||
- Download videos
|
||||
- Search videos, playlists, music, albums etc.
|
||||
- Subscribe/Unsubscribe/Like/Dislike/Comment etc.
|
||||
- Get subscriptions/home feed, notifications and watch history.
|
||||
- Easily sign in to any Google Account.
|
||||
- Fetch live chat & live stats.
|
||||
- Manage account settings.
|
||||
- Create/delete playlists.
|
||||
- Download videos.
|
||||
|
||||
Do note that you must be signed-in to perform actions that involve an account, such as commenting, subscribing, sending messages to a live chat, etc.
|
||||
~ And more!
|
||||
|
||||
### Do I need an API key to use this?
|
||||
|
||||
No, YouTube.js does not use any official API so no API keys are required.
|
||||
Do note that you must be signed-in to perform actions that involve an account; such as commenting, liking/disliking videos, sending messages to a live chat, etc.
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
## Getting Started
|
||||
@@ -114,7 +109,7 @@ And to make things faster, you should do this only once and reuse the Innertube
|
||||
|
||||
```js
|
||||
const Innertube = require('youtubei.js');
|
||||
const youtube = await new Innertube();
|
||||
const youtube = await new Innertube({ gl: 'US' }); // all parameters are optional.
|
||||
```
|
||||
|
||||
### Doing a simple search
|
||||
@@ -180,7 +175,7 @@ const search = await youtube.search('Interstellar Main Theme', { client: 'YTMUSI
|
||||
corrected_query:string,
|
||||
results:{
|
||||
top_result:[Array], // Can be anything; video, playlist, artist etc..
|
||||
songs:[
|
||||
songs:[
|
||||
{
|
||||
id:string,
|
||||
title:string,
|
||||
@@ -337,13 +332,14 @@ const video = await youtube.getDetails('VIDEO_ID');
|
||||
### Get comments:
|
||||
|
||||
```js
|
||||
const response = await youtube.getComments('VIDEO_ID');
|
||||
// Sorting options: `TOP_COMMENTS` and `NEWEST_FIRST`
|
||||
const comments = await youtube.getComments('VIDEO_ID', 'TOP_COMMENTS');
|
||||
```
|
||||
Alternatively you can use:
|
||||
|
||||
```js
|
||||
const video = await youtube.getDetails('VIDEO_ID');
|
||||
const response = await video.getComments();
|
||||
const comments = await video.getComments();
|
||||
```
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
@@ -351,12 +347,14 @@ const response = await video.getComments();
|
||||
|
||||
```js
|
||||
{
|
||||
comments: [
|
||||
page_count: number,
|
||||
comment_count: number,
|
||||
items: [
|
||||
{
|
||||
text: string,
|
||||
author: {
|
||||
name: string,
|
||||
thumbnail: [
|
||||
thumbnails: [
|
||||
{
|
||||
url: string,
|
||||
width: number,
|
||||
@@ -371,35 +369,36 @@ const response = await video.getComments();
|
||||
is_disliked: boolean,
|
||||
is_pinned: boolean,
|
||||
is_channel_owner: boolean,
|
||||
is_reply: boolean,
|
||||
like_count: number,
|
||||
reply_count: number,
|
||||
id: string
|
||||
}
|
||||
},
|
||||
//...
|
||||
],
|
||||
comment_count: string // not available in continuations
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Reply to, like and dislike comments:
|
||||
Reply to, like/dislike and report a comment:
|
||||
```js
|
||||
await response.comments[0].like();
|
||||
await response.comments[0].dislike();
|
||||
await response.comments[0].reply('Nice comment!');
|
||||
await comments.items[0].like();
|
||||
await comments.items[0].dislike();
|
||||
await comments.items[0].report();
|
||||
await comments.items[0].reply('Nice comment!');
|
||||
```
|
||||
|
||||
Get comment replies:
|
||||
Comment replies:
|
||||
```js
|
||||
const replies = await response.comments[0].getReplies();
|
||||
const replies = await comments.items[0].getReplies();
|
||||
```
|
||||
|
||||
Get comments/replies continuation:
|
||||
Comments/replies continuation:
|
||||
```js
|
||||
const continuation = await response.getContinuation();
|
||||
const continuation = await comments.getContinuation();
|
||||
const replies_continuation = await replies.getContinuation();
|
||||
```
|
||||
|
||||
@@ -454,7 +453,7 @@ const homefeed = await youtube.getHomeFeed();
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get continuation:
|
||||
Continuation:
|
||||
```js
|
||||
const continuation = await homefeed.getContinuation();
|
||||
````
|
||||
@@ -515,7 +514,7 @@ const history = await youtube.getHistory();
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get continuation:
|
||||
Continuation:
|
||||
```js
|
||||
const continuation = await history.getContinuation();
|
||||
````
|
||||
@@ -576,11 +575,48 @@ const mysubsfeed = await youtube.getSubscriptionsFeed();
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get continuation:
|
||||
Continuation:
|
||||
```js
|
||||
const continuation = await mysubsfeed.getContinuation();
|
||||
````
|
||||
|
||||
### Get trending content:
|
||||
|
||||
```js
|
||||
const trending = await youtube.getTrending();
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
<p>
|
||||
|
||||
```js
|
||||
{
|
||||
now: {
|
||||
content: [
|
||||
{
|
||||
title: string,
|
||||
videos: []
|
||||
},
|
||||
//...
|
||||
]
|
||||
},
|
||||
// Other categories require an additional call to fetch videos
|
||||
music: { getVideos: Promise.<Array> },
|
||||
gaming: { getVideos: Promise.<Array> },
|
||||
movies: { getVideos: Promise.<Array> }
|
||||
}
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
### Get song lyrics:
|
||||
```js
|
||||
const search = await youtube.search('Never give you up', { client: 'YTMUSIC' });
|
||||
const lyrics = await youtube.getLyrics(search.results.songs[0].id);
|
||||
```
|
||||
|
||||
### Get notifications:
|
||||
|
||||
```js
|
||||
@@ -620,21 +656,15 @@ const notifications = await youtube.getNotifications();
|
||||
</p>
|
||||
</details>
|
||||
|
||||
Get continuation:
|
||||
Continuation:
|
||||
```js
|
||||
const continuation = await notifications.getContinuation();
|
||||
````
|
||||
|
||||
### Get unseen notifications count:
|
||||
Unseen notifications count:
|
||||
|
||||
```js
|
||||
const notifications = await youtube.getUnseenNotificationsCount();
|
||||
```
|
||||
|
||||
### Get song lyrics:
|
||||
```js
|
||||
const search = await youtube.search('Never give you up', { client: 'YTMUSIC' });
|
||||
const lyrics = await youtube.getLyrics(search.results.songs[0].id);
|
||||
const unread_notis_count = await youtube.getUnseenNotificationsCount();
|
||||
```
|
||||
|
||||
### Get playlist:
|
||||
@@ -732,12 +762,24 @@ The library makes it easy to interact with YouTube programmatically. However, do
|
||||
```js
|
||||
await youtube.interact.comment('VIDEO_ID', 'Haha, nice video!');
|
||||
```
|
||||
|
||||
* Change notification preferences:
|
||||
```js
|
||||
// Options: ALL | NONE | PERSONALIZED
|
||||
await youtube.interact.changeNotificationPreferences('CHANNEL_ID', 'ALL');
|
||||
```
|
||||
|
||||
* Playlists:
|
||||
```js
|
||||
// Create a playlist:
|
||||
await youtube.playlist.create('NAME', 'VIDEO_ID');
|
||||
|
||||
// Delete a playlist:
|
||||
await youtube.playlist.delete('PLAYLIST_ID');
|
||||
|
||||
// Add videos to a playlist:
|
||||
await youtube.playlist.addVideos('PLAYLIST_ID', [ 'VIDEO_ID1', 'VIDEO_ID2' ]);
|
||||
```
|
||||
|
||||
* Change notification preferences:
|
||||
```js
|
||||
// Options: ALL | NONE | PERSONALIZED
|
||||
await youtube.interact.setNotificationPreferences('CHANNEL_ID', 'ALL');
|
||||
```
|
||||
|
||||
These methods will always return ```{ success: true, status_code: 200 }``` if successful.
|
||||
|
||||
@@ -1025,6 +1067,13 @@ async function start() {
|
||||
|
||||
start();
|
||||
```
|
||||
Sign-out:
|
||||
```js
|
||||
const response = await youtube.signOut();
|
||||
if (response.success) {
|
||||
console.log('You have successfully signed out');
|
||||
}
|
||||
```
|
||||
|
||||
#### Cookies:
|
||||
|
||||
@@ -1032,7 +1081,7 @@ start();
|
||||
const Innertube = require('youtubei.js');
|
||||
|
||||
async function start() {
|
||||
const youtube = await new Innertube(COOKIE_HERE);
|
||||
const youtube = await new Innertube({ cookie: '...' });
|
||||
//...
|
||||
}
|
||||
|
||||
@@ -1064,4 +1113,4 @@ Should you have any questions or concerns please contact me directly via email.
|
||||
## License
|
||||
Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License.
|
||||
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
<p align="right">(<a href="#top">back to top</a>)</p>
|
||||
834
lib/Innertube.js
834
lib/Innertube.js
File diff suppressed because it is too large
Load Diff
@@ -3,18 +3,19 @@
|
||||
const Uuid = require('uuid');
|
||||
const Axios = require('axios');
|
||||
const Proto = require('../proto');
|
||||
const Utils = require('../utils/Utils');
|
||||
const Constants = require('../utils/Constants');
|
||||
|
||||
/**
|
||||
* Performs direct interactions on YouTube.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} engagement_type - Type of engagement.
|
||||
* @param {object} args - Engagement arguments.
|
||||
* @param {Innertube} session
|
||||
* @param {string} engagement_type
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function engage(session, engagement_type, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed in');
|
||||
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
|
||||
|
||||
const data = { context: session.context };
|
||||
switch (engagement_type) {
|
||||
@@ -40,16 +41,30 @@ async function engage(session, engagement_type, args = {}) {
|
||||
break;
|
||||
case 'comment/perform_comment_action':
|
||||
const action = ({
|
||||
like: () => Proto.encodeCommentActionParams(5, args.comment_id, args.video_id, args.channel_id),
|
||||
dislike: () => Proto.encodeCommentActionParams(4, args.comment_id, args.video_id, args.channel_id),
|
||||
like: () => Proto.encodeCommentActionParams(5, args.comment_id, args.video_id),
|
||||
dislike: () => Proto.encodeCommentActionParams(4, args.comment_id, args.video_id),
|
||||
})[args.comment_action]();
|
||||
data.actions = [action];
|
||||
data.actions = [ action ];
|
||||
break;
|
||||
case 'playlist/create':
|
||||
data.title = args.title;
|
||||
data.videoIds = [args.video_id];
|
||||
break;
|
||||
case 'playlist/delete':
|
||||
data.playlistId = args.playlist_id;
|
||||
break;
|
||||
case 'browse/edit_playlist':
|
||||
data.playlistId = args.playlist_id;
|
||||
data.actions = args.video_ids.map((id) => ({
|
||||
action: args.action,
|
||||
addedVideoId: id
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
throw new Utils.InnertubeError('Invalid action', engagement_type);
|
||||
}
|
||||
|
||||
const response = await session.YTRequester.post(`/${engagement_type}`, JSON.stringify(data)).catch((error) => error);
|
||||
|
||||
const response = await session.request.post(`/${engagement_type}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -61,16 +76,15 @@ async function engage(session, engagement_type, args = {}) {
|
||||
/**
|
||||
* Accesses YouTube's various sections.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action - Type of action.
|
||||
* @param {object} args - Action argumenets.
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function browse(session, action, args = {}) {
|
||||
if (!session.logged_in && action != 'home_feed' &&
|
||||
action !== 'lyrics' && action !== 'music_playlist' &&
|
||||
action !== 'playlist')
|
||||
throw new Error('You are not signed in');
|
||||
if (!session.logged_in && ![ 'home_feed', 'lyrics',
|
||||
'music_playlist', 'playlist', 'trending' ].includes(action))
|
||||
throw new Utils.InnertubeError('You are not signed in');
|
||||
|
||||
const data = { context: session.context };
|
||||
switch (action) {
|
||||
@@ -86,6 +100,10 @@ async function browse(session, action, args = {}) {
|
||||
case 'home_feed':
|
||||
data.browseId = 'FEwhat_to_watch';
|
||||
break;
|
||||
case 'trending':
|
||||
data.browseId = 'FEtrending';
|
||||
args.params && (data.params = args.params);
|
||||
break;
|
||||
case 'subscriptions_feed':
|
||||
data.browseId = 'FEsubscriptions';
|
||||
break;
|
||||
@@ -96,7 +114,7 @@ async function browse(session, action, args = {}) {
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
|
||||
data.context = context;
|
||||
data.browseId = args.browse_id;
|
||||
break;
|
||||
@@ -110,9 +128,8 @@ async function browse(session, action, args = {}) {
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
|
||||
const response = await requester.post('/browse', JSON.stringify(data)).catch((error) => error);
|
||||
|
||||
const response = await session.request.post('/browse', JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -122,17 +139,49 @@ async function browse(session, action, args = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoints used to report content.
|
||||
*
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function flag(session, action, args = {}) {
|
||||
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
|
||||
const data = { context: session.context };
|
||||
|
||||
switch (action) {
|
||||
case 'flag/flag':
|
||||
data.action = args.action;
|
||||
break;
|
||||
case 'flag/get_form':
|
||||
data.params = args.params;
|
||||
break;
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const response = await session.request.post(`/${action}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Account settings endpoints.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action - Type of action.
|
||||
* @param {object} args - Action argumenets.
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function account(session, action, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed in');
|
||||
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
|
||||
|
||||
const data = {};
|
||||
switch (action) {
|
||||
@@ -147,10 +196,10 @@ async function account(session, action, args = {}) {
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const response = await session.YTRequester.post(`/${action}`, JSON.stringify(data)).catch((error) => error);
|
||||
|
||||
const response = await session.request.post(`/${action}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
@@ -161,10 +210,10 @@ async function account(session, action, args = {}) {
|
||||
/**
|
||||
* Accesses YouTube Music endpoints (/youtubei/v1/music/).
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action - Type of action.
|
||||
* @param {object} args - Action arguments.
|
||||
* @todo Implement more actions.
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @todo Implement more endpoints.
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function music(session, action, args) {
|
||||
@@ -174,8 +223,7 @@ async function music(session, action, args) {
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
let data;
|
||||
|
||||
let data = {};
|
||||
switch (action) {
|
||||
case 'get_search_suggestions':
|
||||
data.context = context;
|
||||
@@ -184,8 +232,8 @@ async function music(session, action, args) {
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const response = await session.YTMRequester.post(`/music/${action}`, JSON.stringify(data)).catch((error) => error);
|
||||
|
||||
const response = await session.request.post(`/music/${action}`, JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -198,7 +246,7 @@ async function music(session, action, args) {
|
||||
/**
|
||||
* Searches a given query on YouTube/YTMusic.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {Innertube} session
|
||||
* @param {string} client - YouTube client: YOUTUBE | YTMUSIC
|
||||
* @param {object} args - Search arguments.
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
@@ -220,16 +268,15 @@ async function search(session, client, args = {}) {
|
||||
context.client.originalUrl = Constants.URLS.YT_MUSIC;
|
||||
context.client.clientVersion = Constants.YTMUSIC_VERSION;
|
||||
context.client.clientName = 'WEB_REMIX';
|
||||
|
||||
|
||||
data.context = context;
|
||||
data.query = args.query;
|
||||
break;
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid client', action);
|
||||
throw new Utils.InnertubeError('Invalid client', client);
|
||||
}
|
||||
|
||||
const requester = client == 'YOUTUBE' && session.YTRequester || session.YTMRequester;
|
||||
const response = await requester.post('/search', JSON.stringify(data)).catch((error) => error);
|
||||
|
||||
const response = await session.request.post('/search', JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
@@ -242,13 +289,13 @@ async function search(session, client, args = {}) {
|
||||
/**
|
||||
* Interacts with YouTube's notification system.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action - Type of action.
|
||||
* @param {object} args - Action arguments.
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function notifications(session, action, args = {}) {
|
||||
if (!session.logged_in) throw new Error('You are not signed in');
|
||||
if (!session.logged_in) throw new Utils.InnertubeError('You are not signed in');
|
||||
|
||||
const data = {};
|
||||
switch (action) {
|
||||
@@ -268,8 +315,8 @@ async function notifications(session, action, args = {}) {
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const response = await session.YTRequester.post(`/notification/${action}`, JSON.stringify(data)).catch((err) => err);
|
||||
|
||||
const response = await session.request.post(`/notification/${action}`, JSON.stringify(data)).catch((err) => err);
|
||||
if (response instanceof Error) return { success: false, status_code: response.response?.status || 0, message: response.message };
|
||||
if (action === 'modify_channel_preference') return { success: true, status_code: response.status };
|
||||
|
||||
@@ -283,9 +330,9 @@ async function notifications(session, action, args = {}) {
|
||||
/**
|
||||
* Interacts with YouTube's livechat system.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {string} action - Type of action.
|
||||
* @param {object} args - Action arguments.
|
||||
* @param {Innertube} session
|
||||
* @param {string} action
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; data: object; message?: string }>}
|
||||
*/
|
||||
async function livechat(session, action, args = {}) {
|
||||
@@ -298,7 +345,7 @@ async function livechat(session, action, args = {}) {
|
||||
case 'live_chat/send_message':
|
||||
data.context = session.context;
|
||||
data.params = Proto.encodeMessageParams(args.channel_id, args.video_id);
|
||||
data.clientMessageId = `ytjs-${Uuid.v4()}`;
|
||||
data.clientMessageId = Uuid.v4();
|
||||
data.richMessage = {
|
||||
textSegments: [{ text: args.text }]
|
||||
}
|
||||
@@ -318,18 +365,18 @@ async function livechat(session, action, args = {}) {
|
||||
default:
|
||||
throw new Utils.InnertubeError('Invalid action', action);
|
||||
}
|
||||
|
||||
const response = await session.YTRequester.post(`/${action}`, JSON.stringify(data)).catch((err) => err);
|
||||
|
||||
const response = await session.request.post(`/${action}`, JSON.stringify(data)).catch((err) => err);
|
||||
if (response instanceof Error) return { success: false, message: response.message };
|
||||
|
||||
|
||||
return { success: true, data: response.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests continuation for previously performed actions.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {object} args - Continuation arguments.
|
||||
* @param {Innertube} session
|
||||
* @param {object} args
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function next(session, args = {}) {
|
||||
@@ -356,15 +403,9 @@ async function next(session, args = {}) {
|
||||
data.captionsRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
const requester = args.ytmusic && session.YTMRequester || session.YTRequester;
|
||||
const response = await requester.post('/next', JSON.stringify(data)).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) return {
|
||||
success: false,
|
||||
status_code: response.response?.status || 0,
|
||||
message: response.message
|
||||
};
|
||||
|
||||
const response = await session.request.post('/next', JSON.stringify(data)).catch((error) => error);
|
||||
if (response instanceof Error) return { success: false, status_code: response?.status || 0, message: response.message };
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -376,38 +417,45 @@ async function next(session, args = {}) {
|
||||
/**
|
||||
* Retrieves video data.
|
||||
*
|
||||
* @param {Innertube} session - A valid Innertube session.
|
||||
* @param {object} args - Request arguments.
|
||||
* @param {Innertube} session
|
||||
* @param {object} args
|
||||
* @returns {Promise.<object>} - Video data.
|
||||
*/
|
||||
async function getVideoInfo(session, args = {}) {
|
||||
const response = await session.YTRequester.post(`/player`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context))).catch((err) => err);
|
||||
if (response instanceof Error) throw new Error(`Could not get video info: ${response.message}`);
|
||||
const response = await session.request.post(`/player`, JSON.stringify(Constants.VIDEO_INFO_REQBODY(args.id, session.sts, session.context))).catch((err) => err);
|
||||
if (response instanceof Error) throw new Utils.InnertubeError(`Could not get video info: ${response.message}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets search suggestions.
|
||||
*
|
||||
* @param {Innertube} session - A valid innertube session
|
||||
* @param {string} query - Search query
|
||||
* @param {Innertube} session
|
||||
* @param {string} query
|
||||
* @returns {Promise.<{ success: boolean; status_code: number; data: object; message?: string }>}
|
||||
*/
|
||||
async function getYTSearchSuggestions(session, query) {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_SUGGESTIONS}search?client=firefox&ds=yt&q=${encodeURIComponent(query)}`,
|
||||
Constants.DEFAULT_HEADERS(session)).catch((error) => error);
|
||||
|
||||
if (response instanceof Error) return {
|
||||
success: false,
|
||||
status_code: response.status,
|
||||
message: response.message
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data
|
||||
};
|
||||
async function getSearchSuggestions(session, client, input) {
|
||||
if (!['YOUTUBE', 'YTMUSIC'].includes(client))
|
||||
throw new Utils.InnertubeError('Invalid client', client);
|
||||
|
||||
const response = await ({
|
||||
'YOUTUBE': async () => {
|
||||
const response = await Axios.get(`${Constants.URLS.YT_SUGGESTIONS}search?client=firefox&ds=yt&q=${encodeURIComponent(input)}`,
|
||||
Constants.DEFAULT_HEADERS(session.config)).catch((error) => error);
|
||||
|
||||
return {
|
||||
success: !(response instanceof Error),
|
||||
status_code: response.status,
|
||||
data: response?.data
|
||||
};
|
||||
},
|
||||
'YTMUSIC': async () => {
|
||||
const response = await music(session, 'get_search_suggestions', { input });
|
||||
return response;
|
||||
}
|
||||
}[client])();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
module.exports = { engage, browse, account, music, search, notifications, livechat, getVideoInfo, next, getYTSearchSuggestions };
|
||||
module.exports = { engage, browse, account, flag, music, search, notifications, livechat, getVideoInfo, next, getSearchSuggestions };
|
||||
@@ -2,34 +2,43 @@
|
||||
|
||||
const Axios = require('axios');
|
||||
const Constants = require('../utils/Constants');
|
||||
const EventEmitter = require('events');
|
||||
const Uuid = require('uuid');
|
||||
|
||||
class OAuth extends EventEmitter {
|
||||
constructor(auth_info) {
|
||||
super();
|
||||
this.auth_info = auth_info;
|
||||
this.refresh_interval = 5;
|
||||
|
||||
this.oauth_code_url = `${Constants.URLS.YT_BASE}/o/oauth2/device/code`;
|
||||
this.oauth_token_url = `${Constants.URLS.YT_BASE}/o/oauth2/token`;
|
||||
|
||||
this.model_name = Constants.OAUTH.MODEL_NAME;
|
||||
this.grant_type = Constants.OAUTH.GRANT_TYPE;
|
||||
this.scope = Constants.OAUTH.SCOPE;
|
||||
|
||||
this.auth_script_regex = /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/;
|
||||
this.identity_regex = /.+?={};var .+?={clientId:\"(?<id>.+?)\",.+?:\"(?<secret>.+?)\"},/;
|
||||
|
||||
if (auth_info.access_token) return;
|
||||
this.#requestAuthCode();
|
||||
class OAuth {
|
||||
#scope = Constants.OAUTH.SCOPE;
|
||||
#model_name = Constants.OAUTH.MODEL_NAME;
|
||||
#grant_type = Constants.OAUTH.GRANT_TYPE;
|
||||
|
||||
#oauth_code_url = `${Constants.URLS.YT_BASE}/o/oauth2/device/code`;
|
||||
#oauth_token_url = `${Constants.URLS.YT_BASE}/o/oauth2/token`;
|
||||
#oauth_revoke_url = `${Constants.URLS.YT_BASE}/o/oauth2/revoke`;
|
||||
|
||||
#auth_info = {};
|
||||
#refresh_interval = 5;
|
||||
#ev = null;
|
||||
|
||||
constructor(ev) {
|
||||
this.#ev = ev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auth flow in case no valid credentials are available.
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
async init(auth_info) {
|
||||
this.#auth_info = auth_info;
|
||||
if (!auth_info.access_token) {
|
||||
this.#requestUserCode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the OAuth server for an auth code.
|
||||
* Asks the OAuth server for a user code
|
||||
* and verification URL.
|
||||
*
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
async #requestAuthCode() {
|
||||
async #requestUserCode() {
|
||||
const identity = await this.#getClientIdentity();
|
||||
|
||||
this.client_id = identity.id;
|
||||
@@ -37,20 +46,15 @@ class OAuth extends EventEmitter {
|
||||
|
||||
const data = {
|
||||
client_id: this.client_id,
|
||||
scope: this.scope,
|
||||
scope: this.#scope,
|
||||
device_id: Uuid.v4(),
|
||||
model_name: this.model_name
|
||||
model_name: this.#model_name
|
||||
};
|
||||
|
||||
const response = await Axios.post(this.oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
const response = await Axios.post(this.#oauth_code_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error) return this.#ev.emit('auth', { error: 'Could not obtain user code.', status: 'FAILED' });
|
||||
|
||||
if (response instanceof Error)
|
||||
return this.emit('auth', {
|
||||
error: 'Could not get auth code.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
|
||||
this.emit('auth', {
|
||||
this.#ev.emit('auth', {
|
||||
code: response.data.user_code,
|
||||
status: 'AUTHORIZATION_PENDING',
|
||||
expires_in: response.data.expires_in,
|
||||
@@ -65,7 +69,7 @@ class OAuth extends EventEmitter {
|
||||
/**
|
||||
* Waits for sign-in authorization.
|
||||
*
|
||||
* @param {string} device_code Client's device code.
|
||||
* @param {string} device_code - Client's device code.
|
||||
* @returns
|
||||
*/
|
||||
#waitForAuth(device_code) {
|
||||
@@ -73,16 +77,12 @@ class OAuth extends EventEmitter {
|
||||
client_id: this.client_id,
|
||||
client_secret: this.client_secret,
|
||||
code: device_code,
|
||||
grant_type: this.grant_type
|
||||
grant_type: this.#grant_type
|
||||
};
|
||||
|
||||
setTimeout(async () => {
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error)
|
||||
return this.emit('auth', {
|
||||
error: 'Could not get authentication token.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
const response = await Axios.post(this.#oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error) return this.#ev.emit('auth', { error: 'Could not get authentication token.', status: 'FAILED' });
|
||||
|
||||
if (response.data.error) {
|
||||
switch (response.data.error) {
|
||||
@@ -91,78 +91,97 @@ class OAuth extends EventEmitter {
|
||||
this.#waitForAuth(device_code);
|
||||
break;
|
||||
case 'access_denied':
|
||||
this.emit('auth', {
|
||||
this.#ev.emit('auth', {
|
||||
error: 'Access was denied.',
|
||||
status: 'ACCESS_DENIED'
|
||||
});
|
||||
break;
|
||||
case 'expired_token':
|
||||
this.emit('auth', {
|
||||
error: 'The device code has expired, requesting a new one.',
|
||||
this.#ev.emit('auth', {
|
||||
error: 'The user code has expired, requesting a new one.',
|
||||
status: 'DEVICE_CODE_EXPIRED'
|
||||
});
|
||||
this.#requestAuthCode();
|
||||
this.#requestUserCode();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
const expiration_date = new Date(new Date().getTime() + response.data.expires_in * 1000);
|
||||
|
||||
this.emit('auth', {
|
||||
credentials: {
|
||||
access_token: response.data.access_token,
|
||||
refresh_token: response.data.refresh_token,
|
||||
expires: expiration_date,
|
||||
},
|
||||
token_type: response.data.token_type,
|
||||
const credentials = {
|
||||
access_token: response.data.access_token,
|
||||
refresh_token: response.data.refresh_token,
|
||||
expires: expiration_date,
|
||||
};
|
||||
|
||||
this.#auth_info = credentials;
|
||||
|
||||
this.#ev.emit('auth', {
|
||||
credentials,
|
||||
status: 'SUCCESS'
|
||||
});
|
||||
}
|
||||
}, 1000 * this.refresh_interval);
|
||||
}, 1000 * this.#refresh_interval);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refreshes the access token if necessary.
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
async checkTokenValidity() {
|
||||
if (this.shouldRefreshToken()) {
|
||||
await this.#refreshAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new access token using a refresh token.
|
||||
* @returns {Promise.<{ credentials: { access_token: string; refresh_token: string; expires: Date }; status: string }>}
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
async refreshAccessToken() {
|
||||
async #refreshAccessToken() {
|
||||
const identity = await this.#getClientIdentity();
|
||||
|
||||
const data = {
|
||||
client_id: identity.id,
|
||||
client_secret: identity.secret,
|
||||
refresh_token: this.auth_info.refresh_token,
|
||||
refresh_token: this.#auth_info.refresh_token,
|
||||
grant_type: 'refresh_token',
|
||||
};
|
||||
|
||||
const response = await Axios.post(this.oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
if (response instanceof Error) {
|
||||
this.emit('auth', {
|
||||
const response = await Axios.post(this.#oauth_token_url, JSON.stringify(data), Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
|
||||
if (response instanceof Error)
|
||||
return this.#ev.emit('update-credentials', {
|
||||
error: 'Could not refresh access token.',
|
||||
status: 'FAILED'
|
||||
});
|
||||
|
||||
return {
|
||||
credentials: {
|
||||
access_token: this.auth_info.access_token,
|
||||
refresh_token: this.auth_info.refresh_token,
|
||||
expires: this.auth_info.expires
|
||||
},
|
||||
status: 'FAILED'
|
||||
};
|
||||
}
|
||||
|
||||
const expiration_date = new Date(new Date().getTime() + response.data.expires_in * 1000);
|
||||
|
||||
return {
|
||||
credentials: {
|
||||
refresh_token: this.auth_info.refresh_token,
|
||||
access_token: response.data.access_token,
|
||||
expires: expiration_date
|
||||
},
|
||||
token_type: response.data.token_type,
|
||||
status: 'SUCCESS'
|
||||
const credentials = {
|
||||
access_token: response.data.access_token,
|
||||
refresh_token: response.data.refresh_token || this.#auth_info.refresh_token,
|
||||
expires: expiration_date,
|
||||
};
|
||||
|
||||
this.#auth_info = credentials;
|
||||
|
||||
this.#ev.emit('update-credentials', {
|
||||
credentials,
|
||||
status: 'SUCCESS'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes access token (note that the refresh token will also be revoked).
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
async revokeAccessToken() {
|
||||
const response = await Axios.post(`${this.#oauth_revoke_url}?token=${this.getAccessToken()}`, Constants.OAUTH.HEADERS).catch((error) => error);
|
||||
return {
|
||||
success: !(response instanceof Error),
|
||||
status_code: response.status || 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,24 +194,41 @@ class OAuth extends EventEmitter {
|
||||
if (yttv_response instanceof Error) throw new Error(`Could not extract client identity: ${yttv_response.message}`);
|
||||
|
||||
// Here we download the script and extract the necessary data to proceed with the auth flow.
|
||||
const url_body = this.auth_script_regex.exec(yttv_response.data)[1];
|
||||
const url_body = Constants.OAUTH.REGEX.AUTH_SCRIPT.exec(yttv_response.data)[1];
|
||||
const script_url = `${Constants.URLS.YT_BASE}/${url_body}`;
|
||||
|
||||
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS).catch((error) => error);
|
||||
const response = await Axios.get(script_url, Constants.DEFAULT_HEADERS()).catch((error) => error);
|
||||
if (response instanceof Error) throw new Error(`Could not extract client identity: ${response.message}`);
|
||||
|
||||
const client_identity = response.data.replace(/\n/g, '').match(this.identity_regex);
|
||||
const client_identity = response.data.replace(/\n/g, '').match(Constants.OAUTH.REGEX.CLIENT_IDENTITY);
|
||||
return client_identity.groups;
|
||||
}
|
||||
|
||||
getAccessToken() {
|
||||
return this.#auth_info.access_token;
|
||||
}
|
||||
|
||||
getRefreshToken() {
|
||||
return this.#auth_info.refresh_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the auth info is valid.
|
||||
* @returns {boolean} true | false
|
||||
*/
|
||||
isValidAuthInfo() {
|
||||
return this.#auth_info.hasOwnProperty('access_token')
|
||||
&& this.#auth_info.hasOwnProperty('refresh_token')
|
||||
&& this.#auth_info.hasOwnProperty('expires');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access token validity.
|
||||
* @returns {boolean} true | false
|
||||
*/
|
||||
isTokenValid() {
|
||||
const timestamp = new Date(this.auth_info.expires).getTime();
|
||||
const is_valid = new Date().getTime() < timestamp;
|
||||
return is_valid;
|
||||
shouldRefreshToken() {
|
||||
const timestamp = new Date(this.#auth_info.expires).getTime();
|
||||
return new Date().getTime() > timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const QueryString = require('querystring');
|
||||
|
||||
class SigDecipher {
|
||||
class Signature {
|
||||
constructor(url, player) {
|
||||
this.url = url;
|
||||
this.player = player;
|
||||
@@ -72,4 +72,4 @@ class SigDecipher {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SigDecipher;
|
||||
module.exports = Signature;
|
||||
@@ -5,11 +5,12 @@ const Actions = require('../core/Actions');
|
||||
const Constants = require('../utils/Constants');
|
||||
const YTDataItems = require('./youtube');
|
||||
const YTMusicDataItems = require('./ytmusic');
|
||||
const Proto = require('../proto');
|
||||
|
||||
class Parser {
|
||||
constructor(session, data, args = {}) {
|
||||
this.session = session;
|
||||
this.data = data;
|
||||
this.session = session;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@@ -23,14 +24,23 @@ class Parser {
|
||||
case 'YOUTUBE':
|
||||
processed_data = ({
|
||||
SEARCH: () => this.#processSearch(),
|
||||
CHANNEL: () => this.#processChannel(),
|
||||
PLAYLIST: () => this.#processPlaylist(),
|
||||
VIDEO_INFO: () => this.#processVideoInfo()
|
||||
SUBSFEED: () => this.#processSubscriptionFeed(),
|
||||
HOMEFEED: () => this.#processHomeFeed(),
|
||||
TRENDING: () => this.#processTrending(),
|
||||
HISTORY: () => this.#processHistory(),
|
||||
COMMENTS: () => this.#processComments(),
|
||||
VIDEO_INFO: () => this.#processVideoInfo(),
|
||||
NOTIFICATIONS: () => this.#processNotifications(),
|
||||
SEARCH_SUGGESTIONS: () => this.#processSearchSuggestions(),
|
||||
})[data_type]()
|
||||
break;
|
||||
case 'YTMUSIC':
|
||||
processed_data = ({
|
||||
SEARCH: () => this.#processMusicSearch(),
|
||||
PLAYLIST: () => this.#processMusicPlaylist()
|
||||
PLAYLIST: () => this.#processMusicPlaylist(),
|
||||
SEARCH_SUGGESTIONS: () => this.#processMusicSearchSuggestions(),
|
||||
})[data_type]();
|
||||
break;
|
||||
default:
|
||||
@@ -47,13 +57,13 @@ class Parser {
|
||||
|
||||
const parseItems = (contents) => {
|
||||
const content = contents[0].itemSectionRenderer.contents;
|
||||
|
||||
|
||||
processed_data.query = content[0]?.showingResultsForRenderer?.originalQuery?.simpleText || this.args.query;
|
||||
processed_data.corrected_query = content[0]?.showingResultsForRenderer?.correctedQueryEndpoint?.searchEndpoint?.query || 'N/A';
|
||||
processed_data.estimated_results = parseInt(this.data.estimatedResults);
|
||||
|
||||
processed_data.videos = YTDataItems.VideoResultItem.parse(content);
|
||||
|
||||
|
||||
processed_data.getContinuation = async () => {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
@@ -70,7 +80,7 @@ class Parser {
|
||||
|
||||
return parseItems(contents);
|
||||
}
|
||||
|
||||
|
||||
#processMusicSearch() {
|
||||
const tabs = Utils.findNode(this.data, 'contents', 'tabs').tabs;
|
||||
const contents = Utils.findNode(tabs, '0', 'contents', 5);
|
||||
@@ -93,7 +103,7 @@ class Parser {
|
||||
const section_title = section.title.runs[0].text;
|
||||
|
||||
const section_items = ({
|
||||
['Top result']: () => YTMusicDataItems.TopResultItem.parse(section.contents), // console.log(JSON.stringify(section.contents, null, 4)),
|
||||
['Top result']: () => YTMusicDataItems.TopResultItem.parse(section.contents),
|
||||
['Songs']: () => YTMusicDataItems.SongResultItem.parse(section.contents),
|
||||
['Videos']: () => YTMusicDataItems.VideoResultItem.parse(section.contents),
|
||||
['Featured playlists']: () => YTMusicDataItems.PlaylistResultItem.parse(section.contents),
|
||||
@@ -108,7 +118,16 @@ class Parser {
|
||||
|
||||
return processed_data;
|
||||
}
|
||||
|
||||
|
||||
#processSearchSuggestions() {
|
||||
return YTDataItems.SearchSuggestionItem.parse(this.data[1], this.data[0]);
|
||||
}
|
||||
|
||||
#processMusicSearchSuggestions() {
|
||||
const contents = this.data.contents[0].searchSuggestionsSectionRenderer.contents;
|
||||
return YTMusicDataItems.MusicSearchSuggestionItem.parse(contents);
|
||||
}
|
||||
|
||||
#processPlaylist() {
|
||||
const details = this.data.sidebar.playlistSidebarRenderer.items[0];
|
||||
|
||||
@@ -250,6 +269,280 @@ class Parser {
|
||||
|
||||
return processed_data;
|
||||
}
|
||||
|
||||
#processComments() {
|
||||
if (!this.data.onResponseReceivedEndpoints)
|
||||
throw new Utils.UnavailableContentError('Comments section not available', this.args);
|
||||
|
||||
const header = Utils.findNode(this.data, 'onResponseReceivedEndpoints', 'commentsHeaderRenderer', 5, false);
|
||||
const comment_count = parseInt(header.commentsHeaderRenderer.countText.runs[0].text.replace(/,/g, ''));
|
||||
const page_count = parseInt(comment_count / 20);
|
||||
|
||||
const parseComments = (data) => {
|
||||
const items = Utils.findNode(data, 'onResponseReceivedEndpoints', 'commentRenderer', 4, false);
|
||||
|
||||
const response = {
|
||||
page_count,
|
||||
comment_count,
|
||||
items: []
|
||||
};
|
||||
|
||||
response.items = items.map((item) => {
|
||||
const comment = YTDataItems.CommentThread.parseItem(item);
|
||||
if (comment) {
|
||||
comment.like = () => Actions.engage(this.session, 'comment/perform_comment_action', { comment_action: 'like', comment_id: comment.metadata.id, video_id: this.args.video_id }),
|
||||
comment.dislike = () => Actions.engage(this.session, 'comment/perform_comment_action', { comment_action: 'dislike', comment_id: comment.metadata.id, video_id: this.args.video_id }),
|
||||
comment.reply = (text) => Actions.engage(this.session, 'comment/create_comment_reply', { text, comment_id: comment.metadata.id, video_id: this.args.video_id });
|
||||
|
||||
comment.report = async () => {
|
||||
const payload = Utils.findNode(item, 'commentThreadRenderer', 'params', 10, false);
|
||||
const form = await Actions.flag(this.session, 'flag/get_form', { params: payload.params });
|
||||
|
||||
const action = Utils.findNode(form, 'actions', 'flagAction', 13, false);
|
||||
const flag = await Actions.flag(this.session, 'flag/flag', { action: action.flagAction });
|
||||
|
||||
return flag;
|
||||
};
|
||||
|
||||
comment.getReplies = async () => {
|
||||
if (comment.metadata.reply_count === 0) throw new Utils.InnertubeError('This comment has no replies', comment);
|
||||
const payload = Proto.encodeCommentRepliesParams(this.args.video_id, comment.metadata.id);
|
||||
const next = await Actions.next(this.session, { continuation_token: payload });
|
||||
return parseComments(next.data);
|
||||
};
|
||||
|
||||
return comment;
|
||||
}
|
||||
}).filter((c) => c);
|
||||
|
||||
response.getContinuation = async () => {
|
||||
const continuation_item = items.find((item) => item.continuationItemRenderer);
|
||||
if (!continuation_item) throw new Utils.InnertubeError('You\'ve reached the end');
|
||||
|
||||
const is_reply = !!continuation_item.continuationItemRenderer.button;
|
||||
const payload = Utils.findNode(continuation_item, 'continuationItemRenderer', 'token', is_reply && 5 || 3);
|
||||
const next = await Actions.next(this.session, { continuation_token: payload.token });
|
||||
|
||||
return parseComments(next.data);
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
return parseComments(this.data);
|
||||
}
|
||||
|
||||
#processHomeFeed() {
|
||||
const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false)
|
||||
|
||||
const parseItems = (contents) => {
|
||||
const videos = YTDataItems.VideoItem.parse(contents);
|
||||
|
||||
const getContinuation = async () => {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.browse(this.session, 'continuation', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems);
|
||||
}
|
||||
|
||||
return { videos, getContinuation };
|
||||
}
|
||||
|
||||
return parseItems(contents);
|
||||
}
|
||||
|
||||
#processSubscriptionFeed() {
|
||||
const contents = Utils.findNode(this.data, 'contents', 'contents', 9, false);
|
||||
|
||||
const subsfeed = { items: [] };
|
||||
|
||||
const parseItems = (contents) => {
|
||||
contents.forEach((section) => {
|
||||
if (!section.itemSectionRenderer) return;
|
||||
|
||||
const section_contents = section.itemSectionRenderer.contents[0];
|
||||
const section_title = section_contents.shelfRenderer.title.runs[0].text;
|
||||
const section_items = section_contents.shelfRenderer.content.gridRenderer.items;
|
||||
|
||||
const items = YTDataItems.GridVideoItem.parse(section_items);
|
||||
|
||||
subsfeed.items.push({
|
||||
date: section_title,
|
||||
videos: items
|
||||
});
|
||||
});
|
||||
|
||||
subsfeed.getContinuation = async () => {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.browse(this.session, 'continuation', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
const ccontents = Utils.findNode(response.data, 'onResponseReceivedActions', 'itemSectionRenderer', 4, false);
|
||||
subsfeed.items = [];
|
||||
|
||||
return parseItems(ccontents);
|
||||
}
|
||||
|
||||
return subsfeed;
|
||||
};
|
||||
|
||||
return parseItems(contents);
|
||||
}
|
||||
|
||||
#processChannel() {
|
||||
const tabs = this.data.contents.twoColumnBrowseResultsRenderer.tabs;
|
||||
const metadata = this.data.metadata;
|
||||
|
||||
const home_tab = tabs.find((tab) => tab.tabRenderer.title == 'Home');
|
||||
const home_contents = home_tab.tabRenderer.content.sectionListRenderer.contents;
|
||||
const home_shelves = [];
|
||||
|
||||
home_contents.forEach((content) => {
|
||||
if (content.itemSectionRenderer) {
|
||||
const contents = content.itemSectionRenderer.contents[0];
|
||||
const list = contents?.shelfRenderer?.content.horizontalListRenderer;
|
||||
if (!list) return; // Ignores featured channels (for now only videos & playlists are supported)
|
||||
|
||||
const shelf = {
|
||||
title: contents.shelfRenderer.title.runs[0].text,
|
||||
content: []
|
||||
};
|
||||
|
||||
shelf.content = list.items.map((item) => {
|
||||
if (item.gridVideoRenderer) {
|
||||
return YTDataItems.GridVideoItem.parseItem(item);
|
||||
} else if (item.gridPlaylistRenderer) {
|
||||
return YTDataItems.GridPlaylistItem.parseItem(item);
|
||||
}
|
||||
});
|
||||
|
||||
home_shelves.push(shelf);
|
||||
}
|
||||
});
|
||||
|
||||
const ch_info = YTDataItems.ChannelMetadata.parse(metadata);
|
||||
|
||||
return {
|
||||
...ch_info,
|
||||
content: {
|
||||
// Home page of the channel, always available in the first request.
|
||||
home_page: home_shelves,
|
||||
|
||||
// TODO: Implement these (note: they require additional requests)
|
||||
getVideos: () => {},
|
||||
getPlaylists: () => {},
|
||||
getCommunity: () => {},
|
||||
getChannels: () => {},
|
||||
getAbout: () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#processNotifications() {
|
||||
const contents = this.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[0];
|
||||
if (!contents.multiPageMenuNotificationSectionRenderer) throw new Utils.InnertubeError('No notifications', response);
|
||||
|
||||
const parseItems = (items) => {
|
||||
const parsed_items = YTDataItems.NotificationItem.parse(items);
|
||||
|
||||
const getContinuation = async () => {
|
||||
const citem = items.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem?.continuationItemRenderer?.continuationEndpoint?.getNotificationMenuEndpoint?.ctoken;
|
||||
|
||||
const response = await Actions.notifications(this.session, 'get_notification_menu', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
return parseItems(response.data.actions[0].appendContinuationItemsAction.continuationItems);
|
||||
}
|
||||
|
||||
return { items: parsed_items, getContinuation };
|
||||
}
|
||||
|
||||
return parseItems(contents.multiPageMenuNotificationSectionRenderer.items);
|
||||
}
|
||||
|
||||
#processTrending() {
|
||||
const tabs = Utils.findNode(this.data, 'contents', 'tabRenderer', 4, false);
|
||||
const categories = {};
|
||||
|
||||
const trending = tabs.map((tab) => {
|
||||
const tab_renderer = tab.tabRenderer;
|
||||
const tab_content = tab_renderer?.content;
|
||||
const category_title = tab_renderer.title.toLowerCase();
|
||||
|
||||
categories[category_title] = {};
|
||||
|
||||
if (tab_content) { // The “Now” category is always available
|
||||
const contents = tab_content.sectionListRenderer.contents;
|
||||
|
||||
categories[category_title].content = contents.map((content) => {
|
||||
const shelf = content.itemSectionRenderer.contents[0].shelfRenderer;
|
||||
const parsed_shelf = YTDataItems.ShelfRenderer.parse(shelf);
|
||||
return parsed_shelf;
|
||||
});
|
||||
} else { // The rest can only be fetched with additional calls
|
||||
const params = tab_renderer.endpoint.browseEndpoint.params;
|
||||
categories[category_title].getVideos = async () => {
|
||||
const response = await Actions.browse(this.session, 'trending', { params });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve category videos', response);
|
||||
|
||||
const tabs = Utils.findNode(response, 'contents', 'tabRenderer', 4, false);
|
||||
const tab = tabs.find((tab) => tab.tabRenderer.title === tab_renderer.title);
|
||||
|
||||
const contents = tab.tabRenderer.content.sectionListRenderer.contents;
|
||||
const items = Utils.findNode(contents, 'itemSectionRenderer', 'items', 8, false);
|
||||
|
||||
return YTDataItems.VideoItem.parse(items);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
#processHistory() {
|
||||
const contents = Utils.findNode(this.data, 'contents', 'videoRenderer', 9, false)
|
||||
|
||||
const history = { items: [] };
|
||||
|
||||
const parseItems = (contents) => {
|
||||
contents.forEach((section) => {
|
||||
if (!section.itemSectionRenderer) return;
|
||||
|
||||
const header = section.itemSectionRenderer.header.itemSectionHeaderRenderer.title;
|
||||
const section_title = header?.simpleText || header?.runs.map((run) => run.text).join('');
|
||||
const contents = section.itemSectionRenderer.contents;
|
||||
|
||||
const section_items = YTDataItems.VideoItem.parse(contents);
|
||||
|
||||
history.items.push({
|
||||
date: section_title,
|
||||
videos: section_items
|
||||
});
|
||||
});
|
||||
|
||||
history.getContinuation = async () => {
|
||||
const citem = contents.find((item) => item.continuationItemRenderer);
|
||||
const ctoken = citem.continuationItemRenderer.continuationEndpoint.continuationCommand.token;
|
||||
|
||||
const response = await Actions.browse(this.session, 'continuation', { ctoken });
|
||||
if (!response.success) throw new Utils.InnertubeError('Could not retrieve continuation', response);
|
||||
|
||||
history.items = [];
|
||||
|
||||
return parseItems(response.data.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems);
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
return parseItems(contents);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Parser;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
const VideoResultItem = require('./search/VideoResultItem');
|
||||
const SearchSuggestionItem = require('./search/SearchSuggestionItem');
|
||||
const PlaylistItem = require('./others/PlaylistItem');
|
||||
const NotificationItem = require('./others/NotificationItem');
|
||||
const VideoItem = require('./others/VideoItem');
|
||||
const GridVideoItem = require('./others/GridVideoItem');
|
||||
const GridPlaylistItem = require('./others/GridPlaylistItem');
|
||||
const ChannelMetadata = require('./others/ChannelMetadata');
|
||||
const ShelfRenderer = require('./others/ShelfRenderer');
|
||||
const CommentThread = require('./others/CommentThread');
|
||||
|
||||
module.exports = { VideoResultItem, PlaylistItem };
|
||||
module.exports = { VideoResultItem, SearchSuggestionItem, PlaylistItem, NotificationItem, VideoItem, GridVideoItem, GridPlaylistItem, ChannelMetadata, ShelfRenderer, CommentThread };
|
||||
20
lib/parser/youtube/others/ChannelMetadata.js
Normal file
20
lib/parser/youtube/others/ChannelMetadata.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
class ChannelMetadata {
|
||||
static parse(data) {
|
||||
return {
|
||||
title: data.channelMetadataRenderer.title,
|
||||
description: data.channelMetadataRenderer.description,
|
||||
metadata: {
|
||||
url: data.channelMetadataRenderer?.channelUrl,
|
||||
rss_urls: data.channelMetadataRenderer?.rssUrl,
|
||||
vanity_channel_url: data.channelMetadataRenderer?.vanityChannelUrl,
|
||||
external_id: data.channelMetadataRenderer?.externalId,
|
||||
is_family_safe: data.channelMetadataRenderer?.isFamilySafe,
|
||||
keywords: data.channelMetadataRenderer?.keywords
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChannelMetadata;
|
||||
37
lib/parser/youtube/others/CommentThread.js
Normal file
37
lib/parser/youtube/others/CommentThread.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
const Constants = require('../../../utils/Constants');
|
||||
|
||||
class CommentThread {
|
||||
static parseItem(item) {
|
||||
if (item.commentThreadRenderer || item.commentRenderer) {
|
||||
const comment = item?.commentThreadRenderer?.comment || item;
|
||||
const replies = item?.commentThreadRenderer?.replies;
|
||||
|
||||
const like_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.likeButton;
|
||||
const dislike_btn = comment.commentRenderer?.actionButtons?.commentActionButtonsRenderer.dislikeButton;
|
||||
|
||||
return {
|
||||
text: comment.commentRenderer.contentText.runs.map((run) => run.text).join(''),
|
||||
author: {
|
||||
name: comment.commentRenderer.authorText.simpleText,
|
||||
thumbnails: comment.commentRenderer.authorThumbnail.thumbnails,
|
||||
channel_id: comment.commentRenderer.authorEndpoint.browseEndpoint.browseId,
|
||||
channel_url: Constants.URLS.YT_BASE + comment.commentRenderer.authorEndpoint.browseEndpoint.canonicalBaseUrl
|
||||
},
|
||||
metadata: {
|
||||
published: comment.commentRenderer.publishedTimeText.runs[0].text,
|
||||
is_reply: !!item.commentRenderer,
|
||||
is_liked: like_btn.toggleButtonRenderer.isToggled,
|
||||
is_disliked: dislike_btn.toggleButtonRenderer.isToggled,
|
||||
is_pinned: comment.commentRenderer.pinnedCommentBadge && true || false,
|
||||
is_channel_owner: comment.commentRenderer.authorIsChannelOwner,
|
||||
like_count: parseInt(like_btn?.toggleButtonRenderer?.accessibilityData?.accessibilityData.label.replace(/\D/g, '')),
|
||||
reply_count: comment.commentRenderer.replyCount || 0,
|
||||
id: comment.commentRenderer.commentId,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = CommentThread;
|
||||
20
lib/parser/youtube/others/GridPlaylistItem.js
Normal file
20
lib/parser/youtube/others/GridPlaylistItem.js
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
class GridPlaylistItem {
|
||||
static parse(data) {
|
||||
return data.map((item) => this.parseItem(item)).filter((item) => item);
|
||||
}
|
||||
|
||||
static parseItem(item) {
|
||||
return {
|
||||
id: item?.gridPlaylistRenderer.playlistId,
|
||||
title: item?.gridPlaylistRenderer.title?.runs?.map((run) => run.text).join(''),
|
||||
metadata: {
|
||||
thumbnail: item?.gridPlaylistRenderer.thumbnail?.thumbnails?.slice(-1)[0] || {},
|
||||
video_count: item?.gridPlaylistRenderer.videoCountShortText?.simpleText || 'N/A',
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GridPlaylistItem;
|
||||
35
lib/parser/youtube/others/GridVideoItem.js
Normal file
35
lib/parser/youtube/others/GridVideoItem.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
const Constants = require('../../../utils/Constants');
|
||||
|
||||
class GridVideoItem {
|
||||
static parse(data) {
|
||||
return data.map((item) => this.parseItem(item)).filter((item) => item);
|
||||
}
|
||||
|
||||
static parseItem(item) {
|
||||
return {
|
||||
id: item.gridVideoRenderer.videoId,
|
||||
title: item?.gridVideoRenderer?.title?.runs?.map((run) => run.text).join(' '),
|
||||
channel: {
|
||||
id: item?.gridVideoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.browseId,
|
||||
name: item?.gridVideoRenderer?.shortBylineText?.runs[0]?.text || 'N/A',
|
||||
url: `${Constants.URLS.YT_BASE}${item?.gridVideoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`
|
||||
},
|
||||
metadata: {
|
||||
view_count: item?.gridVideoRenderer?.viewCountText?.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
simple_text: item?.gridVideoRenderer?.shortViewCountText?.simpleText || 'N/A',
|
||||
accessibility_label: item?.gridVideoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A',
|
||||
},
|
||||
thumbnail: item?.gridVideoRenderer?.thumbnail?.thumbnails.slice(-1)[0] || [],
|
||||
moving_thumbnail: item?.gridVideoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {},
|
||||
published: item?.gridVideoRenderer?.publishedTimeText?.simpleText || 'N/A',
|
||||
badges: item?.gridVideoRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [],
|
||||
owner_badges: item?.gridVideoRenderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GridVideoItem;
|
||||
25
lib/parser/youtube/others/NotificationItem.js
Normal file
25
lib/parser/youtube/others/NotificationItem.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
class NotificationItem {
|
||||
static parse(data) {
|
||||
return data.map((item) => this.parseItem(item)).filter((item) => item);
|
||||
}
|
||||
|
||||
static parseItem(item) {
|
||||
if (item.notificationRenderer) {
|
||||
const notification = item.notificationRenderer;
|
||||
return {
|
||||
title: notification?.shortMessage?.simpleText,
|
||||
sent_time: notification?.sentTimeText?.simpleText,
|
||||
channel_name: notification?.contextualMenu?.menuRenderer?.items[1]?.menuServiceItemRenderer?.text?.runs[1]?.text || 'N/A',
|
||||
channel_thumbnail: notification?.thumbnail?.thumbnails[0],
|
||||
video_thumbnail: notification?.videoThumbnail?.thumbnails[0],
|
||||
video_url: notification.navigationEndpoint.watchEndpoint && `https://youtu.be/${notification.navigationEndpoint.watchEndpoint.videoId}` || 'N/A',
|
||||
read: notification.read,
|
||||
notification_id: notification.notificationId,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotificationItem;
|
||||
41
lib/parser/youtube/others/ShelfRenderer.js
Normal file
41
lib/parser/youtube/others/ShelfRenderer.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const VideoItem = require('./VideoItem');
|
||||
const GridVideoItem = require('./GridVideoItem');
|
||||
|
||||
class ShelfRenderer {
|
||||
static parse(data) {
|
||||
return {
|
||||
title: this.getTitle(data.title),
|
||||
videos: this.parseItems(data.content)
|
||||
}
|
||||
}
|
||||
|
||||
static getTitle(data) {
|
||||
if ('runs' in (data || {})) {
|
||||
return data.runs.map((run) => run.text).join('');
|
||||
} else if ('simpleText' in (data || {})) {
|
||||
return data.simpleText;
|
||||
} else {
|
||||
return 'Others';
|
||||
}
|
||||
}
|
||||
|
||||
static parseItems(data) {
|
||||
let items;
|
||||
|
||||
if ('expandedShelfContentsRenderer' in data) {
|
||||
items = data.expandedShelfContentsRenderer.items;
|
||||
} else if ('horizontalListRenderer' in data) {
|
||||
items = data.horizontalListRenderer.items;
|
||||
}
|
||||
|
||||
const videos = ('gridVideoRenderer' in items[0])
|
||||
&& GridVideoItem.parse(items)
|
||||
|| VideoItem.parse(items);
|
||||
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ShelfRenderer;
|
||||
46
lib/parser/youtube/others/VideoItem.js
Normal file
46
lib/parser/youtube/others/VideoItem.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../../../utils/Utils');
|
||||
const Constants = require('../../../utils/Constants');
|
||||
|
||||
class VideoItem {
|
||||
static parse(data) {
|
||||
return data.map((item) => this.parseItem(item)).filter((item) => item);
|
||||
}
|
||||
|
||||
static parseItem(item) {
|
||||
item = (item.richItemRenderer && item.richItemRenderer.content.videoRenderer)
|
||||
&& item.richItemRenderer.content
|
||||
|| item;
|
||||
|
||||
if (item.videoRenderer) return {
|
||||
id: item.videoRenderer.videoId,
|
||||
title: item.videoRenderer.title.runs.map((run) => run.text).join(' '),
|
||||
description: item?.videoRenderer?.descriptionSnippet?.runs[0]?.text || 'N/A',
|
||||
channel: {
|
||||
id: item?.videoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.browseId,
|
||||
name: item?.videoRenderer?.shortBylineText?.runs[0]?.text || 'N/A',
|
||||
url: `${Constants.URLS.YT_BASE}${item?.videoRenderer?.shortBylineText?.runs[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`
|
||||
},
|
||||
metadata: {
|
||||
view_count: item?.videoRenderer?.viewCountText?.simpleText || 'N/A',
|
||||
short_view_count_text: {
|
||||
simple_text: item?.videoRenderer?.shortViewCountText?.simpleText || 'N/A',
|
||||
accessibility_label: item?.videoRenderer?.shortViewCountText?.accessibility?.accessibilityData?.label || 'N/A',
|
||||
},
|
||||
thumbnail: item?.videoRenderer?.thumbnail?.thumbnails.slice(-1)[0] || {},
|
||||
moving_thumbnail: item?.videoRenderer?.richThumbnail?.movingThumbnailRenderer?.movingThumbnailDetails?.thumbnails[0] || {},
|
||||
published: item?.videoRenderer?.publishedTimeText?.simpleText || 'N/A',
|
||||
duration: {
|
||||
seconds: Utils.timeToSeconds(item?.videoRenderer?.lengthText?.simpleText || '0'),
|
||||
simple_text: item?.videoRenderer?.lengthText?.simpleText || 'N/A',
|
||||
accessibility_label: item?.videoRenderer?.lengthText?.accessibility?.accessibilityData?.label || 'N/A'
|
||||
},
|
||||
badges: item?.videoRenderer?.badges?.map((badge) => badge.metadataBadgeRenderer.label) || [],
|
||||
owner_badges: item?.videoRenderer?.ownerBadges?.map((badge) => badge.metadataBadgeRenderer.tooltip) || []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VideoItem;
|
||||
12
lib/parser/youtube/search/SearchSuggestionItem.js
Normal file
12
lib/parser/youtube/search/SearchSuggestionItem.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
class SearchSuggestionItem {
|
||||
static parse(data, bold_text) {
|
||||
return data.map((item) => ({
|
||||
text: item.trim(),
|
||||
bold_text: bold_text.trim().toLowerCase()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SearchSuggestionItem;
|
||||
@@ -5,7 +5,8 @@ const VideoResultItem = require('./search/VideoResultItem');
|
||||
const AlbumResultItem = require('./search/AlbumResultItem');
|
||||
const ArtistResultItem = require('./search/ArtistResultItem');
|
||||
const PlaylistResultItem = require('./search/PlaylistResultItem');
|
||||
const MusicSearchSuggestionItem = require('./search/MusicSearchSuggestionItem');
|
||||
const TopResultItem = require('./search/TopResultItem');
|
||||
const PlaylistItem = require('./others/PlaylistItem');
|
||||
|
||||
module.exports = { SongResultItem, VideoResultItem, AlbumResultItem, ArtistResultItem, PlaylistResultItem, TopResultItem, PlaylistItem };
|
||||
module.exports = { SongResultItem, VideoResultItem, AlbumResultItem, ArtistResultItem, PlaylistResultItem, MusicSearchSuggestionItem, TopResultItem, PlaylistItem };
|
||||
22
lib/parser/ytmusic/search/MusicSearchSuggestionItem.js
Normal file
22
lib/parser/ytmusic/search/MusicSearchSuggestionItem.js
Normal file
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
class MusicSearchSuggestionItem {
|
||||
static parse(data) {
|
||||
return data.map((item) => this.parseItem(item));
|
||||
}
|
||||
|
||||
static parseItem(item) {
|
||||
let suggestion;
|
||||
|
||||
item.historySuggestionRenderer &&
|
||||
(suggestion = item.historySuggestionRenderer.suggestion) ||
|
||||
(suggestion = item.searchSuggestionRenderer.suggestion);
|
||||
|
||||
return {
|
||||
text: suggestion.runs.map((run) => run.text).join('').trim(),
|
||||
bold_text: suggestion.runs[0].text.trim()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MusicSearchSuggestionItem;
|
||||
@@ -7,8 +7,8 @@ class PlaylistResultItem {
|
||||
|
||||
static parseItem(item) {
|
||||
const list_item = item.musicResponsiveListItemRenderer;
|
||||
const watch_playlist_endpoint = list_item.overlay.musicItemThumbnailOverlayRenderer
|
||||
.content.musicPlayButtonRenderer.playNavigationEndpoint.watchPlaylistEndpoint;
|
||||
const watch_playlist_endpoint = list_item?.overlay?.musicItemThumbnailOverlayRenderer
|
||||
?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint;
|
||||
|
||||
return {
|
||||
id: watch_playlist_endpoint?.playlistId,
|
||||
|
||||
@@ -19,11 +19,12 @@ class TopResultItem {
|
||||
song: () => SongResultItem.parseItem(item),
|
||||
video: () => VideoResultItem.parseItem(item),
|
||||
artist: () => ArtistResultItem.parseItem(item),
|
||||
album: () => ArtistResultItem.parseItem(item)
|
||||
album: () => AlbumResultItem.parseItem(item),
|
||||
single: () => AlbumResultItem.parseItem(item)
|
||||
}[type])();
|
||||
|
||||
parsed_item.type = type;
|
||||
|
||||
|
||||
parsed_item && (parsed_item.type = type);
|
||||
|
||||
return parsed_item;
|
||||
}).filter((item) => item);
|
||||
}
|
||||
|
||||
@@ -1,133 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Proto = require('protons');
|
||||
const messages = require('./messages');
|
||||
|
||||
/**
|
||||
* Encodes advanced search filters.
|
||||
*
|
||||
* @param {string} period - Period in which a video is uploaded: any | hour | day | week | month | year
|
||||
* @param {string} duration - The duration of a video: any | short | long
|
||||
* @param {string} order - The order of the search results: relevance | rating | age | views
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeSearchFilter(period, duration, order) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
|
||||
class Proto {
|
||||
static encodeSearchFilter(period, duration, order) {
|
||||
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 };
|
||||
const durations = { 'any': null, 'short': 1, 'long': 2 };
|
||||
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views': 3 };
|
||||
|
||||
const periods = { 'any': null, 'hour': 1, 'day': 2, 'week': 3, 'month': 4, 'year': 5 };
|
||||
const durations = { 'any': null, 'short': 1, 'long': 2 };
|
||||
const orders = { 'relevance': null, 'rating': 1, 'age': 2, 'views': 3 };
|
||||
const buf = messages.SearchFilter.encode({
|
||||
number: orders[order],
|
||||
filter: {
|
||||
param_0: periods[period],
|
||||
param_1: (period == 'hour' && order == 'relevance') ? null : 1,
|
||||
param_2: durations[duration]
|
||||
}
|
||||
});
|
||||
|
||||
const search_filter_buff = youtube_proto.SearchFilter.encode({
|
||||
number: orders[order],
|
||||
filter: {
|
||||
param_0: periods[period],
|
||||
param_1: (period == 'hour' && order == 'relevance') ? null : 1,
|
||||
param_2: durations[duration]
|
||||
}
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(search_filter_buff).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes livestream message parameters.
|
||||
*
|
||||
* @param {string} channel_id - The id of the channel hosting the livestream.
|
||||
* @param {string} video_id - The id of the livestream.
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeMessageParams(channel_id, video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.LiveMessageParams.encode({
|
||||
params: {
|
||||
ids: { channel_id, video_id }
|
||||
},
|
||||
number_0: 1,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment parameters.
|
||||
*
|
||||
* @param {string} video_id - The id of the video you're commenting on.
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeCommentParams(video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.CreateCommentParams.encode({
|
||||
video_id,
|
||||
params: { index: 0 },
|
||||
number: 7
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment reply parameters.
|
||||
*
|
||||
* @param {string} comment_id - The id of the comment.
|
||||
* @param {string} video_id - The id of the video you're commenting on.
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeCommentReplyParams(comment_id, video_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.CreateCommentReplyParams.encode({
|
||||
video_id, comment_id,
|
||||
params: { unk_num: 0 },
|
||||
unk_num: 7
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes comment action parameters (liking, disliking, reporting a comment etc).
|
||||
*
|
||||
* @param {string} type - Type of action.
|
||||
* @param {string} comment_id - The id of the comment.
|
||||
* @param {string} video_id - The id of the video you're commenting on.
|
||||
* @param {string} channel_id - The id of the channel.
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeCommentActionParams(type, comment_id, video_id, channel_id) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
const buf = youtube_proto.PeformCommentActionParams.encode({
|
||||
type, comment_id, channel_id, video_id,
|
||||
unk_num: 2, unk_num_1: 0, unk_num_2: 0,
|
||||
unk_num_3: "0", unk_num_4: 0,
|
||||
unk_num_5: 12, unk_num_6: 0,
|
||||
});
|
||||
static encodeMessageParams(channel_id, video_id) {
|
||||
const buf = messages.LiveMessageParams.encode({
|
||||
params: { ids: { channel_id, video_id } },
|
||||
number_0: 1, number_1: 4
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
return Buffer.from(encodeURIComponent(Buffer.from(buf).toString('base64'))).toString('base64');
|
||||
}
|
||||
|
||||
static encodeCommentsSectionParams(video_id, options = {}) {
|
||||
const sort_menu = { TOP_COMMENTS: 0, NEWEST_FIRST: 1 };
|
||||
|
||||
const buf = messages.GetCommentsSectionParams.encode({
|
||||
ctx: { video_id },
|
||||
unk_param: 6,
|
||||
params: {
|
||||
opts: {
|
||||
video_id,
|
||||
sort_by: sort_menu[options.sort_by || 'TOP_COMMENTS'],
|
||||
type: options.type || 2
|
||||
},
|
||||
target: 'comments-section'
|
||||
}
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
static encodeCommentRepliesParams(video_id, comment_id) {
|
||||
const buf = messages.GetCommentsSectionParams.encode({
|
||||
ctx: { video_id },
|
||||
unk_param: 6,
|
||||
params: {
|
||||
replies_opts: {
|
||||
video_id, comment_id,
|
||||
unkopts: { unk_param: 0 },
|
||||
unk_param_1: 1, unk_param_2: 10,
|
||||
channel_id: ' ' // Seems like this can be omitted
|
||||
},
|
||||
target: `comment-replies-item-${comment_id}`
|
||||
}
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
static encodeCommentParams(video_id) {
|
||||
const buf = messages.CreateCommentParams.encode({
|
||||
video_id, params: { index: 0 },
|
||||
number: 7
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
static encodeCommentReplyParams(comment_id, video_id) {
|
||||
const buf = messages.CreateCommentReplyParams.encode({
|
||||
video_id, comment_id,
|
||||
params: { unk_num: 0 },
|
||||
unk_num: 7
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
static encodeCommentActionParams(type, comment_id, video_id) {
|
||||
const buf = messages.PeformCommentActionParams.encode({
|
||||
type, comment_id, video_id,
|
||||
unk_num: 2, unk_num_1: 0, unk_num_2: 0,
|
||||
unk_num_3: "0", unk_num_4: 0,
|
||||
unk_num_5: 12, unk_num_6: 0,
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
static encodeNotificationPref(channel_id, index) {
|
||||
const buf = messages.NotificationPreferences.encode({
|
||||
channel_id, pref_id: { index },
|
||||
number_0: 0, number_1: 4
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes notification preferences.
|
||||
*
|
||||
* @param {string} channel_id - The id of the channel.
|
||||
* @param {string} index - The index of the preference id.
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeNotificationPref(channel_id, index) {
|
||||
const youtube_proto = Proto(Fs.readFileSync(`${__dirname}/youtube.proto`));
|
||||
|
||||
const buf = youtube_proto.NotificationPreferences.encode({
|
||||
channel_id,
|
||||
pref_id: { index },
|
||||
number_0: 0,
|
||||
number_1: 4
|
||||
});
|
||||
|
||||
return encodeURIComponent(Buffer.from(buf).toString('base64'));
|
||||
}
|
||||
|
||||
module.exports = { encodeMessageParams, encodeCommentParams, encodeCommentReplyParams, encodeCommentActionParams, encodeNotificationPref, encodeSearchFilter };
|
||||
module.exports = Proto;
|
||||
1751
lib/proto/messages.js
Normal file
1751
lib/proto/messages.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,66 +2,111 @@ syntax = "proto2";
|
||||
package proto;
|
||||
|
||||
message NotificationPreferences {
|
||||
string channel_id = 1;
|
||||
required string channel_id = 1;
|
||||
|
||||
message Preference {
|
||||
int32 index = 1;
|
||||
required int32 index = 1;
|
||||
}
|
||||
Preference pref_id = 2;
|
||||
int32 number_0 = 3;
|
||||
int32 number_1 = 4;
|
||||
|
||||
optional int32 number_0 = 3;
|
||||
optional int32 number_1 = 4;
|
||||
}
|
||||
|
||||
message LiveMessageParams {
|
||||
message Params {
|
||||
message Ids {
|
||||
string channel_id = 1;
|
||||
string video_id = 2;
|
||||
required string channel_id = 1;
|
||||
required string video_id = 2;
|
||||
}
|
||||
Ids ids = 5;
|
||||
}
|
||||
Params params = 1;
|
||||
int32 number_0 = 2;
|
||||
int32 number_1 = 3;
|
||||
|
||||
optional int32 number_0 = 2;
|
||||
optional int32 number_1 = 3;
|
||||
}
|
||||
|
||||
message GetCommentsSectionParams {
|
||||
message Context {
|
||||
string video_id = 2;
|
||||
}
|
||||
Context ctx = 2;
|
||||
|
||||
required int32 unk_param = 3;
|
||||
|
||||
message Params {
|
||||
optional string unk_token = 1;
|
||||
|
||||
message Options {
|
||||
required string video_id = 4;
|
||||
required int32 sort_by = 6;
|
||||
required int32 type = 15;
|
||||
}
|
||||
|
||||
message RepliesOptions {
|
||||
required string comment_id = 2;
|
||||
|
||||
message UnkOpts {
|
||||
required int32 unk_param = 1;
|
||||
}
|
||||
UnkOpts unkopts = 4;
|
||||
|
||||
optional string channel_id = 5;
|
||||
required string video_id = 6;
|
||||
|
||||
required int32 unk_param_1 = 8;
|
||||
required int32 unk_param_2 = 9;
|
||||
}
|
||||
|
||||
optional Options opts = 4;
|
||||
optional RepliesOptions replies_opts = 3;
|
||||
|
||||
optional int32 page = 5;
|
||||
required string target = 8;
|
||||
}
|
||||
|
||||
Params params = 6;
|
||||
}
|
||||
|
||||
message CreateCommentParams {
|
||||
string video_id = 2;
|
||||
required string video_id = 2;
|
||||
message Params {
|
||||
int32 index = 1;
|
||||
required int32 index = 1;
|
||||
}
|
||||
Params params = 5;
|
||||
int32 number = 10;
|
||||
required int32 number = 10;
|
||||
}
|
||||
|
||||
message CreateCommentReplyParams {
|
||||
string video_id = 2;
|
||||
string comment_id = 4;
|
||||
required string video_id = 2;
|
||||
required string comment_id = 4;
|
||||
|
||||
message UnknownParams {
|
||||
int32 unk_num = 1;
|
||||
required int32 unk_num = 1;
|
||||
}
|
||||
UnknownParams params = 5;
|
||||
|
||||
int32 unk_num = 10;
|
||||
optional int32 unk_num = 10;
|
||||
}
|
||||
|
||||
message PeformCommentActionParams {
|
||||
int32 type = 1;
|
||||
int32 unk_num = 2;
|
||||
required int32 type = 1;
|
||||
optional int32 unk_num = 2;
|
||||
|
||||
string comment_id = 3;
|
||||
string video_id = 5;
|
||||
required string comment_id = 3;
|
||||
required string video_id = 5;
|
||||
|
||||
int32 unk_num_1 = 6;
|
||||
int32 unk_num_2 = 7;
|
||||
optional int32 unk_num_1 = 6;
|
||||
optional int32 unk_num_2 = 7;
|
||||
|
||||
string unk_num_3 = 9;
|
||||
optional string unk_num_3 = 9;
|
||||
|
||||
int32 unk_num_4 = 10;
|
||||
int32 unk_num_5 = 21;
|
||||
optional int32 unk_num_4 = 10;
|
||||
optional int32 unk_num_5 = 21;
|
||||
|
||||
string channel_id = 23;
|
||||
int32 unk_num_6 = 30;
|
||||
optional string channel_id = 23;
|
||||
optional int32 unk_num_6 = 30;
|
||||
}
|
||||
|
||||
message SearchFilter {
|
||||
|
||||
@@ -6,6 +6,7 @@ module.exports = {
|
||||
URLS: {
|
||||
YT_BASE: 'https://www.youtube.com',
|
||||
YT_BASE_API: 'https://www.youtube.com/youtubei/',
|
||||
YT_STUDIO_BASE_API: 'https://studio.youtube.com/youtubei/',
|
||||
YT_SUGGESTIONS: 'https://suggestqueries.google.com/complete/',
|
||||
YT_MUSIC: 'https://music.youtube.com',
|
||||
YT_MUSIC_BASE_API: 'https://music.youtube.com/youtubei/'
|
||||
@@ -23,16 +24,20 @@ module.exports = {
|
||||
'referer': `https://www.youtube.com/tv`,
|
||||
'accept-language': 'en-US'
|
||||
}
|
||||
},
|
||||
REGEX: {
|
||||
AUTH_SCRIPT: /<script id=\"base-js\" src=\"(.*?)\" nonce=".*?"><\/script>/,
|
||||
CLIENT_IDENTITY: /.+?={};var .+?={clientId:\"(?<id>.+?)\",.+?:\"(?<secret>.+?)\"},/
|
||||
}
|
||||
},
|
||||
DEFAULT_HEADERS: (session) => {
|
||||
DEFAULT_HEADERS: (config) => {
|
||||
return {
|
||||
headers: {
|
||||
'Cookie': session.cookie,
|
||||
'Cookie': config?.cookie || '',
|
||||
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
|
||||
'Referer': 'https://www.google.com/',
|
||||
'Accept': 'text/html',
|
||||
'Accept-Language': 'en-US,en',
|
||||
'Accept-Language': `en-${config?.gl || 'US'}`,
|
||||
'Accept-Encoding': 'gzip'
|
||||
}
|
||||
};
|
||||
@@ -45,30 +50,9 @@ module.exports = {
|
||||
'Referer': 'https://www.youtube.com',
|
||||
'DNT': '?1'
|
||||
},
|
||||
INNERTUBE_HEADERS: (info) => {
|
||||
const origin = info.ytmusic && 'https://music.youtube.com' || 'https://www.youtube.com';
|
||||
|
||||
const headers = {
|
||||
'accept': '*/*',
|
||||
'user-agent': Utils.getRandomUserAgent('desktop').userAgent,
|
||||
'content-type': 'application/json',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
'x-goog-authuser': 0,
|
||||
'x-goog-visitor-id': info.session.context.client.visitorData || '',
|
||||
'x-youtube-client-name': 1,
|
||||
'x-youtube-client-version': info.session.context.client.clientVersion,
|
||||
'x-youtube-chrome-connected': 'source=Chrome,mode=0,enable_account_consistency=true,supervised=false,consistency_enabled_by_default=false',
|
||||
'x-origin': origin,
|
||||
'origin': origin
|
||||
};
|
||||
|
||||
if (info.session.logged_in) {
|
||||
|
||||
headers.Cookie = info.session.cookie;
|
||||
headers.authorization = info.session.cookie.length && info.session.auth_apisid || `Bearer ${info.session.access_token}`;
|
||||
}
|
||||
|
||||
return headers
|
||||
INNERTUBE_HEADERS_BASE: {
|
||||
'accept': '*/*',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
VIDEO_INFO_REQBODY: (id, sts, context) => {
|
||||
return {
|
||||
@@ -138,4 +122,4 @@ module.exports = {
|
||||
TRANSLATE_1: 'function(d,e){for(var f',
|
||||
TRANSLATE_2: 'function(d,e,f){var'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
53
lib/utils/Request.js
Normal file
53
lib/utils/Request.js
Normal file
@@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const Axios = require('axios');
|
||||
const Utils = require('./Utils');
|
||||
const Constants = require('./Constants');
|
||||
|
||||
class Request {
|
||||
constructor (session) {
|
||||
this.session = session;
|
||||
|
||||
this.instance = Axios.create({
|
||||
baseURL: Constants.URLS.YT_BASE_API + session.version,
|
||||
headers: Constants.INNERTUBE_HEADERS_BASE,
|
||||
params: { key: session.key },
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
this.#setupInterceptor();
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
#setupInterceptor() {
|
||||
this.instance.interceptors.request.use((config) => {
|
||||
const is_ytmusic = config.data.includes(Constants.URLS.YT_MUSIC);
|
||||
|
||||
config.headers['accept-language'] = `en-${this.session.config.gl || 'US'}`;
|
||||
config.headers['x-goog-visitor-id'] = this.session.context.client.visitorData || ''
|
||||
config.headers['x-youtube-client-version'] = this.session.context.client.clientVersion;
|
||||
config.headers['x-origin'] = is_ytmusic && Constants.URLS.YT_MUSIC || Constants.URLS.YT_BASE;
|
||||
config.headers['origin'] = is_ytmusic && Constants.URLS.YT_MUSIC || Constants.URLS.YT_BASE;
|
||||
|
||||
is_ytmusic && (config.baseURL = Constants.URLS.YT_MUSIC_BASE_API + this.session.version);
|
||||
|
||||
if (this.session.logged_in) {
|
||||
const cookie = this.session.config.cookie;
|
||||
|
||||
const token = cookie
|
||||
&& this.session.auth_apisid
|
||||
|| this.session.access_token;
|
||||
|
||||
config.headers.cookie = cookie || '';
|
||||
config.headers.authorization = cookie && token || `Bearer ${token}`;
|
||||
|
||||
!cookie && (delete config.params.key);
|
||||
}
|
||||
|
||||
return config;
|
||||
}, (error) => Promise.reject(error));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Request;
|
||||
@@ -1,12 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Crypto = require('crypto');
|
||||
const UserAgent = require('user-agents');
|
||||
const Flatten = require('flat');
|
||||
|
||||
function InnertubeError(message, info) {
|
||||
this.info = info;
|
||||
this.info = info || {};
|
||||
this.stack = Error(message).stack;
|
||||
}
|
||||
|
||||
|
||||
90
package-lock.json
generated
90
package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"flat": "^5.0.2",
|
||||
"protons": "^2.0.3",
|
||||
"protocol-buffers-encodings": "^1.1.1",
|
||||
"user-agents": "^1.0.778",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
@@ -91,25 +91,13 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"node_modules/multiformats": {
|
||||
"version": "9.6.4",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.6.4.tgz",
|
||||
"integrity": "sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg=="
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
|
||||
},
|
||||
"node_modules/protons": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/protons/-/protons-2.0.3.tgz",
|
||||
"integrity": "sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA==",
|
||||
"node_modules/protocol-buffers-encodings": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.1.1.tgz",
|
||||
"integrity": "sha512-5aFshI9SbhtcMiDiZZu3g2tMlZeS5lhni//AGJ7V34PQLU5JA91Cva7TIs6inZhYikS3OpnUzAUuL6YtS0CyDA==",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1",
|
||||
"signed-varint": "^2.0.1",
|
||||
"uint8arrays": "^3.0.0",
|
||||
"varint": "^5.0.0"
|
||||
"varint": "5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/signed-varint": {
|
||||
@@ -120,14 +108,6 @@
|
||||
"varint": "~5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
|
||||
"dependencies": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz",
|
||||
@@ -142,9 +122,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/user-agents": {
|
||||
"version": "1.0.984",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.984.tgz",
|
||||
"integrity": "sha512-gFYUg9GRrUA5LPKBa+K2K6jML3VPseVxm2TzhfTMVpLuxYZGm4qM8egSfQ7DV8X4DTNwECHAhlwv6JWZnIsCHQ==",
|
||||
"version": "1.0.993",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.993.tgz",
|
||||
"integrity": "sha512-15uxQ45RVVNSWLkW9V3KkHoQIp+3evKLAfJSe6WOYNLF897mn7m1LTMn4IC7n4CmviDlQJ/SKyCXEutcYo1rAQ==",
|
||||
"dependencies": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -159,9 +139,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/varint": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
|
||||
"integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -208,25 +188,13 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"multiformats": {
|
||||
"version": "9.6.4",
|
||||
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.6.4.tgz",
|
||||
"integrity": "sha512-fCCB6XMrr6CqJiHNjfFNGT0v//dxOBMrOMqUIzpPc/mmITweLEyhvMpY9bF+jZ9z3vaMAau5E8B68DW77QMXkg=="
|
||||
},
|
||||
"protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
|
||||
},
|
||||
"protons": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/protons/-/protons-2.0.3.tgz",
|
||||
"integrity": "sha512-j6JikP/H7gNybNinZhAHMN07Vjr1i4lVupg598l4I9gSTjJqOvKnwjzYX2PzvBTSVf2eZ2nWv4vG+mtW8L6tpA==",
|
||||
"protocol-buffers-encodings": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.1.1.tgz",
|
||||
"integrity": "sha512-5aFshI9SbhtcMiDiZZu3g2tMlZeS5lhni//AGJ7V34PQLU5JA91Cva7TIs6inZhYikS3OpnUzAUuL6YtS0CyDA==",
|
||||
"requires": {
|
||||
"protocol-buffers-schema": "^3.3.1",
|
||||
"signed-varint": "^2.0.1",
|
||||
"uint8arrays": "^3.0.0",
|
||||
"varint": "^5.0.0"
|
||||
"varint": "5.0.0"
|
||||
}
|
||||
},
|
||||
"signed-varint": {
|
||||
@@ -237,14 +205,6 @@
|
||||
"varint": "~5.0.0"
|
||||
}
|
||||
},
|
||||
"uint8arrays": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
|
||||
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
|
||||
"requires": {
|
||||
"multiformats": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz",
|
||||
@@ -259,9 +219,9 @@
|
||||
}
|
||||
},
|
||||
"user-agents": {
|
||||
"version": "1.0.984",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.984.tgz",
|
||||
"integrity": "sha512-gFYUg9GRrUA5LPKBa+K2K6jML3VPseVxm2TzhfTMVpLuxYZGm4qM8egSfQ7DV8X4DTNwECHAhlwv6JWZnIsCHQ==",
|
||||
"version": "1.0.993",
|
||||
"resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.993.tgz",
|
||||
"integrity": "sha512-15uxQ45RVVNSWLkW9V3KkHoQIp+3evKLAfJSe6WOYNLF897mn7m1LTMn4IC7n4CmviDlQJ/SKyCXEutcYo1rAQ==",
|
||||
"requires": {
|
||||
"dot-json": "^1.2.2",
|
||||
"lodash.clonedeep": "^4.5.0"
|
||||
@@ -273,9 +233,9 @@
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"varint": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz",
|
||||
"integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow=="
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
|
||||
"integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"description": "A full-featured library that allows you to get detailed info about any video, subscribe, unsubscribe, like, dislike, comment, search, download videos/music and much more!",
|
||||
"main": "index.js",
|
||||
"author": "LuanRT <luan.lrt4@gmail.com> (https://github.com/LuanRT)",
|
||||
@@ -12,14 +12,17 @@
|
||||
"scripts": {
|
||||
"test": "node test"
|
||||
},
|
||||
"types": "./typings/index.d.ts",
|
||||
"directories": {
|
||||
"example": "examples",
|
||||
"lib": "lib"
|
||||
"test": "./test",
|
||||
"typings": "./typings",
|
||||
"examples": "./examples",
|
||||
"lib": "./lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"flat": "^5.0.2",
|
||||
"protons": "^2.0.3",
|
||||
"protocol-buffers-encodings": "^1.1.1",
|
||||
"user-agents": "^1.0.778",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
@@ -40,6 +43,7 @@
|
||||
"youtube-downloader",
|
||||
"innertube",
|
||||
"innertubeapi",
|
||||
"unofficial",
|
||||
"downloader",
|
||||
"livechat",
|
||||
"dislike",
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = {
|
||||
expected_ntoken: 'AxwyS-osUl1WhMUd1',
|
||||
client_version: '2.20211101.01.00',
|
||||
test_video_id: 'dQw4w9WgXcQ',
|
||||
test_song_id: 'UGB_Bsm5Unk',
|
||||
sig_decipher_sc: `fB={RP:function(a,b){a.splice(0,b)},
|
||||
Td:function(a){a.reverse()},
|
||||
kq:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c};fB.kq(a,35);fB.RP(a,2);fB.kq(a,46);fB.Td(a,6);`,
|
||||
|
||||
156
test/index.js
156
test/index.js
@@ -1,75 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Innertube = require('..');
|
||||
const NToken = require('../lib/deciphers/NToken');
|
||||
const SigDecipher = require('../lib/deciphers/Sig');
|
||||
const Constants = require('./constants');
|
||||
|
||||
let failed_tests = 0;
|
||||
|
||||
async function performTests() {
|
||||
const youtube = await new Innertube().catch((error) => error);
|
||||
assert(!(youtube instanceof Error), `should retrieve Innertube configuration data`, youtube);
|
||||
|
||||
if (!(youtube instanceof Error)) {
|
||||
const homefeed = await youtube.getHomeFeed();
|
||||
assert(!(homefeed instanceof Error), `should retrieve recommendations`, homefeed);
|
||||
|
||||
const ytsearch = await youtube.search('Carl Sagan - Documentary').catch((error) => error);
|
||||
assert(!(ytsearch instanceof Error) && ytsearch.videos.length, `should search on YouTube`, ytsearch);
|
||||
|
||||
const ytmsearch = await youtube.search('Logic - Obediently Yours', { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmsearch instanceof Error), `should search on YouTube Music`, ytmsearch);
|
||||
|
||||
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);
|
||||
assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details);
|
||||
|
||||
const comments = await youtube.getComments(Constants.test_video_id).catch((error) => error);
|
||||
assert(!(comments instanceof Error), `should retrieve comments for ${Constants.test_video_id}`, comments);
|
||||
|
||||
const ytplaylist = await youtube.getPlaylist(ytmsearch.results.community_playlists[0].id, { client: 'YOUTUBE' });
|
||||
assert(!(ytplaylist instanceof Error), `should retrieve and parse playlist with YouTube`, ytplaylist);
|
||||
|
||||
const ytmplaylist = await youtube.getPlaylist(ytmsearch.results.community_playlists[0].id, { client: 'YTMUSIC' });
|
||||
assert(!(ytmplaylist instanceof Error), `should retrieve and parse playlist with YouTube Music`, ytmplaylist);
|
||||
|
||||
const lyrics = await youtube.getLyrics(ytmsearch.results.songs[0].id);
|
||||
assert(!(lyrics instanceof Error), `should retrieve song lyrics`, lyrics);
|
||||
|
||||
const video = await downloadVideo(Constants.test_video_id, youtube).catch((error) => error);
|
||||
assert(!(video instanceof Error), `should download video (${Constants.test_video_id})`, video);
|
||||
}
|
||||
|
||||
const n_token = new NToken(Constants.n_scramble_sc, Constants.original_ntoken).transform();
|
||||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token);
|
||||
|
||||
const transformed_url = new SigDecipher(Constants.test_url, { sig_decipher_sc: Constants.sig_decipher_sc }).decipher();
|
||||
assert(transformed_url == Constants.expected_url, `should correctly decipher signature`, transformed_url);
|
||||
|
||||
if (failed_tests > 0)
|
||||
throw new Error('Some tests have failed');
|
||||
}
|
||||
|
||||
function downloadVideo(id, youtube) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let got_video_info = false;
|
||||
const stream = youtube.download(id, { type: 'videoandaudio' });
|
||||
stream.pipe(Fs.createWriteStream(`./${id}.mp4`));
|
||||
stream.on('end', () => Fs.existsSync(`./${id}.mp4`) && got_video_info && resolve() || reject());
|
||||
stream.on('info', () => got_video_info = true);
|
||||
stream.on('error', (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
function assert(outcome, description, data) {
|
||||
const pass_fail = outcome ? 'pass' : 'fail';
|
||||
|
||||
console.info(pass_fail, ':', description);
|
||||
!outcome && (failed_tests += 1);
|
||||
!outcome && console.error('Error: ', data);
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
performTests();
|
||||
'use strict';
|
||||
|
||||
const Fs = require('fs');
|
||||
const Innertube = require('..');
|
||||
const NToken = require('../lib/deciphers/NToken');
|
||||
const Signature = require('../lib/deciphers/Signature');
|
||||
const Constants = require('./constants');
|
||||
|
||||
let failed_tests_count = 0;
|
||||
|
||||
async function performTests() {
|
||||
const youtube = await new Innertube().catch((error) => error);
|
||||
assert(!(youtube instanceof Error), `should retrieve Innertube configuration data`, youtube);
|
||||
|
||||
if (!(youtube instanceof Error)) {
|
||||
const homefeed = await youtube.getHomeFeed().catch((error) => error);
|
||||
assert(!(homefeed instanceof Error), `should retrieve recommendations`, homefeed);
|
||||
|
||||
const ytsearch = await youtube.search('Carl Sagan - Documentary').catch((error) => error);
|
||||
assert(!(ytsearch instanceof Error), `should search on YouTube`, ytsearch);
|
||||
|
||||
const ytmsearch = await youtube.search('Logic - Obediently Yours', { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmsearch instanceof Error), `should search on YouTube Music`, ytmsearch);
|
||||
|
||||
const ytsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YOUTUBE' }).catch((error) => error);
|
||||
assert(!(ytsearch_suggestions instanceof Error), `should retrieve YouTube search suggestions`, ytsearch_suggestions);
|
||||
|
||||
const ytmsearch_suggestions = await youtube.getSearchSuggestions('test', { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmsearch_suggestions instanceof Error), `should retrieve YouTube Music search suggestions`, ytmsearch_suggestions);
|
||||
|
||||
const details = await youtube.getDetails(Constants.test_video_id).catch((error) => error);
|
||||
assert(!(details instanceof Error), `should retrieve details for ${Constants.test_video_id}`, details);
|
||||
|
||||
const comments = await youtube.getComments(Constants.test_video_id).catch((error) => error);
|
||||
assert(!(comments instanceof Error), `should retrieve comments for ${Constants.test_video_id}`, comments);
|
||||
|
||||
const ytplaylist = await youtube.getPlaylist(ytmsearch?.results?.community_playlists[0].id, { client: 'YOUTUBE' }).catch((error) => error);
|
||||
assert(!(ytplaylist instanceof Error), `should retrieve and parse playlist with YouTube`, ytplaylist);
|
||||
|
||||
const ytmplaylist = await youtube.getPlaylist(ytmsearch?.results?.community_playlists[0].id, { client: 'YTMUSIC' }).catch((error) => error);
|
||||
assert(!(ytmplaylist instanceof Error), `should retrieve and parse playlist with YouTube Music`, ytmplaylist);
|
||||
|
||||
const lyrics = await youtube.getLyrics(Constants.test_song_id).catch((error) => error);
|
||||
assert(!(lyrics instanceof Error), `should retrieve song lyrics`, lyrics);
|
||||
|
||||
const video = await downloadVideo(Constants.test_video_id, youtube).catch((error) => error);
|
||||
assert(!(video instanceof Error), `should download video (${Constants.test_video_id})`, video);
|
||||
}
|
||||
|
||||
const n_token = new NToken(Constants.n_scramble_sc, Constants.original_ntoken).transform();
|
||||
assert(n_token == Constants.expected_ntoken, `should transform n token into ${Constants.expected_ntoken}`, n_token);
|
||||
|
||||
const transformed_url = new Signature(Constants.test_url, { sig_decipher_sc: Constants.sig_decipher_sc }).decipher();
|
||||
assert(transformed_url == Constants.expected_url, `should decipher signature`, transformed_url);
|
||||
|
||||
if (failed_tests_count > 0)
|
||||
throw new Error(`${failed_tests_count} tests have failed`);
|
||||
}
|
||||
|
||||
function downloadVideo(id, youtube) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let got_video_info = false;
|
||||
const stream = youtube.download(id, { type: 'videoandaudio' });
|
||||
stream.pipe(Fs.createWriteStream(`./${id}.mp4`));
|
||||
stream.on('end', () => Fs.existsSync(`./${id}.mp4`) && got_video_info && resolve() || reject());
|
||||
stream.on('info', () => got_video_info = true);
|
||||
stream.on('error', (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
function assert(outcome, description, data) {
|
||||
const pass_fail = outcome ? 'pass' : 'fail';
|
||||
|
||||
console.info(pass_fail, ':', description);
|
||||
!outcome && (failed_tests_count += 1);
|
||||
!outcome && console.error('Error: ', data);
|
||||
|
||||
return outcome;
|
||||
}
|
||||
|
||||
performTests();
|
||||
190
typings/index.d.ts
vendored
Normal file
190
typings/index.d.ts
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
interface AuthInfo {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires: Date;
|
||||
}
|
||||
|
||||
interface AccountInfo {
|
||||
name: string;
|
||||
photo: Record<string, any>[];
|
||||
country: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
client: 'YTMUSIC' | 'YOUTUBE';
|
||||
period: 'any' | 'hour' | 'day' | 'week' | 'month' | 'year';
|
||||
order: 'relevance' | 'rating' | 'age' | 'views';
|
||||
duration: 'any' | 'short' | 'long';
|
||||
}
|
||||
|
||||
interface YouTubeSearch {
|
||||
query: string;
|
||||
corrected_query: string;
|
||||
estimated_results: number;
|
||||
videos: any[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface YouTubeMusicSearch {
|
||||
query: string;
|
||||
corrected_query: string;
|
||||
results: {
|
||||
top_result?: any[];
|
||||
songs?: any[];
|
||||
albums?: any[];
|
||||
videos?: any[];
|
||||
community_playlists?: any[];
|
||||
artists?: any[];
|
||||
}
|
||||
}
|
||||
|
||||
type SearchResults = YouTubeSearch | YouTubeMusicSearch;
|
||||
|
||||
type ClientOption = Pick<SearchOptions, 'client'>;
|
||||
|
||||
type SortBy = 'TOP_COMMENTS' | 'NEWEST_FIRST';
|
||||
|
||||
interface Suggestion {
|
||||
text: string;
|
||||
bold_text: string;
|
||||
}
|
||||
|
||||
interface ApiStatus {
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: object;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface Comments {
|
||||
page_count: number,
|
||||
comment_count: number;
|
||||
items: any[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface Video {
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail: object;
|
||||
metadata: Record<any, any>;
|
||||
like: () => Promise<ApiStatus>;
|
||||
dislike: () => Promise<ApiStatus>;
|
||||
removeLike: () => Promise<ApiStatus>;
|
||||
subscribe: () => Promise<ApiStatus>;
|
||||
unsubscribe: () => Promise<ApiStatus>;
|
||||
comment: (text: string) => Promise<ApiStatus>;
|
||||
getComments: () => Promise<Comments>;
|
||||
getLivechat: () => any; // TODO type LiveChat
|
||||
setNotificationPreferences: () => Promise<ApiStatus>;
|
||||
}
|
||||
|
||||
interface Channel {
|
||||
title: string;
|
||||
description: string;
|
||||
metadata: object;
|
||||
content: object;
|
||||
}
|
||||
|
||||
interface PlayList {
|
||||
description: string;
|
||||
items: any[];
|
||||
title: string;
|
||||
total_items: string | number;
|
||||
duration?: string;
|
||||
last_updated?: string;
|
||||
views?: string;
|
||||
year?: string;
|
||||
}
|
||||
|
||||
interface History {
|
||||
items: {
|
||||
date: string;
|
||||
videos: any[];
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface SubscriptionFeed {
|
||||
items: {
|
||||
date: string;
|
||||
videos: any[];
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface HomeFeed {
|
||||
videos: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
channel: string;
|
||||
metadata: Record<string, any>;
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface Trending {
|
||||
now: {
|
||||
content: {
|
||||
title: string;
|
||||
videos: [];
|
||||
}[];
|
||||
};
|
||||
music: { getVideos: () => Promise<Array>; };
|
||||
gaming: { getVideos: () => Promise<Array>; };
|
||||
movies: { getVideos: () => Promise<Array>; };
|
||||
}
|
||||
|
||||
interface Notifications {
|
||||
items: {
|
||||
title: string;
|
||||
sent_time: string;
|
||||
channel_name: string;
|
||||
channel_thumbnail: Record<string, any>;
|
||||
video_thumbnail: Record<string, any>;
|
||||
video_url: string;
|
||||
read: boolean;
|
||||
notification_id: string;
|
||||
}[];
|
||||
getContinuation: () => Promise<object>;
|
||||
}
|
||||
|
||||
interface StreamingData {
|
||||
selected_format: Record<string, any>;
|
||||
formats: any[];
|
||||
}
|
||||
interface StreamingOptions {
|
||||
quality?: string;
|
||||
type?: string;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
gl?: string;
|
||||
cookie?: string;
|
||||
}
|
||||
|
||||
export default class Innertube {
|
||||
constructor(auth_info?: Config)
|
||||
|
||||
public signIn(auth_info: AuthInfo): Promise<void>;
|
||||
public signOut(): Promise<ApiStatus>;
|
||||
public getAccountInfo(): Promise<AccountInfo>;
|
||||
public search(query: string, options: SearchOptions): Promise<SearchResults>;
|
||||
public getSearchSuggestions(query: string, options?: ClientOption): Promise<Suggestion>;
|
||||
public getDetails(video_id: string): Promise<ApiStatus>;
|
||||
public getChannel(id: string): Promise<Channel>;
|
||||
public getLyrics(video_id: string): Promise<string>;
|
||||
public getPlaylist(playlist_id: string, options?: ClientOption): Promise<PlayList>;
|
||||
public getComments(video_id: string, sort_by?: SortBy): Promise<Comments>;
|
||||
public getHistory(): Promise<History>;
|
||||
public getHomeFeed(): Promise<HomeFeed>;
|
||||
public getTrending(): Promise<Trending>;
|
||||
public getSubscriptionsFeed(): Promise<SubscriptionFeed>;
|
||||
public getNotifications(): Promise<Notifications>;
|
||||
public getUnseenNotificationsCount(): Promise<number>;
|
||||
public getStreamingData(id: string, options?: StreamingOptions): Promise<StreamingData>;
|
||||
public download(id: string, options?: StreamingOptions): ReadableStream;
|
||||
}
|
||||
Reference in New Issue
Block a user