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

@@ -202,7 +202,7 @@ func TestFindMissingReleases(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FindMissingReleases(tt.local, tt.external, threshold)
got := FindMissingReleases(tt.local, tt.external, threshold, TypeFilter{})
gotRGIDs := make([]string, 0, len(got))
for _, m := range got {
@@ -225,6 +225,67 @@ func TestFindMissingReleases(t *testing.T) {
}
}
func TestFindMissingReleases_TypeFilter(t *testing.T) {
const threshold = 0.85
artist := "artist-a"
external := []database.ExternalRelease{
{RGID: "rg-album", ArtistID: artist, Title: "The Wall", Type: "Album"},
{RGID: "rg-single", ArtistID: artist, Title: "B-side", Type: "Single"},
{RGID: "rg-comp", ArtistID: artist, Title: "Hits", Type: "Compilation"},
{RGID: "rg-comp-sec", ArtistID: artist, Title: "Live at X", Type: "Album", SecondaryTypes: []string{"Compilation"}},
}
tests := []struct {
name string
filter TypeFilter
want []string
}{
{
name: "no filter reports all",
filter: TypeFilter{},
want: []string{"rg-album", "rg-single", "rg-comp", "rg-comp-sec"},
},
{
name: "ignore singles drops Single primary type",
filter: TypeFilter{IgnoreSingles: true},
want: []string{"rg-album", "rg-comp", "rg-comp-sec"},
},
{
name: "ignore compilations drops Compilation primary and secondary type",
filter: TypeFilter{IgnoreCompilations: true},
want: []string{"rg-album", "rg-single"},
},
{
name: "both toggles drop singles and compilations",
filter: TypeFilter{IgnoreSingles: true, IgnoreCompilations: true},
want: []string{"rg-album"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FindMissingReleases(nil, external, threshold, tt.filter)
gotRGIDs := make([]string, 0, len(got))
for _, m := range got {
gotRGIDs = append(gotRGIDs, m.RGID)
}
wantSet := make(map[string]struct{}, len(tt.want))
for _, r := range tt.want {
wantSet[r] = struct{}{}
}
if len(gotRGIDs) != len(tt.want) {
t.Fatalf("got %v, want %v", gotRGIDs, tt.want)
}
for _, r := range gotRGIDs {
if _, ok := wantSet[r]; !ok {
t.Errorf("unexpected RGID %q", r)
}
}
})
}
}
func TestFindMissingReleases_ThresholdBoundaryInclusive(t *testing.T) {
// A title at exactly the threshold must NOT be reported as missing
// (IsMatch uses >= threshold).
@@ -240,7 +301,7 @@ func TestFindMissingReleases_ThresholdBoundaryInclusive(t *testing.T) {
}
// With default threshold 0.85, "The Wall Live" does not match "The Wall";
// at a low threshold it would. Confirms threshold is honoured.
if len(FindMissingReleases(local, external, 0.85)) != 1 {
if len(FindMissingReleases(local, external, 0.85, TypeFilter{})) != 1 {
t.Errorf("expected 1 missing at 0.85 threshold")
}
}