Self-hosted service
ellite/scrob avatar
ellite/scrob

Scrob keeps your watch history on your own server

Scrob - Self-hosted media tracking app. It syncs your libraries from Jellyfin, Plex, and Emby, tracks your watch history, ratings, and personal lists - Your own private Letterboxd + Trakt

548 stars43 forksPythonGPL-3.0

At a glance

What is it?
Scrob is a GPL-3.0 self-hosted media tracker that pulls libraries and watched state from Jellyfin, Plex, Emby, Nuvio and Stremio, pushes activity back to Trakt, Simkl and MDBList, and installs as a PWA on port 7330. The synchronization is two-way and the providers disagree about what counts as watched, which is where the interesting behaviour lives.
Who is it for?
Scrob is a good fit for a household or small group that already runs its own media server and wants a shared watch history, ratings and lists without handing viewing data to a hosted service, and that is prepared to run a Postgres-backed container and read a long configuration file.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A tracker that reads from your media server first

Scrob positions itself as open-source, self-hosted media tracking, a personal Letterboxd plus Trakt, and the architecture follows from that. It syncs libraries from Jellyfin, Plex, Emby, Nuvio, ARVIO and Stremio, and it tracks watch history, ratings and personal lists on top of that. The important word in the feature list is multi-source: you can import libraries, watched status and playback progress from more than one server, and the description says it supports multiple server instances and multiple Nuvio profiles. The data it stores is richer than a play log. Every movie and episode watched is recorded, including multiple plays with individual timestamps, and you can log a play manually with a custom date or remove individual entries. Ratings are on a ten-point scale with optional reviews, and seasons can be rated separately from the show as a whole.

Webhooks versus polling, and the manual scrobble

How a play gets recorded is the part that decides whether the tracker feels current. Scrob supports real-time scrobbling through webhooks from Jellyfin, Plex, Emby and Kodi, so your watch state updates as you play with no manual sync, and the table of contents lists separate webhook sections for each of those four. Alongside that, you can start a watching session directly from any movie or episode page and then pause, resume, stop or mark it as watched, with session progress shown live on the home screen. The manual path is not a fallback for a broken webhook; it is the path for content played outside your own server, and the presence of both is what makes Scrob usable for a library that is partly local and partly streamed. Rewatches are handled as their own cycle, tracked separately and without touching the original watch history.

Pushing watched state back out to Trakt and friends

Most trackers are read-only mirrors. Scrob is not: it can push watched activity back to connected providers, and the integration list is long enough to matter. Trakt sync covers watched history, ratings and lists in both directions, with activity pushed back automatically. Simkl sync covers watched history and ratings, and pushes Scrob activity back automatically. MDBList pulls watched history, ratings and watchlist items, and can push changes back when you supply an MDBList API key. Bingebase receives watch history and live scrobbles through your personal webhook URL. Yamtrack and Floppy are supported for import. The Trakt row carries the most significant restriction in the whole README: connecting live requires a Trakt VIP subscription, described as a recent Trakt-side restriction, while everyone else can still import through a Trakt data export with no VIP needed. So the most popular integration is also the one that is closed to most users.

Metadata, episode ordering and the details users notice

The metadata layer comes from TMDB, which supplies posters, backdrops, cast, crew, trailers and collections, and you can optionally use RatingPosterDB artwork with embedded ratings by configuring your own RPDB key. A preferred display language can be set per profile, so titles, overviews and episode names translate where available while the rest of the interface stays as you chose it. The feature that shows the most engineering is episode ordering: you can watch a show in DVD, absolute, alternate or regional order as defined by TheTVDB, or in any TMDB episode group such as story arc, production or digital order, chosen per show from a control on the show page, and the renumbering propagates to every surface where the episodes appear, including the season and episode pages, Next Up, Now Playing, the calendar, history and cards. That is a small feature with a large blast radius, and it is the kind of detail that separates a tracker people keep from one they test.

Running it with Docker Compose

The compose file gives you two services. The database is postgres:16-alpine with a named volume, a healthcheck that runs pg_isready against the scrob user and database every five seconds with ten retries, and a restart policy of unless-stopped. The application service is bellamy/scrob:latest, it depends on the database being healthy, and it publishes port 7330:

yaml
  scrob:
    container_name: scrob
    image: bellamy/scrob:latest
    restart: unless-stopped
    depends_on:
      scrob-db:
        condition: service_healthy
    ports:
      - "7330:7330"

The environment block sets DATABASE_URL to a postgresql+asyncpg URL pointing at the database service, then a SECRET_KEY with a comment telling you to generate one with openssl rand -hex 32. Registrations are disabled by default, with the variable left commented out and a separate cap on how many additional users can register. There is also an optional Nuvio anon key for a self-hosted Nuvio backend and an optional ARVIO key that overrides the embedded public anon key for auth.arvio.tv. The repository also ships an omnibus compose file, an omnibus Dockerfile, a single-container entrypoint, and supervisord configuration files for both variants, so there is a lighter deployment path than the two-service one.

The image build and the single-file entrypoint

The Dockerfile is a two-stage build. The first stage is node:22-alpine, which installs the frontend dependencies with npm ci and runs npm run build. The runtime stage is python:3.12-slim, and it copies only the node binary from the official node:22-slim image rather than installing NodeSource packages, with a comment explaining that only the node binary is needed at runtime and that NodeSource would bring npm and a whole system Python along with it. Supervisor is installed from PyPI rather than apt for the same reason, and pip is uninstalled afterwards since nothing installs packages at runtime. uv is copied in from its own container image, and the backend is installed with uv sync --frozen --no-dev --no-cache from a copy of pyproject.toml and uv.lock, so the dependency set is locked. The entrypoint chowns the data directory, which the Dockerfile creates first because it must exist even when no volume is mounted over it. The result is a single image carrying the backend, the built frontend and a process supervisor.

Where Scrob is the wrong choice

Three cases. The first is a single user who does not want to run a server, because the cost is a Postgres container, a second container, secret management and an upgrade path, and a hosted tracker asks for no infrastructure at all. The second is a household whose history lives in a service outside the supported set, where Scrob can import from Trakt, Simkl and MDBList but the provider list in the README is finite. The third is a locked-down network, since syncing in both directions with Trakt, Simkl and Bingebase means outbound calls to several services, and while the app itself is self-hosted, the two-way sync is not. Against a hosted service such as Trakt itself, the difference in approach is ownership of the data plus the ability to combine several sources into one history, which no single provider can do, at the price of running the server and of the sync restrictions documented above. The GPL-3.0 licence governs the whole project, which matters if you intend to modify and redistribute it.

Editorial conclusion

Scrob is a good fit for a household or small group that already runs its own media server and wants a shared watch history, ratings and lists without handing viewing data to a hosted service, and that is prepared to run a Postgres-backed container and read a long configuration file. It is a poor fit for a single user who does not want to operate a server, for anyone whose watched history lives in a provider Scrob does not talk to, and for a deployment where outbound internet access is restricted. Check three things before you deploy it: that you set SECRET_KEY from a generated random value rather than leaving the placeholder, because the example configuration ships changeme in both the compose file and the environment template, and that registrations are disabled by default, since the compose file leaves ENABLE_REGISTRATIONS commented out; that the sync directions suit you, because Scrob can push watched activity back to connected providers and the last push was on 2026-10-22, so behaviour may shift; and that the provider restrictions are acceptable, since connecting Trakt live requires a Trakt VIP subscription according to the README, and everyone else has to import from a Trakt data export instead.

Frequently asked questions

Which media servers can Scrob sync with?

The README says Scrob imports libraries, watched status and playback progress from Jellyfin, Plex, Emby, Nuvio and Stremio, and it also names ARVIO as a source. It supports multiple server instances and multiple Nuvio profiles, and real-time scrobbling works through webhooks from Jellyfin, Plex, Emby and Kodi.

Does Scrob need a Trakt subscription to work with Trakt?

The README says connecting live requires a Trakt VIP subscription, which it describes as a recent Trakt-side restriction. Everyone else can still import via a Trakt data export, with no VIP needed.

How do I run Scrob with Docker Compose?

The compose file defines a postgres:16-alpine database service with a healthcheck and a volume, and an application service using bellamy/scrob:latest that depends on the database being healthy and publishes port 7330. The environment sets DATABASE_URL, a SECRET_KEY to generate with openssl rand -hex 32, and registration settings.

What are the default security settings for a new Scrob install?

Registrations are disabled by default, with ENABLE_REGISTRATIONS left commented out in the compose file, and there is a separate cap on the number of additional users. Both the compose file and the environment example ship a placeholder SECRET_KEY value, and the example tells you to generate one with openssl rand -hex 32.

Can Scrob track alternate episode orders for a show?

Yes. The README describes choosing per show from an Episode order control on the show page, with DVD, absolute, alternate and regional order from TheTVDB or any TMDB episode group such as story arc, production or digital order. Every surface where the episodes appear renumbers accordingly.

Official sources

  1. ellite/scrob on GitHub
  2. License: GPL-3.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ellite-scrob.svg)](https://hysenlabs.com/projects/ellite-scrob)

Was this useful?