Initial commit: playlink music library hardlink deduplicator
Go backend (chi, SQLite) with an embedded React/TypeScript UI that finds songs present in both a host and a slave music library and replaces the slave copy with a hardlink to the host copy. - Path-first matching with format/title/album-folder fallbacks - Auto-linking of byte-identical files, re-verified with cmp before linking - Manual review with ffprobe quality comparison, downgrade guard and promote-to-host override - Build mode to populate an empty slave library from the host - Dry run by default, atomic stage+rename replacement, quarantine with retention, undo, path confinement and authenticated UI - Dockerfile and compose file for Unraid Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
ac47e61b08
42 files changed
+8892
No files matched your search
@@ -0,0 +1,57 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScoreOrdering(t *testing.T) {
|
||||
flac24 := Info{Codec: "flac", Lossless: true, BitDepth: 24, SampleRate: 96000}
|
||||
flac16 := Info{Codec: "flac", Lossless: true, BitDepth: 16, SampleRate: 44100}
|
||||
opus160 := Info{Codec: "opus", Bitrate: 160_000}
|
||||
mp3320 := Info{Codec: "mp3", Bitrate: 320_000}
|
||||
mp3128 := Info{Codec: "mp3", Bitrate: 128_000}
|
||||
order := []Info{flac24, flac16, mp3320, opus160, mp3128}
|
||||
for i := 1; i < len(order); i++ {
|
||||
if Score(order[i-1]) <= Score(order[i]) {
|
||||
t.Errorf("%v should outrank %v", order[i-1], order[i])
|
||||
}
|
||||
}
|
||||
if v := Compare(mp3128, flac16); v.Better != "slave" {
|
||||
t.Errorf("lossless slave should win, got %s", v.Better)
|
||||
}
|
||||
if v := Compare(mp3320, Info{Codec: "mp3", Bitrate: 310_000}); v.Better != "equal" {
|
||||
t.Errorf("within 5%% should be equal, got %s", v.Better)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeRealFiles(t *testing.T) {
|
||||
ffmpeg, err1 := exec.LookPath("ffmpeg")
|
||||
ffprobe, err2 := exec.LookPath("ffprobe")
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Skip("ffmpeg/ffprobe not installed")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
flac, mp3 := filepath.Join(dir, "a.flac"), filepath.Join(dir, "a.mp3")
|
||||
for _, args := range [][]string{
|
||||
{"-f", "lavfi", "-i", "sine=f=440:d=2", "-sample_fmt", "s16", flac},
|
||||
{"-f", "lavfi", "-i", "sine=f=440:d=2", "-b:a", "128k", mp3},
|
||||
} {
|
||||
if out, err := exec.Command(ffmpeg, append([]string{"-v", "error", "-y"}, args...)...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("ffmpeg: %v %s", err, out)
|
||||
}
|
||||
}
|
||||
p := Prober{Bin: ffprobe}
|
||||
fi, mi := p.Probe(context.Background(), flac), p.Probe(context.Background(), mp3)
|
||||
if fi.Error != "" || mi.Error != "" {
|
||||
t.Fatalf("probe errors: %q %q", fi.Error, mi.Error)
|
||||
}
|
||||
if !fi.Lossless || fi.BitDepth != 16 || mi.Lossless || mi.Codec != "mp3" {
|
||||
t.Fatalf("unexpected probe results %+v / %+v", fi, mi)
|
||||
}
|
||||
if v := Compare(mi, fi); v.Better != "slave" {
|
||||
t.Fatalf("flac slave should beat mp3 host, got %+v", v)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user