Created CLAUDE.md

This commit is contained in:
2026-08-16 23:00:27 +02:00
parent bed1f4265a
commit d136ef26c5
+72
View File
@@ -0,0 +1,72 @@
# CLAUDE.md
Guidance for Claude Code when working in this repository.
## Project
**Trackpull** (repo dir still named `votify-docker`) — a self-hosted, multi-user Flask web app for downloading music from Spotify URLs. Dockerized, SQLite-backed, no frontend framework.
Full system documentation lives in [docs/](docs/). Start with [docs/onboarding.md](docs/onboarding.md).
## Architecture
```
templates/index.html (vanilla JS SPA, PWA)
│ fetch()
▼
app.py (Flask, served by Gunicorn: 1 worker / 4 threads)
├── db.py SQLite at /config/trackpull.db, thread-local connections
├── utils.py sanitize_filename, rename_from_metadata, cleanup_empty_dirs
└── monochrome/ Tidal/Qobuz download pipeline (pure Python, stdlib only)
├── __init__.py instance discovery, fetch/fetch_json, SSL ctx
├── api.py download_spotify_url() orchestrator
├── spotify_to_ids.py Spotify URL parse + embed scrape + fuzzy match
└── download.py stream URL, download, embed metadata, MP3 convert
```
### Three download backends
| Backend | Route | Runner in app.py | Mechanism |
|---------|-------|------------------|-----------|
| Votify | `POST /api/download` | `run_download()` | `subprocess.Popen` on the `votify` CLI (votify-fix), needs `cookies.txt` |
| Monochrome | `POST /api/monochrome/download` | `run_monochrome_download()` | in-process Python, proxies Tidal/Qobuz, no credentials |
| Unified (default) | `POST /api/unified/download` | `run_unified_download()` | Monochrome at `MP3_320`, then spawns a **separate** Votify job for `fail_info["failed_urls"]` |
Votify accepts a list of URLs; Monochrome and Unified take a **single** URL per job.
## Conventions & invariants
- **Everything mutating `jobs` must hold `jobs_lock`.** The in-memory `jobs: dict[str, dict]` is shared across Gunicorn threads. Read-modify-write patterns are already written this way — match them.
- **Job output is capped at 500 lines** (`[-500:]`) at every append site. Preserve the cap when touching logging.
- **Jobs are persisted only at terminal state** via `database.upsert_job()`. Live job state is in-memory and lost on restart. `GET /api/jobs` merges in-memory + DB.
- **`job["process"]`** (the Popen handle) is stripped before serialization by `job_to_dict()` and must never reach the DB or JSON.
- **Per-user isolation**: all files live under `/downloads/{user_id}/`. Every file route resolves paths through `_resolve_user_path()` / `_admin_resolve_path()`, which `.resolve()` the path and verify it is still inside the user dir. **Never bypass these helpers** when adding file routes.
- **Auth** is enforced by a single `@app.before_request` hook (`require_login`). Adding a public route means adding it to that whitelist. Admin routes call `require_admin()` and return 403.
- **Monochrome package uses only the standard library** (urllib, ssl, json). Keep it dependency-free — `requirements.txt` is intentionally tiny (flask, gunicorn, mutagen, werkzeug).
- Job IDs are `str(uuid.uuid4())[:8]`.
- Timestamps in the DB are **REAL unix floats**, not ISO strings.
## Key gotchas
- `db.init_db(DB_PATH)` is called explicitly from [app.py:41](app.py#L41) — importing `db` alone does not initialize the schema.
- `verify_password(user, password)` takes a **user dict**, not a username.
- `delete_jobs_older_than(cutoff)` takes a **unix timestamp**, not a number of days.
- The hourly `_purge_loop()` daemon deletes expired job rows **and** `rmtree`s any user download dir whose newest file is older than the cutoff. It is genuinely destructive to files — be careful changing `job_expiry_days` semantics.
- The Unified fallback writes into the *same* subfolder Monochrome created (`fail_info["subfolder"]`) and forces `output_format: mp3`.
- `/api/artwork` scrapes `entity.visualIdentity.image[]` from the Spotify embed page, then rewrites the CDN filename's first 16 hex chars to the `ab67616d000082c1` size key to upscale to 2000×2000. Spotify embed structure (`__NEXT_DATA__`) is fragile and can break without notice.
- Monochrome instances are third-party and go down regularly. `MP3_320` is **not** a valid Tidal quality param (404s) — it is only meaningful as a Qobuz mapping / post-conversion target.
## Development
```bash
cp .env.example .env # set ADMIN_USERNAME, ADMIN_PASSWORD, SECRET_KEY, PORT
docker compose up -d --build
```
There is no test suite and no linter config. Verify changes by rebuilding the container and exercising the UI.
Rebuilding is required for any Python or template change — the Dockerfile `COPY`s sources rather than mounting them. `/config` and `/downloads` persist on the host across rebuilds.
## Documentation policy
`docs/` is treated as the source of truth for humans and is expected to be kept current — see the maintenance note at the end of [docs/onboarding.md](docs/onboarding.md). **When you change a feature, update the matching document in the same change**: one document per system, update API endpoint and option tables when routes or parameters change, and record newly discovered third-party quirks as gotchas.