# 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**](https://github.com/vercel-labs/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](#run-on-windows-tauri)

```
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):

```powershell
# 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:

```powershell
pwsh -File scripts\release.ps1
```

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

```bash
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](#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](#run-on-windows-tauri).

```bash
# 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
