A self-hosted web UI for managing and downloading your Qobuz and Tidal music libraries.
Runs as a small FastAPI + SvelteKit server in Docker (or locally) and is accessed through any browser. It syncs your streaming-service favorites into a local database, lets you search and trigger downloads with live progress, and organizes the files on disk with your preferred folder/track templates. Both Qobuz and Tidal go through standalone async Python SDKs (consumed as a git submodule) — no CLI, no TOML config, no hidden state.
Libsync started life as a fork of nathom/streamrip and was rebuilt around a web UI. The original
ripCLI, the TUI, and the Deezer/SoundCloud source clients are no longer part of this project. See Acknowledgements for credit to the upstream. Some internals (env var names, the on-disk SQLite filename, the.streamrip.jsonsentinel) still carry the old name to preserve compatibility with existing deployments — those will rename in a future major release.
- Library browsing — every album you've favorited in Qobuz or Tidal, with cover art, sortable table, and search
- Album detail — full track list, bit-depth / sample-rate / codec info, per-track download status
- Search — queries the streaming services directly; results show whether each album is already in your library
- Downloads — queue albums from the library or search, with real-time per-track progress pushed over WebSocket
- Playlists — browse Qobuz playlists and queue all referenced albums from a playlist detail panel
- Sync — manual refresh or scheduled auto-sync to keep local library in step with the streaming service
- Custom paths — folder/track format templates with live preview in Settings (
{albumartist},{title},{container},{bit_depth},{sampling_rate}, etc.) - Filesystem dedup — a
.streamrip.jsonsentinel file per album folder so re-scans and reset-database operations don't re-download work you already have - Library scan — fuzzy-matches pre-existing local collections against your synced library (bit-depth-aware, read-only-mount friendly) with a review UI for ambiguous cases and a manual Mark as downloaded button on album pages
- Artwork embedding — cover art baked into tags at configurable resolution
- OAuth login for Qobuz and Tidal (browser/device flows, including a headless URL-paste flow for Qobuz)
- Per-source dedup database so Qobuz and Tidal track IDs never collide
- Docker (recommended) — everything runs inside the image, you just need a folder to mount for music and a volume for the SQLite data
- or Python 3.10+, Node 20+,
poetry,ffmpeg, and access toarthursoares/qobuz_tidal_api_clientfor the SDK submodule
A premium Qobuz or Tidal subscription is required for downloads. This project doesn't bypass DRM or account restrictions — it uses your own credentials against the streaming services' own APIs.
# clone with submodules so the Qobuz + Tidal SDKs come along
git clone --recursive git@github.com:arthursoares/libsync.git
cd libsync
# build
docker build -f docker/Dockerfile -t libsync .
# run — mount your music folder and a data volume for the SQLite DB
# (env var and volume names retain the legacy `streamrip` prefix for
# now to keep existing installations working — see top-of-README note)
docker run -p 8080:8080 \
-v ~/Music:/music \
-v streamrip-data:/data \
-e STREAMRIP_DB_PATH=/data/streamrip.db \
libsyncOpen http://localhost:8080 and follow the first-time setup below.
If you forgot --recursive, you can initialize the submodule after the fact:
git submodule update --init --recursivePrefer compose? A ready-to-use file is at docker-compose.example.yml. It pulls the published image from GHCR — no clone, no local build:
curl -O https://raw.githubusercontent.com/arthursoares/libsync/main/docker-compose.example.yml
docker compose -f docker-compose.example.yml up -dConfig, credentials, and the library database land in ./data next to the compose file — back that directory up to preserve your setup. Downloaded music goes to ./music; edit the volume mapping if you want it elsewhere.
The :latest tag tracks stable releases. Swap to :main or :dev if you want rolling branch builds.
The container runs as a fixed non-root user (uid/gid 1000), so the host directories you mount to /music and /data must be writable by that uid — run sudo chown -R 1000:1000 ./music ./data (or the paths you mounted) if they're currently owned by another user, e.g. root from before this change or from a fresh mkdir.
When upgrading an existing installation, stop the app and back up its data first. If /data uses a named volume (such as streamrip-data in the Docker quick start), changing ./data on the host does not change that volume. Repair its ownership separately:
docker run --rm --user 0 --entrypoint chown \
-v YOUR_DATA_VOLUME:/data \
ghcr.io/arthursoares/libsync:latest -R 1000:1000 /dataReplace YOUR_DATA_VOLUME with the actual volume name from docker volume ls; Compose may prefix it with the project name. Ensure the music mount and any custom download paths are writable by UID 1000 as well, then restart the app. New empty named volumes inherit the correct ownership automatically. See the changelog for version-specific upgrade notes.
git clone --recursive git@github.com:arthursoares/libsync.git
cd libsync
# install backend deps + both SDKs from the submodule
make deps
poetry install
# install frontend deps
cd frontend && npm install && cd ..
# build frontend + run backend with hot-reload on :8080
make devSeparate targets if you want more control:
make dev-backend— backend only, assumesbackend/static/already has a built frontendmake dev-frontend— frontend only with Vite hot-reload, proxied to a running backendmake test— unit tests (no credentials needed; ~1s)make lint— ruff check onbackend/cd frontend && npm run check— Svelte typecheck / diagnosticsnode --test frontend/tests/*.test.js— lightweight frontend logic testsmake docker— build and run the Docker image
- Open http://localhost:8080 → Settings.
- Qobuz — click Login with Browser to run the OAuth flow. On a remote/headless host, use Headless Login instead: run the flow on a machine with a browser, copy the redirect URL, paste it into the box.
- Tidal — click Connect Tidal to start the device-code OAuth flow. The Settings page opens the approval URL and polls automatically until authorization completes.
- Set Download Path to
/music(Docker) or your preferred local path. - Choose your Qobuz Quality tier and adjust folder / track format templates if needed. The Settings page has a live preview.
- Optional: use Scan Downloads to reconcile any existing music collection on disk with the library. Fuzzy-matches
Artist/Album/folders by tags + bit-depth, auto-marks exact matches, and surfaces ambiguous cases for manual confirmation. For read-only mounts (NFS/SMB), turn Write sentinel file off before running. - Click Save.
- Go to Library → Refresh Library to pull in your favorites.
backend/ is a FastAPI app serving both a REST API and a WebSocket channel. frontend/ is a SvelteKit app compiled to static HTML/CSS/JS and served by the backend. State lives in a single SQLite database (backend/models/database.py) — albums, tracks, config, sync history. Streaming-service access goes through arthursoares/qobuz_tidal_api_client, consumed as a git submodule at sdks/qobuz_api_client/, which provides two separate Python packages: qobuz and tidal. Each exposes an AlbumDownloader with progress callbacks that the backend wires to WebSocket events. There is no CLI, no TOML config file, no hidden process — everything the app does is controlled from the Settings page and persisted in the SQLite DB.
For more detail see docs/WEB_UI.md, frontend/README.md, and CLAUDE.md.
libsync/
├── backend/ FastAPI app (REST + WebSocket)
│ ├── api/ Route modules (library, downloads, sync, auth, config, websocket)
│ ├── services/ Library, download, sync services wiring the SDKs to the DB
│ ├── models/ SQLite schema + Pydantic request/response models
│ └── main.py create_app factory + lifespan
├── frontend/ SvelteKit app
│ └── src/lib/ Components, stores, API client, design system
├── sdks/
│ └── qobuz_api_client/ Git submodule: qobuz + tidal Python SDKs
├── tests/ Pytest suite covering backend services, API routes, database
├── docker/Dockerfile Multi-stage build (Node → Python)
├── docs/WEB_UI.md Full architecture + API reference
├── CLAUDE.md Architecture notes for AI assistants working in the repo
├── Makefile Dev/test/build/run targets
└── pyproject.toml Poetry, 3.0.0, backend-only
Bug reports and PRs are welcome. Please run make test and make lint before opening a PR. For non-trivial changes, open an issue first so we can agree on the approach.
The original nathom/streamrip — this fork started from there and inherited the Qobuz/Tidal client logic, the tagging helpers, and the MQA decryption primitives. Most of that code now lives in the standalone qobuz_tidal_api_client SDK, but the design and much of the hard-won protocol knowledge came from upstream. Thanks to nathom, Vitiko98, Sorrow446, DashLt, and the projects that inspired streamrip in the first place:
This software is for personal use with your own legitimately-purchased streaming subscriptions. You are responsible for complying with the terms of service of Qobuz and Tidal. The authors take no responsibility for how you use it.
GPL-3.0-only, same as the upstream nathom/streamrip project.