Self-hosted, Docker-compose-deployable sync hub for watch history and ratings across multiple services. This project exists because I use multiple services and want to keep my watch history in sync.
- Multi-user auth with JWT cookies and optional registration.
- Manual watch history (add/update/delete) with optional downstream deletion.
- Ratings support synced where supported.
- Imports from Trakt, SIMKL, Letterboxd, and Stremio (quick import + import all).
- Outbox-based delivery with retries and per-user rate limiting.
- Metadata lookup and enrichment with TMDB, TVDB, IMDb, TVMaze, Kitsu, MyAnimeList.
- Minimal web UI (static HTML + JS).
- Copy the env example:
cp .env.example .env - Edit
.envwith your credentials and secrets. - Start:
docker compose up --build - Open
http://localhost:8000.
By default, docker-compose.override.yml is loaded and builds local images. To pull
the published images instead, run:
docker compose -f docker-compose.yml up --pull=always
All defaults below are from .env.example.
POSTGRES_DB(defaultlibrarysync): database name for the Postgres container.POSTGRES_USER(defaultlibrarysync): database user for the Postgres container.POSTGRES_PASSWORD(defaultlibrarysync): database password for the Postgres container.DATABASE_URL(defaultpostgresql+psycopg://librarysync:librarysync@db:5432/librarysync): SQLAlchemy connection string used by API/worker.
LIBRARYSYNC_SECRET_KEY(defaultchange_me): encryption key for stored secrets. Change this in production.LIBRARYSYNC_ADMIN_API_KEY(defaultyour_admin_api_key): required for admin endpoints.LIBRARYSYNC_BASE_URL(defaulthttp://localhost:8000): base URL for OAuth callbacks.LOG_LEVEL(defaultINFO): logging level.HISTORY_LOOKBACK_DAYS(default30): import-all lookback window (set-1for full history).LIBRARYSYNC_JWT_ACCESS_TOKEN_MINUTES(default60): access token lifetime.LIBRARYSYNC_JWT_ALGORITHM(defaultHS256): JWT signing algorithm.LIBRARYSYNC_ALLOW_REGISTRATION(defaulttrue): enable/api/auth/register.LIBRARYSYNC_MAX_USERS(default1): max number of registered users (-1for unlimited).
TRAKT_CLIENT_ID(defaultyour_trakt_client_id): Trakt OAuth app client ID.TRAKT_CLIENT_SECRET(defaultyour_trakt_client_secret): Trakt OAuth app secret.SIMKL_CLIENT_ID(defaultyour_simkl_client_id): SIMKL OAuth app client ID.SIMKL_CLIENT_SECRET(defaultyour_simkl_client_secret): SIMKL OAuth app secret.
LIBRARYSYNC_WORKER_MODES(defaultall): comma-separated list of worker loops. Options:outbox,metadata,metadata_cache,quick_import,import_all,watchlist.LIBRARYSYNC_WORKER_OUTBOX_CONCURRENCY(default1): outbox loop concurrency.LIBRARYSYNC_WORKER_METADATA_CONCURRENCY(default1): metadata loop concurrency.LIBRARYSYNC_WORKER_METADATA_CACHE_CONCURRENCY(default1): metadata cache loop concurrency.LIBRARYSYNC_WORKER_QUICK_IMPORT_CONCURRENCY(default1): quick import loop concurrency.LIBRARYSYNC_WORKER_IMPORT_ALL_CONCURRENCY(default1): import-all loop concurrency.
LIBRARYSYNC_TRAKT_RATE_LIMIT_PER_MINUTE(default60).LIBRARYSYNC_SIMKL_RATE_LIMIT_PER_MINUTE(default60).LIBRARYSYNC_LETTERBOXD_RATE_LIMIT_PER_MINUTE(default30).LIBRARYSYNC_STREMIO_RATE_LIMIT_PER_MINUTE(default120).LIBRARYSYNC_TMDB_RATE_LIMIT_PER_MINUTE(default150).LIBRARYSYNC_TVDB_RATE_LIMIT_PER_MINUTE(default150).
LIBRARYSYNC_TRAKT_MAX_BATCH_SIZE(default750): Maximum number of items per Trakt batch request.LIBRARYSYNC_SIMKL_MAX_BATCH_SIZE(default750): Maximum number of items per SIMKL batch request (limited by 20MB POST size).
Letterboxd unfortunately does not have a devleoper program to request API access. For personal use it is possible to extract the required information from the app. Special thanks to @dado3212 with https://github.com/dado3212/letterboxd-scripts/ for guidance on retrieving the client_id and client_secret.
Letterboxd tip: users can paste an intercepted request as
curlorhttpieto extract theclient_idandclient_secret.
When configuring connected apps for Trakt and SIMKL, add your domain and callback URLs.
Example values (replace example.com with your domain):
- Trakt app URL:
https://example.com - Trakt redirect URI:
https://example.com/api/integrations/trakt/callback - SIMKL app URL:
https://example.com - SIMKL redirect URI:
https://example.com/api/integrations/simkl
- Install Python 3.14 and
uv. - Sync deps:
uv sync
- Run API:
cd backend && uv run uvicorn librarysync.main:app --reload - Run worker:
cd backend && uv run python -m librarysync.worker - Lint:
uv run ruff check ./backend/
If you’re using docker compose with the scaled-workers profile, worker-metadata-cache
is now available and will run the metadata cache loop (LIBRARYSYNC_WORKER_MODES=metadata_cache).
Use the release helper to bump the version in backend/pyproject.toml, tag,
and create a GitHub release.
Examples:
python scripts/release.py --patchpython scripts/release.py 0.9.0 --no-release
The release helper runs ruff and the unit tests before it commits.
gh must be installed and authenticated if you are creating a GitHub release.
librarySync automatically handles browser cache invalidation for static assets (CSS and JavaScript) by appending version query parameters to their URLs.
- Static files are cached for 7 days by default
- When you update the version in
backend/pyproject.toml, browsers will automatically fetch new files - Example:
/static/core.js?v=0.4.3becomes/static/core.js?v=0.4.5on version bump
No manual cache busting or build-time hash generation is needed.
Thanks to @MunifTanjim with https://github.com/MunifTanjim/stremthru.git for the inspiration behind the Stremio sync workflow.
