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
|
`--ink-dimmer` fails contrast, which is exactly the bug the redesign's
|
||||||
first inspection round caught.
|
first inspection round caught.
|
||||||
- **Navigation is the page number.** Typing any three digits anywhere
|
- **Navigation is the page number.** Typing any three digits anywhere
|
||||||
jumps to that page, and `r`/`g`/`y`/`c` fire the four fastext colour
|
jumps to that page, and `r`/`g`/`y`/`c` fire four of the fastext
|
||||||
keys — both in `static/js/service.js`. `nav_items` in `app.py` is the
|
entries — both in `static/js/service.js`. `nav_items` in `app.py` is
|
||||||
source of truth for the numbers and is serialised into
|
the source of truth for the numbers and is serialised into
|
||||||
`<body data-pages>`.
|
`<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
|
- **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
|
row rather than scrolling, truncating, or folding into a menu. No
|
||||||
header is sticky.
|
header is sticky.
|
||||||
- **`export-to-unraid.sh`** — a pre-existing standalone deploy script
|
- **`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()
|
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):
|
def _ombi_status_label(available, approved, denied):
|
||||||
if denied:
|
if denied:
|
||||||
return "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
|
@app.context_processor
|
||||||
def inject_page_chrome():
|
def inject_page_chrome():
|
||||||
"""Shell-wide context: the nav, the clock's timezone, service links."""
|
"""Shell-wide context: the nav, the clock's timezone, service links."""
|
||||||
|
|
@ -1378,15 +1347,12 @@ def inject_page_chrome():
|
||||||
("activity", "106", "Activity", "/activity"),
|
("activity", "106", "Activity", "/activity"),
|
||||||
("settings", "900", "Settings", "/settings"),
|
("settings", "900", "Settings", "/settings"),
|
||||||
],
|
],
|
||||||
# The four coloured keys along the bottom of every page. A page
|
# Every page, at the top of every page but the index — which
|
||||||
# that wants different destinations passes its own `fastext` to
|
# overrides this with HOME_FASTEXT, its own short closing row,
|
||||||
# render_template, which wins over this default.
|
# since its body already lists all of these once. The four
|
||||||
"fastext": [
|
# letter keys (r/g/y/c) keep their original destinations; the
|
||||||
("k-red", "r", "100", "Index", "/"),
|
# rest have no keyboard shortcut, just a click.
|
||||||
("k-green", "g", "102", "Library", "/library"),
|
"fastext": FULL_FASTEXT,
|
||||||
("k-yellow", "y", "103", "Discover", "/discover"),
|
|
||||||
("k-cyan", "c", "105", "System", "/system"),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1494,6 +1460,51 @@ def search_library(q, limit=24):
|
||||||
return items[:limit]
|
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):
|
def get_activity_feed(sabnzbd, ombi, limit=25):
|
||||||
"""A single chronological feed assembled from what the fetchers return.
|
"""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]
|
others = [s for s in sessions if s is not hero]
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"home.html", page="home",
|
"home.html", page="home", fastext=HOME_FASTEXT,
|
||||||
sonarr=sonarr, radarr=radarr, sabnzbd=r["sabnzbd"], tautulli=tautulli,
|
sonarr=sonarr, radarr=radarr, sabnzbd=r["sabnzbd"], tautulli=tautulli,
|
||||||
prowlarr=r["prowlarr"], ombi=r["ombi"], unraid=r["unraid"],
|
prowlarr=r["prowlarr"], ombi=r["ombi"], unraid=r["unraid"],
|
||||||
hero=hero, other_sessions=others,
|
hero=hero, other_sessions=others,
|
||||||
|
|
@ -1615,7 +1626,7 @@ def discover_page():
|
||||||
return render_template("discover.html", page="discover",
|
return render_template("discover.html", page="discover",
|
||||||
rt_movies=_filter_rt(r["rt_movies"], radarr_titles),
|
rt_movies=_filter_rt(r["rt_movies"], radarr_titles),
|
||||||
rt_tv=_filter_rt(r["rt_tv"], sonarr_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")
|
@app.route("/requests")
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@
|
||||||
<p class="tt-empty">Popular right now, with anything already in library filtered out.</p>
|
<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
|
{#- 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
|
title in mind is the common case. This one hits Radarr's and
|
||||||
sits in front of TheMovieDB/TVDB, then lets you add straight to
|
Sonarr's own lookup directly — the same call /add-to-radarr and
|
||||||
Radarr/Sonarr from the result. -#}
|
/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">
|
<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 }}"
|
<input class="tt-input" type="search" id="q" name="q" value="{{ q }}"
|
||||||
placeholder="SEARCH TO ADD" autocomplete="off" spellcheck="false">
|
placeholder="SEARCH TO ADD" autocomplete="off" spellcheck="false">
|
||||||
<button class="tt-btn" type="submit">Search</button>
|
<button class="tt-btn" type="submit">Search</button>
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
<span class="tt-field-meta">“{{ q }}”</span>
|
<span class="tt-field-meta">“{{ q }}”</span>
|
||||||
</div>
|
</div>
|
||||||
{% if search.status != 'ok' %}
|
{% if search.status != 'ok' %}
|
||||||
{{ service_error('Ombi', search.error) }}
|
{{ service_error('Sonarr/Radarr', search.error) }}
|
||||||
{% elif search.results %}
|
{% elif search.results %}
|
||||||
<div class="tt-tiles">
|
<div class="tt-tiles">
|
||||||
{% for it in search.results %}
|
{% for it in search.results %}
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,19 @@
|
||||||
|
|
||||||
{#- ── Service transmission status ───────────────────────────────
|
{#- ── Service transmission status ───────────────────────────────
|
||||||
Six services, one row each, colour carrying up or down. This is
|
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-field"><span>Transmission</span></div>
|
||||||
<div class="tt-rows">
|
<div class="tt-rows">
|
||||||
{% for name, svc in [('Sonarr', sonarr), ('Radarr', radarr), ('SABnzbd', sabnzbd),
|
{% for name, key, svc in [('Sonarr', 'sonarr', sonarr), ('Radarr', 'radarr', radarr), ('SABnzbd', 'sabnzbd', sabnzbd),
|
||||||
('Tautulli', tautulli), ('Prowlarr', prowlarr), ('Ombi', ombi)] %}
|
('Tautulli', 'tautulli', tautulli), ('Prowlarr', 'prowlarr', prowlarr), ('Ombi', 'ombi', ombi)] %}
|
||||||
<div class="tt-row">
|
<a class="tt-row" href="{{ service_urls[key] }}" target="_blank" rel="noopener">
|
||||||
<span class="tt-row-name">{{ name }}</span>
|
<span class="tt-row-name">{{ name }}</span>
|
||||||
<span class="tt-row-body"></span>
|
<span class="tt-row-body"></span>
|
||||||
<span class="tt-row-val {{ 'ok' if svc.status == 'ok' else 'alert' }}">
|
<span class="tt-row-val {{ 'ok' if svc.status == 'ok' else 'alert' }}">
|
||||||
{{- 'ON AIR' if svc.status == 'ok' else 'OFF AIR' -}}
|
{{- 'ON AIR' if svc.status == 'ok' else 'OFF AIR' -}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue