Library: filter by file properties (quality, codec, HDR, DV, language)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:22:06 +02:00
co-authored by Claude Sonnet 5.5
parent a7d74e2097
commit b34f88ef3e
12 changed files with 564 additions and 45 deletions
+65 -1
View File
@@ -4,6 +4,7 @@ import (
"strings"
"testing"
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
"git.stinnesbeck.com/nils/arr-tui/internal/radarr"
"git.stinnesbeck.com/nils/arr-tui/internal/sonarr"
)
@@ -49,7 +50,7 @@ func TestFilterItems(t *testing.T) {
}
ids := func(f int, text string) string {
var s string
for _, it := range filterItems(items, f, text) {
for _, it := range filterItems(items, f, text, attrFilter{}) {
s += itoa(it.id)
}
return s
@@ -90,3 +91,66 @@ func TestEpisodeStatus(t *testing.T) {
}
}
}
func TestAttrFilter(t *testing.T) {
uhd := arr.FileAttrs{Resolution: 2160, Codec: arr.CodecX265, HDR: true, DV: true, Languages: []string{"English", "German"}}
hd := arr.FileAttrs{Resolution: 1080, Codec: arr.CodecX264, Languages: []string{"French"}}
sd := arr.FileAttrs{Resolution: 480}
for _, tc := range []struct {
name string
f attrFilter
files []arr.FileAttrs
want bool
}{
{"inactive matches any file", attrFilter{}, []arr.FileAttrs{hd}, true},
{"no files never matches an active filter", attrFilter{res: "1080p"}, nil, false},
{"resolution", attrFilter{res: "1080p"}, []arr.FileAttrs{hd}, true},
{"resolution mismatch", attrFilter{res: "2160p"}, []arr.FileAttrs{hd}, false},
{"SD", attrFilter{res: "SD"}, []arr.FileAttrs{sd}, true},
{"SD excludes HD", attrFilter{res: "SD"}, []arr.FileAttrs{hd}, false},
{"codec", attrFilter{codec: "x265"}, []arr.FileAttrs{uhd}, true},
{"hdr includes DV+HDR", attrFilter{hdr: "HDR"}, []arr.FileAttrs{uhd}, true},
{"dv", attrFilter{hdr: "DV"}, []arr.FileAttrs{hd}, false},
{"sdr", attrFilter{hdr: "SDR"}, []arr.FileAttrs{hd}, true},
{"sdr excludes hdr", attrFilter{hdr: "SDR"}, []arr.FileAttrs{uhd}, false},
{"language is case-insensitive", attrFilter{lang: "german"}, []arr.FileAttrs{uhd}, true},
{"all criteria must match one file", attrFilter{res: "1080p", codec: "x265"}, []arr.FileAttrs{hd, uhd}, false},
{"any file of a series", attrFilter{res: "1080p", codec: "x264"}, []arr.FileAttrs{uhd, hd}, true},
} {
if got := tc.f.match(tc.files); got != tc.want {
t.Errorf("%s: match = %v, want %v", tc.name, got, tc.want)
}
}
if s := (attrFilter{res: "1080p", hdr: "DV"}).String(); s != "1080p DV" {
t.Errorf("String = %q", s)
}
}
func TestFilterItemsByAttrs(t *testing.T) {
items := []libItem{
{id: 1, title: "A", attrs: []arr.FileAttrs{{Resolution: 1080, Codec: arr.CodecX265}}, attrsKnown: true},
{id: 2, title: "B", attrs: []arr.FileAttrs{{Resolution: 1080, Codec: arr.CodecX264}}, attrsKnown: true},
{id: 3, title: "C"}, // series whose files were not read yet
}
got := filterItems(items, 0, "", attrFilter{codec: arr.CodecX265})
if len(got) != 1 || got[0].id != 1 {
t.Errorf("got %+v", got)
}
if got := filterItems(items, 0, "", attrFilter{}); len(got) != 3 {
t.Errorf("without filter: %d items", len(got))
}
}
func TestMovieItemAttrs(t *testing.T) {
m := radarr.Movie{ID: 1, Title: "M", HasFile: true, MovieFile: &arr.MediaFile{SceneName: "M.2160p.DV.x265-G"}}
it := newMovieItem(m)
if !it.attrsKnown || len(it.attrs) != 1 || !it.attrs[0].DV || it.attrs[0].Codec != arr.CodecX265 {
t.Errorf("attrs = %+v", it.attrs)
}
if it := newMovieItem(radarr.Movie{ID: 2, Title: "N"}); !it.attrsKnown || len(it.attrs) != 0 {
t.Errorf("movie without file: %+v", it)
}
if it := newSeriesItem(sonarr.Series{ID: 3, Title: "S", Statistics: sonarr.Statistics{EpisodeFileCount: 2}}); it.attrsKnown {
t.Error("series with files must fetch its attributes")
}
}