mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-23 23:09:28 +00:00
Compare commits
2 Commits
v4.0.1-den
...
v4.1.1-den
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9014ac0395 | ||
|
|
31a7de6437 |
@@ -130,6 +130,7 @@ const youtube = await Innertube.create(/* options */);
|
||||
| `lang` | `string` | Language. | `en` |
|
||||
| `location` | `string` | Geolocation. | `US` |
|
||||
| `account_index` | `number` | The account index to use. This is useful if you have multiple accounts logged in. **NOTE:** Only works if you are signed in with cookies. | `0` |
|
||||
| `visitor_data` | `string` | Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in. A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint. | `undefined` |
|
||||
| `retrieve_player` | `boolean` | Specifies whether to retrieve the JS player. Disabling this will make session creation faster. **NOTE:** Deciphering formats is not possible without the JS player. | `true` |
|
||||
| `enable_safety_mode` | `boolean` | Specifies whether to enable safety mode. This will prevent the session from loading any potentially unsafe content. | `false` |
|
||||
| `generate_session_locally` | `boolean` | Specifies whether to generate the session data locally or retrieve it from YouTube. This can be useful if you need more performance. | `false` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.1.1",
|
||||
"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",
|
||||
|
||||
@@ -3,12 +3,12 @@ import EventEmitterLike from '../utils/EventEmitterLike.ts';
|
||||
import Actions from './Actions.ts';
|
||||
import Player from './Player.ts';
|
||||
|
||||
import HTTPClient from '../utils/HTTPClient.ts';
|
||||
import { Platform, DeviceCategory, getRandomUserAgent, InnertubeError, SessionError } from '../utils/Utils.ts';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.ts';
|
||||
import Proto from '../proto/index.ts';
|
||||
import { ICache } from '../types/Cache.ts';
|
||||
import { FetchFunction } from '../types/PlatformShim.ts';
|
||||
import HTTPClient from '../utils/HTTPClient.ts';
|
||||
import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.ts';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.ts';
|
||||
|
||||
export enum ClientType {
|
||||
WEB = 'WEB',
|
||||
@@ -118,6 +118,11 @@ export interface SessionOptions {
|
||||
* YouTube cookies.
|
||||
*/
|
||||
cookie?: string;
|
||||
/**
|
||||
* Setting this to a valid and persistent visitor data string will allow YouTube to give this session tailored content even when not logged in.
|
||||
* A good way to get a valid one is by either grabbing it from a browser or calling InnerTube's `/visitor_id` endpoint.
|
||||
*/
|
||||
visitor_data?: string;
|
||||
/**
|
||||
* Fetch function to use.
|
||||
*/
|
||||
@@ -179,6 +184,7 @@ export default class Session extends EventEmitterLike {
|
||||
options.lang,
|
||||
options.location,
|
||||
options.account_index,
|
||||
options.visitor_data,
|
||||
options.enable_safety_mode,
|
||||
options.generate_session_locally,
|
||||
options.device_category,
|
||||
@@ -198,6 +204,7 @@ export default class Session extends EventEmitterLike {
|
||||
lang = '',
|
||||
location = '',
|
||||
account_index = 0,
|
||||
visitor_data = '',
|
||||
enable_safety_mode = false,
|
||||
generate_session_locally = false,
|
||||
device_category: DeviceCategory = 'desktop',
|
||||
@@ -208,9 +215,9 @@ export default class Session extends EventEmitterLike {
|
||||
let session_data: SessionData;
|
||||
|
||||
if (generate_session_locally) {
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode });
|
||||
session_data = this.#generateSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data });
|
||||
} else {
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode }, fetch);
|
||||
session_data = await this.#retrieveSessionData({ lang, location, time_zone: tz, device_category, client_name, enable_safety_mode, visitor_data }, fetch);
|
||||
}
|
||||
|
||||
return { ...session_data, account_index };
|
||||
@@ -223,16 +230,24 @@ export default class Session extends EventEmitterLike {
|
||||
device_category: string;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}, fetch: FetchFunction = Platform.shim.fetch): Promise<SessionData> {
|
||||
const url = new URL('/sw.js_data', Constants.URLS.YT_BASE);
|
||||
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'accept-language': options.lang || 'en-US',
|
||||
'user-agent': getRandomUserAgent('desktop'),
|
||||
'accept': '*/*',
|
||||
'referer': 'https://www.youtube.com/sw.js',
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${Constants.CLIENTS.WEB.STATIC_VISITOR_ID};`
|
||||
'cookie': `PREF=tz=${options.time_zone.replace('/', '.')};VISITOR_INFO1_LIVE=${visitor_id};`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -292,10 +307,15 @@ export default class Session extends EventEmitterLike {
|
||||
time_zone: string;
|
||||
device_category: DeviceCategory;
|
||||
client_name: string;
|
||||
enable_safety_mode: boolean
|
||||
enable_safety_mode: boolean;
|
||||
visitor_data: string;
|
||||
}): SessionData {
|
||||
const id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
let visitor_id = Constants.CLIENTS.WEB.STATIC_VISITOR_ID;
|
||||
|
||||
if (options.visitor_data) {
|
||||
const decoded_visitor_data = Proto.decodeVisitorData(options.visitor_data);
|
||||
visitor_id = decoded_visitor_data.id;
|
||||
}
|
||||
|
||||
const context: Context = {
|
||||
client: {
|
||||
@@ -305,7 +325,7 @@ export default class Session extends EventEmitterLike {
|
||||
screenHeightPoints: 1080,
|
||||
screenPixelDensity: 1,
|
||||
screenWidthPoints: 1920,
|
||||
visitorData: Proto.encodeVisitorData(id, timestamp),
|
||||
visitorData: Proto.encodeVisitorData(visitor_id, Math.floor(Date.now() / 1000)),
|
||||
userAgent: getRandomUserAgent('desktop'),
|
||||
clientName: options.client_name,
|
||||
clientVersion: CLIENTS.WEB.VERSION,
|
||||
|
||||
@@ -32,7 +32,7 @@ The parser is responsible for sanitizing and standardizing InnerTube responses w
|
||||
* [`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
|
||||
* [`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/parser.ts) - The core of the parser.
|
||||
* [`nodes.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/nodes.ts) - Contains a list of all the InnerTube nodes, which is used to determine the appropriate node for a given renderer. It's important to note that this file is automatically generated and should not be edited manually.
|
||||
* [`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes for. Also automatically generated.
|
||||
* [`misc.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/misc.ts) - Miscellaneous classes. Also automatically generated.
|
||||
|
||||
### Clients
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Text from './misc/Text.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { RawNode } from '../index.ts';
|
||||
|
||||
class PlayerCaptionsTracklist extends YTNode {
|
||||
static type = 'PlayerCaptionsTracklist';
|
||||
|
||||
caption_tracks: {
|
||||
caption_tracks?: {
|
||||
base_url: string;
|
||||
name: Text;
|
||||
vss_id: string;
|
||||
@@ -13,7 +14,7 @@ class PlayerCaptionsTracklist extends YTNode {
|
||||
is_translatable: boolean;
|
||||
}[];
|
||||
|
||||
audio_tracks: {
|
||||
audio_tracks?: {
|
||||
audio_track_id: string;
|
||||
captions_initial_state: string;
|
||||
default_caption_track_index: number;
|
||||
@@ -22,39 +23,48 @@ class PlayerCaptionsTracklist extends YTNode {
|
||||
caption_track_indices: number;
|
||||
}[];
|
||||
|
||||
default_audio_track_index: number;
|
||||
default_audio_track_index?: number;
|
||||
|
||||
translation_languages: {
|
||||
translation_languages?: {
|
||||
language_code: string;
|
||||
language_name: Text;
|
||||
}[];
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.caption_tracks = data.captionTracks.map((ct: any) => ({
|
||||
base_url: ct.baseUrl,
|
||||
name: new Text(ct.name),
|
||||
vss_id: ct.vssId,
|
||||
language_code: ct.languageCode,
|
||||
kind: ct.kind,
|
||||
is_translatable: ct.isTranslatable
|
||||
}));
|
||||
|
||||
this.audio_tracks = data.audioTracks.map((at: any) => ({
|
||||
audio_track_id: at.audioTrackId,
|
||||
captions_initial_state: at.captionsInitialState,
|
||||
default_caption_track_index: at.defaultCaptionTrackIndex,
|
||||
has_default_track: at.hasDefaultTrack,
|
||||
visibility: at.visibility,
|
||||
caption_track_indices: at.captionTrackIndices
|
||||
}));
|
||||
if (Reflect.has(data, 'captionTracks')) {
|
||||
this.caption_tracks = data.captionTracks.map((ct: any) => ({
|
||||
base_url: ct.baseUrl,
|
||||
name: new Text(ct.name),
|
||||
vss_id: ct.vssId,
|
||||
language_code: ct.languageCode,
|
||||
kind: ct.kind,
|
||||
is_translatable: ct.isTranslatable
|
||||
}));
|
||||
}
|
||||
|
||||
this.default_audio_track_index = data.defaultAudioTrackIndex;
|
||||
if (Reflect.has(data, 'audioTracks')) {
|
||||
this.audio_tracks = data.audioTracks.map((at: any) => ({
|
||||
audio_track_id: at.audioTrackId,
|
||||
captions_initial_state: at.captionsInitialState,
|
||||
default_caption_track_index: at.defaultCaptionTrackIndex,
|
||||
has_default_track: at.hasDefaultTrack,
|
||||
visibility: at.visibility,
|
||||
caption_track_indices: at.captionTrackIndices
|
||||
}));
|
||||
}
|
||||
|
||||
this.translation_languages = data.translationLanguages.map((tl: any) => ({
|
||||
language_code: tl.languageCode,
|
||||
language_name: new Text(tl.languageName)
|
||||
}));
|
||||
if (Reflect.has(data, 'defaultAudioTrackIndex')) {
|
||||
this.default_audio_track_index = data.defaultAudioTrackIndex;
|
||||
}
|
||||
|
||||
if (Reflect.has(data, 'translationLanguages')) {
|
||||
this.translation_languages = data.translationLanguages.map((tl: any) => ({
|
||||
language_code: tl.languageCode,
|
||||
language_name: new Text(tl.languageName)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
|
||||
class ShowingResultsFor extends YTNode {
|
||||
export default class ShowingResultsFor extends YTNode {
|
||||
static type = 'ShowingResultsFor';
|
||||
|
||||
corrected_query: Text;
|
||||
endpoint: NavigationEndpoint;
|
||||
original_query: Text;
|
||||
corrected_query_endpoint: NavigationEndpoint;
|
||||
original_query_endpoint: NavigationEndpoint;
|
||||
search_instead_for: Text;
|
||||
showing_results_for: Text;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.corrected_query = new Text(data.correctedQuery);
|
||||
this.endpoint = new NavigationEndpoint(data.correctedQueryEndpoint);
|
||||
this.original_query = new Text(data.originalQuery);
|
||||
this.corrected_query_endpoint = new NavigationEndpoint(data.correctedQueryEndpoint);
|
||||
this.original_query_endpoint = new NavigationEndpoint(data.originalQueryEndpoint);
|
||||
this.search_instead_for = new Text(data.searchInsteadFor);
|
||||
this.showing_results_for = new Text(data.showingResultsFor);
|
||||
}
|
||||
}
|
||||
|
||||
export default ShowingResultsFor;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Thumbnail from './misc/Thumbnail.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import MetadataBadge from './MetadataBadge.ts';
|
||||
import ExpandableMetadata from './ExpandableMetadata.ts';
|
||||
import ThumbnailOverlayTimeStatus from './ThumbnailOverlayTimeStatus.ts';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
@@ -98,7 +99,7 @@ class Video extends YTNode {
|
||||
return this.badges.some((badge) => {
|
||||
if (badge.style === 'BADGE_STYLE_TYPE_LIVE_NOW' || badge.label === 'LIVE')
|
||||
return true;
|
||||
});
|
||||
}) || this.thumbnail_overlays.firstOfType(ThumbnailOverlayTimeStatus)?.style === 'LIVE';
|
||||
}
|
||||
|
||||
get is_upcoming(): boolean | undefined {
|
||||
|
||||
@@ -28,8 +28,8 @@ class Library extends Feed<IBrowseResponse> {
|
||||
if (!this.page.contents_memo)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const stats = this.page.contents_memo.getType(ProfileColumnStats)?.[0];
|
||||
const user_info = this.page.contents_memo.getType(ProfileColumnUserInfo)?.[0];
|
||||
const stats = this.page.contents_memo.getType(ProfileColumnStats).first();
|
||||
const user_info = this.page.contents_memo.getType(ProfileColumnUserInfo).first();
|
||||
|
||||
this.profile = { stats, user_info };
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class Search extends Feed<ISearchResponse> {
|
||||
if (!contents)
|
||||
throw new InnertubeError('No contents found in search response');
|
||||
|
||||
this.results = contents.firstOfType(ItemSection)?.contents;
|
||||
this.results = contents.filterType(ItemSection).find((section) => section.contents && section.contents.length > 0)?.contents;
|
||||
|
||||
this.refinements = this.page.refinements || [];
|
||||
this.estimated_results = this.page.estimated_results;
|
||||
|
||||
@@ -13,7 +13,7 @@ class TimeWatched {
|
||||
contents?: ObservedArray<ItemSection>;
|
||||
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
@@ -26,6 +26,7 @@ const is_cjs = !meta_url;
|
||||
const __dirname__ = is_cjs ? __dirname : path.dirname(fileURLToPath(meta_url));
|
||||
|
||||
const package_json = JSON.parse(readFileSync(path.resolve(__dirname__, is_cjs ? '../package.json' : '../../package.json'), 'utf-8'));
|
||||
const repo_url = package_json.homepage?.split('#')[0];
|
||||
|
||||
class Cache implements ICache {
|
||||
#persistent_directory: string;
|
||||
@@ -102,8 +103,8 @@ Platform.load({
|
||||
runtime: 'node',
|
||||
info: {
|
||||
version: package_json.version,
|
||||
bugs_url: package_json.bugs.url,
|
||||
repo_url: package_json.homepage.split('#')[0]
|
||||
bugs_url: package_json.bugs?.url || `${repo_url}/issues`,
|
||||
repo_url
|
||||
},
|
||||
server: true,
|
||||
Cache: Cache,
|
||||
@@ -130,4 +131,4 @@ Platform.load({
|
||||
|
||||
export * from './lib.ts';
|
||||
import Innertube from './lib.ts';
|
||||
export default Innertube;
|
||||
export default Innertube;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CLIENTS } from '../utils/Constants.ts';
|
||||
import { u8ToBase64 } from '../utils/Utils.ts';
|
||||
import { base64ToU8, u8ToBase64 } from '../utils/Utils.ts';
|
||||
import { VideoMetadata } from '../core/Studio.ts';
|
||||
|
||||
import * as VisitorData from './generated/messages/youtube/VisitorData.ts';
|
||||
@@ -21,6 +21,11 @@ class Proto {
|
||||
return encodeURIComponent(u8ToBase64(buf).replace(/\+/g, '-').replace(/\//g, '_'));
|
||||
}
|
||||
|
||||
static decodeVisitorData(visitor_data: string): VisitorData.Type {
|
||||
const data = VisitorData.decodeBinary(base64ToU8(decodeURIComponent(visitor_data)));
|
||||
return data;
|
||||
}
|
||||
|
||||
static encodeChannelAnalyticsParams(channel_id: string): string {
|
||||
const buf = ChannelAnalytics.encodeBinary({
|
||||
params: {
|
||||
|
||||
@@ -428,8 +428,15 @@ class FormatUtils {
|
||||
const url = new URL(format.decipher(player));
|
||||
url.searchParams.set('cpn', cpn || '');
|
||||
|
||||
let id;
|
||||
if (format.audio_track) {
|
||||
id = `${format.itag?.toString()}-${format.audio_track.id}`;
|
||||
} else {
|
||||
id = format.itag?.toString();
|
||||
}
|
||||
|
||||
const representation = this.#el(document, 'Representation', {
|
||||
id: format.itag?.toString(),
|
||||
id,
|
||||
codecs,
|
||||
bandwidth: format.bitrate?.toString(),
|
||||
audioSamplingRate: format.audio_sample_rate?.toString()
|
||||
|
||||
@@ -169,6 +169,7 @@ export default class HTTPClient {
|
||||
ctx.client.androidSdkVersion = Constants.CLIENTS.ANDROID.SDK_VERSION;
|
||||
break;
|
||||
case 'TV_EMBEDDED':
|
||||
ctx.client.clientName = Constants.CLIENTS.TV_EMBEDDED.NAME;
|
||||
ctx.client.clientVersion = Constants.CLIENTS.TV_EMBEDDED.VERSION;
|
||||
ctx.client.clientScreen = 'EMBED';
|
||||
ctx.thirdParty = { embedUrl: Constants.URLS.YT_BASE };
|
||||
|
||||
@@ -82,7 +82,7 @@ export function getRandomUserAgent(type: DeviceCategory): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an authentication token from a cookies' sid..js
|
||||
* Generates an authentication token from a cookies' sid.
|
||||
* @param sid - Sid extracted from cookies
|
||||
*/
|
||||
export async function generateSidAuth(sid: string): Promise<string> {
|
||||
@@ -116,7 +116,7 @@ export function generateRandomString(length: number): string {
|
||||
* @returns seconds
|
||||
*/
|
||||
export function timeToSeconds(time: string): number {
|
||||
const params = time.split(':').map((param) => parseInt(param));
|
||||
const params = time.split(':').map((param) => parseInt(param.replace(/\D/g, '')));
|
||||
switch (params.length) {
|
||||
case 1:
|
||||
return params[0];
|
||||
@@ -217,4 +217,8 @@ export const debugFetch: FetchFunction = (input, init) => {
|
||||
|
||||
export function u8ToBase64(u8: Uint8Array): string {
|
||||
return btoa(String.fromCharCode.apply(null, Array.from(u8)));
|
||||
}
|
||||
|
||||
export function base64ToU8(base64: string): Uint8Array {
|
||||
return new Uint8Array(atob(base64).split('').map((char) => char.charCodeAt(0)));
|
||||
}
|
||||
Reference in New Issue
Block a user