I turned a spare TV into a cinema poster screen
Every proper cinema has a lit poster case by the door. I wanted one outside my home theater, showing posters from my own movie collection, and switching to the poster of whatever’s actually playing. Commercial “digital movie poster” boxes exist, and they cost hundreds of dollars. I had a spare TV and a $150 mini PC, so I built my own.
It took a lot more breaking than I expected. This is the whole story, including every place it fell over, and enough detail that you can build your own for free.
What you’ll end up with
- A TV (mounted sideways, if you like, so posters fill the screen) showing a random poster from your collection, changing every 45 seconds.
- When a movie starts, it switches to that movie’s poster, with a NOW PLAYING banner across the top.
- A COMING SOON banner along the bottom, cycling through movies you’re waiting on.
- It keeps running when your media server or player is switched off, because the posters are stored locally.
What you need
| Thing | What I used | Rough cost |
|---|---|---|
| A TV or monitor | A 43-inch TV I wasn’t using | $0 (spare) |
| A small PC for the TV | A mini PC with an Intel N100 chip, Ubuntu Desktop | ~$150, or any old laptop or PC |
| A poster source | Radarr (it already had posters for every movie) | $0 |
| Somewhere to run Docker | My always-on homelab server (the mini PC itself works too) | $0 |
| Optional: Home Assistant | For the NOW PLAYING feature | $0 |
You should be comfortable copying commands into a terminal. If Docker or SSH are new to you, read Docker basics and SSH and keys first.
How it works
There are two parts, and keeping them separate is what makes this reliable:
- A small web app (running in Docker on any machine) that downloads posters from Radarr into a local folder, picks one at random every 45 seconds, and serves a web page showing it full-screen.
- A kiosk: the mini PC behind the TV logs in by itself, opens that web page in a full-screen browser, and never sleeps.
The TV’s PC doesn’t need to know anything about Radarr, Home Assistant or your movies. It just shows a web page. That makes the kiosk part trivially replaceable, and it means you can open the same page on your laptop to check it.
Steps
This is the free, do-it-yourself version: random posters from Radarr on a kiosk. The kit at the end adds the extras (NOW PLAYING, COMING SOON, Plex and Jellyfin support, an installer and a printable guide).
1. Get your Radarr API key
In Radarr, go to Settings > General > Security and copy the API Key. Radarr already stores a poster for every movie; we’ll ask it for them.
2. A tiny poster server
On your Docker machine, make a folder with two files. First, posters.py:
import os, random, threading, timeimport requestsfrom flask import Flask, Response
RADARR = os.environ["RADARR_URL"].rstrip("/")KEY = os.environ["RADARR_API_KEY"]CACHE = "/data/posters"app = Flask(__name__)current = {"img": None}
def sync(): while True: try: movies = requests.get(f"{RADARR}/api/v3/movie", headers={"X-Api-Key": KEY}, timeout=20).json() for m in movies: if not m.get("hasFile"): continue poster = next((i for i in m["images"] if i["coverType"] == "poster"), None) if poster and poster.get("url"): r = requests.get(RADARR + poster["url"], timeout=20) if r.ok and r.content[:3] == b"\xff\xd8\xff": open(f"{CACHE}/{m['id']}.jpg", "wb").write(r.content) time.sleep(7 * 24 * 3600) # refresh weekly except Exception as e: print("sync failed, retrying:", e) time.sleep(30) # Radarr down? try again soon
def rotate(): while True: files = os.listdir(CACHE) if files: current["img"] = open(f"{CACHE}/{random.choice(files)}", "rb").read() time.sleep(45)
@app.route("/poster.jpg")def poster(): if not current["img"]: return Response("no posters yet", status=503) return Response(current["img"], mimetype="image/jpeg", headers={"Cache-Control": "no-store"})
@app.route("/")def page(): return """<body style="margin:0;background:#000"><img id="p" src="/poster.jpg" style="width:100vw;height:100vh;object-fit:contain"><script>setInterval(()=>{p.src="/poster.jpg?"+Date.now()},5000)</script></body>"""
os.makedirs(CACHE, exist_ok=True)threading.Thread(target=sync, daemon=True).start()threading.Thread(target=rotate, daemon=True).start()app.run(host="0.0.0.0", port=8090)Then docker-compose.yml next to it:
services: posters: image: python:3.12-slim restart: unless-stopped working_dir: /app command: sh -c "pip install flask requests && python posters.py" environment: RADARR_URL: http://192.0.2.10:7878 RADARR_API_KEY: paste-your-key-here ports: - "8090:8090" volumes: - ./posters.py:/app/posters.py:ro - ./posters:/data/postersPut your own Radarr address and key in, then:
docker compose up -ddocker compose logs -fGive it a few minutes on a big library, then open http://YOUR-SERVER:8090/ in a browser. You should see a poster, changing every 45 seconds.
Notice two things in that code that came straight out of my own failures (more on those below): the sync loop catches errors and retries instead of dying, and rotation reads from a local folder, so it never needs Radarr to be up.
3. Turn the mini PC into a kiosk
Install Ubuntu Desktop on the PC behind the TV, then:
Log in automatically. Edit /etc/gdm3/custom.conf and, under [daemon], set:
AutomaticLoginEnable=trueAutomaticLogin=your-usernameInstall Chromium (on current Ubuntu it only comes as a snap):
sudo snap install chromiumStop the screen from blanking or locking:
gsettings set org.gnome.desktop.session idle-delay 0gsettings set org.gnome.desktop.screensaver lock-enabled falsegsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothingOpen the page full-screen at login. Create ~/.config/autostart/poster-kiosk.desktop:
[Desktop Entry]Type=ApplicationName=Poster KioskExec=/snap/bin/chromium --ozone-platform=x11 --kiosk --noerrdialogs --disable-infobars --no-first-run "http://YOUR-SERVER:8090/"X-GNOME-Autostart-enabled=trueReboot. The PC should log in by itself and fill the screen with a poster. The first time, Chromium may show a one-time welcome screen; click through it once with a keyboard and mouse.
4. Optional: turn the TV sideways
Posters are tall, so a portrait TV looks far better. Physically rotating the TV is the easy part. Telling the PC about it is not, because on a modern Ubuntu desktop the usual tool (xrandr) doesn’t actually rotate anything. The kit includes a script that does it properly through GNOME’s own display settings and re-applies it at every login. Doing it by hand: Settings > Displays > Orientation, and confirm the dialog quickly before it reverts.
Where I broke it
This is the part I’d have wanted to read before I started. In fairness, a lot of the digging below was done by Claude, the AI assistant I build with. I’ll say where.
It ran for three days without showing a single poster
The first version (which Claude and I built together) pulled posters from Kodi, the player I was using at the time. The container started, the web page loaded, everything looked healthy, and the screen never showed a single poster for three days.
Nobody noticed until Claude stumbled on it while setting up something else that wanted the same network port. Its logs showed the cause: the app was asking Kodi for a movie’s thumbnail, which isn’t an image address at all. It’s Kodi’s internal reference to its own thumbnail cache (it starts with image://), so every download attempt failed. The app caught each error, logged it quietly and carried on, which is why nothing looked wrong. The fix was to ask for the movie’s real artwork and convert Kodi’s reference into an address its web server could actually serve.
The lesson: “the container is running” proves nothing. Check that the thing does its job. Look at the actual output.
The poster got stuck for 24 hours
Next problem, and this one I found myself: after a movie ended, the screen kept showing its poster, sometimes for a whole day. Claude traced it: when the player was switched off, the app got a connection error while asking “what’s playing?”, and that error skipped the code that switches back to random posters. “The player is off” and “nothing’s playing” needed to mean the same thing, and in the code they didn’t.
Claude spotted that while we tested a fix, and found a worse bug: if the server restarted while the player was off, the whole app crashed on startup and kept crashing. Then, once the player came back, it could still take 30 minutes to recover, because the first retry was scheduled half an hour out.
That’s why the free version above treats every sync failure as “keep what you have, try again in 30 seconds”, and why rotation reads from a local folder. The second redesign moved the poster source to Radarr entirely, so the screen doesn’t depend on the player at all.
The browser ignored where I put it
This one happened on another kiosk of mine, one PC driving two monitors, but it’s the reason for a flag in every kiosk I build. I told Chromium exactly where to open each window. It ignored me. Modern Ubuntu runs a display system called Wayland, which deliberately doesn’t let apps position their own windows. The fix is the --ozone-platform=x11 flag in the autostart line, which makes Chromium use the older X11 system where positioning works. It’s harmless on a single screen, so I always include it.
The screen came up upside down
For the sideways TV, Wayland’s own documentation said the rotation value I needed was 1. The screen came up upside down. The value that worked was 3. My guess is that this TV reports its native orientation differently from what the documentation assumes. Whatever the reason, try one value, look at the screen, and switch if it’s wrong.
Then GNOME’s “keep these display settings?” dialog reverted the rotation after a few seconds, because nobody was there to click “Keep”. The fix was to apply the rotation in GNOME’s “temporary” mode (no dialog) every time the kiosk logs in.
The text was tiny
I sized the NOW PLAYING text while sitting at my laptop. On a TV across a room, it was unreadable. I ended up with text more than twice the size that looked right on my laptop. Start bigger than feels sensible.
On my office kiosk, a 4K TV, the same thing happened to everything, because the browser renders at full 4K. Adding --force-device-scale-factor=2 to the Chromium line doubles everything.
The kiosk didn’t refresh the page layout
The poster image updates on its own every few seconds, but when I changed the page itself (adding the banners), the TV kept showing the old version. A kiosk browser loads the page once and never reloads it. After any change to the page, reboot the kiosk (or restart Chromium).
Want the full version?
The free build above gets you random posters. The kit is my polished version, with the fixes for the code problems above built in, and the kiosk gotchas handled by its installer:
- NOW PLAYING: watches one or more Home Assistant media players and switches to the right poster, matched by movie ID, then title, then the player’s own artwork.
- COMING SOON banner from Radarr’s calendar.
- Radarr, Plex or Jellyfin as the poster source.
- A kiosk installer script that does all of step 3, plus screen rotation and 4K scaling, with a dry-run mode that shows what it will change first.
- A friendly placeholder instead of a blank screen, settings errors explained in plain English, and safe weekly re-syncs.
- A printable PDF guide and checklist.
Related
- Docker basics for homelabbers
- SSH and SSH keys, without the fear
- What a minimal homelab actually needs
Want it ready to go? Movie Poster Display Kit
Turn a spare TV into a cinema-style poster screen that shows what is playing.
$15
Coming soon