Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3b0ca08
core: Honor audio provider settings in backend matching
dx616b May 28, 2026
b87e247
Release 2.7.7: slskd, library paths, and Navidrome playlist sync
dx616b May 28, 2026
d2f29ab
Document 2.7.7 setup: README, compose example, and dx616b image.
dx616b May 28, 2026
20b68e9
Improve download queue UX with filters, retry, and playlist pruning.
dx616b May 28, 2026
7012ce1
Release 2.7.8: queue filters, retry, and built frontend assets.
dx616b May 28, 2026
d2b1db3
Fix download-to-device filenames with decoded save names (2.7.9).
dx616b May 28, 2026
3f86070
docs: Use upstream image in compose examples for upstream PR.
dx616b May 28, 2026
4b328fe
Fix queue Waiting filter and parallel download status (2.7.10).
dx616b May 28, 2026
f3b840c
Refine queue filters: In progress vs Waiting, no wait spinner.
dx616b May 28, 2026
945fc29
Fix slskd parallel downloads: semaphore instead of global lock.
dx616b May 28, 2026
20daabe
Apply parallel download limit to slskd when settings are saved.
dx616b May 28, 2026
9ae39a2
Hint to keep parallel downloads low when slskd is enabled.
dx616b May 28, 2026
ab54864
Add YouTube cookies settings and upload for yt-dlp.
dx616b May 28, 2026
6a27d5a
Add python-multipart for YouTube cookies file upload.
dx616b May 28, 2026
8dc39f8
Use www.youtube.com and retries for age-restricted yt-dlp downloads.
dx616b May 28, 2026
33f3c46
Show provider and step messages for YouTube downloads in queue UI.
dx616b May 28, 2026
ebe970d
Retry yt-dlp with alternate format selectors when audio unavailable.
dx616b May 28, 2026
9f53a2e
Retry YouTube downloads without cookies when web+cookies yields no au…
dx616b May 28, 2026
a373ac1
Try next slskd search result when a candidate download fails.
dx616b May 28, 2026
02e8ed2
Improve YouTube cookie handling and try alternate search matches.
dx616b May 28, 2026
e8eea1c
Move YouTube cookies to optional collapsed Settings section.
dx616b May 28, 2026
48e46ee
Hide YouTube cookies behind minimal advanced text toggle.
dx616b May 28, 2026
e56e9a6
Style YouTube cookies like other Settings toggle sections.
dx616b May 28, 2026
591596d
Score slskd search results and require confident artist+title match.
dx616b May 29, 2026
4f920b1
Treat Soulseek folder paths as first-class match signals.
dx616b May 29, 2026
9a06c07
Format code
henriquesebastiao Jun 1, 2026
237a6ae
Merge upstream main (v2.8.0) into feature branch
dx616b Jun 2, 2026
1942e51
Fix Navidrome large-playlist sync and align tests with upstream
dx616b Jun 2, 2026
1414c22
Add Docker Hub publish workflow and compose for fork testing
dx616b Jun 2, 2026
bd47f5c
Rename Docker Hub image to dx616b/spoti-to-navidrome
dx616b Jun 2, 2026
4f16177
Log Navidrome playlist POST batches for deploy verification
dx616b Jun 2, 2026
5e35366
Improve Navidrome matching for downtify/ library paths
dx616b Jun 2, 2026
d1609ad
Match Navidrome paths by relative tail, not hardcoded prefixes
dx616b Jun 2, 2026
be92519
Tighten Navidrome search for multi-artist and special-char filenames
dx616b Jun 2, 2026
db94d5a
Prefer exact mutagen tag match for Navidrome library lookup
dx616b Jun 2, 2026
46cb140
Stop Navidrome search early and cap fallback queries per track
dx616b Jun 2, 2026
9050614
Skip wrong slskd paths and relax tag duration for Navidrome match
dx616b Jun 2, 2026
e3901fe
Add library catalog caches, reconcile, and Library UI improvements.
dx616b Jun 2, 2026
73c0a34
Document library catalog, reconcile, and feature-branch changelog.
dx616b Jun 2, 2026
4dabdbf
Fix library delete sync, import crash, and reconcile behavior.
dx616b Jun 2, 2026
94f8cca
Remove fork-only Docker Hub CI from upstream PR scope.
dx616b Jun 2, 2026
cce529d
Add library batch delete, faster Navidrome sync, and queue fixes
dx616b Jun 3, 2026
31ee187
Fix Prettier formatting in Library UI files
dx616b Jun 3, 2026
fe1e6c4
Improve parallel downloads, audio matching, slskd, and mobile player
dx616b Jun 4, 2026
8cc05c8
Fix CI: ruff lint and Prettier formatting
dx616b Jun 4, 2026
bf3b2f7
Improve search UX, player persistence, and playlist refresh after dow…
dx616b Jun 4, 2026
2c63697
Fix Prettier formatting for CI on search UI locale files.
dx616b Jun 4, 2026
12eadd7
Fix queue tab on new playlist download and harden Spotify embed refresh.
dx616b Jun 4, 2026
5809e14
Unify variant filtering across providers and tighten duration defaults.
dx616b Jun 4, 2026
b4971e2
Fix Prettier formatting in Settings.vue slskd defaults.
dx616b Jun 4, 2026
ac4fb41
Keep queue on In progress tab when a new download starts.
dx616b Jun 4, 2026
afcb405
Fix queue not refreshing when a new playlist batch starts.
dx616b Jun 4, 2026
323afbc
Separate In progress and Waiting queue tabs again.
dx616b Jun 4, 2026
ad7efd6
Keep queue on In progress tab with split Waiting filter.
dx616b Jun 4, 2026
bafc5d8
Remove queue page subtitle explanatory text.
dx616b Jun 4, 2026
1f781f6
Fix queue page not updating until full page refresh.
dx616b Jun 5, 2026
b43ea3b
Add Search playlist downloads with live progress and batch tracking.
dx616b Jun 6, 2026
453f5cc
Delete tag-mismatched library files and fix partial playlist batch sync.
dx616b Jun 8, 2026
427e35f
docs: update README for fork features and Docker image
dx616b Jun 9, 2026
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
DOWNLOAD_DIR="/downloads"
WEB_GUI_LOCATION="/frontend/dist"
WEB_GUI_LOCATION="./frontend/dist"
DOWNTIFY_PORT="8000"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -302,4 +302,5 @@ typings/
data/downtify_monitor.db
data/settings.json

.claude/
.claude/
.aider*
25 changes: 25 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## [Unreleased]

**Implemented enhancements:**

- **Library catalog** — SQLite playlist catalog (track membership per Spotify playlist), path-stable `content_key` (basename + file size), and Navidrome song-id index for faster repeat syncs ([#182](https://github.com/henriquesebastiao/downtify/pull/182))
- **Library performance** — In-memory path scan cache (~90s TTL) and SQLite metadata cache for `/api/list`; optional on-disk cover art cache under `/data/cover_cache` (Settings → Library & player)
- **Library UI** — Client-side search (title, artist, album, path), pagination with saved page size, playlist badges on each row, dedicated Library search (navbar search hidden on Library page), refresh with `?refresh=true`
- **Library path sync** — Settings → **Fix library paths** and `POST /api/library/reconcile`: detect moved files, prune stale index/catalog rows after deletes, backfill `content_key`, optionally regenerate M3U and re-sync Navidrome for affected playlists
- **Navidrome large playlists** — `createPlaylist` via POST with batched `updatePlaylist` to avoid HTTP 414 when syncing hundreds of tracks
- **Navidrome matching** — Stronger library matching (mutagen tags, path tails, slskd folder paths, capped fallback queries, early exit on confident match)
- **slskd + Navidrome integration** (feature branch) — slskd provider with leave-in-place, configurable provider order, track index dedupe, `/downloads` + `/slskd` library paths, M3U absolute paths, in-place Navidrome playlist updates, playlist monitor M3U/Navidrome refresh
- **Download queue** — Status filters, per-track and bulk retry, clear completed, prune finished tracks when a new Spotify playlist starts
- **YouTube cookies** (optional) — Settings upload/path for age-restricted yt-dlp fallbacks
- **Startup** — FastAPI lifespan handler replaces deprecated `on_event('startup')`

**Fixed bugs:**

- Navidrome playlist sync failing with HTTP 414 on large playlists
- Library `/list` validation errors when returning playlist names per track
- slskd path false positives during Navidrome match; duration tolerance for tag matching

**Documentation:**

- README feature summary, `docs/features/library-catalog.md`, and changelog entries for catalog, reconcile, and caches

## [2.8.0](https://github.com/henriquesebastiao/downtify/tree/2.8.0) (2026-06-02)

[Full Changelog](https://github.com/henriquesebastiao/downtify/compare/2.7.0...2.8.0)
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ up:
down:
docker compose down

run:
run: frontend-build
uv run python main.py web

frontend-build:
npm run build --prefix frontend

format:
uv run ruff format .; ruff check . --fix
prettier --write frontend/src/.
Expand Down Expand Up @@ -59,4 +62,4 @@ doc:
%:
@:

.PHONY: all build latest clean up down run format lint export changelog version doc
.PHONY: all build latest clean up down run frontend-build format lint export changelog version doc
278 changes: 247 additions & 31 deletions README.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Production-style Compose for Downtify 2.7.x (slskd + Navidrome-friendly).
#
# Usage:
# cp docker-compose.example.yml docker-compose.yml
# # Edit host paths below, then:
# docker compose pull
# docker compose up -d
#
# slskd is not included here — run your existing slskd stack and mount the
# same host folder to /slskd in Downtify. In Settings → slskd set base URL
# (e.g. http://slskd:5030 on a shared Docker network) and source_dir /slskd.

services:
downtify:
container_name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
restart: unless-stopped
ports:
- '8000:30321'
environment:
- DOWNTIFY_PORT=30321
volumes:
- /path/to/music/downloads:/downloads
- /path/to/music/slskd:/slskd
- downtify_data:/data
# Optional: mount cookies instead of uploading in Settings → YouTube cookies
# - /path/to/cookies.txt:/data/youtube-cookies.txt:ro

volumes:
downtify_data:
8 changes: 5 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Local development compose (build from source).
# For production with the published image, use: cp docker-compose.example.yml docker-compose.yml
services:
downtify:
container_name: downtify
Expand All @@ -7,11 +9,11 @@ services:
ports:
- '8000:30321'
volumes:
# If slskd runs elsewhere, mount the SAME host folder as slskd at /slskd
# and set Settings → slskd → folder path to /slskd.
- ./docker/downloads:/downloads
# - ./docker/slskd:/slskd
- ./docker/data:/data
- ./frontend/dist:/downtify/frontend/dist:ro
environment:
- DOWNTIFY_PORT=30321
dns:
- 1.1.1.1
- 1.0.0.1
26 changes: 26 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,32 @@ Delete a downloaded file.

---

### `POST /api/library/delete/batch`

Delete multiple library files by stored relative path.

**Request body:**

```json
{ "files": ["My Playlist/Song.mp3", "Artist - Track.mp3"] }
```

**Response:** `{ "deleted": ["…"], "failed": [{ "file": "…", "error": "…" }], "deleted_count": 1, "failed_count": 0, "playlists_affected": ["My Playlist"], "playlists_refresh_scheduled": true }`

---

### `DELETE /api/library/playlist`

Delete all tracks for a playlist, remove its M3U file(s), and drop the playlist catalog entry.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `playlist_name` | string | yes | Playlist name as shown in Library badges |

**Response:** `{ "ok": true, "playlist": "…", "files": ["…"], "deleted_count": 2, "failed_count": 0, "failed": [], "playlists_affected": ["…"], "playlists_refresh_scheduled": true }`

---

### `GET /cover`

Return the embedded cover art for a file.
Expand Down
37 changes: 37 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@ icon: lucide/history

# Changelog

## [Unreleased] — feature branch

Pending upstream merge ([#182](https://github.com/henriquesebastiao/downtify/pull/182)). See [Library catalog & path sync](features/library-catalog.md) for how the new pieces fit together.

**Enhancements**

- **slskd provider** — Soulseek via slskd with leave-in-place under `/slskd`, timeouts, and fallback to YouTube providers
- **Provider order** — Enable and order slskd / YouTube Music / YouTube in Settings
- **Track index** — Skip re-downloads when a Spotify track is already on disk (including `/slskd` paths)
- **Playlist catalog** — Remember which playlist downloads contain each file; show badges in Library
- **Fast Library** — Path and metadata caches; optional cover cache; search and pagination in the UI
- **Library bulk delete** — Multi-select track delete and delete entire playlist from the Library page
- **Fix library paths** — Manual reconcile after moving files on disk; optional M3U and Navidrome refresh
- **Navidrome** — Update existing playlists in place; POST/batched API for large playlists; improved track matching
- **Download queue** — Filters (Waiting / In progress / …), retry, clear completed
- **YouTube cookies** (optional) — Upload `cookies.txt` for difficult age-restricted fallbacks

**Bug fixes**

- HTTP 414 when syncing very large playlists to Navidrome
- Library list API errors for playlist name fields
- Navidrome match quality for slskd folder layouts and tag duration edge cases

---

## [2.8.0](https://github.com/henriquesebastiao/downtify/tree/2.8.0) — 2026-06-02

**Enhancements**

- Genre tags enriched from the public iTunes Search API at download time ([#187](https://github.com/henriquesebastiao/downtify/pull/187))

**Security**

- Dependency vulnerability fixes ([#170](https://github.com/henriquesebastiao/downtify/issues/170))

---

## [2.6.0](https://github.com/henriquesebastiao/downtify/tree/2.6.0) — 2026-05-03

**Enhancements**
Expand Down
1 change: 1 addition & 0 deletions docs/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Downtify covers everything you need to build and maintain a local music library
| [Download Settings](download-settings.md) | Choose format (MP3/FLAC/M4A/OGG/OPUS) and bitrate per download |
| [Playlist Monitor](playlist-monitor.md) | Watch playlists and auto-download new tracks as they appear on Spotify |
| [Built-in Player](player.md) | Play your downloaded music in the browser with shuffle, repeat and album art |
| [Library catalog & path sync](library-catalog.md) | Fast Library UI, playlist badges, caches, and manual path reconcile after file moves |
| [M3U Export](m3u-export.md) | Auto-generated playlist files for Jellyfin, Navidrome, Plex and any media app |
| [File Organization](file-organization.md) | Flat layout or per-artist subfolders |
| [Lyrics](lyrics.md) | Automatically download and embed lyrics (plain and time-synced) |
Expand Down
86 changes: 86 additions & 0 deletions docs/features/library-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
icon: lucide/library
---

# Library catalog & path sync

Downtify keeps a **catalog** of files under `/downloads` and `/slskd` so the Library page, player, M3U export, Navidrome sync, and deduplication stay fast and consistent when files move or playlists grow.

## What gets stored

| Store | Location | Purpose |
|-------|----------|---------|
| **Track index** | `/data` SQLite | Maps Spotify track IDs → library path; skips re-downloads |
| **Playlist catalog** | `/data` SQLite | Which tracks belong to each downloaded Spotify playlist |
| **Navidrome index** | `/data` SQLite | Caches Navidrome song IDs per file (`content_key`) |
| **Library metadata cache** | `/data` SQLite | Title, artist, album for `/api/list` without re-reading every tag |
| **Path scan cache** | In-memory (~90s TTL) | List of relative paths under `/downloads` and `/slskd` |
| **Cover art cache** | `/data/cover_cache` (optional) | Embedded cover bytes for `/cover` and the Library thumbnails |

Tracks are identified for matching and cache invalidation by a **content key**: SHA-256 of the file **basename + size**. Renaming or moving a file without changing name/size updates the stored path; replacing the file with different content gets a new key.

## Library page (UI)

Open **Library** in the nav bar.

- **Search** — Filters the current list by title, artist, album, or path (client-side). The global navbar search is hidden on this page so only one search box is shown.
- **Pagination** — Page size is configurable and remembered in the browser (`25` / `50` / `100`).
- **Playlist badges** — Shows which saved Spotify playlists include each track (from the playlist catalog).
- **Refresh** — Reloads the list from the server; use **Refresh** after bulk file changes. The backend bypasses caches when `?refresh=true` is passed.
- **Play / download / delete** — Per-track delete removes the file and cleans track index, playlist catalog, and cover cache entries.
- **Delete selected** — Checkboxes and **Delete selected** remove many tracks in one request; affected playlists refresh M3U/Navidrome in the background.
- **Filter by playlist** — Narrow the list; **Delete playlist** removes all catalog tracks for that playlist, audio under its folder, and its M3U file(s).

## Settings → Library & player

### Cache album art on disk

When enabled, Downtify writes extracted cover images under `/data/cover_cache`. The player and Library load covers from disk instead of parsing tags on every request. Safe to disable anytime; extra disk use only.

### Fix library paths (manual reconcile)

Use after you **move or reorganize files on disk** (same filename and size, new folder).

1. Open **Settings** → **Library & player** → **Fix library paths**.
2. Downtify scans `/downloads` and `/slskd`, builds a disk index by `content_key`, and:
- **Updates paths** in the track index and playlist catalog when the old path no longer exists but the same file is found elsewhere.
- **Prunes stale rows** when the file was deleted (including playlist catalog entries).
- **Backfills `content_key`** on older index rows that only had paths.
3. If **Generate M3U** and/or **Create playlist in Navidrome** are enabled, affected playlists are regenerated or re-synced after path updates.

Reconcile does **not** run on a schedule or at startup — only when you press the button (or call `POST /api/library/reconcile`).

!!! note "Deletes vs moves"
Deleting a track from the **Library** UI removes the file, catalog rows, and (when M3U or Navidrome sync is enabled) rewrites affected playlists. **Fix library paths** is for **moves** and for **manual deletes on disk** (Finder, SSH, etc.) that left stale rows in the database. If you already deleted in the UI, reconcile often reports “no changes” because the catalog is already clean — use **Refresh** on the Library page if the list looks stale.

## Navidrome (related behavior)

- **Large playlists** — `createPlaylist` uses POST with batched `updatePlaylist` so hundreds of song IDs do not trigger HTTP 414.
- **Matching** — Library scan, path tail matching, mutagen tags, and slskd-style paths; search stops early when a confident match is found.
- **Playlist catalog** — Monitor and playlist downloads register tracks so reconcile and Navidrome refresh know playlist membership.

See [Navidrome setup](../../README.md#-navidrome-playlist-sync) in the README for server folders and credentials.

## API

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/list` | Library entries with metadata and `playlists: string[]` |
| `GET` | `/api/list?refresh=true` | Bypass path/metadata caches |
| `POST` | `/api/library/delete/batch` | Body `{ "files": ["path/relative.mp3", …] }` — batch delete |
| `DELETE` | `/api/library/playlist?playlist_name=…` | Delete playlist tracks, folder orphans, M3U, catalog row |
| `POST` | `/api/library/reconcile` | Run path reconcile + optional M3U/Navidrome refresh |
| `GET` | `/cover?file=…` | Cover art (uses disk cache when enabled) |

Response shape for reconcile:

```json
{
"paths_updated": 0,
"pruned_stale": 0,
"content_keys_backfilled": 0,
"playlists_affected": [],
"refresh_m3u": false,
"refresh_navidrome": false
}
```
4 changes: 3 additions & 1 deletion docs/features/player.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Downtify ships with a web player so you can listen to your downloaded music with

The player loads every audio file found recursively inside the downloads directory. Files are served directly from the container via the `/downloads` static mount.

Filenames in the format `Artist - Title.ext` are parsed so the now-playing card can show artist and title cleanly. Cover art is fetched on demand from the `/cover` endpoint, which reads the embedded image tags from the file itself — the same artwork Downtify wrote at download time.
Filenames in the format `Artist - Title.ext` are parsed so the now-playing card can show artist and title cleanly. Cover art is served from `/cover`, which reads embedded tags (and optional folder images like `cover.jpg`). When **Cache album art on disk** is enabled in Settings, covers are stored under `/data/cover_cache` for faster Library and player loads.

Files under `slskd/…` are played via `/media/slskd/…` URLs. See [Library catalog & path sync](library-catalog.md) for how paths and playlists are tracked.

Playback uses the browser's native HTML5 audio element. No plugins, no extra processes.

Expand Down
30 changes: 28 additions & 2 deletions docs/getting-started/docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,28 @@ Docker Compose is the recommended way to run Downtify for persistent home-server

## Minimal setup

Create a `docker-compose.yml` file:
Copy the repository example (recommended for 2.7.x + slskd):

```bash
cp docker-compose.example.yml docker-compose.yml
# Edit host paths in docker-compose.yml
docker compose pull
docker compose up -d
```

[`docker-compose.example.yml`](../../docker-compose.example.yml) maps `/downloads` and `/slskd`, and listens on port `30321` inside the container (`8000:30321` on the host).

Or create a minimal `docker-compose.yml` manually:

```yaml
services:
downtify:
container_name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
ports:
- '8000:8000'
- '8000:30321'
environment:
- DOWNTIFY_PORT=30321
volumes:
- ./downloads:/downloads
- downtify_data:/data
Expand All @@ -34,6 +47,19 @@ docker compose up -d

Open **[http://localhost:8000](http://localhost:8000)**.

## With slskd (Soulseek)

Add a second volume so Downtify and slskd share the same Soulseek download folder:

```yaml
volumes:
- ./downloads:/downloads
- ./slskd:/slskd
- downtify_data:/data
```

In Downtify **Settings → slskd**: enable slskd, set **Base URL** to your slskd API (e.g. `http://slskd:5030` on a shared Docker network), **API key**, and **folder path** `/slskd`. Mount that same host directory on your slskd container.

## Custom port

If port 8000 is already in use, map a different host port and set the `DOWNTIFY_PORT` environment variable so the container listens on the same port internally:
Expand Down
2 changes: 2 additions & 0 deletions docs/getting-started/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ All environment variables are optional. Downtify works out of the box without an
| `DOWNTIFY_PORT` | `8000` | Port the server listens on inside the container. Change the left side of the port mapping to expose a different host port. |
| `DOWNLOAD_DIR` | `/downloads` | Directory where audio files are saved. Override if you mount your library at a custom path. |
| `HOST` | `0.0.0.0` | Bind address for the web server. |
| `DOWNTIFY_LOG_LEVEL` | `info` | Application log level (`debug`, `info`, `warning`, …). |
| `DOWNTIFY_ACCESS_LOG` | _(off)_ | Set to `full` (or `1` / `true`) to log every HTTP request. Off by default so queue polling and static assets do not flood the log. |

## Anti-bot / YouTube

Expand Down
Loading