send Kodi's watched history from a settings button

Not automatically on the service's first start any more: with several Kodi
installs, only the one whose history counts should send it. The button
("Send this Kodi's watched history to the server") asks first, shows progress
and can be cancelled; sending again is harmless, the server keeps newer state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jurgis SakalauskasandClaude Opus 5.5 committed 2026-10-12 10:53:07 +03:00
1 parent 227a95abcd
commit 5e20067d6d
6 files changed
+129 -85

No files matched your search

+1 -81
View File
@@ -2,22 +2,15 @@
# Background service: reports playback of this add-on's files to the server
# (POST <server>/api/state/<file id>), which keeps watched state and resume points
# for every app (e.g. the SKLK TV app); listings come back with that state.
#
# On its first start it also hands Kodi's own watched history of this add-on
# to the server, once (whatever the server knows that's newer wins).
# (Kodi's own earlier history: see history.py.)
import json
import os
import re
import sqlite3
import time
import urllib.request
from datetime import datetime
from urllib.parse import unquote
import xbmc
import xbmcaddon
import xbmcvfs
ADDON_ID = xbmcaddon.Addon().getAddonInfo('id')
FILE_ID = re.compile(r'all/play/(\d+)')
@@ -153,82 +146,9 @@ class Player(xbmc.Player):
self.unsent = args + (int(time.time() * 1000),)
def newest_video_database():
folder = xbmcvfs.translatePath('special://database')
versions = [(int(m.group(1)), name) for name in os.listdir(folder)
for m in [re.match(r'MyVideos(\d+)\.db$', name)] if m]
return os.path.join(folder, max(versions)[1]) if versions else None
def to_ms(last_played):
try:
return int(time.mktime(datetime.strptime(last_played, '%Y-%m-%d %H:%M:%S').timetuple()) * 1000)
except (TypeError, ValueError):
return 1 # unknown: older than anything the server has
def import_kodi_history():
"""
Before this service, Kodi kept this add-on's watched state and resume points
in its own database only. Send them to the server once, as late reports.
"""
profile = xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
marker = os.path.join(profile, 'history_imported')
if os.path.exists(marker):
return
database = newest_video_database()
rows = []
if database:
connection = sqlite3.connect('file:{0}?mode=ro'.format(database), uri=True)
try:
rows = connection.execute(
'SELECT path.strPath, files.strFilename, files.playCount, files.lastPlayed,'
' bookmark.timeInSeconds, bookmark.totalTimeInSeconds'
' FROM files JOIN path ON path.idPath = files.idPath'
' LEFT JOIN bookmark ON bookmark.idFile = files.idFile AND bookmark.type = 1'
).fetchall()
finally:
connection.close()
# by file id, the latest wins (a file can be there by plugin url and by stream url)
history = {}
for folder, filename, play_count, last_played, position, total in rows:
url = unquote(unquote((folder or '') + (filename or '')))
match = FILE_ID.search(url)
if match is None or not (url.startswith('plugin://' + ADDON_ID) or is_ours(url)):
continue
if not play_count and not position:
continue
file_id = int(match.group(1))
at = to_ms(last_played)
if file_id not in history or history[file_id][3] < at:
history[file_id] = (play_count or 0, position or 0, total or 0, at)
for file_id, (play_count, position, total, at) in history.items():
sent = True
if play_count > 0:
sent = report(file_id, total, total, 'end', at)
if sent and position > 0:
# (also) a resume point, e.g. rewatching
sent = report(file_id, position, total, 'stop', at)
if not sent:
return # server unreachable: try again next time
if not os.path.isdir(profile):
os.makedirs(profile)
open(marker, 'w').close()
log('imported watch history of {0} files'.format(len(history)))
def run():
monitor = xbmc.Monitor()
player = Player()
try:
import_kodi_history()
except Exception as e:
log('history import failed: {0}'.format(e), xbmc.LOGWARNING)
while not monitor.waitForAbort(1):
player.tick()