Screenshots: Trakt lists, Trakt in the Config view

The fake services gain a Trakt API (port 19894) and a sequel that is not
in the library; a new scene shows the Anticipated list. The other
pictures are retaken (Config has the Trakt section, the search status
bar mentions [ ] lists, the calendar is two days on).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 23:05:35 +02:00
co-authored by Claude Opus 5.5
parent 4dba794b26
commit 68c87bc3cd
11 changed files with 52 additions and 9 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ DejaVu fonts; `oxipng` (optional) compresses the PNGs losslessly. It takes about
|---|---|
| `catalog.py` | The made-up library: movies (with file details) and series (with seasons). |
| `posters.sh` | Draws a poster per title into `posters/` (gradient, title, year); run by `take.sh` when `posters/` is missing. |
| `server.py` | The fake services on 127.0.0.1: Radarr 19891, Sonarr 19892, SABnzbd 19893. Library, search, releases, queues, history, calendar, posters. |
| `server.py` | The fake services on 127.0.0.1: Radarr 19891, Sonarr 19892, SABnzbd 19893, Trakt 19894 (`take.sh` points arr-tui there with `ARR_TUI_TRAKT_URL`). Library, search, lists, releases, queues, history, calendar, posters. |
| `xshot.sh` | Runs a command in Ghostty under Xvfb and follows a scene script. |
| `scenes/*.txt` | What to press and when to take a picture: `key`, `type`, `sleep`, `shot <name>`. |
+3
View File
@@ -43,3 +43,6 @@ SERIES = [
(5,"Orbital",2025,"Vector","continuing",[(8,2)],"The crew of a commercial space station tries to stay profitable, and alive.",("#000814","#ffc300")),
(6,"Ferryman",2020,"Coastline","ended",[(6,0)],"A ferry captain becomes the only link between an island and the mainland during a long quarantine.",("#2f3e46","#cad2c5")),
]
# A sequel the Trakt "Anticipated" list has, not in the library: id, title, year, colors.
UPCOMING = (9001, "The Long Orbit: Return", 2027, ("#0b132b", "#e0aaff"))
+1
View File
@@ -11,6 +11,7 @@ python3 -c '
import catalog
for m in catalog.MOVIES: print("m%d|%s|%d|%s|%s" % (m[0], m[1], m[2], m[10][0], m[10][1]))
for s in catalog.SERIES: print("s%d|%s|%d|%s|%s" % (s[0], s[1], s[2], s[7][0], s[7][1]))
u = catalog.UPCOMING; print("m%d|%s|%d|%s|%s" % (u[0], u[1], u[2], u[3][0], u[3][1]))
' | while IFS='|' read -r id title year c1 c2; do
magick -size 500x750 "gradient:$c1-$c2" \
\( -size 420x320 -background none -fill white -font "$font" -gravity center caption:"$title" \) \
+6
View File
@@ -0,0 +1,6 @@
# The Trakt lists in the Movies view (empty search): Anticipated, with a title
# that is not in the library yet selected.
sleep 4
key bracketright bracketright
sleep 4
shot discover
+37 -7
View File
@@ -1,4 +1,4 @@
"""Fake Radarr (19891), Sonarr (19892) and SABnzbd (19893) serving the made-up
"""Fake Radarr (19891), Sonarr (19892), SABnzbd (19893) and Trakt (19894) serving the made-up
library in catalog.py, for the README screenshots. Only the API calls arr-tui
makes are answered; posters come from posters/ (see posters.sh)."""
import datetime, json, os, sys, threading, urllib.parse
@@ -8,7 +8,7 @@ import catalog
HERE = os.path.dirname(os.path.abspath(__file__))
TODAY = datetime.date.today()
GB = 1 << 30
RADARR, SONARR, SAB = 19891, 19892, 19893
RADARR, SONARR, SAB, TRAKT = 19891, 19892, 19893, 19894
def day(n): return (TODAY + datetime.timedelta(days=n)).isoformat()
@@ -98,10 +98,10 @@ def radarr(path, q):
return next(x for x in movies() if x["id"] == int(path.rsplit("/", 1)[1]))
if path.endswith("/movie/lookup"):
term = q.get("term", [""])[0].lower()
if term.startswith("tmdb:"):
return [m for m in movies() + [UPCOMING] if m["tmdbId"] == int(term[5:])]
res = [m for m in movies() if term and term in m["title"].lower()]
extra = [{"id": 0, "title": "The Long Orbit: Return", "year": 2027, "overview": "The sequel.", "tmdbId": 9001, "titleSlug": "the-long-orbit-return",
"images": [{"coverType": "poster", "remoteUrl": "http://127.0.0.1:%d/remote/m1.jpg" % RADARR}], "status": "announced", "ratings": {}}]
return res + (extra if "orbit" in term else [])
return res + ([UPCOMING] if "orbit" in term else [])
if path.endswith("/qualityprofile"): return [{"id": 1, "name": "Any"}, {"id": 4, "name": "HD-1080p"}, {"id": 5, "name": "Ultra-HD"}]
if path.endswith("/rootfolder"): return [{"id": 1, "path": "/movies", "freeSpace": 3 << 40, "accessible": True}]
if path.endswith("/release"):
@@ -125,6 +125,11 @@ def radarr(path, q):
def sonarr(path, q):
if path.endswith("/system/status"): return {"appName": "Sonarr", "version": "4.0.15"}
if path.endswith("/series/lookup"):
term = q.get("term", [""])[0].lower()
if term.startswith("tvdb:"):
return [x for x in series() if x["tvdbId"] == int(term[5:])]
return [x for x in series() if term in x["title"].lower()]
if path.endswith("/series"): return series()
if "/series/" in path and path.rsplit("/", 1)[1].isdigit():
return next(x for x in series() if x["id"] == int(path.rsplit("/", 1)[1]))
@@ -148,6 +153,28 @@ def sonarr(path, q):
return out
return []
# A title Trakt lists that is not in the library yet.
UPCOMING = {"id": 0, "title": catalog.UPCOMING[1], "year": catalog.UPCOMING[2], "overview": "Years after the first voyage, a second crew follows the freighter's last signal.",
"tmdbId": 9001, "titleSlug": "the-long-orbit-return", "genres": ["Science Fiction"], "studio": "Halcyon Pictures", "runtime": 0,
"images": [{"coverType": "poster", "remoteUrl": "http://127.0.0.1:%d/remote/m9001.jpg" % RADARR}], "status": "announced", "ratings": {}}
def trakt(path):
def movie(id, title, year): return {"title": title, "year": year, "ids": {"trakt": id, "slug": "x", "tmdb": 1000 + id if id < 9000 else id}}
def show(id): s = next(x for x in catalog.SERIES if x[0] == id); return {"title": s[1], "year": s[2], "ids": {"trakt": id, "slug": "x", "tvdb": 2000 + id}}
by = {m[0]: m for m in catalog.MOVIES}
if path == "/movies/trending":
return [{"watchers": n, "movie": movie(i, by[i][1], by[i][2])} for i, n in [(13, 2841), (1, 2217), (11, 1604), (6, 1186), (3, 902), (8, 655), (16, 412), (12, 371)]]
if path == "/movies/anticipated":
return [{"list_count": 18342, "movie": movie(9001, "The Long Orbit: Return", 2027)}] + \
[{"list_count": n, "movie": movie(i, by[i][1], by[i][2])} for i, n in [(15, 9120), (9, 6204), (4, 3311)]]
if path.startswith("/movies/"):
return [{"watcher_count": n * 10, "movie": movie(i, by[i][1], by[i][2])} for i, n in [(1, 5120), (13, 4870), (3, 3010), (2, 1200)]]
if path == "/shows/trending":
return [{"watchers": n, "show": show(i)} for i, n in [(1, 3022), (5, 2410), (3, 1170), (2, 640)]]
if path.startswith("/shows/"):
return [{"watcher_count": n, "show": show(i)} for i, n in [(3, 51200), (1, 40100), (4, 22000)]]
return []
def sab(q):
mode = q.get("mode", [""])[0]
if mode == "version": return {"version": "4.5.1"}
@@ -184,11 +211,14 @@ def handler(port):
if not os.path.exists(p): self.send_response(404); self.end_headers(); return
data = open(p, "rb").read()
self.send_response(200); self.send_header("Content-Type", "image/jpeg"); self.send_header("Content-Length", str(len(data))); self.end_headers(); self.wfile.write(data); return
out = sab(q) if port == SAB else (radarr(path, q) if port == RADARR else sonarr(path, q))
if port == TRAKT:
out = trakt(path)
else:
out = sab(q) if port == SAB else (radarr(path, q) if port == RADARR else sonarr(path, q))
b = json.dumps(out).encode()
self.send_response(200); self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
return H
for port in (RADARR, SONARR):
for port in (RADARR, SONARR, TRAKT):
threading.Thread(target=ThreadingHTTPServer(("127.0.0.1", port), handler(port)).serve_forever, daemon=True).start()
ThreadingHTTPServer(("127.0.0.1", SAB), handler(SAB)).serve_forever()
+4 -1
View File
@@ -29,7 +29,8 @@ go build -C "$repo" -o "$work/arr-tui" ./cmd/arr-tui
[[ -d $here/posters ]] || { echo "generating posters"; "$here/posters.sh"; }
echo "starting the fake Radarr, Sonarr and SABnzbd"
echo "starting the fake Radarr, Sonarr, SABnzbd and Trakt"
export ARR_TUI_TRAKT_URL=http://127.0.0.1:19894
python3 "$here/server.py" &
server=$!
sleep 1
@@ -42,6 +43,7 @@ for theme in dark light; do
"radarr": {"url": "http://127.0.0.1:19891", "apikey": "0123456789abcdef0123456789abcdef", "default_quality_profile": "Ultra-HD", "default_root_folder": "/movies"},
"sonarr": {"url": "http://127.0.0.1:19892", "apikey": "0123456789abcdef0123456789abcdef", "default_quality_profile": "HD-1080p", "default_root_folder": "/tv"},
"sabnzbd": {"url": "http://127.0.0.1:19893", "apikey": "0123456789abcdef0123456789abcdef"},
"trakt": {"client_id": "0123456789abcdef0123456789abcdef"},
"ui": {"theme": "$theme"}
}
JSON
@@ -54,6 +56,7 @@ shoot() { # scene theme [ghostty args]
XDG_CONFIG_HOME=$demo/$2/.config "$here/xshot.sh" "$here/scenes/$1.txt" "$out" "${3:-}" "$work/arr-tui"
}
shoot views dark
shoot discover dark
shoot episodes dark
shoot config dark
shoot light light "--background=eeeeee --foreground=1c1c1c"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB