diff --git a/.eslintrc.yml b/.eslintrc.yml
index 3c00c6d9..76a51765 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -1,17 +1,19 @@
plugins:
- [ jsdoc ]
+ [ '@typescript-eslint', 'eslint-plugin-tsdoc' ]
env:
commonjs: true
es2021: true
node: true
-extends: [ eslint:recommended, plugin:jsdoc/recommended ]
-globals:
- BROWSER: readonly
-settings:
- jsdoc:
- mode: 'typescript'
+extends: [ eslint:recommended, 'plugin:@typescript-eslint/recommended' ]
+parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: latest
+overrides:
+ -
+ files:
+ - '**/*.js'
+ rules:
+ 'tsdoc/syntax': 'off'
rules:
max-len:
- error
@@ -24,12 +26,10 @@ rules:
ignoreRegExpLiterals: true
quotes: [error, single]
-
- jsdoc/newline-after-description: 'off'
- jsdoc/require-returns-description: 'off'
- jsdoc/require-param-description: 'off'
- jsdoc/no-undefined-types: 'off'
- jsdoc/require-returns: 'off'
+
+ '@typescript-eslint/ban-types': 'off'
+ 'tsdoc/syntax': 'warn'
+ '@typescript-eslint/no-explicit-any': 'off'
no-template-curly-in-string: error
no-unreachable-loop: error
@@ -42,7 +42,7 @@ rules:
no-implied-eval: error
arrow-spacing: error
no-invalid-this: error
- no-lone-blocks: error
+ no-lone-blocks: 'off'
no-new-func: error
no-new-wrappers: error
no-new: error
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 0a6a0947..7040873a 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
- node-version: [ 12.x, 14.x, 16.x ]
+ node-version: [ 16.x, 18.x ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
diff --git a/.gitignore b/.gitignore
index dfdfb958..a8975723 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,3 +62,11 @@ pnpm-lock.yaml
# Temporary files for testing
tmp/
+
+# Build output
+dist/
+bundle/*.js.*
+bundle/*.js
+
+# MacOS
+.DS_Store
diff --git a/README_v2.0.0WIP.md b/README_v2.0.0WIP.md
index 1b0917f4..d809fe37 100644
--- a/README_v2.0.0WIP.md
+++ b/README_v2.0.0WIP.md
@@ -88,17 +88,19 @@ Innertube is an API used across all YouTube clients, it was created to simplify[
And huge thanks to [@gatecrasher777][gatecrasher] for his research on the workings of the Innertube API!
+If you have any questions or need help, feel free to contact us on our chat server [here](https://discord.gg/syDu7Yks54).
+
## Getting Started
### Prerequisites
-- [NodeJS][nodejs] v14 or greater
+YouTube.js runs on Node.js, Deno and in modern browsers.
-To verify things are set up
-properly, run this:
-```bash
-node --version
-```
+It requires a runtime with the following features:
+- [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
+ - On Node we use [undici]()'s fetch implementation which requires Node.js 16.8+. You may provide your own fetch implementation if you need to use an older version. See [providing your own fetch implementation](#custom-fetch) for more information.
+ - The `Response` object returned by fetch must thus be spec compliant and return a `ReadableStream` object if you want to use the `VideoInfo#download` method. (Implementations like `node-fetch` returns a non-standard `Readable` object.)
+- [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) and [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) required.
### Installation
- NPM:
@@ -114,15 +116,113 @@ yarn add youtubei.js@latest
npm install git+https://github.com/LuanRT/YouTube.js.git
```
+**TODO: Deno install instructions (esm.sh possibly?)**
+
## Usage
-
Create an Innertube instance (or session):
-```js
-// const Innertube = require('youtubei.js');
-import Innertube from 'youtubei.js';
-const youtube = await new Innertube({ gl: 'US' });
+```ts
+// const { Innertube } = require('youtubei.js');
+import { Innertube } from 'youtubei.js';
+const youtube = await Innertube.create();
```
+
+## Browser Usage
+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`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/proxy/deno.ts).
+
+You may provide your own fetch implementation to be used by YouTube.js. Which we will use here to modify and send the requests to through our proxy. See [`examples/browser/web`](https://github.com/LuanRT/YouTube.js/tree/main/examples/browser/web) for an simple example using [Vite](https://vitejs.dev/).
+
+```ts
+// Pre-bundled version for the web
+import { Innertube } from 'youtubei.js/bundle/browser';
+await Innertube.create({
+ fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
+ // Modify the request
+ // and send it to the proxy
+
+ // fetch the url
+ return fetch(request, init);
+ }
+});
+```
+
+### Streaming
+YouTube.js supports streaming of videos in the browser by converting YouTube's streaming data into a MPEG-DASH manifest.
+
+The example below uses [`dash.js`](https://github.com/Dash-Industry-Forum/dash.js) to play the video.
+
+```ts
+import { Innertube } from 'youtubei.js';
+import dashjs from 'dashjs';
+
+const youtube = await Innertube.create({ /* setup - see above */ });
+
+// get the video info
+const videoInfo = await youtube.getInfo('videoId');
+
+// now convert to a dash manifest
+// again - to be able to stream the video in the browser - we must proxy the requests through our own server
+// to do this, we provide a method to transform the urls before writing them to the manifest
+const manifest = videoInfo.toDash(url => {
+ // modify the url
+ // and return it
+ return url;
+});
+
+const uri = "data:application/dash+xml;charset=utf-8;base64," + btoa(manifest);
+
+const videoElement = document.getElementById('video_player');
+
+const player = dashjs.MediaPlayer().create();
+player.initialize(videoElement, uri, true);
+```
+
+Our browser example in [`examples/browser/web`]() provides a full working example.
+
+
+
+
+## Providing your own fetch implementation
+You may provide your own fetch implementation to be used by YouTube.js. This may be useful in some cases to modify the requests before they are sent and transform the responses before they are returned (eg. for proxies).
+
+```ts
+// provide a fetch implementation
+const yt = await Innertube.create({
+ fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
+ // make the request with your own fetch implementation
+ // and return the response
+ return new Response(
+ /* ... */
+ );
+ }
+});
+```
+
+
+
+## Caching
+To improve performance, you may wish to cache the transformed player instance which we use to decode the streaming urls.
+
+Our cache uses the `node:fs` module in Node-like environments, `Deno.writeFile` in Deno and `indexedDB` in browsers.
+
+```ts
+import { Innertube, UniversalCache } from 'youtubei.js';
+// By default, cache stores files in the OS temp directory (or indexedDB in browsers).
+const yt = await Innertube.create({
+ cache: new UniversalCache()
+});
+
+// You may wish to make the cache persistent (on Node and Deno)
+const yt = await Innertube.create({
+ cache: new UniversalCache(
+ // Enables persistent caching
+ true,
+ // Path to the cache directory, will create the directory if it doesn't exist
+ './.cache'
+ )
+});
+```
+
## API
## Innertube : `object`
diff --git a/browser.ts b/browser.ts
new file mode 100644
index 00000000..31d626d7
--- /dev/null
+++ b/browser.ts
@@ -0,0 +1,12 @@
+// Deno and browser runtimes
+
+// Polyfill buffer
+import { Buffer } from 'buffer';
+if (!Reflect.has(globalThis, 'Buffer')) {
+ Reflect.set(globalThis, 'Buffer', Buffer);
+}
+
+import Innertube from './lib/Innertube';
+export { default as Innertube } from './lib/Innertube.js';
+export * from './lib/utils';
+export default Innertube;
diff --git a/build/browser.d.ts b/build/browser.d.ts
deleted file mode 100644
index ae3e583d..00000000
--- a/build/browser.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import Innertube from "..";
-export default Innertube;
\ No newline at end of file
diff --git a/build/browser.js b/build/browser.js
deleted file mode 100644
index 898b0827..00000000
--- a/build/browser.js
+++ /dev/null
@@ -1,40 +0,0 @@
-"use strict";/* eslint-disable */
-var or=Object.defineProperty;var JC=Object.getOwnPropertyDescriptor;var ZC=Object.getOwnPropertyNames;var eL=Object.prototype.hasOwnProperty;var tL=(t,e,i)=>e in t?or(t,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[e]=i;var p=(t,e)=>or(t,"name",{value:e,configurable:!0});var Ut=(t,e)=>()=>(t&&(e=t(t=0)),e);var u=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),iL=(t,e)=>{for(var i in e)or(t,i,{get:e[i],enumerable:!0})},nL=(t,e,i,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of ZC(e))!eL.call(t,o)&&o!==i&&or(t,o,{get:()=>e[o],enumerable:!(n=JC(e,o))||n.enumerable});return t};var fv=t=>nL(or({},"__esModule",{value:!0}),t);var Qt=(t,e,i)=>(tL(t,typeof e!="symbol"?e+"":e,i),i),nd=(t,e,i)=>{if(!e.has(t))throw TypeError("Cannot "+i)};var M=(t,e,i)=>(nd(t,e,"read from private field"),i?i.call(t):e.get(t)),z=(t,e,i)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,i)},X=(t,e,i,n)=>(nd(t,e,"write to private field"),n?n.call(t,i):e.set(t,i),i);var oe=(t,e,i)=>(nd(t,e,"access private method"),i);function rr(){if(!Aa&&(Aa=typeof crypto<"u"&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||typeof msCrypto<"u"&&typeof msCrypto.getRandomValues=="function"&&msCrypto.getRandomValues.bind(msCrypto),!Aa))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return Aa(oL)}var Aa,oL,od=Ut(()=>{oL=new Uint8Array(16);p(rr,"rng")});var wv,uv=Ut(()=>{wv=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i});function rL(t){return typeof t=="string"&&wv.test(t)}var W0,ar=Ut(()=>{uv();p(rL,"validate");W0=rL});function aL(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,i=(At[t[e+0]]+At[t[e+1]]+At[t[e+2]]+At[t[e+3]]+"-"+At[t[e+4]]+At[t[e+5]]+"-"+At[t[e+6]]+At[t[e+7]]+"-"+At[t[e+8]]+At[t[e+9]]+"-"+At[t[e+10]]+At[t[e+11]]+At[t[e+12]]+At[t[e+13]]+At[t[e+14]]+At[t[e+15]]).toLowerCase();if(!W0(i))throw TypeError("Stringified UUID is invalid");return i}var At,Ia,k0,pr=Ut(()=>{ar();At=[];for(Ia=0;Ia<256;++Ia)At.push((Ia+256).toString(16).substr(1));p(aL,"stringify");k0=aL});function pL(t,e,i){var n=e&&i||0,o=e||new Array(16);t=t||{};var r=t.node||Wv,a=t.clockseq!==void 0?t.clockseq:rd;if(r==null||a==null){var c=t.random||(t.rng||rr)();r==null&&(r=Wv=[c[0]|1,c[1],c[2],c[3],c[4],c[5]]),a==null&&(a=rd=(c[6]<<8|c[7])&16383)}var l=t.msecs!==void 0?t.msecs:Date.now(),s=t.nsecs!==void 0?t.nsecs:pd+1,g=l-ad+(s-pd)/1e4;if(g<0&&t.clockseq===void 0&&(a=a+1&16383),(g<0||l>ad)&&t.nsecs===void 0&&(s=0),s>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");ad=l,pd=s,rd=a,l+=122192928e5;var h=((l&268435455)*1e4+s)%4294967296;o[n++]=h>>>24&255,o[n++]=h>>>16&255,o[n++]=h>>>8&255,o[n++]=h&255;var d=l/4294967296*1e4&268435455;o[n++]=d>>>8&255,o[n++]=d&255,o[n++]=d>>>24&15|16,o[n++]=d>>>16&255,o[n++]=a>>>8|128,o[n++]=a&255;for(var f=0;f<6;++f)o[n+f]=r[f];return e||k0(o)}var Wv,rd,ad,pd,kv,mv=Ut(()=>{od();pr();ad=0,pd=0;p(pL,"v1");kv=pL});function cL(t){if(!W0(t))throw TypeError("Invalid UUID");var e,i=new Uint8Array(16);return i[0]=(e=parseInt(t.slice(0,8),16))>>>24,i[1]=e>>>16&255,i[2]=e>>>8&255,i[3]=e&255,i[4]=(e=parseInt(t.slice(9,13),16))>>>8,i[5]=e&255,i[6]=(e=parseInt(t.slice(14,18),16))>>>8,i[7]=e&255,i[8]=(e=parseInt(t.slice(19,23),16))>>>8,i[9]=e&255,i[10]=(e=parseInt(t.slice(24,36),16))/1099511627776&255,i[11]=e/4294967296&255,i[12]=e>>>24&255,i[13]=e>>>16&255,i[14]=e>>>8&255,i[15]=e&255,i}var Ka,cd=Ut(()=>{ar();p(cL,"parse");Ka=cL});function lL(t){t=unescape(encodeURIComponent(t));for(var e=[],i=0;i{pr();cd();p(lL,"stringToBytes");sL="6ba7b810-9dad-11d1-80b4-00c04fd430c8",gL="6ba7b811-9dad-11d1-80b4-00c04fd430c8";p(cr,"default")});function hL(t){if(typeof t=="string"){var e=unescape(encodeURIComponent(t));t=new Uint8Array(e.length);for(var i=0;i>5]>>>o%32&255,a=parseInt(n.charAt(r>>>4&15)+n.charAt(r&15),16);e.push(a)}return e}function Mv(t){return(t+64>>>9<<4)+14+1}function vL(t,e){t[e>>5]|=128<>5]|=(t[n/8]&255)<>16)+(e>>16)+(i>>16);return n<<16|i&65535}function wL(t,e){return t<>>32-e}function Sa(t,e,i,n,o,r){return m0(wL(m0(m0(e,t),m0(n,r)),o),i)}function _t(t,e,i,n,o,r,a){return Sa(e&i|~e&n,t,e,o,r,a)}function zt(t,e,i,n,o,r,a){return Sa(e&n|i&~n,t,e,o,r,a)}function Et(t,e,i,n,o,r,a){return Sa(e^i^n,t,e,o,r,a)}function Ot(t,e,i,n,o,r,a){return Sa(i^(e|~n),t,e,o,r,a)}var yv,Hv=Ut(()=>{p(hL,"md5");p(dL,"md5ToHexEncodedArray");p(Mv,"getOutputLength");p(vL,"wordsToMd5");p(fL,"bytesToWords");p(m0,"safeAdd");p(wL,"bitRotateLeft");p(Sa,"md5cmn");p(_t,"md5ff");p(zt,"md5gg");p(Et,"md5hh");p(Ot,"md5ii");yv=hL});var uL,Tv,Nv=Ut(()=>{ld();Hv();uL=cr("v3",48,yv),Tv=uL});function WL(t,e,i){t=t||{};var n=t.random||(t.rng||rr)();if(n[6]=n[6]&15|64,n[8]=n[8]&63|128,e){i=i||0;for(var o=0;o<16;++o)e[i+o]=n[o];return e}return k0(n)}var Cv,Lv=Ut(()=>{od();pr();p(WL,"v4");Cv=WL});function kL(t,e,i,n){switch(t){case 0:return e&i^~e&n;case 1:return e^i^n;case 2:return e&i^e&n^i&n;case 3:return e^i^n}}function sd(t,e){return t<>>32-e}function mL(t){var e=[1518500249,1859775393,2400959708,3395469782],i=[1732584193,4023233417,2562383102,271733878,3285377520];if(typeof t=="string"){var n=unescape(encodeURIComponent(t));t=[];for(var o=0;o>>0;N=C,C=T,T=sd(y,30)>>>0,y=w,w=O}i[0]=i[0]+w>>>0,i[1]=i[1]+y>>>0,i[2]=i[2]+T>>>0,i[3]=i[3]+C>>>0,i[4]=i[4]+N>>>0}return[i[0]>>24&255,i[0]>>16&255,i[0]>>8&255,i[0]&255,i[1]>>24&255,i[1]>>16&255,i[1]>>8&255,i[1]&255,i[2]>>24&255,i[2]>>16&255,i[2]>>8&255,i[2]&255,i[3]>>24&255,i[3]>>16&255,i[3]>>8&255,i[3]&255,i[4]>>24&255,i[4]>>16&255,i[4]>>8&255,i[4]&255]}var Av,Iv=Ut(()=>{p(kL,"f");p(sd,"ROTL");p(mL,"sha1");Av=mL});var ML,Kv,Sv=Ut(()=>{ld();Iv();ML=cr("v5",80,Av),Kv=ML});var Gv,bv=Ut(()=>{Gv="00000000-0000-0000-0000-000000000000"});function yL(t){if(!W0(t))throw TypeError("Invalid UUID");return parseInt(t.substr(14,1),16)}var xv,_v=Ut(()=>{ar();p(yL,"version");xv=yL});var gd={};iL(gd,{NIL:()=>Gv,parse:()=>Ka,stringify:()=>k0,v1:()=>kv,v3:()=>Tv,v4:()=>Cv,v5:()=>Kv,validate:()=>W0,version:()=>xv});var hd=Ut(()=>{mv();Nv();Lv();Sv();bv();_v();ar();pr();cd()});var gi=u((tU,zv)=>{"use strict";zv.exports={URLS:{YT_BASE:"https://www.youtube.com",YT_MUSIC_BASE:"https://music.youtube.com",YT_SUGGESTIONS:"https://suggestqueries.google.com/complete/",API:{BASE:"https://youtubei.googleapis.com",PRODUCTION:"https://youtubei.googleapis.com/youtubei/",STAGING:"https://green-youtubei.sandbox.googleapis.com/youtubei/",RELEASE:"https://release-youtubei.sandbox.googleapis.com/youtubei/",TEST:"https://test-youtubei.sandbox.googleapis.com/youtubei/",CAMI:"http://cami-youtubei.sandbox.googleapis.com/youtubei/",UYTFE:"https://uytfe.sandbox.google.com/youtubei/"}},OAUTH:{SCOPE:"http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content",GRANT_TYPE:"http://oauth.net/grant_type/device/1.0",MODEL_NAME:"ytlr::",HEADERS:{accept:"*/*",origin:"https://www.youtube.com","user-agent":"Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version","content-type":"application/json",referer:"https://www.youtube.com/tv","accept-language":"en-US"},REGEX:{AUTH_SCRIPT:/
+