Skip to content

Commit 97e16bd

Browse files
authored
Merge pull request #7 from SushyDev/feature/media-add-verification-process
improvements
2 parents cd0a6ed + 0001954 commit 97e16bd

File tree

2 files changed

+167
-14
lines changed

2 files changed

+167
-14
lines changed

internal/qbittorrent/handler.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -790,9 +790,9 @@ func (h *Handler) validateExpectedFileCount(ctx context.Context, r *http.Request
790790
// This ensures the queue has fresh data before we query it
791791
h.logger.Debug("triggering RefreshMonitoredDownloads before validation",
792792
zap.String("downloadId", downloadID))
793-
if err := h.servarrClient.RefreshMonitoredDownloads(ctx, servarrHost, servarrAPIKey); err != nil {
794-
h.logger.Warn("failed to trigger RefreshMonitoredDownloads", zap.Error(err))
795-
// Don't fail - continue with validation even if refresh fails
793+
if err := h.servarrClient.RefreshMonitoredDownloadsAndWait(ctx, servarrHost, servarrAPIKey, 15); err != nil {
794+
h.logger.Warn("failed to wait for RefreshMonitoredDownloads completion", zap.Error(err))
795+
// Don't fail - continue with validation even if refresh times out
796796
}
797797

798798
// Query *arr queue API - this is populated as soon as Servarr sends the torrent
@@ -906,16 +906,16 @@ func (h *Handler) processTorrentAsync(magnetURL, category string, servarrHost, s
906906
h.logger.Info("triggering RefreshMonitoredDownloads",
907907
zap.String("hash", hash),
908908
zap.String("servarr_host", servarrHost))
909-
if err := h.servarrClient.RefreshMonitoredDownloads(ctx, servarrHost, servarrAPIKey); err != nil {
910-
h.logger.Warn("failed to trigger RefreshMonitoredDownloads", zap.Error(err))
911-
// Don't fail - continue with validation even if refresh fails
909+
if err := h.servarrClient.RefreshMonitoredDownloadsAndWait(ctx, servarrHost, servarrAPIKey, 15); err != nil {
910+
h.logger.Warn("failed to wait for RefreshMonitoredDownloads completion", zap.Error(err))
911+
// Don't fail - continue with validation even if refresh times out
912912
} else {
913-
h.logger.Info("RefreshMonitoredDownloads command sent successfully", zap.String("hash", hash))
913+
h.logger.Info("RefreshMonitoredDownloads completed successfully", zap.String("hash", hash))
914914
}
915915

916-
// Wait for Sonarr to process the refresh and update the queue
917-
h.logger.Info("waiting for queue to refresh", zap.String("hash", hash))
918-
time.Sleep(3 * time.Second)
916+
// Wait a brief moment for the queue to be fully updated
917+
h.logger.Info("waiting for queue to be fully updated", zap.String("hash", hash))
918+
time.Sleep(2 * time.Second)
919919

920920
// Validate file count if enabled
921921
if h.config.MediaValidation.ValidateFileCount {
@@ -953,8 +953,8 @@ func (h *Handler) processTorrentAsync(magnetURL, category string, servarrHost, s
953953

954954
// Trigger RefreshMonitoredDownloads after successful validation
955955
h.logger.Info("triggering RefreshMonitoredDownloads after successful validation", zap.String("hash", hash))
956-
if err := h.servarrClient.RefreshMonitoredDownloads(ctx, servarrHost, servarrAPIKey); err != nil {
957-
h.logger.Warn("failed to trigger RefreshMonitoredDownloads after validation", zap.Error(err))
956+
if err := h.servarrClient.RefreshMonitoredDownloadsAndWait(ctx, servarrHost, servarrAPIKey, 15); err != nil {
957+
h.logger.Warn("failed to wait for RefreshMonitoredDownloads completion after validation", zap.Error(err))
958958
}
959959

960960
// Clear cache so Info() picks up the new torrent from Real-Debrid

internal/servarr/client.go

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func (c *Client) GetHistoryByDownloadID(ctx context.Context, baseURL string, api
309309
type CommandResponse struct {
310310
ID int `json:"id"`
311311
Name string `json:"name"`
312-
Status string `json:"status"`
312+
Status string `json:"status"` // Possible values: queued, started, completed, failed
313313
}
314314

315315
// RefreshMonitoredDownloads triggers Sonarr/Radarr to refresh the download queue
@@ -361,10 +361,163 @@ func (c *Client) RefreshMonitoredDownloads(ctx context.Context, baseURL string,
361361
return fmt.Errorf("decode response: %w", err)
362362
}
363363

364-
c.logger.Info("RefreshMonitoredDownloads command response",
364+
c.logger.Info("RefreshMonitoredDownloads command triggered",
365365
zap.Int("commandId", cmdResp.ID),
366366
zap.String("name", cmdResp.Name),
367367
zap.String("status", cmdResp.Status))
368368

369369
return nil
370370
}
371+
372+
// GetCommandStatus retrieves the status of a command by ID
373+
func (c *Client) GetCommandStatus(ctx context.Context, baseURL string, apiKey string, commandID int) (*CommandResponse, error) {
374+
parsedURL, err := url.Parse(baseURL)
375+
if err != nil {
376+
return nil, fmt.Errorf("invalid base URL: %w", err)
377+
}
378+
379+
parsedURL.Path = fmt.Sprintf("%s/api/v3/command/%d", parsedURL.Path, commandID)
380+
381+
req, err := http.NewRequestWithContext(ctx, "GET", parsedURL.String(), nil)
382+
if err != nil {
383+
return nil, fmt.Errorf("create request: %w", err)
384+
}
385+
386+
req.Header.Set("X-Api-Key", apiKey)
387+
388+
resp, err := c.httpClient.Do(req)
389+
if err != nil {
390+
return nil, fmt.Errorf("request failed: %w", err)
391+
}
392+
defer resp.Body.Close()
393+
394+
if resp.StatusCode == http.StatusUnauthorized {
395+
return nil, fmt.Errorf("unauthorized: check API key")
396+
}
397+
398+
if resp.StatusCode != http.StatusOK {
399+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
400+
}
401+
402+
var cmdResp CommandResponse
403+
if err := json.NewDecoder(resp.Body).Decode(&cmdResp); err != nil {
404+
return nil, fmt.Errorf("decode response: %w", err)
405+
}
406+
407+
return &cmdResp, nil
408+
}
409+
410+
// RefreshMonitoredDownloadsAndWait triggers a refresh and waits for it to complete
411+
func (c *Client) RefreshMonitoredDownloadsAndWait(ctx context.Context, baseURL string, apiKey string, maxWaitSeconds int) error {
412+
parsedURL, err := url.Parse(baseURL)
413+
if err != nil {
414+
return fmt.Errorf("invalid base URL: %w", err)
415+
}
416+
417+
parsedURL.Path = parsedURL.Path + "/api/v3/command"
418+
419+
// Command payload
420+
payload := map[string]string{
421+
"name": "RefreshMonitoredDownloads",
422+
}
423+
424+
jsonData, err := json.Marshal(payload)
425+
if err != nil {
426+
return fmt.Errorf("marshal payload: %w", err)
427+
}
428+
429+
req, err := http.NewRequestWithContext(ctx, "POST", parsedURL.String(), strings.NewReader(string(jsonData)))
430+
if err != nil {
431+
return fmt.Errorf("create request: %w", err)
432+
}
433+
434+
req.Header.Set("X-Api-Key", apiKey)
435+
req.Header.Set("Content-Type", "application/json")
436+
437+
c.logger.Debug("triggering RefreshMonitoredDownloads",
438+
zap.String("url", parsedURL.Host))
439+
440+
resp, err := c.httpClient.Do(req)
441+
if err != nil {
442+
return fmt.Errorf("request failed: %w", err)
443+
}
444+
defer resp.Body.Close()
445+
446+
if resp.StatusCode == http.StatusUnauthorized {
447+
return fmt.Errorf("unauthorized: check API key")
448+
}
449+
450+
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
451+
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
452+
}
453+
454+
var cmdResp CommandResponse
455+
if err := json.NewDecoder(resp.Body).Decode(&cmdResp); err != nil {
456+
return fmt.Errorf("decode response: %w", err)
457+
}
458+
459+
c.logger.Info("RefreshMonitoredDownloads command triggered, waiting for completion",
460+
zap.Int("commandId", cmdResp.ID),
461+
zap.String("name", cmdResp.Name),
462+
zap.String("initialStatus", cmdResp.Status),
463+
zap.Int("maxWaitSeconds", maxWaitSeconds))
464+
465+
// Poll command status until completed or timeout
466+
ticker := time.NewTicker(500 * time.Millisecond) // Poll every 500ms
467+
defer ticker.Stop()
468+
469+
deadline := time.Now().Add(time.Duration(maxWaitSeconds) * time.Second)
470+
471+
for {
472+
select {
473+
case <-ctx.Done():
474+
return fmt.Errorf("context cancelled while waiting for command completion")
475+
476+
case <-ticker.C:
477+
// Check if we've exceeded the deadline
478+
if time.Now().After(deadline) {
479+
c.logger.Warn("RefreshMonitoredDownloads command did not complete in time",
480+
zap.Int("commandId", cmdResp.ID),
481+
zap.Int("maxWaitSeconds", maxWaitSeconds))
482+
return fmt.Errorf("command did not complete within %d seconds", maxWaitSeconds)
483+
}
484+
485+
// Get current command status
486+
status, err := c.GetCommandStatus(ctx, baseURL, apiKey, cmdResp.ID)
487+
if err != nil {
488+
c.logger.Warn("failed to get command status, will retry",
489+
zap.Int("commandId", cmdResp.ID),
490+
zap.Error(err))
491+
continue
492+
}
493+
494+
c.logger.Debug("RefreshMonitoredDownloads command status",
495+
zap.Int("commandId", cmdResp.ID),
496+
zap.String("status", status.Status))
497+
498+
// Check if command is completed
499+
switch status.Status {
500+
case "completed":
501+
c.logger.Info("RefreshMonitoredDownloads command completed",
502+
zap.Int("commandId", cmdResp.ID),
503+
zap.Duration("elapsed", time.Since(time.Now().Add(-time.Duration(maxWaitSeconds)*time.Second))))
504+
return nil
505+
506+
case "failed":
507+
c.logger.Error("RefreshMonitoredDownloads command failed",
508+
zap.Int("commandId", cmdResp.ID))
509+
return fmt.Errorf("command failed")
510+
511+
case "queued", "started":
512+
// Still in progress, continue polling
513+
continue
514+
515+
default:
516+
c.logger.Warn("RefreshMonitoredDownloads command unknown status",
517+
zap.Int("commandId", cmdResp.ID),
518+
zap.String("status", status.Status))
519+
continue
520+
}
521+
}
522+
}
523+
}

0 commit comments

Comments
 (0)