103 lines
3.5 KiB
Go
103 lines
3.5 KiB
Go
package scanner
|
|
|
|
import (
|
|
"naviwatcher/internal/database"
|
|
"naviwatcher/internal/musicbrainz"
|
|
)
|
|
|
|
// MissingRelease describes an external release that has no sufficiently similar
|
|
// local album. It is a flattened, consumer-friendly projection of a
|
|
// database.ExternalRelease.
|
|
type MissingRelease struct {
|
|
RGID string `json:"rgid"`
|
|
ArtistID string `json:"artist_id"`
|
|
Title string `json:"title"`
|
|
Type string `json:"type"`
|
|
ReleaseDate string `json:"release_date"`
|
|
}
|
|
|
|
// TypeFilter carries the per-artist type toggles that suppress whole release
|
|
// categories from the missing set. It mirrors the ignore_singles /
|
|
// ignore_compilations columns on artist_settings.
|
|
//
|
|
// These toggles are applied at scan/read time (not only when the MusicBrainz
|
|
// discography is synced) so a user flipping a toggle takes effect immediately on
|
|
// the dashboard, artist page, and Telegram digest — rather than waiting for the
|
|
// artist's MusicBrainz cache to expire and the rows to be pruned on the next
|
|
// cache-miss re-sync.
|
|
type TypeFilter struct {
|
|
IgnoreSingles bool
|
|
IgnoreCompilations bool
|
|
}
|
|
|
|
// suppressed reports whether an external release is dropped by the type toggles.
|
|
// A release counts as a Single/Compilation via either its primary Type or its
|
|
// secondary types, matching musicbrainz.FilterReleaseGroups so both the
|
|
// cache-miss (store-time) and read-time paths agree.
|
|
func (f TypeFilter) suppressed(ext database.ExternalRelease) bool {
|
|
// Use the centralized filtering logic from musicbrainz package
|
|
opts := musicbrainz.FilterOptions{
|
|
IgnoreSingles: f.IgnoreSingles,
|
|
IgnoreCompilations: f.IgnoreCompilations,
|
|
}
|
|
filtered := musicbrainz.ApplyTypeToggles([]database.ExternalRelease{ext}, opts)
|
|
return len(filtered) == 0
|
|
}
|
|
|
|
// FindMissingReleases compares an artist's external discography against the
|
|
// user's local albums and returns the releases that are present externally but
|
|
// have no sufficiently similar local album.
|
|
//
|
|
// Rules:
|
|
// - External releases flagged IsIgnored are never reported.
|
|
// - External releases suppressed by the per-artist type toggles (filter) are
|
|
// never reported.
|
|
// - A local album only matches an external release for the same ArtistID.
|
|
// - An external release is "missing" when none of the local albums (same
|
|
// ArtistID) IsMatch at the given threshold.
|
|
func FindMissingReleases(local []database.LocalAlbum, external []database.ExternalRelease, threshold float64, filter TypeFilter) []MissingRelease {
|
|
// Resolve the threshold exactly as ScanArtist/ScanAll do, so the exported
|
|
// primitive honors the same zero-means-default contract rather than treating
|
|
// 0 as "always match" (which would report nothing as missing).
|
|
threshold = resolveThreshold(threshold)
|
|
|
|
// Group local albums by artist for O(1) lookup per external release.
|
|
localByArtist := make(map[string][]database.LocalAlbum)
|
|
for _, a := range local {
|
|
localByArtist[a.ArtistID] = append(localByArtist[a.ArtistID], a)
|
|
}
|
|
|
|
var missing []MissingRelease
|
|
for _, ext := range external {
|
|
if ext.IsIgnored {
|
|
continue
|
|
}
|
|
if filter.suppressed(ext) {
|
|
continue
|
|
}
|
|
|
|
albums := localByArtist[ext.ArtistID]
|
|
matched := false
|
|
for _, a := range albums {
|
|
if IsMatch(a.Title, ext.Title, threshold) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if matched {
|
|
continue
|
|
}
|
|
|
|
missing = append(missing, MissingRelease{
|
|
RGID: ext.RGID,
|
|
ArtistID: ext.ArtistID,
|
|
Title: ext.Title,
|
|
Type: ext.Type,
|
|
ReleaseDate: ext.ReleaseDate,
|
|
})
|
|
}
|
|
|
|
return missing
|
|
}
|