fix: address code review findings

- Honor ignore_singles/ignore_compilations at scanner read time so toggles
  take effect immediately on the dashboard, artist page, and digest instead
  of waiting for the MusicBrainz cache to expire and prune rows.
- Run notifier notify synchronously in the scheduler loop to avoid overlapping
  read-send-mark runs double-sending the digest.
- Show artist name (with ID fallback) on the archive page instead of raw IDs.
- Select last_synced in GetAllArtistSettings for contract consistency.
- Fix stale startPeriodicSync comment and remove redundant error var.
- Remove dead ignored-branch from the artist template (never rendered).
- Add tests: CSRF sameOrigin, ArtistCacheFresh, secondary_types round-trip,
  and scanner type-toggle filtering.
- Update Specification.md schema/config to reflect mbid, last_synced,
  secondary_types, sync.interval, and server.public_url.
This commit is contained in:
2026-07-20 06:18:21 +03:00
parent f5b0034b4d
commit a8aa445d94
13 changed files with 402 additions and 31 deletions

View File

@@ -15,16 +15,56 @@ type MissingRelease struct {
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 {
if f.IgnoreSingles && (ext.Type == "Single" || hasType(ext.SecondaryTypes, "Single")) {
return true
}
if f.IgnoreCompilations && (ext.Type == "Compilation" || hasType(ext.SecondaryTypes, "Compilation")) {
return true
}
return false
}
// hasType reports whether types contains want.
func hasType(types []string, want string) bool {
for _, t := range types {
if t == want {
return true
}
}
return false
}
// 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) []MissingRelease {
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).
@@ -41,6 +81,9 @@ func FindMissingReleases(local []database.LocalAlbum, external []database.Extern
if ext.IsIgnored {
continue
}
if filter.suppressed(ext) {
continue
}
albums := localByArtist[ext.ArtistID]
matched := false