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
+103
View File
@@ -0,0 +1,103 @@
package arr
import (
"regexp"
"strings"
)
// MediaFile is a downloaded file as Radarr (movieFile) and Sonarr
// (episodeFile) describe it.
type MediaFile struct {
Size int64 `json:"size"`
SceneName string `json:"sceneName,omitempty"`
RelativePath string `json:"relativePath,omitempty"`
Quality struct {
Quality struct {
Name string `json:"name"`
Resolution int `json:"resolution"`
} `json:"quality"`
} `json:"quality"`
MediaInfo struct {
VideoCodec string `json:"videoCodec"`
VideoDynamicRange string `json:"videoDynamicRange"`
VideoDynamicRangeType string `json:"videoDynamicRangeType"`
} `json:"mediaInfo"`
Languages []struct {
Name string `json:"name"`
} `json:"languages"`
}
// Codec names used for filtering.
const (
CodecX265 = "x265"
CodecX264 = "x264"
CodecAV1 = "AV1"
)
// FileAttrs are the properties of a file the library can be filtered by.
type FileAttrs struct {
Resolution int // 2160, 1080, 720, 480 ... or 0 if unknown
Codec string // CodecX265, CodecX264, CodecAV1 or ""
HDR bool // HDR10, HDR10+, HLG, PQ (also with Dolby Vision on top)
DV bool // Dolby Vision
Languages []string // audio languages
}
var (
reRes = regexp.MustCompile(`(?i)\b(2160|1080|720|576|480)p\b`)
re265 = regexp.MustCompile(`(?i)(x\.?265|h\.?265|hevc)`)
re264 = regexp.MustCompile(`(?i)(x\.?264|h\.?264|\bavc\b)`)
reAV1 = regexp.MustCompile(`(?i)\bav1\b`)
reDV = regexp.MustCompile(`(?i)(\bdv\b|dovi|dolby[ .]?vision)`)
reHDR = regexp.MustCompile(`(?i)\b(hdr(10(\+|plus)?)?|hlg|pq)\b`)
)
// Attrs derives the filterable properties. Radarr/Sonarr's media info is used
// when present, otherwise the names (scene name, path, quality) are searched.
func (f MediaFile) Attrs() FileAttrs {
names := strings.Join([]string{f.SceneName, f.RelativePath, f.Quality.Quality.Name}, " ")
a := FileAttrs{Resolution: f.Quality.Quality.Resolution}
if a.Resolution == 0 {
if m := reRes.FindStringSubmatch(names); m != nil {
a.Resolution = atoi(m[1])
}
}
codec := f.MediaInfo.VideoCodec + " " + names
switch {
case re265.MatchString(f.MediaInfo.VideoCodec):
a.Codec = CodecX265
case re264.MatchString(f.MediaInfo.VideoCodec):
a.Codec = CodecX264
case reAV1.MatchString(f.MediaInfo.VideoCodec):
a.Codec = CodecAV1
case f.MediaInfo.VideoCodec != "":
// A codec we do not offer as a filter (VC-1, MPEG-2...): do not guess from the name.
case re265.MatchString(codec):
a.Codec = CodecX265
case re264.MatchString(codec):
a.Codec = CodecX264
case reAV1.MatchString(codec):
a.Codec = CodecAV1
}
dr := f.MediaInfo.VideoDynamicRangeType + " " + f.MediaInfo.VideoDynamicRange
if strings.TrimSpace(dr) == "" {
dr = names
}
a.DV = reDV.MatchString(dr)
a.HDR = reHDR.MatchString(dr)
for _, l := range f.Languages {
if l.Name != "" {
a.Languages = append(a.Languages, l.Name)
}
}
return a
}
func atoi(s string) (n int) {
for _, c := range s {
n = n*10 + int(c-'0')
}
return n
}