Library: mark titles that are downloading

While the Library is visible it reads the Radarr and Sonarr queues (on
opening, every 10 s, and shortly after a grab) and matches them to the
titles by movieId / seriesId. Those titles get a blue dot instead of the
green or grey one, and their details say how far the download is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 10:04:53 +02:00
co-authored by Claude Opus 5.5
parent 7261667d72
commit 2b6cf71967
10 changed files with 181 additions and 4 deletions
+105
View File
@@ -0,0 +1,105 @@
package ui
import (
"context"
"fmt"
"time"
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
)
// libQueueInterval is how often the Library asks for the download queues
// while it is visible.
const libQueueInterval = 10 * time.Second
// dlInfo says that a library title is in the Radarr/Sonarr download queue.
type dlInfo struct {
count int // queue entries (episodes of a series)
progress float64 // percent, of the first entry
timeLeft string // of the first entry
}
// dlLine is the details pane line for a title in the queue.
func (d dlInfo) dlLine() string {
if d.count > 1 {
return fmt.Sprintf("[blue]Downloading[-] %d episodes\n", d.count)
}
line := fmt.Sprintf("[blue]Downloading[-] %.0f%%", d.progress)
if d.timeLeft != "" {
line += " · " + d.timeLeft + " left"
}
return line + "\n"
}
// queueByTitle groups queue entries by the library title they belong to.
func queueByTitle(items []arr.QueueItem, series bool) map[int]dlInfo {
out := map[int]dlInfo{}
for _, q := range items {
id := q.MovieID
if series {
id = q.SeriesID
}
if id == 0 {
continue
}
d, seen := out[id]
d.count++
if !seen {
d.progress, d.timeLeft = q.Progress(), q.TimeLeft
}
out[id] = d
}
return out
}
// loadQueue fetches both download queues and marks the titles in them.
// Failures (a service down or not configured) leave the marks as they were.
func (v *libraryView) loadQueue() {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
movies, merr := v.radarr.Queue(ctx)
series, serr := v.sonarr.Queue(ctx)
v.app.post(func() {
if merr == nil {
v.downloading[kindMovies] = queueByTitle(movies, false)
}
if serr == nil {
v.downloading[kindSeries] = queueByTitle(series, true)
}
if merr == nil || serr == nil {
v.render()
}
})
}()
}
// startQueueWatch refreshes the queue marks while the Library is visible.
func (v *libraryView) startQueueWatch() {
v.loadQueue()
if v.queueStop != nil {
return
}
stop := make(chan struct{})
v.queueStop = stop
go func() {
t := time.NewTicker(libQueueInterval)
defer t.Stop()
for {
select {
case <-stop:
return
case <-t.C:
v.app.post(v.loadQueue)
}
}
}()
}
// deactivate stops the queue watch when the Library is left.
func (v *libraryView) deactivate() {
if v.queueStop != nil {
close(v.queueStop)
v.queueStop = nil
}
}