Add a setting to hide the quarantine from Plex

Settings -> Media servers -> "Hide quarantine from Plex" writes a
.plexignore into every pair's quarantine folder (.playlink-trash by
default), with patterns for every folder depth quarantined files live
at. Quarantine folders opened later get the file automatically.

The file carries a marker line: turning the option off removes only
playlink's own file, and an existing .plexignore written by someone else
is never overwritten or deleted (reported instead). Writes go through a
temporary file and a rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-28 21:57:56 -07:00
1 parent 42161e4287
commit 1da0132d5e
7 files changed
+238 -2

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
package engine
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/cdnutter/playlink/internal/store"
)
func TestPlexIgnoreToggle(t *testing.T) {
ev := setup(t, false)
q := ev.pair.QuarantineDir
ignore := filepath.Join(q, PlexIgnoreName)
if err := ev.st.SetSetting(ctx, "plex_ignore", "1"); err != nil {
t.Fatal(err)
}
res := ev.e.ApplyPlexIgnore(ctx, true)
if len(res) != 1 || res[0].Error != "" {
t.Fatalf("apply: %+v", res)
}
b, err := os.ReadFile(ignore)
if err != nil || !strings.HasPrefix(string(b), plexIgnoreMarker) || !strings.Contains(string(b), "\n*\n*/*\n") {
t.Fatalf("expected playlink's .plexignore, got %q (%v)", b, err)
}
// A quarantine created later (new pair) gets the file automatically.
other := store.Pair{Name: "other", MasterRoot: ev.master, SlaveRoot: ev.slave, QuarantineDir: filepath.Join(ev.root, "q2")}
if _, err := ev.e.NormalizePair(&other); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(ev.root, "q2", PlexIgnoreName)); err != nil {
t.Fatalf("new quarantine folder should get a .plexignore: %v", err)
}
// Turning it off removes playlink's file.
if err := ev.st.SetSetting(ctx, "plex_ignore", "0"); err != nil {
t.Fatal(err)
}
ev.e.ApplyPlexIgnore(ctx, false)
if _, err := os.Stat(ignore); !errors.Is(err, os.ErrNotExist) {
t.Fatal(".plexignore should be removed when the option is turned off")
}
}
func TestPlexIgnoreLeavesUserFileAlone(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, PlexIgnoreName)
os.WriteFile(p, []byte("my own rules\n"), 0o644)
if err := syncPlexIgnore(dir, true); !errors.Is(err, ErrForeignIgnore) {
t.Fatalf("want ErrForeignIgnore, got %v", err)
}
if err := syncPlexIgnore(dir, false); err != nil {
t.Fatal(err)
}
if b, _ := os.ReadFile(p); string(b) != "my own rules\n" {
t.Fatal("a user's .plexignore must never be changed or removed")
}
}
func TestPlexIgnoreCoversDeepQuarantinePaths(t *testing.T) {
lines := strings.Split(strings.TrimSpace(string(plexIgnoreContent())), "\n")
deepest := lines[len(lines)-1]
// e.g. 12/slave/Artist/Album/Disc 1/01 Song.flac is 6 levels deep.
if strings.Count(deepest, "*") < 6 {
t.Fatalf("patterns do not reach deep enough: %q", deepest)
}
if err := syncPlexIgnore(t.TempDir(), true); err != nil {
t.Fatal(err)
}
}