diff --git a/app.py b/app.py
index 37ede9d..4ab59b1 100644
--- a/app.py
+++ b/app.py
@@ -726,6 +726,61 @@ 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"
@@ -1404,6 +1459,41 @@ def get_recently_added(limit=6):
return items
+def search_library(q, limit=24):
+ """Titles in Sonarr/Radarr matching q, case-insensitive substring.
+
+ Filters the same cached lists get_recently_added uses — no new fetch.
+ """
+ needle = q.strip().lower()
+ if not needle:
+ return []
+ items = []
+ try:
+ for sr in sonarr_series_cached():
+ if needle in sr.get("title", "").lower():
+ items.append({
+ "title": sr.get("title", "Unknown"),
+ "kind": "TV",
+ "year": str(sr.get("year", "")),
+ "poster": _poster_url(sr.get("images")),
+ })
+ except Exception:
+ pass
+ try:
+ for mv in radarr_movies_cached():
+ if needle in mv.get("title", "").lower():
+ items.append({
+ "title": mv.get("title", "Unknown"),
+ "kind": "Film",
+ "year": str(mv.get("year", "")),
+ "poster": _poster_url(mv.get("images")),
+ })
+ except Exception:
+ pass
+ items.sort(key=lambda x: x["title"].lower())
+ return items[:limit]
+
+
def get_activity_feed(sabnzbd, ombi, limit=25):
"""A single chronological feed assembled from what the fetchers return.
@@ -1508,9 +1598,11 @@ def watch_page():
@login_required
def library_page():
r = fetch_services(["sonarr", "radarr"])
+ q = request.args.get("q", "").strip()
return render_template("library.html", page="library",
sonarr=r["sonarr"], radarr=r["radarr"],
- recently_added=get_recently_added(12))
+ recently_added=get_recently_added(12),
+ q=q, search_results=search_library(q) if q else None)
@app.route("/discover")
@@ -1519,9 +1611,11 @@ def discover_page():
r = fetch_services(["sonarr", "radarr", "rt_movies", "rt_tv"])
sonarr_titles = set(r["sonarr"].get("sonarr_titles", []))
radarr_titles = set(r["radarr"].get("radarr_titles", []))
+ q = request.args.get("q", "").strip()
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))
+ rt_tv=_filter_rt(r["rt_tv"], sonarr_titles),
+ q=q, search=ombi_search(q) if q else None)
@app.route("/requests")
@@ -1760,7 +1854,7 @@ def api_delete_show():
@login_required
def settings_page():
s = load_settings()
- return render_template("settings.html",
+ return render_template("settings.html", page="settings",
timezone=s.get("timezone", "America/New_York"),
sonarr_api_key=SONARR_API_KEY,
radarr_api_key=RADARR_API_KEY,
diff --git a/static/css/teletext.css b/static/css/teletext.css
index ad36959..69eb404 100644
--- a/static/css/teletext.css
+++ b/static/css/teletext.css
@@ -465,10 +465,15 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
.tt-empty { padding: 0 1ch; color: var(--ink-dim); }
/* ── Fastext ────────────────────────────────────────────
- The four coloured keys along the bottom of every teletext
- page. Real navigation, not decoration: click one, or press
- its initial. Deliberately NOT fixed to the viewport — it is
- the last row of the page, where it always was. */
+ The four coloured keys. Real navigation, not decoration:
+ click one, or press its initial. Deliberately NOT fixed to
+ the viewport — it is a row of the page, where it always
+ was, not furniture bolted over it. On the index it closes
+ the page, under Now Showing/Just Added, because the index's
+ own body is already the full nav. Everywhere else — where
+ these four keys are the only nav on the page — it opens the
+ page, right under the header, so it doesn't need a scroll
+ to reach. */
.fastext {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -477,6 +482,9 @@ a.tt-row:focus-visible .tt-row-sub { color: var(--tt-black); }
padding: 0 var(--gutter);
gap: 1ch;
}
+.fastext-top {
+ margin: var(--row) auto calc(var(--row) * 1.5);
+}
.fastext a {
display: flex;
align-items: center;
diff --git a/static/js/service.js b/static/js/service.js
index d95f0b2..f7e71fc 100644
--- a/static/js/service.js
+++ b/static/js/service.js
@@ -129,7 +129,7 @@
})
.then(function (r) { return r.json(); })
.then(function (d) {
- if (d && d.ok) {
+ if (d && d.success) {
btn.classList.add('done');
btn.textContent = 'ADDED';
} else {
diff --git a/templates/_shell.html b/templates/_shell.html
index 6970434..2509aa4 100644
--- a/templates/_shell.html
+++ b/templates/_shell.html
@@ -45,19 +45,28 @@ provenance.
-
- {% block content %}{% endblock %}
-
-
{#- Fastext: the four coloured keys. Each page names its own four, so the
- keys always point somewhere useful from where you are. -#}
-