Broke It First

I turned a spare TV into a cinema poster screen

November 1, 2026 · beginner · 1 to 2 hours · $0 to $150 (if you need a mini PC)

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

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:

  1. 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.
  2. 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, time
import requests
from 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/posters

Put your own Radarr address and key in, then:

Terminal window
docker compose up -d
docker compose logs -f

Give 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=true
AutomaticLogin=your-username

Install Chromium (on current Ubuntu it only comes as a snap):

Terminal window
sudo snap install chromium

Stop the screen from blanking or locking:

Terminal window
gsettings set org.gnome.desktop.session idle-delay 0
gsettings set org.gnome.desktop.screensaver lock-enabled false
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing

Open the page full-screen at login. Create ~/.config/autostart/poster-kiosk.desktop:

[Desktop Entry]
Type=Application
Name=Poster Kiosk
Exec=/snap/bin/chromium --ozone-platform=x11 --kiosk --noerrdialogs --disable-infobars --no-first-run "http://YOUR-SERVER:8090/"
X-GNOME-Autostart-enabled=true

Reboot. 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:

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

Everything above stays free. The kit just skips the fiddly parts.