mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
When a listen arrives with no MBZ IDs and no album title (the common multi-scrobbler/Last.fm case), search MusicBrainz by artist+track name to resolve recording, release, and release group IDs. This unlocks CoverArtArchive album art, proper album association, and duration data. New file: internal/mbz/search.go - SearchRecording() method with Lucene query escaping - Confidence filter: case-insensitive exact match on title + artist credit - Release selection: prefer Official status, then first available - Uses existing rate-limited queue (1 req/sec) Integration in catalog.go: - Only triggers when RecordingMbzID, ReleaseMbzID, AND ReleaseTitle are all missing — no impact on scrobbles that already have MBZ data - Soft failure — search errors don't block the listen - KOITO_DISABLE_MUSICBRAINZ handled automatically (MbzErrorCaller returns error) Interface + mocks updated: - SearchRecording added to MusicBrainzCaller interface - MbzMockCaller: SearchResults map for test data - MbzErrorCaller: returns error (existing pattern) New tests: - TestSubmitListen_SearchByName — mock search, verify album+duration resolved - TestSubmitListen_SearchByNameNoMatch — verify graceful fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
3.2 KiB
Go
110 lines
3.2 KiB
Go
package mbz
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// implements a mock caller
|
|
|
|
type MbzMockCaller struct {
|
|
Artists map[uuid.UUID]*MusicBrainzArtist
|
|
ReleaseGroups map[uuid.UUID]*MusicBrainzReleaseGroup
|
|
Releases map[uuid.UUID]*MusicBrainzRelease
|
|
Tracks map[uuid.UUID]*MusicBrainzTrack
|
|
SearchResults map[string]*MusicBrainzSearchResult
|
|
}
|
|
|
|
func (m *MbzMockCaller) GetReleaseGroup(ctx context.Context, id uuid.UUID) (*MusicBrainzReleaseGroup, error) {
|
|
releaseGroup, exists := m.ReleaseGroups[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("release group with ID %s not found", id)
|
|
}
|
|
return releaseGroup, nil
|
|
}
|
|
|
|
func (m *MbzMockCaller) GetRelease(ctx context.Context, id uuid.UUID) (*MusicBrainzRelease, error) {
|
|
release, exists := m.Releases[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("release group with ID %s not found", id)
|
|
}
|
|
return release, nil
|
|
}
|
|
|
|
func (m *MbzMockCaller) GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error) {
|
|
rg, exists := m.ReleaseGroups[RGID]
|
|
if !exists {
|
|
return nil, fmt.Errorf("release with ID %s not found", RGID)
|
|
}
|
|
|
|
var titles []string
|
|
for _, release := range rg.Releases {
|
|
if !slices.Contains(titles, release.Title) {
|
|
titles = append(titles, release.Title)
|
|
}
|
|
}
|
|
return titles, nil
|
|
}
|
|
|
|
func (m *MbzMockCaller) GetTrack(ctx context.Context, id uuid.UUID) (*MusicBrainzTrack, error) {
|
|
track, exists := m.Tracks[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("track with ID %s not found", id)
|
|
}
|
|
return track, nil
|
|
}
|
|
|
|
func (m *MbzMockCaller) GetArtistPrimaryAliases(ctx context.Context, id uuid.UUID) ([]string, error) {
|
|
artist, exists := m.Artists[id]
|
|
if !exists {
|
|
return nil, fmt.Errorf("artist with ID %s not found", id)
|
|
}
|
|
name := artist.Name
|
|
ss := make([]string, len(artist.Aliases)+1)
|
|
ss[0] = name
|
|
for i, alias := range artist.Aliases {
|
|
ss[i+1] = alias.Name
|
|
}
|
|
return ss, nil
|
|
}
|
|
|
|
func (m *MbzMockCaller) SearchRecording(ctx context.Context, artist string, track string) (*MusicBrainzSearchResult, error) {
|
|
key := artist + "\x00" + track
|
|
if result, exists := m.SearchResults[key]; exists {
|
|
return result, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MbzMockCaller) Shutdown() {}
|
|
|
|
type MbzErrorCaller struct{}
|
|
|
|
func (m *MbzErrorCaller) GetReleaseGroup(ctx context.Context, id uuid.UUID) (*MusicBrainzReleaseGroup, error) {
|
|
return nil, fmt.Errorf("error: GetReleaseGroup not implemented")
|
|
}
|
|
|
|
func (m *MbzErrorCaller) GetRelease(ctx context.Context, id uuid.UUID) (*MusicBrainzRelease, error) {
|
|
return nil, fmt.Errorf("error: GetRelease not implemented")
|
|
}
|
|
|
|
func (m *MbzErrorCaller) GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error) {
|
|
return nil, fmt.Errorf("error: GetReleaseTitles not implemented")
|
|
}
|
|
|
|
func (m *MbzErrorCaller) GetTrack(ctx context.Context, id uuid.UUID) (*MusicBrainzTrack, error) {
|
|
return nil, fmt.Errorf("error: GetTrack not implemented")
|
|
}
|
|
|
|
func (m *MbzErrorCaller) GetArtistPrimaryAliases(ctx context.Context, id uuid.UUID) ([]string, error) {
|
|
return nil, fmt.Errorf("error: GetArtistPrimaryAliases not implemented")
|
|
}
|
|
|
|
func (m *MbzErrorCaller) SearchRecording(ctx context.Context, artist string, track string) (*MusicBrainzSearchResult, error) {
|
|
return nil, fmt.Errorf("error: SearchRecording not implemented")
|
|
}
|
|
|
|
func (m *MbzErrorCaller) Shutdown() {}
|