Rotate your own art and photos on a Samsung Frame TV — point it at a folder, pick a shuffle interval, done. A small local web UI lets you browse your collection, filter by folder / tag / color, and push any image to the TV with a click.
No cloud, no account, no subscription. Everything runs on your own machine (a Raspberry Pi is perfect) and talks to the TV directly over your LAN.
Why this exists: the Samsung art API got noticeably flakier on the 2024 ("Pontus M") Frame models — uploads that never confirm, deletes that silently no-op, WebSockets that hang for an hour. FrameShuffle bakes in the workarounds that took a long time to find. See Hard-won lessons at the bottom.
- Shuffle your art folder on a timer (default 30 min, change it live).
- Choose your art — a web grid of thumbnails; click one to show it now.
- Filter by folder, by free-form tags, or by auto-detected color theme.
- Auto-converts HEIC / PNG / DNG to JPEG and resizes/letterboxes to the Frame's native 3840×2160.
- Self-cleaning — deletes the previous image each rotation so the TV never fills up with uploads.
- Robust — every TV operation runs under a watchdog; a hung or unreachable TV can't wedge the service.
- A Samsung Frame TV with the Art API (2021+ models; built/tested against a 2024 QN65LS03D).
- Python 3.9+ on a machine on the same network (Raspberry Pi, NAS, old laptop, etc.).
- A folder of images.
git clone https://github.com/illinigirl/FrameShuffle.git
cd FrameShuffle
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txtThat's it — there's no config file to edit. You set your TV and art folder in the web UI on first run (next section).
Prefer a config file? For a headless/pre-seeded install, copy
config.example.pytoconfig.pyand fill in the values. Anything you later change in the UI (saved tosettings.json) takes precedence.
Anything works. Subfolders automatically become filterable categories:
art/
├── Impressionism/ ← category "Impressionism"
│ ├── Monet - Water Lilies.jpg
│ └── ...
├── Family Photos/ ← category "Family Photos"
└── a-loose-image.jpg ← category "root"
Filenames like Artist - Title (Year).jpg are parsed for nicer captions,
but any filename is fine.
Your art doesn't have to live on the FrameShuffle machine. Anything the OS mounts as a folder works — a NAS share or a USB/external drive. Mount it the normal way, then point the art folder at the mount point:
| Source | Art folder path |
|---|---|
| NAS on a Raspberry Pi | /mnt/nas/Frame TV |
| NAS on a Mac | /Volumes/Public/Frame TV |
| USB drive on a Pi | /media/pi/MYDRIVE/art |
| External drive on a Mac | /Volumes/MyDrive/art |
You don't have to type it: in ⚙ Settings, click Browse… and navigate
to the folder (it surfaces /Volumes, /media, and /mnt so mounted drives
are easy to find). Letting the OS handle the mount means it manages the
network login and reconnection — FrameShuffle just reads the folder.
python3 shuffle.pyThen open the web UI:
http://<machine-ip>:8080
A setup banner will prompt you to open ⚙ Settings, where you:
- Scan for your TV (or type its IP) — the scanner finds Samsung TVs on your network and lists them; click yours to fill in the IP.
- Enter your art folder path (it'll confirm the folder exists).
- Set a rotation interval, then Save.
The first time FrameShuffle connects, the TV may show an "Allow / Deny" prompt for a device named Display — choose Allow (the TV remembers it). Now toggle Shuffle on and your art starts rotating.
Tip: the repo includes a
sample-art/folder — point the art folder there first to confirm uploads and rotation work, then switch to your real collection.
The web UI is the whole control panel:
- Shuffle toggle — start/stop automatic rotation.
- every N min — change the interval on the fly.
- Next › — jump to the next image immediately.
- Art mode / TV mode — put the TV into art mode or back to normal.
- Filters — click folder / tag / color chips to preview a subset in the grid, then Set as shuffle filter to make the rotation use only those.
- Click any thumbnail — show that exact image on the TV right now.
Command-line options:
python3 shuffle.py # rotation loop + web UI (default)
python3 shuffle.py --no-web # loop only
python3 shuffle.py --once # show one image and exit
python3 shuffle.py --interval 20 # set interval to 20 min, then runTo enable the color chips, build a color index once (re-run anytime you add art — it's incremental):
python3 tools/generate_color_themes.py
python3 tools/generate_color_themes.py --stats # see the countsThis writes art_colors.json next to your art. Tags work the same way via
an art_tags.json file (a {"relative/path.jpg": ["tag1", "tag2"]} map) if
you want to maintain one.
A frameshuffle.service template is included for systemd (Raspberry Pi /
home server). It auto-starts on boot and restarts on failure:
# edit the paths/user in frameshuffle.service first
sudo cp frameshuffle.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now frameshuffle
journalctl -u frameshuffle -f # follow the logssystemd is Linux-only. On macOS/Windows just run
python3 shuffle.py(or wrap it withlaunchd/ Task Scheduler).
| File | Role |
|---|---|
frame_tv.py |
The Frame TV art-channel client — connect, upload, delete, select, art-mode. The hard part. |
settings.py |
Source of truth for TV IP / art folder / interval (settings.json) + SSDP TV discovery. |
art_library.py |
Scans your folder; folder/tag/color filtering; JPEG conversion. |
shuffle.py |
The service: owns the TV connection, runs the rotation loop. |
web.py |
The local web UI. |
state.py |
Tiny request/status files shared between the loop and the UI. |
tools/generate_color_themes.py |
Optional color index builder. |
The shuffle loop is the only thing that talks to the TV. The web UI just writes small request files that the loop picks up — so there's never more than one connection to the (connection-sensitive) TV.
If you're building your own Frame automation, these are the firmware quirks
that cost the most time. They're all handled in frame_tv.py.
-
Every upload creates a new
content_id. There is no overwrite and no "change current image" — you upload a fresh copy each time, so you must delete the old one or the TV fills up. FrameShuffle deletes the previous image on every rotation and clears leftovers on startup. -
delete_image_listis silently ignored if you pass a JSON string. The TV acknowledges both forms but only acts on a raw Python list:content_id_list=[{"content_id": cid}] # works content_id_list=json.dumps([...]) # acknowledged, does nothing
-
Delete never sends a confirmation. Waiting for one hangs every time. Treat delete as fire-and-forget.
-
A quiet TV will block your socket forever.
create_connection's timeout only covers the handshake; without an explicitsock.settimeout(...)on the live socket, a.send()/.recv()can hang for an hour. Every op here has a hard socket timeout, and uploads additionally run under a SIGALRM watchdog so the loop can always recover. -
The art channel can wedge itself. Sustained
select_imagefailures put thecom.samsung.art-appchannel into ad2d_service_message:errorstate where it rejects everything. The fix is to tear the connection down and reopen for the next attempt — not to retry inline (inline retries interact badly with the watchdog and cause the long hangs). -
Don't open the art channel just to "listen." On the 2024 model, connecting to
com.samsung.art-appcan wake the TV out of an app (e.g. YouTube) into art mode. Only connect when you actually intend to change what's displayed. (FrameShuffle connects only to upload/select/delete.)
MIT — see LICENSE.
