Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,16 @@ tracks that *are* present, which streamrip writes into every file. An album is:
**Missing disc**.
- **Unknown** — completeness cannot be determined, because no present track has
readable tags to reveal the expected total.

### Source
A single music service (Qobuz, Tidal, Deezer, SoundCloud, ...) and everything
that is specific to it: how to build a share URL from an id and read an id back
out of one (**url** / parse, both directions), how to fetch its **album art**,
and how to read **metadata** from one of its URLs. A Source owns its
credentials/keys too (Qobuz, for instance, reads its app_id and auth token from
the **Streamrip database**'s config). Each Source reaches the network only
through the injectable `http_get` seam, mirroring the `run_rip` and
`read_audio_tags` seams, so an adapter can be exercised with canned JSON and no
real network. Sources are migrated behind this single interface one at a time;
a not-yet-migrated source keeps its quirks inline (e.g. in `construct_url` and
`extract_metadata_from_url`) until its slice lands.
23 changes: 19 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ RUN groupadd -g 1000 appuser && \
# Set working directory
WORKDIR /app

# Install Python dependencies
# Install Python dependencies. streamrip is installed from the upstream `dev`
# branch (not the PyPI release, which lags behind): PyPI's latest is 2.1.0
# while dev/host run 2.2.0+, and the config schema version is tied to the
# streamrip version — a mismatch makes host and container fight over the shared
# self-mounted config.toml (each rewrites it to its own schema on every run).
# Tracking dev keeps the container aligned with the host build.
# Note: `@dev` is a moving target, so rebuilds are not reproducible; Docker
# layer caching means this only re-pulls when this layer is invalidated
# (e.g. `--no-cache` or an earlier-layer change).
RUN pip install --no-cache-dir \
flask \
flask-cors \
streamrip \
"git+https://github.com/nathom/streamrip.git@dev" \
gunicorn \
gevent

Expand All @@ -38,5 +46,12 @@ USER 1000:1000
# Expose port
EXPOSE 5000

# Run with aggressive worker recycling
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--worker-class", "gevent", "--workers", "2", "--timeout", "60", "app:app"]
# Single worker only: Active/History state, the SSE subscriber list, and the
# download worker threads all live in-process (ADR-0002). A second gunicorn
# worker is a separate process with its own copy of all three, so the browser's
# SSE stream and the request that starts a download can land on different
# workers — the download runs but never shows up in Active/History. The gevent
# worker class serves many concurrent SSE clients in this one process via
# greenlets, and download concurrency is handled internally by
# MAX_CONCURRENT_DOWNLOADS worker threads, so one worker loses nothing.
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--worker-class", "gevent", "--workers", "1", "--timeout", "60", "app:app"]
Loading