@startuml MonitorAlbum Happy Path skinparam sequenceMessageAlign center skinparam responseMessageBelowArrow true actor Client participant "gRPC Server" as Server participant "MusicAgregatorService" as Service participant "MetadataService" as Metadata database "metadata-agregator\n(gRPC)" as MetaGRPC database "PostgreSQL" as DB participant "IndexerService\n(Jackett)" as Indexer participant "MagnetResolver" as Magnet participant "TorrentClient\n(qBittorrent)" as QBit participant "River Queue" as River participant "PollDownloadWorker" as PollWorker == 1. Fetch Album Metadata == Client -> Server: MonitorAlbum(album_id, quality, tracker) Server -> Service: MonitorAlbum(ctx, req) Service -> Metadata: GetAlbum(album_id) Metadata -> MetaGRPC: GetAlbum(id) MetaGRPC --> Metadata: Album (title, artists, genres, ...) Metadata -> DB: albums.GetByExternalID(external_id) note right: Check if album already persisted DB --> Metadata: not found Metadata -> DB: artists.Create(artist, state=monitored) note right: Upsert artist\nnever downgrades\nmonitored/excluded Metadata -> DB: albums.Create(album, state=monitored) note right: Upsert album\nnever downgrades\nmonitored/excluded Metadata --> Service: Album == 2. Set Monitor State == Service -> DB: albums.GetByExternalID(external_id) DB --> Service: dbAlbum Service -> DB: albums.SetMonitorState(id, monitored) note right: Explicitly mark\nalbum as monitored == 3. Check If Already Owned == Service -> DB: downloads.HasAlbumInQuality(album_id, format, quality) DB --> Service: false (not owned) == 4. Search Indexers == Service -> Indexer: Search(artist + album title, tracker) Indexer -> Indexer: Jackett API\n/api/v2.0/indexers/all/results Indexer --> Service: SearchResponse (N items) == 5. Parse & Resolve Releases == loop for each search result (with download link & seeders > 0) alt magnet link Service -> Magnet: Resolve(magnet_uri) note right: DHT lookup, 30s timeout\n15s early exit if peers\nbut none active Magnet --> Service: torrent metadata (files, hash, size) Service -> Service: ParseTorrent(torrentData, album) else HTTP torrent link Service -> Service: downloadTorrentData(url) Service -> Service: ParseTorrent(torrentData, album) end note right: Extract: format, bitDepth, sampleRate,\nsource, trackCount, coverArt, cueSheet, ripLog end == 6. Filter & Select Best == Service -> Service: filterByQuality(parsed, quality) note right: Match LOSSLESS/LOSSY/UNSPECIFIED\nagainst release format Service -> Service: selectBestRelease(filtered) note right: Highest seeder count wins == 7. Add to Torrent Client == Service -> QBit: Find(hash) QBit --> Service: not found alt magnet link Service -> QBit: AddMagnet(magnet_uri) else torrent file Service -> QBit: AddTorrent(file) end QBit --> Service: OK == 8. Persist Torrent & Download == Service -> DB: torrents.Create(torrent) note right: Upsert on info_hash\nupdates seeders/peers Service -> DB: torrents.GetByInfoHash(hash) DB --> Service: savedTorrent (with DB id) Service -> DB: downloads.GetActiveByTorrentID(torrent_id) DB --> Service: not found (no active download) Service -> DB: downloads.Create(download) note right: state = "downloading"\nformat, quality, qbit_hash DB --> Service: download (with DB id) == 9. Schedule Download Poll == Service -> River: Insert(PollDownloadArgs) note right: download_id, torrent_hash\ncheck_interval = 30s\nscheduled_at = now + 30s River --> Service: job scheduled == 10. Build & Return Response == Service -> DB: albums.GetByExternalID(external_id) DB --> Service: dbAlbum (refreshed) Service -> DB: downloads.GetByAlbumID(album_id) DB --> Service: downloads (with state) Service -> DB: artists.GetByExternalID(artist_external_id) DB --> Service: dbArtist Service --> Server: MonitorAlbumResponse note right: album: id, title, monitor_state=monitored,\n download: state, format, quality\nartist: id, name, monitor_state\nrelease: hash, format, seeders, tracks Server --> Client: MonitorAlbumResponse == 11. Async: Download Polling (River Worker) == River -> PollWorker: Work(PollDownloadArgs) PollWorker -> QBit: Find(hash) QBit --> PollWorker: TorrentInfo (progress, state, path) alt progress < 100% PollWorker -> River: Insert(PollDownloadArgs) note right: Reschedule after check_interval else progress == 100% PollWorker -> DB: downloads.SetCompleted(id, save_path) PollWorker -> PollWorker: scanAndHashFiles(content_path) note right: Walk directory, identify audio files\n(.flac, .mp3, .aac, ...)\nSHA-256 hash each file PollWorker -> DB: download_files.CreateBatch(files) note right: file_path, file_size, file_type,\nsha256_hash, verified_at end @enduml