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 "" }