Rewrite README in plainer language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b3e86b6013
commit
0664af5b8a
1 file changed
+161
-147
@@ -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 (`<slave>/.playlink-trash/<op-id>/…`). 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
|
||||
(`<slave>/.playlink-trash/<op-id>/…`). 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://<server>:7480`, create your account, and add a pair:
|
||||
Then open `http://<server>: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
|
||||
|
||||
Reference in new issue
Block a user