mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b29244b41 | ||
|
|
f9754f5ac6 | ||
|
|
b2253df802 | ||
|
|
f3517708ff | ||
|
|
0d35fe0ca5 | ||
|
|
3e3dc351bb |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [3.3.0](https://github.com/LuanRT/YouTube.js/compare/v3.2.0...v3.3.0) (2023-03-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **parser:** add `ConversationBar` node ([b2253df](https://github.com/LuanRT/YouTube.js/commit/b2253df8022217dc486155d8cacbf22db04dd9c2))
|
||||
* **VideoInfo:** support get by endpoint + more info ([#342](https://github.com/LuanRT/YouTube.js/issues/342)) ([0d35fe0](https://github.com/LuanRT/YouTube.js/commit/0d35fe0ca5e87a877b76cbb6cf3c92843eac5a99))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **MultiMarkersPlayerBar:** avoid observing undefined objects ([f351770](https://github.com/LuanRT/YouTube.js/commit/f3517708ff34093a544c09d6f5f1ec806130d5cc))
|
||||
* **SharedPost:** import `Menu` node directly (oops) ([3e3dc35](https://github.com/LuanRT/YouTube.js/commit/3e3dc351bb44faec87616d9b922924d14a95f29f))
|
||||
* **ytmusic:** use static visitor id to avoid empty API responses ([f9754f5](https://github.com/LuanRT/YouTube.js/commit/f9754f5ac61d0f11b025f37f93783f971560268b)), closes [#279](https://github.com/LuanRT/YouTube.js/issues/279)
|
||||
|
||||
## [3.2.0](https://github.com/LuanRT/YouTube.js/compare/v3.1.1...v3.2.0) (2023-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ const yt = await Innertube.create({
|
||||
<summary>Methods</summary>
|
||||
<p>
|
||||
|
||||
* [.getInfo(video_id, client?)](#getinfo)
|
||||
* [.getInfo(target, client?)](#getinfo)
|
||||
* [.getBasicInfo(video_id, client?)](#getbasicinfo)
|
||||
* [.search(query, filters?)](#search)
|
||||
* [.getSearchSuggestions(query)](#getsearchsuggestions)
|
||||
@@ -273,7 +273,7 @@ const yt = await Innertube.create({
|
||||
</details>
|
||||
|
||||
<a name="getinfo"></a>
|
||||
### getInfo(video_id, client?)
|
||||
### getInfo(target, client?)
|
||||
|
||||
Retrieves video info, including playback data and even layout elements such as menus, buttons, etc — all nicely parsed.
|
||||
|
||||
@@ -281,7 +281,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 |
|
||||
| target | `string` \| `NavigationEndpoint` | If `string`, the id of the video. If `NavigationEndpoint`, the endpoint of watchable elements such as `Video`, `Mix` and `Playlist`. To clarify, valid endpoints have payloads containing at least `videoId` and optionally `playlistId`, `params` and `index`. |
|
||||
| client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |
|
||||
|
||||
<details>
|
||||
@@ -321,6 +321,9 @@ Retrieves video info, including playback data and even layout elements such as m
|
||||
- `<info>#addToWatchHistory()`
|
||||
- Adds the video to the watch history.
|
||||
|
||||
- `<info>#autoplay_video_endpoint`
|
||||
- Returns the endpoint of the video for Autoplay.
|
||||
|
||||
- `<info>#page`
|
||||
- Returns original InnerTube response (sanitized).
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
|
||||
@@ -31,7 +31,7 @@ import type Format from './parser/classes/misc/Format.js';
|
||||
import type { ApiResponse } from './core/Actions.js';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js';
|
||||
import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.js';
|
||||
import { generateRandomString, throwIfMissing } from './utils/Utils.js';
|
||||
import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.js';
|
||||
|
||||
export type InnertubeConfig = SessionOptions;
|
||||
|
||||
@@ -72,16 +72,48 @@ class Innertube {
|
||||
|
||||
/**
|
||||
* Retrieves video info.
|
||||
* @param video_id - The video id.
|
||||
* @param target - The video id or `NavigationEndpoint`.
|
||||
* @param client - The client to use.
|
||||
*/
|
||||
async getInfo(video_id: string, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ video_id });
|
||||
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
|
||||
throwIfMissing({ target });
|
||||
|
||||
let payload: {
|
||||
videoId: string,
|
||||
playlistId?: string,
|
||||
params?: string,
|
||||
playlistIndex?: number
|
||||
};
|
||||
|
||||
if (target instanceof NavigationEndpoint) {
|
||||
const video_id = target.payload?.videoId;
|
||||
if (!video_id) {
|
||||
throw new InnertubeError('Missing video id in endpoint payload.', target);
|
||||
}
|
||||
payload = {
|
||||
videoId: video_id
|
||||
};
|
||||
if (target.payload.playlistId) {
|
||||
payload.playlistId = target.payload.playlistId;
|
||||
}
|
||||
if (target.payload.params) {
|
||||
payload.params = target.payload.params;
|
||||
}
|
||||
if (target.payload.index) {
|
||||
payload.playlistIndex = target.payload.index;
|
||||
}
|
||||
} else if (typeof target === 'string') {
|
||||
payload = {
|
||||
videoId: target
|
||||
};
|
||||
} else {
|
||||
throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target);
|
||||
}
|
||||
|
||||
const cpn = generateRandomString(16);
|
||||
|
||||
const initial_info = this.actions.getVideoInfo(video_id, cpn, client);
|
||||
const continuation = this.actions.execute('/next', { videoId: video_id });
|
||||
const initial_info = this.actions.getVideoInfo(payload.videoId, cpn, client);
|
||||
const continuation = this.actions.execute('/next', payload);
|
||||
|
||||
const response = await Promise.all([ initial_info, continuation ]);
|
||||
return new VideoInfo(response, this.actions, this.session.player, cpn);
|
||||
|
||||
@@ -4,7 +4,7 @@ import Actions from './Actions.js';
|
||||
import Player from './Player.js';
|
||||
|
||||
import HTTPClient from '../utils/HTTPClient.js';
|
||||
import { Platform, DeviceCategory, generateRandomString, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
|
||||
import { Platform, DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
import Proto from '../proto/index.js';
|
||||
import { ICache } from '../types/Cache.js';
|
||||
@@ -232,7 +232,7 @@ export default class Session extends EventEmitterLike {
|
||||
'user-agent': getRandomUserAgent('desktop'),
|
||||
'accept': '*/*',
|
||||
'referer': 'https://www.youtube.com/sw.js',
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')}`
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${Constants.CLIENTS.WEB.STATIC_VISITOR_ID};`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -294,7 +294,7 @@ export default class Session extends EventEmitterLike {
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean
|
||||
}): SessionData {
|
||||
const id = generateRandomString(11);
|
||||
const id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const context: Context = {
|
||||
|
||||
16
src/parser/classes/ConversationBar.ts
Normal file
16
src/parser/classes/ConversationBar.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Parser, { RawNode } from '../index.js';
|
||||
import Message from './Message.js';
|
||||
|
||||
class ConversationBar extends YTNode {
|
||||
static type = 'ConversationBar';
|
||||
|
||||
availability_message: Message | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.availability_message = Parser.parseItem<Message>(data.availabilityMessage, Message);
|
||||
}
|
||||
}
|
||||
|
||||
export default ConversationBar;
|
||||
@@ -2,6 +2,7 @@ import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type Button from './Button.js';
|
||||
import type MultiMarkersPlayerBar from './MultiMarkersPlayerBar.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
class DecoratedPlayerBar extends YTNode {
|
||||
static type = 'DecoratedPlayerBar';
|
||||
@@ -9,7 +10,7 @@ class DecoratedPlayerBar extends YTNode {
|
||||
player_bar: MultiMarkersPlayerBar | null;
|
||||
player_bar_action_button: Button | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.player_bar = Parser.parseItem<MultiMarkersPlayerBar>(data.playerBar);
|
||||
this.player_bar_action_button = Parser.parseItem<Button>(data.playerBarActionButton);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Parser from '../index.js';
|
||||
import type Chapter from './Chapter.js';
|
||||
import type Heatmap from './Heatmap.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
|
||||
import { observe, ObservedArray, YTNode } from '../helpers.js';
|
||||
|
||||
@@ -13,7 +14,7 @@ class Marker extends YTNode {
|
||||
chapters?: Chapter[];
|
||||
};
|
||||
|
||||
constructor (data: any) {
|
||||
constructor (data: RawNode) {
|
||||
super();
|
||||
this.marker_key = data.key;
|
||||
|
||||
@@ -34,9 +35,12 @@ class MultiMarkersPlayerBar extends YTNode {
|
||||
|
||||
markers_map: ObservedArray<Marker>;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.markers_map = observe(data.markersMap?.map((marker: { key: string; value: { [key: string ]: any }}) => new Marker(marker)));
|
||||
this.markers_map = observe(data.markersMap?.map((marker: {
|
||||
key: string;
|
||||
value: { [key: string ]: any
|
||||
}}) => new Marker(marker)) || []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import { Menu } from '../map.js';
|
||||
import Parser from '../parser.js';
|
||||
import BackstagePost from './BackstagePost.js';
|
||||
import Button from './Button.js';
|
||||
import Menu from './menus/Menu.js';
|
||||
import Author from './misc/Author.js';
|
||||
import Text from './misc/Text.js';
|
||||
import Thumbnail from './misc/Thumbnail.js';
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import Parser from '../index.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
import Text from './misc/Text.js';
|
||||
import PlaylistAuthor from './misc/PlaylistAuthor.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
|
||||
import type Menu from './menus/Menu.js';
|
||||
|
||||
type AutoplaySet = {
|
||||
autoplay_video: NavigationEndpoint,
|
||||
next_button_video?: NavigationEndpoint
|
||||
};
|
||||
|
||||
class TwoColumnWatchNextResults extends YTNode {
|
||||
static type = 'TwoColumnWatchNextResults';
|
||||
@@ -7,12 +17,66 @@ class TwoColumnWatchNextResults extends YTNode {
|
||||
results;
|
||||
secondary_results;
|
||||
conversation_bar;
|
||||
playlist?: {
|
||||
id: string,
|
||||
title: string,
|
||||
author: Text | PlaylistAuthor,
|
||||
contents: YTNode[],
|
||||
current_index: number,
|
||||
is_infinite: boolean,
|
||||
menu: Menu | null
|
||||
};
|
||||
autoplay?: {
|
||||
sets: AutoplaySet[],
|
||||
modified_sets?: AutoplaySet[],
|
||||
count_down_secs?: number
|
||||
};
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.results = Parser.parseArray(data.results?.results.contents);
|
||||
this.secondary_results = Parser.parseArray(data.secondaryResults?.secondaryResults.results);
|
||||
this.conversation_bar = Parser.parseItem(data?.conversationBar);
|
||||
|
||||
const playlistData = data.playlist?.playlist;
|
||||
if (playlistData) {
|
||||
this.playlist = {
|
||||
id: playlistData.playlistId,
|
||||
title: playlistData.title,
|
||||
author: playlistData.shortBylineText?.simpleText ?
|
||||
new Text(playlistData.shortBylineText) :
|
||||
new PlaylistAuthor(playlistData.longBylineText),
|
||||
contents: Parser.parseArray(playlistData.contents),
|
||||
current_index: playlistData.currentIndex,
|
||||
is_infinite: !!playlistData.isInfinite,
|
||||
menu: Parser.parseItem<Menu>(playlistData.menu)
|
||||
};
|
||||
}
|
||||
|
||||
const autoplayData = data.autoplay?.autoplay;
|
||||
if (autoplayData) {
|
||||
this.autoplay = {
|
||||
sets: autoplayData.sets.map((set: any) => this.#parseAutoplaySet(set))
|
||||
};
|
||||
if (autoplayData.modifiedSets) {
|
||||
this.autoplay.modified_sets = autoplayData.modifiedSets.map((set: any) => this.#parseAutoplaySet(set));
|
||||
}
|
||||
if (autoplayData.countDownSecs) {
|
||||
this.autoplay.count_down_secs = autoplayData.countDownSecs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#parseAutoplaySet(data: any): AutoplaySet {
|
||||
const result = {
|
||||
autoplay_video: new NavigationEndpoint(data.autoplayVideo)
|
||||
} as AutoplaySet;
|
||||
|
||||
if (data.nextButtonVideo) {
|
||||
result.next_button_video = new NavigationEndpoint(data.nextButtonVideo);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ import { default as ConfirmDialog } from './classes/ConfirmDialog.js';
|
||||
export { ConfirmDialog };
|
||||
import { default as ContinuationItem } from './classes/ContinuationItem.js';
|
||||
export { ContinuationItem };
|
||||
import { default as ConversationBar } from './classes/ConversationBar.js';
|
||||
export { ConversationBar };
|
||||
import { default as CopyLink } from './classes/CopyLink.js';
|
||||
export { CopyLink };
|
||||
import { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog.js';
|
||||
@@ -750,6 +752,7 @@ const map: Record<string, YTNodeConstructor> = {
|
||||
CompactVideo,
|
||||
ConfirmDialog,
|
||||
ContinuationItem,
|
||||
ConversationBar,
|
||||
CopyLink,
|
||||
CreatePlaylistDialog,
|
||||
DecoratedPlayerBar,
|
||||
|
||||
@@ -20,6 +20,7 @@ import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults.js';
|
||||
import VideoPrimaryInfo from '../classes/VideoPrimaryInfo.js';
|
||||
import VideoSecondaryInfo from '../classes/VideoSecondaryInfo.js';
|
||||
import LiveChatWrap from './LiveChat.js';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
|
||||
import type CardCollection from '../classes/CardCollection.js';
|
||||
import type Endscreen from '../classes/Endscreen.js';
|
||||
@@ -60,6 +61,7 @@ class VideoInfo {
|
||||
|
||||
primary_info?: VideoPrimaryInfo | null;
|
||||
secondary_info?: VideoSecondaryInfo | null;
|
||||
playlist?;
|
||||
game_info?;
|
||||
merchandise?: MerchandiseShelf | null;
|
||||
related_chip_cloud?: ChipCloud | null;
|
||||
@@ -67,6 +69,7 @@ class VideoInfo {
|
||||
player_overlays?: PlayerOverlay | null;
|
||||
comments_entry_point_header?: CommentsEntryPointHeader | null;
|
||||
livechat?: LiveChat | null;
|
||||
autoplay?;
|
||||
|
||||
/**
|
||||
* @param data - API response.
|
||||
@@ -141,6 +144,10 @@ class VideoInfo {
|
||||
this.merchandise = results.firstOfType(MerchandiseShelf);
|
||||
this.related_chip_cloud = secondary_results.firstOfType(RelatedChipCloud)?.content.item().as(ChipCloud);
|
||||
|
||||
if (two_col?.playlist) {
|
||||
this.playlist = two_col.playlist;
|
||||
}
|
||||
|
||||
this.watch_next_feed = secondary_results.firstOfType(ItemSection)?.contents || secondary_results;
|
||||
|
||||
if (this.watch_next_feed && Array.isArray(this.watch_next_feed) && this.watch_next_feed.at(-1)?.is(ContinuationItem))
|
||||
@@ -148,6 +155,10 @@ class VideoInfo {
|
||||
|
||||
this.player_overlays = next?.player_overlays?.item().as(PlayerOverlay);
|
||||
|
||||
if (two_col?.autoplay) {
|
||||
this.autoplay = two_col.autoplay;
|
||||
}
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
|
||||
if (segmented_like_dislike_button?.like_button?.is(ToggleButton) && segmented_like_dislike_button?.dislike_button?.is(ToggleButton)) {
|
||||
@@ -377,6 +388,13 @@ class VideoInfo {
|
||||
return !!this.#watch_next_continuation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the endpoint of the autoplay video
|
||||
*/
|
||||
get autoplay_video_endpoint(): NavigationEndpoint | null {
|
||||
return this.autoplay?.sets?.[0]?.autoplay_video || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get songs used in the video.
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,8 @@ export const CLIENTS = Object.freeze({
|
||||
NAME: 'WEB',
|
||||
VERSION: '2.20230104.01.00',
|
||||
API_KEY: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
|
||||
API_VERSION: 'v1'
|
||||
API_VERSION: 'v1',
|
||||
STATIC_VISITOR_ID: '6zpwvWUNAco'
|
||||
},
|
||||
WEB_KIDS: {
|
||||
NAME: 'WEB_KIDS',
|
||||
|
||||
Reference in New Issue
Block a user