Files
bvandeusenandClaude Opus 5.5 89b3b44779
release / govulncheck (push) Successful in 21s
release / go (push) Failing after 33s
release / web (push) Successful in 56s
release / integration (push) Failing after 4m48s
release / android (push) Successful in 5m6s
release / Build signed APK (releases and dev) (push) Successful in 5m16s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
feat: search and grab — music release names, a quality ladder, a paced search schedule and one live grab per want (M502 #5521)
- internal/release: reads a release name (format, bitrate, bit depth and
  sample rate, source, year, video, discography) and judges it against the
  album wanted: the artist and title must be in it; video rips,
  discographies, other versions (drumless, instrumental, karaoke, remix,
  live unless the album is live…) and singles or EPs for an album are
  refused. Quality tiers on Lidarr's ladder; a .torrent's file list is read
  for format, video files and a track count short of the album.
- internal/acquire: fan-out across every search source with a narrowed
  search kept, a process-wide pacer per source, every candidate judged and
  kept with its reasons, and the grab: one live grab per want, tie-broken
  (a person's grab beats the schedule's; a better release replaces a worse
  one unless it is nearly done). Each download lands in its own folder
  under the client's downloads folder.
- internal/wanted: the search schedule, every ten minutes, never-searched
  first, then requests, missing files and followed artists, taking the
  best release when auto-grab is on (the default).
- Migration 0082: acquisition_settings (auto-grab, interval, allowed
  qualities seeded from Lidarr's "Any"), want search columns,
  want_candidates, grabs, release_blocks.
- Web: the Wanted page shows each want's last search, a Search button and
  every release found with its verdict (Grab, Grab anyway, Never this one,
  Allow again). Integrations gains "Searching and grabbing".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:56:47 -04:00

77 lines
1.9 KiB
Go

package release
import (
"path"
"strconv"
"strings"
)
// File is one file inside a torrent.
type File struct {
Index int
Path string
Size int64
}
// Contents is what a torrent's file list holds.
type Contents struct {
Audio int
Video int
// Format is the format most of the audio files are in, "" when none.
Format string
}
var videoExt = map[string]bool{
"mkv": true, "mp4": true, "avi": true, "m2ts": true, "vob": true,
"mov": true, "webm": true, "wmv": true, "m4v": true, "ts": true,
}
// Inspect counts a file list's audio and video files.
func Inspect(files []File) Contents {
var c Contents
formats := map[string]int{}
for _, f := range files {
ext := strings.TrimPrefix(strings.ToLower(path.Ext(f.Path)), ".")
if fm := formatOf(ext); fm != "" {
c.Audio++
formats[fm]++
} else if videoExt[ext] {
c.Video++
}
}
best := 0
for fm, n := range formats {
if n > best || (n == best && formatRank[fm] > formatRank[c.Format]) {
c.Format, best = fm, n
}
}
return c
}
// JudgeFiles says what a torrent's files say about whether it is the album:
// a veto (code and sentence), or a doubt for a person, or nothing.
func JudgeFiles(w Want, files []File) (veto, reason, doubt string) {
if len(files) == 0 {
return "", "", ""
}
c := Inspect(files)
switch {
case c.Audio == 0 && c.Video > 0:
return VetoVideo, "It holds video files and no audio.", ""
case c.Audio == 0:
return VetoNoAudio, "It holds no audio files Minstrel can read.", ""
case w.MinTracks > 0 && c.Audio < w.MinTracks:
return VetoIncomplete, "It holds " + tracks(c.Audio) + "; the shortest edition has " + tracks(w.MinTracks) + ".", ""
case w.MaxTracks > 0 && c.Audio > 2*w.MaxTracks+4:
return "", "", "It holds " + tracks(c.Audio) + ", more than any edition of the album has: probably more than one album."
}
return "", "", ""
}
func tracks(n int) string {
if n == 1 {
return "1 track"
}
return strconv.Itoa(n) + " tracks"
}