Disclaimer: This documentation is provided for convenience and may contain errors. Always verify information against the official Kodi and provider documentation. Report issues.
Fastmail
Home / Files / service.py

service.py — scheduled sync daemon

Path /service.py Lines 129 Type xbmc.service (long-lived) Single instance? yes — Kodi starts one per profile, lives until Kodi exits ← invoked by Kodi at startup

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

PROVIDER_SCHEDULE = [("bbc",0), ("itvx",30), ("c4",60), ("my5",90), ("stv",120), ("blaze",150)]

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.

SYNC_INTERVAL_SECS = 6 * 3600 · line 18

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

__init__() · lines 22–45

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.

BBC respect TV-license setting

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.

_run_provider_sync(provider, force=False) · lines 47–65

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.

_check_force_markers() · lines 67–99

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".

Why marker files instead of JSON-RPC?

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.

run() · lines 101–124

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:

  1. 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.
  2. Call _check_force_markers().
  3. 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.

Daemon thread + pymysql = corruption if you share a connection

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