mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 09:32:12 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ade5feb31c | ||
|
|
13ebf0a039 | ||
|
|
cb8fafe94b | ||
|
|
bd35faa597 | ||
|
|
a8b507ee65 | ||
|
|
e7eacd9742 | ||
|
|
1c72a41675 | ||
|
|
62a68b207c | ||
|
|
1d9587e8c1 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [4.1.0](https://github.com/LuanRT/YouTube.js/compare/v4.0.1...v4.1.0) (2023-03-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Session:** allow setting a custom visitor data token ([#371](https://github.com/LuanRT/YouTube.js/issues/371)) ([13ebf0a](https://github.com/LuanRT/YouTube.js/commit/13ebf0a03973e7ba7b65e9f72c4333927e4254f6))
|
||||
* **ShowingResultsFor:** parse all props ([1d9587e](https://github.com/LuanRT/YouTube.js/commit/1d9587e8c1ee0b11bb0e444c3d1e98162e9e1059))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **http:** android tv http client missing `clientName` ([#370](https://github.com/LuanRT/YouTube.js/issues/370)) ([cb8fafe](https://github.com/LuanRT/YouTube.js/commit/cb8fafe94b8ab330ae58211a892923321d65d890))
|
||||
* **node:** Electron apps crashing ([#367](https://github.com/LuanRT/YouTube.js/issues/367)) ([e7eacd9](https://github.com/LuanRT/YouTube.js/commit/e7eacd974211c90e7fbddfbf8019388cda3dfa5a))
|
||||
* **parser:** Make Video.is_live work on channel pages ([#368](https://github.com/LuanRT/YouTube.js/issues/368)) ([bd35faa](https://github.com/LuanRT/YouTube.js/commit/bd35faa5978f0b822e98d019523be1303374ddc0))
|
||||
* **toDash:** Generate unique Representation ids ([#366](https://github.com/LuanRT/YouTube.js/issues/366)) ([a8b507e](https://github.com/LuanRT/YouTube.js/commit/a8b507ee65ccd8edc9aea2aef8a908fa272bb23c))
|
||||
* **Utils:** Properly parse timestamps with thousands separators ([#363](https://github.com/LuanRT/YouTube.js/issues/363)) ([1c72a41](https://github.com/LuanRT/YouTube.js/commit/1c72a41675e47f711bd61ebb898bca5527406a79))
|
||||
|
||||
## [4.0.1](https://github.com/LuanRT/YouTube.js/compare/v4.0.0...v4.0.1) (2023-03-16)
|
||||
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.1.0",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/LuanRT"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.1.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",
|
||||
|
||||
@@ -3,12 +3,12 @@ import EventEmitterLike from '../utils/EventEmitterLike.js';
|
||||
import Actions from './Actions.js';
|
||||
import Player from './Player.js';
|
||||
|
||||
import HTTPClient from '../utils/HTTPClient.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';
|
||||
import { FetchFunction } from '../types/PlatformShim.js';
|
||||
import HTTPClient from '../utils/HTTPClient.js';
|
||||
import { DeviceCategory, getRandomUserAgent, InnertubeError, Platform, SessionError } from '../utils/Utils.js';
|
||||
import OAuth, { Credentials, OAuthAuthErrorEventHandler, OAuthAuthEventHandler, OAuthAuthPendingEventHandler } from './OAuth.js';
|
||||
|
||||
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,20 +1,25 @@
|
||||
import { YTNode } from '../helpers.js';
|
||||
import type { RawNode } from '../index.js';
|
||||
import Text from './misc/Text.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
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.js';
|
||||
import NavigationEndpoint from './NavigationEndpoint.js';
|
||||
import MetadataBadge from './MetadataBadge.js';
|
||||
import ExpandableMetadata from './ExpandableMetadata.js';
|
||||
import ThumbnailOverlayTimeStatus from './ThumbnailOverlayTimeStatus.js';
|
||||
|
||||
import { timeToSeconds } from '../../utils/Utils.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.js';
|
||||
import Innertube from './lib.js';
|
||||
export default Innertube;
|
||||
export default Innertube;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CLIENTS } from '../utils/Constants.js';
|
||||
import { u8ToBase64 } from '../utils/Utils.js';
|
||||
import { base64ToU8, u8ToBase64 } from '../utils/Utils.js';
|
||||
import { VideoMetadata } from '../core/Studio.js';
|
||||
|
||||
import * as VisitorData from './generated/messages/youtube/VisitorData.js';
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -233,7 +233,7 @@ describe('YouTube.js Tests', () => {
|
||||
it('should retrieve the "Related" tab', async () => {
|
||||
const info = await yt.music.getInfo(VIDEOS[1].ID);
|
||||
const related = await info.getRelated();
|
||||
expect((related as any).length).toBeGreaterThan(3);
|
||||
expect((related as any).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should retrieve albums', async () => {
|
||||
@@ -278,9 +278,6 @@ describe('YouTube.js Tests', () => {
|
||||
});
|
||||
|
||||
async function download(id: string, yt: Innertube): Promise<boolean> {
|
||||
// TODO: add back info
|
||||
// let got_video_info = false;
|
||||
|
||||
const stream = await yt.download(id, { type: 'video+audio' });
|
||||
const file = fs.createWriteStream(`./${id}.mp4`);
|
||||
|
||||
@@ -288,5 +285,5 @@ async function download(id: string, yt: Innertube): Promise<boolean> {
|
||||
file.write(chunk);
|
||||
}
|
||||
|
||||
return fs.existsSync(`./${id}.mp4`); // && got_video_info;
|
||||
return fs.existsSync(`./${id}.mp4`);
|
||||
}
|
||||
Reference in New Issue
Block a user