diff --git a/CLAUDE.md b/CLAUDE.md index a5b4c8d..49d525f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -96,12 +96,20 @@ the user's Unraid home server. Server-side project directory is still named `--ink-dimmer` fails contrast, which is exactly the bug the redesign's first inspection round caught. - **Navigation is the page number.** Typing any three digits anywhere - jumps to that page, and `r`/`g`/`y`/`c` fire the four fastext colour - keys — both in `static/js/service.js`. `nav_items` in `app.py` is the - source of truth for the numbers and is serialised into + jumps to that page, and `r`/`g`/`y`/`c` fire four of the fastext + entries — both in `static/js/service.js`. `nav_items` in `app.py` is + the source of truth for the numbers and is serialised into ``. + - **Fastext leads everywhere but the index, and closes the index.** + `FULL_FASTEXT` in `app.py` lists every destination and renders right + under the header on every page but the index — those pages carry no + other nav, so it needs no scroll to reach. The index overrides it + with `HOME_FASTEXT`, the original four-entry row, as its own closing + line, since its body already lists every destination once. Only + those original four (Index/Library/Discover/System) carry an `r`/ + `g`/`y`/`c` keyboard shortcut; the rest are click-only. - **Nothing is clipped, hidden, or sticky.** All seven page numbers and - all four fastext keys stay visible at every width, reflowing to another + every fastext entry stay visible at every width, reflowing to another row rather than scrolling, truncating, or folding into a menu. No header is sticky. - **`export-to-unraid.sh`** — a pre-existing standalone deploy script diff --git a/app.py b/app.py index 4ab59b1..e6db2dc 100644 --- a/app.py +++ b/app.py @@ -726,61 +726,6 @@ def ombi_get(endpoint): return resp.json() -def _tmdb_poster(path): - """Ombi's movie search returns a bare TMDB path ('/abc123.jpg'), not a - usable URL; TV search returns a full TVDB banner URL already.""" - if not path: - return "" - if path.startswith("http"): - return path - return f"https://image.tmdb.org/t/p/w300{path}" - - -def ombi_search(q, limit=12): - """Movies and TV matching q, via Ombi's own search — it already sits in - front of TheMovieDB/TVDB, so this is one call instead of two more API - keys. Cross-referenced against the cached Sonarr/Radarr title sets so a - title already held shows that instead of an Add button. - """ - term = q.strip() - if not term: - return {"status": "ok", "results": []} - encoded = requests.utils.quote(term) - try: - radarr_titles = {t.lower() for t in get_radarr_summary().get("radarr_titles", [])} - sonarr_titles = {t.lower() for t in get_sonarr_summary().get("sonarr_titles", [])} - results = [] - try: - for m in ombi_get(f"Search/movie/{encoded}")[:limit]: - title = m.get("title", "") - results.append({ - "title": title, - "year": (m.get("releaseDate") or "")[:4], - "poster": _tmdb_poster(m.get("posterPath")), - "kind": "Film", - "target": "radarr", - "held": bool(m.get("available")) or title.lower() in radarr_titles, - }) - except Exception: - pass - try: - for s in ombi_get(f"Search/tv/{encoded}")[:limit]: - title = s.get("title", "") - results.append({ - "title": title, - "year": (s.get("firstAired") or "")[:4], - "poster": s.get("banner") or _tmdb_poster(s.get("posterPath")), - "kind": "TV", - "target": "sonarr", - "held": bool(s.get("available")) or title.lower() in sonarr_titles, - }) - except Exception: - pass - return {"status": "ok", "results": results} - except Exception as e: - return {"status": "error", "error": str(e), "results": []} - - def _ombi_status_label(available, approved, denied): if denied: return "Denied" @@ -1358,6 +1303,30 @@ def service_urls(): } +# Every destination, for the top of every page but the index. Order +# matches nav_items; the four letter keys keep the destinations they +# always had, cycling colour for the rest since only four exist. +FULL_FASTEXT = [ + ("k-red", "r", "100", "Index", "/"), + ("k-cyan", "", "101", "Watch", "/watch"), + ("k-green", "g", "102", "Library", "/library"), + ("k-yellow", "y", "103", "Discover", "/discover"), + ("k-red", "", "104", "Requests", "/requests"), + ("k-cyan", "c", "105", "System", "/system"), + ("k-green", "", "106", "Activity", "/activity"), + ("k-yellow", "", "900", "Settings", "/settings"), +] + +# The index's own closing row: just the four letter keys, since the +# index's body already lists every one of the above once. +HOME_FASTEXT = [ + ("k-red", "r", "100", "Index", "/"), + ("k-green", "g", "102", "Library", "/library"), + ("k-yellow", "y", "103", "Discover", "/discover"), + ("k-cyan", "c", "105", "System", "/system"), +] + + @app.context_processor def inject_page_chrome(): """Shell-wide context: the nav, the clock's timezone, service links.""" @@ -1378,15 +1347,12 @@ def inject_page_chrome(): ("activity", "106", "Activity", "/activity"), ("settings", "900", "Settings", "/settings"), ], - # The four coloured keys along the bottom of every page. A page - # that wants different destinations passes its own `fastext` to - # render_template, which wins over this default. - "fastext": [ - ("k-red", "r", "100", "Index", "/"), - ("k-green", "g", "102", "Library", "/library"), - ("k-yellow", "y", "103", "Discover", "/discover"), - ("k-cyan", "c", "105", "System", "/system"), - ], + # Every page, at the top of every page but the index — which + # overrides this with HOME_FASTEXT, its own short closing row, + # since its body already lists all of these once. The four + # letter keys (r/g/y/c) keep their original destinations; the + # rest have no keyboard shortcut, just a click. + "fastext": FULL_FASTEXT, } @@ -1494,6 +1460,51 @@ def search_library(q, limit=24): return items[:limit] +def arr_search(q, limit=12): + """Movies and TV matching q, straight from Radarr's and Sonarr's own + lookup endpoints — the same ones /add-to-radarr and /add-to-sonarr + already use to resolve a title before adding. No Ombi round trip and + no second title-matching path to keep in sync with the Add buttons. + """ + term = q.strip() + if not term: + return {"status": "ok", "results": []} + encoded = requests.utils.quote(term) + results = [] + radarr_ok = sonarr_ok = True + try: + radarr_titles = {t.lower() for t in get_radarr_summary().get("radarr_titles", [])} + for m in radarr_get(f"movie/lookup?term={encoded}")[:limit]: + title = m.get("title", "") + results.append({ + "title": title, + "year": str(m.get("year", "")), + "poster": _poster_url(m.get("images")), + "kind": "Film", + "target": "radarr", + "held": title.lower() in radarr_titles, + }) + except Exception: + radarr_ok = False + try: + sonarr_titles = {t.lower() for t in get_sonarr_summary().get("sonarr_titles", [])} + for s in sonarr_get(f"series/lookup?term={encoded}")[:limit]: + title = s.get("title", "") + results.append({ + "title": title, + "year": str(s.get("year", "")), + "poster": _poster_url(s.get("images")), + "kind": "TV", + "target": "sonarr", + "held": title.lower() in sonarr_titles, + }) + except Exception: + sonarr_ok = False + if not results and not radarr_ok and not sonarr_ok: + return {"status": "error", "error": "Sonarr and Radarr both unreachable", "results": []} + return {"status": "ok", "results": results} + + def get_activity_feed(sabnzbd, ombi, limit=25): """A single chronological feed assembled from what the fetchers return. @@ -1572,7 +1583,7 @@ def home(): others = [s for s in sessions if s is not hero] return render_template( - "home.html", page="home", + "home.html", page="home", fastext=HOME_FASTEXT, sonarr=sonarr, radarr=radarr, sabnzbd=r["sabnzbd"], tautulli=tautulli, prowlarr=r["prowlarr"], ombi=r["ombi"], unraid=r["unraid"], hero=hero, other_sessions=others, @@ -1615,7 +1626,7 @@ def discover_page(): return render_template("discover.html", page="discover", rt_movies=_filter_rt(r["rt_movies"], radarr_titles), rt_tv=_filter_rt(r["rt_tv"], sonarr_titles), - q=q, search=ombi_search(q) if q else None) + q=q, search=arr_search(q) if q else None) @app.route("/requests") diff --git a/templates/discover.html b/templates/discover.html index c54c94d..5063a0b 100644 --- a/templates/discover.html +++ b/templates/discover.html @@ -7,11 +7,12 @@

Popular right now, with anything already in library filtered out.

{#- Search sits at the top for the same reason it does on Library: a - title in mind is the common case. This one asks Ombi, which already - sits in front of TheMovieDB/TVDB, then lets you add straight to - Radarr/Sonarr from the result. -#} + title in mind is the common case. This one hits Radarr's and + Sonarr's own lookup directly — the same call /add-to-radarr and + /add-to-sonarr already make — so a result here always resolves the + same way a click on its own Add button will. -#}