Keep your Plex Watchlist aligned with reality.
PlexPulse ensures your Plex Account Watchlist reflects what is actually present in your Plex libraries — safely, deterministically, and without guesswork.
No stale Watchlist entries.
No accidental removals.
No operator memory required.
PlexPulse is a library-driven reconciliation system for Plex Watchlists.
It answers one authoritative question:
“Does Plex still report this item as present?”
Everything flows from that single truth.
PlexPulse observes Plex, records belief in a persistent ledger, and applies cleanup policy deliberately — never implicitly.
Plex Watchlists can drift over time:
- Media is deleted
- Libraries are reorganised
- Arr tools remove items
- Plex metadata refreshes lag behind filesystem changes
PlexPulse prevents Watchlist drift by reacting only to what Plex itself reports — not filesystem state, not assumptions, and not partial signals.
- Plex is authoritative
- Nothing is inferred
- State is persisted
- Side-effects are explicit
- All actions are auditable
- Safe to re-run, safe to restart
PlexPulse is a deterministic multi-phase system with a persistent state plane.
PHASE A — Baseline (Observe Plex)
PHASE B — Backfill (Annotate Arr intent)
PHASE C — Ledger (Persistent State Plane)
PHASE D — Reconcile (Enforce policy)
Only Phase D performs side-effects.
PlexPulse maintains a persistent JSON-Lines ledger representing the system’s current belief about every known media item.
The ledger is:
- Human-auditable
- jq-friendly
- Persistent across restarts
- Never truncated automatically
It is not a cache, log, or event stream.
- Baseline observes Plex and updates belief
- Backfill records historical Arr intent
- Ledger stores truth
- Reconcile applies policy
No phase collapses responsibilities.
The diagram below shows how executable phases interact with the persistent ledger and where side-effects are allowed to occur.
flowchart TB
%% PlexPulse — Phase Interaction Model
classDef entry fill:#ECFEFF,stroke:#0891B2,stroke-width:2px,color:#000,font-weight:bold
classDef phase fill:#FFF7CC,stroke:#D4B106,stroke-width:2px,color:#000,font-weight:bold
classDef ledger fill:#E6F0FF,stroke:#3B82F6,stroke-width:3px,color:#000,font-weight:bold
classDef exec fill:#FEE2E2,stroke:#DC2626,stroke-width:3px,color:#000,font-weight:bold
subgraph SE["ENTRYPOINT CONTRACT<br/>(Startup Order)"]
E1["1) Ensure state dir exists<br/>
2) Ensure ledger exists<br/>
3) Run Baseline (A)<br/>
4) Run Backfill (B)<br/>
5) Run Reconcile (D)<br/>
6) Start server"]
end
class SE,E1 entry
subgraph SA["PHASE A — BASELINE"]
A1["Observe Plex library truth<br/>Mutates ledger only<br/>NO side-effects"]
end
class SA,A1 phase
subgraph SB["PHASE B — BACKFILL"]
B1["Radarr intent only"]
B2["Sonarr intent only"]
end
class SB,B1,B2 phase
subgraph SC["PHASE C — LEDGER"]
C1["Persistent system belief<br/>Survives restarts"]
end
class SC,C1 ledger
subgraph SD["PHASE D — RECONCILE"]
D1["Apply policy<br/>Side-effects via Admin API"]
end
class SD,D1 phase
subgraph SS["WEBHOOK SERVER"]
S1["Executes Watchlist + Arr deletions"]
end
class SS,S1 exec
A1 --> C1
B1 --> C1
B2 --> C1
C1 --> D1
D1 --> S1
E1 -.-> A1
E1 -.-> B1
E1 -.-> D1
E1 -.-> S1
Every item follows a strict lifecycle:
current → missing → deleted
- current: Plex reports the item
- missing: Plex no longer reports it
- deleted: Absence persisted beyond policy threshold
Transitions are driven by Plex observation and elapsed time only.
On container startup, PlexPulse always:
- Ensures the state directory exists
- Ensures the ledger file exists
- Runs Baseline
- Runs Backfill
- Runs Reconcile
- Starts the webhook server
No steps are skipped.
The following commands are safe and idempotent:
maintenance/plexpulse-baseline.sh
maintenance/plexpulse-reconcile.shPlexPulse integrates with Plex, Radarr, and Sonarr as a coordinated system.
| System | Role in PlexPulse |
|---|---|
| Plex | Authoritative source of library truth |
| Radarr | Movie deletion intent + acceleration signal |
| Sonarr | Series deletion intent + acceleration signal |
Plex determines what is real.
Radarr and Sonarr provide intent evidence and early signals.
- Baseline scans query Plex libraries
- Status is derived solely from Plex observation
- Filesystem state is never inspected
If Plex still reports an item, it is not deleted, regardless of Arr activity.
Radarr and Sonarr are core integrations in PlexPulse.
They provide:
- Deletion intent evidence (recorded in the ledger)
- Immediate Watchlist cleanup signals when items are removed
Radarr and Sonarr never determine truth. Plex remains the authoritative source of library presence.
Webhook delivery is best-effort:
- Missed or delayed webhooks are safe
- Baseline + Reconcile will always converge state correctly
Radarr and Sonarr must be configured correctly to ensure:
- full-series / full-movie deletions behave as expected
- Watchlist cleanup occurs only when Plex no longer reports the item
For step-by-step setup, including:
- exact webhook configuration
- required event selections
- delete-file vs remove-only semantics
- common misconfiguration pitfalls
👉 See the full integration setup guide:
docs/setup-integrations.md
Configure in Radarr → Settings → Connect → Add → Webhook:
| Field | Value |
|---|---|
| URL | http://<plexpulse-host>:8080/webhook/radarr |
| Method | POST |
| On Delete | ✅ Enabled |
Radarr sends:
movie.tmdbId→tmdb://<tmdbId>
Configure in Sonarr → Settings → Connect → Add → Webhook:
| Field | Value |
|---|---|
| URL | http://<plexpulse-host>:8080/webhook/sonarr |
| Method | POST |
| On Delete | ✅ Enabled |
Sonarr sends:
series.tvdbId→tvdb://<tvdbId>
- Webhooks are best-effort
- Missed webhooks are not a problem
- Baseline + Reconcile always converge state correctly
- Arr tools never determine status
The webhook server is the only execution surface in PlexPulse.
It:
- Maintains the in-memory Watchlist index (
ACTIVE_INDEX) - Executes Watchlist removals
- Executes Arr deletions
- Exposes Admin and Inspection APIs
Shell scripts never call Plex or Arr APIs directly.
| Endpoint | Source | Purpose |
|---|---|---|
POST /webhook/radarr |
Radarr | Movie deletion intent + immediate cleanup |
POST /webhook/sonarr |
Sonarr | Series deletion intent + immediate cleanup |
| Endpoint | Purpose |
|---|---|
GET /inspect/index |
Full Watchlist index |
GET /inspect/watchlist |
Current Watchlist items |
GET /inspect/lookup/<external_id> |
Resolve canonical ID |
| Endpoint | Method | Purpose |
|---|---|---|
/admin/index-ready |
GET | Check index readiness |
/admin/rebuild-index |
POST | Rebuild Watchlist index |
/admin/reconcile/library-deletions |
POST | Enforce Watchlist cleanup |
/admin/reconcile/arr-deletions/preview |
GET | Preview Arr deletions |
/admin/reconcile/arr-deletions/execute |
POST | Execute Arr deletions |
| Command | Purpose |
|---|---|
maintenance/plexpulse-baseline.sh |
Observe Plex and update ledger |
maintenance/plexpulse-reconcile.sh |
Enforce deletion policy |
Commands are safe to run:
- manually
- via cron
- after large library changes
- Docker-first
- Single bind-mounted state directory
- No Docker-in-Docker
- No hidden host assumptions
PlexPulse never:
- Deletes media files
- Modifies Plex libraries
- Performs silent automation
- Acts on partial data
All actions are explicit, gated, and auditable.
All runtime state lives in a single directory:
${PLEXPULSE_STATE_DIR}/${PLEXPULSE_LIBRARY_LEDGER}
Configuration is provided via .env.
Required and optional values are documented in .env.example.
PlexPulse uses internal versioning for compatibility and evolution.
These versions are maintainer-facing and do not affect day-to-day usage.
MIT License — see LICENSE.
⭐ PlexPulse
Because your Watchlist should reflect reality, not memory.