Claude Worker d9371fdea4 feat: fix portrait mode design in pwa — YouTube-style bottom bar
Task #46 completed by ClaudeQueue

ClaudeQueue
2026-06-30 17:02:25 +00:00

YT Player

A lightweight, ad-free YouTube player with on-device playlists. No login, no tracking, no ads — it extracts direct video/audio streams with yt-dlp and plays them in a native window.

Built on zero-native (Zig + the system WebView) instead of Electron, so the binary is tiny — no bundled Chromium or Node runtime.

Features

  • 🔍 Search YouTube without an account
  • 🚫 No ads — plays the raw stream, never the YouTube player
  • 📺 High quality via a dual-stream engine: a muted video track synced with a separate audio track (1080p+) with no ffmpeg muxing required; progressive formats are used as a fallback
  • 🎵 Audio-only mode — great for music, saves bandwidth
  • 📂 On-device playlists — create, rename, delete, add/remove videos. Stored locally as JSON; nothing leaves your machine
  • 💾 Offline cache / preload — a Save button and an Add to playlist button on the now-playing video download a self-contained copy into a permanent file cache, so it plays instantly and works offline. Videos added to a playlist are auto-preloaded and kept until you remove them.
  • Settings page — playback defaults (quality, volume, audio-only) plus cache management: see storage used, toggle auto-preload, and delete cached videos individually or all at once
  • 🕘 Watch history
  • ⏯ Full controls: seek, volume, playback speed, quality switching, fullscreen, next/prev, and keyboard shortcuts (space, ←/→, f, m)

Architecture

The same frontend drives two native shells:

  • Linux / macOS → zero-native (Zig + system WebView)
  • Windows → Tauri (Rust + WebView2) — see Run on Windows
frontend/            Static web UI (no framework, no build step) — shared
  index.html
  styles.css
  app.js             Player engine + UI; auto-detects Tauri vs zero-native
src/                 zero-native (Linux/macOS) shell
  main.zig           App definition + bridge handler registration
  bridge.zig         Native handlers: spawn yt-dlp, slim its JSON, local store
src-tauri/           Tauri (Windows) shell
  src/main.rs        Same handlers as Rust commands
  tauri.conf.json    Window, CSP, bundle (icons, yt-dlp resource)
  Cargo.toml
scripts/
  setup-ytdlp.js     Downloads the standalone yt-dlp binary into ./bin
  make-icon.js       Generates appicon.png for `tauri icon`
bin/                 yt-dlp lands here (gitignored)
app.zon              zero-native manifest

The web UI talks to the native side over whichever bridge is present (window.__TAURI__.core.invoke on Windows, window.zero.invoke elsewhere):

window.zero.invoke(...) Native handler Returns
yt.search { query } ytSearch { ok, results:[…] }
yt.streams { videoId } ytStreams { ok, data:{ meta, audioUrl, qualities[] } }
store.load {} storeLoad playlists / history / settings
store.save { data } storeSave { ok }
cache.download { videoId } cache_download downloads a single-file copy into the offline cache
cache.status { videoId } cache_status { ok, cached, path?, size? }
cache.list {} cache_list { ok, items:[{id,size,path}], total }
cache.delete { videoId } cache_delete removes one cached file
cache.clear {} cache_clear removes all cached files

The offline cache is implemented in the Tauri (Windows) shell. Files live in <app_cache_dir>/videos/<videoId>.<ext> and persist until deleted from the Settings page. The frontend falls back to live streaming if a cached file is missing, and the cache calls degrade gracefully on shells that don't implement them.

Because the bridge is size-limited, the Zig handlers parse yt-dlp's large JSON and return only the compact fields the UI needs.

Prerequisites

  • Zig (0.14.x recommended) — https://ziglang.org/download/
  • zero-native CLI: npm install -g zero-native
  • Node.js (only to run the yt-dlp downloader script)
  • No system yt-dlp needed — the setup script bundles it. (On Linux/macOS the bundled build is self-contained; Python is not required.)

Run on Windows (Tauri)

zero-native does not target Windows yet, so the Windows build uses a Tauri shell (Rust + the WebView2 runtime that ships with Windows 10/11). The result is a small standalone .exe/installer — no bundled Chromium or Node.

Build natively on the Windows machine (cross-compiling a WebView2 app from WSL/Linux is unreliable, so do this on Windows):

# Prerequisites (one time):
#   • Rust          https://rustup.rs   (MSVC toolchain)
#   • Microsoft C++ Build Tools (Desktop development with C++)
#   • WebView2 runtime — preinstalled on Win11; on Win10 grab the Evergreen runtime
#   • Node.js (to run the helper scripts + Tauri CLI)

npm install                       # installs the Tauri CLI (@tauri-apps/cli)
npm run setup                     # downloads yt-dlp.exe into .\bin
npm run make-icon                 # writes appicon.png
npm run tauri icon .\appicon.png  # expands it into src-tauri\icons\*

npm run tauri:dev                 # hot dev window
npm run tauri:build               # produces the installer (see below)

The installer lands in src-tauri\target\release\bundle\ (nsis\*-setup.exe and msi\*.msi). yt-dlp.exe is bundled as an app resource, so the installed app is self-contained.

One-shot release

scripts\release.ps1 does the whole flow — build, copy installers into .\releases, then commit and push through WSL git:

pwsh -File scripts\release.ps1

To only commit + push from WSL (e.g. after building separately):

bash scripts/push.sh "your commit message"

Already in WSL and just want it running fast? Your WSL is WSLg-enabled, so you can instead build the Linux (zero-native) target and its window appears on your Windows desktop — see Setup & run. That needs WSL running each time; the Tauri build above is a true standalone Windows app.

Setup & run

Linux / macOS (zero-native). For Windows see Run on Windows.

# 1. Download the yt-dlp binary into ./bin
npm run setup            # or: node scripts/setup-ytdlp.js

# 2. Generate the zero-native build files for your installed version
#    (build.zig, build.zig.zon, src/runner.zig). Run this in a scratch dir and
#    copy the generated build.zig / build.zig.zon next to this project, OR run
#    init here and keep your files:
zero-native init ytplayer --frontend none

# 3. Merge: keep THIS repo's frontend/, src/main.zig, src/bridge.zig and app.zon.
#    (src/main.zig shows exactly how the handlers are registered — fold that into
#    the generated App if the scaffold differs.)

# 4. Build & launch the native window
zig build run

To refresh yt-dlp later (YouTube changes often): npm run update-ytdlp.

Notes & caveats

  • zero-native is pre-1.0. The bridge handler signature used here follows its documented contract (fn(context, invocation, output) anyerror![]const u8). If your installed version exposes the Invocation type or BridgeDispatcher fields slightly differently, only the wiring in src/main.zig and the payloadField helper need adjusting — the handler logic is self-contained.
  • Stream URLs from yt-dlp are IP-locked and expire after a few hours; the app re-fetches them each time you play a video, so this is transparent.
  • This is for personal use. Respect YouTube's Terms of Service and the rights of content creators.

License

MIT

Description
No description provided
Readme 1,018 MiB
Languages
JavaScript 67.3%
CSS 17.7%
Zig 4.9%
Rust 4.9%
HTML 3.5%
Other 1.7%