mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-13 17:42:18 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aefecd061e | ||
|
|
7485726f1e | ||
|
|
9e703abe3a | ||
|
|
affbe84284 | ||
|
|
fcbdae3e34 | ||
|
|
059c858021 |
12
README.md
12
README.md
@@ -42,7 +42,9 @@
|
||||
[][codefactor]
|
||||
[][npm]
|
||||
[][say-thanks]
|
||||
|
||||
<br>
|
||||
[][github-sponsors]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SPONSORS -->
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.3"
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "youtubei.js",
|
||||
"version": "2.2.1",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "2.2.1",
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,9 @@ export interface Context {
|
||||
user: {
|
||||
lockedSafetyMode: false;
|
||||
};
|
||||
thirdParty?: {
|
||||
embedUrl: string;
|
||||
};
|
||||
request: {
|
||||
useSsl: true;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -147,6 +147,7 @@ export default class HTTPClient {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user