Search Sonarr/Radarr directly on Discover, full top nav, live System links
All checks were successful
Deploy / deploy (push) Successful in 24s

- Discover's search now hits Radarr's/Sonarr's own lookup endpoints
  directly (the same ones /add-to-radarr and /add-to-sonarr already
  use), instead of Ombi — one fewer round trip, and a result always
  resolves the same way its own Add button will. The Ombi-based
  ombi_search/_tmdb_poster helpers are gone.
- The top nav on every page but the index now lists all eight
  destinations (FULL_FASTEXT) instead of the original four; the index
  keeps the original four-entry row as its own closing line
  (HOME_FASTEXT), since its body already lists every destination once.
- System page's Transmission rows are now links to each service's own
  reverse-proxied instance (service_urls, already injected into every
  template but unused until now).
This commit is contained in:
Barely Removable 2026-08-30 08:40:52 -07:00
parent abaccaaacd
commit cef66e7361
4 changed files with 101 additions and 80 deletions

143
app.py
View file

@ -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")