- 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>
160 lines
5.4 KiB
Markdown
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`
|