Trending, most watched and anticipated movies and series from Trakt

With an empty search box, Movies and Series show one of Trakt's public
lists: trending now, most watched this week, anticipated, popular, and
for movies the US box office. [ ] switch lists, r reloads. Each entry is
looked up in Radarr (tmdb:) or Sonarr (tvdb:), so it behaves like a
search result: details, poster, tick when in the library, add, releases.

Needs a Trakt client ID: new optional trakt.client_id setting (Config
view section "Trakt", ARR_TUI_TRAKT_CLIENT_ID), masked like the API keys
and checked by Test. ARR_TUI_TRAKT_URL points at another server for
development.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 23:05:35 +02:00
co-authored by Claude Opus 5.5
parent b71659b357
commit 4dba794b26
18 changed files with 764 additions and 40 deletions
+23 -3
View File
@@ -13,6 +13,7 @@ import (
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
"git.stinnesbeck.com/nils/arr-tui/internal/config"
"git.stinnesbeck.com/nils/arr-tui/internal/sabnzbd"
"git.stinnesbeck.com/nils/arr-tui/internal/trakt"
)
// settingsValues are the settings as shown in the Config view.
@@ -20,6 +21,7 @@ type settingsValues struct {
radarrURL, radarrKey, radarrProfile, radarrFolder string
sonarrURL, sonarrKey, sonarrProfile, sonarrFolder string
sabURL, sabKey string
traktID string
refresh string // seconds, as typed
covers bool
coverStyle string
@@ -47,6 +49,7 @@ func settingsFrom(c *config.Config) settingsValues {
sonarrURL: c.Sonarr.URL, sonarrKey: c.Sonarr.APIKey,
sonarrProfile: c.Sonarr.DefaultQualityProfile, sonarrFolder: c.Sonarr.DefaultRootFolder,
sabURL: c.SABnzbd.URL, sabKey: c.SABnzbd.APIKey,
traktID: c.Trakt.ClientID,
refresh: strconv.Itoa(c.UI.RefreshSeconds),
covers: c.UI.ShowCovers(), coverStyle: style,
notify: c.UI.ShouldNotifyDownloads(), bell: c.UI.Bell,
@@ -70,6 +73,7 @@ func applySettings(raw config.Config, v settingsValues, overridden map[string]st
set("sonarr.apikey", &raw.Sonarr.APIKey, v.sonarrKey)
set("sabnzbd.url", &raw.SABnzbd.URL, v.sabURL)
set("sabnzbd.apikey", &raw.SABnzbd.APIKey, v.sabKey)
set("trakt.client_id", &raw.Trakt.ClientID, v.traktID)
raw.Radarr.DefaultQualityProfile = strings.TrimSpace(v.radarrProfile)
raw.Radarr.DefaultRootFolder = strings.TrimSpace(v.radarrFolder)
raw.Sonarr.DefaultQualityProfile = strings.TrimSpace(v.sonarrProfile)
@@ -161,7 +165,7 @@ func newSettingsView(app *App) *settingsView {
input := func(label, key string) {
in := tview.NewInputField().SetLabel(label)
if strings.HasSuffix(key, ".apikey") {
if secret(key) {
in.SetMaskCharacter('*')
}
if _, ok := v.overridden[key]; ok {
@@ -195,6 +199,8 @@ func newSettingsView(app *App) *settingsView {
service("Radarr", "radarr", true)
service("Sonarr", "sonarr", true)
service("SABnzbd", "sabnzbd", false)
f.AddFormItem(newFormHeading("Trakt (optional: trending, most watched, anticipated)"))
input("Client ID", "trakt.client_id")
// Right below the connection settings it checks.
v.testItem = newFormAction("Test", v.test)
f.AddFormItem(v.testItem)
@@ -295,10 +301,13 @@ func (v *settingsView) edited() {
}
}
// secret reports whether a setting is masked like a password.
func secret(key string) bool { return strings.HasSuffix(key, ".apikey") || key == "trakt.client_id" }
// maskKeys hides or shows the API keys.
func (v *settingsView) maskKeys(hide bool) {
for key, in := range v.inputs {
if strings.HasSuffix(key, ".apikey") {
if secret(key) {
if hide {
in.SetMaskCharacter('*')
} else {
@@ -316,6 +325,7 @@ func (v *settingsView) fill(s settingsValues) {
"radarr.url": s.radarrURL, "radarr.apikey": s.radarrKey,
"sonarr.url": s.sonarrURL, "sonarr.apikey": s.sonarrKey,
"sabnzbd.url": s.sabURL, "sabnzbd.apikey": s.sabKey,
"trakt.client_id": s.traktID,
}
for key, value := range set {
v.inputs[key].SetText(value)
@@ -366,6 +376,7 @@ func (v *settingsView) read() settingsValues {
sonarrURL: text("sonarr.url"), sonarrKey: text("sonarr.apikey"),
sonarrProfile: text("sonarr.profile"), sonarrFolder: text("sonarr.folder"),
sabURL: text("sabnzbd.url"), sabKey: text("sabnzbd.apikey"),
traktID: text("trakt.client_id"),
refresh: v.refresh.GetText(),
covers: v.covers.IsChecked(), coverStyle: style,
notify: v.notify.IsChecked(), bell: v.bell.IsChecked(),
@@ -441,6 +452,15 @@ func (v *settingsView) test() {
return sabnzbd.NewClient(strings.TrimSpace(s.sabURL), strings.TrimSpace(s.sabKey)).Version(ctx)
}},
}
if strings.TrimSpace(s.traktID) != "" {
tests = append(tests, struct {
name string
run probeFn
}{"Trakt", func(ctx context.Context) (string, error) {
_, err := trakt.NewClient(s.traktID, "arr-tui").Entries(ctx, trakt.MovieLists[0], 1)
return "", err
}})
}
for _, t := range tests {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -451,7 +471,7 @@ func (v *settingsView) test() {
v.report("[red]✗ " + t.name + ": " + tview.Escape(err.Error()) + "[-]")
return
}
v.report("[green]✓ " + t.name + " " + tview.Escape(version) + " answers[-]")
v.report("[green]✓ " + strings.TrimSpace(t.name+" "+tview.Escape(version)) + " answers[-]")
})
}()
}