Self-hosted · sits on top of your own Jellyfin

Your library,
put back in order.

jellystructure watches a Jellyfin library and does the part nobody enjoys — matching the right title, the right poster, the right episode number, the right subtitle — so what you press play on actually looks right.

What it actually does

The tedious half of running a media library, automated.

Jellyfin still does all the streaming — jellystructure just keeps what's behind it accurate, and stays out of the way once it is.

Matching that locks in place

Finds the right movie or show, pulls artwork and cast & crew, and remembers any correction you make — a re-scan never quietly overwrites it.

Episode numbers that hold up

Works out season and episode numbers even when the filename doesn't say, and repairs the ones that drift out of sync over time.

Skip intro, skip credits

Detects the intro and end-credits span in every episode of a season and publishes it back to Jellyfin, so any client can skip it.

Files the way people expect

Multi-episode files, several audio tracks, several subtitle languages — shown the way a person would describe them, not the way the filesystem stores them.

Plugs into the rest of the stack

Talks to Radarr, Sonarr, Bazarr and Seerr, so requests, downloads and subtitles land in the same well-kept library automatically.

A proper admin panel

A dashboard for the whole library — search, filters, drift warnings, and a raw editing surface for the rare case automation gets it wrong.

How the pieces fit

Three layers, each doing one job.

Layer 1 · streaming

Jellyfin

Stores and streams the media. Untouched — jellystructure never replaces it, only tidies what it shows.

→
Layer 2 · this project

jellystructure

Continuously scans the library, fixes metadata and artwork, and detects segments — running quietly in the background.

→
Layer 3 · what you watch on

Ravilo

The TV and phone app your household actually opens.

Run it

Three containers, one file.

The whole stack as a docker-compose.yml: Jellyfin, jellystructure and the Ravilo web app. Both of our images are on GitHub; 1.52 is the latest release.

docker-compose.yml
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    user: "1000:1000"
    ports:
      - "8096:8096"
    volumes:
      - ./jellyfin:/config
      - ./jellyfin-cache:/cache
      - /mnt/media:/media
    restart: unless-stopped

  jellystructure:
    image: ghcr.io/jeggy/jellystructure:1.52
    user: "1000:1000"
    ports:
      - "9505:9505"
    volumes:
      - ./jellystructure:/config
      - /mnt/media:/media
    environment:
      # where the Ravilo web app is served from, so its browser calls are allowed
      CORS_ALLOWED_ORIGINS: "localhost:8080"
    restart: unless-stopped

  ravilo-web:
    image: ghcr.io/jeggy/ravilo-web:1.52
    ports:
      - "8080:8080"
    environment:
      # the jellystructure address as a browser reaches it
      DEFAULT_SERVER_URL: "http://localhost:9505"
      # plain http on a home network; drop this once it sits behind https
      CSP_ALLOW_HTTP_CONNECT: "1"
    restart: unless-stopped
  1. Media in one place. Mount your files at the same path in both containers (here /mnt/media → /media) so the two agree on every filename, and make sure the 1000:1000 user owns them — jellystructure writes artwork and NFO files next to your media.
  2. Set up Jellyfin at http://localhost:8096: create the admin account and add the /media library.
  3. Open jellystructure at http://localhost:9505. The first screen asks for the Jellyfin address — use http://jellyfin:8096, the container name — a Jellyfin API token and a TMDB API key. Sign in with the Jellyfin admin account; there is no separate login.
  4. Watch at http://localhost:8080, or install Ravilo on the TV and point it at the same jellystructure address.

Swap localhost for the machine's name or address when you open it from another device, in all three places. Needs Jellyfin 12 or later.