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:
1 parent
42161e4287
commit
1da0132d5e
7 files changed
+238
-2
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cdnutter/playlink/internal/fsops"
|
||||
)
|
||||
|
||||
// PlexIgnoreName is the file Plex reads to skip content in a folder.
|
||||
const PlexIgnoreName = ".plexignore"
|
||||
|
||||
// plexIgnoreMarker identifies a .plexignore written by playlink, so only
|
||||
// that file is ever replaced or removed; a user's own file is left alone.
|
||||
const plexIgnoreMarker = "# Created by playlink"
|
||||
|
||||
// plexIgnoreDepth covers quarantine paths like <op>/<side>/Artist/Album/Disc/Song.
|
||||
const plexIgnoreDepth = 12
|
||||
|
||||
func plexIgnoreContent() []byte {
|
||||
var b strings.Builder
|
||||
b.WriteString(plexIgnoreMarker + ": keeps Plex from scanning files in playlink's quarantine.\n")
|
||||
b.WriteString("# Turn off \"Hide quarantine from Plex\" in playlink's settings to remove it.\n")
|
||||
for i := 1; i <= plexIgnoreDepth; i++ {
|
||||
b.WriteString(strings.TrimSuffix(strings.Repeat("*/", i), "/") + "\n")
|
||||
}
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// ErrForeignIgnore means a .plexignore exists that playlink did not write.
|
||||
var ErrForeignIgnore = errors.New("a .plexignore not created by playlink already exists; it was left unchanged")
|
||||
|
||||
// syncPlexIgnore makes the quarantine folder's .plexignore match the setting.
|
||||
func syncPlexIgnore(quarantine string, enabled bool) error {
|
||||
p := filepath.Join(quarantine, PlexIgnoreName)
|
||||
cur, err := os.ReadFile(p)
|
||||
exists := err == nil
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
ours := exists && bytes.HasPrefix(cur, []byte(plexIgnoreMarker))
|
||||
want := plexIgnoreContent()
|
||||
switch {
|
||||
case enabled && exists && !ours:
|
||||
return ErrForeignIgnore
|
||||
case enabled && ours && bytes.Equal(cur, want):
|
||||
return nil
|
||||
case enabled:
|
||||
// Write via a temporary file so Plex never sees a half-written file.
|
||||
tmp := filepath.Join(quarantine, fsops.TmpPrefix+"plexignore")
|
||||
if err := os.WriteFile(tmp, want, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, p); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case ours:
|
||||
return os.Remove(p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IgnoreResult reports what happened to one pair's quarantine folder.
|
||||
type IgnoreResult struct {
|
||||
Pair string `json:"pair"`
|
||||
Path string `json:"path"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ApplyPlexIgnore adds or removes the .plexignore in every pair's quarantine.
|
||||
func (e *Engine) ApplyPlexIgnore(ctx context.Context, enabled bool) []IgnoreResult {
|
||||
pairs, err := e.st.Pairs(ctx)
|
||||
if err != nil {
|
||||
return []IgnoreResult{{Error: err.Error()}}
|
||||
}
|
||||
out := []IgnoreResult{}
|
||||
for _, p := range pairs {
|
||||
r := IgnoreResult{Pair: p.Name, Path: filepath.Join(p.QuarantineDir, PlexIgnoreName)}
|
||||
// open re-validates the pair and syncs the file as a side effect.
|
||||
l, err := e.open(p)
|
||||
if err == nil {
|
||||
err = syncPlexIgnore(l.quarantine, enabled)
|
||||
}
|
||||
if err != nil {
|
||||
r.Error = err.Error()
|
||||
e.log.Warn(".plexignore not updated", "pair", p.Name, "err", err)
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
e.log.Info(".plexignore setting applied", "enabled", enabled, "pairs", len(pairs))
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Engine) plexIgnoreEnabled() bool {
|
||||
return e.st.Setting(context.Background(), "plex_ignore", "0") == "1"
|
||||
}
|
||||
|
||||
func plexIgnoreError(quarantine string, err error) error {
|
||||
return fmt.Errorf("%s: %w", filepath.Join(quarantine, PlexIgnoreName), err)
|
||||
}
|
||||
Reference in new issue
Block a user