package release import "strings" // Want is the album a search is for, as the catalogue describes it. type Want struct { Artist string Title string Year int PrimaryType string // Album, EP, Single, Broadcast, Other SecondaryTypes []string // MinTracks and MaxTracks span the group's editions' track counts; zero // when the catalogue has not read them. MinTracks int MaxTracks int } // Veto codes: why a name is not the album. Kept on a candidate as its // reason's code. const ( VetoArtist = "wrong_artist" VetoTitle = "wrong_title" VetoVideo = "video" VetoDiscography = "discography" VetoVersion = "other_version" VetoType = "wrong_type" VetoNoAudio = "no_audio" VetoIncomplete = "incomplete" ) // Match is what a name says about whether it is the album wanted. type Match struct { // Veto is a code from above when the name is not the album, with Reason // saying why in a sentence. Veto string Reason string // Doubt is set when the name may be the album and cannot say for sure: a // person decides. It is a sentence. Doubt string // YearAgrees: the name states the album's year, give or take one. YearAgrees bool } // versions are words that make a release a different recording of the // album, with what to call it. A word the wanted title or artist holds does // not count ("Live Through This", "Instrumental Core"). var versions = []struct { words []string what string // typ is the MusicBrainz secondary type that wants this version. typ string }{ {[]string{"instrumental", "instrumentals"}, "an instrumental version", ""}, {[]string{"drumless"}, "a drumless edition", ""}, {[]string{"karaoke"}, "a karaoke version", ""}, {[]string{"acapella", "acappella", "accapella", "acapellas"}, "an a cappella version", ""}, {[]string{"remix", "remixes", "remixed"}, "a remix release", "Remix"}, {[]string{"live", "bootleg"}, "a live recording", "Live"}, {[]string{"demo", "demos"}, "demo recordings", "Demo"}, // Not "covers": in a release name that is usually the artwork scans. {[]string{"tribute", "lullaby", "lullabies"}, "a tribute release", ""}, {[]string{"nightcore", "slowed", "sped"}, "a slowed or sped-up edit", ""}, {[]string{"commentary", "interview", "interviews"}, "commentary or interviews", "Interview"}, } // collections are words naming more than one album. var collections = []string{"collection", "anthology", "albums", "boxset", "discography"} // Judge says whether a release name is the album wanted. It looks only at // the name; JudgeFiles looks at what a torrent holds. func Judge(w Want, r Release) Match { var m Match m.YearAgrees = r.Year != 0 && w.Year != 0 && abs(r.Year-w.Year) <= 1 if r.Video != "" { m.Veto, m.Reason = VetoVideo, "A video, not audio (“"+r.Video+"”)." return m } artist := Tokens(w.Artist) title := Tokens(w.Title) if len(artist) > 0 && !contains(r.Tokens, artist) && !contains(r.Tokens, withoutThe(artist)) { m.Veto, m.Reason = VetoArtist, "The name doesn’t mention "+w.Artist+"." return m } if len(title) > 0 && !contains(r.Tokens, title) && !contains(r.Tokens, bare(w.Title)) { m.Veto, m.Reason = VetoTitle, "The name doesn’t contain the title “"+w.Title+"”." return m } own := map[string]bool{} for _, t := range append(append([]string{}, title...), artist...) { own[t] = true } if r.Discography != "" && !own[r.Discography] { m.Veto, m.Reason = VetoDiscography, "A discography or collection ("+r.Discography+"), and one album is wanted." return m } for _, c := range collections { if !own[c] && has(r.Tokens, c) { m.Veto, m.Reason = VetoDiscography, "A collection of albums (“"+c+"”), and one album is wanted." return m } } for _, v := range versions { if v.typ != "" && hasType(w.SecondaryTypes, v.typ) { continue } for _, word := range v.words { if !own[word] && has(r.Tokens, word) { m.Veto, m.Reason = VetoVersion, "This is "+v.what+" (“"+word+"”)." return m } } } if w.PrimaryType == "Album" || w.PrimaryType == "EP" { if !own["single"] && has(r.Tokens, "single") { m.Veto, m.Reason = VetoType, "A single, and the "+strings.ToLower(w.PrimaryType)+" is wanted." return m } } if w.PrimaryType == "Album" && !own["ep"] && has(r.Tokens, "ep") { m.Veto, m.Reason = VetoType, "An EP, and the album is wanted." return m } // A self-titled album: every release of the artist names it once. Two // mentions, or its year, say it is this one. if len(title) > 0 && equal(title, artist) && count(r.Tokens, title) < 2 && !m.YearAgrees { m.Doubt = "The album shares the artist’s name, and nothing else in the name says it is this album." } return m } func abs(n int) int { if n < 0 { return -n } return n } func withoutThe(t []string) []string { if len(t) > 1 && t[0] == "the" { return t[1:] } return t } // bare is a title without a trailing bracketed part ("Album (Remastered)"), // for names that leave it off. func bare(title string) []string { if i := strings.IndexAny(title, "(["); i > 0 { return Tokens(title[:i]) } return nil } // contains says whether want's words appear in name in order and together, // or, for two or more words, run together ("ACDC" for AC/DC). A single word // is never looked for run together: "muse" is inside "museum". func contains(name, want []string) bool { if len(want) == 0 { return false } if count(name, want) > 0 { return true } joined := strings.Join(want, "") if len(want) < 2 || len(joined) < 4 { return false } return strings.Contains(strings.Join(name, ""), joined) } // count is how often want's words appear in name in order and together. func count(name, want []string) int { n := 0 for i := 0; i+len(want) <= len(name); i++ { if equal(name[i:i+len(want)], want) { n++ } } return n } func equal(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func has(tokens []string, word string) bool { for _, t := range tokens { if t == word { return true } } return false } func hasType(types []string, t string) bool { for _, x := range types { if strings.EqualFold(x, t) { return true } } return false }