139 lines
4.6 KiB
Plaintext
139 lines
4.6 KiB
Plaintext
@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
|