package ui import ( "cmp" "context" "fmt" "slices" "github.com/rivo/tview" "git.stinnesbeck.com/nils/arr-tui/internal/arr" "git.stinnesbeck.com/nils/arr-tui/internal/sonarr" ) // seasonSummary counts the episodes of one season. type seasonSummary struct { number, total, have int } // summarizeSeasons groups episodes by season, newest season first and specials last. func summarizeSeasons(episodes []sonarr.Episode) []seasonSummary { byNumber := map[int]*seasonSummary{} for _, e := range episodes { s := byNumber[e.SeasonNumber] if s == nil { s = &seasonSummary{number: e.SeasonNumber} byNumber[e.SeasonNumber] = s } s.total++ if e.HasFile { s.have++ } } out := make([]seasonSummary, 0, len(byNumber)) for _, s := range byNumber { out = append(out, *s) } slices.SortFunc(out, func(a, b seasonSummary) int { // specials (season 0) last, otherwise descending return cmp.Or(cmp.Compare(boolInt(a.number == 0), boolInt(b.number == 0)), cmp.Compare(b.number, a.number)) }) return out } func seasonLabel(n int) string { if n == 0 { return "Specials" } return fmt.Sprintf("Season %d", n) } // pickList shows a selectable list in a dialog; Esc closes it. func (a *App) pickList(title string, labels []string, onPick func(i int)) { list := tview.NewList().ShowSecondaryText(false) for _, l := range labels { list.AddItem(l, "", 0, nil) } list.SetSelectedFunc(func(i int, _, _ string, _ rune) { a.closeModal() onPick(i) }) list.SetDoneFunc(a.closeModal) list.SetBorder(true).SetTitle(" " + title + " ") a.openModal(list, 56, min(len(labels)+2, 22)) } // pickSeason lets the user choose a season, then an episode or the whole season, // and opens the interactive search for it. func (a *App) pickSeason(c *sonarr.Client, s sonarr.Series, episodes []sonarr.Episode) { seasons := summarizeSeasons(episodes) if len(seasons) == 0 { a.SetMessage("[yellow]Sonarr has no episodes for " + tview.Escape(s.Title)) return } labels := make([]string, len(seasons)) for i, se := range seasons { labels[i] = fmt.Sprintf("%s (%d/%d downloaded)", seasonLabel(se.number), se.have, se.total) } a.pickList("Search releases for "+tview.Escape(s.Title), labels, func(i int) { a.pickEpisode(c, s, seasons[i].number, episodes) }) } func (a *App) pickEpisode(c *sonarr.Client, s sonarr.Series, season int, all []sonarr.Episode) { var episodes []sonarr.Episode for _, e := range all { if e.SeasonNumber == season { episodes = append(episodes, e) } } slices.SortFunc(episodes, func(x, y sonarr.Episode) int { return cmp.Compare(x.EpisodeNumber, y.EpisodeNumber) }) labels := []string{"Whole season"} for _, e := range episodes { mark := "" if e.HasFile { mark = " ✓" } labels = append(labels, fmt.Sprintf("E%02d %s%s", e.EpisodeNumber, tview.Escape(e.Title), mark)) } title := tview.Escape(s.Title) + ", " + seasonLabel(season) a.pickList(title, labels, func(i int) { if i == 0 { a.openReleases("Releases for "+title, func(ctx context.Context) ([]arr.Release, error) { return c.SeasonReleases(ctx, s.ID, season) }, c.Grab) return } e := episodes[i-1] a.openReleases(fmt.Sprintf("Releases for %s S%02dE%02d", tview.Escape(s.Title), season, e.EpisodeNumber), func(ctx context.Context) ([]arr.Release, error) { return c.EpisodeReleases(ctx, e.ID) }, c.Grab) }) }