sleep-meditation/docs/TECHNICAL.md
Ivo Oskamp 0d9f20690f Release v0.1.6
- Switch to shared build-and-push.sh; version read from docs/changelog.md
- Add docs/changelog.md; remove version.txt, .last-branch, .gitignore
- Stack: image tag via SLEEP_MEDITATION_IMAGE_TAG
- Downloader: YouTube support (yt-dlp + ffmpeg), best audio to mp3
- Downloader: Content-Type validation for direct URLs
- Downloader: auto-fetch YouTube title; title field optional
- Downloader: progress bar with phase (downloading/converting)
- Downloader: store source URL per file; show Source link in manage list

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:13:10 +02:00

160 lines
5.4 KiB
Markdown

# 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://<host>:${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`