Compare commits

...

10 Commits

Author SHA1 Message Date
LuanRT
4ecd3360e0 chore(release): v2.2.1 2022-09-17 20:47:55 -03:00
LuanRT
08e9527931 chore: update proto [skip ci] 2022-09-17 20:07:23 -03:00
LuanRT
a9f03a1523 fix: like/dislike methods not working correctly 2022-09-17 19:49:05 -03:00
LuanRT
c8980c7985 chore(docs): fix typo 2022-09-17 19:28:46 -03:00
LuanRT
2e5688f235 feat: add TVHTML5_SIMPLY_EMBEDDED_PLAYER client (#193)
* feat: add `TV_EMBEDDED` client

See #191, this should help bypassing some age restricted videos.

* dev(VideoInfo): update format options interface

* dev: set `clientScreen` to `EMBED`

* dev: update API ref

* dev: update `Context` interface
2022-09-17 19:15:20 -03:00
LuanRT
dcf2b720a0 fix: minor parsing issues and other improvements (#194)
* feat: add `ConfirmDialog`

This usually appears in the `playability_status` object.

* fix(PlayerErrorMessage): check if `iconType` exists before parsing

* chore(parser): fix a few inconsistencies

* feat(ytmusic): add `MetadataScreen`

TODO: Check TrackInfo, YouTube Music is probably getting some minor UI updates.
2022-09-17 19:14:46 -03:00
SALVADOR, 1 M. LIGAYAO
a90f5eb853 fix: add missing import in index.ts (#188)
* added missing import in index.ts

* commit changes suggested by LuanRT

Co-authored-by: LuanRT <luan.lrt4@gmail.com>

* removed extra require

Co-authored-by: Salvador Ligayao <futuremr.ligayao03@gmail.com>
Co-authored-by: LuanRT <luan.lrt4@gmail.com>
2022-09-17 01:30:35 -03:00
Supertiger
c6482e07b9 docs: fix a small typo in api/session.md (#189)
It says "key" twice, someone forgot to rename one of them to "api_version" :)
2022-09-16 12:21:57 -03:00
LuanRT
2de77c8f2c fix: make cookie auth possible again
See #105
2022-09-14 14:52:10 -03:00
LuanRT
2aaa209906 chore(docs): fix typo [skip ci] 2022-09-13 03:03:22 -03:00
21 changed files with 106 additions and 31 deletions

View File

@@ -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.
};

View File

@@ -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`

View File

@@ -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
View File

@@ -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"
],

View File

@@ -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",

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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> {

View File

@@ -25,6 +25,7 @@ export interface Context {
userAgent: string;
clientName: string;
clientVersion: string;
clientScreen?: string,
androidSdkVersion?: string;
osName: string;
osVersion: string;

View File

@@ -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:

View 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;

View 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;

View File

@@ -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;
}
}

View File

@@ -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]);
}
}

View File

@@ -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);

View File

@@ -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 || ''

View File

@@ -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,

View File

@@ -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 });

View File

@@ -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";

View File

@@ -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({

View File

@@ -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;
}