Add album/track releases with audio analysis, AnalyzeAlbumRelease RPC, Docker path auto-resolution, release parsing decision tree
This commit is contained in:
+151
-5
@@ -15,6 +15,7 @@ import (
|
||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
|
||||
|
||||
"homelab.lan/music-agregator/internal/analysis"
|
||||
"homelab.lan/music-agregator/internal/config"
|
||||
"homelab.lan/music-agregator/internal/database"
|
||||
"homelab.lan/music-agregator/internal/indexer"
|
||||
@@ -38,13 +39,17 @@ type MusicAgregatorService struct {
|
||||
torrentClient torrent.TorrentClient
|
||||
magnetResolver torrentParser.Resolver
|
||||
riverClient *river.Client[pgx.Tx]
|
||||
pathMapper *torrent.PathMapper
|
||||
torrents *database.TorrentRepository
|
||||
downloads *database.DownloadRepository
|
||||
artists *database.ArtistRepository
|
||||
downloadFiles *database.DownloadFileRepository
|
||||
albumReleases *database.AlbumReleaseRepository
|
||||
trackReleases *database.TrackReleaseRepository
|
||||
analyzer *analysis.ReleaseAnalyzer
|
||||
}
|
||||
|
||||
func NewMusicAgregatorService(cfg config.Config, riverClient *river.Client[pgx.Tx], torrentClient torrent.TorrentClient, db *database.DB) (*MusicAgregatorService, error) {
|
||||
func NewMusicAgregatorService(cfg config.Config, riverClient *river.Client[pgx.Tx], torrentClient torrent.TorrentClient, pathMapper *torrent.PathMapper, db *database.DB) (*MusicAgregatorService, error) {
|
||||
idx, err := indexer.NewIndexerService(cfg, riverClient, nil)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create IndexerService")
|
||||
@@ -70,10 +75,14 @@ func NewMusicAgregatorService(cfg config.Config, riverClient *river.Client[pgx.T
|
||||
torrentClient: torrentClient,
|
||||
magnetResolver: magnetResolver,
|
||||
riverClient: riverClient,
|
||||
pathMapper: pathMapper,
|
||||
torrents: database.NewTorrentRepository(db.Pool),
|
||||
downloads: database.NewDownloadRepository(db.Pool),
|
||||
artists: database.NewArtistRepository(db.Pool),
|
||||
downloadFiles: database.NewDownloadFileRepository(db.Pool),
|
||||
albumReleases: database.NewAlbumReleaseRepository(db.Pool),
|
||||
trackReleases: database.NewTrackReleaseRepository(db.Pool),
|
||||
analyzer: analysis.NewReleaseAnalyzer(db),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -83,6 +92,7 @@ func NewMusicAgregatorServiceWithDeps(
|
||||
torrentClient torrent.TorrentClient,
|
||||
magnetResolver torrentParser.Resolver,
|
||||
riverClient *river.Client[pgx.Tx],
|
||||
pathMapper *torrent.PathMapper,
|
||||
db *database.DB,
|
||||
) *MusicAgregatorService {
|
||||
return &MusicAgregatorService{
|
||||
@@ -91,10 +101,14 @@ func NewMusicAgregatorServiceWithDeps(
|
||||
torrentClient: torrentClient,
|
||||
magnetResolver: magnetResolver,
|
||||
riverClient: riverClient,
|
||||
pathMapper: pathMapper,
|
||||
torrents: database.NewTorrentRepository(db.Pool),
|
||||
downloads: database.NewDownloadRepository(db.Pool),
|
||||
artists: database.NewArtistRepository(db.Pool),
|
||||
downloadFiles: database.NewDownloadFileRepository(db.Pool),
|
||||
albumReleases: database.NewAlbumReleaseRepository(db.Pool),
|
||||
trackReleases: database.NewTrackReleaseRepository(db.Pool),
|
||||
analyzer: analysis.NewReleaseAnalyzer(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +218,15 @@ func (service *MusicAgregatorService) GetAlbum(ctx context.Context, req *pb.GetA
|
||||
return nil, fmt.Errorf("album not found: %w", err)
|
||||
}
|
||||
|
||||
info, err := service.buildAlbumInfo(ctx, dbAlbum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.GetAlbumResponse{Info: info}, nil
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) buildAlbumInfo(ctx context.Context, dbAlbum *database.Album) (*pb.AlbumInfo, error) {
|
||||
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")
|
||||
@@ -299,10 +322,128 @@ func (service *MusicAgregatorService) GetAlbum(ctx context.Context, req *pb.GetA
|
||||
tracks = append(tracks, td)
|
||||
}
|
||||
|
||||
return &pb.GetAlbumResponse{
|
||||
info := &pb.AlbumInfo{
|
||||
Album: album,
|
||||
Tracks: tracks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
albumReleases, err := service.albumReleases.GetByAlbumID(ctx, dbAlbum.ID)
|
||||
if err == nil && len(albumReleases) > 0 {
|
||||
ar := albumReleases[0]
|
||||
releaseDetail := &pb.AlbumReleaseDetail{
|
||||
Id: ar.ID,
|
||||
Format: ar.Format,
|
||||
Channels: int32(ar.Channels),
|
||||
IsLossless: ar.IsLossless,
|
||||
TotalSize: ar.TotalSize,
|
||||
TotalDurationMs: int32(ar.TotalDurationMs),
|
||||
TrackCount: int32(ar.TrackCount),
|
||||
HasCoverArt: ar.HasCoverArt,
|
||||
HasCueSheet: ar.HasCueSheet,
|
||||
HasRipLog: ar.HasRipLog,
|
||||
Path: ar.Path,
|
||||
}
|
||||
if ar.BitDepth != nil {
|
||||
releaseDetail.BitDepth = int32(*ar.BitDepth)
|
||||
}
|
||||
if ar.SampleRate != nil {
|
||||
releaseDetail.SampleRate = int32(*ar.SampleRate)
|
||||
}
|
||||
if ar.Source != nil {
|
||||
releaseDetail.Source = *ar.Source
|
||||
}
|
||||
|
||||
trackReleases, err := service.trackReleases.GetByAlbumReleaseID(ctx, ar.ID)
|
||||
if err == nil {
|
||||
for _, tr := range trackReleases {
|
||||
trd := &pb.TrackReleaseDetail{
|
||||
Id: tr.ID,
|
||||
Title: tr.Title,
|
||||
TrackNumber: int32(tr.TrackNumber),
|
||||
DiscNumber: int32(tr.DiscNumber),
|
||||
Format: tr.Format,
|
||||
Channels: int32(tr.Channels),
|
||||
FileSize: tr.FileSize,
|
||||
FilePath: tr.FilePath,
|
||||
}
|
||||
if tr.TrackID != nil {
|
||||
trd.TrackId = *tr.TrackID
|
||||
}
|
||||
if tr.DurationMs != nil {
|
||||
trd.DurationMs = int32(*tr.DurationMs)
|
||||
}
|
||||
if tr.BitDepth != nil {
|
||||
trd.BitDepth = int32(*tr.BitDepth)
|
||||
}
|
||||
if tr.SampleRate != nil {
|
||||
trd.SampleRate = int32(*tr.SampleRate)
|
||||
}
|
||||
if tr.BitrateKbps != nil {
|
||||
trd.BitrateKbps = int32(*tr.BitrateKbps)
|
||||
}
|
||||
releaseDetail.Tracks = append(releaseDetail.Tracks, trd)
|
||||
}
|
||||
}
|
||||
|
||||
info.Release = releaseDetail
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) AnalyzeAlbumRelease(ctx context.Context, req *pb.AnalyzeAlbumReleaseRequest) (*pb.AnalyzeAlbumReleaseResponse, error) {
|
||||
dbAlbum, err := service.metadata.GetAlbumByID(ctx, req.GetAlbumId())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("album not found: %w", err)
|
||||
}
|
||||
|
||||
downloads, err := service.downloads.GetByAlbumID(ctx, dbAlbum.ID)
|
||||
if err != nil || len(downloads) == 0 {
|
||||
return nil, fmt.Errorf("no downloads found for album")
|
||||
}
|
||||
|
||||
var download *database.Download
|
||||
for _, d := range downloads {
|
||||
if d.State == "completed" || d.State == "seeding" {
|
||||
download = d
|
||||
break
|
||||
}
|
||||
}
|
||||
if download == nil {
|
||||
return nil, fmt.Errorf("no completed download found for album")
|
||||
}
|
||||
|
||||
contentPath := ""
|
||||
existingRelease, err := service.albumReleases.GetByDownloadID(ctx, download.ID)
|
||||
if err == nil {
|
||||
contentPath = existingRelease.Path
|
||||
}
|
||||
|
||||
if contentPath == "" && download.QbitHash != "" {
|
||||
torrents, err := service.torrentClient.Find(torrent.FindOptions{Hash: download.QbitHash})
|
||||
if err == nil && len(torrents) > 0 {
|
||||
contentPath = torrents[0].ContentPath
|
||||
if service.pathMapper != nil {
|
||||
contentPath = service.pathMapper.ToHost(contentPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if contentPath == "" {
|
||||
return nil, fmt.Errorf("cannot determine content path for download")
|
||||
}
|
||||
|
||||
_, _, err = service.analyzer.AnalyzeAndPersist(ctx, download.ID, contentPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyzing release: %w", err)
|
||||
}
|
||||
|
||||
info, err := service.buildAlbumInfo(ctx, dbAlbum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.AnalyzeAlbumReleaseResponse{Info: info}, nil
|
||||
}
|
||||
|
||||
func (service *MusicAgregatorService) MonitorAlbum(ctx context.Context, req *pb.MonitorAlbumRequest) (*pb.MonitorAlbumResponse, error) {
|
||||
@@ -475,8 +616,13 @@ func (service *MusicAgregatorService) addToTorrentClient(best parsedItem) error
|
||||
}
|
||||
}
|
||||
|
||||
savePath := ""
|
||||
if service.pathMapper != nil {
|
||||
savePath = service.pathMapper.ContainerDownloadPath()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(best.item.DownloadLink, "magnet:") {
|
||||
if err := service.torrentClient.AddMagnet(best.item.DownloadLink); err != nil {
|
||||
if err := service.torrentClient.AddMagnet(best.item.DownloadLink, savePath); err != nil {
|
||||
log.Error().Err(err).Str("title", best.item.Title).Msg("failed to add magnet to client")
|
||||
return err
|
||||
}
|
||||
@@ -488,7 +634,7 @@ func (service *MusicAgregatorService) addToTorrentClient(best parsedItem) error
|
||||
if err := service.torrentClient.AddTorrent(torrent.TorrentFile{
|
||||
Filename: best.rel.Album + ".torrent",
|
||||
Data: best.torrentData,
|
||||
}); err != nil {
|
||||
}, savePath); err != nil {
|
||||
log.Error().Err(err).Str("title", best.item.Title).Msg("failed to add torrent to client")
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user