package release import ( "fmt" "strconv" "strings" ) // Tier is a rung of the quality ladder, Lidarr's names for them. type Tier string // The ladder, best first. const ( TierLosslessHiRes Tier = "lossless_hires" TierLossless Tier = "lossless" TierLossyHigh Tier = "lossy_high" TierLossyStandard Tier = "lossy_standard" TierLossyLow Tier = "lossy_low" TierLossyPoor Tier = "lossy_poor" TierLossyTrash Tier = "lossy_trash" TierUnknown Tier = "unknown" ) // losslessCategory is Newznab's Audio/Lossless. const losslessCategory = 3040 // TierInfo is a rung as the settings page shows it. type TierInfo struct { Tier Tier `json:"tier"` Label string `json:"label"` What string `json:"what"` } // Tiers is the ladder, best first. var Tiers = []TierInfo{ {TierLosslessHiRes, "Lossless, hi-res", "24-bit or above 48 kHz: FLAC, ALAC, DSD"}, {TierLossless, "Lossless", "FLAC, ALAC, APE, WavPack, WAV at CD quality"}, {TierLossyHigh, "High quality lossy", "MP3 320 or V0, AAC 256 and up"}, {TierLossyStandard, "Standard quality lossy", "MP3 224–256 or V1/V2, AAC 192, Vorbis, Opus, or a lossy format with no bitrate stated"}, {TierLossyLow, "Low quality lossy", "160–192 kbps"}, {TierLossyPoor, "Poor quality lossy", "128 kbps"}, {TierLossyTrash, "Trash quality lossy", "Under 128 kbps"}, {TierUnknown, "Unknown", "Nothing says what format it is"}, } // DefaultTiers is what a new install allows: Lidarr's "Any" profile, every // format from trash lossy to lossless (note #5527). Unknown is left out; a // release that names no format has its .torrent read to find out. var DefaultTiers = []Tier{ TierLosslessHiRes, TierLossless, TierLossyHigh, TierLossyStandard, TierLossyLow, TierLossyPoor, TierLossyTrash, } // KnownTier says whether t is a rung of the ladder. func KnownTier(t string) bool { for _, info := range Tiers { if string(info.Tier) == t { return true } } return false } // Rank orders tiers: higher is better, Unknown lowest. func Rank(t Tier) int { for i, info := range Tiers { if info.Tier == t { return len(Tiers) - i } } return 0 } // Label is a tier's name for a person. func Label(t Tier) string { for _, info := range Tiers { if info.Tier == t { return info.Label } } return string(t) } // TierOf places a release on the ladder. category is the indexer's; 3040 // (Lossless) stands in for a name that states no format. func TierOf(r Release, category int) Tier { switch r.Format { case FormatDSD: return TierLosslessHiRes case FormatFLAC, FormatALAC, FormatAPE, FormatWV, FormatWAV, FormatAIFF: if r.BitDepth >= 24 || r.SampleRate > 48 { return TierLosslessHiRes } return TierLossless case FormatMP3: switch r.VBR { case "V0": return TierLossyHigh case "V1", "V2": return TierLossyStandard } return byBitrate(r.Bitrate, 320) case FormatAAC, FormatVorbis, FormatOpus: return byBitrate(r.Bitrate, 256) } if category == losslessCategory { return TierLossless } return TierUnknown } // byBitrate places a lossy bitrate; high is the bitrate from which the codec // counts as high quality. An unstated bitrate is standard. func byBitrate(kbps, high int) Tier { switch { case kbps == 0: return TierLossyStandard case kbps >= high: return TierLossyHigh case kbps >= 224 || (high == 256 && kbps >= 192): return TierLossyStandard case kbps >= 160: return TierLossyLow case kbps >= 128: return TierLossyPoor } return TierLossyTrash } // Describe is a release's format in a few words: "FLAC 24-bit/96 kHz", // "MP3 V0", "AAC 256". Empty when the name states none. func Describe(r Release) string { if r.Format == "" { return "" } parts := []string{r.Format} switch { case r.BitDepth > 0 && r.SampleRate > 0: parts = append(parts, fmt.Sprintf("%d-bit/%s kHz", r.BitDepth, khz(r.SampleRate))) case r.BitDepth > 0: parts = append(parts, fmt.Sprintf("%d-bit", r.BitDepth)) case r.VBR != "": parts = append(parts, r.VBR) case r.Bitrate > 0: parts = append(parts, strconv.Itoa(r.Bitrate)) } return strings.Join(parts, " ") } func khz(f float64) string { return strings.TrimSuffix(strconv.FormatFloat(f, 'f', 1, 64), ".0") } // Profile is which tiers the operator takes. type Profile struct { Allowed map[Tier]bool } // NewProfile builds a profile from the stored tier list. func NewProfile(tiers []string) Profile { p := Profile{Allowed: map[Tier]bool{}} for _, t := range tiers { p.Allowed[Tier(t)] = true } return p } // Allows says whether the profile takes a tier. func (p Profile) Allows(t Tier) bool { return p.Allowed[t] }