# FlightTube

A desktop app that merges all your YouTube subscriptions into one newest-first feed,
downloads the videos you pick, and — when you have no connection — shows only what you
already downloaded. Built for the flight.

Tauri 2 · Rust · React · Tailwind CSS 4 · SQLite · self-contained (bundles yt-dlp + ffmpeg)

Styled to `DESIGN-SYSTEM.md`: slate and sky, a 9–15px type ladder, outline-first
controls, borders for separation and shadows only for elevation, with light and dark
both designed rather than one derived from the other.

## How it works

**Subscriptions come from Google Takeout, not the API.** Export *YouTube subscriptions*
from [Takeout](https://takeout.google.com/) and import the `subscriptions.csv` in
Settings — which carries a seven-step walkthrough with the exact URLs. No OAuth, no API
key, no Google Cloud project, nothing secret on disk.

Importing **replaces** your subscription list: the CSV becomes the whole truth. Channels
no longer in it are removed along with their videos and downloaded files. A confirmation
dialog names exactly what will be deleted before anything happens.

**Video metadata comes from YouTube's public Atom feed** — one request per channel to
`youtube.com/feeds/videos.xml?channel_id=…`, fetched 8 at a time and merged into a single
feed sorted by publish date. Thumbnails are mirrored to local disk so the feed still
renders with no network.

**Undownloaded videos stream in-app.** YouTube's iframe embed refuses a Tauri window —
its origin is `tauri://localhost`, not an http(s) origin, which produces "Error 153" — so
`yt-dlp` resolves YouTube's HLS master playlist instead. Its variants are H.264 + AAC up
to 1080p, which AVFoundation plays natively in WKWebView with adaptive bitrate. One player
element serves both local files and streams.

**Downloads shell out to `yt-dlp`,** pinned to H.264 video + AAC audio in an MP4
container. That caps quality at 1080p — YouTube only serves H.264 that high, and anything
above it is VP9 or AV1, which the app's own player cannot reliably decode. The tradeoff is
deliberate: every download is guaranteed to play inside FlightTube.

## Requirements

None at runtime — `yt-dlp`, `ffmpeg` and `ffprobe` ship inside the app as Tauri sidecars,
so a fresh Mac needs nothing installed. Settings shows each tool's version and whether it
came from the bundle or the system; a copy on your machine takes precedence, which is how
you run a newer yt-dlp than the bundled one.

The binaries are not in git (~140 MB together). Fetch them once before building:

```bash
./scripts/fetch-binaries.sh
```

That pulls yt-dlp from its official GitHub release and static arm64 ffmpeg/ffprobe from
osxexperts.net — Homebrew's ffmpeg links a dozen dylibs and cannot be relocated into an
app bundle. Note that static ffmpeg builds are GPL, which matters if you redistribute.

## Running it

```bash
npm install && npm run tauri dev
```

Build and install a real app bundle:

```bash
npm run tauri build -- --bundles app && cp -R src-tauri/target/release/bundle/macos/FlightTube.app /Applications/
```

## Using it

1. **Settings** — follow the seven-step Takeout guide, then **Import subscriptions.csv**.
2. **Refresh** — pulls the latest videos from every channel.
3. **Download** on any video — progress shows live on the button; click again to cancel.
4. Click any video to play it **in the app** — a downloaded one from disk, anything else
   streamed. Nothing hands off to a browser.
5. **List or Tiles** — switch layouts in the top bar; the choice is remembered.
6. **Offline** — the app detects a lost connection and collapses the feed to your
   downloads. The connectivity pill also toggles a forced offline mode for testing.

Videos land in `~/Movies/FlightTube` (changeable in Settings). Appearance follows the
system by default; Settings offers System / Light / Dark.

## Limitations

- The Atom feed returns only the **~15 most recent videos per channel**. There is no
  backfill and no pagination — this is a rolling recent window, not an archive.
- Quality tops out at 1080p, by the deliberate choice described above.
- The bundled `yt-dlp` is frozen at build time and YouTube changes often. Re-run
  `./scripts/fetch-binaries.sh` and rebuild, or install a newer one on your system —
  the app prefers a system copy when it finds one.
- Downloading videos is contrary to YouTube's Terms of Service.

## Tests

```bash
cd src-tauri && cargo test
```

42 unit tests cover the three places malformed input actually bites: Takeout CSV parsing,
Atom feed parsing (against a captured real response), and `yt-dlp` progress-line parsing —
plus the database rules that a refresh must never clobber download state and that a
replacing import drops exactly the right channels.

Two network-dependent tests are excluded by default:

```bash
cargo test --test pipeline -- --ignored --nocapture      # full pipeline vs. live feeds
cargo test --test seed_real_db -- --ignored --nocapture  # populate the installed app's db
```

## Layout

```
src-tauri/src/
  takeout.rs     subscriptions.csv -> channels        (pure, tested)
  feed.rs        Atom XML -> videos, plus fetching    (parse is pure, tested)
  downloader.rs  yt-dlp arguments and progress lines  (pure, tested)
  db.rs          schema and every SQL statement
  thumbs.rs      local thumbnail cache
  net.rs         reachability probe
  commands.rs    Tauri command surface — delegates only
src/
  components/    Sidebar, TopBar, VideoRow, VideoTile, DownloadButton, Player, Settings
  components/ui.tsx        the design system's component vocabulary, in one place
  components/TakeoutGuide  the seven-step export walkthrough
  hooks/         useFeed, useDownloads, useConnectivity, useAppearance
```

Design notes and the implementation plan are in `docs/superpowers/`.
