# -*- coding: utf-8 -*- # Background service: reports playback of this add-on's files to the server # (POST /api/state/), which keeps watched state and resume points # for every app (e.g. the SKLK TV app); listings come back with that state. # (Kodi's own earlier history: see history.py.) import json import re import time import urllib.request import xbmc import xbmcaddon ADDON_ID = xbmcaddon.Addon().getAddonInfo('id') FILE_ID = re.compile(r'all/play/(\d+)') REPORT_EVERY = 30 # seconds, while playing CLIENT = 'kodi' def log(message, level=xbmc.LOGINFO): xbmc.log('[{0} service] {1}'.format(ADDON_ID, message), level) def server_url(): """The server setting (see main.py), read each time since it may change.""" return re.sub(r'/api$', '', xbmcaddon.Addon().getSetting('api_url').strip().rstrip('/')) def report(file_id, position, duration, event, at=None): """ Positions in seconds; at (ms since epoch) only for late reports. True if the server got it (an answer that it doesn't know the file counts too). """ if not server_url(): return False payload = {'position': position, 'event': event, 'client': CLIENT} if duration: payload['duration'] = duration if at: payload['at'] = at request = urllib.request.Request( server_url() + '/api/state/' + str(file_id), data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json', 'Authorization': xbmcaddon.Addon().getSetting('auth_key')}, method='POST', ) try: with urllib.request.urlopen(request, timeout=10) as response: response.read() return True except Exception as e: log('report failed: {0}'.format(e), xbmc.LOGWARNING) return False def is_ours(url): """Streams of this add-on's server (as opposed to other add-ons, or LRT streams).""" server = server_url() return bool(server) and url.startswith(server + '/') def refresh_if_ours(): # e.g. Continue watching moves on to the next episode if xbmc.getInfoLabel('Container.FolderPath').startswith('plugin://' + ADDON_ID): xbmc.executebuiltin('Container.Refresh') class Player(xbmc.Player): def __init__(self): super().__init__() self.file_id = None self.position = 0 self.duration = 0 self.last_report = 0 # (file_id, position, duration, event, at): retried until it goes through self.unsent = None def onAVStarted(self): self.finish('stop') # in case another file was playing try: url = self.getPlayingFile() except RuntimeError: return match = FILE_ID.search(url) if is_ours(url) else None if match is None: return self.file_id = int(match.group(1)) self.position = 0 self.duration = 0 self.update() self.send('start') def onPlayBackPaused(self): if self.file_id is not None: self.update() self.send('pause') def onPlayBackStopped(self): self.finish('stop') def onPlayBackEnded(self): self.finish('end') def onPlayBackError(self): self.finish('stop') def tick(self): """Every second, from the service loop.""" if self.unsent and time.time() - self.last_report >= REPORT_EVERY: self.last_report = time.time() if report(*self.unsent): self.unsent = None if self.file_id is None: return self.update() if time.time() - self.last_report >= REPORT_EVERY and not xbmc.getCondVisibility('Player.Paused'): self.send('tick') def update(self): # after a stop there's no time to ask for, the last one known is reported try: if self.isPlayingVideo(): self.position = self.getTime() self.duration = self.getTotalTime() or self.duration except RuntimeError: pass def finish(self, event): if self.file_id is None: return if event == 'end': self.position = self.duration self.send(event) self.file_id = None refresh_if_ours() def send(self, event): self.last_report = time.time() args = (self.file_id, self.position, self.duration, event) if report(*args): if self.unsent and self.unsent[0] == self.file_id: self.unsent = None else: self.unsent = args + (int(time.time() * 1000),) def run(): monitor = xbmc.Monitor() player = Player() while not monitor.waitForAbort(1): player.tick() if __name__ == '__main__': run()