diff --git a/internal/provider/musicbrainz/client.go b/internal/provider/musicbrainz/client.go index a4bd482..867ba18 100644 --- a/internal/provider/musicbrainz/client.go +++ b/internal/provider/musicbrainz/client.go @@ -137,12 +137,17 @@ func (c *client) lookup(ctx context.Context, entity, id string, inc []string) ([ } func (c *client) browse(ctx context.Context, entity, linkedEntity, linkedID string, limit, offset int, inc []string) ([]byte, error) { + return c.browseWithTypes(ctx, entity, linkedEntity, linkedID, limit, offset, inc, nil) +} + +func (c *client) browseWithTypes(ctx context.Context, entity, linkedEntity, linkedID string, limit, offset int, inc []string, types []string) ([]byte, error) { zerolog.Ctx(ctx).Debug(). Str("entity", entity). Str("linked_entity", linkedEntity). Str("linked_id", linkedID). Int("limit", limit). Int("offset", offset). + Strs("types", types). Msg("provider browse") params := url.Values{} @@ -154,6 +159,10 @@ func (c *client) browse(ctx context.Context, entity, linkedEntity, linkedID stri params.Set("inc", joined) } + if typeStr := strings.Join(types, "|"); typeStr != "" { + params.Set("type", typeStr) + } + return c.get(ctx, entity, params) } diff --git a/internal/provider/musicbrainz/provider.go b/internal/provider/musicbrainz/provider.go index f55e0c1..a0a86ac 100644 --- a/internal/provider/musicbrainz/provider.go +++ b/internal/provider/musicbrainz/provider.go @@ -91,7 +91,7 @@ func (p *Provider) GetAlbum(ctx context.Context, id string) (*domain.Album, erro return mapAlbum(mb, release), nil } -func (p *Provider) SearchAlbums(ctx context.Context, query string, artist string, limit, offset int) (*domain.SearchResult[domain.Album], error) { +func (p *Provider) SearchAlbums(ctx context.Context, query string, artist string, limit, offset int, albumTypes []string) (*domain.SearchResult[domain.Album], error) { if limit <= 0 || limit > 100 { limit = 25 } @@ -105,6 +105,10 @@ func (p *Provider) SearchAlbums(ctx context.Context, query string, artist string luceneQuery = fmt.Sprintf("releasegroup:%s", escapeQuery(query)) } + if typeFilter := buildTypeFilter(albumTypes); typeFilter != "" { + luceneQuery += " AND " + typeFilter + } + data, err := p.client.search(ctx, "release-group", luceneQuery, limit, offset) if err != nil { return nil, fmt.Errorf("search albums: %w", err) @@ -134,12 +138,12 @@ func (p *Provider) SearchAlbums(ctx context.Context, query string, artist string return result, nil } -func (p *Provider) GetArtistAlbums(ctx context.Context, artistID string, limit, offset int) (*domain.SearchResult[domain.Album], error) { +func (p *Provider) GetArtistAlbums(ctx context.Context, artistID string, limit, offset int, albumTypes []string) (*domain.SearchResult[domain.Album], error) { if limit <= 0 || limit > 100 { limit = 25 } - data, err := p.client.browse(ctx, "release-group", "artist", artistID, limit, offset, []string{"artist-credits"}) + data, err := p.client.browseWithTypes(ctx, "release-group", "artist", artistID, limit, offset, []string{"artist-credits"}, albumTypes) if err != nil { return nil, fmt.Errorf("browse release-groups: %w", err) } @@ -311,6 +315,20 @@ func selectCanonicalRelease(releases []*mbRelease) *mbRelease { return best } +func buildTypeFilter(types []string) string { + if len(types) == 0 { + return "" + } + if len(types) == 1 { + return fmt.Sprintf("primarytype:%s", types[0]) + } + escaped := make([]string, len(types)) + for i, t := range types { + escaped[i] = fmt.Sprintf("primarytype:%s", t) + } + return "(" + strings.Join(escaped, " OR ") + ")" +} + func escapeQuery(s string) string { special := []string{`+`, `-`, `&`, `|`, `!`, `(`, `)`, `{`, `}`, `[`, `]`, `^`, `"`, `~`, `*`, `?`, `:`, `/`, `\`} result := s diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d350567..b941d63 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,8 +13,8 @@ type Provider interface { SearchArtists(ctx context.Context, query string, limit, offset int) (*domain.SearchResult[domain.Artist], error) GetAlbum(ctx context.Context, id string) (*domain.Album, error) - SearchAlbums(ctx context.Context, query string, artist string, limit, offset int) (*domain.SearchResult[domain.Album], error) - GetArtistAlbums(ctx context.Context, artistID string, limit, offset int) (*domain.SearchResult[domain.Album], error) + SearchAlbums(ctx context.Context, query string, artist string, limit, offset int, albumTypes []string) (*domain.SearchResult[domain.Album], error) + GetArtistAlbums(ctx context.Context, artistID string, limit, offset int, albumTypes []string) (*domain.SearchResult[domain.Album], error) GetTrack(ctx context.Context, id string) (*domain.Track, error) GetAlbumTracks(ctx context.Context, albumID string) ([]domain.Track, error) diff --git a/internal/server/server.go b/internal/server/server.go index c3bbd3f..a6c1d6d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -114,9 +114,10 @@ func (s *MetadataServer) SearchAlbums(ctx context.Context, req *metadatav1.Searc limit = 25 } - log.Debug().Str("query", req.Query).Str("artist", req.Artist).Int("limit", limit).Int("offset", int(req.Offset)).Msg("searching albums") + albumTypes := defaultAlbumTypes(req.AlbumTypes) + log.Debug().Str("query", req.Query).Str("artist", req.Artist).Int("limit", limit).Int("offset", int(req.Offset)).Strs("album_types", albumTypes).Msg("searching albums") - result, err := svc.SearchAlbums(ctx, req.Query, req.Artist, limit, int(req.Offset)) + result, err := svc.SearchAlbums(ctx, req.Query, req.Artist, limit, int(req.Offset), albumTypes) if err != nil { return nil, toGRPCError(ctx, err) } @@ -176,9 +177,10 @@ func (s *MetadataServer) GetArtistAlbums(ctx context.Context, req *metadatav1.Ge limit = 25 } - log.Debug().Str("artist_id", req.ArtistId).Int("limit", limit).Int("offset", int(req.Offset)).Msg("getting artist albums") + albumTypes := defaultAlbumTypes(req.AlbumTypes) + log.Debug().Str("artist_id", req.ArtistId).Int("limit", limit).Int("offset", int(req.Offset)).Strs("album_types", albumTypes).Msg("getting artist albums") - result, err := svc.GetArtistAlbums(ctx, req.ArtistId, limit, int(req.Offset)) + result, err := svc.GetArtistAlbums(ctx, req.ArtistId, limit, int(req.Offset), albumTypes) if err != nil { return nil, toGRPCError(ctx, err) } @@ -268,6 +270,15 @@ func (s *MetadataServer) SyncArtist(ctx context.Context, req *metadatav1.SyncArt return nil, status.Error(codes.Unimplemented, "sync not yet implemented") } +var defaultTypes = []string{"album", "ep", "single"} + +func defaultAlbumTypes(types []string) []string { + if len(types) > 0 { + return types + } + return defaultTypes +} + func toGRPCError(ctx context.Context, err error) error { if err == nil { return nil diff --git a/internal/service/metadata.go b/internal/service/metadata.go index 47ecbcd..049fac3 100644 --- a/internal/service/metadata.go +++ b/internal/service/metadata.go @@ -80,9 +80,9 @@ func (s *MetadataService) SearchArtists(ctx context.Context, query string, limit return s.provider.SearchArtists(ctx, query, limit, offset) } -func (s *MetadataService) SearchAlbums(ctx context.Context, query string, artist string, limit, offset int) (*domain.SearchResult[domain.Album], error) { - zerolog.Ctx(ctx).Debug().Str("query", query).Str("artist", artist).Str("provider", s.provider.Name()).Msg("searching albums via provider") - return s.provider.SearchAlbums(ctx, query, artist, limit, offset) +func (s *MetadataService) SearchAlbums(ctx context.Context, query string, artist string, limit, offset int, albumTypes []string) (*domain.SearchResult[domain.Album], error) { + zerolog.Ctx(ctx).Debug().Str("query", query).Str("artist", artist).Strs("album_types", albumTypes).Str("provider", s.provider.Name()).Msg("searching albums via provider") + return s.provider.SearchAlbums(ctx, query, artist, limit, offset, albumTypes) } func (s *MetadataService) GetAlbum(ctx context.Context, id string) (*domain.Album, error) { @@ -117,7 +117,7 @@ func (s *MetadataService) GetAlbum(ctx context.Context, id string) (*domain.Albu return album, nil } -func (s *MetadataService) GetArtistAlbums(ctx context.Context, artistID string, limit, offset int) (*domain.SearchResult[domain.Album], error) { +func (s *MetadataService) GetArtistAlbums(ctx context.Context, artistID string, limit, offset int, albumTypes []string) (*domain.SearchResult[domain.Album], error) { log := zerolog.Ctx(ctx) result, err := s.albums.GetByArtistID(ctx, artistID, limit, offset) @@ -128,8 +128,8 @@ func (s *MetadataService) GetArtistAlbums(ctx context.Context, artistID string, } metrics.CacheMisses.WithLabelValues("artist_albums").Inc() - log.Debug().Str("artist_id", artistID).Str("provider", s.provider.Name()).Msg("artist albums cache miss, querying provider") - return s.provider.GetArtistAlbums(ctx, artistID, limit, offset) + log.Debug().Str("artist_id", artistID).Strs("album_types", albumTypes).Str("provider", s.provider.Name()).Msg("artist albums cache miss, querying provider") + return s.provider.GetArtistAlbums(ctx, artistID, limit, offset, albumTypes) } func (s *MetadataService) GetTrack(ctx context.Context, id string) (*domain.Track, error) { diff --git a/pkg/gen/metadata/v1/metadata.pb.go b/pkg/gen/metadata/v1/metadata.pb.go index 248592b..8cf8484 100644 --- a/pkg/gen/metadata/v1/metadata.pb.go +++ b/pkg/gen/metadata/v1/metadata.pb.go @@ -321,6 +321,7 @@ type GetArtistAlbumsRequest struct { Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` Provider Provider `protobuf:"varint,4,opt,name=provider,proto3,enum=metadata.v1.Provider" json:"provider,omitempty"` + AlbumTypes []string `protobuf:"bytes,5,rep,name=album_types,json=albumTypes,proto3" json:"album_types,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -383,6 +384,13 @@ func (x *GetArtistAlbumsRequest) GetProvider() Provider { return Provider_PROVIDER_UNSPECIFIED } +func (x *GetArtistAlbumsRequest) GetAlbumTypes() []string { + if x != nil { + return x.AlbumTypes + } + return nil +} + type GetTrackRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Identifier: @@ -548,6 +556,7 @@ type SearchAlbumsRequest struct { Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` Offset int32 `protobuf:"varint,4,opt,name=offset,proto3" json:"offset,omitempty"` Provider Provider `protobuf:"varint,5,opt,name=provider,proto3,enum=metadata.v1.Provider" json:"provider,omitempty"` + AlbumTypes []string `protobuf:"bytes,6,rep,name=album_types,json=albumTypes,proto3" json:"album_types,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -617,6 +626,13 @@ func (x *SearchAlbumsRequest) GetProvider() Provider { return Provider_PROVIDER_UNSPECIFIED } +func (x *SearchAlbumsRequest) GetAlbumTypes() []string { + if x != nil { + return x.AlbumTypes + } + return nil +} + type SyncArtistRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Target: @@ -1808,12 +1824,14 @@ const file_metadata_v1_metadata_proto_rawDesc = "" + "\bexternal\x18\x02 \x01(\v2\x17.metadata.v1.ExternalIDH\x00R\bexternal\x121\n" + "\bprovider\x18\x03 \x01(\x0e2\x15.metadata.v1.ProviderR\bproviderB\f\n" + "\n" + - "identifier\"\x96\x01\n" + + "identifier\"\xb7\x01\n" + "\x16GetArtistAlbumsRequest\x12\x1b\n" + "\tartist_id\x18\x01 \x01(\tR\bartistId\x12\x14\n" + "\x05limit\x18\x02 \x01(\x05R\x05limit\x12\x16\n" + "\x06offset\x18\x03 \x01(\x05R\x06offset\x121\n" + - "\bprovider\x18\x04 \x01(\x0e2\x15.metadata.v1.ProviderR\bprovider\"\xb1\x01\n" + + "\bprovider\x18\x04 \x01(\x0e2\x15.metadata.v1.ProviderR\bprovider\x12\x1f\n" + + "\valbum_types\x18\x05 \x03(\tR\n" + + "albumTypes\"\xb1\x01\n" + "\x0fGetTrackRequest\x12\x10\n" + "\x02id\x18\x01 \x01(\tH\x00R\x02id\x125\n" + "\bexternal\x18\x02 \x01(\v2\x17.metadata.v1.ExternalIDH\x00R\bexternal\x12\x14\n" + @@ -1823,13 +1841,15 @@ const file_metadata_v1_metadata_proto_rawDesc = "" + "identifier\"e\n" + "\x15GetAlbumTracksRequest\x12\x19\n" + "\balbum_id\x18\x01 \x01(\tR\aalbumId\x121\n" + - "\bprovider\x18\x02 \x01(\x0e2\x15.metadata.v1.ProviderR\bprovider\"\xa4\x01\n" + + "\bprovider\x18\x02 \x01(\x0e2\x15.metadata.v1.ProviderR\bprovider\"\xc5\x01\n" + "\x13SearchAlbumsRequest\x12\x14\n" + "\x05query\x18\x01 \x01(\tR\x05query\x12\x16\n" + "\x06artist\x18\x02 \x01(\tR\x06artist\x12\x14\n" + "\x05limit\x18\x03 \x01(\x05R\x05limit\x12\x16\n" + "\x06offset\x18\x04 \x01(\x05R\x06offset\x121\n" + - "\bprovider\x18\x05 \x01(\x0e2\x15.metadata.v1.ProviderR\bprovider\"\x9d\x01\n" + + "\bprovider\x18\x05 \x01(\x0e2\x15.metadata.v1.ProviderR\bprovider\x12\x1f\n" + + "\valbum_types\x18\x06 \x03(\tR\n" + + "albumTypes\"\x9d\x01\n" + "\x11SyncArtistRequest\x12\x14\n" + "\x04name\x18\x01 \x01(\tH\x00R\x04name\x125\n" + "\bexternal\x18\x02 \x01(\v2\x17.metadata.v1.ExternalIDH\x00R\bexternal\x121\n" + diff --git a/proto/metadata/v1/metadata.proto b/proto/metadata/v1/metadata.proto index 7ec0f4f..46aab08 100644 --- a/proto/metadata/v1/metadata.proto +++ b/proto/metadata/v1/metadata.proto @@ -50,6 +50,7 @@ message GetArtistAlbumsRequest { int32 limit = 2; int32 offset = 3; Provider provider = 4; + repeated string album_types = 5; } message GetTrackRequest { @@ -72,6 +73,7 @@ message SearchAlbumsRequest { int32 limit = 3; int32 offset = 4; Provider provider = 5; + repeated string album_types = 6; } message SyncArtistRequest {