diff --git a/README.md b/README.md index f0eea6d..7ab8d1e 100644 --- a/README.md +++ b/README.md @@ -2,216 +2,230 @@ # Playlink -Deduplicate music libraries with hardlinks. Playlink finds songs that exist in both a **master** library (e.g. your full `Albums`) and a -**slave** library (e.g. a curated `Singles` library) and replaces the slave copy with a hardlink to the master copy. Each song then takes -up disk space only once, while both libraries keep looking exactly the same to Plex, Navidrome, Jellyfin and other media servers. +Playlink dedupes music libraries with hardlinks. You give it a master library (say, your full `Albums` folder) and a slave library +(say, a curated `Singles` folder). It finds songs that are in both and swaps the slave copy for a hardlink to the master copy. The song +takes up disk space once, and Plex, Navidrome, Jellyfin and friends still see both libraries exactly as before. -It ships as a single Go binary with an embedded React web UI and SQLite state, packaged as a small Docker image, in the style of +It's one Go binary with a React UI and SQLite baked in, shipped as a small Docker image. The setup is modeled on [qui](https://github.com/autobrr/qui). -## What it does +## How matching works -Files are matched by their **path relative to each library root**. Any folder layout works (`Artist/Album/Title.ext`, -`Genre/Artist/Title.ext`, flat, …) as long as both libraries use the same one, so that a song sits at the same relative -path on both sides. The fallbacks below catch small differences, and a scan warns when the two layouts don't line up. +Songs are matched by their path relative to each library root. The folder layout doesn't matter (`Artist/Album/Title.ext`, +`Genre/Artist/Title.ext`, flat) as long as both libraries use the same one. A few fallbacks catch small differences, and a scan warns +you if the two layouts don't line up. -| Situation | What Playlink does | -|---|---| -| Same relative path, **byte-identical** | Status `identical`. Link with one click, in bulk, or automatically after each scan. | -| Same path, **different bytes** (re-encode, different tags) | Status `different`. Manual review with a codec/bitrate/bit-depth/duration comparison. | -| Same title, **different format** (`Song.mp3` vs `Song.flac`) | Status `similar` (other format). Manual review. | -| Same title after ignoring track numbers/punctuation (`01 - Song!.flac` vs `Song.mp3`) | Status `similar` (similar title). Manual review. | -| Same top-level folder and title, but a **different subfolder** | Status `similar` (same top folder, other subfolder). Manual review. | -| *(opt-in)* Approximately the same song in another folder, e.g. a **single vs. the album version** or a **disc folder** | Status `similar` (approximate match), with the reason shown. Manual review. | -| Only exists in the slave library | Status `unmatched`. Never touched. | -| Already the same inode | Status `linked`. | +| Situation | Status | What happens | +|---|---|---| +| Same path, byte-identical | `identical` | Link it with one click, in bulk, or automatically after each scan. | +| Same path, different bytes (re-encode, different tags) | `different` | You review it, with codec, bitrate, bit depth and duration side by side. | +| Same title, different format (`Song.mp3` vs `Song.flac`) | `similar` (other format) | You review it. | +| Same title once track numbers and punctuation are ignored (`01 - Song!.flac` vs `Song.mp3`) | `similar` (similar title) | You review it. | +| Same top-level folder and title, different subfolder | `similar` (same top folder, other subfolder) | You review it. | +| Close match somewhere else, like a single vs. the album version or a disc folder (opt-in) | `similar` (approximate match) | You review it, and the reason is shown. | +| Only in the slave library | `unmatched` | Left alone. | +| Already the same inode | `linked` | Nothing to do. | -**Approximate matching** is an opt-in setting on each pair. It finds songs whose file name or folder doesn't line up exactly with the -master library and recommends linking them to the master's album version: +### Approximate matching + +This is off by default and turned on per pair. It catches songs whose file name or folder doesn't quite line up with the master, and +suggests linking them to the master's album version: | Case | Slave | Master | |---|---|---| -| Single → album version | `Avenged Sevenfold/Hail to the King - Single/Hail to the King.mp3` | `Avenged Sevenfold/Hail to the King/01 Hail to the King.flac` | +| Single to album version | `Avenged Sevenfold/Hail to the King - Single/Hail to the King.mp3` | `Avenged Sevenfold/Hail to the King/01 Hail to the King.flac` | | Disc folders ignored | `Avenged Sevenfold/Nightmare/04 Buried Alive.mp3` | `Avenged Sevenfold/Nightmare/Disc 1/04 Buried Alive.flac` | | Small title differences | `Avenged Sevenfold/City of Evil/Bat Country (Remastered).mp3` | `Avenged Sevenfold/City of Evil/03 Bat Country.flac` | -It searches the same album first (disc folders ignored), then the artist's other albums, then, as a last resort, the whole master -library. Titles are compared by similarity against a threshold you choose (85% by default), and release labels such as *Single -Version*, *Remastered* or *Explicit* are ignored. When a song exists on several releases, the studio album wins over singles, EPs -and compilations, even if the master library also has the single. Linking such a match places the file at the master's path in the -slave library (the emptied single folder is removed), or you can keep it in the slave's current folder. +It looks in the same album first (ignoring disc folders), then the artist's other albums, and only then the whole master library. +Titles are compared by similarity against a threshold you set (85% by default), and labels like *Single Version*, *Remastered* and +*Explicit* are ignored. If a song is on several releases, the studio album wins over singles, EPs and compilations, even when the master +also has the single. When you link one of these, the file goes to the master's path in the slave library and the emptied single folder +is removed. You can also keep it where it is. -Safeguards: +What it won't do: -- Different recordings never match. *Live*, *Remix*, *Acoustic*, *Instrumental* and similar versions are kept apart, including - whole live or acoustic albums. -- Matches found outside the artist's folder require both files' artist tags to agree. -- Ambiguous candidates are skipped rather than guessed. -- Approximate matches are never linked automatically. +- Match different recordings. *Live*, *Remix*, *Acoustic*, *Instrumental* and similar versions stay separate, including whole live or + acoustic albums. +- Match outside the artist's folder unless both files' artist tags agree. +- Guess. Ambiguous candidates are skipped. +- Link approximate matches automatically. -**Manual review** shows both files side by side and ranks their quality: lossless beats lossy, then bit depth and sample rate, then -codec-weighted bitrate (Opus and AAC count for more per kbps than MP3). You then have three choices: +### Reviewing matches -- **Upgrade / link to master copy.** The default. When the master copy is better, the match is flagged as an **upgrade** and listed - under the *Upgrades* filter. The master is kept because it belongs to a complete album. If the master is *lower* quality, Playlink - warns you and asks for explicit confirmation. -- **Use slave copy as master.** The override. The slave's better file replaces the master file, the old master file is quarantined, and both - libraries share the better file. -- **Ignore.** The pair is hidden from review until you un-ignore it. +Review shows both files side by side and ranks them: lossless beats lossy, then bit depth and sample rate, then bitrate weighted by +codec (Opus and AAC get more credit per kbps than MP3). You've got three options: -**Rename folders to match (recommended first step).** When a scan finds slave folders that are the same as a master folder with a -different spelling (`City Of Evil` vs `City of Evil`, `AC-DC` vs `AC_DC`, look-alike hyphens), the warning offers to rename the slave -folders to the master's spelling so their songs match by path. Do this right after a scan, before reviewing matches. A preview lists -every rename with the differences highlighted; conflicts (the slave already has a folder with the master's name) are skipped with the -reason. Only slave folders are renamed, nested folders revealed by a rename are handled in the same run, every rename appears in -History and can be undone, and the library is rescanned afterwards. +- Upgrade / link to master copy. This is the default. If the master is better, the match is flagged as an upgrade and shows up under the + Upgrades filter. The master wins because it's part of a full album. If the master is worse, Playlink warns you and asks you to confirm. +- Use slave copy as master. The slave's better file replaces the master, the old master goes to quarantine, and both libraries share the + better file. +- Ignore. Hides the pair until you un-ignore it. -**Bulk review.** Switch the Review list from *Songs* to *Albums* to see one row per album (disc folders are folded in), with -counts of upgrades, identical files, songs where the master is lower quality and approximate matches. Tick whole albums, or expand an -album and tick individual songs, then **Link**, **Use slave copies as master**, **Ignore** or **Un-ignore** them in one go. Before -anything runs, a preview shows how many songs will be processed and which are skipped and why, with warnings and examples for -downgrades, approximate matches, duration mismatches and files that move folders. Downgrades are skipped unless you tick a box, and -replacing master files needs an explicit acknowledgement. Each song then goes through the same checks as a single action. +Before reviewing anything, fix folder spelling. If a scan finds slave folders that match a master folder except for spelling +(`City Of Evil` vs `City of Evil`, `AC-DC` vs `AC_DC`, look-alike hyphens), the warning offers to rename the slave folders to match so +their songs line up by path. You get a preview of every rename with the differences highlighted. Conflicts, where the slave already has a +folder with the master's name, are skipped and explained. Only slave folders get renamed. Nested folders that turn up after a rename +are handled in the same run. Every rename lands in History and can be undone, and the library is rescanned afterwards. -**Export.** The Review tab's **Export CSV** button downloads either the current view (filter and search applied) or the entire scan. -Each row has the match status and type, a recommendation, both paths, where linking would place the file, codec, bitrate, sample rate, -bit depth and duration for both sides, the quality verdict and any notes. The file opens cleanly in Excel, Numbers and LibreOffice -(UTF-8, and cells a spreadsheet would treat as formulas are escaped). +To review in bulk, switch the Review list from Songs to Albums. You get one row per album (disc folders folded in) with counts of +upgrades, identical files, songs where the master is worse, and approximate matches. Tick whole albums or expand one and pick songs, +then Link, Use slave copies as master, Ignore or Un-ignore them all at once. Before anything runs you see how many songs will be +processed, which get skipped and why, and examples of downgrades, approximate matches, duration mismatches and files that would move +folders. Downgrades are skipped unless you tick a box, and replacing master files needs an extra confirmation. Each song still goes +through the same checks as a single action. -**Duplicates within the slave library.** The Review tab's **Duplicates** filter is separate from master/slave matching: it looks -only inside the slave library for songs filed more than once, such as a single next to its album version, a compilation track, or one -song in two formats. **Find duplicates** groups songs in the same top-level folder by title, using the same rules as approximate matching -(track numbers, disc folders and release labels such as *Single Version* or *Remastered* are ignored; *Live*, *Remix*, *Acoustic* and -similar versions are kept apart). With ffprobe, copies whose artist tags disagree are kept apart too, and each copy shows its quality and -length, with a warning when lengths differ by more than 10 seconds. Each song has a play button to compare the copies. One copy is -recommended to keep (the album version, then one already linked to the master, then the better quality) and the others start selected. -**Remove** moves the selected copies into the quarantine after re-checking that each is unchanged and that another copy is still there; -one copy of every song is always kept, emptied folders are removed, dry run applies, and every removal can be restored from History. +Export CSV on the Review tab downloads either the current view (with your filter and search) or the whole scan. Each row has the status +and match type, a recommendation, both paths, where linking would put the file, codec, bitrate, sample rate, bit depth and duration for +both sides, the quality verdict and any notes. It's UTF-8 and escapes anything a spreadsheet would read as a formula, so it opens fine in +Excel, Numbers and LibreOffice. -**Listening in build mode.** Every song in the Build tab has a play button. A mini-player along the bottom of the page plays the -folder in order, with seeking, previous/next, volume and space to pause. Songs are streamed from the master library as they are -(nothing is converted), so they play when the browser supports the format: FLAC, MP3, AAC/M4A, Ogg/Opus and WAV in current browsers, -while formats such as WMA or APE show a message instead. +### Duplicates inside the slave library -**Folders without music** (the *Cleanup* tab). Choose the slave or master library and **Find folders**: Playlink lists the top-most -folders with no music file anywhere inside them (music = the pair's file extensions), such as album folders left empty after linking or -ones holding only a cover or playlist, with what they still contain. Empty folders are pre-selected, folders that still hold files are -not. **Remove** asks for confirmation (and an extra acknowledgement when the folders still hold files), re-checks each folder for music -right before touching it, and moves it into the quarantine instead of deleting it: it can be restored from History until the retention -period purges it. The library root, the quarantine and hidden folders are never listed, and dry run applies. +The Duplicates filter on the Review tab has nothing to do with master/slave matching. It looks only inside the slave library for songs +filed more than once: a single next to its album version, a compilation track, the same song in two formats. Find duplicates groups songs +in the same top-level folder by title, using the same rules as approximate matching (track numbers, disc folders and labels like *Single +Version* or *Remastered* ignored; *Live*, *Remix*, *Acoustic* and the like kept apart). With ffprobe installed, copies with different +artist tags are kept apart too, and each copy shows its quality and length, with a warning if lengths differ by more than 10 seconds. +Every song has a play button so you can compare. -**Automatic cleanup** (per pair, off by default) runs the same search on the slave library after every scan and removes only folders -with nothing at all inside them. A folder holding any file, even a cover or a `.DS_Store`, is left for you in Cleanup, and folders -changed within the settle time are skipped. Removals go to the quarantine and History like manual ones; in dry run nothing is removed. +Playlink suggests one copy to keep (the album version first, then one already linked to the master, then the better quality) and +pre-selects the rest. Remove re-checks that each selected copy is unchanged and that another copy still exists, then moves it to +quarantine. One copy of every song is always kept, emptied folders are removed, dry run applies, and every removal can be restored from +History. -**Notifications** (Settings → Notifications) send a message to Discord, Pushover, ntfy, Gotify or any JSON webhook when a scan finds -songs that newly need review, when automatic linking replaced identical files, and when an automatic cleanup removed empty folders. -Each event can be turned off, and each service has a **Test** button. Set the optional Playlink address to get a link back to the -right page in every message. +### Build mode -**Backups** (Settings → Backups), similar to Sonarr. A backup is a zip of Playlink's database (settings, library pairs, scan results and -history, not music files) taken safely while the app runs. Choose a folder (default `/config/backups`), a schedule (off, every 1, 3, 7 -or 14 days) and a retention period for scheduled backups; **Back up now** makes a manual one (kept until deleted). Backups can be -downloaded, deleted, or restored, also from an uploaded zip. A restore is validated first (damaged backups and ones from a newer version -are refused), then playlink restarts to swap the database in and keeps the replaced one as `playlink.db.pre-restore-…`; Docker's restart -policy brings it back automatically. Optionally, **back up before every action in live mode**: one backup per action you start (and -before scans change files), never one per file; if that backup fails the action does not run, and the newest 10 are kept. +Build mode is for starting fresh. Point the slave at an empty folder, browse the master library, tick the folders or songs you want, and +Playlink hardlinks them in at the same relative path. Remove deletes slave files, but only ones that are hardlinks of the master file. -**Build mode** covers the "start fresh" workflow. Point the slave at an empty folder, browse the master library, tick the -folders or songs you want, and Playlink hardlinks them in at the same relative path. **Remove** deletes slave files, but only ones that are hardlinks of the -master file. +Every song in the Build tab has a play button. A player along the bottom plays the folder in order, with seeking, previous/next, volume, +and space to pause. Files are streamed from the master library as-is with no transcoding, so they play if your browser supports the +format. Current browsers handle FLAC, MP3, AAC/M4A, Ogg/Opus and WAV. Things like WMA or APE show a message instead. -## Safety model +### Cleaning up empty folders -Playlink is built so that no user data is ever lost, even if it crashes or loses power mid-operation: +On the Cleanup tab, pick the slave or master library and hit Find folders. Playlink lists the top-most folders that have no music +anywhere inside them (music meaning the pair's file extensions), like album folders left empty after linking or ones holding only a cover +or a playlist, along with whatever they still contain. Truly empty folders are pre-selected, the others aren't. Remove asks you to +confirm (and confirm again if the folders still hold files), re-checks each one for music right before touching it, and moves it to +quarantine instead of deleting it. You can restore it from History until the retention period purges it. The library root, the +quarantine and hidden folders never show up here, and dry run applies. -- **Dry run is on by default.** Every action is recorded in Activity as *planned* until you switch to live mode. -- **Files are re-verified before any change.** Both files must still be plain regular files with the same inode, size and mtime that - the scan recorded, or the action is refused ("changed since scan"). -- **Identical files are compared again right before linking.** The check is a full byte-for-byte compare, like `cmp`, and it also - detects files that change while being read. -- **Replacements are atomic.** The new link is staged next to the target (`.playlink-tmp-*`), checked by inode, and then `rename(2)`d - over the target. The path always points at a complete file. -- **Nothing non-identical is ever deleted directly.** Before a non-identical file is replaced, a hardlink to it is created in the - quarantine (`/.playlink-trash//…`). It can be restored with one click until the retention period (default 30 days) - expires, or until you purge it. -- **Every operation can be undone.** An identical link is undone by writing an independent, verified copy back. A quarantined file is - undone by restoring the original. -- **Removal is limited to hardlinks.** Build/Remove only deletes a slave file that shares an inode with the master file, so the last - copy of any data is never deleted. -- **Paths are confined.** Libraries and the quarantine must be inside `PLAYLINK_ALLOWED_ROOTS`. Symlinks are never followed or - replaced, `..` is rejected, and master and slave may not overlap. -- **Mutations are serialized.** Only one filesystem mutation runs at a time, and only one job runs per pair. -- **Auto-link is conservative.** It only acts on files with the *exact* same path, and it skips files modified within the settle time - (default 10 minutes) so it never touches a download in progress. Automatic cleanup likewise only removes slave folders that are - completely empty and unchanged for the settle time, and never touches the master library. -- **Interrupted operations are cleaned up.** Operations cut short by a crash are marked *interrupted* on startup, and their staging - files are removed. -- **The web UI requires login.** Passwords are hashed with bcrypt, sessions use `SameSite=Strict` HttpOnly cookies, and state-changing - requests need a custom request header and a same-origin check. Logins are rate-limited, and a strict CSP is sent. +Automatic cleanup is a per-pair setting, off by default. It runs the same search on the slave library after every scan, but only +removes folders with nothing at all in them. A folder with any file, even a cover or a `.DS_Store`, is left for you to deal with in +Cleanup, and folders changed within the settle time are skipped. Removals go to quarantine and History like manual ones. In dry run, +nothing is removed. -## Quick start (Docker / Unraid) +### Notifications -`docker-compose.yml` takes every host-specific value from a `.env` file next to it, so the compose file itself never needs editing. That -suits [Arcane](https://github.com/ofkm/arcane): point a project's Git Sync at this repository and set the values in Arcane's `.env` editor; -Arcane builds the image from the `Dockerfile`. With plain Compose: +Settings → Notifications can send a message to Discord, Pushover, ntfy, Gotify or any JSON webhook when a scan finds new songs to +review, when auto-link replaced identical files, or when automatic cleanup removed empty folders. Each event can be turned off, and each +service has a Test button. If you fill in Playlink's address, every message links back to the right page. + +### Backups + +Settings → Backups works a lot like Sonarr's. A backup is a zip of Playlink's database (settings, library pairs, scan results and +history, not your music), taken safely while the app is running. You choose a folder (default `/config/backups`), a schedule (off, or +every 1, 3, 7 or 14 days), and how long to keep scheduled backups. Back up now makes a manual one that's kept until you delete it. + +Backups can be downloaded, deleted or restored, including from an uploaded zip. A restore is checked first, and damaged backups or ones +from a newer version are refused. Playlink then restarts to swap the database in and keeps the old one as `playlink.db.pre-restore-…`. +Docker's restart policy brings it back up on its own. + +You can also have it back up before every action in live mode. That's one backup per action you start (and before scans change files), +not one per file. If the backup fails, the action doesn't run. The newest 10 are kept. + +## Safety + +The goal is that nothing gets lost, even if Playlink crashes or the power goes out mid-operation. + +- Dry run is on by default. Every action shows up in Activity as *planned* until you switch to live mode. +- Files are re-checked before any change. Both must still be regular files with the same inode, size and mtime the scan saw, or the + action is refused ("changed since scan"). +- Identical files get a full byte-for-byte compare (like `cmp`) right before linking, which also catches files that change while being + read. +- Replacements are atomic. The new link is staged next to the target (`.playlink-tmp-*`), checked by inode, then `rename(2)`d over the + target, so the path always points at a complete file. +- Nothing non-identical is deleted outright. Before a non-identical file is replaced, it's hardlinked into the quarantine + (`/.playlink-trash//…`). You can restore it with one click until the retention period runs out (30 days by default) or + you purge it. +- Everything can be undone. Undoing an identical link writes a separate, verified copy back. Undoing a quarantined file restores the + original. +- Build mode's Remove only deletes a slave file that shares an inode with the master file, so it can never delete the last copy of + anything. +- Paths are locked down. Libraries and the quarantine have to be inside `PLAYLINK_ALLOWED_ROOTS`. Symlinks are never followed or + replaced, `..` is rejected, and master and slave can't overlap. +- Only one filesystem change runs at a time, and only one job per pair. +- Auto-link only touches files with the exact same path, and skips anything modified within the settle time (10 minutes by default) so + it won't grab a download in progress. Automatic cleanup only removes slave folders that are completely empty and haven't changed within + the settle time, and never touches the master library. +- If Playlink crashes mid-operation, that operation is marked *interrupted* on the next start and its staging files are cleaned up. +- The web UI needs a login. Passwords are bcrypt-hashed, sessions use `SameSite=Strict` HttpOnly cookies, and anything that changes + state needs a custom request header and a same-origin check. Logins are rate-limited and a strict CSP is sent. + +## Running it (Docker / Unraid) + +`docker-compose.yml` pulls every host-specific value from a `.env` file next to it, so you never have to edit the compose file. That +works well with [Arcane](https://github.com/ofkm/arcane): point a project's Git Sync at this repo, fill in the values in Arcane's `.env` +editor, and Arcane builds the image from the `Dockerfile`. With plain Compose: ```sh cp .env.example .env # then edit it docker compose up -d --build ``` -The main settings (all listed with defaults in `.env.example`): +The main settings (all of them, with defaults, are in `.env.example`): -| Variable | Default | Meaning | +| Variable | Default | What it's for | |---|---|---| -| `PLAYLINK_DATA_PATH` | `/mnt/user/Sapphire` | Host folder containing **both** libraries, mounted at `/data`. | -| `PLAYLINK_CONFIG_PATH` | `/mnt/user/Aqua/playlink` | Host folder for the database and backups, mounted at `/config`. | -| `PUID` / `PGID` | `99` / `100` | User and group that own your music (Unraid `nobody:users`). | -| `PLAYLINK_PORT` | `7480` | Host port of the web UI. | +| `PLAYLINK_DATA_PATH` | `/mnt/user/Sapphire` | Host folder that contains both libraries. Mounted at `/data`. | +| `PLAYLINK_CONFIG_PATH` | `/mnt/user/Aqua/playlink` | Host folder for the database and backups. Mounted at `/config`. | +| `PUID` / `PGID` | `99` / `100` | User and group that own your music (Unraid's `nobody:users`). | +| `PLAYLINK_PORT` | `7480` | Host port for the web UI. | | `PLAYLINK_NETWORK` | `chromanet` | Existing Docker network to join. | | `TZ` | `America/Los_Angeles` | Time zone for logs and backup schedules. | -| `PLAYLINK_SECURE_COOKIES` | `false` | `true` behind HTTPS. | +| `PLAYLINK_SECURE_COOKIES` | `false` | Set to `true` behind HTTPS. | -`.env` is ignored by Git, so your settings stay local while the repository syncs. +`.env` is git-ignored, so your settings stay local while the repo syncs. -Then open `http://:7480`, create your account, and add a pair: +Then open `http://:7480`, create your account and add a pair, for example: - Master: `/data/Music/Albums` - Slave: `/data/Music/Instances/cdnutter/Singles` -Run a scan, check the Review tab, and turn off dry run once the planned actions in Activity look right. +Run a scan, look over the Review tab, and turn off dry run once the planned actions in Activity look right. -### Hardlink requirements +### Hardlink gotchas -- **Both libraries must be visible through one bind mount.** Docker bind mounts are separate mount points, and `link(2)` fails across - them (`EXDEV`) even when they are on the same disk. Mount a common parent folder, as shown above. -- **Both libraries must be on the same filesystem.** Playlink checks this when you save a pair and before every operation. -- **Unraid:** hardlinks through `/mnt/user` require *Settings → Global Share Settings → Tunable (support Hard Links)* to be *Yes* - (the default). If the share uses a cache pool, make sure your mover preserves hardlinks. Every link is verified by inode, and a - failed link aborts safely with the original untouched. +- Both libraries have to be visible through one bind mount. Docker bind mounts are separate mount points, and `link(2)` fails across + them (`EXDEV`) even on the same disk. Mount a shared parent folder like the example above. +- Both libraries have to be on the same filesystem. Playlink checks this when you save a pair and before every operation. +- On Unraid, hardlinks through `/mnt/user` need Settings → Global Share Settings → Tunable (support Hard Links) set to Yes, which is + the default. If the share uses a cache pool, make sure your mover keeps hardlinks intact. Every link is verified by inode, and if one + fails the original is left alone. ## Configuration -| Variable | Default | Description | +| Variable | Default | What it's for | |---|---|---| | `PLAYLINK_ADDR` | `:7480` | Listen address. | | `PLAYLINK_DATA_DIR` | `/config` (Docker) or `./config` | Where the SQLite database lives. | -| `PLAYLINK_ALLOWED_ROOTS` | `/data` | Comma-separated folders that libraries and the quarantine must live in. `/` is refused. | +| `PLAYLINK_ALLOWED_ROOTS` | `/data` | Comma-separated folders that libraries and the quarantine must be inside. `/` isn't allowed. | | `PLAYLINK_SECURE_COOKIES` | `false` | Set to `true` when served over HTTPS. | -| `PLAYLINK_FFPROBE` | `ffprobe` | Path to ffprobe. Without it, quality comparison is disabled but everything else works. | +| `PLAYLINK_FFPROBE` | `ffprobe` | Path to ffprobe. Without it you lose quality comparison, but everything else still works. | | `PLAYLINK_LOG_LEVEL` | `info` | `debug` for verbose logs. | -**Hide quarantine from Plex** (Settings → Media servers) writes a `.plexignore` into each pair's quarantine folder so Plex does not -pick up quarantined songs; new quarantine folders get it automatically. Turning it off removes the file again, but only if Playlink -created it. +Settings → Media servers has a Hide quarantine from Plex option. It writes a `.plexignore` into each pair's quarantine folder so Plex +doesn't pick up quarantined songs, and new quarantine folders get one automatically. Turning it off removes the file, but only if +Playlink created it. -Per-pair settings (in the UI) are file extensions, auto-link, automatic cleanup, approximate matching and its threshold, scan interval, +Per-pair settings live in the UI: file extensions, auto-link, automatic cleanup, approximate matching and its threshold, scan interval, quarantine folder, retention and settle time. ## Development -Requires Go 1.27+ and Node 22+. +You'll need Go 1.27+ and Node 22+. ```sh make test # go vet, go test -race, tsc