Refactor MonitorAlbumStream: EventPublisher interface, background workflows, DB-before-qBit save order
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+82
-139
@@ -17,112 +17,20 @@ import (
|
||||
var MaxPromptTimeout = 300 * time.Second
|
||||
|
||||
type monitorWorkflow struct {
|
||||
stream pb.MusicAgregatorService_MonitorAlbumStreamServer
|
||||
mode pb.InteractionMode
|
||||
req *pb.StartMonitorRequest
|
||||
service *MusicAgregatorService
|
||||
publisher EventPublisher
|
||||
|
||||
stream pb.MusicAgregatorService_MonitorAlbumStreamServer
|
||||
decisions chan *pb.UserDecision
|
||||
cancel context.CancelFunc
|
||||
mu sync.Mutex
|
||||
promptID int
|
||||
}
|
||||
|
||||
func newMonitorWorkflow(
|
||||
stream pb.MusicAgregatorService_MonitorAlbumStreamServer,
|
||||
req *pb.StartMonitorRequest,
|
||||
service *MusicAgregatorService,
|
||||
cancel context.CancelFunc,
|
||||
) *monitorWorkflow {
|
||||
return &monitorWorkflow{
|
||||
stream: stream,
|
||||
mode: req.Mode,
|
||||
req: req,
|
||||
service: service,
|
||||
decisions: make(chan *pb.UserDecision, 1),
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
addedHash string
|
||||
workflowRunID string
|
||||
|
||||
func (w *monitorWorkflow) sendStatus(step pb.MonitorStep, msg string) error {
|
||||
select {
|
||||
case <-w.stream.Context().Done():
|
||||
return w.stream.Context().Err()
|
||||
default:
|
||||
}
|
||||
return w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Status{
|
||||
Status: &pb.StatusUpdate{Step: step, Message: msg},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) sendStatusWithAlbumInfo(step pb.MonitorStep, msg string, info *pb.StreamAlbumInfo) error {
|
||||
select {
|
||||
case <-w.stream.Context().Done():
|
||||
return w.stream.Context().Err()
|
||||
default:
|
||||
}
|
||||
return w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Status{
|
||||
Status: &pb.StatusUpdate{
|
||||
Step: step,
|
||||
Message: msg,
|
||||
Data: &pb.StatusUpdate_AlbumInfo{AlbumInfo: info},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) sendStatusWithTorrents(step pb.MonitorStep, msg string, torrents *pb.TorrentList) error {
|
||||
select {
|
||||
case <-w.stream.Context().Done():
|
||||
return w.stream.Context().Err()
|
||||
default:
|
||||
}
|
||||
return w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Status{
|
||||
Status: &pb.StatusUpdate{
|
||||
Step: step,
|
||||
Message: msg,
|
||||
Data: &pb.StatusUpdate_Torrents{Torrents: torrents},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) sendStatusWithRelease(step pb.MonitorStep, msg string, release *pb.ReleaseInfo) error {
|
||||
select {
|
||||
case <-w.stream.Context().Done():
|
||||
return w.stream.Context().Err()
|
||||
default:
|
||||
}
|
||||
return w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Status{
|
||||
Status: &pb.StatusUpdate{
|
||||
Step: step,
|
||||
Message: msg,
|
||||
Data: &pb.StatusUpdate_ReleaseInfo{ReleaseInfo: release},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) sendError(step pb.MonitorStep, err error, recoverable bool) error {
|
||||
return w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Error{
|
||||
Error: &pb.ErrorUpdate{
|
||||
FailedStep: step,
|
||||
Message: err.Error(),
|
||||
Recoverable: recoverable,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) sendResult(result *pb.MonitorAlbumResponse) error {
|
||||
return w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Result{Result: result},
|
||||
})
|
||||
mu sync.Mutex
|
||||
promptID int
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) nextPromptID() string {
|
||||
@@ -137,6 +45,10 @@ func (w *monitorWorkflow) promptAndWait(ctx context.Context, prompt *pb.PromptFo
|
||||
return w.defaultDecision(prompt), nil
|
||||
}
|
||||
|
||||
if w.stream == nil {
|
||||
return w.defaultDecision(prompt), nil
|
||||
}
|
||||
|
||||
if err := w.stream.Send(&pb.MonitorAlbumStreamResponse{
|
||||
Message: &pb.MonitorAlbumStreamResponse_Prompt{Prompt: prompt},
|
||||
}); err != nil {
|
||||
@@ -184,6 +96,10 @@ func (w *monitorWorkflow) defaultDecision(prompt *pb.PromptForDecision) *pb.User
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) receiveDecisions(ctx context.Context) {
|
||||
if w.stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -197,7 +113,9 @@ func (w *monitorWorkflow) receiveDecisions(ctx context.Context) {
|
||||
}
|
||||
|
||||
if msg.GetCancel() != nil {
|
||||
w.cancel()
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -217,7 +135,7 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
default:
|
||||
}
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, "Fetching album metadata...")
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, "Fetching album metadata...", nil)
|
||||
|
||||
album, err := w.service.metadata.GetAlbum(ctx, w.req.AlbumId)
|
||||
if err != nil {
|
||||
@@ -225,7 +143,7 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
return ctx.Err()
|
||||
}
|
||||
log.Error().Err(err).Str("album_id", w.req.AlbumId).Msg("failed to get album")
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, err, false)
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, err, false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -234,7 +152,7 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
artistName = album.GetArtists()[0].GetArtist().GetName()
|
||||
}
|
||||
|
||||
w.sendStatusWithAlbumInfo(pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA,
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA,
|
||||
fmt.Sprintf("Got metadata: %s - %s", artistName, album.GetTitle()),
|
||||
&pb.StreamAlbumInfo{
|
||||
Artist: artistName,
|
||||
@@ -242,18 +160,29 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
ReleaseDate: album.GetReleaseDate(),
|
||||
})
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED, "Checking if already owned...")
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED, "Checking if already owned...", nil)
|
||||
|
||||
dbAlbum, _ := w.service.metadata.GetAlbumByExternalID(ctx, album.GetId())
|
||||
if dbAlbum != nil {
|
||||
w.publisher.SetAlbumID(dbAlbum.ID)
|
||||
w.service.metadata.SetAlbumMonitorState(ctx, dbAlbum.ID, database.Monitored)
|
||||
dbAlbum.MonitorState = database.Monitored
|
||||
|
||||
if w.workflowRunID == "" {
|
||||
run := &database.WorkflowRun{AlbumID: dbAlbum.ID, Quality: w.req.Quality.String()}
|
||||
if err := w.service.workflowRuns.Create(ctx, run); err != nil && err != database.ErrWorkflowAlreadyRunning {
|
||||
log.Warn().Err(err).Msg("failed to create workflow run")
|
||||
} else if err == nil {
|
||||
w.workflowRunID = run.ID
|
||||
w.publisher.SetWorkflowRunID(run.ID)
|
||||
}
|
||||
}
|
||||
|
||||
qualityStr := normalizeQuality(w.req.Quality, 0, 0)
|
||||
owned, err := w.service.downloads.HasAlbumInQuality(ctx, dbAlbum.ID, w.req.Quality.String(), qualityStr)
|
||||
if err == nil && owned {
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED,
|
||||
fmt.Sprintf("Already owned in %s quality", qualityStr))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED,
|
||||
fmt.Sprintf("Already owned in %s quality", qualityStr), nil)
|
||||
|
||||
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL {
|
||||
decision, err := w.promptAndWait(ctx, &pb.PromptForDecision{
|
||||
@@ -270,26 +199,26 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED, err, false)
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED, err, false)
|
||||
return err
|
||||
}
|
||||
if !decision.GetConfirm() {
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_COMPLETE, "Skipped - already owned")
|
||||
return w.sendResult(w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Skipped - already owned", nil)
|
||||
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
}
|
||||
} else {
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_COMPLETE, "Already owned")
|
||||
return w.sendResult(w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Already owned", nil)
|
||||
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER,
|
||||
fmt.Sprintf("Searching indexers for %s - %s...", artistName, album.GetTitle()))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER,
|
||||
fmt.Sprintf("Searching indexers for %s - %s...", artistName, album.GetTitle()), nil)
|
||||
|
||||
searchResult, err := w.service.searchIndexer(album, w.req.IndexerOptions.GetTracker())
|
||||
if err != nil {
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER, err, true)
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER, err, true)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -307,17 +236,17 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
Lossless: p.rel.Format.IsLossless(),
|
||||
}
|
||||
}
|
||||
w.sendStatusWithTorrents(pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
|
||||
fmt.Sprintf("Parsed %d from %d torrents", len(parsed), len(searchResult.Items)),
|
||||
&pb.TorrentList{Torrents: summaries})
|
||||
} else {
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
|
||||
fmt.Sprintf("Found %d torrents, none parseable", len(searchResult.Items)))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
|
||||
fmt.Sprintf("Found %d torrents, none parseable", len(searchResult.Items)), nil)
|
||||
}
|
||||
|
||||
if len(parsed) == 0 {
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_COMPLETE, "No parseable results found")
|
||||
return w.sendResult(w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "No parseable results found", nil)
|
||||
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
}
|
||||
|
||||
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL && len(parsed) > 1 {
|
||||
@@ -348,7 +277,7 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS, err, false)
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS, err, false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -371,14 +300,14 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY,
|
||||
fmt.Sprintf("Filtering %d results by quality...", len(parsed)))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY,
|
||||
fmt.Sprintf("Filtering %d results by quality...", len(parsed)), nil)
|
||||
|
||||
filtered := filterByQuality(parsed, w.req.Quality)
|
||||
if len(filtered) == 0 {
|
||||
log.Warn().Str("album", album.GetTitle()).Str("quality", w.req.Quality.String()).Msg("no releases match quality filter")
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_COMPLETE, "No releases match quality filter")
|
||||
return w.sendResult(w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "No releases match quality filter", nil)
|
||||
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
}
|
||||
|
||||
var best parsedItem
|
||||
@@ -412,7 +341,7 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE, err, false)
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE, err, false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -430,7 +359,7 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
best = selectBestRelease(filtered)
|
||||
}
|
||||
|
||||
w.sendStatusWithRelease(pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE,
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE,
|
||||
fmt.Sprintf("Selected: %s (%d seeders)", best.item.Title, best.item.Seeders),
|
||||
&pb.ReleaseInfo{
|
||||
InfoHash: best.rel.InfoHash,
|
||||
@@ -441,8 +370,8 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
Tracker: best.item.Tracker,
|
||||
})
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT,
|
||||
fmt.Sprintf("Adding torrent: %s...", best.item.Title))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT,
|
||||
fmt.Sprintf("Adding torrent: %s...", best.item.Title), nil)
|
||||
|
||||
if w.mode == pb.InteractionMode_INTERACTION_MODE_MANUAL {
|
||||
decision, err := w.promptAndWait(ctx, &pb.PromptForDecision{
|
||||
@@ -459,30 +388,44 @@ func (w *monitorWorkflow) run(ctx context.Context) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, err, false)
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, err, false)
|
||||
return err
|
||||
}
|
||||
if !decision.GetConfirm() {
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_COMPLETE, "Skipped by user")
|
||||
return w.sendResult(w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Skipped by user", nil)
|
||||
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, nil))
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.service.addToTorrentClient(best); err != nil {
|
||||
w.sendError(pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, err, true)
|
||||
return err
|
||||
}
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_SAVING, "Saving to database...")
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_SAVING, "Saving to database...", nil)
|
||||
|
||||
dbAlbum, _ = w.service.metadata.GetAlbumByExternalID(ctx, album.GetId())
|
||||
if dbAlbum != nil {
|
||||
w.publisher.SetAlbumID(dbAlbum.ID)
|
||||
w.service.saveTorrentAndDownload(ctx, dbAlbum.ID, best)
|
||||
} else {
|
||||
log.Warn().Str("album_id", w.req.AlbumId).Msg("album not in DB, skipping torrent/download persistence")
|
||||
}
|
||||
|
||||
w.sendStatus(pb.MonitorStep_MONITOR_STEP_COMPLETE, "Done!")
|
||||
w.addedHash = best.rel.InfoHash
|
||||
|
||||
return w.sendResult(w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, &best))
|
||||
if err := w.service.addToTorrentClient(best); err != nil {
|
||||
w.publisher.PublishError(ctx, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, err, true)
|
||||
return err
|
||||
}
|
||||
|
||||
w.publisher.PublishStatus(ctx, pb.MonitorStep_MONITOR_STEP_COMPLETE, "Done!", nil)
|
||||
|
||||
return w.publisher.PublishResult(ctx, w.service.buildMonitorAlbumResponse(ctx, album, dbAlbum, &best))
|
||||
}
|
||||
|
||||
func (w *monitorWorkflow) cleanup(ctx context.Context) {
|
||||
if w.addedHash != "" {
|
||||
if err := w.service.torrentClient.DeleteTorrent(w.addedHash); err != nil {
|
||||
log.Warn().Err(err).Str("hash", w.addedHash).Msg("failed to delete torrent during cancel cleanup")
|
||||
}
|
||||
if err := w.service.downloads.SetCancelledByQbitHash(ctx, w.addedHash); err != nil {
|
||||
log.Warn().Err(err).Str("hash", w.addedHash).Msg("failed to cancel download during cleanup")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user