mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-14 10:02:16 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ecd3360e0 | ||
|
|
08e9527931 | ||
|
|
a9f03a1523 | ||
|
|
c8980c7985 | ||
|
|
2e5688f235 | ||
|
|
dcf2b720a0 | ||
|
|
a90f5eb853 | ||
|
|
c6482e07b9 | ||
|
|
2de77c8f2c | ||
|
|
2aaa209906 |
@@ -292,7 +292,7 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID` or `YTMUSIC` |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
|
||||
|
||||
<details>
|
||||
<summary>Methods & Getters</summary>
|
||||
@@ -347,7 +347,7 @@ Suitable for cases where you only need basic video metadata. Also, it is faster
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| video_id | `string` | The id of the video |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID` or `YTMUSIC` |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC_ANDROID`, `YTMUSIC`, `TV_EMBEDDED` |
|
||||
|
||||
<a name="search"></a>
|
||||
### search(query, filters?)
|
||||
@@ -569,7 +569,7 @@ For example, you may want to call an endpoint directly, that can be achieved wit
|
||||
|
||||
const payload = {
|
||||
videoId: 'jLTOuvBTLxA',
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, WEB
|
||||
client: 'YTMUSIC', // InnerTube client, can be ANDROID, YTMUSIC, YTMUSIC_ANDROID, WEB or TV_EMBEDDED
|
||||
parse: true // tells YouTube.js to parse the response, this is not sent to InnerTube.
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ InnerTube API key.
|
||||
**Returns:** `string`
|
||||
|
||||
<a name="api_version"></a>
|
||||
### key
|
||||
### api_version
|
||||
|
||||
InnerTube API version.
|
||||
|
||||
@@ -80,4 +80,4 @@ Player script object.
|
||||
|
||||
Client language.
|
||||
|
||||
**Returns:** `string`
|
||||
**Returns:** `string`
|
||||
|
||||
5
index.ts
5
index.ts
@@ -10,6 +10,11 @@ if (getRuntime() === 'node') {
|
||||
Reflect.set(globalThis, 'Response', undici.Response);
|
||||
Reflect.set(globalThis, 'FormData', undici.FormData);
|
||||
Reflect.set(globalThis, 'File', undici.File);
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const { ReadableStream } = require('node:stream/web');
|
||||
Reflect.set(globalThis, 'ReadableStream', ReadableStream);
|
||||
} catch { /* do nothing */ }
|
||||
}
|
||||
|
||||
import Innertube from './src/Innertube';
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.1",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Full-featured wrapper around YouTube's private API.",
|
||||
"main": "./dist/index.js",
|
||||
"browser": "./bundle/browser.js",
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface SearchFilters {
|
||||
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
|
||||
}
|
||||
|
||||
export type InnerTubeClient = 'ANDROID' | 'YTMUSIC_ANDROID' | 'WEB' | 'YTMUSIC';
|
||||
export type InnerTubeClient = 'WEB' | 'ANDROID' | 'YTMUSIC_ANDROID' | 'YTMUSIC' | 'TV_EMBEDDED';
|
||||
|
||||
class Innertube {
|
||||
session;
|
||||
|
||||
@@ -64,11 +64,11 @@ class Actions {
|
||||
/**
|
||||
* Mimmics the Axios API using Fetch's Response object.
|
||||
*/
|
||||
async #wrap(response: Response, protobuf?: boolean) {
|
||||
async #wrap(response: Response) {
|
||||
return {
|
||||
success: response.ok,
|
||||
status_code: response.status,
|
||||
data: protobuf ? await response.text() : JSON.parse(await response.text())
|
||||
data: JSON.parse(await response.text())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -776,7 +776,7 @@ class Actions {
|
||||
return Parser.parseResponse(await response.json());
|
||||
}
|
||||
|
||||
return this.#wrap(response, args.protobuf);
|
||||
return this.#wrap(response);
|
||||
}
|
||||
|
||||
#needsLogin(id: string) {
|
||||
|
||||
@@ -38,7 +38,7 @@ class Music {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrives track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
|
||||
* Retrieves track info. Passing a list item of type MusicTwoRowItem automatically starts a radio.
|
||||
* @param target - video id or a list item.
|
||||
*/
|
||||
getInfo(target: string | MusicTwoRowItem): Promise<TrackInfo> {
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Context {
|
||||
userAgent: string;
|
||||
clientName: string;
|
||||
clientVersion: string;
|
||||
clientScreen?: string,
|
||||
androidSdkVersion?: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
|
||||
@@ -89,7 +89,7 @@ const is_null = response.is_null;
|
||||
## YTNode
|
||||
All renderers returned by InnerTube are converted to this generic class and then extended for the specific renderers.
|
||||
|
||||
This class is allows us a typesafe way to use data returned by the InnerTube API.
|
||||
This class is what allows us a typesafe way to use data returned by the InnerTube API.
|
||||
|
||||
Here's how to use this class to access returned data:
|
||||
|
||||
|
||||
24
src/parser/classes/ConfirmDialog.ts
Normal file
24
src/parser/classes/ConfirmDialog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import Parser from '..';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class ConfirmDialog extends YTNode {
|
||||
static type = 'ConfirmDialog';
|
||||
|
||||
title: Text;
|
||||
confirm_button: Button | null;
|
||||
cancel_button: Button | null;
|
||||
dialog_messages: Text[];
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.title = new Text(data.title);
|
||||
this.confirm_button = Parser.parseItem<Button>(data.confirmButton, Button);
|
||||
this.cancel_button = Parser.parseItem<Button>(data.cancelButton, Button);
|
||||
this.dialog_messages = data.dialogMessages.map((txt: any) => new Text(txt));
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
15
src/parser/classes/MetadataScreen.ts
Normal file
15
src/parser/classes/MetadataScreen.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Parser from '..';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class MetadataScreen extends YTNode {
|
||||
static type = 'MetadataScreen';
|
||||
|
||||
section_list;
|
||||
|
||||
constructor (data: any) {
|
||||
super();
|
||||
this.section_list = Parser.parseItem(data);
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataScreen;
|
||||
@@ -1,6 +1,8 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Thumbnail from './misc/Thumbnail';
|
||||
import Button from './Button';
|
||||
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class PlayerErrorMessage extends YTNode {
|
||||
@@ -8,17 +10,17 @@ class PlayerErrorMessage extends YTNode {
|
||||
|
||||
subreason: Text;
|
||||
reason: Text;
|
||||
proceed_button;
|
||||
proceed_button: Button | null;
|
||||
thumbnails: Thumbnail[];
|
||||
icon_type: string;
|
||||
icon_type: string | null;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.subreason = new Text(data.subreason);
|
||||
this.reason = new Text(data.reason);
|
||||
this.proceed_button = Parser.parse(data.proceedButton);
|
||||
this.proceed_button = Parser.parseItem<Button>(data.proceedButton, Button);
|
||||
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
|
||||
this.icon_type = data.icon.iconType;
|
||||
this.icon_type = data.icon?.iconType || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import NavigationEndpoint from './NavigationEndpoint';
|
||||
import SubscriptionNotificationToggleButton from './SubscriptionNotificationToggleButton';
|
||||
import { YTNode } from '../helpers';
|
||||
|
||||
class SubscribeButton extends YTNode {
|
||||
@@ -14,7 +15,7 @@ class SubscribeButton extends YTNode {
|
||||
show_preferences: boolean;
|
||||
subscribed_text: Text;
|
||||
unsubscribed_text: Text;
|
||||
notification_preference_button;
|
||||
notification_preference_button: SubscriptionNotificationToggleButton | null;
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: any) {
|
||||
@@ -27,7 +28,7 @@ class SubscribeButton extends YTNode {
|
||||
this.show_preferences = data.showPreferences;
|
||||
this.subscribed_text = new Text(data.subscribedButtonText);
|
||||
this.unsubscribed_text = new Text(data.unsubscribedButtonText);
|
||||
this.notification_preference_button = Parser.parse(data.notificationPreferenceButton);
|
||||
this.notification_preference_button = Parser.parseItem<SubscriptionNotificationToggleButton>(data.notificationPreferenceButton, SubscriptionNotificationToggleButton);
|
||||
this.endpoint = new NavigationEndpoint(data.serviceEndpoints?.[0] || data.onSubscribeEndpoints?.[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index';
|
||||
import Text from './misc/Text';
|
||||
import Button from './Button';
|
||||
import VideoOwner from './VideoOwner';
|
||||
import SubscribeButton from './SubscribeButton';
|
||||
import MetadataRowContainer from './MetadataRowContainer';
|
||||
import { YTNode } from '../helpers';
|
||||
@@ -8,7 +9,7 @@ import { YTNode } from '../helpers';
|
||||
class VideoSecondaryInfo extends YTNode {
|
||||
static type = 'VideoSecondaryInfo';
|
||||
|
||||
owner; // TODO: VideoOwner?
|
||||
owner: VideoOwner | null;// TODO: VideoOwner?
|
||||
description: Text;
|
||||
subscribe_button;
|
||||
metadata: MetadataRowContainer | null;
|
||||
@@ -19,7 +20,7 @@ class VideoSecondaryInfo extends YTNode {
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.owner = Parser.parse(data.owner);
|
||||
this.owner = Parser.parseItem<VideoOwner>(data.owner);
|
||||
this.description = new Text(data.description);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton | Button>(data.subscribeButton, [ SubscribeButton, Button ]);
|
||||
this.metadata = Parser.parseItem<MetadataRowContainer>(data.metadataRowContainer, MetadataRowContainer);
|
||||
|
||||
@@ -259,7 +259,7 @@ export default class Parser {
|
||||
} : null,
|
||||
playability_status: data.playabilityStatus ? {
|
||||
status: data.playabilityStatus.status as string,
|
||||
error_screen: Parser.parse(data.playabilityStatus.errorScreen),
|
||||
error_screen: Parser.parseItem(data.playabilityStatus.errorScreen),
|
||||
audio_only_playablility: Parser.parseItem<AudioOnlyPlayability>(data.playabilityStatus.audioOnlyPlayability, AudioOnlyPlayability),
|
||||
embeddable: !!data.playabilityStatus.playableInEmbed || false,
|
||||
reason: data.playabilityStatus?.reason || ''
|
||||
|
||||
@@ -55,6 +55,7 @@ import { default as CompactLink } from './classes/CompactLink';
|
||||
import { default as CompactMix } from './classes/CompactMix';
|
||||
import { default as CompactPlaylist } from './classes/CompactPlaylist';
|
||||
import { default as CompactVideo } from './classes/CompactVideo';
|
||||
import { default as ConfirmDialog } from './classes/ConfirmDialog';
|
||||
import { default as ContinuationItem } from './classes/ContinuationItem';
|
||||
import { default as CopyLink } from './classes/CopyLink';
|
||||
import { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog';
|
||||
@@ -143,6 +144,7 @@ import { default as MetadataBadge } from './classes/MetadataBadge';
|
||||
import { default as MetadataRow } from './classes/MetadataRow';
|
||||
import { default as MetadataRowContainer } from './classes/MetadataRowContainer';
|
||||
import { default as MetadataRowHeader } from './classes/MetadataRowHeader';
|
||||
import { default as MetadataScreen } from './classes/MetadataScreen';
|
||||
import { default as MicroformatData } from './classes/MicroformatData';
|
||||
import { default as Mix } from './classes/Mix';
|
||||
import { default as Movie } from './classes/Movie';
|
||||
@@ -328,6 +330,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CompactMix,
|
||||
CompactPlaylist,
|
||||
CompactVideo,
|
||||
ConfirmDialog,
|
||||
ContinuationItem,
|
||||
CopyLink,
|
||||
CreatePlaylistDialog,
|
||||
@@ -416,6 +419,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
MetadataRow,
|
||||
MetadataRowContainer,
|
||||
MetadataRowHeader,
|
||||
MetadataScreen,
|
||||
MicroformatData,
|
||||
Mix,
|
||||
Movie,
|
||||
|
||||
@@ -46,9 +46,9 @@ export interface FormatOptions {
|
||||
*/
|
||||
format?: string;
|
||||
/**
|
||||
* InnerTube client, can be ANDROID, WEB or YTMUSIC
|
||||
* InnerTube client, can be ANDROID, WEB, YTMUSIC, YTMUSIC_ANDROID or TV_EMBEDDED
|
||||
*/
|
||||
client?: 'ANDROID' | 'WEB' | 'YTMUSIC'
|
||||
client?: 'ANDROID' | 'WEB' | 'YTMUSIC' | 'YTMUSIC_ANDROID' | 'TV_EMBEDDED'
|
||||
}
|
||||
|
||||
export interface DownloadOptions extends FormatOptions {
|
||||
@@ -229,7 +229,8 @@ class VideoInfo {
|
||||
* Likes the video.
|
||||
*/
|
||||
async like() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ button_id: 'TOGGLE_BUTTON_ID_TYPE_LIKE' })?.as(ToggleButton);
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.like_button?.as(ToggleButton);
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Like button not found', { video_id: this.basic_info.id });
|
||||
@@ -246,7 +247,8 @@ class VideoInfo {
|
||||
* Dislikes the video.
|
||||
*/
|
||||
async dislike() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ button_id: 'TOGGLE_BUTTON_ID_TYPE_DISLIKE' })?.as(ToggleButton);
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.dislike_button?.as(ToggleButton);
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('Dislike button not found', { video_id: this.basic_info.id });
|
||||
@@ -263,7 +265,17 @@ class VideoInfo {
|
||||
* Removes like/dislike.
|
||||
*/
|
||||
async removeLike() {
|
||||
const button = this.primary_info?.menu?.top_level_buttons?.get({ is_toggled: true })?.as(ToggleButton);
|
||||
let button;
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const like_button = segmented_like_dislike_button?.like_button?.as(ToggleButton);
|
||||
const dislike_button = segmented_like_dislike_button?.dislike_button?.as(ToggleButton);
|
||||
|
||||
if (like_button?.is_toggled) {
|
||||
button = like_button;
|
||||
} else if (dislike_button?.is_toggled) {
|
||||
button = dislike_button;
|
||||
}
|
||||
|
||||
if (!button)
|
||||
throw new InnertubeError('This video is not liked/disliked', { video_id: this.basic_info.id });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @generated by protobuf-ts 2.8.0
|
||||
// @generated by protobuf-ts 2.7.0
|
||||
// @generated from protobuf file "youtube.proto" (package "youtube", syntax proto2)
|
||||
// tslint:disable
|
||||
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||
|
||||
@@ -6,7 +6,8 @@ export const URLS = Object.freeze({
|
||||
YT_UPLOAD: 'https://upload.youtube.com/',
|
||||
API: Object.freeze({
|
||||
BASE: 'https://youtubei.googleapis.com',
|
||||
PRODUCTION: 'https://youtubei.googleapis.com/youtubei/',
|
||||
PRODUCTION_1: 'https://www.youtube.com/youtubei/',
|
||||
PRODUCTION_2: 'https://youtubei.googleapis.com/youtubei/',
|
||||
STAGING: 'https://green-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
RELEASE: 'https://release-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
TEST: 'https://test-youtubei.sandbox.googleapis.com/youtubei/',
|
||||
@@ -48,6 +49,10 @@ export const CLIENTS = Object.freeze({
|
||||
YTMUSIC_ANDROID: {
|
||||
NAME: 'ANDROID_MUSIC',
|
||||
VERSION: '5.17.51'
|
||||
},
|
||||
TV_EMBEDDED: {
|
||||
NAME: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
|
||||
VERSION: '2.0'
|
||||
}
|
||||
});
|
||||
export const STREAM_HEADERS = Object.freeze({
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class HTTPClient {
|
||||
input: URL | Request | string,
|
||||
init?: RequestInit & HTTPClientInit
|
||||
) {
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION + this.#session.api_version;
|
||||
const innertube_url = Constants.URLS.API.PRODUCTION_1 + this.#session.api_version;
|
||||
const baseURL = init?.baseURL || innertube_url;
|
||||
|
||||
const request_url =
|
||||
@@ -143,6 +143,11 @@ export default class HTTPClient {
|
||||
ctx.client.clientName = Constants.CLIENTS.YTMUSIC_ANDROID.NAME;
|
||||
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
|
||||
break;
|
||||
case 'TV_EMBEDDED':
|
||||
ctx.client.clientVersion = Constants.CLIENTS.TV_EMBEDDED.VERSION;
|
||||
ctx.client.clientName = Constants.CLIENTS.TV_EMBEDDED.NAME;
|
||||
ctx.client.clientScreen = 'EMBED';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user