Compare commits

..

16 Commits

Author SHA1 Message Date
LuanRT
aefecd061e chore(release): v2.2.3 2022-09-23 03:19:54 -03:00
LuanRT
7485726f1e refactor: fix a few parsing inconsistencies 2022-09-23 03:06:21 -03:00
LuanRT
9e703abe3a chore(deps): bump jintr to 0.3.1 2022-09-22 18:44:16 -03:00
LuanRT
affbe84284 fix: include thirdParty prop for requests using TV_EMBEDDED (#198)
* dev: update `Context` interface

* dev: include `thirdParty` prop in requests using `TV_EMBEDDED`
2022-09-18 16:58:51 -03:00
Daniel Wykerd
fcbdae3e34 fix: browser example (#197) 2022-09-18 12:46:19 -03:00
LuanRT
059c858021 chore(docs): add a note about streaming data [skip ci] 2022-09-17 21:29:33 -03:00
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
27 changed files with 189 additions and 62 deletions

View File

@@ -42,7 +42,9 @@
[![Codefactor](https://www.codefactor.io/repository/github/luanrt/youtube.js/badge)][codefactor]
[![Downloads](https://img.shields.io/npm/dt/youtubei.js)][npm]
[![Say thanks](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)][say-thanks]
<br>
[![Donate](https://img.shields.io/badge/donate-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#white)][github-sponsors]
</div>
<!-- SPONSORS -->
@@ -292,7 +294,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 +349,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?)
@@ -527,6 +529,14 @@ Retrieves playlist contents.
### getStreamingData(video_id, options)
Returns deciphered streaming data.
**Note:**
It is recommended to retrieve streaming data from a `VideoInfo`/`TrackInfo` object instead if you want to select formats manually, example:
```ts
const info = await yt.getBasicInfo('somevideoid');
const url = info.streaming_data?.formats[0].decipher(yt.session.player);
console.info('Playback url:', url);
```
**Returns**: `Promise.<object>`
| Param | Type | Description |
@@ -569,7 +579,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

@@ -6,17 +6,52 @@ YouTube.js works in the browser!
To use YouTube.js in the browser you must proxy requests through your own server. You can see our simple reference implementation in Deno in `examples/browser/proxy/deno.ts`.
Once the proxy is set up you need to tell Innertube about it when instantiating it.
We'll use our own fetch implementation to proxy requests through our server. This is a simple example, but you can use any fetch implementation you want.
```ts
import { Innertube } from "youtubei.js/build/browser";
const yt = await Innertube.create({
browser_proxy: {
host: "localhost",
schema: 'http',
}
})
fetch: async (input, init) => {
// url
const url = typeof input === 'string'
? new URL(input)
: input instanceof URL
? input
: new URL(input.url);
// transform the url for use with our proxy
url.searchParams.set('__host', url.host);
url.host = 'localhost:8080';
url.protocol = 'http';
const headers = init?.headers
? new Headers(init.headers)
: input instanceof Request
? input.headers
: new Headers();
// now serialize the headers
url.searchParams.set('__headers', JSON.stringify([...headers]));
// copy over the request
const request = new Request(
url,
input instanceof Request ? input : undefined,
);
headers.delete('user-agent');
// fetch the url
return fetch(request, init ? {
...init,
headers
} : {
headers
});
},
cache: new UniversalCache(),
});
```
after that you can use the library as normal.

View File

@@ -18,7 +18,7 @@ const handler = async (request: Request): Promise<Response> => {
'Access-Control-Allow-Origin': request.headers.get('origin') || '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, Accept-Language, Range',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-origin, x-youtube-client-version, Accept-Language, Range, Referer',
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Credentials': 'true',
}),
@@ -45,7 +45,7 @@ const handler = async (request: Request): Promise<Response> => {
JSON.parse(url.searchParams.get('__headers') || '{}'),
);
copyHeader('range', request_headers, request.headers);
copyHeader('user-agent', request_headers, request.headers);
!request_headers.has('user-agent') && copyHeader('user-agent', request_headers, request.headers);
url.searchParams.delete('__headers');
// Make the request to YouTube
@@ -62,6 +62,8 @@ const handler = async (request: Request): Promise<Response> => {
copyHeader('content-length', headers, fetchRes.headers);
copyHeader('content-type', headers, fetchRes.headers);
copyHeader('content-disposition', headers, fetchRes.headers);
copyHeader('accept-ranges', headers, fetchRes.headers);
copyHeader('content-range', headers, fetchRes.headers);
// add cors headers
headers.set(

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

18
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "youtubei.js",
"version": "2.1.0",
"version": "2.2.3"
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "youtubei.js",
"version": "2.1.0",
"version": "2.2.3",
"funding": [
"https://github.com/sponsors/LuanRT"
],
"license": "MIT",
"dependencies": {
"@protobuf-ts/runtime": "^2.7.0",
"jintr": "^0.2.0",
"jintr": "^0.3.1",
"linkedom": "^0.14.12",
"undici": "^5.7.0"
},
@@ -3960,9 +3960,9 @@
}
},
"node_modules/jintr": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.2.0.tgz",
"integrity": "sha512-b1TmzzCnVivT6ftmRFtvT/pPu/t/pCDH/GA8yzXKfOGOVG6X/pmrRU9vdWgV1I/EJTKt/i/cvx6MfmGMjlhWMA==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.3.1.tgz",
"integrity": "sha512-AUcq8fKL4BE9jDx8TizZmJ9UOvk1CHKFW0nQcWaOaqk9tkLS9S10fNmusTWGEYTncn7U43nXrCbhYko/ylqrSg==",
"funding": [
"https://github.com/sponsors/LuanRT"
],
@@ -8180,9 +8180,9 @@
}
},
"jintr": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.2.0.tgz",
"integrity": "sha512-b1TmzzCnVivT6ftmRFtvT/pPu/t/pCDH/GA8yzXKfOGOVG6X/pmrRU9vdWgV1I/EJTKt/i/cvx6MfmGMjlhWMA==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/jintr/-/jintr-0.3.1.tgz",
"integrity": "sha512-AUcq8fKL4BE9jDx8TizZmJ9UOvk1CHKFW0nQcWaOaqk9tkLS9S10fNmusTWGEYTncn7U43nXrCbhYko/ylqrSg==",
"requires": {
"acorn": "^8.8.0"
}

View File

@@ -1,6 +1,6 @@
{
"name": "youtubei.js",
"version": "2.1.0",
"version": "2.2.3",
"description": "Full-featured wrapper around YouTube's private API.",
"main": "./dist/index.js",
"browser": "./bundle/browser.js",
@@ -39,7 +39,7 @@
"license": "MIT",
"dependencies": {
"@protobuf-ts/runtime": "^2.7.0",
"jintr": "^0.2.0",
"jintr": "^0.3.1",
"linkedom": "^0.14.12",
"undici": "^5.7.0"
},

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;
@@ -42,6 +43,9 @@ export interface Context {
user: {
lockedSafetyMode: false;
};
thirdParty?: {
embedUrl: string;
};
request: {
useSsl: true;
};

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

@@ -6,8 +6,8 @@ class Card extends YTNode {
teaser;
content;
card_id: string;
feature: string;
card_id: string | null;
feature: string | null;
cue_ranges: {
start_card_active_ms: string;
@@ -18,10 +18,10 @@ class Card extends YTNode {
constructor(data: any) {
super();
this.teaser = Parser.parse(data.teaser);
this.content = Parser.parse(data.content);
this.card_id = data.cardId;
this.feature = data.feature;
this.teaser = Parser.parseItem(data.teaser);
this.content = Parser.parseItem(data.content);
this.card_id = data.cardId || null;
this.feature = data.feature || null;
this.cue_ranges = data.cueRanges.map((cr: any) => ({
start_card_active_ms: cr.startCardActiveMs,
@@ -32,4 +32,4 @@ class Card extends YTNode {
}
}
export default Card;
export default Card;

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

@@ -8,7 +8,7 @@ class ProfileColumnStats extends YTNode {
constructor(data: any) {
super();
this.items = Parser.parse(data.items);
this.items = Parser.parseArray(data.items);
}
// XXX: alias for consistency
@@ -17,4 +17,4 @@ class ProfileColumnStats extends YTNode {
}
}
export default ProfileColumnStats;
export default ProfileColumnStats;

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 Author from './misc/Author';
import Menu from './menus/Menu';
import Thumbnail from './misc/Thumbnail';
import NavigationEndpoint from './NavigationEndpoint';
import { timeToSeconds } from '../../utils/Utils';
@@ -53,8 +54,8 @@ class Video extends YTNode {
})) || [];
this.thumbnails = Thumbnail.fromResponse(data.thumbnail);
this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays);
this.rich_thumbnail = data.richThumbnail && Parser.parse(data.richThumbnail);
this.thumbnail_overlays = Parser.parseArray(data.thumbnailOverlays);
this.rich_thumbnail = data.richThumbnail ? Parser.parse(data.richThumbnail) : null;
this.author = new Author(data.ownerText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint);
this.published = new Text(data.publishedTimeText);
@@ -73,7 +74,7 @@ class Video extends YTNode {
this.show_action_menu = data.showActionMenu;
this.is_watched = data.isWatched || false;
this.menu = Parser.parse(data.menu);
this.menu = Parser.parseItem<Menu>(data.menu, Menu);
}
get description(): string {

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

@@ -24,7 +24,7 @@ class Search extends Feed {
super(actions, data, already_parsed);
const contents =
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents.item().as(SectionList).contents.array() ||
this.page.contents?.item().as(TwoColumnSearchResults).primary_contents?.item().as(SectionList).contents.array() ||
this.page.on_response_received_commands?.[0].contents;
const secondary_contents_maybe = this.page.contents?.item().key('secondary_contents');
@@ -59,7 +59,7 @@ class Search extends Feed {
if (typeof card === 'string') {
target_card = this.refinement_cards.cards.get({ query: card });
if (!target_card)
throw new InnertubeError('Refinement card not found!', { available_cards: this.refinement_card_queries });
throw new InnertubeError(`Refinement card "${card}" not found`, { available_cards: this.refinement_card_queries });
} else if (card.type === 'SearchRefinementCard') {
target_card = card;
} else {

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,12 @@ 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';
ctx.thirdParty = { embedUrl: Constants.URLS.YT_BASE };
break;
default:
break;
}