From 2740585261a7e6db671b5bca5b4282a403d69641 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 9 May 2026 23:16:59 +0200 Subject: [PATCH] Add album/track releases with audio analysis, AnalyzeAlbumRelease RPC, Docker path auto-resolution, release parsing decision tree --- cmd/music-agregator/main.go | 16 +- .../flows/release-parsing-decision-tree.png | Bin 0 -> 152442 bytes .../flows/release-parsing-decision-tree.puml | 193 ++++++ go.mod | 34 +- go.sum | 73 ++- internal/analysis/analyzer.go | 285 +++++++++ internal/audio/analyzer.go | 35 ++ internal/audio/flac.go | 27 + internal/audio/mp3.go | 54 ++ internal/config/config.go | 9 +- internal/database/album_release_repository.go | 91 +++ internal/database/download_repository.go | 12 + internal/database/track_release_repository.go | 79 +++ internal/server.go | 8 +- internal/service.go | 156 ++++- internal/torrent/client.go | 5 +- internal/torrent/path_mapper.go | 88 +++ internal/torrent/qbittorrent_client.go | 42 +- internal/torrent/service.go | 2 +- internal/workers/poll_download.go | 95 +-- .../music_agregator/v1/music_agregator.proto | 50 +- test/component/mocks_test.go | 26 +- test/component/monitor_album_test.go | 8 +- test/component/setup_test.go | 1 + test/unit/generic_parser_test.go | 577 ++++++++++++++++++ 25 files changed, 1841 insertions(+), 125 deletions(-) create mode 100644 docs/architecture/flows/release-parsing-decision-tree.png create mode 100644 docs/architecture/flows/release-parsing-decision-tree.puml create mode 100644 internal/analysis/analyzer.go create mode 100644 internal/audio/analyzer.go create mode 100644 internal/audio/flac.go create mode 100644 internal/audio/mp3.go create mode 100644 internal/database/album_release_repository.go create mode 100644 internal/database/track_release_repository.go create mode 100644 internal/torrent/path_mapper.go create mode 100644 test/unit/generic_parser_test.go diff --git a/cmd/music-agregator/main.go b/cmd/music-agregator/main.go index e6b8ce7..00642b3 100644 --- a/cmd/music-agregator/main.go +++ b/cmd/music-agregator/main.go @@ -26,6 +26,7 @@ import ( grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" "homelab.lan/music-agregator/internal" + "homelab.lan/music-agregator/internal/analysis" "homelab.lan/music-agregator/internal/config" "homelab.lan/music-agregator/internal/database" "homelab.lan/music-agregator/internal/hello" @@ -82,12 +83,16 @@ type riverSetup struct { cacheRefreshWorker *indexer.CacheRefreshWorker } -func setupRiver(ctx context.Context, cfg config.Config, db *database.DB, torrentClient torrent.TorrentClient) *riverSetup { +func setupRiver(ctx context.Context, cfg config.Config, db *database.DB, torrentClient torrent.TorrentClient, pathMapper *torrent.PathMapper) *riverSetup { cacheWorker := &indexer.CacheRefreshWorker{} pollWorker := &workers.PollDownloadWorker{ Downloads: database.NewDownloadRepository(db.Pool), DownloadFiles: database.NewDownloadFileRepository(db.Pool), + AlbumReleases: database.NewAlbumReleaseRepository(db.Pool), + TrackReleases: database.NewTrackReleaseRepository(db.Pool), TorrentClient: torrentClient, + PathMapper: pathMapper, + Analyzer: analysis.NewReleaseAnalyzer(db), } riverWorkers := river.NewWorkers() @@ -161,9 +166,14 @@ func serveGrpc(config config.Config) { log.Fatal().Err(err).Msg("failed to create torrent client") } - rs := setupRiver(ctx, config, db, torrentClient) + pathMapper, err := torrent.NewPathMapper(config.Torrent.ContainerName, torrentClient) + if err != nil { + log.Fatal().Err(err).Msg("failed to create path mapper") + } - musiscAgregatorSeerver, err := internal.NewMusicAgregatorServer(config, rs.client, torrentClient, db) + rs := setupRiver(ctx, config, db, torrentClient, pathMapper) + + musiscAgregatorSeerver, err := internal.NewMusicAgregatorServer(config, rs.client, torrentClient, pathMapper, db) if err != nil { log.Fatal().Err(err).Msg("failed to create MusicAgregatorServer") } diff --git a/docs/architecture/flows/release-parsing-decision-tree.png b/docs/architecture/flows/release-parsing-decision-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..76e8cd82595b42a1c81d2949a4212ac7f10556ea GIT binary patch literal 152442 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 Tx^5x^0|SGntDnm{r-UW|{DEQr literal 0 HcmV?d00001 diff --git a/docs/architecture/flows/release-parsing-decision-tree.puml b/docs/architecture/flows/release-parsing-decision-tree.puml new file mode 100644 index 0000000..e268059 --- /dev/null +++ b/docs/architecture/flows/release-parsing-decision-tree.puml @@ -0,0 +1,193 @@ +@startuml Release Parsing Decision Tree +skinparam ActivityBackgroundColor #f8f8f8 +skinparam ActivityBorderColor #333333 +skinparam DiamondBackgroundColor #fffde7 +skinparam NoteBackgroundColor #e3f2fd + +title Release Parsing Decision Tree + +start + +partition "1. Resolve Torrent Data" { + if (DownloadLink starts\nwith "magnet:?") then (yes) + :MagnetResolver.Resolve(magnetURI); + note right + DHT lookup via anacrolix/torrent + 30s timeout, 15s early exit + if peers but none active + end note + if (Resolve succeeded?) then (yes) + :torrentData = resolved bytes; + else (no) + :fallback to **title-only parse**; + note right + parser.Parse(item.Title) + No torrent data available + No info_hash computed + end note + goto TitleOnlyParse + endif + else (HTTP link) + :downloadTorrentData(url); + note right + HTTP GET with 30s timeout + Expects .torrent file bytes + end note + if (Download succeeded?) then (yes) + :torrentData = downloaded bytes; + else (no) + :fallback to **title-only parse**; + goto TitleOnlyParse + endif + endif +} + +partition "2. ParseTorrent (torrentData + metadata album)" { + + partition "2a. Fill from Metadata Album" { + :Artist = album.Artists[0].Name; + :Album = album.Title; + :Year = album.ReleaseDate[:4]; + :Type = album.AlbumType + (album/ep/single/compilation/...); + :Genres = album.Genres[].Name; + :Label = album.Label.Name; + :TrackCount = album.TotalTracks; + :ReleaseCount = album.TotalDiscs; + } + + partition "2b. Fill from Torrent Data" { + :metainfo.Load(torrentData); + note right + Bencode decode via + anacrolix/torrent/metainfo + end note + if (Parse failed?) then (yes) + :Append to ParseErrors; + :Skip torrent analysis; + else (no) + :info = mi.UnmarshalInfo(); + if (Unmarshal failed?) then (yes) + :Append to ParseErrors; + :Skip torrent analysis; + else (no) + :RawTitle = info.Name; + :InfoHash = SHA1(info dict); + + if (info.Files is empty?\n(single-file torrent)) then (yes) + :ext = filepath.Ext(info.Name); + if (ext is audio?\n(.flac/.mp3/.aac/...)) then (yes) + :Format = audioExtensions[ext]; + :AudioFileCount = 1; + :TotalAudioSize = info.Length; + else (no) + :Format = unknown; + endif + else (multi-file torrent) + :Iterate all files in torrent; + + repeat + :file = next torrent file; + :ext = filepath.Ext(file.Path); + + if (ext is audio?) then (yes) + :formatCounts[ext]++; + :formatSizes[ext] += file.Length; + :TrackNames += cleanTrackName(file); + note right + Strip leading "01. " or "1 - " + from filename + end note + elseif (ext is .jpg/.jpeg/.png?) then (yes) + :HasCoverArt = true; + elseif (ext is .cue?) then (yes) + :HasCueSheet = true; + elseif (ext is .log?) then (yes) + :HasRipLog = true; + endif + repeat while (more files?) + + :Format = dominant format\n(most audio files); + :AudioFileCount = count of dominant; + :TotalAudioSize = sum of dominant; + endif + + if (HasRipLog?) then (yes) + :Source = CD; + note right + .log file = EAC/XLD rip log + implies CD source + end note + endif + + if (TrackCount == 0?) then (yes) + :TrackCount = AudioFileCount; + endif + endif + endif + } + + partition "2c. Fill from Title (torrent name)" #f0f0f0 { + label TitleParsing + :title = info.Name (or item.Title for fallback); + + if (title matches\n"(\\d{2,3})\\s*kbps"?) then (yes) + :Bitrate = matched value + " kbps"; + endif + + :Try hi-res patterns (in order):; + note right + 1. "24Bit-96kHz" / "24 Bit / 48 kHz" + 2. "FLAC 24-96" / "Flac 24-44" + 3. "24Bit" (bit depth only) + First match wins. + end note + if (Hi-res pattern matched?) then (yes) + if (BitDepth still 0?) then (yes) + :BitDepth = matched group 1; + endif + if (SampleRate still 0\nand group 2 exists?) then (yes) + :SampleRate = matched × 1000; + endif + endif + + if (title matches\n"\\[(CD|WEB|Vinyl|...)\\]"\nand Source still unknown?) then (yes) + :Source = matched value; + note right + CD, WEB, Vinyl/LP, + Cassette/MC, DVD, + Blu-Ray + end note + endif + + if (title matches\nrip type pattern?) then (yes) + :RipType = matched value; + note right + vinyl rip, SACD-R, + HDCD, DSD, tape rip + end note + endif + } + + :ParsedSuccessfully = (Artist != "" && Album != ""); + if (not ParsedSuccessfully?) then (yes) + :ParseErrors += "missing artist or album"; + endif + + :Return Release; + stop +} + +partition "3. Title-Only Parse (fallback)" #fff3e0 { + label TitleOnlyParse + :r = Release{RawTitle: item.Title}; + note right + No torrent data available. + No InfoHash. No file analysis. + No TrackNames. No cover/cue/log. + Format stays **unknown**. + end note + goto TitleParsing +} + +@enduml diff --git a/go.mod b/go.mod index de748bb..e210878 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,21 @@ module homelab.lan/music-agregator go 1.26.2 -require github.com/rs/zerolog v1.35.1 +require ( + github.com/anacrolix/torrent v1.61.0 + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 + github.com/jackc/pgx/v5 v5.9.2 + github.com/mewkiz/flac v1.0.13 + github.com/prometheus/client_golang v1.23.2 + github.com/riverqueue/river v0.35.1 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1 + github.com/rs/zerolog v1.35.1 + github.com/stretchr/testify v1.11.1 + github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 + github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 +) require ( dario.cat/mergo v1.0.2 // indirect @@ -24,7 +38,6 @@ require ( github.com/anacrolix/multiless v0.4.0 // indirect github.com/anacrolix/stm v0.5.0 // indirect github.com/anacrolix/sync v0.5.5-0.20251119100342-d78dd1f686f1 // indirect - github.com/anacrolix/torrent v1.61.0 // indirect github.com/anacrolix/upnp v0.1.4 // indirect github.com/anacrolix/utp v0.1.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -42,6 +55,7 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.3.2+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect @@ -53,23 +67,23 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/huandu/xstrings v1.3.2 // indirect + github.com/icza/bitio v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d // indirect + github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect @@ -106,25 +120,18 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/protolambda/ctxlock v0.1.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/riverqueue/river v0.35.1 // indirect github.com/riverqueue/river/riverdriver v0.35.1 // indirect - github.com/riverqueue/river/riverdriver/riverpgxv5 v0.35.1 // indirect github.com/riverqueue/river/rivershared v0.35.1 // indirect github.com/riverqueue/river/rivertype v0.35.1 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/stretchr/testify v1.11.1 // indirect - github.com/testcontainers/testcontainers-go v0.42.0 // indirect - github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 // indirect github.com/tidwall/btree v1.8.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect @@ -147,7 +154,6 @@ require ( golang.org/x/sync v0.20.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.1.6 // indirect modernc.org/libc v1.22.3 // indirect diff --git a/go.sum b/go.sum index 3c07303..5f73c2d 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,16 @@ crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oX crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= @@ -17,8 +22,12 @@ github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVO github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= +github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o= github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -46,6 +55,8 @@ github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2Rd github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb h1:nGNLCQbxFQZz7/9PXLGQ9GmavI/W+eX66pSwVeUwugU= github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb/go.mod h1:YjBZbwe2v3RsU7WdoBlVSPVpfKuOAno9SRQ/8tIl+hk= github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM= +github.com/anacrolix/lsan v0.1.0 h1:TbgB8fdVXgBwrNsJGHtht9+9FepNFu5H7dU8ek6XYAY= +github.com/anacrolix/lsan v0.1.0/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM= github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s= github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= @@ -114,11 +125,15 @@ github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7np github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= +github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -138,6 +153,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= @@ -161,9 +178,13 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -207,6 +228,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= @@ -214,6 +237,12 @@ github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0= +github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -227,6 +256,7 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= @@ -238,7 +268,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -246,6 +275,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= @@ -255,6 +288,14 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/mewkiz/flac v1.0.13 h1:6wF8rRQKBFW159Daqx6Ro7K5ZnlVhHUKfS5aTsC4oXs= +github.com/mewkiz/flac v1.0.13/go.mod h1:HfPYDA+oxjyuqMu2V+cyKcxF51KM6incpw5eZXmfA6k= +github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d h1:IL2tii4jXLdhCeQN69HNzYYW1kl0meSG0wt5+sLwszU= +github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d/go.mod h1:SIpumAnUWSy0q9RzKD3pyH3g1t5vdawUAPcW5tQrUtI= +github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8= +github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985/go.mod h1:uiPmbdUbdt1NkGApKl7htQjZ8S7XaGUAVulJUJ9v6q4= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -382,17 +423,18 @@ github.com/riverqueue/river/rivershared v0.35.1 h1:XEHf7yj35p5Os5r6K08q9BVaAKsvW github.com/riverqueue/river/rivershared v0.35.1/go.mod h1:YqVk7bZoojLsx58kyQ6ZU2FHP91HP4whVj6MTCtih/c= github.com/riverqueue/river/rivertype v0.35.1 h1:7SfjZ3Hkr7gRjItMHAUzJBAHIqx41yS/4yjVPQVtNfM= github.com/riverqueue/river/rivertype v0.35.1/go.mod h1:D1Ad+EaZiaXbQbJcJcfeicXJMBKno0n6UcfKI5Q7DIQ= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= @@ -412,6 +454,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -424,6 +468,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI= +github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI= github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= @@ -452,6 +498,8 @@ github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPyS github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -482,6 +530,7 @@ go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= @@ -494,6 +543,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -509,6 +560,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -525,6 +578,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -544,6 +598,7 @@ golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -565,6 +620,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -573,8 +630,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -586,6 +641,8 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -633,6 +690,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= @@ -645,5 +704,7 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU= modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o= zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4= diff --git a/internal/analysis/analyzer.go b/internal/analysis/analyzer.go new file mode 100644 index 0000000..5507b3f --- /dev/null +++ b/internal/analysis/analyzer.go @@ -0,0 +1,285 @@ +package analysis + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/rs/zerolog/log" + + "homelab.lan/music-agregator/internal/audio" + "homelab.lan/music-agregator/internal/database" +) + +var audioExtensions = map[string]bool{ + ".flac": true, ".mp3": true, ".aac": true, ".m4a": true, + ".ape": true, ".wv": true, ".ogg": true, ".wav": true, ".alac": true, +} + +var trackNumberRegex = regexp.MustCompile(`^(\d+)[\s\-._]+`) + +type ReleaseAnalyzer struct { + downloads *database.DownloadRepository + downloadFiles *database.DownloadFileRepository + albumReleases *database.AlbumReleaseRepository + trackReleases *database.TrackReleaseRepository +} + +func NewReleaseAnalyzer(db *database.DB) *ReleaseAnalyzer { + return &ReleaseAnalyzer{ + downloads: database.NewDownloadRepository(db.Pool), + downloadFiles: database.NewDownloadFileRepository(db.Pool), + albumReleases: database.NewAlbumReleaseRepository(db.Pool), + trackReleases: database.NewTrackReleaseRepository(db.Pool), + } +} + +func (a *ReleaseAnalyzer) Analyze(ctx context.Context, downloadID string, contentPath string) (*database.AlbumRelease, []*database.TrackRelease, error) { + download, err := a.downloads.GetByID(ctx, downloadID) + if err != nil { + return nil, nil, fmt.Errorf("getting download: %w", err) + } + + files, err := a.downloadFiles.GetByDownloadID(ctx, downloadID) + if err != nil || len(files) == 0 { + log.Info().Str("download_id", downloadID).Msg("no download files in DB, scanning filesystem") + scanned, scanErr := ScanAndHashFiles(contentPath) + if scanErr != nil { + return nil, nil, fmt.Errorf("scanning files: %w", scanErr) + } + for _, f := range scanned { + f.DownloadID = downloadID + } + if err := a.downloadFiles.CreateBatch(ctx, scanned); err != nil { + return nil, nil, fmt.Errorf("persisting scanned files: %w", err) + } + files = scanned + } + + var audioFiles []*database.DownloadFile + var hasCoverArt, hasCueSheet, hasRipLog bool + for _, f := range files { + if audioExtensions["."+f.FileType] { + audioFiles = append(audioFiles, f) + } + switch f.FileType { + case "jpg", "jpeg", "png", "gif", "webp": + hasCoverArt = true + case "cue": + hasCueSheet = true + case "log": + hasRipLog = true + } + } + + if len(audioFiles) == 0 { + return nil, nil, fmt.Errorf("no audio files found for download %s", downloadID) + } + + var trackReleases []*database.TrackRelease + var totalSize int64 + var totalDuration int + formatCounts := make(map[string]int) + var firstBitDepth, firstSampleRate, firstChannels int + var firstIsLossless bool + + for i, f := range audioFiles { + fullPath := filepath.Join(contentPath, f.FilePath) + info, err := audio.Analyze(fullPath) + if err != nil { + log.Warn().Err(err).Str("path", f.FilePath).Msg("failed to analyze audio file") + info = &audio.TrackInfo{ + Format: strings.ToUpper(f.FileType), + } + } + + if i == 0 { + firstBitDepth = info.BitDepth + firstSampleRate = info.SampleRate + firstChannels = info.Channels + firstIsLossless = info.IsLossless + } + + formatCounts[info.Format]++ + totalSize += f.FileSize + totalDuration += info.DurationMs + + trackNum := extractTrackNumber(f.FilePath) + title := extractTitle(f.FilePath) + + tr := &database.TrackRelease{ + Title: title, + TrackNumber: trackNum, + DiscNumber: 1, + Format: info.Format, + Channels: info.Channels, + FileSize: f.FileSize, + FilePath: f.FilePath, + } + if info.DurationMs > 0 { + dur := info.DurationMs + tr.DurationMs = &dur + } + if info.BitDepth > 0 { + bd := info.BitDepth + tr.BitDepth = &bd + } + if info.SampleRate > 0 { + sr := info.SampleRate + tr.SampleRate = &sr + } + if info.BitrateKbps > 0 { + br := info.BitrateKbps + tr.BitrateKbps = &br + } + trackReleases = append(trackReleases, tr) + } + + dominantFormat := "" + maxCount := 0 + for format, count := range formatCounts { + if count > maxCount { + dominantFormat = format + maxCount = count + } + } + + var source *string + if hasRipLog { + s := "CD" + source = &s + } + + release := &database.AlbumRelease{ + AlbumID: download.AlbumID, + DownloadID: downloadID, + Format: dominantFormat, + Channels: firstChannels, + IsLossless: firstIsLossless, + Source: source, + TotalSize: totalSize, + TotalDurationMs: totalDuration, + TrackCount: len(audioFiles), + HasCoverArt: hasCoverArt, + HasCueSheet: hasCueSheet, + HasRipLog: hasRipLog, + Path: contentPath, + } + if firstBitDepth > 0 { + release.BitDepth = &firstBitDepth + } + if firstSampleRate > 0 { + release.SampleRate = &firstSampleRate + } + + return release, trackReleases, nil +} + +func (a *ReleaseAnalyzer) AnalyzeAndPersist(ctx context.Context, downloadID string, contentPath string) (*database.AlbumRelease, []*database.TrackRelease, error) { + release, trackReleases, err := a.Analyze(ctx, downloadID, contentPath) + if err != nil { + return nil, nil, err + } + + if err := a.albumReleases.DeleteByDownloadID(ctx, downloadID); err != nil { + log.Warn().Err(err).Str("download_id", downloadID).Msg("failed to delete existing album release") + } + + if err := a.albumReleases.Create(ctx, release); err != nil { + return nil, nil, fmt.Errorf("creating album release: %w", err) + } + + for _, tr := range trackReleases { + tr.AlbumReleaseID = release.ID + } + if err := a.trackReleases.CreateBatch(ctx, trackReleases); err != nil { + return nil, nil, fmt.Errorf("creating track releases: %w", err) + } + + return release, trackReleases, nil +} + +func extractTrackNumber(filePath string) int { + base := filepath.Base(filePath) + matches := trackNumberRegex.FindStringSubmatch(base) + if len(matches) >= 2 { + var num int + fmt.Sscanf(matches[1], "%d", &num) + return num + } + return 0 +} + +func extractTitle(filePath string) string { + base := filepath.Base(filePath) + ext := filepath.Ext(base) + name := strings.TrimSuffix(base, ext) + name = trackNumberRegex.ReplaceAllString(name, "") + return strings.TrimSpace(name) +} + +func IsAudioExtension(ext string) bool { + return audioExtensions[ext] +} + +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 IsAudioExtension(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 +} diff --git a/internal/audio/analyzer.go b/internal/audio/analyzer.go new file mode 100644 index 0000000..02a9258 --- /dev/null +++ b/internal/audio/analyzer.go @@ -0,0 +1,35 @@ +package audio + +import ( + "fmt" + "path/filepath" + "strings" +) + +type TrackInfo struct { + Format string + BitDepth int + SampleRate int + Channels int + DurationMs int + BitrateKbps int + IsLossless bool +} + +func Analyze(filePath string) (*TrackInfo, error) { + ext := strings.ToLower(filepath.Ext(filePath)) + + switch ext { + case ".flac": + return analyzeFLAC(filePath) + case ".mp3": + return analyzeMP3(filePath) + case ".aac", ".m4a", ".ogg", ".wav", ".ape", ".wv", ".alac": + return &TrackInfo{ + Format: strings.ToUpper(strings.TrimPrefix(ext, ".")), + IsLossless: ext == ".wav" || ext == ".ape" || ext == ".wv" || ext == ".alac", + }, nil + default: + return nil, fmt.Errorf("unsupported audio format: %s", ext) + } +} diff --git a/internal/audio/flac.go b/internal/audio/flac.go new file mode 100644 index 0000000..cb5dab2 --- /dev/null +++ b/internal/audio/flac.go @@ -0,0 +1,27 @@ +package audio + +import ( + "fmt" + + "github.com/mewkiz/flac" +) + +func analyzeFLAC(filePath string) (*TrackInfo, error) { + stream, err := flac.ParseFile(filePath) + if err != nil { + return nil, fmt.Errorf("parsing FLAC: %w", err) + } + defer stream.Close() + + info := stream.Info + durationMs := int(info.NSamples * 1000 / uint64(info.SampleRate)) + + return &TrackInfo{ + Format: "FLAC", + BitDepth: int(info.BitsPerSample), + SampleRate: int(info.SampleRate), + Channels: int(info.NChannels), + DurationMs: durationMs, + IsLossless: true, + }, nil +} diff --git a/internal/audio/mp3.go b/internal/audio/mp3.go new file mode 100644 index 0000000..dad0287 --- /dev/null +++ b/internal/audio/mp3.go @@ -0,0 +1,54 @@ +package audio + +import ( + "fmt" + "os" + "time" + + "github.com/tcolgate/mp3" +) + +func analyzeMP3(filePath string) (*TrackInfo, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("opening MP3: %w", err) + } + defer f.Close() + + decoder := mp3.NewDecoder(f) + var frame mp3.Frame + var skipped int + var totalDuration time.Duration + var sampleRate, channels, bitrate int + var frameCount int + + for { + err := decoder.Decode(&frame, &skipped) + if err != nil { + break + } + if frameCount == 0 { + sampleRate = int(frame.Header().SampleRate()) + channels = channelCount(frame.Header().ChannelMode()) + bitrate = int(frame.Header().BitRate()) / 1000 + } + totalDuration += frame.Duration() + frameCount++ + } + + return &TrackInfo{ + Format: "MP3", + SampleRate: sampleRate, + Channels: channels, + DurationMs: int(totalDuration.Milliseconds()), + BitrateKbps: bitrate, + IsLossless: false, + }, nil +} + +func channelCount(mode mp3.FrameChannelMode) int { + if mode == mp3.SingleChannel { + return 1 + } + return 2 +} diff --git a/internal/config/config.go b/internal/config/config.go index 8c8871c..c2e5627 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,10 +35,11 @@ type Config struct { } `yaml:"indexer"` Torrent struct { - ClientType TorrentClientType `yaml:"client_type"` - Url string `yaml:"url"` - Username string `yaml:"username"` - Password string `yaml:"password"` + ClientType TorrentClientType `yaml:"client_type"` + Url string `yaml:"url"` + Username string `yaml:"username"` + Password string `yaml:"password"` + ContainerName string `yaml:"container_name"` } `yaml:"torrent"` Metadata struct { diff --git a/internal/database/album_release_repository.go b/internal/database/album_release_repository.go new file mode 100644 index 0000000..641c745 --- /dev/null +++ b/internal/database/album_release_repository.go @@ -0,0 +1,91 @@ +package database + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type AlbumRelease struct { + ID string + AlbumID string + DownloadID string + Format string + BitDepth *int + SampleRate *int + Channels int + IsLossless bool + Source *string + TotalSize int64 + TotalDurationMs int + TrackCount int + HasCoverArt bool + HasCueSheet bool + HasRipLog bool + Path string + CreatedAt time.Time +} + +type AlbumReleaseRepository struct { + pool *pgxpool.Pool +} + +func NewAlbumReleaseRepository(pool *pgxpool.Pool) *AlbumReleaseRepository { + return &AlbumReleaseRepository{pool: pool} +} + +func (r *AlbumReleaseRepository) Create(ctx context.Context, ar *AlbumRelease) error { + err := r.pool.QueryRow(ctx, + `INSERT INTO album_releases (album_id, download_id, format, bit_depth, sample_rate, channels, is_lossless, source, total_size, total_duration_ms, track_count, has_cover_art, has_cue_sheet, has_rip_log, path) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + RETURNING id, created_at`, + ar.AlbumID, ar.DownloadID, ar.Format, ar.BitDepth, ar.SampleRate, ar.Channels, ar.IsLossless, ar.Source, ar.TotalSize, ar.TotalDurationMs, ar.TrackCount, ar.HasCoverArt, ar.HasCueSheet, ar.HasRipLog, ar.Path, + ).Scan(&ar.ID, &ar.CreatedAt) + if err != nil { + return fmt.Errorf("creating album release: %w", err) + } + return nil +} + +func (r *AlbumReleaseRepository) GetByAlbumID(ctx context.Context, albumID string) ([]*AlbumRelease, error) { + rows, err := r.pool.Query(ctx, + `SELECT id, album_id, download_id, format, bit_depth, sample_rate, channels, is_lossless, source, total_size, total_duration_ms, track_count, has_cover_art, has_cue_sheet, has_rip_log, path, created_at + FROM album_releases WHERE album_id = $1 ORDER BY created_at DESC`, albumID, + ) + if err != nil { + return nil, fmt.Errorf("listing album releases: %w", err) + } + defer rows.Close() + + var releases []*AlbumRelease + for rows.Next() { + ar := &AlbumRelease{} + if err := rows.Scan(&ar.ID, &ar.AlbumID, &ar.DownloadID, &ar.Format, &ar.BitDepth, &ar.SampleRate, &ar.Channels, &ar.IsLossless, &ar.Source, &ar.TotalSize, &ar.TotalDurationMs, &ar.TrackCount, &ar.HasCoverArt, &ar.HasCueSheet, &ar.HasRipLog, &ar.Path, &ar.CreatedAt); err != nil { + return nil, fmt.Errorf("scanning album release: %w", err) + } + releases = append(releases, ar) + } + return releases, nil +} + +func (r *AlbumReleaseRepository) GetByDownloadID(ctx context.Context, downloadID string) (*AlbumRelease, error) { + ar := &AlbumRelease{} + err := r.pool.QueryRow(ctx, + `SELECT id, album_id, download_id, format, bit_depth, sample_rate, channels, is_lossless, source, total_size, total_duration_ms, track_count, has_cover_art, has_cue_sheet, has_rip_log, path, created_at + FROM album_releases WHERE download_id = $1`, downloadID, + ).Scan(&ar.ID, &ar.AlbumID, &ar.DownloadID, &ar.Format, &ar.BitDepth, &ar.SampleRate, &ar.Channels, &ar.IsLossless, &ar.Source, &ar.TotalSize, &ar.TotalDurationMs, &ar.TrackCount, &ar.HasCoverArt, &ar.HasCueSheet, &ar.HasRipLog, &ar.Path, &ar.CreatedAt) + if err != nil { + return nil, fmt.Errorf("getting album release by download: %w", err) + } + return ar, nil +} + +func (r *AlbumReleaseRepository) DeleteByDownloadID(ctx context.Context, downloadID string) error { + _, err := r.pool.Exec(ctx, `DELETE FROM album_releases WHERE download_id = $1`, downloadID) + if err != nil { + return fmt.Errorf("deleting album release by download: %w", err) + } + return nil +} diff --git a/internal/database/download_repository.go b/internal/database/download_repository.go index 0a39efb..c4d0d6f 100644 --- a/internal/database/download_repository.go +++ b/internal/database/download_repository.go @@ -141,6 +141,18 @@ func (r *DownloadRepository) GetActive(ctx context.Context) ([]*Download, error) return downloads, nil } +func (r *DownloadRepository) GetByID(ctx context.Context, id string) (*Download, error) { + d := &Download{} + err := r.pool.QueryRow(ctx, + `SELECT id, torrent_id, album_id, format, quality, state, qbit_hash, save_path, error_message, queued_at, started_at, completed_at, created_at, updated_at + FROM downloads WHERE id = $1`, id, + ).Scan(&d.ID, &d.TorrentID, &d.AlbumID, &d.Format, &d.Quality, &d.State, &d.QbitHash, &d.SavePath, &d.ErrorMessage, &d.QueuedAt, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, &d.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("getting download by id: %w", err) + } + return d, nil +} + func (r *DownloadRepository) HasAlbumInQuality(ctx context.Context, albumID string, format string, quality string) (bool, error) { var exists bool err := r.pool.QueryRow(ctx, diff --git a/internal/database/track_release_repository.go b/internal/database/track_release_repository.go new file mode 100644 index 0000000..4c03420 --- /dev/null +++ b/internal/database/track_release_repository.go @@ -0,0 +1,79 @@ +package database + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +type TrackRelease struct { + ID string + AlbumReleaseID string + TrackID *string + DownloadFileID *string + Title string + TrackNumber int + DiscNumber int + DurationMs *int + Format string + BitDepth *int + SampleRate *int + Channels int + BitrateKbps *int + FileSize int64 + FilePath string + CreatedAt time.Time +} + +type TrackReleaseRepository struct { + pool *pgxpool.Pool +} + +func NewTrackReleaseRepository(pool *pgxpool.Pool) *TrackReleaseRepository { + return &TrackReleaseRepository{pool: pool} +} + +func (r *TrackReleaseRepository) Create(ctx context.Context, tr *TrackRelease) error { + err := r.pool.QueryRow(ctx, + `INSERT INTO track_releases (album_release_id, track_id, download_file_id, title, track_number, disc_number, duration_ms, format, bit_depth, sample_rate, channels, bitrate_kbps, file_size, file_path) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + RETURNING id, created_at`, + tr.AlbumReleaseID, tr.TrackID, tr.DownloadFileID, tr.Title, tr.TrackNumber, tr.DiscNumber, tr.DurationMs, tr.Format, tr.BitDepth, tr.SampleRate, tr.Channels, tr.BitrateKbps, tr.FileSize, tr.FilePath, + ).Scan(&tr.ID, &tr.CreatedAt) + if err != nil { + return fmt.Errorf("creating track release: %w", err) + } + return nil +} + +func (r *TrackReleaseRepository) CreateBatch(ctx context.Context, tracks []*TrackRelease) error { + for _, tr := range tracks { + if err := r.Create(ctx, tr); err != nil { + return err + } + } + return nil +} + +func (r *TrackReleaseRepository) GetByAlbumReleaseID(ctx context.Context, albumReleaseID string) ([]*TrackRelease, error) { + rows, err := r.pool.Query(ctx, + `SELECT id, album_release_id, track_id, download_file_id, title, track_number, disc_number, duration_ms, format, bit_depth, sample_rate, channels, bitrate_kbps, file_size, file_path, created_at + FROM track_releases WHERE album_release_id = $1 ORDER BY disc_number, track_number`, albumReleaseID, + ) + if err != nil { + return nil, fmt.Errorf("listing track releases: %w", err) + } + defer rows.Close() + + var tracks []*TrackRelease + for rows.Next() { + tr := &TrackRelease{} + if err := rows.Scan(&tr.ID, &tr.AlbumReleaseID, &tr.TrackID, &tr.DownloadFileID, &tr.Title, &tr.TrackNumber, &tr.DiscNumber, &tr.DurationMs, &tr.Format, &tr.BitDepth, &tr.SampleRate, &tr.Channels, &tr.BitrateKbps, &tr.FileSize, &tr.FilePath, &tr.CreatedAt); err != nil { + return nil, fmt.Errorf("scanning track release: %w", err) + } + tracks = append(tracks, tr) + } + return tracks, nil +} diff --git a/internal/server.go b/internal/server.go index b6065a4..5aed235 100644 --- a/internal/server.go +++ b/internal/server.go @@ -19,8 +19,8 @@ type MusicAgregatorServer struct { pb.UnimplementedMusicAgregatorServiceServer } -func NewMusicAgregatorServer(cfg config.Config, riverClient *river.Client[pgx.Tx], torrentClient torrent.TorrentClient, db *database.DB) (*MusicAgregatorServer, error) { - service, err := NewMusicAgregatorService(cfg, riverClient, torrentClient, db) +func NewMusicAgregatorServer(cfg config.Config, riverClient *river.Client[pgx.Tx], torrentClient torrent.TorrentClient, pathMapper *torrent.PathMapper, db *database.DB) (*MusicAgregatorServer, error) { + service, err := NewMusicAgregatorService(cfg, riverClient, torrentClient, pathMapper, db) if err != nil { log.Err(err).Msg("failed to create MusicAgregatorService") return nil, err @@ -46,6 +46,10 @@ func (s *MusicAgregatorServer) MonitorAlbum(ctx context.Context, req *pb.Monitor return s.service.MonitorAlbum(ctx, req) } +func (s *MusicAgregatorServer) AnalyzeAlbumRelease(ctx context.Context, req *pb.AnalyzeAlbumReleaseRequest) (*pb.AnalyzeAlbumReleaseResponse, error) { + return s.service.AnalyzeAlbumRelease(ctx, req) +} + func (s *MusicAgregatorServer) Register(server *grpc.Server) { pb.RegisterMusicAgregatorServiceServer(server, s) } diff --git a/internal/service.go b/internal/service.go index 6f229b7..d010448 100644 --- a/internal/service.go +++ b/internal/service.go @@ -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 } diff --git a/internal/torrent/client.go b/internal/torrent/client.go index d281937..7d46cba 100644 --- a/internal/torrent/client.go +++ b/internal/torrent/client.go @@ -43,6 +43,7 @@ type TorrentClient interface { Login(username string, password string) (string, error) List() ([]TorrentInfo, error) Find(opts FindOptions) ([]TorrentInfo, error) - AddTorrent(file TorrentFile) error - AddMagnet(magnetURI string) error + AddTorrent(file TorrentFile, savePath string) error + AddMagnet(magnetURI string, savePath string) error + DefaultSavePath() (string, error) } diff --git a/internal/torrent/path_mapper.go b/internal/torrent/path_mapper.go new file mode 100644 index 0000000..54a9c54 --- /dev/null +++ b/internal/torrent/path_mapper.go @@ -0,0 +1,88 @@ +package torrent + +import ( + "context" + "fmt" + "strings" + "time" + + dockerclient "github.com/docker/docker/client" + "github.com/rs/zerolog/log" +) + +type PathMapper struct { + containerPath string + hostPath string +} + +func NewPathMapper(containerName string, torrentClient TorrentClient) (*PathMapper, error) { + if containerName == "" { + savePath, err := torrentClient.DefaultSavePath() + if err != nil { + return nil, fmt.Errorf("getting default save path: %w", err) + } + log.Info().Str("path", savePath).Msg("no container configured, using direct path") + return &PathMapper{containerPath: savePath, hostPath: savePath}, nil + } + + savePath, err := torrentClient.DefaultSavePath() + if err != nil { + return nil, fmt.Errorf("getting default save path: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) + if err != nil { + return nil, fmt.Errorf("creating docker client: %w", err) + } + defer cli.Close() + + inspect, err := cli.ContainerInspect(ctx, containerName) + if err != nil { + return nil, fmt.Errorf("inspecting container %s: %w", containerName, err) + } + + hostPath := "" + for _, mount := range inspect.Mounts { + if mount.Destination == savePath { + hostPath = mount.Source + break + } + } + + if hostPath == "" { + return nil, fmt.Errorf("no mount found for %s in container %s", savePath, containerName) + } + + log.Info(). + Str("container", containerName). + Str("container_path", savePath). + Str("host_path", hostPath). + Msg("resolved download path mapping") + + return &PathMapper{containerPath: savePath, hostPath: hostPath}, nil +} + +func (m *PathMapper) ToHost(containerPath string) string { + if m.containerPath == m.hostPath { + return containerPath + } + return strings.Replace(containerPath, m.containerPath, m.hostPath, 1) +} + +func (m *PathMapper) ToContainer(hostPath string) string { + if m.containerPath == m.hostPath { + return hostPath + } + return strings.Replace(hostPath, m.hostPath, m.containerPath, 1) +} + +func (m *PathMapper) HostDownloadPath() string { + return m.hostPath +} + +func (m *PathMapper) ContainerDownloadPath() string { + return m.containerPath +} diff --git a/internal/torrent/qbittorrent_client.go b/internal/torrent/qbittorrent_client.go index 1853556..99fb094 100644 --- a/internal/torrent/qbittorrent_client.go +++ b/internal/torrent/qbittorrent_client.go @@ -173,8 +173,8 @@ func filterLocally(torrents []TorrentInfo, opts FindOptions) []TorrentInfo { return result } -func (c *QbittorrentClient) AddTorrent(file TorrentFile) error { - log.Trace().Str("filename", file.Filename).Int("size", len(file.Data)).Msg("qbittorrent adding torrent file") +func (c *QbittorrentClient) AddTorrent(file TorrentFile, savePath string) error { + log.Trace().Str("filename", file.Filename).Int("size", len(file.Data)).Str("save_path", savePath).Msg("qbittorrent adding torrent file") var buf bytes.Buffer writer := multipart.NewWriter(&buf) @@ -190,6 +190,12 @@ func (c *QbittorrentClient) AddTorrent(file TorrentFile) error { return fmt.Errorf("writing torrent data: %w", err) } + if savePath != "" { + if err := writer.WriteField("savepath", savePath); err != nil { + return fmt.Errorf("writing savepath field: %w", err) + } + } + if err := writer.Close(); err != nil { return fmt.Errorf("closing multipart writer: %w", err) } @@ -205,14 +211,17 @@ func (c *QbittorrentClient) AddTorrent(file TorrentFile) error { return c.doAdd(req, file.Filename) } -func (c *QbittorrentClient) AddMagnet(magnetURI string) error { +func (c *QbittorrentClient) AddMagnet(magnetURI string, savePath string) error { truncated := magnetURI if len(truncated) > 80 { truncated = truncated[:80] + "..." } - log.Trace().Str("magnet", truncated).Msg("qbittorrent adding magnet") + log.Trace().Str("magnet", truncated).Str("save_path", savePath).Msg("qbittorrent adding magnet") data := url.Values{"urls": {magnetURI}} + if savePath != "" { + data.Set("savepath", savePath) + } req, err := http.NewRequest("POST", c.baseURL+"/api/v2/torrents/add", strings.NewReader(data.Encode())) if err != nil { log.Error().Err(err).Msg("qbittorrent creating magnet add request failed") @@ -303,3 +312,28 @@ func (t *QbittorrentListItem) toTorrentInfo() TorrentInfo { Availability: t.Availability, } } + +func (c *QbittorrentClient) DefaultSavePath() (string, error) { + req, err := http.NewRequest("GET", c.baseURL+"/api/v2/app/defaultSavePath", nil) + if err != nil { + return "", fmt.Errorf("creating request: %w", err) + } + req.AddCookie(&http.Cookie{Name: "SID", Value: c.sid}) + + resp, err := c.client.Do(req) + if err != nil { + return "", fmt.Errorf("requesting default save path: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("default save path returned status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("reading response: %w", err) + } + + return strings.TrimSpace(string(body)), nil +} diff --git a/internal/torrent/service.go b/internal/torrent/service.go index 6d87266..f4b60b0 100644 --- a/internal/torrent/service.go +++ b/internal/torrent/service.go @@ -51,7 +51,7 @@ func (service *TorrentService) Add(req *pb.AddRequest) (*pb.AddResponse, error) return nil, fmt.Errorf("either torrent_data or download_url must be provided") } - if err := service.client.AddTorrent(file); err != nil { + if err := service.client.AddTorrent(file, ""); err != nil { return nil, err } diff --git a/internal/workers/poll_download.go b/internal/workers/poll_download.go index f776e21..91c124c 100644 --- a/internal/workers/poll_download.go +++ b/internal/workers/poll_download.go @@ -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 -} diff --git a/proto/music_agregator/v1/music_agregator.proto b/proto/music_agregator/v1/music_agregator.proto index c7b08b6..34b952a 100644 --- a/proto/music_agregator/v1/music_agregator.proto +++ b/proto/music_agregator/v1/music_agregator.proto @@ -6,6 +6,7 @@ service MusicAgregatorService { rpc MonitorAlbum(MonitorAlbumRequest) returns (MonitorAlbumResponse) {} rpc GetArtists(GetArtistsRequest) returns (GetArtistsResponse) {} rpc GetAlbum(GetAlbumRequest) returns (GetAlbumResponse) {} + rpc AnalyzeAlbumRelease(AnalyzeAlbumReleaseRequest) returns (AnalyzeAlbumReleaseResponse) {} } message MonitorAlbumRequest { @@ -81,9 +82,22 @@ message GetAlbumRequest { string album_id = 1; } -message GetAlbumResponse { +message AlbumInfo { AlbumDetail album = 1; repeated TrackDetail tracks = 2; + AlbumReleaseDetail release = 3; +} + +message GetAlbumResponse { + AlbumInfo info = 1; +} + +message AnalyzeAlbumReleaseRequest { + string album_id = 1; +} + +message AnalyzeAlbumReleaseResponse { + AlbumInfo info = 1; } message TrackDetail { @@ -110,6 +124,40 @@ message TrackFile { int64 size = 3; } +message AlbumReleaseDetail { + string id = 1; + string format = 2; + int32 bit_depth = 3; + int32 sample_rate = 4; + int32 channels = 5; + bool is_lossless = 6; + string source = 7; + int64 total_size = 8; + int32 total_duration_ms = 9; + int32 track_count = 10; + bool has_cover_art = 11; + bool has_cue_sheet = 12; + bool has_rip_log = 13; + string path = 14; + repeated TrackReleaseDetail tracks = 15; +} + +message TrackReleaseDetail { + string id = 1; + string track_id = 2; + string title = 3; + int32 track_number = 4; + int32 disc_number = 5; + int32 duration_ms = 6; + string format = 7; + int32 bit_depth = 8; + int32 sample_rate = 9; + int32 channels = 10; + int32 bitrate_kbps = 11; + int64 file_size = 12; + string file_path = 13; +} + message MonitoredRelease { string info_hash = 1; string artist = 2; diff --git a/test/component/mocks_test.go b/test/component/mocks_test.go index bc27730..faad61e 100644 --- a/test/component/mocks_test.go +++ b/test/component/mocks_test.go @@ -81,11 +81,12 @@ func (m *mockMetadataClient) SyncArtist(ctx context.Context, in *metadataPb.Sync } type mockTorrentClient struct { - LoginFunc func(username, password string) (string, error) - ListFunc func() ([]torrent.TorrentInfo, error) - FindFunc func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) - AddTorrentFunc func(file torrent.TorrentFile) error - AddMagnetFunc func(magnetURI string) error + LoginFunc func(username, password string) (string, error) + ListFunc func() ([]torrent.TorrentInfo, error) + FindFunc func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) + AddTorrentFunc func(file torrent.TorrentFile, savePath string) error + AddMagnetFunc func(magnetURI string, savePath string) error + DefaultSavePathFunc func() (string, error) } func (m *mockTorrentClient) Login(username, password string) (string, error) { @@ -109,20 +110,27 @@ func (m *mockTorrentClient) Find(opts torrent.FindOptions) ([]torrent.TorrentInf return nil, fmt.Errorf("not mocked") } -func (m *mockTorrentClient) AddTorrent(file torrent.TorrentFile) error { +func (m *mockTorrentClient) AddTorrent(file torrent.TorrentFile, savePath string) error { if m.AddTorrentFunc != nil { - return m.AddTorrentFunc(file) + return m.AddTorrentFunc(file, savePath) } return fmt.Errorf("not mocked") } -func (m *mockTorrentClient) AddMagnet(magnetURI string) error { +func (m *mockTorrentClient) AddMagnet(magnetURI string, savePath string) error { if m.AddMagnetFunc != nil { - return m.AddMagnetFunc(magnetURI) + return m.AddMagnetFunc(magnetURI, savePath) } return fmt.Errorf("not mocked") } +func (m *mockTorrentClient) DefaultSavePath() (string, error) { + if m.DefaultSavePathFunc != nil { + return m.DefaultSavePathFunc() + } + return "/downloads", nil +} + type mockSearcher struct { SearchFunc func(query string, limit int32, indexer string) (*indexer.SearchResponse, error) } diff --git a/test/component/monitor_album_test.go b/test/component/monitor_album_test.go index 67f4262..ad17633 100644 --- a/test/component/monitor_album_test.go +++ b/test/component/monitor_album_test.go @@ -46,7 +46,7 @@ func TestMonitorAlbum_HappyPath(t *testing.T) { return []torrent.TorrentInfo{}, nil } - suite.mocks.torrent.AddMagnetFunc = func(magnetURI string) error { + suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error { return nil } @@ -181,7 +181,7 @@ func TestMonitorAlbum_ArtistPersistFails(t *testing.T) { return []torrent.TorrentInfo{}, nil } - suite.mocks.torrent.AddMagnetFunc = func(magnetURI string) error { + suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error { return nil } @@ -460,7 +460,7 @@ func TestMonitorAlbum_QBitDown(t *testing.T) { return nil, assert.AnError } - suite.mocks.torrent.AddMagnetFunc = func(magnetURI string) error { + suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error { return assert.AnError } @@ -589,7 +589,7 @@ func TestMonitorAlbum_AddMagnetFails(t *testing.T) { return []torrent.TorrentInfo{}, nil } - suite.mocks.torrent.AddMagnetFunc = func(magnetURI string) error { + suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error { return assert.AnError } diff --git a/test/component/setup_test.go b/test/component/setup_test.go index a6183af..6ae6590 100644 --- a/test/component/setup_test.go +++ b/test/component/setup_test.go @@ -98,6 +98,7 @@ func setupSuite(t *testing.T) *testSuite { mocks.torrent, mocks.magnet, nil, + nil, db, ) diff --git a/test/unit/generic_parser_test.go b/test/unit/generic_parser_test.go new file mode 100644 index 0000000..6aaebbd --- /dev/null +++ b/test/unit/generic_parser_test.go @@ -0,0 +1,577 @@ +package unit + +import ( + "bytes" + "fmt" + "testing" + + metadataPb "homelab.lan/music-agregator/gen/metadata/v1" + "homelab.lan/music-agregator/internal/release" + "homelab.lan/music-agregator/internal/tracker" +) + +type testFile struct { + path string + size int64 +} + +func buildTorrentData(name string, files []testFile) []byte { + var buf bytes.Buffer + buf.WriteString("d8:announce35:http://tracker.example.com/announce4:infod") + + if len(files) == 0 { + buf.WriteString(fmt.Sprintf("6:lengthi0e4:name%d:%s12:piece lengthi16384e6:pieces20:01234567890123456789", len(name), name)) + } else if len(files) == 1 { + buf.WriteString(fmt.Sprintf("6:lengthi%de4:name%d:%s12:piece lengthi16384e6:pieces20:01234567890123456789", files[0].size, len(files[0].path), files[0].path)) + } else { + buf.WriteString("5:filesl") + for _, f := range files { + buf.WriteString(fmt.Sprintf("d6:lengthi%de4:pathl%d:%see", f.size, len(f.path), f.path)) + } + buf.WriteString(fmt.Sprintf("e4:name%d:%s12:piece lengthi16384e6:pieces20:01234567890123456789", len(name), name)) + } + + buf.WriteString("ee") + return buf.Bytes() +} + +func TestGenericParser_Parse(t *testing.T) { + p := tracker.NewGenericParser() + + tests := []struct { + name string + title string + wantBitrate string + wantBitDepth int + wantSampleRate int + wantSource release.Source + wantRipType string + }{ + { + name: "discography no hires", + title: "System Of A Down - Discography [FLAC Songs] [PMEDIA]", + }, + { + name: "hiphop hires 24-44", + title: "Snoop Dogg - 10 Til' Midnight (2026 Hip Hop Rap) [Flac 24-44]", + wantBitDepth: 24, + wantSampleRate: 44000, + }, + { + name: "pop hires 24bit", + title: "Sabrina Carpenter - Short n' Sweet [Deluxe] [2025] [Hi-Res FLAC 24bit]-Sc4r3cr0w", + wantBitDepth: 24, + }, + { + name: "rock hires 24bit", + title: "Linkin Park - From Zero [Deluxe Edition] [2025] [Hi-Res] [FLAC-24bit]-Sc4r3cr0w", + }, + { + name: "rock hires 24-48", + title: "Linkin Park - From Zero (2024) [24Bit-48kHz] FLAC [PMEDIA]", + wantBitDepth: 24, + wantSampleRate: 48000, + }, + { + name: "hiphop hires 24-96", + title: "J. Cole - The Fall-Off (2026 Hip Hop Rap) [Flac 24-96]", + wantBitDepth: 24, + wantSampleRate: 96000, + }, + { + name: "minimal format", + title: "Bjork-Bastards.2012.FLAC-NewAlbumReleases", + }, + { + name: "vinyl hires", + title: "Gorillaz - Demon Days [Live From The Apollo Theater] [2025] [Vinyl Hi-Res] [FLAC-24bit]-Sc4r3cr0w", + }, + { + name: "cd with log", + title: "Linkin Park - Meteora (Tracks, Log, Cue, Scans) (2003) [FLAC] 88", + }, + { + name: "rock 16-44", + title: "Heart - Jupiters Darling (2004 Rock) [Flac 16-44]", + wantBitDepth: 16, + wantSampleRate: 44000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := p.Parse(tt.title) + + if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate { + t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate) + } + if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth { + t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth) + } + if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate { + t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate) + } + if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource { + t.Errorf("Source = %v, want %v", r.Source, tt.wantSource) + } + if tt.wantRipType != "" && r.RipType != tt.wantRipType { + t.Errorf("RipType = %q, want %q", r.RipType, tt.wantRipType) + } + }) + } +} + +func TestGenericParser_ParseTorrent(t *testing.T) { + p := tracker.NewGenericParser() + + makeFlacFiles := func(count int, sizeMB float64) []testFile { + files := make([]testFile, count) + for i := range files { + files[i] = testFile{ + path: fmt.Sprintf("%02d - Track %d.flac", i+1, i+1), + size: int64(sizeMB * 1024 * 1024), + } + } + return files + } + + makeMp3Files := func(count int, sizeMB float64) []testFile { + files := make([]testFile, count) + for i := range files { + files[i] = testFile{ + path: fmt.Sprintf("%02d - Track %d.mp3", i+1, i+1), + size: int64(sizeMB * 1024 * 1024), + } + } + return files + } + + tests := []struct { + name string + torrentName string + files []testFile + album *metadataPb.Album + wantFormat release.AudioFormat + wantAudioFileCount int + wantHasCoverArt bool + wantHasCueSheet bool + wantHasRipLog bool + wantSource release.Source + wantInfoHashEmpty bool + wantBitDepth int + wantSampleRate int + wantTrackNames []string + wantArtist string + wantAlbum string + wantYear int + wantType release.Type + wantGenres []string + wantLabel string + wantParseErrors bool + }{ + { + name: "flac album with cover cue log", + torrentName: "Test Artist - Test Album (2024) [FLAC]", + files: append(append(makeFlacFiles(12, 30), + testFile{path: "cover.jpg", size: 500000}, + testFile{path: "album.cue", size: 2000}), + testFile{path: "rip.log", size: 5000}), + album: &metadataPb.Album{ + Title: "Test Album", + AlbumType: "Album", + ReleaseDate: "2024-01-15", + TotalTracks: 12, + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Test Artist"}}}, + Genres: []*metadataPb.Genre{{Name: "Rock"}}, + Label: &metadataPb.Label{Name: "Test Label"}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 12, + wantHasCoverArt: true, + wantHasCueSheet: true, + wantHasRipLog: true, + wantSource: release.SourceCD, + wantArtist: "Test Artist", + wantAlbum: "Test Album", + wantYear: 2024, + wantType: release.TypeAlbum, + wantGenres: []string{"Rock"}, + wantLabel: "Test Label", + }, + { + name: "mp3 album with cover", + torrentName: "Artist - MP3 Album (2023)", + files: append(makeMp3Files(10, 10), + testFile{path: "cover.jpg", size: 300000}), + album: &metadataPb.Album{ + Title: "MP3 Album", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + ReleaseDate: "2023-05-20", + }, + wantFormat: release.FormatMP3, + wantAudioFileCount: 10, + wantHasCoverArt: true, + wantHasCueSheet: false, + wantHasRipLog: false, + wantArtist: "Artist", + wantAlbum: "MP3 Album", + wantYear: 2023, + }, + { + name: "mixed format dominant wins", + torrentName: "Mixed Format Album", + files: append(makeFlacFiles(10, 30), + testFile{path: "bonus1.mp3", size: 10485760}, + testFile{path: "bonus2.mp3", size: 10485760}), + album: &metadataPb.Album{ + Title: "Mixed Format Album", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 10, + }, + { + name: "single file torrent flac", + torrentName: "Single Track.flac", + files: []testFile{{path: "Single Track.flac", size: 50 * 1024 * 1024}}, + album: &metadataPb.Album{ + Title: "Single Track", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 1, + }, + { + name: "single file torrent mp3", + torrentName: "Single.mp3", + files: []testFile{{path: "Single.mp3", size: 10 * 1024 * 1024}}, + album: &metadataPb.Album{ + Title: "Single", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + }, + wantFormat: release.FormatMP3, + wantAudioFileCount: 1, + }, + { + name: "no audio files", + torrentName: "Not Music", + files: []testFile{ + {path: "readme.txt", size: 1000}, + {path: "image.jpg", size: 500000}, + }, + album: &metadataPb.Album{ + Title: "Not Music", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Someone"}}}, + }, + wantFormat: release.FormatUnknown, + wantAudioFileCount: 0, + wantHasCoverArt: true, + }, + { + name: "hires in title", + torrentName: "Artist - Album (2024) [24Bit-96kHz] FLAC", + files: makeFlacFiles(12, 100), + album: &metadataPb.Album{ + Title: "Album", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 12, + wantBitDepth: 24, + wantSampleRate: 96000, + }, + { + name: "source from title", + torrentName: "Artist - Album [WEB] FLAC", + files: makeFlacFiles(10, 30), + album: &metadataPb.Album{ + Title: "Album", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 10, + wantSource: release.SourceWEB, + }, + { + name: "track names cleaned", + torrentName: "Artist - Album", + files: []testFile{ + {path: "01 - First Track.flac", size: 30 * 1024 * 1024}, + {path: "02 - Second Track.flac", size: 30 * 1024 * 1024}, + }, + album: &metadataPb.Album{ + Title: "Album", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 2, + wantTrackNames: []string{"First Track", "Second Track"}, + }, + { + name: "metadata fills release fields", + torrentName: "Test Torrent", + files: makeFlacFiles(8, 30), + album: &metadataPb.Album{ + Title: "Metadata Album", + AlbumType: "EP", + ReleaseDate: "2020-06-15", + TotalTracks: 8, + TotalDiscs: 1, + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Metadata Artist"}}}, + Genres: []*metadataPb.Genre{{Name: "Electronic"}, {Name: "Ambient"}}, + Label: &metadataPb.Label{Name: "Metadata Label"}, + }, + wantFormat: release.FormatFLAC, + wantAudioFileCount: 8, + wantArtist: "Metadata Artist", + wantAlbum: "Metadata Album", + wantYear: 2020, + wantType: release.TypeEP, + wantGenres: []string{"Electronic", "Ambient"}, + wantLabel: "Metadata Label", + }, + { + name: "empty torrent data", + torrentName: "", + files: nil, + album: &metadataPb.Album{ + Title: "Album Only", + ReleaseDate: "2022-01-01", + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist Only"}}}, + }, + wantFormat: release.FormatUnknown, + wantAudioFileCount: 0, + wantInfoHashEmpty: true, + wantArtist: "Artist Only", + wantAlbum: "Album Only", + wantYear: 2022, + }, + { + name: "invalid torrent data", + torrentName: "invalid", + files: nil, + album: &metadataPb.Album{Title: "Album", Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}}, + wantArtist: "Artist", + wantAlbum: "Album", + wantParseErrors: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var torrentData []byte + if tt.name == "empty torrent data" { + torrentData = nil + } else if tt.name == "invalid torrent data" { + torrentData = []byte("garbage data that is not valid bencode") + } else { + torrentData = buildTorrentData(tt.torrentName, tt.files) + } + + r := p.ParseTorrent(torrentData, tt.album) + + if r.Format != tt.wantFormat { + t.Errorf("Format = %v, want %v", r.Format, tt.wantFormat) + } + if r.AudioFileCount != tt.wantAudioFileCount { + t.Errorf("AudioFileCount = %d, want %d", r.AudioFileCount, tt.wantAudioFileCount) + } + if r.HasCoverArt != tt.wantHasCoverArt { + t.Errorf("HasCoverArt = %v, want %v", r.HasCoverArt, tt.wantHasCoverArt) + } + if r.HasCueSheet != tt.wantHasCueSheet { + t.Errorf("HasCueSheet = %v, want %v", r.HasCueSheet, tt.wantHasCueSheet) + } + if r.HasRipLog != tt.wantHasRipLog { + t.Errorf("HasRipLog = %v, want %v", r.HasRipLog, tt.wantHasRipLog) + } + if tt.wantSource != release.SourceUnknown && r.Source != tt.wantSource { + t.Errorf("Source = %v, want %v", r.Source, tt.wantSource) + } + if tt.wantInfoHashEmpty && r.InfoHash != "" { + t.Errorf("InfoHash = %q, want empty", r.InfoHash) + } + if !tt.wantInfoHashEmpty && tt.name != "invalid torrent data" && r.InfoHash == "" { + t.Error("InfoHash should not be empty") + } + if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth { + t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth) + } + if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate { + t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate) + } + if len(tt.wantTrackNames) > 0 { + if len(r.TrackNames) != len(tt.wantTrackNames) { + t.Errorf("TrackNames length = %d, want %d", len(r.TrackNames), len(tt.wantTrackNames)) + } else { + for i, name := range tt.wantTrackNames { + if r.TrackNames[i] != name { + t.Errorf("TrackNames[%d] = %q, want %q", i, r.TrackNames[i], name) + } + } + } + } + if tt.wantArtist != "" && r.Artist != tt.wantArtist { + t.Errorf("Artist = %q, want %q", r.Artist, tt.wantArtist) + } + if tt.wantAlbum != "" && r.Album != tt.wantAlbum { + t.Errorf("Album = %q, want %q", r.Album, tt.wantAlbum) + } + if tt.wantYear != 0 && r.Year != tt.wantYear { + t.Errorf("Year = %d, want %d", r.Year, tt.wantYear) + } + if tt.wantType != release.TypeUnknown && r.Type != tt.wantType { + t.Errorf("Type = %v, want %v", r.Type, tt.wantType) + } + if len(tt.wantGenres) > 0 { + if len(r.Genres) != len(tt.wantGenres) { + t.Errorf("Genres length = %d, want %d", len(r.Genres), len(tt.wantGenres)) + } else { + for i, g := range tt.wantGenres { + if r.Genres[i] != g { + t.Errorf("Genres[%d] = %q, want %q", i, r.Genres[i], g) + } + } + } + } + if tt.wantLabel != "" && r.Label != tt.wantLabel { + t.Errorf("Label = %q, want %q", r.Label, tt.wantLabel) + } + if tt.wantParseErrors && len(r.ParseErrors) == 0 { + t.Error("expected ParseErrors but got none") + } + }) + } +} + +func TestGenericParser_DeduceFromFileSize(t *testing.T) { + p := tracker.NewGenericParser() + + makeFlacFiles := func(count int, avgSizeMB float64) []testFile { + files := make([]testFile, count) + size := int64(avgSizeMB * 1024 * 1024) + for i := range files { + files[i] = testFile{path: fmt.Sprintf("%02d - Track %d.flac", i+1, i+1), size: size} + } + return files + } + + makeMp3Files := func(count int, avgSizeMB float64) []testFile { + files := make([]testFile, count) + size := int64(avgSizeMB * 1024 * 1024) + for i := range files { + files[i] = testFile{path: fmt.Sprintf("%02d - Track %d.mp3", i+1, i+1), size: size} + } + return files + } + + tests := []struct { + name string + torrentName string + files []testFile + wantBitDepth int + wantSampleRate int + wantBitrate string + }{ + { + name: "flac 16/44.1 from small files", + torrentName: "Artist - Album FLAC", + files: makeFlacFiles(12, 30), + wantBitDepth: 16, + wantSampleRate: 44100, + }, + { + name: "flac 24/48 from medium files", + torrentName: "Artist - Album FLAC", + files: makeFlacFiles(12, 50), + wantBitDepth: 24, + wantSampleRate: 48000, + }, + { + name: "flac 24/96 from large files", + torrentName: "Artist - Album FLAC", + files: makeFlacFiles(12, 100), + wantBitDepth: 24, + wantSampleRate: 96000, + }, + { + name: "flac 24/192 from very large files", + torrentName: "Artist - Album FLAC", + files: makeFlacFiles(12, 200), + wantBitDepth: 24, + wantSampleRate: 192000, + }, + { + name: "title overrides heuristic", + torrentName: "Artist - Album [24Bit-48kHz] FLAC", + files: makeFlacFiles(12, 30), + wantBitDepth: 24, + wantSampleRate: 48000, + }, + { + name: "mp3 320kbps from large files", + torrentName: "Artist - Album MP3", + files: makeMp3Files(12, 10), + wantBitrate: "320 kbps", + }, + { + name: "mp3 128kbps from small files", + torrentName: "Artist - Album MP3", + files: makeMp3Files(12, 3.5), + wantBitrate: "128 kbps", + }, + { + name: "mp3 title overrides", + torrentName: "Artist - Album 320 kbps MP3", + files: makeMp3Files(12, 3.5), + wantBitrate: "320 kbps", + }, + { + name: "no audio files skips deduction", + torrentName: "Artist - Album", + files: []testFile{ + {path: "cover.jpg", size: 500000}, + }, + }, + { + name: "aac files no deduction", + torrentName: "Artist - Album", + files: func() []testFile { + files := make([]testFile, 12) + for i := range files { + files[i] = testFile{path: fmt.Sprintf("%02d.aac", i+1), size: 50 * 1024 * 1024} + } + return files + }(), + }, + } + + album := &metadataPb.Album{ + Title: "Album", + TotalTracks: 12, + Artists: []*metadataPb.ArtistCredit{{Artist: &metadataPb.Artist{Name: "Artist"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data := buildTorrentData(tt.torrentName, tt.files) + r := p.ParseTorrent(data, album) + + if tt.wantBitDepth != 0 && r.BitDepth != tt.wantBitDepth { + t.Errorf("BitDepth = %d, want %d", r.BitDepth, tt.wantBitDepth) + } + if tt.wantSampleRate != 0 && r.SampleRate != tt.wantSampleRate { + t.Errorf("SampleRate = %d, want %d", r.SampleRate, tt.wantSampleRate) + } + if tt.wantBitrate != "" && r.Bitrate != tt.wantBitrate { + t.Errorf("Bitrate = %q, want %q", r.Bitrate, tt.wantBitrate) + } + if tt.name == "no audio files skips deduction" || tt.name == "aac files no deduction" { + if r.BitDepth != 0 || r.SampleRate != 0 || r.Bitrate != "" { + t.Errorf("expected no deduction, got BitDepth=%d, SampleRate=%d, Bitrate=%q", + r.BitDepth, r.SampleRate, r.Bitrate) + } + } + }) + } +}