feat(plex): Phase C — incremental + full Plex↔Siftlode watch reconcile
Completes the two-way watch-state sync with two scheduler jobs: - plex_watch_sync (default 30m): pull recent Plex-side changes (watch history + on-deck, filtered to the owner account) into Siftlode under last-write-wins (_pull_apply + a _same_state ping-pong guard + skew tolerance), then re-push any still-unsynced local states. - plex_watch_reconcile (default daily): full section rescan; uses synced_to_plex to settle what the incremental feed can't — notably propagating a Plex-side un-watch (clear a previously-mirrored row Plex no longer has) — while re-pushing never-synced local states and never touching hidden (Siftlode-only) rows. Union-preserving. PlexClient gains accounts/watch_history/on_deck; _scan_plex_states is factored out and shared with the one-time import. Owner accountID is resolved once and cached on the link. Both jobs are registered in the scheduler (pause-skip, activity tracking, run-now, admin-tunable intervals) with trilingual (HU/EN/DE) labels + descriptions. New config default plex_watch_reconcile_interval_min. Verified live against the real Plex server: read feeds, last-write-wins, dirty re-push, the incremental job end-to-end, and a full reconcile that cleared exactly the one un-watched item with zero collateral across 17976 scanned.
This commit is contained in:
parent
bdf35c3375
commit
bbbcf4ff5a
7 changed files with 332 additions and 43 deletions
|
|
@ -15,6 +15,7 @@ from app.db import SessionLocal
|
|||
from app.downloads.gc import run_download_gc
|
||||
from app.models import SchedulerSetting
|
||||
from app.plex.sync import sync as run_plex_sync
|
||||
from app.plex.watch_sync import run_plex_watch_reconcile, run_plex_watch_sync
|
||||
from app.notifications import create_notification
|
||||
from app.state import is_sync_paused
|
||||
from app.sync.autotag import run_autotag_all
|
||||
|
|
@ -63,6 +64,8 @@ JOB_INTERVALS: dict[str, int] = {
|
|||
"explore_cleanup": settings.explore_cleanup_minutes,
|
||||
"download_gc": settings.download_gc_minutes,
|
||||
"plex_sync": settings.plex_sync_interval_min,
|
||||
"plex_watch_sync": settings.plex_watch_sync_interval_min,
|
||||
"plex_watch_reconcile": settings.plex_watch_reconcile_interval_min,
|
||||
}
|
||||
|
||||
# Sane bounds for an admin-set interval (minutes).
|
||||
|
|
@ -292,6 +295,18 @@ def _plex_sync_job() -> None:
|
|||
_job("plex_sync", run_plex_sync)
|
||||
|
||||
|
||||
def _plex_watch_sync_job() -> None:
|
||||
# Incremental Plex→Siftlode watch-state pull (history + on-deck) + re-push of unsynced local
|
||||
# states. Cheap; a no-op when Plex is disabled or no owner link has sync enabled.
|
||||
_job("plex_watch_sync", run_plex_watch_sync)
|
||||
|
||||
|
||||
def _plex_watch_reconcile_job() -> None:
|
||||
# Full watch-state reconcile (whole-section rescan) — catches un-watches the incremental feed
|
||||
# can't. Heavier, so it runs far less often (daily default).
|
||||
_job("plex_watch_reconcile", run_plex_watch_reconcile)
|
||||
|
||||
|
||||
# job_id -> wrapper. The single source of truth for which jobs exist and how to run one,
|
||||
# shared by start_scheduler (recurring registration) and trigger_job (manual "run now").
|
||||
JOB_FUNCS: dict[str, Callable[[], None]] = {
|
||||
|
|
@ -307,6 +322,8 @@ JOB_FUNCS: dict[str, Callable[[], None]] = {
|
|||
"explore_cleanup": _explore_cleanup_job,
|
||||
"download_gc": _download_gc_job,
|
||||
"plex_sync": _plex_sync_job,
|
||||
"plex_watch_sync": _plex_watch_sync_job,
|
||||
"plex_watch_reconcile": _plex_watch_reconcile_job,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue