Files
arr-tui/internal/ui/libfilter.go
T

178 lines
4.9 KiB
Go

package ui
import (
"slices"
"strings"
"github.com/rivo/tview"
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
)
// attrFilter narrows the library by properties of the downloaded files. An
// empty field means "any". All fields must match the same file; a series
// matches when any of its files does.
type attrFilter struct {
res, codec, hdr, audio, lang string
}
const anyOption = "any"
var (
resOptions = []string{"2160p", "1080p", "720p", "SD"}
codecOptions = []string{arr.CodecX265, arr.CodecX264, arr.CodecAV1}
hdrOptions = []string{"HDR", "DV", "SDR"}
audioOptions = []string{arr.AudioAtmos, arr.AudioTrueHD, arr.AudioDTSX, arr.AudioDTSHD, arr.AudioDTS,
arr.AudioDDPlus, arr.AudioDD, arr.AudioAAC, arr.AudioFLAC, arr.AudioOpus}
)
func (f attrFilter) active() bool { return f != attrFilter{} }
// String lists the active criteria for the header, e.g. "1080p x265 DV".
func (f attrFilter) String() string {
var parts []string
for _, p := range []string{f.res, f.codec, f.hdr, f.audio, f.lang} {
if p != "" {
parts = append(parts, p)
}
}
return strings.Join(parts, " ")
}
func (f attrFilter) matchFile(a arr.FileAttrs) bool {
switch f.res {
case "":
case "SD":
if a.Resolution <= 0 || a.Resolution > 576 {
return false
}
default:
if f.res != itoa(a.Resolution)+"p" {
return false
}
}
if f.codec != "" && f.codec != a.Codec {
return false
}
switch f.hdr {
case "HDR":
if !a.HDR {
return false
}
case "DV":
if !a.DV {
return false
}
case "SDR":
if a.HDR || a.DV {
return false
}
}
if f.audio != "" && !slices.Contains(a.Audio, f.audio) {
return false
}
if f.lang != "" && !slices.ContainsFunc(a.Languages, func(l string) bool { return strings.EqualFold(l, f.lang) }) {
return false
}
return true
}
// match reports whether any of the files matches. Without files nothing matches.
func (f attrFilter) match(files []arr.FileAttrs) bool {
return slices.ContainsFunc(files, f.matchFile)
}
// languagesOf collects the languages of all known files, sorted.
func languagesOf(items ...[]libItem) []string {
seen := map[string]bool{}
var out []string
for _, list := range items {
for _, it := range list {
for _, a := range it.attrs {
for _, l := range a.Languages {
if !seen[l] {
seen[l] = true
out = append(out, l)
}
}
}
}
}
slices.Sort(out)
return out
}
func indexOf(options []string, v string) int {
if i := slices.Index(options, v); i >= 0 {
return i + 1 // option 0 is "any"
}
return 0
}
// openAttrFilter shows the dialog to pick quality, codec, HDR, audio and
// language. Every change is passed to set immediately so the list behind the
// dialog follows along; Cancel and Esc restore cur.
func (a *App) openAttrFilter(cur attrFilter, languages []string, set func(attrFilter)) {
if cur.lang != "" && !slices.Contains(languages, cur.lang) {
languages = append([]string{cur.lang}, languages...)
}
withAny := func(o []string) []string { return append([]string{anyOption}, o...) }
const (
lRes = "Quality"
lCodec = "Codec"
lHDR = "HDR"
lAudio = "Audio"
lLang = "Language"
)
form := tview.NewForm().
AddDropDown(lRes, withAny(resOptions), indexOf(resOptions, cur.res), nil).
AddDropDown(lCodec, withAny(codecOptions), indexOf(codecOptions, cur.codec), nil).
AddDropDown(lHDR, withAny(hdrOptions), indexOf(hdrOptions, cur.hdr), nil).
AddDropDown(lAudio, withAny(audioOptions), indexOf(audioOptions, cur.audio), nil).
AddDropDown(lLang, withAny(languages), indexOf(languages, cur.lang), nil)
// filterWith reads the dialog; the entry at index in dropdown over (if
// any) stands in for that dropdown's committed choice.
filterWith := func(over *tview.DropDown, index int) attrFilter {
pick := func(label string, options []string) string {
dd := form.GetFormItemByLabel(label).(*tview.DropDown)
i, _ := dd.GetCurrentOption()
if dd == over {
i = index
}
if i <= 0 {
return ""
}
return options[i-1]
}
return attrFilter{
res: pick(lRes, resOptions),
codec: pick(lCodec, codecOptions),
hdr: pick(lHDR, hdrOptions),
audio: pick(lAudio, audioOptions),
lang: pick(lLang, languages),
}
}
current := func() attrFilter { return filterWith(nil, 0) }
for _, label := range []string{lRes, lCodec, lHDR, lAudio, lLang} {
form.GetFormItemByLabel(label).(*tview.DropDown).SetSelectedFunc(func(string, int) { set(current()) })
}
cancel := func() {
a.closeModal()
set(cur)
}
form.AddButton("Apply", a.closeModal)
form.AddButton("Reset", func() {
a.closeModal()
set(attrFilter{})
})
form.AddButton("Cancel", cancel)
arrowNavigation(form, cancel, &browseHooks{
browse: func(dd *tview.DropDown, index int) { set(filterWith(dd, index)) },
abort: func() { set(current()) }, // list closed without choosing: back to the committed value
})
form.SetBorder(true).SetTitle(" Filter by properties ")
a.openSidePanel(form, 40, 17, "↑↓ move · ←→ change (applies live) · Enter apply · Esc cancel")
}