# Harmony — Documentation > Self-hosted music grabber, tag editor & library manager for the Nightwing music library. > This file lives in a **hidden, safe** location (`/mnt/Nightwing/media/music/.harmony/`) — it is > ignored by Navidrome (dot-dir + `.ndignore`), by Harmony's own library scanner (skips dot-dirs), > and by the duplicate/delete tools (they only touch albums with audio under non-dot artist dirs). > Keep appending to it as features change. _Last updated: 2026-07-07 (FLAC-torrent-first acquisition ladder + FLAC/Best formats + multi-select + upgrade + provenance-aware dedup)_ ## What Harmony is A small Flask web app that grabs music and lets you edit/organize the library from a browser, writing clean **Navidrome-ready tags**. Files land in `/mnt/Nightwing/media/music// ()/NN - Title.ext` and Navidrome's file-watcher picks them up automatically. **Acquisition ladder (2026-07-07):** a grab now tries the **best FLAC torrent first** (Prowlarr search → qBittorrent, VPN-routed) and only **falls back to a YouTube rip** if no usable torrent is found. Real lossless comes from the torrent tier; YouTube is the lossy last resort. ## The ladder — how a grab flows 1. **Search** Prowlarr (`GET /api/v1/search`, categories 3000/3010/3040) for `" "`. Results are ranked **lossless-first** (FLAC/24-bit/etc. by title+category regex), then by seeders, then size. 2. **Grab** the best candidate via qBittorrent (`POST /api/v2/torrents/add`, category `harmony`, savepath `/media/downloads/harmony`). qBittorrent 5.x returns JSON with the infohash. 3. **Poll** to completion (`/torrents/info`), with stall / no-seeder / timeout detection; on failure it walks to the next candidate. Force-stop is honored at every stage. 4. **Import**: completed audio is **reflink-copied** (CoW, same ZFS dataset — never a hardlink, which would corrupt the seeding torrent) into the library, retagged with your overrides, **FLAC kept lossless (no transcode)**, and a `.harmony.json` provenance sidecar is written (`source: torrent`). The torrent is **left seeding**. Only torrents Harmony itself created are ever deleted (never another category's). 5. **Fallback**: no usable torrent → yt-dlp YouTube rip (existing behavior), sidecar `source: youtube`. - Config is env in the stack `compose.yaml`: `PROWLARR_URL/KEY`, `QBIT_URL/USER/PASS/CATEGORY`, `DOWNLOADS_DIR=/downloads` (mounted, same dataset as `/music`), `QBIT_DL_PREFIX`, `TORRENT_MIN_SEEDERS`, `TORRENT_TIMEOUT`. The Grab tab shows a live "ladder active/​not configured" banner (`GET /api/ladder/status`). - **Coverage caveat:** the torrent tier is only as good as the enabled Prowlarr indexers. As of 2026-07-07 those are Darkpeers + RetroToon (anime/retro) + **The Pirate Bay** (general, added for music). Mainstream albums the enabled trackers don't carry will fall through to YouTube — add a dedicated music tracker for broader FLAC coverage. The **real lossless path for Lidarr-managed artists is MusicSeerr → Lidarr**. ## Access - LAN: `http://192.168.50.109:8770` (login password lives in the stack `.env`) - Runs as uid/gid **568** (apps); writes are group-readable so Navidrome (also 568) can serve them. ## Where things live | Thing | Path | |---|---| | Stack (compose, Dockerfile, app) | `/mnt/Nightwing/stacks/harmony/` | | App code (bind-mounted, edit + `docker compose restart harmony`) | `/mnt/Nightwing/stacks/harmony/app/app.py` | | Secrets (`HARMONY_PASSWORD`, `SECRET_KEY`) | `/mnt/Nightwing/stacks/harmony/.env` | | Config/data (job history, temp) | `/mnt/Nightwing/configs/harmony/` | | Music library (shared, rw) | `/mnt/Nightwing/media/music/` → `/music` in container | | This doc | `/mnt/Nightwing/media/music/.harmony/DOCUMENTATION.md` | ## Features ### Grab tab A **segmented control** switches between two ways to add music (keeps the page uncluttered): **🔎 Search & grab** and **⬆ Upload ZIP**. Below them sit a slim **Amazon Music finder** (type any artist + album → opens `music.amazon.com/search/…` in a new tab) and the shared **jobs list**. *Search & grab:* enter **album artist + album title** (drives the FLAC-torrent search) and/or paste a **YouTube URL** (the last-resort rip). Optional overrides: year, genre, **Expected tracks**. **"Try a FLAC torrent first"** toggle (default on). - **Expected tracks**: if set, the ladder verifies each torrent's track count matches before importing — wrong-count releases (a single, a 2-album combo, a discography) are auto-skipped and the next candidate is tried. The **⬆ Upgrade to lossless** menu auto-uses the existing album's track count. YouTube grabs - **Expected tracks**: if set, the ladder verifies each torrent's track count matches before importing — wrong-count releases (a single, a 2-album combo, a discography) are auto-skipped and the next candidate is tried. The **⬆ Upgrade to lossless** button auto-uses the existing album's track count. YouTube grabs only warn on a mismatch (they're the last resort). `expected_tracks` in `POST /api/grab`. ### Upload an album (.zip) On the Grab tab: choose a **.zip** of an album (optional artist/album/year/genre/expected-tracks overrides) → **⬆ Upload & sort**. Harmony unpacks it (zip-slip-guarded), reads each file's tags, sorts into `/ ()/NN - Title.ext`, keeps **FLAC lossless (no transcode)**, copies a bundled cover to `folder.jpg`, writes a `source: upload` provenance sidecar, and Navidrome auto-scans. Runs as a background job (shows in the jobs list). Endpoint: `POST /api/upload` (multipart `zip=@file`). - **YouTube format** select: MP3 320 / Opus / M4A / **FLAC (container)** / **Best available**. ⚠️ FLAC/Best on the *YouTube* side is a lossless *container* around a ~128–160k stream — **no real fidelity gain**; true lossless comes from the torrent tier. (Honest hint shown in-UI.) - **Artist links pull the whole discography** from YouTube — each album sorted into its own folder. - Live per-track / per-torrent progress in each job's log; **■ Stop** force-stops a running job. - Scriptable: `POST /api/grab` (after `POST /login`) with `{artist,album,year,genre,url,format,prefer_torrent}`. ### Library tab Album grid → click an album to open the editor: - Edit album-level (album, album artist, year, genre) and per-track (track #, title, artist) tags. - **Multi-select**: tick per-track checkboxes (or the header "select all") → **Delete selected** / **Move selected** (batch). The advanced tree view has the same per-track checkboxes + batch bar. - **⬆ Upgrade to lossless ▾** (dropdown): two paths, both pass the album's track count as Expected tracks — **🔎 Search for a torrent** (re-runs the torrent-only ladder) or **⬆ Upload a ZIP** (drop in a lossless zip of this album). Either way the old copy is left in place; then use **⧉ Aggregate duplicates** to drop the lower-quality one (the new real-lossless out-ranks it). - Amazon Music lookup moved to the standalone **finder bar on the Grab tab** (declutters the editor). - Replace cover art (writes `folder.jpg` + embeds into every track). - Optional "rename folder & files to match" reorganizes on disk. - Tags are written format-agnostically (MP3/FLAC/M4A/Opus). Navidrome updates via its watcher. ### Provenance & the fake-FLAC guard Every Harmony-written album gets a `.harmony.json` sidecar recording `source` (`torrent`/`upload`/`youtube`/`mixed`). The **Aggregate duplicates** ranker uses it: - **torrent and upload are "verified real"** — a real-lossless keeper outranks a lossy copy; a **YouTube-sourced "FLAC" gets zero lossless credit** so it can never out-rank and trash a real file. - A **YouTube rip that's strictly lower quality than a real (torrent/upload) keeper is auto-removed even across different album folders** (no need to tick "include cross-album") — this is what collapses a ripped album once you've grabbed/uploaded the FLAC. - The keeper is the **best audio**; its **metadata is enriched** from the most-complete copy before the others are trashed (`_enrich_keeper`) — best quality *and* richest tags. - Still keeps a group **for review** (never auto-deletes) when the best copy is *unverified*-provenance lossless in a mixed-tier group, or when member durations diverge (likely different songs sharing a title). ### Appearance / Settings tab Flat, minimalist "Sonic Gray Pearl" look (no raised shadows), a subtle vinyl-shelf backdrop, no emoji. The **Settings** tab switches **Theme** (Sonic Gray Pearl / Graphite / Slate Blue / Onyx) and **Accent** (Blue-Yellow default, plus Teal-Amber, Indigo-Gold, Sky-Lime, Rose-Gold) — saved per-browser in localStorage, applied by overriding CSS variables on `:root`. All colors are theme variables. ### Stats tab Library totals (size, tracks, albums, artists, total play time), a drive used/free bar, top artists by track count and by size, genres by total time, biggest album, and format breakdown. Backed by `GET /api/stats` (45s cache). ### Delete / Force-stop / Aggregate duplicates / Advanced filesystem move - **Delete track** — 🗑 button on each row of the album editor; sends the file to Harmony's recoverable trash and auto-cleans an album/artist dir if it was the last track. (`POST /api/track/delete`) - **Force-stop** — a **■ Stop** button on any running/queued job cancels it mid-download; the yt-dlp progress hook raises `DownloadCancelled` and the job is marked `cancelled`. (`POST /api/jobs//cancel`) - **Aggregate duplicates** — the **⧉ Aggregate duplicates** button runs a library-wide de-dup that groups by (artist, base title, **version bucket**). Genuinely different versions each keep one copy — **a studio "Song" and a "Song (Live)" are BOTH kept** (different buckets, different keys); only same-version copies collapse to the best one (by bitrate → size → duration → cover → format). Preview-then-confirm modal; "include cross-album" toggle is off by default (cross-album dupes are shown but kept for review). Removed files go to recoverable trash. Apply is guarded: a group whose keeper vanished is skipped, and the keeper file itself is never removed — the tool can never leave zero copies. (`POST /api/dedup/preview`, `POST /api/dedup/apply` — needs `{confirm:true}`) - **Advanced toggle** — the **Advanced** checkbox swaps the album grid for a filesystem-like `artist → album → track` tree (`GET /api/tree`). Each track has a **Move…** button: pick an existing destination album by number, or type a new artist/album/year. The moved file is re-tagged (album/albumartist/year) to match its new home and the emptied source album is cleaned up. (`POST /api/track/move`) All destructive paths are traversal-safe: `safe_dir`/`_under` resolve `realpath` and reject anything outside `/music`. Deletions land in `/harmony/trash//` — recover by moving a file back into the library and letting Navidrome re-scan. ## Operations - **Update yt-dlp** (when YouTube changes break downloads): `cd /mnt/Nightwing/stacks/harmony && docker compose build --no-cache && docker compose up -d` - **Edit the app**: change `app/app.py`, then `docker compose restart harmony` (no rebuild — it's bind-mounted). - **Templates** are injected with `.replace('__CSS__'/'__ERROR__'/'__DEFAULT_FMT__', …)`, NOT `%`-format (the HTML/CSS contains literal `%`, which would break `%`-formatting). - **Theme**: gray-dominant with blue + yellow accents; "A Mark Into Design" cursive signature. ## Legal Harmony is a general downloader. Fine for Creative-Commons / public-domain / your-own content; downloading copyrighted material is the operator's responsibility.