All apps · 0 apps
Obzorarr
Docker app from grtgbln's Repository
Overview
Readme
View on GitHub
Obzorarr
Year in Review for Plex Media Server
What is Obzorarr?
Obzorarr is a "Wrapped for Plex" application that syncs viewing history from your Plex Media Server and generates yearly statistics with an animated slideshow presentation - similar to Spotify Wrapped. It doesn't require Tautulli; it only relies on the Plex API.
Features
- Yearly Statistics — Total watch time, top movies, shows, and genres
- 19 Slide Types — From watch streaks and binge sessions to decade breakdowns and series completion
- Two Ways to Watch — An animated story-mode slideshow, or a scrollable single-page recap
- Watch Patterns — Monthly, hourly, and weekday distribution charts
- Percentile Rankings — See how you compare to other users on your server
- Server Wrapped — A server-wide recap with a top-viewers leaderboard
- Privacy Modes — Real, hybrid, or fully anonymous names, with one-click privacy presets
- Five Themes — UI and Wrapped themes are chosen independently
- Slide Editor — Reorder, enable, or disable slides and add your own custom ones
- Plex OAuth — Secure authentication with your Plex account
- Automatic Sync — Scheduled background sync of viewing history, with live progress
- Reverse-Proxy Diagnostic — Compares what your browser sees, what the proxy forwards, and what Obzorarr uses
- AI Fun Facts — Optional AI-written fun facts, with the built-in templates as the fallback
Issues & Support
Found a bug or have a feature request? Please submit issues and feature requests to the obzorarr-docker repository rather than this repository. This ensures your report reaches the maintainers monitoring issue tracking across the project.
Screenshots
Every screenshot below is captured from a running instance. Usernames are rendered by Obzorarr's own anonymisation mode, and server addresses are demo values.
Onboarding
First run walks through seven steps: Claim → Security → Reverse proxy → Connect → Sync → Configure → Done.
| Claim setup | CSRF origin |
|---|---|
![]() |
![]() |
| Reverse-proxy trust | Proxy diagnostic |
|---|---|
![]() |
![]() |
| Server picker | Connection choice |
|---|---|
![]() |
![]() |
| Connected | Sync in progress |
|---|---|
![]() |
![]() |
| Sync complete | Choose slides |
|---|---|
![]() |
![]() |
| Pick a theme | Setup complete |
|---|---|
![]() |
![]() |
Admin
| Dashboard | Wrapped overview | Slide editor |
|---|---|---|
![]() |
![]() |
![]() |
| Sync (idle) | Sync running | Users |
|---|---|---|
![]() |
![]() |
![]() |
| Live logs | Settings | Connections |
|---|---|---|
![]() |
![]() |
![]() |
| Appearance | Privacy | Security |
|---|---|---|
![]() |
![]() |
![]() |
| Data | System |
|---|---|
![]() |
![]() |
Your Wrapped
Story mode plays the slides one at a time; scroll mode puts the whole recap on a single page.
| Total time | Top movies | Top shows |
|---|---|---|
![]() |
![]() |
![]() |
| Genres | Viewing patterns | Weekday patterns |
|---|---|---|
![]() |
![]() |
![]() |
| Movies vs shows | By decade | Series completion |
|---|---|---|
![]() |
![]() |
![]() |
| Rewatches | Marathon day | Longest streak |
|---|---|---|
![]() |
![]() |
![]() |
| Year comparison | Percentile | Binge sessions |
|---|---|---|
![]() |
![]() |
![]() |
| First and last | Fun fact | Summary |
|---|---|---|
![]() |
![]() |
![]() |
Scroll mode — the same recap as one continuous page:
| Scroll mode (top) | Scroll mode (further down) |
|---|---|
![]() |
![]() |
On a phone — the Wrapped experience is built portrait-first:
Themes
Five presets ship with Obzorarr. The admin UI theme and the Wrapped theme are set independently under Admin → Settings → Appearance, so the panel you work in and the recap your users see do not have to match.
UI themes — the admin dashboard in each preset:
Wrapped themes — the same slide in each preset:
Sharing & privacy
Names shown below come from Obzorarr's anonymous privacy mode, which renders every user as
User #1, User #2, and so on. Real and hybrid (you see your own name, everyone else is
anonymised) are the other options — see Admin → Settings → Privacy.
| Share modal | Public Wrapped |
|---|---|
![]() |
![]() |
| Server Wrapped | Top viewers |
|---|---|
![]() |
![]() |
For your users
| Landing page | Dashboard | Sharing preferences |
|---|---|---|
![]() |
![]() |
![]() |
Tech Stack
| Component | Technology |
|---|---|
| Runtime | Bun |
| Framework | SvelteKit + Svelte 5 |
| Database | SQLite (Drizzle ORM) |
| Styling | UnoCSS + shadcn-svelte |
| Animation | GSAP + Motion |
Quick Start
Docker (Recommended) — Image Repo
services:
obzorarr:
container_name: obzorarr
image: ghcr.io/engels74/obzorarr-docker
ports:
- 3000:3000
environment:
- PUID=1000
- PGID=1000
- UMASK=002
- TZ=Etc/UTC
# Optional: lock Plex connection at the env layer. You can also leave
# these unset and configure the server from the admin UI after onboarding.
# - PLEX_SERVER_URL=http://plex-url-here:32400
# - PLEX_TOKEN=your-plex-token-here
volumes:
- /<host_folder_config>:/config
Replace /<host_folder_config> with your desired config path. Access the web UI at http://localhost:3000 to complete setup.
From Source
git clone https://github.com/engels74/obzorarr.git
cd obzorarr
cp .env.example .env
bun install
bun run dev
Note on
.envin local dev.bun run devdoes not auto-load.env, so anyPLEX_SERVER_URL/OPENAI_*values you put there are ignored — local dev configures the server through onboarding and the admin UI (values stored in the SQLite DB). Environment-variable precedence (and the "Locked by environment variable" UI) applies to Docker/production, where the container passes the vars into the process. To exercise env-precedence locally (e.g. to see an env-locked field render itsENVbadge), runbun run dev:env, which loads.envvia--env-file.
When PLEX_SERVER_URL and PLEX_TOKEN come from the environment, onboarding and
Admin → Settings → Connections show them as read-only with an ENV badge — the value is owned
by your container config, not the database:
First-Time Setup
The first time you open the web UI, Obzorarr runs a short onboarding wizard: Claim → Security → Reverse proxy → Connect → Sync → Configure.
The claim token
So nobody can grab your fresh install before you do, the first step asks for a one-time bootstrap token. Obzorarr prints it to the server console — on a fresh install it is never shown in the browser:
Obzorarr initial setup requires a bootstrap claim.
Setup URL: http://localhost:3000/onboarding/claim
Bootstrap token: xxxx-xxxx-xxxx
With Docker, read it from the container logs:
docker logs obzorarr
The token expires after 15 minutes and only one browser can hold the claim at a time. If it lapses, restart Obzorarr to print a new one.
The remaining steps connect your Plex server (or confirm the values you set via PLEX_SERVER_URL /
PLEX_TOKEN), run the first history sync, and let you choose which slides users see. Anything set
here can be changed later under Admin → Settings.
Starting over
Admin → Settings → Data has a Danger zone with Reset instance, which deletes everything Obzorarr has stored and drops you back at the claim screen, signed out. Before wiping, it shows you a fresh claim token to paste on the next screen — that one lasts 60 minutes, since you have to sign in to Plex, reconfigure, and sync again. It is also printed to the console as usual, so losing the tab is recoverable.
Your watch statistics come back: they re-sync from Plex. Everything else does not. That covers all
settings, every per-user share setting, and every share link you have already handed out stops
working, along with any manual curation and the log history. Anything configured through
environment variables (Plex, OpenAI, ORIGIN, TRUST_PROXY, TZ) is not in the database, so it
survives and the new setup arrives partly pre-filled. Obzorarr refuses to reset while a sync is
running.
Scheduled Syncs and Time Zones
Admin → Sync holds the automatic sync schedule as a cron expression. The schedule survives restarts: Obzorarr stores the expression and whether you left the scheduler running, paused, or stopped, and rebuilds the job on the next boot.
Cron expressions are interpreted in the configured timezone, which also drives the nightly log retention cleanup. Obzorarr resolves it in this order:
- the
TZenvironment variable, when it names a zone the runtime knows (TZ=Europe/Copenhagen); - the timezone saved under Admin → Settings → System;
UTC.
As with every other environment-backed setting, TZ wins: the field renders read-only with an ENV
badge, and a database value it shadows is dropped at startup. A TZ the runtime cannot resolve is
ignored rather than applied, so a typo leaves the admin field editable instead of scheduling syncs
in an unknown zone. Fixed offsets such as +02:00 are rejected for the same reason a DST-aware zone
is wanted here: 0 0 * * * should mean local midnight all year.
Running Behind a Reverse Proxy
Obzorarr needs to know the address your browser uses, not the internal one it listens on. Otherwise login redirects, share links, and CSRF checks get built from the wrong hostname.
Set ORIGIN to your public URL, including the port if it isn't 80 or 443:
ORIGIN=https://obzorarr.example.com
That covers most setups. TRUST_PROXY is a separate, optional switch: when enabled, Obzorarr takes
the hostname and protocol from the X-Forwarded-Host and X-Forwarded-Proto headers your proxy
sends instead. Only turn it on when both of these are true:
- Obzorarr can only be reached through the proxy — nothing can hit it directly.
- Your proxy sets both headers itself, overwriting whatever a visitor sends.
If either is false, a visitor can forge those headers and make Obzorarr build links pointing at a
domain they control. When in doubt, leave TRUST_PROXY off and rely on ORIGIN.
Onboarding and Admin → Settings → Security include a diagnostic that compares what your browser
sees, what the proxy forwards, and what Obzorarr actually uses, with hints for Caddy, Nginx, Nginx
Proxy Manager, and Apache. Changing either variable through the environment requires a restart.
(Client-IP detection is configured separately, via the Bun adapter's ADDRESS_HEADER and
XFF_DEPTH.)
| Reverse-proxy step | Technical evidence |
|---|---|
![]() |
![]() |
How Plex Users Are Matched
Plex watch history records a server-local account ID rather than a global Plex identity, so on every sync Obzorarr rebuilds the mapping between those local IDs and real Plex users by comparing your server's account list with the users you've shared the server with. In practice:
- You, the server owner, are always local account
1. Never edit account IDs by hand. - A user only gets a Wrapped once their share is confirmed. If the check comes back incomplete (Plex unreachable, partial response), Obzorarr keeps the previous mapping instead of guessing.
- Un-sharing removes access — that user's public Wrapped link stops resolving. Re-share and run a sync to restore it.
- Mappings go stale after 24 hours and are re-proved by the next sync. They also reset whenever the Plex URL or token changes — back up your database before changing either, then restart and run a normal sync.
Public Wrapped links deliberately return the same "not found" response for an unknown user, a private profile, and a stale mapping, so the page can't be used to discover who has an account on your server.
Whether real usernames appear at all is a separate setting. Admin → Settings → Privacy offers
five presets — from Maximum Privacy (members-only, anonymous names) to Public Showcase (public
recap, real names) — plus a Custom card that lights up once you change anything underneath, and a
Names in stats control with Real, Anonymous (User #1, User #2, …), and Hybrid (you
see your own name, everyone else is anonymised).
License
This project is licensed under the GNU Affero General Public License v3.0.
Media gallery
1 / 13Install Obzorarr on Unraid in a few clicks.
Find Obzorarr in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Requirements
Requires a separate Plex instance.
Related apps
Explore more like this
Explore allDetails
ghcr.io/engels74/obzorarr-docker:latestRuntime arguments
- Web UI
http://[IP]:[PORT:3000]/- Network
bridge- Privileged
- false
Template configuration
Container Port: 3000
- Target
- 3000
- Default
- 3000
- Value
- 3000
URL of your Plex Server (e.g., http://localhost:32400)
- Target
- PLEX_SERVER_URL
Plex token
- Target
- PLEX_TOKEN
Path to the config data
- Target
- /config
- Default
- /mnt/user/appdata/obzorarr/config
- Value
- /mnt/user/appdata/obzorarr/config
- Default
- 1000
- Value
- 1000
- Default
- 1000
- Value
- 1000
- Default
- 002
- Value
- 002


















































