From d89909a19a1486bee7e3275014725b4e3dc2cbe2 Mon Sep 17 00:00:00 2001 From: Luan Date: Mon, 26 Aug 2024 18:42:14 -0300 Subject: [PATCH] fix(examples): Use BgUtils to generate pot [skip ci] + Improve readme. --- examples/browser/README.md | 68 ++---- examples/browser/web/package-lock.json | 283 +++++++++++++++++++++++++ examples/browser/web/package.json | 3 +- examples/browser/web/src/main.ts | 128 +++++++---- examples/download/index.ts | 7 +- 5 files changed, 386 insertions(+), 103 deletions(-) create mode 100644 examples/browser/web/package-lock.json diff --git a/examples/browser/README.md b/examples/browser/README.md index 6cb111d0..4fe4d194 100644 --- a/examples/browser/README.md +++ b/examples/browser/README.md @@ -1,61 +1,19 @@ -# Browser Usage Example - -YouTube.js works in the browser! - -## Usage - +# 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`. -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. +## Example +**NOTE**: Build the library before running the examples. -```ts -import { Innertube } from "youtubei.js/web.bundle.min"; +Web application: -const yt = await Innertube.create({ - 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(), -}); +```shell +cd examples/browser/web +npm install +npm run dev ``` -After that, you can use the library as normal. - -## Example - -We've got a full example in `examples/browser/web` using vite. \ No newline at end of file +Proxy: + +```shell +deno run --allow-net --allow-read examples/browser/proxy/deno.ts +``` \ No newline at end of file diff --git a/examples/browser/web/package-lock.json b/examples/browser/web/package-lock.json new file mode 100644 index 00000000..4ad92f7b --- /dev/null +++ b/examples/browser/web/package-lock.json @@ -0,0 +1,283 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "bgutils-js": "^1.1.0", + "shaka-player": "^4.3.8" + }, + "devDependencies": { + "typescript": "^4.6.4", + "vite": "^3.0.0" + } + }, + "node_modules/bgutils-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bgutils-js/-/bgutils-js-1.1.0.tgz", + "integrity": "sha512-+v+MWO02VAfSKuuh9gpjxBTllFGkIiqzZT7ELwScOtm2UWk6MOm7lqkVtzctcjCrG0sgRZccfEbgaEWHozXLSQ==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ] + }, + "node_modules/eme-encryption-scheme-polyfill": { + "version": "2.1.1", + "license": "Apache-2.0" + }, + "node_modules/esbuild": { + "version": "0.15.18", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.18", + "@esbuild/linux-loong64": "0.15.18", + "esbuild-android-64": "0.15.18", + "esbuild-android-arm64": "0.15.18", + "esbuild-darwin-64": "0.15.18", + "esbuild-darwin-arm64": "0.15.18", + "esbuild-freebsd-64": "0.15.18", + "esbuild-freebsd-arm64": "0.15.18", + "esbuild-linux-32": "0.15.18", + "esbuild-linux-64": "0.15.18", + "esbuild-linux-arm": "0.15.18", + "esbuild-linux-arm64": "0.15.18", + "esbuild-linux-mips64le": "0.15.18", + "esbuild-linux-ppc64le": "0.15.18", + "esbuild-linux-riscv64": "0.15.18", + "esbuild-linux-s390x": "0.15.18", + "esbuild-netbsd-64": "0.15.18", + "esbuild-openbsd-64": "0.15.18", + "esbuild-sunos-64": "0.15.18", + "esbuild-windows-32": "0.15.18", + "esbuild-windows-64": "0.15.18", + "esbuild-windows-arm64": "0.15.18" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.33", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.79.1", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/shaka-player": { + "version": "4.7.7", + "license": "Apache-2.0", + "dependencies": { + "eme-encryption-scheme-polyfill": "^2.1.1" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vite": { + "version": "3.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.18", + "resolve": "^1.22.1", + "rollup": "^2.79.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/examples/browser/web/package.json b/examples/browser/web/package.json index 54593919..c8647373 100644 --- a/examples/browser/web/package.json +++ b/examples/browser/web/package.json @@ -13,6 +13,7 @@ "vite": "^3.0.0" }, "dependencies": { + "bgutils-js": "^1.1.0", "shaka-player": "^4.3.8" } -} \ No newline at end of file +} diff --git a/examples/browser/web/src/main.ts b/examples/browser/web/src/main.ts index 55a4662b..c01f7f5a 100644 --- a/examples/browser/web/src/main.ts +++ b/examples/browser/web/src/main.ts @@ -1,8 +1,8 @@ -import { Innertube, UniversalCache } from '../../../../bundle/browser'; +import { Innertube, Proto, UniversalCache, Utils } from '../../../../bundle/browser'; +import BG from 'bgutils-js'; // @ts-ignore - Shaka's TS support is not the best. import shaka from 'shaka-player/dist/shaka-player.ui.js'; - import "shaka-player/dist/controls.css"; const title = document.getElementById('title') as HTMLHeadingElement; @@ -11,51 +11,17 @@ const metadata = document.getElementById('metadata') as HTMLDivElement; const loader = document.getElementById('loader') as HTMLDivElement; const form = document.querySelector('form') as HTMLFormElement; + async function main() { + const visitorData = Proto.encodeVisitorData(Utils.generateRandomString(11), Math.floor(Date.now() / 1000)); + const poToken = await getPo(visitorData); + const yt = await Innertube.create({ + po_token: poToken, + visitor_data: visitorData, generate_session_locally: true, - fetch: async (input: RequestInfo | URL, init?: RequestInit) => { - 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])); - - if (input instanceof Request) { - // @ts-ignore - input.duplex = 'half'; - } - - // Copy over the request. - const request = new Request( - url, - input instanceof Request ? input : undefined, - ); - - headers.delete('user-agent'); - - return fetch(request, init ? { - ...init, - headers - } : { - headers - }); - }, - cache: new UniversalCache(false), + fetch: fetchFn, + cache: new UniversalCache(true), }); form.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' }); @@ -258,6 +224,80 @@ async function main() { }); } +async function getPo(identity: string): Promise { + const requestKey = 'O43z0dpjhgX20SCx4KAo'; + + const bgConfig = { + fetch: fetchFn, + globalObj: window, + requestKey, + identity + }; + + const challenge = await BG.Challenge.create(bgConfig); + + if (!challenge) + throw new Error('Could not get challenge'); + + if (challenge.script) { + const script = challenge.script.find((sc) => sc !== null); + if (script) + new Function(script)(); + } else { + console.warn('Unable to load VM.'); + } + + const poToken = await BG.PoToken.generate({ + program: challenge.challenge, + globalName: challenge.globalName, + bgConfig + }); + + return poToken; +} + +function fetchFn(input: RequestInfo | URL, init?: RequestInit) { + 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])); + + if (input instanceof Request) { + // @ts-expect-error - x + input.duplex = 'half'; + } + + // Copy over the request. + const request = new Request( + url, + input instanceof Request ? input : undefined + ); + + headers.delete('user-agent'); + + return fetch(request, init ? { + ...init, + headers + } : { + headers + }); +} + function showUI(args: { hidePlayer?: boolean } = { hidePlayer: true, }) { diff --git a/examples/download/index.ts b/examples/download/index.ts index 9b59a04a..86cac73c 100644 --- a/examples/download/index.ts +++ b/examples/download/index.ts @@ -14,13 +14,14 @@ import { existsSync, mkdirSync, createWriteStream } from 'fs'; if (!album.contents) throw new Error('Album appears to be empty'); - console.info(`Album "${album.header?.title.toString()}" by ${album.header?.author?.name}`, '\n'); + console.info(`Album "${album.header?.title.toString()}"`, '\n'); for (const song of album.contents) { const stream = await yt.download(song.id as string, { type: 'audio', // audio, video or video+audio quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on. - format: 'mp4' // media container format + format: 'mp4', // media container format, + client: 'YTMUSIC' }); console.info(`Downloading ${song.title} (${song.id})`); @@ -40,5 +41,5 @@ import { existsSync, mkdirSync, createWriteStream } from 'fs'; console.info(`${song.id} - Done!`, '\n'); } - console.info(`Downloaded ${album.header?.song_count}!`); + console.info(`Done!`); })(); \ No newline at end of file