Add album/track releases with audio analysis, AnalyzeAlbumRelease RPC, Docker path auto-resolution, release parsing decision tree
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user