Letterboxd → Seerr local watchlist + Marathon Man Plex watchlist on download.
Automation in bin/ is ~2k lines of Python/shell (standalone; not the Seerr application).
Install: clone this repo and symlink or copy bin/ into your Seerr install (e.g. ~/.config/seerr/bin/) and config/*.env.example into ~/.config/seerr/config/.
| Phase | Destination | Trigger |
|---|---|---|
| Browse / request | benmo Seerr local watchlist (SEERR_USER_ID=3) |
Letterboxd sync cron (30s) |
| Playback queue | Plex Home user Marathon Man watchlist | Seerr Media Available webhook |
Letterboxd sync never touches Plex. The webhook is add-only on Marathon Man (never removeFromWatchlist).
cd ~/.config/seerr
python3 -m venv venv
./venv/bin/pip install -r bin/requirements-letterboxd-plex.txt
chmod 600 config/letterboxd-sync.env config/plex-watchlist-hook.env
chmod +x bin/*.sh bin/*.pyCopy examples if env files are missing:
cp config/letterboxd-sync.env.example config/letterboxd-sync.env
cp config/plex-watchlist-hook.env.example config/plex-watchlist-hook.env
# Edit both; API key from config/settings.json → main.apiKeyPreferred: export watchlist.csv from Letterboxd → save as config/letterboxd-watchlist.csv:
~/.config/seerr/venv/bin/python3 bin/letterboxd-seerr-watchlist-backfill.py --csv config/letterboxd-watchlist.csvAlternatives:
--json config/letterboxd-watchlist-scrape.json(off-box scrape)--scrape ben__hyman(often 403 on Whatbox / Cloudflare)
bin/letterboxd-seerr-watchlist-sync.sh sources config/letterboxd-sync.env, uses flock, runs the Python sync.
On Whatbox, direct Letterboxd RSS and public RSSHub instances usually return 403/503 (Cloudflare). No laptop required: refresh the snapshot on-box with HTML scrape (browser user-agent).
| Script | Where | Purpose |
|---|---|---|
bin/letterboxd-fetch-rss-snapshot-whatbox.sh |
Whatbox | Refresh letterboxd-rss-snapshot.json (proxy RSS → direct RSS → scrape) |
bin/letterboxd-fetch-rss-snapshot.py |
Any host | Same fetch logic; -o path from env |
bin/letterboxd-seerr-watchlist-sync.sh |
Whatbox | Every 30s: read snapshot, sync to Seerr (no RSS if LETTERBOXD_SNAPSHOT_JSON is set) |
bin/letterboxd-push-snapshot.sh |
Optional laptop | Legacy off-box fetch + rsync (zero-laptop setup ignores this) |
bin/letterboxd-seerr-diary-remove.sh |
Whatbox | Every 5m: diary RSS → remove benmo watchlist for new watches |
Logs: config/logs/letterboxd-sync.log, config/logs/letterboxd-fetch.log
Env (config/letterboxd-sync.env):
LETTERBOXD_SNAPSHOT_JSON=/mnt/mpathe/ahhhhhhh/.config/seerr/config/letterboxd-rss-snapshot.json
LETTERBOXD_RSS_URL=https://letterboxd.com/ben__hyman/rss/watchlist/
# Optional if a proxy works from your network:
# LETTERBOXD_RSS_PROXY_URL=https://rsshub.app/letterboxd/watchlist/{username}While LETTERBOXD_SNAPSHOT_JSON is set, sync never calls Letterboxd RSS. Fetch order: proxy URL (if set) → direct RSS → HTML scrape.
Optional freshness for sync only:
LETTERBOXD_SNAPSHOT_MAX_AGE_MINUTES=180— skip sync if file older than 3hLETTERBOXD_SNAPSHOT_ALWAYS=1— use snapshot regardless of age
Manual refresh
/mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-fetch-rss-snapshot-whatbox.shFetch only
./venv/bin/python3 bin/letterboxd-fetch-rss-snapshot.py -o config/letterboxd-rss-snapshot.jsonJSON format: array of objects with slug, title, year, link (or {"items":[...]} with --wrap).
Letterboxd has no webhooks. “Constant listening” means polling; on Whatbox use cron (not true push). Diary RSS is small (latest ~50 watches), so every 5 minutes is reasonable. The 30s watchlist sync is separate (adds from snapshot); diary removal is cheap and does not need 30s unless you want it — match sync with flock if you run both often.
bin/letterboxd-seerr-diary-remove.sh polls LETTERBOXD_DIARY_RSS_URL (default https://letterboxd.com/ben__hyman/rss/ — main activity/diary feed, browser UA like watchlist fetch). New RSS guids since last poll → TMDB → DELETE /api/v1/watchlist/{tmdbId}?mediaType=movie (X-Api-Key, X-API-User: 3).
| Control | Meaning |
|---|---|
LETTERBOXD_DIARY_RSS_URL |
Diary/activity RSS, e.g. https://letterboxd.com/ben__hyman/rss/ |
LETTERBOXD_DIARY_STATE_FILE |
Optional; default config/letterboxd-diary-state.json (seen_guids) |
Logs: config/logs/letterboxd-diary-remove.log
Safety
- Feed parse failure: no deletes
- Empty feed on first run (no state): no deletes, no state write
- First successful run: records all current guids as baseline — no mass delete
- Later runs: only guids not in
seen_guidstrigger removal
Dry run
./venv/bin/python3 bin/letterboxd-seerr-diary-remove.py --dry-runCron (Whatbox)
@reboot /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-diary-remove.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-diary-remove.log 2>&1
*/5 * * * * /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-diary-remove.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-diary-remove.log 2>&1When a film leaves the Letterboxd watchlist, incremental sync can DELETE it from benmo’s Seerr watchlist (SEERR_USER_ID=3) via DELETE /api/v1/watchlist/{tmdbId}?mediaType=movie.
| Control | Default | Meaning |
|---|---|---|
LETTERBOXD_SYNC_REMOVE |
on (1) |
Set 0 to disable removal pass |
LETTERBOXD_SNAPSHOT_TRUST_FULL |
off | Set 1 when the snapshot is a full watchlist (Whatbox scrape, CSV backfill, or trusted full fetch). Required for safe removals if the file might be RSS-truncated |
State (config/letterboxd-sync-state.json) is the source of truth for Letterboxd-managed items only:
synced:{ "slug": tmdbId, ... }(plus legacysynced_slugs/synced_tmdb_ids)- Removals run only for slugs in
syncedthat are not in the current snapshot slug set - Manual Seerr watchlist adds are not removed unless they were recorded in
syncedby this sync/backfill
Safety
- Empty snapshot or failed fetch: no removal pass (avoids mass-delete)
- If snapshot has fewer slugs than
syncedandLETTERBOXD_SNAPSHOT_TRUST_FULLis unset, removals are skipped (RSS/proxy feeds are often truncated) - Prefer
letterboxd-fetch-rss-snapshot-whatbox.sh(scrape fallback) +LETTERBOXD_SNAPSHOT_TRUST_FULL=1on Whatbox
Dry run
./venv/bin/python3 bin/letterboxd-seerr-watchlist-sync.py --dry-runOnly if on-box scrape stops working. Standard Whatbox setup uses letterboxd-fetch-rss-snapshot-whatbox.sh only. bin/letterboxd-push-snapshot.sh needs WHATBOX_HOST from a laptop that can reach Letterboxd RSS.
bin/plex-watchlist-webhook-control.sh start
bin/plex-watchlist-webhook-control.sh is-runningEnsure script (cron @reboot + every minute):
bin/plex-watchlist-webhook-ensure.shLogs: config/logs/plex-watchlist-webhook.log
- Settings → Notifications → Webhook → enable
- URL:
http://127.0.0.1:27861/ - Enable Media Available only (disable other types)
- Test with curl (see below)
curl -sS -X POST http://127.0.0.1:27861/ \
-H 'Content-Type: application/json' \
-d '{
"notification_type": "MEDIA_AVAILABLE",
"media_tmdbid": 550,
"media_type": "movie",
"requestedBy_email": "benmo.hyman@gmail.com",
"requestedBy_username": "benmo"
}'Expect 200 JSON; check plex-watchlist-webhook.log for add/skip/no_match.
Merge with existing entries (do not remove Seerr ensure lines):
@reboot /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-fetch-rss-snapshot-whatbox.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-fetch.log 2>&1
@reboot /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-watchlist-sync.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-sync.log 2>&1
@reboot /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-diary-remove.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-diary-remove.log 2>&1
*/10 * * * * /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-fetch-rss-snapshot-whatbox.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-fetch.log 2>&1
*/5 * * * * /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-diary-remove.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-diary-remove.log 2>&1
* * * * * /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-watchlist-sync.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-sync.log 2>&1
* * * * * sleep 30; /mnt/mpathe/ahhhhhhh/.config/seerr/bin/letterboxd-seerr-watchlist-sync.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/letterboxd-sync.log 2>&1
@reboot /mnt/mpathe/ahhhhhhh/.config/seerr/bin/plex-watchlist-webhook-ensure.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/plex-watchlist-webhook-ensure.log 2>&1
* * * * * /mnt/mpathe/ahhhhhhh/.config/seerr/bin/plex-watchlist-webhook-ensure.sh >>/mnt/mpathe/ahhhhhhh/.config/seerr/config/logs/plex-watchlist-webhook-ensure.log 2>&1- Rotate benmo Seerr password if it was shared in chat; do not store passwords in env files.
- Keep
letterboxd-sync.envandplex-watchlist-hook.envat mode600.
- CSV upload: Full watchlist needs Letterboxd CSV export or off-box JSON; RSS alone is incomplete.
- Cloudflare: Direct RSS and public RSSHub often fail on Whatbox; use
letterboxd-fetch-rss-snapshot-whatbox.sh(HTML scrape fallback) +LETTERBOXD_SNAPSHOT_JSON. - Seerr webhook: Must enable Media Available →
http://127.0.0.1:27861/in UI. - Plex match: Title must exist in Plex library for TMDB guid search before watchlist add.