diff --git a/internal/metadata/service.go b/internal/metadata/service.go index 96ae188..6ddc9a5 100644 --- a/internal/metadata/service.go +++ b/internal/metadata/service.go @@ -14,6 +14,7 @@ type MetadataService struct { client metadataPb.MetadataServiceClient artists *database.ArtistRepository albums *database.AlbumRepository + tracks *database.TrackRepository } func NewMetadataService(client metadataPb.MetadataServiceClient, db *database.DB) *MetadataService { @@ -21,6 +22,7 @@ func NewMetadataService(client metadataPb.MetadataServiceClient, db *database.DB client: client, artists: database.NewArtistRepository(db.Pool), albums: database.NewAlbumRepository(db.Pool), + tracks: database.NewTrackRepository(db.Pool), } } @@ -35,8 +37,8 @@ func (s *MetadataService) GetAlbum(ctx context.Context, albumID string) (*metada album := resp.GetAlbum() if _, err := s.albums.GetByExternalID(ctx, album.GetId()); err != nil { - s.persistArtist(ctx, album) - s.persistAlbum(ctx, album) + s.PersistArtist(ctx, album, database.Monitored) + s.PersistAlbum(ctx, album, database.Monitored) } return album, nil @@ -52,10 +54,24 @@ func (s *MetadataService) GetArtistAlbums(ctx context.Context, artistExternalID return resp.GetAlbums(), nil } +func (s *MetadataService) GetAlbumTracks(ctx context.Context, albumExternalID string) ([]*metadataPb.Track, error) { + resp, err := s.client.GetAlbumTracks(ctx, &metadataPb.GetAlbumTracksRequest{ + AlbumId: albumExternalID, + }) + if err != nil { + return nil, fmt.Errorf("fetching album tracks: %w", err) + } + return resp.GetTracks(), nil +} + func (s *MetadataService) GetArtistByExternalID(ctx context.Context, externalID string) (*database.Artist, error) { return s.artists.GetByExternalID(ctx, externalID) } +func (s *MetadataService) GetAlbumByID(ctx context.Context, id string) (*database.Album, error) { + return s.albums.GetByID(ctx, id) +} + func (s *MetadataService) GetAlbumByExternalID(ctx context.Context, externalID string) (*database.Album, error) { return s.albums.GetByExternalID(ctx, externalID) } @@ -64,7 +80,11 @@ func (s *MetadataService) GetAlbumsByArtistID(ctx context.Context, artistID stri return s.albums.GetByArtistID(ctx, artistID) } -func (s *MetadataService) persistArtist(ctx context.Context, album *metadataPb.Album) { +func (s *MetadataService) GetTracksByAlbumID(ctx context.Context, albumID string) ([]*database.Track, error) { + return s.tracks.GetByAlbumID(ctx, albumID) +} + +func (s *MetadataService) PersistArtist(ctx context.Context, album *metadataPb.Album, state database.MonitorState) { if len(album.GetArtists()) == 0 { return } @@ -82,14 +102,14 @@ func (s *MetadataService) persistArtist(ctx context.Context, album *metadataPb.A Country: artist.GetCountry(), Genres: genres, ImageURL: artist.GetImageUrl(), - MonitorState: database.Monitored, + MonitorState: state, }) if err != nil { log.Warn().Err(err).Str("name", artist.GetName()).Msg("failed to persist artist") } } -func (s *MetadataService) persistAlbum(ctx context.Context, album *metadataPb.Album) { +func (s *MetadataService) PersistAlbum(ctx context.Context, album *metadataPb.Album, state database.MonitorState) { artistID := "" if len(album.GetArtists()) > 0 { a, err := s.artists.GetByExternalID(ctx, album.GetArtists()[0].GetArtist().GetId()) @@ -123,9 +143,26 @@ func (s *MetadataService) persistAlbum(ctx context.Context, album *metadataPb.Al Label: labelName, Genres: genres, CoverURL: album.GetCoverUrl(), - MonitorState: database.Monitored, + MonitorState: state, }) if err != nil { log.Warn().Err(err).Str("title", album.GetTitle()).Msg("failed to persist album") } } + +func (s *MetadataService) PersistTracks(ctx context.Context, albumDBID string, tracks []*metadataPb.Track) { + for _, t := range tracks { + err := s.tracks.Create(ctx, &database.Track{ + ExternalID: t.GetId(), + AlbumID: albumDBID, + Title: t.GetTitle(), + DurationMS: int(t.GetDurationMs()), + ISRC: t.GetIsrc(), + DiscNumber: int(t.GetDiscNumber()), + TrackNumber: int(t.GetTrackNumber()), + }) + if err != nil { + log.Warn().Err(err).Str("title", t.GetTitle()).Msg("failed to persist track") + } + } +} diff --git a/internal/server.go b/internal/server.go index 08d0af6..028658f 100644 --- a/internal/server.go +++ b/internal/server.go @@ -33,6 +33,10 @@ func (s *MusicAgregatorServer) GetArtists(ctx context.Context, req *pb.GetArtist return s.service.GetArtists(ctx, req) } +func (s *MusicAgregatorServer) GetAlbum(ctx context.Context, req *pb.GetAlbumRequest) (*pb.GetAlbumResponse, error) { + return s.service.GetAlbum(ctx, req) +} + func (s *MusicAgregatorServer) MonitorAlbum(ctx context.Context, req *pb.MonitorAlbumRequest) (*pb.MonitorAlbumResponse, error) { return s.service.MonitorAlbum(ctx, req) } diff --git a/internal/service.go b/internal/service.go index 2efe6f8..0153e93 100644 --- a/internal/service.go +++ b/internal/service.go @@ -41,6 +41,7 @@ type MusicAgregatorService struct { torrents *database.TorrentRepository downloads *database.DownloadRepository artists *database.ArtistRepository + downloadFiles *database.DownloadFileRepository } func NewMusicAgregatorService(cfg config.Config, riverClient *river.Client[pgx.Tx], db *database.DB) (*MusicAgregatorService, error) { @@ -78,6 +79,7 @@ func NewMusicAgregatorService(cfg config.Config, riverClient *river.Client[pgx.T torrents: database.NewTorrentRepository(db.Pool), downloads: database.NewDownloadRepository(db.Pool), artists: database.NewArtistRepository(db.Pool), + downloadFiles: database.NewDownloadFileRepository(db.Pool), }, nil } @@ -123,6 +125,10 @@ func (service *MusicAgregatorService) buildAlbumsForArtist(ctx context.Context, return nil, fmt.Errorf("fetching metadata albums: %w", err) } + for _, ma := range metadataAlbums { + service.metadata.PersistAlbum(ctx, ma, database.Unmonitored) + } + dbAlbums, err := service.metadata.GetAlbumsByArtistID(ctx, artist.ID) if err != nil { log.Warn().Err(err).Str("artist_id", artist.ID).Msg("failed to get local albums") @@ -177,6 +183,113 @@ func (service *MusicAgregatorService) buildAlbumsForArtist(ctx context.Context, return albums, nil } +func (service *MusicAgregatorService) GetAlbum(ctx context.Context, req *pb.GetAlbumRequest) (*pb.GetAlbumResponse, error) { + dbAlbum, err := service.metadata.GetAlbumByID(ctx, req.GetAlbumId()) + if err != nil { + return nil, fmt.Errorf("album not found: %w", err) + } + + metadataAlbum, err := service.metadata.GetAlbum(ctx, dbAlbum.ExternalID) + if err != nil { + log.Error().Err(err).Str("album_id", dbAlbum.ExternalID).Msg("failed to get album from metadata") + return nil, fmt.Errorf("fetching album: %w", err) + } + + metadataTracks, err := service.metadata.GetAlbumTracks(ctx, dbAlbum.ExternalID) + if err != nil { + log.Warn().Err(err).Str("album_id", dbAlbum.ExternalID).Msg("failed to get tracks from metadata") + } + + service.metadata.PersistTracks(ctx, dbAlbum.ID, metadataTracks) + + album := &pb.AlbumDetail{ + Id: dbAlbum.ID, + ExternalId: metadataAlbum.GetId(), + Title: metadataAlbum.GetTitle(), + AlbumType: metadataAlbum.GetAlbumType(), + ReleaseDate: metadataAlbum.GetReleaseDate(), + TotalTracks: metadataAlbum.GetTotalTracks(), + TotalDiscs: metadataAlbum.GetTotalDiscs(), + CoverUrl: metadataAlbum.GetCoverUrl(), + MonitorState: toProtoMonitorState(dbAlbum.MonitorState), + } + + if metadataAlbum.GetLabel() != nil { + album.Label = metadataAlbum.GetLabel().GetName() + } + for _, g := range metadataAlbum.GetGenres() { + album.Genres = append(album.Genres, g.GetName()) + } + + downloads, err := service.downloads.GetByAlbumID(ctx, dbAlbum.ID) + if err == nil && len(downloads) > 0 { + best := downloads[0] + album.Download = &pb.DownloadInfo{ + State: best.State, + Format: best.Format, + Quality: best.Quality, + SavePath: best.SavePath, + } + } + + var downloadFilesByTrackID map[string]*database.DownloadFile + if album.Download != nil { + files, err := service.downloadFiles.GetByDownloadID(ctx, downloads[0].ID) + if err == nil { + downloadFilesByTrackID = make(map[string]*database.DownloadFile, len(files)) + for _, f := range files { + if f.TrackID != nil { + downloadFilesByTrackID[*f.TrackID] = f + } + } + } + } + + dbTracks, _ := service.metadata.GetTracksByAlbumID(ctx, dbAlbum.ID) + dbTracksByExternalID := make(map[string]*database.Track, len(dbTracks)) + for _, t := range dbTracks { + dbTracksByExternalID[t.ExternalID] = t + } + + tracks := make([]*pb.TrackDetail, 0, len(metadataTracks)) + for _, mt := range metadataTracks { + td := &pb.TrackDetail{ + ExternalId: mt.GetId(), + Title: mt.GetTitle(), + DurationMs: mt.GetDurationMs(), + DiscNumber: mt.GetDiscNumber(), + TrackNumber: mt.GetTrackNumber(), + Isrc: mt.GetIsrc(), + Explicit: mt.GetExplicit(), + } + + for _, ac := range mt.GetArtists() { + td.Artists = append(td.Artists, &pb.ArtistCredit{ + Id: ac.GetArtist().GetId(), + Name: ac.GetArtist().GetName(), + }) + } + + if dbTrack, ok := dbTracksByExternalID[mt.GetId()]; ok { + td.Id = dbTrack.ID + if df, ok := downloadFilesByTrackID[dbTrack.ID]; ok { + td.File = &pb.TrackFile{ + Path: df.FilePath, + Format: df.FileType, + Size: df.FileSize, + } + } + } + + tracks = append(tracks, td) + } + + return &pb.GetAlbumResponse{ + Album: album, + Tracks: tracks, + }, nil +} + func (service *MusicAgregatorService) MonitorAlbum(ctx context.Context, req *pb.MonitorAlbumRequest) (*pb.MonitorAlbumResponse, error) { album, err := service.metadata.GetAlbum(ctx, req.GetAlbumId()) if err != nil { diff --git a/proto/music_agregator/v1/music_agregator.proto b/proto/music_agregator/v1/music_agregator.proto index d725f7d..3d909a8 100644 --- a/proto/music_agregator/v1/music_agregator.proto +++ b/proto/music_agregator/v1/music_agregator.proto @@ -5,6 +5,7 @@ option go_package = "homelab.lan/music-agregator/gen/music_agregator/v1/"; service MusicAgregatorService { rpc MonitorAlbum(MonitorAlbumRequest) returns (MonitorAlbumResponse) {} rpc GetArtists(GetArtistsRequest) returns (GetArtistsResponse) {} + rpc GetAlbum(GetAlbumRequest) returns (GetAlbumResponse) {} } message MonitorAlbumRequest { @@ -74,6 +75,39 @@ message DownloadInfo { string save_path = 4; } +message GetAlbumRequest { + string album_id = 1; +} + +message GetAlbumResponse { + AlbumDetail album = 1; + repeated TrackDetail tracks = 2; +} + +message TrackDetail { + string id = 1; + string external_id = 2; + string title = 3; + int32 duration_ms = 4; + int32 disc_number = 5; + int32 track_number = 6; + string isrc = 7; + bool explicit = 8; + repeated ArtistCredit artists = 9; + TrackFile file = 10; +} + +message ArtistCredit { + string id = 1; + string name = 2; +} + +message TrackFile { + string path = 1; + string format = 2; + int64 size = 3; +} + message MonitoredRelease { string info_hash = 1; string artist = 2;