# Sleep Meditation — Technical Documentation ## Scope This project provides a web app in a container that plays MP3 audio on desktop and mobile, including iPhone lock-screen controls. A separate internal container handles downloading and managing audio tracks. ## Architecture Two containers share a host volume containing all MP3 files and metadata. ### sleep-meditation (public) - Runtime: `nginx:1.27-alpine` + Python 3 (managed by supervisord) - Nginx: serves static files and proxies `/api/` to the local Python API on `localhost:8001` - Python API: read-only Flask app — lists downloaded tracks only (`GET /api/downloads`) - App: frontend (`index.html`, `styles.css`, `app.js`) - MP3 source: mounted host volume at `/usr/share/nginx/html/mp3` - Externally accessible; no write operations exposed ### sleep-meditation-downloader (internal) - Runtime: `python:3.12-alpine` + Flask - UI: management page at `http://:${SLEEP_MEDITATION_DOWNLOADER_PORT}/` - API: all write operations — download, delete, rename downloads and playlist tracks - MP3 volume mounted at `/mp3` - Accessible on `${SLEEP_MEDITATION_DOWNLOADER_PORT}` within the local network (not publicly exposed) - After downloading, use the refresh button in the sleep app to pick up new downloads ## File overview ``` containers/sleep-meditation/ Dockerfile nginx.conf supervisord.conf api.py ← read-only: GET /api/downloads only site/ index.html styles.css app.js manifest.webmanifest service-worker.js mp3/playlist.json containers/sleep-meditation-downloader/ Dockerfile api.py ← all write operations + UI file serving site/ index.html styles.css app.js stack/ stack.yml sleep-meditation.env ``` ## iPhone background playback - Autoplay without user interaction is blocked. - The user must start playback manually at least once. - After playback starts, audio usually continues when the screen is locked. - Media Session API enables lock-screen controls for play/pause/track skip. ## UI Single page — no routing. Contains: - Playlist selector and audio player with controls - Downloads list (read-only, populated via `GET /api/downloads`) - Refresh button (↺) in the header: reloads the full page; useful when the app is saved as a home screen bookmark on iPhone (no browser refresh available) ## Player controls - `Previous`: go to previous track (clamped at first track) - `Play/Pause`: toggle playback - `Next`: go to next track (clamped at last track) - `Back to start`: select first track, reset position to `0:00`, no autoplay When a playlist track ends: - If there is a next track, it starts automatically. - If the last track ends, playback stops (no loop). When a downloaded track ends: - Playback stops. No auto-advance (`singleMode` flag is set when playing a download). ## Track loading behavior The app loads meditation tracks in this order: 1. `/mp3/playlist.json` 2. Auto-discovery from `/mp3/` directory listing (Nginx `autoindex on`), excluding `/mp3/downloads/` 3. Empty list (no fallback track) Downloaded tracks are loaded separately via `GET /api/downloads`. ## Playlist contract File: `/mp3/playlist.json` ```json [ { "title": "Track name", "artist": "Artist", "src": "/mp3/track.mp3" } ] ``` Rules: - `title` optional (derived from filename if omitted) - `artist` optional - `src` required; absolute path (`/mp3/...`) or bare filename When a track title is renamed via the downloader UI, the API writes the updated title back to `playlist.json`. If `playlist.json` does not yet exist in the volume, it is created from the current directory listing. ## Downloaded tracks - Stored in `/mp3/downloads/` inside the mp3 volume. - Filenames are URL- and filesystem-safe (letters, digits, spaces, hyphens only). - Display titles (preserving special characters like `:` and `|`) are stored in `/mp3/downloads/titles.json`. - Renaming a downloaded track updates `titles.json` only; the filename is not changed. - Deleting a downloaded track removes the file and its entries from `titles.json` and `skip.json`. ## Skip intro - A per-track skip offset (seconds) can be set in the downloader UI. - Stored in `/mp3/downloads/skip.json` as `{ "filename.mp3": seconds }`. - `GET /api/downloads` returns a `skip` field (default `0`) for each track. - The sleep app seeks to `skip` seconds after `loadedmetadata` fires when playing a downloaded track. - Setting skip to `0` removes the entry from `skip.json`. ## API endpoints ### sleep-meditation (public, read-only) | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/downloads` | List downloaded tracks | ### sleep-meditation-downloader (internal only) | Method | Path | Description | |--------|------|-------------| | `GET` | `/` | Management UI | | `GET` | `/api/downloads` | List downloaded tracks | | `GET` | `/api/tracks` | List playlist tracks | | `POST` | `/api/download` | Start server-side download `{url, title}` | | `GET` | `/api/download/status/{track_id}` | Poll download status | | `DELETE` | `/api/downloads/{filename}` | Delete a downloaded track | | `POST` | `/api/downloads/{filename}/rename` | Rename display title `{title}` | | `POST` | `/api/downloads/{filename}/skip` | Set skip-intro offset `{seconds}` | | `POST` | `/api/tracks/rename` | Update playlist track title `{src, title}` | ## Container healthcheck - Endpoint: `GET /` - Expected: HTTP 200 - Configured in Dockerfile with `wget`