- JavaScript 77.6%
- Python 12.1%
- HTML 10.3%
|
All checks were successful
Deploy Pages / deploy (push) Successful in 5s
The previous PUBLIC_HOST pointed at the Pages domain which doesn't serve the Git endpoint, producing 404s on clone/push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| server | ||
| .gitignore | ||
| index.html | ||
| LICENSE.md | ||
| main.js | ||
| network_patch.py | ||
| README.md | ||
| ui-plugin.js | ||
| worker.js | ||
yt-dlp.wasm
A browser-based YouTube (and more) downloader and player, powered by yt-dlp running in WebAssembly via Pyodide. All heavy lifting — video, audio and subtitle byte transfer — happens directly between the visitor's browser and the upstream CDN (googlevideo for YouTube). Only the tiny metadata-extraction step uses a CORS proxy.
Why a proxy at all?
YouTube's watch HTML, youtubei API and signature JavaScript do not return
Access-Control-Allow-Origin headers, so browser code cannot fetch them
cross-origin. yt-dlp inside Pyodide hits those endpoints during the
extraction step. The proxy is a thin pass-through that forwards just those
metadata calls and rewrites headers. It is not in the path for actual
video/audio downloads or playback — that's all direct fetch from the
visitor's browser to googlevideo.com, which does serve CORS headers.
Concretely:
- Metadata extraction (~10–50 KB / video): visitor's browser → proxy → youtube.com
- HLS playback, all variants, all segments: visitor's browser ↔ googlevideo.com directly
- Subtitle (.vtt) fetch: visitor's browser ↔ origin directly
- Single-file and DASH (video-only + audio-only) downloads: visitor's browser ↔ googlevideo.com directly. DASH muxing into a single .mp4 happens locally via ffmpeg.wasm.
Features
- Plays and downloads videos from YouTube and many other sites supported by yt-dlp.
- Quality selection: progressive, DASH (1080p+, includes 4K), audio-only.
- DASH downloads are muxed to MP4 client-side with ffmpeg.wasm (no re-encode).
- Subtitle extraction and track selection.
- NoMercy video player with a custom controls plugin.
- yt-dlp wheel is cached in IndexedDB after first load.
Project Structure
index.html # Web UI + proxy configuration
main.js # UI, player integration, direct CDN fetch, DASH mux orchestration
worker.js # Pyodide worker: extract() + mux() RPCs
ui-plugin.js # Custom video player controls plugin
network_patch.py # urllib monkey-patch routing through the metadata proxy
.forgejo/workflows/deploy.yaml # Forgejo Pages deploy workflow
server/
proxy.js # Minimal Node.js CORS proxy (metadata only)
package.json
How It Works
- The user enters a video URL.
main.jsasks the worker toextract(url).- The worker (Pyodide + yt-dlp) calls youtube.com through the proxy and returns the full info dict (formats, subtitles, etc.).
main.jsbuilds an HLS master playlist whose variant manifests point at the raw googlevideo URLs. The NoMercy player loads playback directly from googlevideo via the visitor's connection.- For downloads,
main.jsfetches the format URL(s) directly withfetch()and (for DASH) ships the bytes to the worker forffmpeg.wasmmuxing.
Prerequisites
- Node.js 18+ (for the metadata proxy)
- A modern browser
- Internet (yt-dlp wheel is cached after first run)
Setup & Usage
1. Install proxy dependencies
cd server
npm install
2. Start the metadata proxy
npm start
Listens on http://localhost:8181 by default.
3. Serve the static files
Open index.html via a local web server (recommended) or directly. Module
imports and Workers need the proper origin, so a static server such as
python -m http.server works well.
4. Use it
- Paste a video URL.
- Click Load video. Metadata extraction starts.
- Pick a quality and click Save to download — bytes flow straight from the CDN to your browser, no server round-trip.
Configuring the proxy URL
The proxy URL is read from window.YT_DLP_PROXY in index.html:
<script>
// Empty string falls back to http://localhost:8181.
// Set to your own public proxy if you deploy the UI to a static host.
window.YT_DLP_PROXY = "";
</script>
If you publish the UI somewhere public (Forgejo Pages, GitHub Pages, …),
deploy server/proxy.js somewhere reachable (a small VPS, Fly.io, etc.) and
point window.YT_DLP_PROXY at it. The proxy only sees small metadata
requests; it is never in the path of actual video bytes.
Deploying to Forgejo Pages
The .forgejo/workflows/deploy.yaml workflow pushes a static copy of the UI
to the pages branch on every push to main. It clones via the public
hostname (configurable via PUBLIC_HOST in the workflow file) because the
internal Docker name forgejo is not resolvable inside the workflow's
container network.
Security & Privacy
- Video bytes go directly between the visitor's browser and the upstream CDN.
- The proxy only relays metadata calls and never sees video content; it doesn't log or persist anything.
Troubleshooting
ModuleNotFoundError: No module named 'ssl'— older builds. The current worker explicitly loads thesslPyodide package on boot.- Network error on extraction — the proxy isn't running, isn't reachable,
or
window.YT_DLP_PROXYpoints at the wrong URL. - CORS error on playback — the format URL has expired (yt-dlp's URLs are short-lived signed URLs). Re-extract.
License
MIT. See LICENSE for details.