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 }