172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
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"`
|
|
AudioCodec string `json:"audioCodec"`
|
|
AudioAdditional string `json:"audioAdditionalFeatures"`
|
|
} `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
|
|
Audio []string // audio formats, see the Audio* constants
|
|
Languages []string // audio languages
|
|
}
|
|
|
|
// Audio formats offered as filters. Atmos and the DTS flavours overlap with
|
|
// their carrier format: a "TrueHD Atmos" track is both Atmos and TrueHD, and
|
|
// every DTS flavour also counts as plain DTS.
|
|
const (
|
|
AudioAtmos = "Atmos"
|
|
AudioTrueHD = "TrueHD"
|
|
AudioDTSX = "DTS:X"
|
|
AudioDTSHD = "DTS-HD"
|
|
AudioDTS = "DTS"
|
|
AudioDDPlus = "DD+"
|
|
AudioDD = "DD"
|
|
AudioAAC = "AAC"
|
|
AudioFLAC = "FLAC"
|
|
AudioOpus = "Opus"
|
|
)
|
|
|
|
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)`)
|
|
reAtmos = regexp.MustCompile(`(?i)(atmos|\bddpa\b)`)
|
|
reTrueHD = regexp.MustCompile(`(?i)true[ .-]?hd`)
|
|
reDTSX = regexp.MustCompile(`(?i)\bdts[ .:-]?x\b`)
|
|
reDTSHD = regexp.MustCompile(`(?i)dts[ .-]?(hd|ma|hra|es)`)
|
|
reDTS = regexp.MustCompile(`(?i)\bdts([^a-z]|$)`)
|
|
reDDPlus = regexp.MustCompile(`(?i)(e[ .-]?ac[ .-]?3|\bddp|dd\+|dolby digital plus)`)
|
|
reDD = regexp.MustCompile(`(?i)(\bac[ .-]?3\b|\bdd[ .]?[0-9]|dolby digital\b)`)
|
|
reAAC = regexp.MustCompile(`(?i)\baac`)
|
|
reFLAC = regexp.MustCompile(`(?i)\bflac\b`)
|
|
reOpus = regexp.MustCompile(`(?i)\bopus\b`)
|
|
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])
|
|
}
|
|
}
|
|
|
|
if f.MediaInfo.VideoCodec != "" {
|
|
// A codec we do not offer as a filter (VC-1, MPEG-2...) stays empty
|
|
// rather than being guessed from the name.
|
|
a.Codec = codecOf(f.MediaInfo.VideoCodec)
|
|
} else {
|
|
a.Codec = codecOf(names)
|
|
}
|
|
|
|
dr := f.MediaInfo.VideoDynamicRangeType + " " + f.MediaInfo.VideoDynamicRange
|
|
if strings.TrimSpace(dr) == "" {
|
|
dr = names
|
|
}
|
|
a.DV = reDV.MatchString(dr)
|
|
a.HDR = reHDR.MatchString(dr)
|
|
audio := f.MediaInfo.AudioCodec + " " + f.MediaInfo.AudioAdditional
|
|
if strings.TrimSpace(audio) == "" {
|
|
audio = names
|
|
}
|
|
a.Audio = audioTags(audio)
|
|
for _, l := range f.Languages {
|
|
if l.Name != "" {
|
|
a.Languages = append(a.Languages, l.Name)
|
|
}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// audioTags finds the audio formats we offer as filters in free text.
|
|
func audioTags(s string) []string {
|
|
var tags []string
|
|
add := func(re *regexp.Regexp, tag string) {
|
|
if re.MatchString(s) {
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
add(reAtmos, AudioAtmos)
|
|
add(reTrueHD, AudioTrueHD)
|
|
dtsx, dtshd := reDTSX.MatchString(s), reDTSHD.MatchString(s)
|
|
if dtsx {
|
|
tags = append(tags, AudioDTSX)
|
|
}
|
|
if dtshd {
|
|
tags = append(tags, AudioDTSHD)
|
|
}
|
|
if dtsx || dtshd || reDTS.MatchString(s) {
|
|
tags = append(tags, AudioDTS)
|
|
}
|
|
if reDDPlus.MatchString(s) {
|
|
tags = append(tags, AudioDDPlus)
|
|
} else {
|
|
add(reDD, AudioDD)
|
|
}
|
|
add(reAAC, AudioAAC)
|
|
add(reFLAC, AudioFLAC)
|
|
add(reOpus, AudioOpus)
|
|
return tags
|
|
}
|
|
|
|
func atoi(s string) (n int) {
|
|
for _, c := range s {
|
|
n = n*10 + int(c-'0')
|
|
}
|
|
return n
|
|
}
|
|
|
|
// codecOf finds a codec we offer as a filter in free text ("x265", "H.264", "HEVC"...).
|
|
func codecOf(s string) string {
|
|
switch {
|
|
case re265.MatchString(s):
|
|
return CodecX265
|
|
case re264.MatchString(s):
|
|
return CodecX264
|
|
case reAV1.MatchString(s):
|
|
return CodecAV1
|
|
}
|
|
return ""
|
|
}
|