Merge branch 'develop'
This commit is contained in:
commit
a360663134
10 changed files with 495 additions and 86 deletions
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<addon id="plugin.video.plexkodiconnect"
|
<addon id="plugin.video.plexkodiconnect"
|
||||||
name="PlexKodiConnect"
|
name="PlexKodiConnect"
|
||||||
version="1.0.7"
|
version="1.0.8"
|
||||||
provider-name="croneter">
|
provider-name="croneter">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="2.1.0"/>
|
<import addon="xbmc.python" version="2.1.0"/>
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
version 1.0.8
|
||||||
|
- Redesigned fast incremential sync
|
||||||
|
- Workaround to sync Kodi time with PMS time
|
||||||
|
- Fix library sync key error
|
||||||
|
|
||||||
version 1.0.7
|
version 1.0.7
|
||||||
- Fix userclient
|
- Fix userclient
|
||||||
- Fix KeyError in library sync
|
- Fix KeyError in library sync
|
||||||
|
|
|
@ -1476,36 +1476,6 @@ class API():
|
||||||
"""
|
"""
|
||||||
return self.part
|
return self.part
|
||||||
|
|
||||||
def DateToKodi(self, stamp):
|
|
||||||
"""
|
|
||||||
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
|
||||||
propper, human-readable time stamp used by Kodi
|
|
||||||
|
|
||||||
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
|
||||||
"""
|
|
||||||
# DATEFORMAT = xbmc.getRegion('dateshort')
|
|
||||||
# TIMEFORMAT = xbmc.getRegion('meridiem')
|
|
||||||
# date_time = time.localtime(stamp)
|
|
||||||
# if DATEFORMAT[1] == 'd':
|
|
||||||
# localdate = time.strftime('%d-%m-%Y', date_time)
|
|
||||||
# elif DATEFORMAT[1] == 'm':
|
|
||||||
# localdate = time.strftime('%m-%d-%Y', date_time)
|
|
||||||
# else:
|
|
||||||
# localdate = time.strftime('%Y-%m-%d', date_time)
|
|
||||||
# if TIMEFORMAT != '/':
|
|
||||||
# localtime = time.strftime('%I:%M%p', date_time)
|
|
||||||
# else:
|
|
||||||
# localtime = time.strftime('%H:%M', date_time)
|
|
||||||
# return localtime + ' ' + localdate
|
|
||||||
try:
|
|
||||||
# DATEFORMAT = xbmc.getRegion('dateshort')
|
|
||||||
# TIMEFORMAT = xbmc.getRegion('meridiem')
|
|
||||||
date_time = time.localtime(float(stamp))
|
|
||||||
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
|
|
||||||
except:
|
|
||||||
localdate = None
|
|
||||||
return localdate
|
|
||||||
|
|
||||||
def getType(self):
|
def getType(self):
|
||||||
"""
|
"""
|
||||||
Returns the type of media, e.g. 'movie' or 'clip' for trailers
|
Returns the type of media, e.g. 'movie' or 'clip' for trailers
|
||||||
|
@ -1543,7 +1513,7 @@ class API():
|
||||||
"""
|
"""
|
||||||
Returns the date when this library item was created
|
Returns the date when this library item was created
|
||||||
"""
|
"""
|
||||||
return self.DateToKodi(self.item.attrib.get('addedAt', None))
|
return utils.DateToKodi(self.item.attrib.get('addedAt', None))
|
||||||
|
|
||||||
def getUserData(self):
|
def getUserData(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1576,7 +1546,7 @@ class API():
|
||||||
played = True
|
played = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lastPlayedDate = self.DateToKodi(int(item['lastViewedAt']))
|
lastPlayedDate = utils.DateToKodi(int(item['lastViewedAt']))
|
||||||
except:
|
except:
|
||||||
lastPlayedDate = None
|
lastPlayedDate = None
|
||||||
|
|
||||||
|
@ -1851,10 +1821,10 @@ class API():
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
item = self.item.attrib
|
item = self.item.attrib
|
||||||
key = item['grandparentRatingKey']
|
key = item.get('grandparentRatingKey')
|
||||||
title = item['grandparentTitle']
|
title = item.get('grandparentTitle')
|
||||||
season = item['parentIndex']
|
season = item.get('parentIndex')
|
||||||
episode = item['index']
|
episode = item.get('index')
|
||||||
return key, title, season, episode
|
return key, title, season, episode
|
||||||
|
|
||||||
def addPlexHeadersToUrl(self, url, arguments={}):
|
def addPlexHeadersToUrl(self, url, arguments={}):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from urllib import urlencode
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from urlparse import urlparse, parse_qs
|
from urlparse import urlparse, parse_qs
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from xbmcaddon import Addon
|
from xbmcaddon import Addon
|
||||||
|
|
||||||
|
@ -371,3 +372,22 @@ def PMSHttpsEnabled(url):
|
||||||
# couldn't get an xml - switch to http traffic
|
# couldn't get an xml - switch to http traffic
|
||||||
logMsg('PMSHttpsEnabled', 'PMS on %s talks HTTPS' % url, 1)
|
logMsg('PMSHttpsEnabled', 'PMS on %s talks HTTPS' % url, 1)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def scrobble(ratingKey, state):
|
||||||
|
"""
|
||||||
|
Tells the PMS to set an item's watched state to state="watched" or
|
||||||
|
state="unwatched"
|
||||||
|
"""
|
||||||
|
args = {
|
||||||
|
'key': ratingKey,
|
||||||
|
'identifier': 'com.plexapp.plugins.library'
|
||||||
|
}
|
||||||
|
if state == "watched":
|
||||||
|
url = "{server}/:/scrobble?" + urlencode(args)
|
||||||
|
elif state == "unwatched":
|
||||||
|
url = "{server}/:/unscrobble?" + urlencode(args)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
downloadutils.DownloadUtils().downloadUrl(url, type="GET")
|
||||||
|
logMsg("Toggled watched state for Plex item %s" % ratingKey, 1)
|
||||||
|
|
|
@ -152,6 +152,22 @@ class Embydb_Functions():
|
||||||
))
|
))
|
||||||
self.embycursor.execute(query, (viewid,))
|
self.embycursor.execute(query, (viewid,))
|
||||||
|
|
||||||
|
def getItem_byFileId(self, fileId):
|
||||||
|
"""
|
||||||
|
Returns the Plex itemId by using the Kodi fileId
|
||||||
|
"""
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT emby_id",
|
||||||
|
"FROM emby",
|
||||||
|
"WHERE kodi_fileid = ?"
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
self.embycursor.execute(query, (fileId,))
|
||||||
|
item = self.embycursor.fetchone()[0]
|
||||||
|
return item
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def getItem_byId(self, embyid):
|
def getItem_byId(self, embyid):
|
||||||
|
|
||||||
embycursor = self.embycursor
|
embycursor = self.embycursor
|
||||||
|
@ -183,6 +199,23 @@ class Embydb_Functions():
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def getPlexId(self, kodiid):
|
||||||
|
"""
|
||||||
|
Returns the Plex ID usind the Kodiid. Result:
|
||||||
|
(Plex Id, Parent's Plex Id)
|
||||||
|
"""
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT emby_id, parent_id",
|
||||||
|
"FROM emby",
|
||||||
|
"WHERE kodi_id = ?"
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
self.embycursor.execute(query, (kodiid))
|
||||||
|
item = self.embycursor.fetchone()
|
||||||
|
return item
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def getItem_byKodiId(self, kodiid, mediatype):
|
def getItem_byKodiId(self, kodiid, mediatype):
|
||||||
|
|
||||||
embycursor = self.embycursor
|
embycursor = self.embycursor
|
||||||
|
|
|
@ -231,10 +231,12 @@ class Items(object):
|
||||||
time=time,
|
time=time,
|
||||||
sound=False)
|
sound=False)
|
||||||
|
|
||||||
def updateUserdata(self, xml):
|
def updateUserdata(self, xml, viewtag=None, viewid=None):
|
||||||
"""
|
"""
|
||||||
Updates the Kodi watched state of the item from PMS. Also retrieves
|
Updates the Kodi watched state of the item from PMS. Also retrieves
|
||||||
Plex resume points for movies in progress.
|
Plex resume points for movies in progress.
|
||||||
|
|
||||||
|
viewtag and viewid only serve as dummies
|
||||||
"""
|
"""
|
||||||
for mediaitem in xml:
|
for mediaitem in xml:
|
||||||
API = PlexAPI.API(mediaitem)
|
API = PlexAPI.API(mediaitem)
|
||||||
|
@ -1256,12 +1258,13 @@ class TVShows(Items):
|
||||||
seriesId, seriesName, season, episode = API.getEpisodeDetails()
|
seriesId, seriesName, season, episode = API.getEpisodeDetails()
|
||||||
|
|
||||||
if season is None:
|
if season is None:
|
||||||
if item.get('AbsoluteEpisodeNumber'):
|
|
||||||
# Anime scenario
|
|
||||||
season = 1
|
|
||||||
episode = item['AbsoluteEpisodeNumber']
|
|
||||||
else:
|
|
||||||
season = -1
|
season = -1
|
||||||
|
# if item.get('AbsoluteEpisodeNumber'):
|
||||||
|
# # Anime scenario
|
||||||
|
# season = 1
|
||||||
|
# episode = item['AbsoluteEpisodeNumber']
|
||||||
|
# else:
|
||||||
|
# season = -1
|
||||||
|
|
||||||
# Specials ordering within season
|
# Specials ordering within season
|
||||||
if item.get('AirsAfterSeasonNumber'):
|
if item.get('AirsAfterSeasonNumber'):
|
||||||
|
|
|
@ -17,7 +17,7 @@ class GetKodiDB():
|
||||||
do stuff with kodi_db
|
do stuff with kodi_db
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
itemType: itemtype for Kodi DB, e.g. 'video'
|
itemType: itemtype for Kodi DB, e.g. 'video', 'music'
|
||||||
|
|
||||||
On exiting "with" (no matter what), commits get automatically committed
|
On exiting "with" (no matter what), commits get automatically committed
|
||||||
and the db gets closed
|
and the db gets closed
|
||||||
|
@ -701,6 +701,68 @@ class Kodidb_Functions():
|
||||||
)
|
)
|
||||||
cursor.execute(query, (fileid, 2, subtitletrack))
|
cursor.execute(query, (fileid, 2, subtitletrack))
|
||||||
|
|
||||||
|
def getResumes(self):
|
||||||
|
"""
|
||||||
|
VIDEOS
|
||||||
|
|
||||||
|
Returns all Kodi idFile that have a resume point set (not unwatched
|
||||||
|
ones or items that have already been completely watched)
|
||||||
|
"""
|
||||||
|
cursor = self.cursor
|
||||||
|
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT idFile",
|
||||||
|
"FROM bookmark"
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
rows = cursor.execute(query)
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
ids = []
|
||||||
|
for row in rows:
|
||||||
|
ids.append(row[0])
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def getUnplayedMusicItems(self):
|
||||||
|
"""
|
||||||
|
MUSIC
|
||||||
|
|
||||||
|
Returns all Kodi Item idFile that have not yet been completely played
|
||||||
|
"""
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT idPath",
|
||||||
|
"FROM song",
|
||||||
|
"WHERE iTimesPlayed IS NULL OR iTimesPlayed = ''"
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
rows = self.cursor.execute(query)
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
ids = []
|
||||||
|
for row in rows:
|
||||||
|
ids.append(row[0])
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def getUnplayedItems(self):
|
||||||
|
"""
|
||||||
|
VIDEOS
|
||||||
|
|
||||||
|
Returns all Kodi Item idFile that have not yet been completely played
|
||||||
|
"""
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT idFile",
|
||||||
|
"FROM files",
|
||||||
|
"WHERE playCount IS NULL OR playCount = ''"
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
rows = self.cursor.execute(query)
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
ids = []
|
||||||
|
for row in rows:
|
||||||
|
ids.append(row[0])
|
||||||
|
return ids
|
||||||
|
|
||||||
def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed):
|
def addPlaystate(self, fileid, resume_seconds, total_seconds, playcount, dateplayed):
|
||||||
|
|
||||||
cursor = self.cursor
|
cursor = self.cursor
|
||||||
|
|
|
@ -11,8 +11,7 @@ import downloadutils
|
||||||
import embydb_functions as embydb
|
import embydb_functions as embydb
|
||||||
import playbackutils as pbutils
|
import playbackutils as pbutils
|
||||||
import utils
|
import utils
|
||||||
|
from PlexFunctions import scrobble
|
||||||
from urllib import urlencode
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -144,16 +143,11 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
utils.window('emby_skipWatched%s' % itemid, clear=True)
|
utils.window('emby_skipWatched%s' % itemid, clear=True)
|
||||||
else:
|
else:
|
||||||
# notify the server
|
# notify the server
|
||||||
args = {'key': itemid,
|
|
||||||
'identifier': 'com.plexapp.plugins.library'}
|
|
||||||
if playcount != 0:
|
if playcount != 0:
|
||||||
url = "{server}/:/scrobble?" + urlencode(args)
|
scrobble(itemid, 'watched')
|
||||||
doUtils.downloadUrl(url, type="GET")
|
|
||||||
self.logMsg("Mark as watched for itemid: %s" % itemid, 1)
|
|
||||||
else:
|
else:
|
||||||
url = "{server}/:/unscrobble?" + urlencode(args)
|
scrobble(itemid, 'unwatched')
|
||||||
doUtils.downloadUrl(url, type="GET")
|
|
||||||
self.logMsg("Mark as unwatched for itemid: %s" % itemid, 1)
|
|
||||||
finally:
|
finally:
|
||||||
embycursor.close()
|
embycursor.close()
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
|
|
||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
import Queue
|
import Queue
|
||||||
|
try:
|
||||||
|
import xml.etree.cElementTree as etree
|
||||||
|
except ImportError:
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
@ -234,6 +238,12 @@ class LibrarySync(Thread):
|
||||||
self.enableBackgroundSync = True if utils.settings(
|
self.enableBackgroundSync = True if utils.settings(
|
||||||
'enableBackgroundSync') == "true" else False
|
'enableBackgroundSync') == "true" else False
|
||||||
|
|
||||||
|
# Time offset between Kodi and PMS in seconds (=Koditime - PMStime)
|
||||||
|
self.timeoffset = 0
|
||||||
|
# Time in seconds to look into the past when looking for PMS changes
|
||||||
|
# (safety margin - the larger, the more items we need to process)
|
||||||
|
self.syncPast = 30
|
||||||
|
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
|
|
||||||
def showKodiNote(self, message, forced=False, icon="plex"):
|
def showKodiNote(self, message, forced=False, icon="plex"):
|
||||||
|
@ -261,6 +271,180 @@ class LibrarySync(Thread):
|
||||||
time=7000,
|
time=7000,
|
||||||
sound=True)
|
sound=True)
|
||||||
|
|
||||||
|
def syncPMStime(self):
|
||||||
|
"""
|
||||||
|
PMS does not provide a means to get a server timestamp. This is a work-
|
||||||
|
around.
|
||||||
|
"""
|
||||||
|
self.logMsg('Synching time with PMS server', 0)
|
||||||
|
# Find a PMS item where we can toggle the view state to enforce a
|
||||||
|
# change in lastViewedAt
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
unplayedIds = kodi_db.getUnplayedItems()
|
||||||
|
resumeIds = kodi_db.getResumes()
|
||||||
|
self.logMsg('resumeIds: %s' % resumeIds, 1)
|
||||||
|
|
||||||
|
plexId = False
|
||||||
|
for unplayedId in unplayedIds:
|
||||||
|
if unplayedId not in resumeIds:
|
||||||
|
# Found an item we can work with!
|
||||||
|
kodiId = unplayedId
|
||||||
|
self.logMsg('Found kodiId: %s' % kodiId, 1)
|
||||||
|
# Get Plex ID using the Kodi ID
|
||||||
|
with embydb.GetEmbyDB() as emby_db:
|
||||||
|
plexId = emby_db.getItem_byFileId(kodiId)
|
||||||
|
if plexId:
|
||||||
|
self.logMsg('Found plexId: %s' % plexId, 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Try getting a music item if we did not find a video item
|
||||||
|
if not plexId:
|
||||||
|
self.logMsg("Could not find a video item to sync time with", 0)
|
||||||
|
with kodidb.GetKodiDB('music') as kodi_db:
|
||||||
|
unplayedIds = kodi_db.getUnplayedMusicItems()
|
||||||
|
# We don't care about resuming songs in the middle
|
||||||
|
for unplayedId in unplayedIds:
|
||||||
|
# Found an item we can work with!
|
||||||
|
kodiId = unplayedId
|
||||||
|
self.logMsg('Found kodiId: %s' % kodiId, 1)
|
||||||
|
# Get Plex ID using the Kodi ID
|
||||||
|
with embydb.GetEmbyDB() as emby_db:
|
||||||
|
plexId = emby_db.getItem_byFileId(kodiId)
|
||||||
|
if plexId:
|
||||||
|
self.logMsg('Found plexId: %s' % plexId, 1)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.logMsg("Could not find an item to sync time with", -1)
|
||||||
|
self.logMsg("Aborting PMS-Kodi time sync", -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the Plex item's metadata
|
||||||
|
xml = PlexFunctions.GetPlexMetadata(plexId)
|
||||||
|
if not xml:
|
||||||
|
self.logMsg("Could not download metadata, aborting time sync", -1)
|
||||||
|
return
|
||||||
|
libraryId = xml[0].attrib['librarySectionID']
|
||||||
|
# Get a PMS timestamp to start our question with
|
||||||
|
timestamp = xml[0].attrib.get('lastViewedAt')
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = xml[0].attrib.get('updatedAt')
|
||||||
|
self.logMsg('Using items updatedAt=%s' % timestamp, 1)
|
||||||
|
if not timestamp:
|
||||||
|
timestamp = xml[0].attrib.get('addedAt')
|
||||||
|
self.logMsg('Using items addedAt=%s' % timestamp, 1)
|
||||||
|
# Set the timer
|
||||||
|
koditime = utils.getUnixTimestamp()
|
||||||
|
# Toggle watched state
|
||||||
|
PlexFunctions.scrobble(plexId, 'watched')
|
||||||
|
# Let the PMS process this first!
|
||||||
|
xbmc.sleep(2000)
|
||||||
|
# Get all PMS items to find the item we changed
|
||||||
|
items = PlexFunctions.GetAllPlexLeaves(libraryId,
|
||||||
|
lastViewedAt=timestamp)
|
||||||
|
# Toggle watched state back
|
||||||
|
PlexFunctions.scrobble(plexId, 'unwatched')
|
||||||
|
# Get server timestamp for this change
|
||||||
|
plextime = None
|
||||||
|
for item in items:
|
||||||
|
if item.attrib['ratingKey'] == plexId:
|
||||||
|
plextime = item.attrib.get('lastViewedAt')
|
||||||
|
break
|
||||||
|
if not plextime:
|
||||||
|
self.logMsg("Could not set the items watched state, abort", -1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate time offset Kodi-PMS
|
||||||
|
self.timeoffset = int(koditime) - int(plextime)
|
||||||
|
self.logMsg("Time offset Koditime - PMStime in seconds: %s"
|
||||||
|
% str(self.timeoffset), 0)
|
||||||
|
|
||||||
|
def getPMSfromKodiTime(self, koditime):
|
||||||
|
"""
|
||||||
|
Uses self.timeoffset to return the PMS time for a given Kodi timestamp
|
||||||
|
(in unix time)
|
||||||
|
|
||||||
|
Feed with integers
|
||||||
|
"""
|
||||||
|
return koditime - self.timeoffset
|
||||||
|
|
||||||
|
def resetProcessedItems(self):
|
||||||
|
"""
|
||||||
|
Resets the list of PMS items that we have already processed
|
||||||
|
"""
|
||||||
|
self.processed = {
|
||||||
|
'movie': {},
|
||||||
|
'show': {},
|
||||||
|
'season': {},
|
||||||
|
'episode': {},
|
||||||
|
'artist': {},
|
||||||
|
'album': {},
|
||||||
|
'track': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getFastUpdateList(self, xml, plexType, viewName, viewId):
|
||||||
|
"""
|
||||||
|
THIS METHOD NEEDS TO BE FAST! => e.g. no API calls
|
||||||
|
|
||||||
|
Adds items to self.updatelist as well as self.allPlexElementsId dict
|
||||||
|
|
||||||
|
Input:
|
||||||
|
xml: PMS answer for section items
|
||||||
|
plexType: 'movie', 'show', 'episode', ...
|
||||||
|
viewName: Name of the Plex view (e.g. 'My TV shows')
|
||||||
|
viewId: Id/Key of Plex library (e.g. '1')
|
||||||
|
|
||||||
|
Output: self.updatelist, self.allPlexElementsId
|
||||||
|
self.updatelist APPENDED(!!) list itemids (Plex Keys as
|
||||||
|
as received from API.getRatingKey())
|
||||||
|
One item in this list is of the form:
|
||||||
|
'itemId': xxx,
|
||||||
|
'itemType': 'Movies','TVShows', ...
|
||||||
|
'method': 'add_update', 'add_updateSeason', ...
|
||||||
|
'viewName': xxx,
|
||||||
|
'viewId': xxx,
|
||||||
|
'title': xxx
|
||||||
|
|
||||||
|
self.allPlexElementsId APPENDED(!!) dict
|
||||||
|
= {itemid: checksum}
|
||||||
|
"""
|
||||||
|
# Needs to call other methods than if we're only updating userdata
|
||||||
|
for item in xml:
|
||||||
|
itemId = item.attrib.get('ratingKey')
|
||||||
|
# Skipping items 'title=All episodes' without a 'ratingKey'
|
||||||
|
if not itemId:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lastViewedAt = item.attrib.get('lastViewedAt')
|
||||||
|
updatedAt = item.attrib.get('updatedAt')
|
||||||
|
|
||||||
|
# returns the tuple (lastViewedAt, updatedAt) for the
|
||||||
|
# specific item
|
||||||
|
res = self.processed[plexType].get(itemId)
|
||||||
|
if res:
|
||||||
|
# Only look at the updatedAt flag!
|
||||||
|
# tuple: (lastViewedAt, updatedAt)
|
||||||
|
if res[1] == updatedAt:
|
||||||
|
# Nothing to update, we have already processed this
|
||||||
|
# item
|
||||||
|
continue
|
||||||
|
title = item.attrib.get('title', 'Missing Title Name')
|
||||||
|
# We need to process this:
|
||||||
|
self.updatelist.append({
|
||||||
|
'itemId': itemId,
|
||||||
|
'itemType': PlexFunctions.GetItemClassFromType(
|
||||||
|
plexType),
|
||||||
|
'method': PlexFunctions.GetMethodFromPlexType(plexType),
|
||||||
|
'viewName': viewName,
|
||||||
|
'viewId': viewId,
|
||||||
|
'title': title
|
||||||
|
})
|
||||||
|
# And safe to self.processed:
|
||||||
|
self.processed[plexType][itemId] = (lastViewedAt, updatedAt)
|
||||||
|
# Quickly log
|
||||||
|
if self.updatelist:
|
||||||
|
self.logMsg('fastSync updatelist: %s' % self.updatelist, 1)
|
||||||
|
self.logMsg('fastSync processed list: %s' % self.processed, 1)
|
||||||
|
|
||||||
def fastSync(self):
|
def fastSync(self):
|
||||||
"""
|
"""
|
||||||
Fast incremential lib sync
|
Fast incremential lib sync
|
||||||
|
@ -271,24 +455,24 @@ class LibrarySync(Thread):
|
||||||
This will NOT remove items from Kodi db that were removed from the PMS
|
This will NOT remove items from Kodi db that were removed from the PMS
|
||||||
(happens only during fullsync)
|
(happens only during fullsync)
|
||||||
|
|
||||||
Currently, ALL items returned by the PMS (because they've just been
|
Items that are processed are appended to the dict self.processed:
|
||||||
edited by the PMS or have been watched) will be processed. This will
|
{
|
||||||
probably happen several times.
|
'<Plex itemtype>': e.g. 'movie'
|
||||||
|
{
|
||||||
|
'<ratingKey>': ( unique plex id 'ratingKey' as str
|
||||||
|
lastViewedAt,
|
||||||
|
updatedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
self.compare = True
|
# Get last sync time and look a bit in the past (safety margin)
|
||||||
# Get last sync time
|
|
||||||
lastSync = self.lastSync - self.syncPast
|
lastSync = self.lastSync - self.syncPast
|
||||||
if not lastSync:
|
|
||||||
# Original Emby format:
|
|
||||||
# lastSync = "2016-01-01T00:00:00Z"
|
|
||||||
# January 1, 2015 at midnight:
|
|
||||||
lastSync = 1420070400
|
|
||||||
# Set new timestamp NOW because sync might take a while
|
# Set new timestamp NOW because sync might take a while
|
||||||
self.saveLastSync()
|
self.saveLastSync()
|
||||||
|
|
||||||
# Original idea: Get all PMS items already saved in Kodi
|
# Original idea: Get all PMS items already saved in Kodi
|
||||||
# Also get checksums of every Plex items already saved in Kodi
|
# Also get checksums of every Plex items already saved in Kodi
|
||||||
# NEW idea: process every item returned by the PMS
|
|
||||||
self.allKodiElementsId = {}
|
self.allKodiElementsId = {}
|
||||||
|
|
||||||
# Run through views and get latest changed elements using time diff
|
# Run through views and get latest changed elements using time diff
|
||||||
|
@ -296,39 +480,94 @@ class LibrarySync(Thread):
|
||||||
self.updateKodiMusicLib = False
|
self.updateKodiMusicLib = False
|
||||||
for view in self.views:
|
for view in self.views:
|
||||||
self.updatelist = []
|
self.updatelist = []
|
||||||
if self.threadStopped():
|
|
||||||
return True
|
|
||||||
# Get items per view
|
# Get items per view
|
||||||
items = PlexFunctions.GetAllPlexLeaves(view['id'],
|
items = PlexFunctions.GetAllPlexLeaves(
|
||||||
updatedAt=lastSync)
|
view['id'], updatedAt=self.getPMSfromKodiTime(lastSync))
|
||||||
# Just skip item if something went wrong
|
# Just skip if something went wrong
|
||||||
if not items:
|
if not items:
|
||||||
continue
|
continue
|
||||||
# Get one itemtype, because they're the same in the PMS section
|
# Get one itemtype, because they're the same in the PMS section
|
||||||
|
try:
|
||||||
plexType = items[0].attrib['type']
|
plexType = items[0].attrib['type']
|
||||||
|
except:
|
||||||
|
# There was no child - PMS response is empty
|
||||||
|
continue
|
||||||
# Populate self.updatelist
|
# Populate self.updatelist
|
||||||
self.GetUpdatelist(items,
|
self.getFastUpdateList(
|
||||||
PlexFunctions.GetItemClassFromType(plexType),
|
items, plexType, view['name'], view['id'])
|
||||||
PlexFunctions.GetMethodFromPlexType(plexType),
|
|
||||||
view['name'],
|
|
||||||
view['id'])
|
|
||||||
# Process self.updatelist
|
# Process self.updatelist
|
||||||
if self.updatelist:
|
if self.updatelist:
|
||||||
if self.updatelist[0]['itemType'] in ['Movies', 'TVShows']:
|
if self.updatelist[0]['itemType'] in ['Movies', 'TVShows']:
|
||||||
self.updateKodiVideoLib = True
|
self.updateKodiVideoLib = True
|
||||||
elif self.updatelist[0]['itemType'] == 'Music':
|
elif self.updatelist[0]['itemType'] == 'Music':
|
||||||
self.updateKodiMusicLib = True
|
self.updateKodiMusicLib = True
|
||||||
|
# Do the work
|
||||||
self.GetAndProcessXMLs(
|
self.GetAndProcessXMLs(
|
||||||
PlexFunctions.GetItemClassFromType(plexType),
|
PlexFunctions.GetItemClassFromType(plexType),
|
||||||
showProgress=False)
|
showProgress=False)
|
||||||
|
|
||||||
self.updatelist = []
|
self.updatelist = []
|
||||||
|
|
||||||
# Update userdata
|
# Update userdata DIRECTLY
|
||||||
|
# We don't need to refresh the Kodi library for deltas!!
|
||||||
|
# Start with an empty ElementTree and attach items to update
|
||||||
|
movieupdate = False
|
||||||
|
episodeupdate = False
|
||||||
|
songupdate = False
|
||||||
for view in self.views:
|
for view in self.views:
|
||||||
self.PlexUpdateWatched(
|
items = PlexFunctions.GetAllPlexLeaves(
|
||||||
view['id'],
|
view['id'], lastViewedAt=self.getPMSfromKodiTime(lastSync))
|
||||||
PlexFunctions.GetItemClassFromType(view['itemtype']),
|
for item in items:
|
||||||
lastViewedAt=lastSync)
|
itemId = item.attrib.get('ratingKey')
|
||||||
|
# Skipping items 'title=All episodes' without a 'ratingKey'
|
||||||
|
if not itemId:
|
||||||
|
continue
|
||||||
|
plexType = item.attrib['type']
|
||||||
|
lastViewedAt = item.attrib.get('lastViewedAt')
|
||||||
|
updatedAt = item.attrib.get('updatedAt')
|
||||||
|
|
||||||
|
# returns the tuple (lastViewedAt, updatedAt) for the
|
||||||
|
# specific item
|
||||||
|
res = self.processed[plexType].get(itemId)
|
||||||
|
if res:
|
||||||
|
# Only look at lastViewedAt
|
||||||
|
if res[0] == lastViewedAt:
|
||||||
|
# Nothing to update, we have already processed this
|
||||||
|
# item
|
||||||
|
continue
|
||||||
|
if plexType == 'movie':
|
||||||
|
movieupdate = True
|
||||||
|
try:
|
||||||
|
movieXML.append(item)
|
||||||
|
except:
|
||||||
|
movieXML = etree.Element('root')
|
||||||
|
movieXML.append(item)
|
||||||
|
elif plexType == 'episode':
|
||||||
|
episodeupdate = True
|
||||||
|
try:
|
||||||
|
episodeXML.append(item)
|
||||||
|
except:
|
||||||
|
episodeXML = etree.Element('root')
|
||||||
|
episodeXML.append(item)
|
||||||
|
elif plexType == 'track':
|
||||||
|
songupdate = True
|
||||||
|
try:
|
||||||
|
musicXML.append(item)
|
||||||
|
except:
|
||||||
|
musicXML = etree.Element('root')
|
||||||
|
musicXML.append(item)
|
||||||
|
# And safe to self.processed:
|
||||||
|
self.processed[plexType][itemId] = (lastViewedAt, updatedAt)
|
||||||
|
|
||||||
|
if movieupdate:
|
||||||
|
with itemtypes.Movies() as movies:
|
||||||
|
movies.updateUserdata(movieXML)
|
||||||
|
if episodeupdate:
|
||||||
|
with itemtypes.TVShows() as tvshows:
|
||||||
|
tvshows.updateUserdata(episodeXML)
|
||||||
|
if songupdate:
|
||||||
|
with itemtypes.Music() as music:
|
||||||
|
music.updateUserdata(musicXML)
|
||||||
|
|
||||||
# Let Kodi update the library now (artwork and userdata)
|
# Let Kodi update the library now (artwork and userdata)
|
||||||
if self.updateKodiVideoLib:
|
if self.updateKodiVideoLib:
|
||||||
|
@ -338,8 +577,6 @@ class LibrarySync(Thread):
|
||||||
self.logMsg("Doing Kodi Music Lib update", 1)
|
self.logMsg("Doing Kodi Music Lib update", 1)
|
||||||
xbmc.executebuiltin('UpdateLibrary(music)')
|
xbmc.executebuiltin('UpdateLibrary(music)')
|
||||||
|
|
||||||
# Reset and return
|
|
||||||
self.allPlexElementsId = {}
|
|
||||||
# Show warning if itemtypes.py crashed at some point
|
# Show warning if itemtypes.py crashed at some point
|
||||||
if utils.window('plex_scancrashed') == 'true':
|
if utils.window('plex_scancrashed') == 'true':
|
||||||
xbmcgui.Dialog().ok(self.addonName, self.__language__(39408))
|
xbmcgui.Dialog().ok(self.addonName, self.__language__(39408))
|
||||||
|
@ -1168,6 +1405,9 @@ class LibrarySync(Thread):
|
||||||
count = 0
|
count = 0
|
||||||
errorcount = 0
|
errorcount = 0
|
||||||
|
|
||||||
|
# Initialize self.processed
|
||||||
|
self.resetProcessedItems()
|
||||||
|
|
||||||
log("---===### Starting LibrarySync ###===---", 0)
|
log("---===### Starting LibrarySync ###===---", 0)
|
||||||
while not threadStopped():
|
while not threadStopped():
|
||||||
|
|
||||||
|
@ -1223,6 +1463,8 @@ class LibrarySync(Thread):
|
||||||
log("Db version: %s" % settings('dbCreatedWithVersion'), 0)
|
log("Db version: %s" % settings('dbCreatedWithVersion'), 0)
|
||||||
log("Initial start-up full sync starting", 0)
|
log("Initial start-up full sync starting", 0)
|
||||||
librarySync = fullSync(manualrun=True)
|
librarySync = fullSync(manualrun=True)
|
||||||
|
# Initialize time offset Kodi - PMS
|
||||||
|
self.syncPMStime()
|
||||||
window('emby_dbScan', clear=True)
|
window('emby_dbScan', clear=True)
|
||||||
if librarySync:
|
if librarySync:
|
||||||
log("Initial start-up full sync successful", 0)
|
log("Initial start-up full sync successful", 0)
|
||||||
|
@ -1282,6 +1524,10 @@ class LibrarySync(Thread):
|
||||||
# Run full lib scan approx every 30min
|
# Run full lib scan approx every 30min
|
||||||
if count >= 1800:
|
if count >= 1800:
|
||||||
count = 0
|
count = 0
|
||||||
|
# Also reset self.processed, just in case
|
||||||
|
self.resetProcessedItems()
|
||||||
|
# Recalculate time offset Kodi - PMS
|
||||||
|
self.syncPMStime()
|
||||||
window('emby_dbScan', value="true")
|
window('emby_dbScan', value="true")
|
||||||
log('Running background full lib scan', 0)
|
log('Running background full lib scan', 0)
|
||||||
fullSync(manualrun=True)
|
fullSync(manualrun=True)
|
||||||
|
|
|
@ -19,12 +19,88 @@ import xbmcaddon
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcvfs
|
import xbmcvfs
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
addonName = xbmcaddon.Addon().getAddonInfo('name')
|
addonName = xbmcaddon.Addon().getAddonInfo('name')
|
||||||
|
|
||||||
|
|
||||||
|
def DateToKodi(stamp):
|
||||||
|
"""
|
||||||
|
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
||||||
|
propper, human-readable time stamp used by Kodi
|
||||||
|
|
||||||
|
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
||||||
|
"""
|
||||||
|
# DATEFORMAT = xbmc.getRegion('dateshort')
|
||||||
|
# TIMEFORMAT = xbmc.getRegion('meridiem')
|
||||||
|
# date_time = time.localtime(stamp)
|
||||||
|
# if DATEFORMAT[1] == 'd':
|
||||||
|
# localdate = time.strftime('%d-%m-%Y', date_time)
|
||||||
|
# elif DATEFORMAT[1] == 'm':
|
||||||
|
# localdate = time.strftime('%m-%d-%Y', date_time)
|
||||||
|
# else:
|
||||||
|
# localdate = time.strftime('%Y-%m-%d', date_time)
|
||||||
|
# if TIMEFORMAT != '/':
|
||||||
|
# localtime = time.strftime('%I:%M%p', date_time)
|
||||||
|
# else:
|
||||||
|
# localtime = time.strftime('%H:%M', date_time)
|
||||||
|
# return localtime + ' ' + localdate
|
||||||
|
try:
|
||||||
|
# DATEFORMAT = xbmc.getRegion('dateshort')
|
||||||
|
# TIMEFORMAT = xbmc.getRegion('meridiem')
|
||||||
|
date_time = time.localtime(float(stamp))
|
||||||
|
localdate = time.strftime('%Y-%m-%d %H:%M:%S', date_time)
|
||||||
|
except:
|
||||||
|
localdate = None
|
||||||
|
return localdate
|
||||||
|
|
||||||
|
|
||||||
|
def changePlayState(itemType, kodiId, playCount, lastplayed):
|
||||||
|
"""
|
||||||
|
YET UNUSED
|
||||||
|
|
||||||
|
kodiId: int or str
|
||||||
|
playCount: int or str
|
||||||
|
lastplayed: str or int unix timestamp
|
||||||
|
"""
|
||||||
|
logMsg("changePlayState", "start", 1)
|
||||||
|
lastplayed = DateToKodi(lastplayed)
|
||||||
|
|
||||||
|
kodiId = int(kodiId)
|
||||||
|
playCount = int(playCount)
|
||||||
|
method = {
|
||||||
|
'movie': ' VideoLibrary.SetMovieDetails',
|
||||||
|
'episode': 'VideoLibrary.SetEpisodeDetails',
|
||||||
|
'musicvideo': ' VideoLibrary.SetMusicVideoDetails', # TODO
|
||||||
|
'show': 'VideoLibrary.SetTVShowDetails', # TODO
|
||||||
|
'': 'AudioLibrary.SetAlbumDetails', # TODO
|
||||||
|
'': 'AudioLibrary.SetArtistDetails', # TODO
|
||||||
|
'track': 'AudioLibrary.SetSongDetails'
|
||||||
|
}
|
||||||
|
params = {
|
||||||
|
'movie': {
|
||||||
|
'movieid': kodiId,
|
||||||
|
'playcount': playCount,
|
||||||
|
'lastplayed': lastplayed
|
||||||
|
},
|
||||||
|
'episode': {
|
||||||
|
'episodeid': kodiId,
|
||||||
|
'playcount': playCount,
|
||||||
|
'lastplayed': lastplayed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
}
|
||||||
|
query['method'] = method[itemType]
|
||||||
|
query['params'] = params[itemType]
|
||||||
|
result = xbmc.executeJSONRPC(json.dumps(query))
|
||||||
|
result = json.loads(result)
|
||||||
|
result = result.get('result')
|
||||||
|
logMsg("changePlayState", "JSON result was: %s" % result, 1)
|
||||||
|
|
||||||
|
|
||||||
def IfExists(path):
|
def IfExists(path):
|
||||||
"""
|
"""
|
||||||
Kodi's xbmcvfs.exists is broken - it caches the results for directories.
|
Kodi's xbmcvfs.exists is broken - it caches the results for directories.
|
||||||
|
|
Loading…
Reference in a new issue