Jonathan Sykes ebe60a7be2 fix: start next track at beginning (or A point) instead of resuming, and store A-B per playlist
When auto-advancing through a queue or playlist — or stepping with
next/prev — the next video previously jumped to its saved resume
timestamp. A freshly selected track now starts at the beginning, or at
the A marker when an A-B loop is set for it. Resuming still applies when
you reopen a single video directly.

A-B markers set while playing from a playlist are now stored on that
playlist's own copy of the video (entry.ab), so each playlist keeps its
own loop and the markers sync to the database alongside the playlist.
Non-playlist playback keeps using the global per-video marker map.

Bumps service worker to v1.0.3 to bust the client cache.
2026-07-01 04:29:00 +08: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%