feat: a held album below the quality you settle for, or holding a video rip, stays wanted for a better copy, and the import replaces the old tracks with their likes and plays carried over (M502 #5524)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped
- acquisition_settings.upgrade_until (default High quality lossy, Lidarr's cutoff, note #5527; "" is off), an "Upgrade until" row under Searching. - The wanted worker judges held albums hourly: tier per format from size per second (the scanner records no bitrate), the album's worst. Below the cutoff it is wanted for quality; a video rip among its tracks always. - The schedule searches upgrades weekly, after open wants; a release no better than the copy held is refused ("not_better"). - The import matches each file to the track it would replace (recording, then disc/track) and replaces it when it is a rip or the file is a better tier: over the same path in one rename, or filed beside it and the old track folded in (library.ReplaceTrack: file first, then foldTrackInto). Nothing better: the release is set aside. - Collection › Upgrades (Lidarr's Cutoff Unmet); Suspect sources says when a clean copy was looked for instead of Lidarr's search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -167,3 +167,105 @@ func NewProfile(tiers []string) Profile {
|
||||
|
||||
// Allows says whether the profile takes a tier.
|
||||
func (p Profile) Allows(t Tier) bool { return p.Allowed[t] }
|
||||
|
||||
// Held is what the library's copy of an album is: its tier, estimated from
|
||||
// what the scanner records, and the same in words.
|
||||
type Held struct {
|
||||
Tier Tier
|
||||
Words string
|
||||
}
|
||||
|
||||
// HeldFormat is an album's present tracks of one format, summed.
|
||||
type HeldFormat struct {
|
||||
Format string
|
||||
Bytes, Ms int64
|
||||
}
|
||||
|
||||
// HeldCopy judges an album's copy from its tracks grouped by format: the
|
||||
// lowest tier among them, as Lidarr judged an album by its worst file.
|
||||
// Summing a format's tracks first keeps a near-silent VBR interlude from
|
||||
// dragging the album down. ok is false when no format can be placed.
|
||||
func HeldCopy(formats []HeldFormat) (Held, bool) {
|
||||
var out Held
|
||||
found := false
|
||||
for _, f := range formats {
|
||||
kbps := FileKbps(f.Bytes, f.Ms)
|
||||
t, ok := TierOfFile(f.Format, kbps)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !found || Rank(t) < Rank(out.Tier) {
|
||||
out = Held{Tier: t, Words: HeldWords(f.Format, t, kbps)}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return out, found
|
||||
}
|
||||
|
||||
// FileKbps is the average bitrate of files totalling bytes over ms of audio:
|
||||
// the scanner records no bitrate (tracks.bitrate is never filled), so size
|
||||
// per second is the measure there is. Cover art and tags count too, which
|
||||
// only ever reads high.
|
||||
func FileKbps(bytes, ms int64) int {
|
||||
if bytes <= 0 || ms <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int(bytes * 8 / ms)
|
||||
}
|
||||
|
||||
// mp3High is the average bitrate from which an MP3 counts as high quality
|
||||
// when only its size is known. A V0 encode averages 220 to 260 kbps, so the
|
||||
// line sits below it: an estimate errs toward the copy held, and an album is
|
||||
// never fetched again on a guess (#5524).
|
||||
const mp3High = 220
|
||||
|
||||
// TierOfFile places files of one format on the ladder from their average
|
||||
// bitrate (FileKbps). format is the scanner's extension, without the dot.
|
||||
// ok is false for a format the ladder cannot place.
|
||||
func TierOfFile(format string, kbps int) (Tier, bool) {
|
||||
switch strings.ToLower(strings.TrimPrefix(format, ".")) {
|
||||
case "flac", "wav", "aiff", "aif", "ape", "wv", "alac":
|
||||
// 16-bit/44.1 kHz stereo is 1,411 kbps before compression; past
|
||||
// 1,600 on average is more bits or a higher rate than CD.
|
||||
if kbps > 1600 {
|
||||
return TierLosslessHiRes, true
|
||||
}
|
||||
return TierLossless, true
|
||||
case "dsf", "dff":
|
||||
return TierLosslessHiRes, true
|
||||
case "m4a", "mp4":
|
||||
// ALAC or AAC: the container says nothing, the rate does. No AAC
|
||||
// encode runs near 500 kbps.
|
||||
if kbps > 500 {
|
||||
return TierLossless, true
|
||||
}
|
||||
return byBitrate(kbps, 256), kbps > 0
|
||||
case "mp3":
|
||||
if kbps >= mp3High {
|
||||
return TierLossyHigh, true
|
||||
}
|
||||
return byBitrate(kbps, 320), kbps > 0
|
||||
case "aac", "ogg", "oga", "opus":
|
||||
return byBitrate(kbps, 256), kbps > 0
|
||||
}
|
||||
return TierUnknown, false
|
||||
}
|
||||
|
||||
// HeldWords says what a held copy is: "FLAC", "MP3, about 192 kbps".
|
||||
func HeldWords(format string, tier Tier, kbps int) string {
|
||||
f := strings.ToUpper(strings.TrimPrefix(format, "."))
|
||||
switch tier {
|
||||
case TierLosslessHiRes:
|
||||
return f + ", hi-res"
|
||||
case TierLossless:
|
||||
return f
|
||||
}
|
||||
if kbps <= 0 {
|
||||
return f
|
||||
}
|
||||
return fmt.Sprintf("%s, about %d kbps", f, roundKbps(kbps))
|
||||
}
|
||||
|
||||
// roundKbps rounds an average bitrate to the nearest 8, so a 191 reads as
|
||||
// the 192 it was encoded at.
|
||||
func roundKbps(k int) int { return (k + 4) / 8 * 8 }
|
||||
|
||||
Reference in New Issue
Block a user