service.py — scheduled sync daemon
Role
Long-running xbmc.Monitor daemon. This is the server-role
history page for the original single-addon service (removed in the 3-package
split). Today the server logic lives in service.easyplaytv/lib/kodi_service.py
(and the standalone easyplaytv-service runtime); there is no
follower/device-role distinction anymore — the Service addon is always a server,
and client boxes point their video addon PVR server URL at it.
The current kodi_service.py no longer staggers per-provider timers
or polls force-sync marker files: the runtime's scheduler.py runs a
sequential batch (providers one-after-another, then PVR refresh) capped by the
sync interval, and manual/forced sync was removed. The staggered marker-file
behaviour below is historical only.
Module-level configuration
One (provider, minute-offset) tuple per provider. The offset is the minute of the day at which the first sync should fire, so they're spread across the first 2.5 h of the day and then every 6 h after. So BBC fires at 00:00, 06:00, 12:00, 18:00; ITVX at 00:30, 06:30, …; C4 at 01:00, 07:00, …; etc.
Stagger window. 6 h chosen because that's the longest we can go without missing the time-window for newly-published episodes (especially BBC, which publishes ~30 min after broadcast) while keeping TMDb rate-limit usage modest. Each provider's TMDb lookup is independent so they don't compound.
EPTVMonitor class
Legacy master/follower section — removed in the 3-package split. The old
code read db_source via resources.lib.is_master();
in follower mode the daemon logged and returned without scheduling
syncs (libraries are written by the server box only; clients just read the
shared MariaDB). In master mode it initialised
self._next_sync[provider] for every provider to the next future
slot in the day. The Service addon now always runs as a server.
If has_tv_license is false, BBC is not added to
self._next_sync; the loop at line 110 then skips it. This is
the same gate used in default.py
when the user clicks a BBC live channel.
Worker function run inside a daemon thread. Imports
library_sync (also late-imported
since startup order can race Kodi's addon loader), calls
library_sync.sync_provider(provider, force=force), logs the
result, and unconditionally removes itself from
self._sync_threads in finally so the next slot can
be scheduled.
Polls the addon profile directory for marker files named
force_sync_ (e.g.
force_sync_c4). If found, the file is deleted and a
force sync is fired immediately in a new daemon thread — bypassing
the scheduled slot. The implementation deliberately does nothing if a sync
for that provider is already running: "Force-sync marker for 'c4'
ignored — sync already running".
Kodi 21's Addons.ExecuteAddon does not invoke a
python.pluginsource addon with query-string params — it loads
the addon's default entry-point without forwarding argv. JSON-RPC
RunPlugin is also unreliable for this. The marker-file approach
lets external scripts (a Home Assistant automation, a Cron job, a Kodi
settings UI button via RunScript) trigger syncs by simply
touching a file. The service polls every 30 s so latency is at
worst half a minute.
The main loop. Logs the first-sync delay for each provider (so you can
read in kodi.log when the next slot is), then loops on
waitForAbort(30) — Kodi's monitor-friendly sleep that returns
truthy when Kodi is shutting down. Each iteration:
- For every provider in
self._next_sync: if we've reached its time and no thread is currently running for it, reschedule (self._next_sync[p] = now + SYNC_INTERVAL_SECS) and spawn a daemon thread. - Call
_check_force_markers(). - Wait 30 s.
Concurrency model
One daemon thread per provider at any time. self._sync_threads
maps provider name to threading.Thread and is populated when a
sync starts and cleared when it ends — the scheduler checks membership
before launching, so two concurrent syncs of the same provider can't
happen. Syncs of different providers can run concurrently,
which is why library_sync._get_db()
uses threading.local() for its DB connections.
See troubleshooting. Daemon
threads spawned here each establish their own
pymysql.connect() via
library_sync._get_db()'s
threading.local() cache. Never share a
pymysql connection across the daemon threads spawned here —
concurrent queries on the same socket corrupt the MySQL packet stream
("Packet sequence number wrong - got 2 expected 1" /
"'NoneType' object has no attribute 'read'").
Lifecycle log messages you'll see in kodi.log
EasyPlayTV: Service started — 6 providers at 6h intervals
EasyPlayTV: 'bbc' first sync in 480 min
EasyPlayTV: 'itvx' first sync in 510 min
…
EasyPlayTV: Scheduled sync starting for 'c4'
EasyPlayTV: Scheduled sync complete for 'c4'
EasyPlayTV: Force-sync marker detected for 'my5'
EasyPlayTV: Service stopped