Add album/track releases with audio analysis, AnalyzeAlbumRelease RPC, Docker path auto-resolution, release parsing decision tree

This commit is contained in:
Alexander
2026-05-09 23:16:59 +02:00
parent 1e8506f146
commit 2740585261
25 changed files with 1841 additions and 125 deletions
+25 -70
View File
@@ -2,19 +2,13 @@ package workers
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/riverqueue/river"
"github.com/rs/zerolog/log"
"homelab.lan/music-agregator/internal/analysis"
"homelab.lan/music-agregator/internal/database"
"homelab.lan/music-agregator/internal/torrent"
)
@@ -32,7 +26,11 @@ type PollDownloadWorker struct {
TorrentClient torrent.TorrentClient
Downloads *database.DownloadRepository
DownloadFiles *database.DownloadFileRepository
AlbumReleases *database.AlbumReleaseRepository
TrackReleases *database.TrackReleaseRepository
RiverClient *river.Client[pgx.Tx]
PathMapper *torrent.PathMapper
Analyzer *analysis.ReleaseAnalyzer
}
func (w *PollDownloadWorker) Work(ctx context.Context, job *river.Job[PollDownloadArgs]) error {
@@ -77,14 +75,24 @@ func (w *PollDownloadWorker) Work(ctx context.Context, job *river.Job[PollDownlo
func (w *PollDownloadWorker) onCompleted(ctx context.Context, args PollDownloadArgs, t torrent.TorrentInfo) error {
log.Info().Str("hash", args.TorrentHash).Str("path", t.ContentPath).Msg("download completed")
if err := w.Downloads.SetCompleted(ctx, args.DownloadID, t.SavePath); err != nil {
contentPath := t.ContentPath
if w.PathMapper != nil {
contentPath = w.PathMapper.ToHost(contentPath)
}
savePath := t.SavePath
if w.PathMapper != nil {
savePath = w.PathMapper.ToHost(savePath)
}
if err := w.Downloads.SetCompleted(ctx, args.DownloadID, savePath); err != nil {
log.Error().Err(err).Msg("failed to update download as completed")
return err
}
files, err := scanAndHashFiles(t.ContentPath)
files, err := analysis.ScanAndHashFiles(contentPath)
if err != nil {
log.Error().Err(err).Str("path", t.ContentPath).Msg("failed to scan downloaded files")
log.Error().Err(err).Str("path", contentPath).Msg("failed to scan downloaded files")
return nil
}
@@ -102,6 +110,13 @@ func (w *PollDownloadWorker) onCompleted(ctx context.Context, args PollDownloadA
Int("files", len(files)).
Msg("download files scanned and hashed")
if w.Analyzer != nil {
_, _, err := w.Analyzer.AnalyzeAndPersist(ctx, args.DownloadID, contentPath)
if err != nil {
log.Error().Err(err).Msg("failed to analyze release")
}
}
return nil
}
@@ -148,63 +163,3 @@ func (w *PollDownloadWorker) RecoverOrphanedDownloads(ctx context.Context) {
}
}
}
var audioExtensions = map[string]bool{
".flac": true, ".mp3": true, ".aac": true, ".m4a": true,
".ape": true, ".wv": true, ".ogg": true, ".wav": true, ".alac": true,
}
func scanAndHashFiles(rootPath string) ([]*database.DownloadFile, error) {
var files []*database.DownloadFile
err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
ext := strings.ToLower(filepath.Ext(path))
relPath, _ := filepath.Rel(rootPath, path)
fileType := strings.TrimPrefix(ext, ".")
if fileType == "" {
return nil
}
df := &database.DownloadFile{
FilePath: relPath,
FileSize: info.Size(),
FileType: fileType,
}
if audioExtensions[ext] || ext == ".cue" || ext == ".log" {
hash, err := hashFile(path)
if err != nil {
log.Warn().Err(err).Str("path", path).Msg("failed to hash file")
} else {
df.SHA256Hash = hash
now := time.Now()
df.VerifiedAt = &now
}
}
files = append(files, df)
return nil
})
return files, err
}
func hashFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("opening file: %w", err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("hashing file: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}