Series drill-down: mark downloading episodes and seasons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+82
-12
@@ -44,16 +44,22 @@ type seriesView struct {
|
||||
expanded map[int]bool
|
||||
rows []epRow
|
||||
loaded bool
|
||||
|
||||
// Episodes in Sonarr's download queue, by episode id; watched while the
|
||||
// view is open.
|
||||
downloading map[int]dlInfo
|
||||
queueStop func()
|
||||
}
|
||||
|
||||
// openSeries opens the season/episode drill-down above the current view.
|
||||
func (a *App) openSeries(c *sonarr.Client, s sonarr.Series, onChange func()) {
|
||||
v := &seriesView{
|
||||
app: a, c: c, series: s, onChange: onChange,
|
||||
header: tview.NewTextView().SetDynamicColors(true),
|
||||
table: tview.NewTable().SetSelectable(true, false).SetFixed(1, 0),
|
||||
detail: newDetailPane(a),
|
||||
expanded: map[int]bool{},
|
||||
header: tview.NewTextView().SetDynamicColors(true),
|
||||
table: tview.NewTable().SetSelectable(true, false).SetFixed(1, 0),
|
||||
detail: newDetailPane(a),
|
||||
expanded: map[int]bool{},
|
||||
downloading: map[int]dlInfo{},
|
||||
}
|
||||
v.table.SetBorder(true)
|
||||
v.cover = coverFor(c.Client, s.Images, true)
|
||||
@@ -90,8 +96,54 @@ func (a *App) openSeries(c *sonarr.Client, s sonarr.Series, onChange func()) {
|
||||
|
||||
v.render()
|
||||
// The pane goes away with the view; stop restyling it then.
|
||||
a.openFull(v.root, "Enter expand/releases · i releases · s search · t monitor · r reload · Esc back", func() { a.forgetPane(v.detail) })
|
||||
a.openFull(v.root, "Enter expand/releases · i releases · s search · t monitor · r reload · Esc back", func() {
|
||||
a.forgetPane(v.detail)
|
||||
v.queueStop()
|
||||
})
|
||||
v.reload()
|
||||
v.loadQueue()
|
||||
v.queueStop = a.every(libQueueInterval, v.loadQueue)
|
||||
}
|
||||
|
||||
// loadQueue marks the episodes in Sonarr's download queue. When one has left
|
||||
// it (imported, or removed) the episodes are loaded again, so its status
|
||||
// follows. Failures leave the marks as they were.
|
||||
func (v *seriesView) loadQueue() {
|
||||
id := v.series.ID
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
items, err := v.c.Queue(ctx)
|
||||
v.app.post(func() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
now := queueByEpisode(items, id)
|
||||
left := false
|
||||
for ep := range v.downloading {
|
||||
if _, still := now[ep]; !still {
|
||||
left = true
|
||||
}
|
||||
}
|
||||
v.downloading = now
|
||||
if left {
|
||||
v.reload()
|
||||
} else {
|
||||
v.render()
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// seasonDownloads counts the episodes of a season in the download queue.
|
||||
func (v *seriesView) seasonDownloads(season int) int {
|
||||
n := 0
|
||||
for _, e := range v.episodes {
|
||||
if _, ok := v.downloading[e.ID]; ok && e.SeasonNumber == season {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// reload fetches the series and its episodes.
|
||||
@@ -192,18 +244,23 @@ func (v *seriesView) render() {
|
||||
} else {
|
||||
status = "[gray]" + status
|
||||
}
|
||||
v.table.SetCell(row, 0, tview.NewTableCell(monMark(v.seasonMonitored(r.season))))
|
||||
v.table.SetCell(row, 0, tview.NewTableCell(libMark(v.seasonMonitored(r.season), v.seasonDownloads(r.season) > 0)))
|
||||
v.table.SetCell(row, 1, tview.NewTableCell(arrow))
|
||||
v.table.SetCell(row, 2, tview.NewTableCell("[::b]"+seasonLabel(r.season)).SetExpansion(1))
|
||||
v.table.SetCell(row, 3, tview.NewTableCell(""))
|
||||
v.table.SetCell(row, 4, tview.NewTableCell(status))
|
||||
} else {
|
||||
e := r.ep
|
||||
v.table.SetCell(row, 0, tview.NewTableCell(monMark(e.Monitored)))
|
||||
dl, downloading := v.downloading[e.ID]
|
||||
status := episodeStatus(*e, today)
|
||||
if downloading && !e.HasFile {
|
||||
status = fmt.Sprintf("[blue]downloading %.0f%%", dl.progress)
|
||||
}
|
||||
v.table.SetCell(row, 0, tview.NewTableCell(libMark(e.Monitored, downloading)))
|
||||
v.table.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("E%02d", e.EpisodeNumber)))
|
||||
v.table.SetCell(row, 2, tview.NewTableCell(tview.Escape(e.Title)).SetExpansion(1))
|
||||
v.table.SetCell(row, 3, tview.NewTableCell(e.AirDate))
|
||||
v.table.SetCell(row, 4, tview.NewTableCell(episodeStatus(*e, today)))
|
||||
v.table.SetCell(row, 4, tview.NewTableCell(status))
|
||||
}
|
||||
if hasSel && r.key() == selKey {
|
||||
selRow = row
|
||||
@@ -248,7 +305,7 @@ func episodeStatus(e sonarr.Episode, today string) string {
|
||||
func (v *seriesView) renderDetail() {
|
||||
r, ok := v.selected()
|
||||
if !ok {
|
||||
v.detail.Show(seriesRowDetail(v.series, nil, 0, seasonSummary{}, false, ""), v.cover)
|
||||
v.detail.Show(seriesRowDetail(v.series, nil, 0, seasonSummary{}, false, "", dlInfo{}), v.cover)
|
||||
return
|
||||
}
|
||||
var sum seasonSummary
|
||||
@@ -257,7 +314,11 @@ func (v *seriesView) renderDetail() {
|
||||
sum = se
|
||||
}
|
||||
}
|
||||
v.detail.Show(seriesRowDetail(v.series, r.ep, r.season, sum, v.seasonMonitored(r.season), time.Now().Format("2006-01-02")), v.seasonCover(r.season))
|
||||
dl := dlInfo{count: v.seasonDownloads(r.season)}
|
||||
if r.ep != nil {
|
||||
dl = v.downloading[r.ep.ID]
|
||||
}
|
||||
v.detail.Show(seriesRowDetail(v.series, r.ep, r.season, sum, v.seasonMonitored(r.season), time.Now().Format("2006-01-02"), dl), v.seasonCover(r.season))
|
||||
}
|
||||
|
||||
// seasonCover is the poster of a season, for its row and its episodes: the
|
||||
@@ -287,8 +348,9 @@ func (v *seriesView) seasonCover(season int) coverRef {
|
||||
|
||||
// seriesRowDetail is the detail text of the drill-down: the series, then the
|
||||
// selected season (ep == nil) or episode. The plot is the episode's, or the
|
||||
// series' while a season is selected.
|
||||
func seriesRowDetail(s sonarr.Series, ep *sonarr.Episode, season int, sum seasonSummary, seasonMonitored bool, today string) detailText {
|
||||
// series' while a season is selected. dl is the episode's place in the
|
||||
// download queue, or for a season the number of its episodes there.
|
||||
func seriesRowDetail(s sonarr.Series, ep *sonarr.Episode, season int, sum seasonSummary, seasonMonitored bool, today string, dl dlInfo) detailText {
|
||||
head := "[::b]" + tview.Escape(s.Title) + "[::-]"
|
||||
if s.Year > 0 {
|
||||
head += fmt.Sprintf(" (%d)", s.Year)
|
||||
@@ -306,6 +368,11 @@ func seriesRowDetail(s sonarr.Series, ep *sonarr.Episode, season int, sum season
|
||||
case ep == nil:
|
||||
head += "[::b]" + seasonLabel(season) + "[::-]\n"
|
||||
head += fmt.Sprintf("%d of %d episodes downloaded\n", sum.have, sum.total)
|
||||
if dl.count == 1 {
|
||||
head += "[blue]Downloading[-] 1 episode\n"
|
||||
} else if dl.count > 1 {
|
||||
head += dl.dlLine()
|
||||
}
|
||||
head += monitoredLine(seasonMonitored)
|
||||
head += "[gray]Enter expands · t monitors the whole season · i searches for releases[-]\n"
|
||||
default:
|
||||
@@ -314,6 +381,9 @@ func seriesRowDetail(s sonarr.Series, ep *sonarr.Episode, season int, sum season
|
||||
head += "Aired: " + ep.AirDate + "\n"
|
||||
}
|
||||
head += "Status: " + episodeStatus(*ep, today) + "[-]\n"
|
||||
if dl.count > 0 {
|
||||
head += dl.dlLine()
|
||||
}
|
||||
head += monitoredLine(ep.Monitored)
|
||||
if ep.Overview != "" {
|
||||
plot = tview.Escape(ep.Overview)
|
||||
|
||||
Reference in New Issue
Block a user