Skip to content

About

Rotate desktop wallpaper from a self-hosted Immich library, with a tray icon for status and controls

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

immich-wallpaper logo

immich-wallpaper

Rotate your desktop wallpaper using photos pulled live from your own Immich library — filtered by album or person, changed on whatever interval you like, with a system tray icon for status and quick controls.

Two portrait photos are automatically paired side-by-side into a single composite when your screen resolution can be detected; landscape/square photos are shown singly.

Only ever a handful of images sit on disk at once — the app asks Immich's API for a random photo on each rotation and keeps a small rolling window (your choice how many) rather than syncing your whole library locally.

Features

  • Settings window (settings_window.py) — a native GTK window for entering your Immich server URL/API key, picking albums and/or people to pull from, and setting the rotation interval and how many images to keep on disk. It runs as its own small process with no server and no open port, and the API key sits in a masked field. On platforms where GTK isn't practical to install (Windows, macOS) the same settings are offered as a local web page instead (config_ui.py); it isn't installed or started on Linux.
  • Rotator (rotate.py) — does the actual work: picks a random photo (or two portraits, paired), downloads it, sets it as the desktop wallpaper, and keeps a bounded back/forward history.
  • Tray icon (tray_app.py) — shows live status (active / paused / error), lets you step back/forward through recently-shown photos, open the current photo in Immich's web viewer, save a copy elsewhere, refresh on demand, pause/resume, and jump to settings.
  • Desktop support: KDE Plasma (via D-Bus scripting — no QtWebEngine involved) and XFCE (via xfconf-query/xrandr). Desktop is auto-detected at runtime.
  • Multiple screens (Settings → Screens): with more than one monitor you can show the same photo on every screen (each drawn at that screen's own size), a different photo on each, or one picture across all screens (photos side by side filling the whole width, never cropped). You can also change only some screens and leave the others as you set them, and allow more than two photos on a very wide screen. Supported on KDE Plasma and XFCE, which set wallpapers per screen; see the notes under Known issues.
  • On-image overlays (optional, off by default) — inspired by Immich Kiosk: a discreet caption per photo (people names, location, date taken, stacked top to bottom) and/or today's date in the corner. Baked into the image at each rotation rather than rendered live, since this is a static wallpaper, not a web page. Margins automatically clear any taskbar/panel the desktop reports via the _NET_WORKAREA X11 property (works through XWayland on Wayland KDE sessions too), on top of a base 0.5" edge margin computed from the display's real DPI.
  • No syncing, no local mirror of your library — everything is fetched on-demand from Immich's REST API.

Installing

Debian / Ubuntu

sudo apt install ./immich-wallpaper_<version>_all.deb

(or build it yourself — see packaging/debian/build.sh)

This pulls in python3-pystray, python3-pil, python3-gi, gir1.2-ayatanaappindicator3-0.1, and x11-xserver-utils (for xrandr, needed on XFCE) automatically, installs the app under /usr/share/immich-wallpaper/, and registers /etc/xdg/autostart so the tray icon starts on login for any user.

Arch / Manjaro

cd packaging/arch
makepkg -si

All dependencies (python-pystray and xorg-xrandr, needed on XFCE, included) are in the official extra repo, so plain makepkg -si resolves everything — no AUR helper needed. Verified building/installing on Manjaro.

Manual / from source

Needs Python 3.8+, Pillow, pystray, PyGObject with GTK 3, an AppIndicator provider, and xrandr (on every desktop but KDE). On Debian/Ubuntu:

sudo apt install python3-pystray python3-pil python3-gi gir1.2-gtk-3.0 gir1.2-ayatanaappindicator3-0.1 x11-xserver-utils
python3 tray_app.py

The tray app drives its own periodic rotation internally (no cron/systemd timer needed) — as long as it's running, it self-paces against the interval you set in the settings window.

Setup

  1. Run the tray icon and choose Settings… from its menu (or run immich-wallpaper-config, or python3 settings_window.py).
  2. Enter your Immich server URL and an API key. Create the key under Account Settings → API Keys in Immich, and grant only: asset.read, asset.download, album.read, person.read — no write/delete/admin scopes needed. (If your Immich version doesn't offer granular scopes, an all-permissions key also works.)
  3. Test the connection, then load and pick albums and/or people to filter by (leave both empty to pull from your whole library).
  4. Set the rotation interval and how many images to keep on disk, save.
  5. From the tray icon menu: Refresh now to pull the first photo, or just wait for the interval to elapse.

How it decides portrait pairing

On each rotation, a batch of random candidates (filtered by your album/person selection) is fetched with EXIF data. If the first pick is portrait-oriented and the screen resolution can be detected, a second portrait from the same batch is paired with it into one composite image sized exactly to your screen (via KDE's screenGeometry() scripting API, or xrandr on XFCE). Otherwise the photo is shown singly, letterboxed to your screen size (whole photo, black bars, never cropped) so it looks the same whatever fill mode the desktop applies by default. Only when the screen size can't be detected, or a file can't be decoded (e.g. HEIC without a Pillow plugin), is the original file used as-is and the desktop's own fill mode applies. "Save a copy" saves the wallpaper as displayed, bars included.

Known issues

  • Multiple screens are new and lightly tested. They were built and tested against simulated monitors, and on KDE Plasma the per-screen setter was checked on a real session with one monitor. Real multi-screen behaviour on KDE, and the XFCE setter (checked against a recording fake only), still need confirming on real hardware, so reports are welcome. With one screen nothing changes.

  • Spanned pictures ignore each monitor's real vertical position. The screens are laid side by side as one strip, with shorter screens centred, so a photo across two screens of different heights may not line up perfectly at the join.

  • Overlay margins with several screens. Photo info and the date use the base margin only, not taskbar-aware margins, when more than one screen is in use. The date appears on the primary screen only.

  • Screens are identified by connector name (for example HDMI-A-1), so a selection survives unplugging and replugging, but renaming a connector (for instance by moving a cable to a different port) means selecting it again.

  • libayatana-appindicator is deprecated (please use libayatana-appindicator-glib in newly written code) — you may see this printed when starting the tray icon by hand. It's emitted by the system's libayatana-appindicator C library itself, the moment pystray loads its AyatanaAppIndicator3 GObject-Introspection binding — not by anything in this project. pystray doesn't yet support the newer libayatana-appindicator-glib, which also has no widely-packaged Python/GI binding yet, so there's nothing to switch to. Harmless; the tray icon works normally.

Security

The app holds an API key for your Immich server and downloads files from it, so it is deliberately careful:

  • No network server on Linux. The settings screen is a native window, not a web page, so there is no local port for a website or another user to reach.
  • Private storage. The config (which holds the API key), the state, the cached photos and the failure log are readable by you only (mode 0600, directories 0700).
  • A strict Immich client. Server URLs must be plain http(s)://; a redirect is followed only within the same host, so a server or proxy can't bounce the API key elsewhere; replies are size-limited; ids put in URLs are encoded as single path segments.
  • Untrusted files. Photos come from a remote server, so decoding refuses "decompression bombs", any decoder failure falls back to the original file, and the file name's extension is only trusted if it is short and alphanumeric.
  • The web settings page (other platforms only) requires a per-run access token, checks the Host and Origin headers, accepts only JSON posts, never sends the API key to the browser, and serves a strict Content-Security-Policy.

Security linting (ruff's bandit rules) is part of the standard checks.

Development

python3 -m unittest discover -s tests -t .   # the test suite (~13 s)
ruff check .                                 # style, docstrings, security
mypy *.py                                    # types

The tests run against a throwaway HOME, so they never touch your real config, state or wallpaper, and the GTK tests skip themselves on a machine without a display. To add support for another desktop, write its screen-size and set-wallpaper functions and register a DesktopBackend in desktops.py.

License

MIT — see LICENSE.

The Immich flower logo used in the tray icon is Immich's own trademark, used here only to indicate integration with their project — this is an independent, unofficial companion tool and isn't affiliated with or endorsed by the Immich project.

About

Rotate desktop wallpaper from a self-hosted Immich library, with a tray icon for status and controls

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages