Notifications, and automatic cleanup of empty folders

Settings → Notifications sends a message to Discord, Pushover, ntfy,
Gotify or a JSON webhook when a scan finds songs that newly need
review, when auto-link replaced identical files, and when an automatic
cleanup ran. Each event can be turned off and each service tested; an
optional Playlink address adds a link back to the right page.

A new per-pair option removes empty slave folders after each scan. Only
folders with no file at all inside are removed (into the quarantine,
recorded in History); folders holding any file wait in Cleanup, folders
changed within the settle time are skipped, and dry run removes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-10-08 15:36:04 -07:00
1 parent 590b8abe08
commit faebc5dfb0
17 files changed
+1086 -26

No files matched your search

+36
View File
@@ -14,6 +14,7 @@ import (
"time"
"github.com/cdnutter/playlink/internal/fsops"
"github.com/cdnutter/playlink/internal/notify"
"github.com/cdnutter/playlink/internal/probe"
"github.com/cdnutter/playlink/internal/store"
)
@@ -42,6 +43,7 @@ type Engine struct {
mu sync.Mutex
beforeAction func(ctx context.Context, reason string) error
notify func(ctx context.Context, ev notify.Event)
emptyMu sync.Mutex
empty map[int64]*NoMusicScan // latest scan for folders without music, per pair
@@ -366,6 +368,40 @@ func (e *Engine) SetBeforeAction(f func(ctx context.Context, reason string) erro
e.beforeAction = f
}
// SetNotify installs the hook that tells the user about things that
// happened without them (see the notify package).
func (e *Engine) SetNotify(f func(ctx context.Context, ev notify.Event)) {
e.notify = f
}
func (e *Engine) emit(ctx context.Context, l *libs, ev notify.Event) {
if e.notify == nil {
return
}
ev.PairID, ev.Pair = l.pair.ID, l.pair.Name
e.notify(ctx, ev)
}
func plural(n int, one, many string) string {
if n == 1 {
return "1 " + one
}
return fmt.Sprintf("%d %s", n, many)
}
func formatBytes(n int64) string {
const unit = 1024
if n < unit {
return fmt.Sprintf("%d B", n)
}
div, exp := int64(unit), 0
for m := n / unit; m >= unit; m /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp])
}
// BeforeAction runs the pre-action backup, once per user action (not per
// file). It does nothing in dry run, where no files change. An error means
// the action must not run.