Compare commits

...

9 Commits

Author SHA1 Message Date
github-actions[bot]
ade5feb31c chore(main): release 4.1.0 (#362)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-24 01:34:29 -03:00
LuanRT
13ebf0a039 feat(Session): allow setting a custom visitor data token (#371)
* feat(Session): allow setting a custom visitor data token

* docs: update init options

* chore: lint
2023-03-24 01:30:24 -03:00
Araxeus
cb8fafe94b fix(http): android tv http client missing clientName (#370) 2023-03-22 19:45:37 -03:00
absidue
bd35faa597 fix(parser): Make Video.is_live work on channel pages (#368) 2023-03-22 18:02:21 -03:00
absidue
a8b507ee65 fix(toDash): Generate unique Representation ids (#366) 2023-03-22 17:48:09 -03:00
Araxeus
e7eacd9742 fix(node) Electron apps crashing (#367)
Inside a `app.asar` file, the package.json might get trimmed and the `bugs_url` might be missing

`repo_url` conditional check was added for good measure

* fix(node) resolve `bugs_url` from repo_url
2023-03-22 17:13:40 -03:00
absidue
1c72a41675 fix(Utils): Properly parse timestamps with thousands separators (#363) 2023-03-22 03:48:01 -03:00
LuanRT
62a68b207c chore(docs): fix typo 2023-03-17 18:19:10 -03:00
LuanRT
1d9587e8c1 feat(ShowingResultsFor): parse all props 2023-03-17 07:27:00 -03:00
14 changed files with 94 additions and 35 deletions

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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