Search Sonarr/Radarr directly on Discover, full top nav, live System links
All checks were successful
Deploy / deploy (push) Successful in 24s
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:
parent
abaccaaacd
commit
cef66e7361
4 changed files with 101 additions and 80 deletions
16
CLAUDE.md
16
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
|
||||
`<body data-pages>`.
|
||||
- **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
|
||||
|
|
|
|||
143
app.py
143
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")
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
<p class="tt-empty">Popular right now, with anything already in library filtered out.</p>
|
||||
|
||||
{#- 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. -#}
|
||||
<form class="tt-search" action="{{ request.script_root }}/discover" method="get" role="search">
|
||||
<label class="visually-hidden" for="q">Search Ombi for a title</label>
|
||||
<label class="visually-hidden" for="q">Search Sonarr and Radarr for a title to add</label>
|
||||
<input class="tt-input" type="search" id="q" name="q" value="{{ q }}"
|
||||
placeholder="SEARCH TO ADD" autocomplete="off" spellcheck="false">
|
||||
<button class="tt-btn" type="submit">Search</button>
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
<span class="tt-field-meta">“{{ q }}”</span>
|
||||
</div>
|
||||
{% if search.status != 'ok' %}
|
||||
{{ service_error('Ombi', search.error) }}
|
||||
{{ service_error('Sonarr/Radarr', search.error) }}
|
||||
{% elif search.results %}
|
||||
<div class="tt-tiles">
|
||||
{% for it in search.results %}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,19 @@
|
|||
|
||||
{#- ── Service transmission status ───────────────────────────────
|
||||
Six services, one row each, colour carrying up or down. This is
|
||||
what the old header pills were, given room to say something. -#}
|
||||
what the old header pills were, given room to say something. Each
|
||||
row opens that service's own instance, via the reverse proxy. -#}
|
||||
<div class="tt-field"><span>Transmission</span></div>
|
||||
<div class="tt-rows">
|
||||
{% for name, svc in [('Sonarr', sonarr), ('Radarr', radarr), ('SABnzbd', sabnzbd),
|
||||
('Tautulli', tautulli), ('Prowlarr', prowlarr), ('Ombi', ombi)] %}
|
||||
<div class="tt-row">
|
||||
{% for name, key, svc in [('Sonarr', 'sonarr', sonarr), ('Radarr', 'radarr', radarr), ('SABnzbd', 'sabnzbd', sabnzbd),
|
||||
('Tautulli', 'tautulli', tautulli), ('Prowlarr', 'prowlarr', prowlarr), ('Ombi', 'ombi', ombi)] %}
|
||||
<a class="tt-row" href="{{ service_urls[key] }}" target="_blank" rel="noopener">
|
||||
<span class="tt-row-name">{{ name }}</span>
|
||||
<span class="tt-row-body"></span>
|
||||
<span class="tt-row-val {{ 'ok' if svc.status == 'ok' else 'alert' }}">
|
||||
{{- 'ON AIR' if svc.status == 'ok' else 'OFF AIR' -}}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue