Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ for container hostnames, volume mounts and Remote Path Mappings.

## Web interface

Open `http://<magnetowid-host>:8484/` and sign in with the API key to watch the
download queue: the current download's progress, what's next, and any retries.
See [Web interface](docs/usage.md#web-interface).
Open `http://<magnetowid-host>:8484/` and sign in with the API key to see the
queue and history, and to pause, resume or remove downloads. See
[Web interface](docs/usage.md#web-interface).

## Help and contributing

Expand Down
27 changes: 18 additions & 9 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,9 @@ default). Subtitle failures log a warning and keep the video.

### Pausing downloads

Use SABnzbd API commands to pause or resume the queue or individual jobs.
Sonarr and Radarr display paused jobs but cannot control pausing:
Pause or resume the queue or single jobs in the [web interface](#web-interface)
or with SABnzbd API commands. Sonarr and Radarr show paused jobs but cannot
pause them:

```sh
curl 'http://localhost:8484/api?mode=pause&apikey=YOUR_API_KEY'
Expand All @@ -163,13 +164,21 @@ interrupted downloads restart from the beginning when resumed.

## Web interface

Open `http://<host>:8484/` in a browser and sign in with the API key. The page
shows the running download's progress and time left, the queued jobs in the
order they will run, and paused or retrying jobs with their last error. It
refreshes itself every second.
Open `http://<host>:8484/` in a browser and sign in with the API key.

**Queue** shows the running download's progress and time left, queued jobs in
run order, and paused or retrying jobs with their last error. It refreshes
every second. Pause or resume the queue or a job, or remove a job; removing the
running download deletes its partial file. Resumed downloads restart from the
beginning.

**History** lists finished and failed downloads, newest first, and refreshes
every five seconds. Downloads stay for 30 days unless Sonarr or Radarr removes
them first. Removing one can also delete its folder; Sonarr and Radarr cannot
import a download removed from history.

Signing in lasts 30 days. Changing `MAGNETOWID_API_KEY` signs every browser
out. Search for titles and manage downloads in Sonarr/Radarr.
out. Search for titles in Sonarr/Radarr.

## Correcting matches

Expand Down Expand Up @@ -252,8 +261,8 @@ under **Settings → Download Clients**:

## Troubleshooting

Search for titles and manage downloads in Sonarr/Radarr. The
[web interface](#web-interface) shows magnetowid's download queue.
Search for titles in Sonarr/Radarr. The [web interface](#web-interface) shows
the queue and history, with the last error of each failed or retrying job.

| Problem | What to check |
|---|---|
Expand Down
17 changes: 14 additions & 3 deletions internal/downloader/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ func (j Job) TimeLeft(now time.Time) time.Duration {
return time.Duration(float64(now.Sub(j.Started)) * (1 - j.Fraction) / j.Fraction)
}

const historyRetention = 30 * 24 * time.Hour
// HistoryRetention is how long finished jobs are kept.
const HistoryRetention = 30 * 24 * time.Hour

const incompleteDir = ".incomplete"

Expand Down Expand Up @@ -308,9 +309,19 @@ func (q *Queue) Jobs() []Job {
// Delete cancels the job and optionally removes its files. It returns false for
// unknown IDs; a save failure leaves the job unchanged.
func (q *Queue) Delete(id string, deleteFiles bool) (bool, error) {
return q.discard(id, deleteFiles, false)
}

// Cancel deletes an unfinished job. It returns false for unknown and finished
// jobs, which stay in history for import.
func (q *Queue) Cancel(id string) (bool, error) {
return q.discard(id, false, true)
}

func (q *Queue) discard(id string, deleteFiles, unfinishedOnly bool) (bool, error) {
q.mu.Lock()
job, ok := q.jobs[id]
if !ok {
if !ok || (unfinishedOnly && job.Status != StatusQueued && job.Status != StatusDownloading) {
q.mu.Unlock()
return false, nil
}
Expand Down Expand Up @@ -506,7 +517,7 @@ func (q *Queue) prune(now time.Time) {
var old []string
for _, id := range q.order {
j := q.jobs[id]
if (j.Status == StatusCompleted || j.Status == StatusFailed) && now.Sub(j.Finished) > historyRetention {
if (j.Status == StatusCompleted || j.Status == StatusFailed) && now.Sub(j.Finished) > HistoryRetention {
old = append(old, id)
}
}
Expand Down
32 changes: 31 additions & 1 deletion internal/downloader/queue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,36 @@ func TestPauseRunningJob(t *testing.T) {
waitJob(t, q, id, func(j Job) bool { return j.Status == StatusDownloading && !j.Paused && j.Attempts == 1 })
}

func TestCancel(t *testing.T) {
e := &blockingEngine{started: make(chan string, 2)}
q := newQueue(t, t.TempDir(), &fakeProvider{}, e)
run(t, q)
running := add(t, q, "a", "tv", 0, nzb.Ref{Provider: "fake", ID: "a"})
<-e.started
queued := add(t, q, "b", "tv", 0, nzb.Ref{Provider: "fake", ID: "b"})
for _, id := range []string{queued, running} {
if ok, err := q.Cancel(id); !ok || err != nil {
t.Fatalf("Cancel(%s) = %v, %v", id, ok, err)
}
}
if jobs := q.Jobs(); len(jobs) != 0 {
t.Errorf("jobs after cancelling = %+v", jobs)
}
if ok, err := q.Cancel("SABnzbd_nzo_gone"); ok || err != nil {
t.Errorf("cancelling an unknown job = %v, %v", ok, err)
}

// A finished job stays for import.
q = startQueue(t, &fakeProvider{}, &fakeEngine{})
done := waitFinished(t, q, add(t, q, "c", "tv", 0, nzb.Ref{Provider: "fake", ID: "c"}))
if ok, err := q.Cancel(done.ID); ok || err != nil {
t.Errorf("cancelling a finished job = %v, %v", ok, err)
}
if _, err := os.Stat(done.Storage); len(q.Jobs()) != 1 || err != nil {
t.Errorf("finished job after a cancel: %+v, files %v", q.Jobs(), err)
}
}

func TestPauseBeforeDownloadStarts(t *testing.T) {
for name, pause := range map[string]func(q *Queue, id string) error{
"job": func(q *Queue, id string) error { _, err := q.PauseJobs(id); return err },
Expand Down Expand Up @@ -586,7 +616,7 @@ func TestPruneOldHistory(t *testing.T) {
dir := t.TempDir()
q := newQueue(t, dir, &fakeProvider{}, &fakeEngine{})
now := time.Now()
old := now.Add(-historyRetention - time.Hour)
old := now.Add(-HistoryRetention - time.Hour)
set := func(name string, status Status, finished time.Time) string {
id := add(t, q, name, "tv", 0, nzb.Ref{Provider: "fake", ID: name})
q.mu.Lock()
Expand Down
Loading
Loading