# Project Status

Last updated: 2026-08-20

A YouTube downloader + local media player. Two parts in this repo:

- **`server/`** — Express + yt-dlp backend. Fetches video/playlist info, downloads
  video/audio in the background, serves finished files.
- **`my-player/`** — Expo Router (React Native) app. Paste a URL, pick a format,
  download it to the device, then play/tag/organize it locally.

## The vision

1. Paste a YouTube URL → see available video/audio formats.
2. Pick one → it downloads to the server first, then to the device, and **stays
   on the device permanently** (no re-downloading, no server dependency after
   that point).
3. Play video and audio locally.
4. Tag media with free-form labels ("happy", "romantic", "workout") and filter
   by tag to build an on-the-fly mood-based queue.
5. Build named playlists out of downloaded media.

## What works today

### Server (`server/`)
- `POST /api/info` — probes a URL. Single video → full format list (resolution
  ladder + mp3 bitrate ladder, with size estimates). Playlist → lightweight
  video list (no per-video probing, stays fast on long playlists).
- `POST /api/download` — kicks off a background yt-dlp job, returns a `jobId`
  immediately (202).
- `GET /api/status/:jobId` — poll until `status: "completed"`, then
  `result.downloadUrl` is a direct link under `/downloads/<file>`.
- In-memory job store; finished jobs + their files auto-expire after
  `JOB_TTL_MS` (1h default).
- See [server/README.md](../server/README.md) for the full API reference.

### App (`my-player/`)
- **Downloads tab** — paste a URL, fetch formats, pick one, download. Shows a
  format picker (video resolutions + mp3 bitrates) with real file-size
  estimates from the server. Downloaded items list with tag/playlist/delete
  actions per row.
- **Player tab** — plays the selected item (video via `expo-video`, audio via
  `expo-audio`), with a persistent bottom playback bar. Tag filter chips
  (derived from whatever tags exist — not a fixed list) narrow the track list.
- **Playlists tab** — create/delete named playlists; tap into one to see its
  tracks and play them (routes to the Player tab).
- **Tags** — genuinely free-form. Type any word into the tag editor and it
  becomes a real, filterable tag immediately, shared across the whole app.
  This was the most important piece per the original ask, and it's built.
- **Settings** — "Clear Local Download Cache" wipes all downloaded files, tags,
  and playlists (with a confirmation prompt). Background-playback toggle is
  present but not yet wired to real behavior (see Known gaps).
- Local persistence via `AsyncStorage` (media items + playlists); actual media
  files live in the app's document directory via `expo-file-system`'s newer
  `File`/`Paths` API.

## Bugs found and fixed this session

These were pre-existing issues in the code before today, found while wiring up
tags/playlists and testing on the iOS Simulator:

- **Double `/api` prefix on status polling** — the client prepended
  `API_BASE_URL` (already ending in `/api`) onto `statusUrl` (which the server
  already returns as `/api/status/<id>`), producing `/api/api/status/...` →
  404 → an HTML error page → a JSON-parse crash on every poll tick. This meant
  **no download could ever complete on the client**, regardless of the tag
  work. Fixed in [MediaContext.tsx](../my-player/src/context/MediaContext.tsx).
- **Client double-prepended the download URL** too (`downloadUrl` from the
  server is already absolute).
- **Video playback was silently routed through the audio player** — every
  `playMedia()` call created an `expo-audio` player even for video items,
  wasting a player instance and leaving the video's own play/pause state
  disconnected from the bottom bar. Fixed: audio items get an audio player,
  video items are left to the `VideoView`/`useVideoPlayer` instance in the
  Player screen, with the bottom bar's toggle now correctly routed to
  whichever one is active.
- **Stale-closure bug in the download-completion handler** — concurrent
  downloads could overwrite each other in `AsyncStorage` because the state
  update captured `downloads` from closure instead of using a functional
  update.
- **URL input had no `autoCapitalize="none"`/`autoCorrect={false}`** — iOS
  would mangle pasted YouTube URLs (e.g. auto-capitalizing `https` to `HTTPS`).
- **Several Expo SDK 57 API mismatches** left over from whenever this was
  scaffolded: `expo-file-system`'s old `FileSystem.documentDirectory` /
  `downloadAsync` / `deleteAsync` API was replaced with the current
  `File`/`Paths` classes; `expo-audio`'s `AudioMode.staysActiveInBackground`
  is now `shouldPlayInBackground`, and its status field is `playing` not
  `isPlaying`; `expo-video`'s `VideoView` no longer takes an `allowsFullscreen`
  prop (fullscreen is available by default).
- **Dead scaffolding code removed**: `use-theme.ts` and its only dependents
  (`use-color-scheme.ts`/`.web.ts`) were unused leftovers from the Expo
  template, referencing a `constants/theme` file that was never created.
- **Playlist detail screen rendered under the status bar/notch** — it's a
  plain `Stack` screen outside the tab navigator, so it doesn't get automatic
  safe-area insets the way tab screens do. Fixed with `SafeAreaView`.

## A real Expo/RN gotcha worth knowing about

The tag-editor and add-to-playlist sheets are built on `@expo/ui`'s
`BottomSheet` (a genuine native sheet — SwiftUI on iOS), not React Native's
built-in `<Modal>`. Two non-obvious things had to be worked around, both
documented inline in the two component files:

1. Plain RN views passed as `BottomSheet` children don't inherit the sheet's
   width from SwiftUI — they collapse to their content's intrinsic size unless
   explicitly given one (`useWindowDimensions()` based width in both sheets).
2. RN's `TouchableOpacity`/`Pressable` touch targets **inside** a `BottomSheet`
   don't reliably receive touches unless the whole subtree is wrapped in
   `@expo/ui`'s `RNHostView` — this is the officially-provided bridge for
   hosting an interactive RN view tree inside a SwiftUI container, and without
   it, taps on buttons/list rows silently do nothing (typing into a
   `TextInput` still worked without it, since that's a native UIKit
   responder, which is what made this so confusing to track down).

If a future sheet/modal is added anywhere, follow the pattern already in
[TagEditorModal.tsx](../my-player/src/components/TagEditorModal.tsx) or
[PlaylistPickerModal.tsx](../my-player/src/components/PlaylistPickerModal.tsx).
Also avoid nesting `FlatList`/`ScrollView` inside a `BottomSheet` — their own
gesture handling conflicts with the sheet's native drag-to-dismiss gesture; a
plain mapped `View` (as used for both the playlist list and tag suggestion
chips) is the safe choice for the size of lists these sheets show.

## Known gaps / next steps

Roughly in order of what's likely to matter next:

- **Background-playback toggle in Settings is cosmetic** — flip it and
  nothing happens; `shouldPlayInBackground` is hardcoded `true` at app start.
  Trivial to wire up (call `setAudioModeAsync` again on toggle) if it matters.
- **No download progress indicator** — the app polls silently and only
  surfaces success/failure via a one-shot alert; there's no visible progress
  bar during the download.
- **No playlist reordering** — tracks are added in the order you tap them;
  no drag-to-reorder yet.
- **No "play all" / queue-through-playlist** — tapping a track plays just
  that track; there's no auto-advance to the next track in the
  playlist/filtered list.
- **`API_BASE_URL` is hardcoded to `http://localhost:4000`** in
  `MediaContext.tsx` — fine for the iOS Simulator (shares the host's
  network), but will need to become configurable (env var / settings screen)
  before running on a physical device or Android emulator (which needs
  `10.0.2.2` instead of `localhost`).
- **No playlist rename**, only create/delete.
- Server: in-memory job store means a server restart loses in-flight job
  history (documented as a known tradeoff in `server/README.md`, not something
  this session touched).

## Running it locally

```bash
# Terminal 1 — backend
cd server
npm install
cp .env.example .env
npm run dev

# Terminal 2 — app
cd my-player
npx expo start
```

Open in Expo Go (iOS Simulator or a physical device on the same network as
the server). `npx tsc --noEmit` and `npx expo lint` are both clean as of this
writeup.
