Releases: arthursoares/libsync
Release list
v0.0.7 — Reliability fixes and non-root Docker
Reliability release for downloads, library reconciliation, authentication, and the web UI, with a non-root Docker runtime. Includes all changes since v0.0.6.
Upgrade — Docker permissions required
The container now runs as UID/GID 1000:1000. Existing bind mounts and previously created named volumes must be writable by that user before upgrading. Stop the app and back up its data directory (including streamrip.db, downloads.db, and downloads-tidal.db) first. Existing music files are not moved or renamed.
For the published-image Compose example, run these commands from the directory containing your Compose file. Substitute your actual host mount paths for ./data and ./music:
docker compose -f docker-compose.example.yml stop
sudo chown -R 1000:1000 ./data ./music
docker compose -f docker-compose.example.yml pull
docker compose -f docker-compose.example.yml up -dEquivalent filesystem permissions or ACLs are also sufficient; only change ownership on the directories dedicated to this deployment. Custom download paths must also be writable by UID 1000.
If /data uses an existing named volume, migrate that volume's ownership separately while the app is stopped. Replace YOUR_DATA_VOLUME with the actual name shown by docker volume ls (Compose may prefix it with the project name):
docker run --rm --user 0 --entrypoint chown \
-v YOUR_DATA_VOLUME:/data \
ghcr.io/arthursoares/libsync:0.0.7 -R 1000:1000 /dataNew empty named volumes inherit the correct ownership automatically. After startup, verify the container becomes healthy and that downloads can write to the music mount. The stable Docker tags are ghcr.io/arthursoares/libsync:0.0.7 and :latest.
Manual mark/unmark and scan reconciliation now require a connected source and a complete online track catalog. Unset or explicitly empty naming formats use the canonical Settings defaults; stored custom formats are preserved. Existing database names, environment variables, and sentinel filenames remain compatible. The internal Python package version (3.0.0) and frontend package version (0.0.1) remain independent of Libsync's release tags.
Added
- Sync Download Selected. The New in Library selection now queues the chosen albums, and disconnected sources and sync failures have explicit states. (#63)
- Container health check. Docker probes
/api/healthevery 30 seconds using Python's standard library; the loopback probe ignores outbound proxy settings. (#62)
Changed
- Non-root Docker runtime. The app runs as
libsync(UID/GID 1000), with writable default data and music directories. See the required upgrade steps above. (#62)
Fixed
- Tidal HiRes busy retry. A busy response preserves the authorization handle and pasted redirect URL, allowing an explicit retry without restarting login. (#93)
- Scan polling lifecycle. Scan status requests run sequentially and stop on close, navigation, or error. Missing jobs offer a fresh scan; connection failures offer an explicit status retry without repeated polling errors. (#88)
- Completion detail refresh. Queue UUID completion events now resolve the album's source and catalog ID before refreshing matching open details; late progress cannot restore completed queue items. (#87)
- Cancellation feedback. Individual and bulk cancellation now reload the canonical queue immediately, updating active counts without WebSocket traffic and ignoring late progress for cancelled items. (#86)
- Search pagination. Load More appends the next page without triggering a page-one reload; changing services still reruns the active query from page one. (#84)
- Sync selection effect loop. Initial selection notifications no longer track parent state; deselection stays intact and replacement sync results reseed once. (#83)
- Source-scoped selections and details. Switching services clears Library and Search selections and detail panels; album actions use the album's source, and late detail responses cannot replace a newer selection. (#82)
- Detail refresh during status updates. Same-album status events no longer discard pending track details or suppress mark/unmark refreshes; newer status is preserved when a pending detail response arrives. (#82)
- Settings load protection. Save stays disabled until configuration loads successfully, with a visible retry action on failure and no partial form hydration during auth checks. (#80)
- Frontend development API proxy. Vite now forwards same-origin HTTP and WebSocket API traffic to the backend on port 8080; production behavior is unchanged. (#79)
- Tidal authentication transitions. Completing device-code authentication now replaces a previously stored PKCE auth method, keeping persisted credentials and client initialization consistent. (#78)
- Reliable mark/unmark reconciliation. Downloads now cache the complete authoritative track catalog before starting, while manual mark/unmark and fuzzy auto-mark refresh it online before changing album, sentinel, or dedup state. Mark/unmark now requires a connected source and fails clearly if the catalog is unavailable or incomplete. (#81)
- Atomic album/dedup updates. Manual and scan mark/unmark now update album state and the per-source dedup database in one attached SQLite transaction, rolling both back on ordinary statement or lock failures. Best-effort sentinel writes and removals happen only after that mandatory commit. (#85)
- Safe legacy sentinel reconciliation. The downloads scan now discovers Qobuz and Tidal sentinels itself, requires a complete online catalog and matching local audio set, records the actual folder, and uses the same atomic album/dedup update as manual and fuzzy reconciliation. Malformed, partial, offline, or unsafe folders are reported without aborting healthy entries. (#89)
- Owned shutdown drainage. Shutdown now rejects new background work, drains the current album without advancing queued downloads, interrupts and records active syncs, cooperatively stops scans after cancellation-safe off-loop writes, waits for progress events, and only then closes current SDK clients. Repeated caller cancellation is propagated after the retained cleanup operation finishes. (#90)
- Transactional credential reloads. Qobuz and Tidal credential changes now build, open, and validate replacement SDK clients before atomically persisting credentials and publishing them through the shared client map. Active source work returns HTTP 409 without being interrupted; failed or cancelled activation preserves the previous credentials and exact client objects. (#91)
- Consistent naming defaults. Unset folder and track naming formats now use the same canonical defaults in Settings, the config API, and both source downloaders; explicitly stored custom formats remain unchanged. (#92)
- Download integrity and retries. Downloads preserve album artwork and metadata, isolate metadata failures within a batch, persist failed status across restarts, and retain track file metadata during status updates. The
alllibrary filter reports the correct total. (#60) - Accurate download completion and progress. Completed downloads record the album folder; below-threshold downloads remove misleading sentinels without discarding successful-track dedup records. Concurrent track byte progress is aggregated, and finished queue/scan history is bounded in memory. (#65)
- Responsive library sync and restart recovery. Album writes are batched off the event loop, and startup clears orphaned queued/downloading states without automatically re-enqueueing them. (#64)
- Safer fuzzy scans. Track-count mismatches go to review, folder walking runs off the event loop, and per-folder failures are reported without discarding healthy results. (#56)
- API lifecycle and validation. Pagination is bounded, sentinel settings parse consistently, album status events reach WebSocket consumers, and client shutdown cleanup is explicit. (#57)
- Clearer frontend feedback. Enqueue actions show feedback, album details display errors, stale source/query responses are ignored, and the WebSocket connection store tracks actual connection state. (#55, #63)
Internal
- Updated the yanked aiohttp dependency, consolidated development dependencies, and capped Python support at
>=3.10,<3.14. (#58) - Pinned Ruff to match CI, excluded non-product files from lint/build inputs, fixed branch build triggers, and removed the unused E2E job. ([#59](https://github.com/...
v0.0.6 — UX polish: rebrand strings + per-page titles
Small UX polish release on top of v0.0.5.1. No behavior changes.
Added
- Per-page document titles. Every route (
library,search,playlists,sync,settings,downloads) now sets its own<title>via<svelte:head>, so browser tabs / bookmarks / history land on something meaningful instead of the bare URL path.
Changed
- UI rebrand to "Libsync". Replaces the last user-facing
streamripstrings: app<title>default, sidebar header (also drops the stale "v3.0.0 — library manager" subtitle inherited from the upstream Python package version, in favor of "Qobuz & Tidal library manager"), Settings Download Path placeholder. Internals that retain the legacystreamripname (Python module,STREAMRIP_DB_PATH,streamrip.db,.streamrip.json,logging.getLogger("streamrip")) stay as-is per the v1.0 plan inCLAUDE.md.
Internal
- SDK submodule pin bumped from the v0.0.5.1 feature-branch SHA to the merged
mainSHA onarthursoares/qobuz_tidal_api_client. Same code; pin now points at a published, non-feature-branch commit.
Upgrade
Docker:
docker compose pull && docker compose up -dFull commit list
v0.0.5.1 — Codex review fixes for v0.0.5
Picks up three SDK fixes from PR #11 (the v0.0.5 PKCE + DASH work) flagged by Codex's automated review:
- DASH segment retry no longer corrupts the output file when a segment fails mid-stream. The downloader now snapshots file position before each segment and rewinds on retry instead of appending duplicate bytes.
- HiRes downloads no longer fail outright on systems without ffmpeg. When ffmpeg isn't available, the file is kept as MP4-with-FLAC (
.flacextension, mp4 magic) and the mutagen tag step is skipped with a warning instead of raisingFLACNoHeaderErrorand deleting the download. - BTS manifest decode falls back to a lower tier on corrupt base64 instead of bubbling out as an unhandled error. A v0.0.5 refactor accidentally moved the decode out of the
try/except.
Plus a ruff format fix to backend/services/library.py (long ternary line) and a submodule pin bump to pick up the SDK fixes.
Upgrade
Docker:
docker compose pull && docker compose up -dNo schema or config changes. Strongly recommended over v0.0.5 if you've already authenticated via PKCE.
Full commit list
v0.0.5 — Tidal HiRes (PKCE) + search→download metadata + retry button
Bug-fix and feature release. Headlines: real Tidal HiRes Lossless via a new PKCE OAuth flow (the legacy device-code client was capped at 320 kbps AAC regardless of subscription), search→download metadata round-trip, and a retry button on failed downloads.
Added
- Tidal HiRes login. New "Connect Tidal (HiRes)" button in Settings runs an Authorization Code + PKCE flow against Tidal's HiRes-capable client (
6BDSRdpK9hqEBTgU). The legacy device-code button is preserved but renamed "(legacy, AAC only)" — it's an entitlement cap on the OAuth client itself, independent of subscription tier. Tokens are persisted with atidal_auth_methodmarker so refresh dispatches to the matching helper. - DASH manifest + multi-segment download. PKCE-issued tokens make Tidal return
application/dash+xmlmanifests instead of the legacy single-URL JSON. The SDK now parses MPEG-DASHSegmentTemplate+SegmentTimeline, downloads init + N media segments sequentially, and concatenates them into a fragmented MP4. Whenffmpegis on PATH it gets remuxed (-c:a copy) into native FLAC so mutagen can tag it; otherwise the file is left as MP4-with-FLAC and a warning logs. - Retry button on failed/cancelled rows in the Downloads page. Posts to
/api/downloads/queuewithforce=trueso the per-source dedup DB doesn't skip-mark partial downloads. - Search→download metadata round-trip. Search results now forward their full title/artist/cover/track-count payload alongside
album_idswhen enqueueing. Backend prefers the supplied dict over re-fetching from the streaming service.
Fixed
- Search downloads showed
Album <id>/ Unknown._fetch_album_metadataused to silently fall back to a placeholder string when the SDK round-trip failed, then persist that placeholder to the DB where it stuck. Now: prefer caller-supplied metadata, fail loud when neither is available. - Folder label vs actual codec mismatch (Tidal). Folders were tagged
[FLAC-…]even when the real downloads were AAC m4a, because_tidal_quality_fieldsused the album's max-available tier instead of the actual download tier. Now computesmin(album_cap, user_request). Also:HI_RES(legacy MQA) correctly labels as[FLAC-16-44.1]since MQA is physically 16/44.1 with extra subbands. - Folder label sample-rate mismatch (Qobuz). Same family of bug: a 24/192 album downloaded at CD quality got
[FLAC] [24B-192kHz]. Now mirrors the requested tier (CD = 16/44.1, tier 3 = 24/96-cap, tier 4 = album max). Load Moredisappeared after page 1 in Search. When the Qobuz SDK returnedtotal=None, the backend'sgetattr(result, "total", default)leakedNoneto the JSON response (total: null), the frontend's?? items.lengthfell back to page size, andtotal > results.lengthevaluated false. Replaced with an explicit None check.- Tidal silent-downgrade is now visible. Manifest decode failures used to silently walk down the tier ladder; now a warning logs the requested quality + manifest mime type before fallback. Added a one-line
tidal manifest …debug log on every successful manifest decode.
SDK
- HI_RES_LOSSLESS (tier 4) added to
QUALITY_MAP. Settings dropdown gains the new option. - Tidal search envelope is now defensively unwrapped — handles both
{items, totalNumberOfItems}and{albums: {items, …}}shapes.
Schema
AppConfiggainstidal_auth_method: str(defaults to"device_code"; PKCE flow sets it to"pkce").DownloadRequestgains an optionalalbums: list[DownloadAlbumMetadata]field.
API additions
| Endpoint | Method | Description |
|---|---|---|
/api/auth/tidal/pkce-start |
POST | Returns auth_url + handle for the PKCE flow |
/api/auth/tidal/pkce-complete |
POST | {handle, redirect_url} — exchange code for HiRes-capable token |
Notes
- ffmpeg is required for HiRes. Without it, lossless downloads land as MP4-with-FLAC and the tag step is skipped (with a warning). The Docker image already installs
ffmpeg; standalone-Python users need it on PATH. - Tidal subscription tier ≠ OAuth client tier. Two separate caps. The HiRes button uses a HiRes-capable client; users on TIDAL Free/Premium will still be capped at HIGH (AAC) by their subscription — but for HiFi/Individual+ accounts, real HiRes is now reachable.
Upgrade
Docker:
docker compose pull && docker compose up -dTo enable HiRes on existing installs: open Settings → click ▸ Connect Tidal (HiRes) → sign in → paste the redirect URL back. The legacy "(AAC only)" button remains for users who want to stay on it.
Full commit list
v0.0.4 — Scan UI bugfixes
Bugfix release on top of v0.0.3. Four issues found while testing the fuzzy-scan against a real 1,800-album library.
Fixed
- Scan panel stuck at
0 / ?—GET /api/library/scan-fuzzy/{job_id}now returns live{scanned, total}progress while the job runs. The scan's event-bus progress events already drove the WebSocket channel; the polling endpoint was just returning{"status": "running"}with nothing for the UI to render. - Scan Review panel had no background. The Svelte component referenced
--surface,--fg,--muted,--shadow-color— none of which exist in the design system. Browsers resolved unknownvar()to empty so the panel blended into the page. Switched to the real tokens:--canvas-raised,--text-primary,--text-secondary,--shadow-lg, plus a--borderleft edge. - Library grid didn't refresh on Mark / Unmark. The backend already publishes
album_status_changed; the frontend library store now subscribes to it and patches the matching row in place. Fuzzy-scan auto-matches flow through the same event, so the grid also updates live during a scan. - Load More was instantly overwritten by a page-1 refetch. The library page's reload-on-filter
$effectcalledfetchAlbums(), which readcurrentPagewhile building params. Svelte 5$effecttracks reactive reads transitively, socurrentPagebecame a dependency — bumping it via Load More re-triggered the effect, which resetcurrentPageto 1 and overwrote page 2 with page 1. Fixed by wrapping the effect body inuntrack().
Upgrade
Docker:
```bash
docker compose pull && docker compose up -d
```
No schema changes since v0.0.3 — this release is pure bugfix.
Full commit list
v0.0.3 — Library fuzzy-scan + mark-as-downloaded
Highlights
- Library fuzzy-scan — the Settings Scan Folder action now walks your existing
Artist/Album/collection and matches against the synced Qobuz/Tidal library. Exact matches (normalized artist+album + matching bit-depth) are auto-marked; ambiguous cases land in a three-section review slide-over. - Manual Mark as downloaded / Unmark button on the album detail panel — for when you know you have something but the scan can't make a clean match.
- Tidal quality selector now exposed in Settings. Backend already honored
tidal_quality; only the UI input was missing.
Schema
Schema v2 adds bit_depth, sample_rate, local_folder_path to albums. Existing v1 DBs migrate on first open with best-effort backfill from the legacy quality string.
Configuration
scan_sentinel_write_enabledtoggle in Settings — disable when pointing the scan at a read-only NFS/SMB mount.
Security
POST /api/library/albums/{id}/mark-downloadedvalidateslocal_folder_pathis inside the configureddownloads_path(400 otherwise)._find_album_foldersskips symlinked children so a symlink inside the downloads root can't escape it.
Quality
All 26 pre-existing ruff check errors cleaned up; ruff format applied across the repo. Both ruff CI steps green for the first time.
API additions
| Endpoint | Method | Description |
|---|---|---|
/api/library/scan-fuzzy |
POST | Start a background fuzzy-scan job. 409 if another scan is running. |
/api/library/scan-fuzzy/{job_id} |
GET | Poll job status. |
/api/library/albums/{id}/mark-downloaded |
POST | {local_folder_path?} — flip status, populate dedup DB, optional sentinel write. |
/api/library/albums/{id}/unmark-downloaded |
POST | Reverse the above. |
New WebSocket events: scan_progress, scan_complete, album_status_changed.
Docker
Images published to ghcr.io/arthursoares/libsync:v0.0.3 (and :latest, :0.0, :0). Upgrade path: bump the image tag, restart the container — the schema migration runs automatically on first start.
Full commit list
v0.0.2 — Rebrand to Libsync, Codex fixes, SDK rename
First release after detach + rebrand. Image: `ghcr.io/arthursoares/libsync:v0.0.2` (also tagged `0.0`, `0`, `latest`).
Highlights
Project rebrand
- Repo renamed: `arthursoares/streamrip` → `arthursoares/libsync`
- New GHCR image path (`ghcr.io/arthursoares/libsync`)
- Internals retain the legacy `streamrip` prefix (env vars, SQLite filename, `.streamrip.json` sentinel, Python module name) for compatibility — full internal rename queued for v1.0
Fixes (from Codex review)
- P1 path traversal in SPA static-file route — `os.path.realpath` containment check
- P1 auto-sync re-evaluation after credential hot-reload — fixes the enable-auto-sync-then-authenticate dead-loop
- P2 case-insensitive boolean config parsing — Pydantic-stringified `"True"`/`"False"` now honored
Infrastructure
- SDK renamed `arthursoares/qobuz_api_client` → `arthursoares/qobuz_tidal_api_client`, now public and GPL-3.0
- Removed dead upstream `poetry-publish.yml` workflow
- CI hardening: dropped flaky poetry venv cache (concurrent push+PR runs no longer race on stale .venv)
- New OAuth-redirect contribution from @leolobato — Qobuz callback uses `window.location.origin` instead of hardcoded `localhost:11111`
Upgrade notes
Existing Docker deployments using `ghcr.io/arthursoares/streamrip:` should switch image references to `ghcr.io/arthursoares/libsync:`. Env vars and on-disk format are unchanged.