package server import ( "context" "errors" "strings" "github.com/rs/zerolog" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/metadata-agregator/internal/domain" "github.com/metadata-agregator/internal/provider/musicbrainz" "github.com/metadata-agregator/internal/repository" "github.com/metadata-agregator/internal/service" metadatav1 "github.com/metadata-agregator/pkg/gen/metadata/v1" ) type MetadataServer struct { metadatav1.UnimplementedMetadataServiceServer services map[metadatav1.Provider]*service.MetadataService } func NewMetadataServer(services map[metadatav1.Provider]*service.MetadataService) *MetadataServer { return &MetadataServer{services: services} } func (s *MetadataServer) getService(ctx context.Context, p metadatav1.Provider) (*service.MetadataService, error) { if p == metadatav1.Provider_PROVIDER_UNSPECIFIED { p = metadatav1.Provider_PROVIDER_MUSICBRAINZ } zerolog.Ctx(ctx).Debug().Str("provider", p.String()).Msg("resolved provider") svc, ok := s.services[p] if !ok { zerolog.Ctx(ctx).Warn().Str("provider", p.String()).Msg("unknown provider requested") return nil, status.Errorf(codes.InvalidArgument, "unknown provider: %v", p) } return svc, nil } func (s *MetadataServer) GetArtist(ctx context.Context, req *metadatav1.GetArtistRequest) (*metadatav1.GetArtistResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } var id string switch v := req.Identifier.(type) { case *metadatav1.GetArtistRequest_Id: id = v.Id log.Debug().Str("lookup", "id").Str("artist_id", id).Msg("getting artist") case *metadatav1.GetArtistRequest_External: id = v.External.SourceId log.Debug().Str("lookup", "external").Str("source_id", id).Str("source", v.External.Source).Msg("getting artist") default: log.Warn().Msg("get artist called without identifier") return nil, status.Error(codes.InvalidArgument, "identifier required") } artist, err := svc.GetArtist(ctx, id) if err != nil { return nil, toGRPCError(ctx, err) } log.Trace().Str("artist_id", artist.ID).Str("name", artist.Name).Msg("artist found") return &metadatav1.GetArtistResponse{Artist: toProtoArtist(artist)}, nil } func (s *MetadataServer) SearchArtists(ctx context.Context, req *metadatav1.SearchArtistsRequest) (*metadatav1.SearchArtistsResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } limit := int(req.Limit) if limit <= 0 { limit = 25 } log.Debug().Str("query", req.Query).Int("limit", limit).Int("offset", int(req.Offset)).Msg("searching artists") result, err := svc.SearchArtists(ctx, req.Query, limit, int(req.Offset)) if err != nil { return nil, toGRPCError(ctx, err) } resp := &metadatav1.SearchArtistsResponse{ Total: int32(result.Total), } for _, a := range result.Items { resp.Artists = append(resp.Artists, toProtoArtist(&a)) } log.Trace().Int("total", result.Total).Int("returned", len(resp.Artists)).Msg("artist search complete") return resp, nil } func (s *MetadataServer) SearchAlbums(ctx context.Context, req *metadatav1.SearchAlbumsRequest) (*metadatav1.SearchAlbumsResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } limit := int(req.Limit) if limit <= 0 { limit = 25 } log.Debug().Str("query", req.Query).Str("artist", req.Artist).Int("limit", limit).Int("offset", int(req.Offset)).Msg("searching albums") result, err := svc.SearchAlbums(ctx, req.Query, req.Artist, limit, int(req.Offset)) if err != nil { return nil, toGRPCError(ctx, err) } resp := &metadatav1.SearchAlbumsResponse{ Total: int32(result.Total), } for _, a := range result.Items { resp.Albums = append(resp.Albums, toProtoAlbum(&a)) } log.Trace().Int("total", result.Total).Int("returned", len(resp.Albums)).Msg("album search complete") return resp, nil } func (s *MetadataServer) GetAlbum(ctx context.Context, req *metadatav1.GetAlbumRequest) (*metadatav1.GetAlbumResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } var id string switch v := req.Identifier.(type) { case *metadatav1.GetAlbumRequest_Id: id = v.Id log.Debug().Str("lookup", "id").Str("album_id", id).Msg("getting album") case *metadatav1.GetAlbumRequest_External: id = v.External.SourceId log.Debug().Str("lookup", "external").Str("source_id", id).Str("source", v.External.Source).Msg("getting album") default: log.Warn().Msg("get album called without identifier") return nil, status.Error(codes.InvalidArgument, "identifier required") } album, err := svc.GetAlbum(ctx, id) if err != nil { return nil, toGRPCError(ctx, err) } log.Trace().Str("album_id", album.ID).Str("title", album.Title).Msg("album found") return &metadatav1.GetAlbumResponse{Album: toProtoAlbum(album)}, nil } func (s *MetadataServer) GetArtistAlbums(ctx context.Context, req *metadatav1.GetArtistAlbumsRequest) (*metadatav1.GetArtistAlbumsResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } albumTypes := defaultAlbumTypes(req.AlbumTypes) log.Debug().Str("artist_id", req.ArtistId).Strs("album_types", albumTypes).Msg("getting artist albums") allAlbums, err := svc.GetArtistAlbums(ctx, req.ArtistId) if err != nil { return nil, toGRPCError(ctx, err) } filtered := filterAlbums(allAlbums, albumTypes) resp := &metadatav1.GetArtistAlbumsResponse{ Total: int32(len(filtered)), } for _, a := range filtered { resp.Albums = append(resp.Albums, toProtoAlbum(&a)) } log.Trace().Int("total_fetched", len(allAlbums)).Int("filtered", len(filtered)).Strs("album_types", albumTypes).Msg("artist albums retrieved") return resp, nil } func (s *MetadataServer) GetTrack(ctx context.Context, req *metadatav1.GetTrackRequest) (*metadatav1.GetTrackResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } var track *metadatav1.Track switch v := req.Identifier.(type) { case *metadatav1.GetTrackRequest_Id: log.Debug().Str("lookup", "id").Str("track_id", v.Id).Msg("getting track") t, err := svc.GetTrack(ctx, v.Id) if err != nil { return nil, toGRPCError(ctx, err) } track = toProtoTrack(t) case *metadatav1.GetTrackRequest_External: log.Debug().Str("lookup", "external").Str("source_id", v.External.SourceId).Str("source", v.External.Source).Msg("getting track") t, err := svc.GetTrack(ctx, v.External.SourceId) if err != nil { return nil, toGRPCError(ctx, err) } track = toProtoTrack(t) case *metadatav1.GetTrackRequest_Isrc: log.Debug().Str("lookup", "isrc").Str("isrc", v.Isrc).Msg("getting track") t, err := svc.GetTrackByISRC(ctx, v.Isrc) if err != nil { return nil, toGRPCError(ctx, err) } track = toProtoTrack(t) default: log.Warn().Msg("get track called without identifier") return nil, status.Error(codes.InvalidArgument, "identifier required") } log.Trace().Str("track_id", track.Id).Str("title", track.Title).Msg("track found") return &metadatav1.GetTrackResponse{Track: track}, nil } func (s *MetadataServer) GetAlbumTracks(ctx context.Context, req *metadatav1.GetAlbumTracksRequest) (*metadatav1.GetAlbumTracksResponse, error) { log := zerolog.Ctx(ctx) svc, err := s.getService(ctx, req.Provider) if err != nil { return nil, err } log.Debug().Str("album_id", req.AlbumId).Msg("getting album tracks") tracks, err := svc.GetAlbumTracks(ctx, req.AlbumId) if err != nil { return nil, toGRPCError(ctx, err) } resp := &metadatav1.GetAlbumTracksResponse{} for _, t := range tracks { resp.Tracks = append(resp.Tracks, toProtoTrack(&t)) } log.Trace().Int("track_count", len(resp.Tracks)).Msg("album tracks retrieved") return resp, nil } func (s *MetadataServer) SyncArtist(ctx context.Context, req *metadatav1.SyncArtistRequest) (*metadatav1.SyncArtistResponse, error) { zerolog.Ctx(ctx).Warn().Msg("sync artist called but not implemented") 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 filterAlbums(albums []domain.Album, types []string) []domain.Album { allowed := make(map[string]bool, len(types)) for _, t := range types { allowed[strings.ToLower(t)] = true } var result []domain.Album for _, a := range albums { if len(a.SecondaryTypes) > 0 { continue } if !allowed[strings.ToLower(a.Type)] { continue } result = append(result, a) } return result } func toGRPCError(ctx context.Context, err error) error { if err == nil { return nil } log := zerolog.Ctx(ctx) if errors.Is(err, musicbrainz.ErrBadRequest) { log.Debug().Err(err).Msg("bad request") return status.Error(codes.InvalidArgument, err.Error()) } if errors.Is(err, repository.ErrNotFound) { log.Debug().Msg("entity not found") return status.Error(codes.NotFound, "not found") } if errors.Is(err, musicbrainz.ErrNotFound) { log.Debug().Msg("provider entity not found") return status.Error(codes.NotFound, "not found") } if errors.Is(err, musicbrainz.ErrRateLimited) { log.Warn().Msg("provider rate limited") return status.Error(codes.ResourceExhausted, "rate limited") } log.Error().Err(err).Msg("internal error") return status.Errorf(codes.Internal, "internal error: %v", err) }