PlexKodiConnect/resources/lib/librarysync.py

1755 lines
69 KiB
Python
Raw Normal View History

2015-12-25 07:07:00 +11:00
# -*- coding: utf-8 -*-
2016-02-12 00:03:04 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
2016-02-12 00:03:04 +11:00
from threading import Thread, Lock
import Queue
import xml.etree.ElementTree as etree
2015-12-25 07:07:00 +11:00
import xbmc
import xbmcgui
import xbmcvfs
2016-03-08 21:47:46 +11:00
import xbmcaddon
2015-12-25 07:07:00 +11:00
import utils
import clientinfo
import downloadutils
import itemtypes
import embydb_functions as embydb
import kodidb_functions as kodidb
import read_embyserver as embyserver
import userclient
import videonodes
2016-03-25 04:52:02 +11:00
import PlexFunctions as PF
2015-12-29 04:47:16 +11:00
2016-02-12 00:03:04 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
@utils.logging
2016-01-30 06:07:21 +11:00
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
2016-01-27 01:13:03 +11:00
@utils.ThreadMethods
2016-02-12 00:03:04 +11:00
class ThreadedGetMetadata(Thread):
"""
Threaded download of Plex XML metadata for a certain library item.
Fills the out_queue with the downloaded etree XML objects
Input:
queue Queue.Queue() object that you'll need to fill up
with Plex itemIds
2016-02-12 00:03:04 +11:00
out_queue Queue() object where this thread will store
the downloaded metadata XMLs as etree objects
2016-02-12 00:03:04 +11:00
lock Lock(), used for counting where we are
"""
def __init__(self, queue, out_queue, lock, processlock):
self.queue = queue
self.out_queue = out_queue
self.lock = lock
self.processlock = processlock
2016-02-12 00:03:04 +11:00
Thread.__init__(self)
def run(self):
2016-01-28 01:14:30 +11:00
# cache local variables because it's faster
queue = self.queue
out_queue = self.out_queue
lock = self.lock
processlock = self.processlock
2016-01-28 01:14:30 +11:00
threadStopped = self.threadStopped
global getMetadataCount
global processMetadataCount
2016-01-30 18:43:28 +11:00
while threadStopped() is False:
# grabs Plex item from queue
try:
updateItem = queue.get(block=False)
# Empty queue
except Queue.Empty:
2016-02-10 19:14:31 +11:00
xbmc.sleep(100)
2016-01-30 18:43:28 +11:00
continue
# Download Metadata
2016-03-25 04:52:02 +11:00
plexXML = PF.GetPlexMetadata(updateItem['itemId'])
2016-02-07 22:38:50 +11:00
if plexXML is None:
# Did not receive a valid XML - skip that item for now
self.logMsg("Could not get metadata for %s. "
"Skipping that item for now"
% updateItem['itemId'], 0)
# Increase BOTH counters - since metadata won't be processed
with lock:
getMetadataCount += 1
with processlock:
processMetadataCount += 1
2016-01-30 06:07:21 +11:00
queue.task_done()
2016-01-30 18:43:28 +11:00
continue
updateItem['XML'] = plexXML
# place item into out queue
out_queue.put(updateItem)
# Keep track of where we are at
with lock:
getMetadataCount += 1
# signals to queue job is done
queue.task_done()
2016-01-30 06:07:21 +11:00
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
2016-01-27 01:13:03 +11:00
@utils.ThreadMethods
2016-02-12 00:03:04 +11:00
class ThreadedProcessMetadata(Thread):
"""
Not yet implemented - if ever. Only to be called by ONE thread!
Processes the XML metadata in the queue
Input:
queue: Queue.Queue() object that you'll need to fill up with
the downloaded XML eTree objects
2016-01-11 17:55:22 +11:00
itemType: as used to call functions in itemtypes.py
e.g. 'Movies' => itemtypes.Movies()
2016-02-12 00:03:04 +11:00
lock: Lock(), used for counting where we are
"""
2016-01-30 18:43:28 +11:00
def __init__(self, queue, itemType, lock):
self.queue = queue
self.lock = lock
2016-01-11 17:55:22 +11:00
self.itemType = itemType
2016-02-12 00:03:04 +11:00
Thread.__init__(self)
def run(self):
# Constructs the method name, e.g. itemtypes.Movies
2016-01-11 17:55:22 +11:00
itemFkt = getattr(itemtypes, self.itemType)
2016-01-28 01:14:30 +11:00
# cache local variables because it's faster
queue = self.queue
lock = self.lock
threadStopped = self.threadStopped
global processMetadataCount
2016-01-11 17:55:22 +11:00
global processingViewName
2016-01-30 18:43:28 +11:00
with itemFkt() as item:
while threadStopped() is False:
# grabs item from queue
try:
updateItem = queue.get(block=False)
except Queue.Empty:
2016-02-10 19:14:31 +11:00
xbmc.sleep(100)
2016-01-30 18:43:28 +11:00
continue
# Do the work
2016-01-30 18:43:28 +11:00
plexitem = updateItem['XML']
method = updateItem['method']
viewName = updateItem['viewName']
viewId = updateItem['viewId']
title = updateItem['title']
itemSubFkt = getattr(item, method)
# Get the one child entry in the xml and process
for child in plexitem:
itemSubFkt(child,
viewtag=viewName,
viewid=viewId)
# Keep track of where we are at
2016-01-30 18:43:28 +11:00
with lock:
processMetadataCount += 1
processingViewName = title
# signals to queue job is done
queue.task_done()
2016-01-30 06:07:21 +11:00
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
2016-01-27 01:13:03 +11:00
@utils.ThreadMethods
2016-02-12 00:03:04 +11:00
class ThreadedShowSyncInfo(Thread):
"""
Threaded class to show the Kodi statusbar of the metadata download.
Input:
dialog xbmcgui.DialogProgressBG() object to show progress
locks = [downloadLock, processLock] Locks() to the other threads
total: Total number of items to get
"""
2016-01-27 01:13:03 +11:00
def __init__(self, dialog, locks, total, itemType):
self.locks = locks
self.total = total
self.addonName = clientinfo.ClientInfo().getAddonName()
self.dialog = dialog
2016-01-11 17:55:22 +11:00
self.itemType = itemType
2016-02-12 00:03:04 +11:00
Thread.__init__(self)
def run(self):
2016-01-28 01:14:30 +11:00
# cache local variables because it's faster
total = self.total
2016-01-28 01:14:30 +11:00
dialog = self.dialog
threadStopped = self.threadStopped
downloadLock = self.locks[0]
processLock = self.locks[1]
2016-03-08 22:13:47 +11:00
dialog.create("%s: Sync %s: %s items"
2016-03-07 23:38:45 +11:00
% (self.addonName,
self.itemType,
2016-03-08 22:13:47 +11:00
str(total)),
2016-01-28 01:14:30 +11:00
"Starting")
global getMetadataCount
global processMetadataCount
2016-01-11 17:55:22 +11:00
global processingViewName
total = 2 * total
totalProgress = 0
2016-01-28 01:14:30 +11:00
while threadStopped() is False:
with downloadLock:
getMetadataProgress = getMetadataCount
2016-01-11 17:55:22 +11:00
with processLock:
processMetadataProgress = processMetadataCount
viewName = processingViewName
totalProgress = getMetadataProgress + processMetadataProgress
try:
percentage = int(float(totalProgress) / float(total)*100.0)
except ZeroDivisionError:
percentage = 0
2016-03-08 22:13:47 +11:00
dialog.update(percentage,
message="Downloaded: %s. Processed: %s: %s"
% (getMetadataProgress,
processMetadataProgress,
viewName))
# Sleep for x milliseconds
2016-01-12 06:24:14 +11:00
xbmc.sleep(500)
2016-01-28 01:14:30 +11:00
dialog.close()
2016-01-27 03:20:13 +11:00
@utils.logging
2016-01-27 22:18:54 +11:00
@utils.ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
2016-01-30 06:07:21 +11:00
@utils.ThreadMethodsAdditionalStop('emby_shouldStop')
2016-01-27 01:13:03 +11:00
@utils.ThreadMethods
2016-02-12 00:03:04 +11:00
class LibrarySync(Thread):
2016-03-25 04:52:02 +11:00
"""
librarysync.LibrarySync(queue)
where (communication with websockets)
queue: Queue object for background sync
"""
2016-02-11 22:56:57 +11:00
# Borg, even though it's planned to only have 1 instance up and running!
2015-12-25 07:07:00 +11:00
_shared_state = {}
2016-02-11 22:54:15 +11:00
# How long should we look into the past for fast syncing items (in s)
syncPast = 30
2015-12-25 07:07:00 +11:00
2016-03-25 04:52:02 +11:00
def __init__(self, queue):
2015-12-25 07:07:00 +11:00
self.__dict__ = self._shared_state
2016-03-08 21:47:46 +11:00
self.__language__ = xbmcaddon.Addon().getLocalizedString
2016-03-25 04:52:02 +11:00
# Communication with websockets
self.queue = queue
self.itemsToProcess = []
self.safteyMargin = 30
2015-12-25 07:07:00 +11:00
self.clientInfo = clientinfo.ClientInfo()
self.user = userclient.UserClient()
self.emby = embyserver.Read_EmbyServer()
self.vnodes = videonodes.VideoNodes()
self.syncThreadNumber = int(utils.settings('syncThreadNumber'))
2016-01-28 06:41:28 +11:00
self.installSyncDone = True if \
utils.settings('SyncInstallRunDone') == 'true' else False
2016-02-11 22:54:15 +11:00
self.showDbSync = True if \
utils.settings('dbSyncIndicator') == 'true' else False
self.enableMusic = True if utils.settings('enableMusic') == "true" \
else False
self.enableBackgroundSync = True if utils.settings(
2016-03-08 22:13:47 +11:00
'enableBackgroundSync') == "true" else False
self.limitindex = int(utils.settings('limitindex'))
2015-12-25 07:07:00 +11:00
if utils.settings('emby_pathverified') == 'true':
utils.window('emby_pathverified', value='true')
2016-03-12 00:42:14 +11:00
# 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
2016-02-12 00:03:04 +11:00
Thread.__init__(self)
2015-12-25 07:07:00 +11:00
def showKodiNote(self, message, forced=False, icon="plex"):
2016-02-12 00:44:11 +11:00
"""
2016-03-08 22:13:47 +11:00
Shows a Kodi popup, if user selected to do so. Pass message in unicode
or string
icon: "plex": shows Plex icon
"error": shows Kodi error icon
2016-02-12 00:44:11 +11:00
"""
if not (self.showDbSync or forced):
return
if icon == "plex":
xbmcgui.Dialog().notification(
heading=self.addonName,
message=message,
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
time=5000,
sound=False)
elif icon == "error":
xbmcgui.Dialog().notification(
heading=self.addonName,
message=message,
icon=xbmcgui.NOTIFICATION_ERROR,
time=7000,
sound=True)
2016-02-12 00:44:11 +11:00
2016-03-12 00:42:14 +11:00
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:
2016-03-18 02:03:02 +11:00
plexId = emby_db.getItem_byFileId(kodiId, 'movie')
if plexId:
self.logMsg('Found movie plexId: %s' % plexId, 1)
break
else:
plexId = emby_db.getItem_byFileId(kodiId, 'episode')
if plexId:
self.logMsg('Found episode plexId: %s' % plexId, 1)
break
2016-03-12 00:42:14 +11:00
# 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:
2016-03-18 02:03:02 +11:00
plexId = emby_db.getMusicItem_byFileId(kodiId, 'song')
2016-03-12 00:42:14 +11:00
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
2016-03-25 04:52:02 +11:00
xml = PF.GetPlexMetadata(plexId)
if xml is None:
2016-03-12 00:42:14 +11:00
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
2016-03-25 04:52:02 +11:00
PF.scrobble(plexId, 'watched')
2016-03-12 00:42:14 +11:00
# Let the PMS process this first!
xbmc.sleep(2000)
# Get all PMS items to find the item we changed
2016-03-25 04:52:02 +11:00
items = PF.GetAllPlexLeaves(libraryId,
lastViewedAt=timestamp,
containerSize=self.limitindex)
2016-03-12 00:42:14 +11:00
# Toggle watched state back
2016-03-25 04:52:02 +11:00
PF.scrobble(plexId, 'unwatched')
2016-03-12 00:42:14 +11:00
# 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
2016-03-11 04:34:11 +11:00
def resetProcessedItems(self):
"""
Resets the list of PMS items that we have already processed
"""
self.processed = {
'movie': {},
'show': {},
'season': {},
'episode': {},
'artist': {},
'album': {},
'track': {}
}
2016-03-12 00:42:14 +11:00
def getFastUpdateList(self, xml, plexType, viewName, viewId):
2016-03-11 04:34:11 +11:00
"""
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}
"""
2016-03-12 00:42:14 +11:00
# 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
2016-03-11 04:34:11 +11:00
2016-03-12 00:42:14 +11:00
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
2016-03-11 04:34:11 +11:00
continue
2016-03-12 00:42:14 +11:00
title = item.attrib.get('title', 'Missing Title Name')
# We need to process this:
self.updatelist.append({
'itemId': itemId,
2016-03-25 04:52:02 +11:00
'itemType': PF.GetItemClassFromType(
2016-03-12 00:42:14 +11:00
plexType),
2016-03-25 04:52:02 +11:00
'method': PF.GetMethodFromPlexType(plexType),
2016-03-12 00:42:14 +11:00
'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)
2016-03-11 04:34:11 +11:00
2016-01-12 03:37:01 +11:00
def fastSync(self):
2016-01-28 01:14:30 +11:00
"""
Fast incremential lib sync
2016-01-12 03:37:01 +11:00
2016-01-28 01:14:30 +11:00
Using /library/recentlyAdded is NOT working as changes to lib items are
not reflected
This will NOT remove items from Kodi db that were removed from the PMS
(happens only during fullsync)
2016-03-11 04:34:11 +11:00
Items that are processed are appended to the dict self.processed:
{
'<Plex itemtype>': e.g. 'movie'
{
'<ratingKey>': ( unique plex id 'ratingKey' as str
lastViewedAt,
updatedAt
)
}
}
2016-01-28 01:14:30 +11:00
"""
2016-03-12 00:42:14 +11:00
# Get last sync time and look a bit in the past (safety margin)
2016-01-30 06:07:21 +11:00
lastSync = self.lastSync - self.syncPast
# Set new timestamp NOW because sync might take a while
self.saveLastSync()
2016-01-28 01:14:30 +11:00
# Original idea: Get all PMS items already saved in Kodi
# Also get checksums of every Plex items already saved in Kodi
self.allKodiElementsId = {}
2016-01-28 01:14:30 +11:00
# Run through views and get latest changed elements using time diff
2016-02-11 22:44:12 +11:00
self.updateKodiVideoLib = False
self.updateKodiMusicLib = False
2016-01-28 06:41:28 +11:00
for view in self.views:
self.updatelist = []
# Get items per view
2016-03-25 04:52:02 +11:00
items = PF.GetAllPlexLeaves(
view['id'],
updatedAt=self.getPMSfromKodiTime(lastSync),
containerSize=self.limitindex)
2016-03-11 04:34:11 +11:00
# Just skip if something went wrong
if items is None:
2016-01-28 06:41:28 +11:00
continue
# Get one itemtype, because they're the same in the PMS section
2016-03-11 04:34:11 +11:00
try:
plexType = items[0].attrib['type']
except:
# There was no child - PMS response is empty
continue
2016-01-28 06:41:28 +11:00
# Populate self.updatelist
2016-03-11 04:34:11 +11:00
self.getFastUpdateList(
items, plexType, view['name'], view['id'])
2016-01-28 06:41:28 +11:00
# Process self.updatelist
if self.updatelist:
if self.updatelist[0]['itemType'] in ['Movies', 'TVShows']:
2016-02-11 22:44:12 +11:00
self.updateKodiVideoLib = True
elif self.updatelist[0]['itemType'] == 'Music':
self.updateKodiMusicLib = True
2016-03-11 04:34:11 +11:00
# Do the work
2016-02-04 00:44:11 +11:00
self.GetAndProcessXMLs(
2016-03-25 04:52:02 +11:00
PF.GetItemClassFromType(plexType),
showProgress=False)
2016-03-11 04:34:11 +11:00
self.updatelist = []
# 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
2016-01-28 06:41:28 +11:00
for view in self.views:
2016-03-25 04:52:02 +11:00
items = PF.GetAllPlexLeaves(
view['id'],
lastViewedAt=self.getPMSfromKodiTime(lastSync),
containerSize=self.limitindex)
if items is None:
2016-03-18 00:30:47 +11:00
continue
2016-03-11 04:34:11 +11:00
for item in items:
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:
2016-03-12 00:42:14 +11:00
# Only look at lastViewedAt
if res[0] == lastViewedAt:
2016-03-11 04:34:11 +11:00
# Nothing to update, we have already processed this
# item
continue
if plexType == 'movie':
movieupdate = True
2016-03-12 00:42:14 +11:00
try:
movieXML.append(item)
except:
movieXML = etree.Element('root')
movieXML.append(item)
2016-03-11 04:34:11 +11:00
elif plexType == 'episode':
episodeupdate = True
2016-03-12 00:42:14 +11:00
try:
episodeXML.append(item)
except:
episodeXML = etree.Element('root')
episodeXML.append(item)
2016-03-11 04:34:11 +11:00
elif plexType == 'track':
songupdate = True
2016-03-12 00:42:14 +11:00
try:
musicXML.append(item)
except:
musicXML = etree.Element('root')
musicXML.append(item)
2016-03-11 04:34:11 +11:00
# 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)
2016-02-11 22:44:12 +11:00
# Let Kodi update the library now (artwork and userdata)
if self.updateKodiVideoLib:
2016-03-01 23:31:35 +11:00
self.logMsg("Doing Kodi Video Lib update", 1)
2016-02-11 22:44:12 +11:00
xbmc.executebuiltin('UpdateLibrary(video)')
if self.updateKodiMusicLib:
2016-03-01 23:31:35 +11:00
self.logMsg("Doing Kodi Music Lib update", 1)
xbmc.executebuiltin('UpdateLibrary(music)')
# Show warning if itemtypes.py crashed at some point
if utils.window('plex_scancrashed') == 'true':
xbmcgui.Dialog().ok(self.addonName, self.__language__(39408))
utils.window('plex_scancrashed', clear=True)
2016-01-28 06:41:28 +11:00
return True
2016-01-12 03:37:01 +11:00
2015-12-25 07:07:00 +11:00
def saveLastSync(self):
# Save last sync time
2016-01-30 06:07:21 +11:00
self.lastSync = utils.getUnixTimestamp()
2015-12-25 07:07:00 +11:00
def initializeDBs(self):
"""
Run once during startup to verify that emby db exists.
"""
2015-12-25 07:07:00 +11:00
embyconn = utils.kodiSQL('emby')
embycursor = embyconn.cursor()
# Create the tables for the emby database
# emby, view, version
embycursor.execute(
"""CREATE TABLE IF NOT EXISTS emby(
emby_id TEXT UNIQUE, media_folder TEXT, emby_type TEXT, media_type TEXT, kodi_id INTEGER,
kodi_fileid INTEGER, kodi_pathid INTEGER, parent_id INTEGER, checksum INTEGER)""")
embycursor.execute(
"""CREATE TABLE IF NOT EXISTS view(
view_id TEXT UNIQUE, view_name TEXT, media_type TEXT, kodi_tagid INTEGER)""")
embycursor.execute("CREATE TABLE IF NOT EXISTS version(idVersion TEXT)")
embyconn.commit()
2016-03-01 22:10:09 +11:00
2015-12-25 07:07:00 +11:00
# content sync: movies, tvshows, musicvideos, music
embyconn.close()
return
2016-03-03 03:27:21 +11:00
@utils.LogTime
def fullSync(self, manualrun=False, repair=False):
2016-03-03 03:27:21 +11:00
# self.compare == False: we're syncing EVERY item
# True: we're syncing only the delta, e.g. different checksum
2016-01-30 06:07:21 +11:00
self.compare = manualrun or repair
2016-03-02 02:52:09 +11:00
xbmc.executebuiltin('InhibitIdleShutdown(true)')
2016-03-03 18:10:06 +11:00
screensaver = utils.getScreensaver()
utils.setScreensaver(value="")
2016-03-02 02:52:09 +11:00
# Add sources
utils.sourcesXML()
2015-12-25 07:07:00 +11:00
# Deactivate Kodi popup showing that it's (unsuccessfully) trying to
# scan music folders
if self.enableMusic:
utils.musiclibXML()
utils.advancedSettingsXML()
2016-01-30 06:07:21 +11:00
# Set new timestamp NOW because sync might take a while
self.saveLastSync()
2015-12-25 07:07:00 +11:00
2016-01-11 19:57:45 +11:00
# Ensure that DBs exist if called for very first time
self.initializeDBs()
2016-03-03 03:27:21 +11:00
# Set views. Abort if unsuccessful
if not self.maintainViews():
xbmc.executebuiltin('InhibitIdleShutdown(false)')
2016-03-03 18:10:06 +11:00
utils.setScreensaver(value=screensaver)
2016-03-03 03:27:21 +11:00
return False
2016-01-11 19:57:45 +11:00
2015-12-28 23:10:05 +11:00
process = {
2016-01-10 02:14:02 +11:00
'movies': self.PlexMovies,
'tvshows': self.PlexTVShows,
2015-12-28 23:10:05 +11:00
}
if self.enableMusic:
process['music'] = self.PlexMusic
2015-12-25 07:07:00 +11:00
for itemtype in process:
completed = process[itemtype]()
2015-12-25 07:07:00 +11:00
if not completed:
2016-03-02 02:52:09 +11:00
xbmc.executebuiltin('InhibitIdleShutdown(false)')
2016-03-03 18:10:06 +11:00
utils.setScreensaver(value=screensaver)
2015-12-25 07:07:00 +11:00
return False
2016-03-03 03:27:21 +11:00
# Let kodi update the views in any case, since we're doing a full sync
2015-12-25 07:07:00 +11:00
xbmc.executebuiltin('UpdateLibrary(video)')
if self.enableMusic:
xbmc.executebuiltin('UpdateLibrary(music)')
2016-02-12 00:44:11 +11:00
2016-03-03 03:27:21 +11:00
utils.window('emby_initialScan', clear=True)
2016-03-02 02:52:09 +11:00
xbmc.executebuiltin('InhibitIdleShutdown(false)')
2016-03-03 18:10:06 +11:00
utils.setScreensaver(value=screensaver)
# Show warning if itemtypes.py crashed at some point
if utils.window('plex_scancrashed') == 'true':
xbmcgui.Dialog().ok(self.addonName, self.__language__(39408))
utils.window('plex_scancrashed', clear=True)
2015-12-25 07:07:00 +11:00
return True
2016-02-12 00:03:04 +11:00
def processView(self, folderItem, kodi_db, emby_db, totalnodes):
vnodes = self.vnodes
folder = folderItem.attrib
mediatype = folder['type']
# Only process supported formats
2016-03-03 03:27:21 +11:00
if mediatype not in ('movie', 'show', 'artist'):
return totalnodes
# Prevent duplicate for nodes of the same type
nodes = self.nodes[mediatype]
# Prevent duplicate for playlists of the same type
playlists = self.playlists[mediatype]
2016-03-03 19:04:15 +11:00
sorted_views = self.sorted_views
2016-02-12 00:03:04 +11:00
folderid = folder['key']
foldername = folder['title']
viewtype = folder['type']
# Get current media folders from emby database
view = emby_db.getView_byId(folderid)
try:
current_viewname = view[0]
current_viewtype = view[1]
current_tagid = view[2]
except TypeError:
2016-03-03 03:27:21 +11:00
self.logMsg("Creating viewid: %s in Plex database."
2016-02-12 00:03:04 +11:00
% folderid, 1)
tagid = kodi_db.createTag(foldername)
# Create playlist for the video library
2016-03-03 03:27:21 +11:00
if (foldername not in playlists and
mediatype in ('movie', 'show', 'musicvideos')):
utils.playlistXSP(mediatype, foldername, folderid, viewtype)
playlists.append(foldername)
2016-02-12 00:03:04 +11:00
# Create the video node
2016-03-03 03:27:21 +11:00
if (foldername not in nodes and
mediatype not in ("musicvideos", "artist")):
vnodes.viewNode(sorted_views.index(foldername),
2016-02-12 00:03:04 +11:00
foldername,
mediatype,
2016-03-01 22:10:09 +11:00
viewtype,
folderid)
2016-03-03 03:27:21 +11:00
nodes.append(foldername)
2016-02-12 00:03:04 +11:00
totalnodes += 1
# Add view to emby database
emby_db.addView(folderid, foldername, viewtype, tagid)
else:
self.logMsg(' '.join((
"Found viewid: %s" % folderid,
"viewname: %s" % current_viewname,
"viewtype: %s" % current_viewtype,
2016-03-08 22:13:47 +11:00
"tagid: %s" % current_tagid)), 1)
2016-03-01 21:26:46 +11:00
# Remove views that are still valid to delete rest later
try:
self.old_views.remove(folderid)
except ValueError:
# View was just created, nothing to remove
pass
2016-02-12 00:03:04 +11:00
# View was modified, update with latest info
if current_viewname != foldername:
self.logMsg("viewid: %s new viewname: %s"
% (folderid, foldername), 1)
tagid = kodi_db.createTag(foldername)
# Update view with new info
emby_db.updateView(foldername, tagid, folderid)
2016-03-03 03:27:21 +11:00
if mediatype != "artist":
2016-02-12 00:03:04 +11:00
if emby_db.getView_byName(current_viewname) is None:
# The tag could be a combined view. Ensure there's
# no other tags with the same name before deleting
# playlist.
utils.playlistXSP(mediatype,
current_viewname,
2016-03-03 03:27:21 +11:00
folderid,
2016-02-12 00:03:04 +11:00
current_viewtype,
True)
# Delete video node
if mediatype != "musicvideos":
2016-03-03 03:27:21 +11:00
vnodes.viewNode(
indexnumber=sorted_views.index(foldername),
tagname=current_viewname,
mediatype=mediatype,
viewtype=current_viewtype,
viewid=folderid,
delete=True)
2016-02-12 00:03:04 +11:00
# Added new playlist
2016-03-03 03:27:21 +11:00
if (foldername not in playlists and
mediatype in ('movie', 'show', 'musicvideos')):
utils.playlistXSP(mediatype,
foldername,
folderid,
viewtype)
playlists.append(foldername)
2016-02-12 00:03:04 +11:00
# Add new video node
2016-03-03 03:27:21 +11:00
if foldername not in nodes and mediatype != "musicvideos":
vnodes.viewNode(sorted_views.index(foldername),
2016-02-12 00:03:04 +11:00
foldername,
mediatype,
2016-03-01 22:10:09 +11:00
viewtype,
folderid)
2016-03-03 03:27:21 +11:00
nodes.append(foldername)
2016-02-12 00:03:04 +11:00
totalnodes += 1
# Update items with new tag
items = emby_db.getItem_byView(folderid)
for item in items:
# Remove the "s" from viewtype for tags
2016-03-03 03:27:21 +11:00
kodi_db.updateTag(
current_tagid, tagid, item[0], current_viewtype[:-1])
2016-02-12 00:03:04 +11:00
else:
2016-03-03 03:27:21 +11:00
# Validate the playlist exists or recreate it
if mediatype != "artist":
if (foldername not in playlists and
mediatype in ('movie', 'show', 'musicvideos')):
utils.playlistXSP(mediatype,
foldername,
folderid,
viewtype)
playlists.append(foldername)
2016-02-12 00:03:04 +11:00
# Create the video node if not already exists
2016-03-03 03:27:21 +11:00
if foldername not in nodes and mediatype != "musicvideos":
vnodes.viewNode(sorted_views.index(foldername),
2016-02-12 00:03:04 +11:00
foldername,
mediatype,
2016-03-01 22:10:09 +11:00
viewtype,
folderid)
2016-03-03 03:27:21 +11:00
nodes.append(foldername)
2016-02-12 00:03:04 +11:00
totalnodes += 1
2016-03-03 03:27:21 +11:00
return totalnodes
2016-02-12 00:03:04 +11:00
2016-01-11 19:57:45 +11:00
def maintainViews(self):
2015-12-28 23:10:05 +11:00
"""
2016-01-11 19:57:45 +11:00
Compare the views to Plex
2015-12-28 23:10:05 +11:00
"""
self.views = []
2015-12-25 07:07:00 +11:00
vnodes = self.vnodes
2016-01-11 19:57:45 +11:00
2015-12-25 07:07:00 +11:00
# Get views
2016-03-01 21:26:46 +11:00
sections = downloadutils.DownloadUtils().downloadUrl(
2016-02-20 06:03:06 +11:00
"{server}/library/sections")
2016-03-01 22:10:09 +11:00
try:
sections.attrib
except AttributeError:
self.logMsg("Error download PMS views, abort maintainViews", -1)
return False
2015-12-25 07:07:00 +11:00
# For whatever freaking reason, .copy() or dict() does NOT work?!?!?!
2016-03-03 03:27:21 +11:00
self.nodes = {
'movie': [],
'show': [],
'artist': []
}
self.playlists = {
'movie': [],
'show': [],
'artist': []
}
2016-03-03 19:04:15 +11:00
self.sorted_views = []
2016-03-03 03:27:21 +11:00
for view in sections:
itemType = view.attrib['type']
if itemType in ('movie', 'show'): # and NOT artist for now
2016-03-03 19:04:15 +11:00
self.sorted_views.append(view.attrib['title'])
self.logMsg('Sorted views: %s' % self.sorted_views, 1)
# total nodes for window properties
vnodes.clearProperties()
totalnodes = len(self.sorted_views)
2015-12-25 07:07:00 +11:00
2016-03-01 20:40:30 +11:00
with embydb.GetEmbyDB() as emby_db:
2016-03-01 21:26:46 +11:00
# Backup old views to delete them later, if needed (at the end
# of this method, only unused views will be left in oldviews)
self.old_views = emby_db.getViews()
2016-03-01 20:40:30 +11:00
with kodidb.GetKodiDB('video') as kodi_db:
2016-03-01 21:26:46 +11:00
for folderItem in sections:
2016-03-03 03:27:21 +11:00
totalnodes = self.processView(folderItem,
kodi_db,
emby_db,
totalnodes)
2016-03-03 19:04:15 +11:00
# Add video nodes listings
# Plex: there seem to be no favorites/favorites tag
# vnodes.singleNode(totalnodes,
# "Favorite movies",
# "movies",
# "favourites")
# totalnodes += 1
# vnodes.singleNode(totalnodes,
# "Favorite tvshows",
# "tvshows",
# "favourites")
# totalnodes += 1
# vnodes.singleNode(totalnodes,
# "channels",
# "movies",
# "channels")
# totalnodes += 1
with kodidb.GetKodiDB('music') as kodi_db:
pass
# Save total
utils.window('Emby.nodes.total', str(totalnodes))
2016-02-12 00:03:04 +11:00
2016-03-03 03:27:21 +11:00
# Reopen DB connection to ensure that changes were commited before
2016-03-01 21:26:46 +11:00
with embydb.GetEmbyDB() as emby_db:
2016-02-12 00:03:04 +11:00
# update views for all:
self.views = emby_db.getAllViewInfo()
2016-03-01 22:10:09 +11:00
self.logMsg("Removing views: %s" % self.old_views, 1)
2016-03-01 21:26:46 +11:00
for view in self.old_views:
emby_db.removeView(view)
2016-03-03 03:27:21 +11:00
self.logMsg("Finished processing views. Views saved: %s"
% self.views, 1)
return True
2015-12-25 07:07:00 +11:00
def GetUpdatelist(self, xml, itemType, method, viewName, viewId,
dontCheck=False):
"""
THIS METHOD NEEDS TO BE FAST! => e.g. no API calls
Adds items to self.updatelist as well as self.allPlexElementsId dict
2015-12-25 07:07:00 +11:00
Input:
xml: PMS answer for section items
itemType: 'Movies', 'TVShows', ...
method: Method name to be called with this itemtype
see itemtypes.py
viewName: Name of the Plex view (e.g. 'My TV shows')
viewId: Id/Key of Plex library (e.g. '1')
dontCheck: If True, skips checksum check but assumes
that all items in xml must be processed
Output: self.updatelist, self.allPlexElementsId
self.updatelist APPENDED(!!) list itemids (Plex Keys as
2016-01-30 06:07:21 +11:00
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,
2016-01-28 06:41:28 +11:00
'viewId': xxx,
'title': xxx
self.allPlexElementsId APPENDED(!!) dict
= {itemid: checksum}
"""
if self.compare or not dontCheck:
2016-03-01 22:10:09 +11:00
# Only process the delta - new or changed items
for item in xml:
itemId = item.attrib.get('ratingKey')
# Skipping items 'title=All episodes' without a 'ratingKey'
if not itemId:
2016-01-12 20:30:28 +11:00
continue
title = item.attrib.get('title', 'Missing Title Name')
plex_checksum = ("K%s%s"
% (itemId, item.attrib.get('updatedAt', '')))
2016-01-12 20:30:28 +11:00
self.allPlexElementsId[itemId] = plex_checksum
kodi_checksum = self.allKodiElementsId.get(itemId)
2016-03-01 22:10:09 +11:00
# Only update if movie is not in Kodi or checksum is
# different
2016-01-12 20:30:28 +11:00
if kodi_checksum != plex_checksum:
self.updatelist.append({'itemId': itemId,
'itemType': itemType,
'method': method,
'viewName': viewName,
'viewId': viewId,
'title': title})
else:
# Initial or repair sync: get all Plex movies
for item in xml:
itemId = item.attrib.get('ratingKey')
# Skipping items 'title=All episodes' without a 'ratingKey'
if not itemId:
2016-01-12 20:30:28 +11:00
continue
title = item.attrib.get('title', 'Missing Title Name')
plex_checksum = ("K%s%s"
% (itemId, item.attrib.get('updatedAt', '')))
2016-01-12 20:30:28 +11:00
self.allPlexElementsId[itemId] = plex_checksum
self.updatelist.append({'itemId': itemId,
'itemType': itemType,
'method': method,
'viewName': viewName,
'viewId': viewId,
'title': title})
def GetAndProcessXMLs(self, itemType, showProgress=True):
"""
Downloads all XMLs for itemType (e.g. Movies, TV-Shows). Processes them
by then calling itemtypes.<itemType>()
2015-12-29 04:47:16 +11:00
Input:
itemType: 'Movies', 'TVShows', ...
2016-01-11 17:55:22 +11:00
self.updatelist
showProgress If False, NEVER shows sync progress
"""
# Some logging, just in case.
self.logMsg("self.updatelist: %s" % self.updatelist, 2)
2016-01-28 06:41:28 +11:00
itemNumber = len(self.updatelist)
if itemNumber == 0:
2016-03-03 03:27:21 +11:00
return
# Run through self.updatelist, get XML metadata per item
# Initiate threads
self.logMsg("Starting sync threads", 1)
getMetadataQueue = Queue.Queue()
2016-01-12 19:50:15 +11:00
processMetadataQueue = Queue.Queue(maxsize=100)
2016-02-12 00:03:04 +11:00
getMetadataLock = Lock()
processMetadataLock = Lock()
# To keep track
global getMetadataCount
getMetadataCount = 0
global processMetadataCount
processMetadataCount = 0
2016-01-11 17:55:22 +11:00
global processingViewName
processingViewName = ''
2016-01-10 02:14:02 +11:00
# Populate queue: GetMetadata
2016-01-11 17:55:22 +11:00
for updateItem in self.updatelist:
getMetadataQueue.put(updateItem)
# Spawn GetMetadata threads for downloading
threads = []
2016-01-28 06:41:28 +11:00
for i in range(min(self.syncThreadNumber, itemNumber)):
2016-01-11 17:55:22 +11:00
thread = ThreadedGetMetadata(getMetadataQueue,
processMetadataQueue,
getMetadataLock,
processMetadataLock)
2016-01-11 17:55:22 +11:00
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("%s download threads spawned" % len(threads), 1)
2016-01-30 18:43:28 +11:00
# Spawn one more thread to process Metadata, once downloaded
thread = ThreadedProcessMetadata(processMetadataQueue,
itemType,
processMetadataLock)
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("Processing thread spawned", 1)
# Start one thread to show sync progress
if showProgress:
if self.showDbSync:
dialog = xbmcgui.DialogProgressBG()
thread = ThreadedShowSyncInfo(
dialog,
[getMetadataLock, processMetadataLock],
itemNumber,
itemType)
thread.setDaemon(True)
thread.start()
threads.append(thread)
self.logMsg("Kodi Infobox thread spawned", 1)
2016-01-30 06:07:21 +11:00
# Wait until finished
2016-01-30 18:43:28 +11:00
getMetadataQueue.join()
processMetadataQueue.join()
# Kill threads
self.logMsg("Waiting to kill threads", 1)
for thread in threads:
thread.stopThread()
self.logMsg("Stop sent to all threads", 1)
# Wait till threads are indeed dead
for thread in threads:
2016-01-27 01:13:03 +11:00
thread.join(5.0)
2016-01-12 19:50:15 +11:00
if thread.isAlive():
self.logMsg("Could not terminate thread", -1)
2016-01-28 01:14:30 +11:00
try:
del threads
except:
self.logMsg("Could not delete threads", -1)
self.logMsg("Sync threads finished", 1)
2016-01-28 06:41:28 +11:00
self.updatelist = []
2015-12-29 04:47:16 +11:00
2016-03-03 03:27:21 +11:00
@utils.LogTime
def PlexMovies(self):
# Initialize
self.allPlexElementsId = {}
2016-01-12 00:38:01 +11:00
2016-01-11 17:55:22 +11:00
itemType = 'Movies'
2016-01-28 06:41:28 +11:00
views = [x for x in self.views if x['itemtype'] == 'movie']
2016-01-11 17:55:22 +11:00
self.logMsg("Processing Plex %s. Libraries: %s" % (itemType, views), 1)
self.allKodiElementsId = {}
if self.compare:
2016-02-10 21:00:32 +11:00
with embydb.GetEmbyDB() as emby_db:
# Get movies from Plex server
# Pull the list of movies and boxsets in Kodi
try:
self.allKodiElementsId = dict(emby_db.getChecksum('Movie'))
except ValueError:
self.allKodiElementsId = {}
2016-02-20 06:03:06 +11:00
# PROCESS MOVIES #####
2016-01-11 17:55:22 +11:00
self.updatelist = []
for view in views:
2016-01-27 01:13:03 +11:00
if self.threadStopped():
return False
# Get items per view
viewId = view['id']
viewName = view['name']
2016-03-25 04:52:02 +11:00
all_plexmovies = PF.GetPlexSectionResults(
viewId, args=None, containerSize=self.limitindex)
if all_plexmovies is None:
self.logMsg("Couldnt get section items, aborting for view.", 1)
continue
# Populate self.updatelist and self.allPlexElementsId
2016-01-11 17:55:22 +11:00
self.GetUpdatelist(all_plexmovies,
itemType,
'add_update',
viewName,
viewId)
self.GetAndProcessXMLs(itemType)
2016-03-04 23:34:30 +11:00
self.logMsg("Processed view", 1)
# Update viewstate
for view in views:
if self.threadStopped():
return False
self.PlexUpdateWatched(view['id'], itemType)
2016-02-20 06:03:06 +11:00
# PROCESS DELETES #####
2016-01-11 01:16:59 +11:00
if self.compare:
# Manual sync, process deletes
with itemtypes.Movies() as Movie:
for kodimovie in self.allKodiElementsId:
if kodimovie not in self.allPlexElementsId:
Movie.remove(kodimovie)
2016-01-11 18:10:36 +11:00
self.logMsg("%s sync is finished." % itemType, 1)
2016-01-11 01:16:59 +11:00
return True
2016-01-30 06:07:21 +11:00
def PlexUpdateWatched(self, viewId, itemType,
lastViewedAt=None, updatedAt=None):
"""
2016-01-30 06:07:21 +11:00
Updates plex elements' view status ('watched' or 'unwatched') and
also updates resume times.
This is done by downloading one XML for ALL elements with viewId
"""
2016-03-25 04:52:02 +11:00
xml = PF.GetAllPlexLeaves(viewId,
lastViewedAt=lastViewedAt,
updatedAt=updatedAt,
containerSize=self.limitindex)
2016-02-11 22:44:12 +11:00
# Return if there are no items in PMS reply - it's faster
try:
xml[0].attrib
except (TypeError, AttributeError, IndexError):
return
2016-03-01 23:31:35 +11:00
if itemType in ('Movies', 'TVShows'):
2016-02-11 22:44:12 +11:00
self.updateKodiVideoLib = True
2016-03-01 23:31:35 +11:00
elif itemType in ('Music'):
self.updateKodiMusicLib = True
2016-02-11 22:44:12 +11:00
itemMth = getattr(itemtypes, itemType)
with itemMth() as method:
method.updateUserdata(xml)
def musicvideos(self, embycursor, kodicursor, pdialog):
2016-02-20 06:03:06 +11:00
log = self.logMsg
2015-12-25 07:07:00 +11:00
# Get musicvideos from emby
emby = self.emby
emby_db = embydb.Embydb_Functions(embycursor)
mvideos = itemtypes.MusicVideos(embycursor, kodicursor)
views = emby_db.getView_byType('musicvideos')
2016-02-20 06:03:06 +11:00
log("Media folders: %s" % views, 1)
2015-12-25 07:07:00 +11:00
for view in views:
2016-02-20 06:03:06 +11:00
if self.shouldStop():
2015-12-25 07:07:00 +11:00
return False
# Get items per view
viewId = view['id']
viewName = view['name']
if pdialog:
pdialog.update(
heading="Emby for Kodi",
2016-02-20 06:03:06 +11:00
message="%s %s..." % (utils.language(33019), viewName))
2015-12-25 07:07:00 +11:00
# Initial or repair sync
all_embymvideos = emby.getMusicVideos(viewId, dialog=pdialog)
total = all_embymvideos['TotalRecordCount']
embymvideos = all_embymvideos['Items']
2015-12-25 07:07:00 +11:00
if pdialog:
pdialog.update(heading="Processing %s / %s items" % (viewName, total))
count = 0
for embymvideo in embymvideos:
# Process individual musicvideo
2016-02-20 06:03:06 +11:00
if self.shouldStop():
2015-12-25 07:07:00 +11:00
return False
title = embymvideo['Name']
if pdialog:
percentage = int((float(count) / float(total))*100)
pdialog.update(percentage, message=title)
count += 1
mvideos.add_update(embymvideo, viewName, viewId)
else:
2016-02-20 06:03:06 +11:00
log("MusicVideos finished.", 2)
2015-12-25 07:07:00 +11:00
return True
2016-03-03 03:27:21 +11:00
@utils.LogTime
2016-01-10 02:14:02 +11:00
def PlexTVShows(self):
# Initialize
self.allPlexElementsId = {}
2016-01-11 17:55:22 +11:00
itemType = 'TVShows'
2016-01-28 06:41:28 +11:00
views = [x for x in self.views if x['itemtype'] == 'show']
2016-01-11 18:10:36 +11:00
self.logMsg("Media folders for %s: %s" % (itemType, views), 1)
self.allKodiElementsId = {}
2016-01-10 02:14:02 +11:00
if self.compare:
2016-02-10 21:00:32 +11:00
with embydb.GetEmbyDB() as emby_db:
# Pull the list of TV shows already in Kodi
for kind in ('Series', 'Season', 'Episode'):
try:
elements = dict(emby_db.getChecksum(kind))
self.allKodiElementsId.update(elements)
# Yet empty/not yet synched
except ValueError:
pass
2016-02-20 06:03:06 +11:00
# PROCESS TV Shows #####
2016-01-11 17:55:22 +11:00
self.updatelist = []
for view in views:
2016-01-27 01:13:03 +11:00
if self.threadStopped():
return False
# Get items per view
viewId = view['id']
viewName = view['name']
2016-03-25 04:52:02 +11:00
allPlexTvShows = PF.GetPlexSectionResults(
viewId, containerSize=self.limitindex)
if allPlexTvShows is None:
self.logMsg(
"Error downloading show view xml for view %s" % viewId, -1)
continue
2016-01-10 02:14:02 +11:00
# Populate self.updatelist and self.allPlexElementsId
2016-01-11 17:55:22 +11:00
self.GetUpdatelist(allPlexTvShows,
itemType,
'add_update',
viewName,
viewId)
2016-01-11 18:10:36 +11:00
self.logMsg("Analyzed view %s with ID %s" % (viewName, viewId), 1)
2016-01-11 01:16:59 +11:00
# COPY for later use
allPlexTvShowsId = self.allPlexElementsId.copy()
2016-03-14 02:06:54 +11:00
# Process self.updatelist
self.GetAndProcessXMLs(itemType)
self.logMsg("GetAndProcessXMLs completed for tv shows", 1)
2016-02-20 06:03:06 +11:00
# PROCESS TV Seasons #####
2016-01-10 02:14:02 +11:00
# Cycle through tv shows
for tvShowId in allPlexTvShowsId:
2016-01-27 01:13:03 +11:00
if self.threadStopped():
2016-01-11 17:55:22 +11:00
return False
2016-01-10 02:14:02 +11:00
# Grab all seasons to tvshow from PMS
2016-03-25 04:52:02 +11:00
seasons = PF.GetAllPlexChildren(
tvShowId, containerSize=self.limitindex)
if seasons is None:
self.logMsg(
"Error downloading season xml for show %s" % tvShowId, -1)
continue
2016-01-10 02:14:02 +11:00
# Populate self.updatelist and self.allPlexElementsId
2016-01-11 17:55:22 +11:00
self.GetUpdatelist(seasons,
itemType,
'add_updateSeason',
None,
tvShowId) # send showId instead of viewid
self.logMsg("Analyzed all seasons of TV show with Plex Id %s"
% tvShowId, 1)
2016-01-10 02:14:02 +11:00
2016-03-14 02:06:54 +11:00
# Process self.updatelist
self.GetAndProcessXMLs(itemType)
self.logMsg("GetAndProcessXMLs completed for seasons", 1)
2016-02-20 06:03:06 +11:00
# PROCESS TV Episodes #####
2016-01-10 02:14:02 +11:00
# Cycle through tv shows
for view in views:
2016-01-27 01:13:03 +11:00
if self.threadStopped():
2016-01-11 17:55:22 +11:00
return False
2016-01-10 02:14:02 +11:00
# Grab all episodes to tvshow from PMS
2016-03-25 04:52:02 +11:00
episodes = PF.GetAllPlexLeaves(
view['id'], containerSize=self.limitindex)
if episodes is None:
self.logMsg(
"Error downloading episod xml for view %s"
% view.get('name'), -1)
continue
2016-01-10 02:14:02 +11:00
# Populate self.updatelist and self.allPlexElementsId
2016-01-11 17:55:22 +11:00
self.GetUpdatelist(episodes,
itemType,
'add_updateEpisode',
None,
None)
self.logMsg("Analyzed all episodes of TV show with Plex Id %s"
% view['id'], 1)
2016-01-11 17:55:22 +11:00
# Process self.updatelist
self.GetAndProcessXMLs(itemType)
2016-03-14 02:06:54 +11:00
self.logMsg("GetAndProcessXMLs completed for episodes", 1)
2016-01-11 17:55:22 +11:00
# Refresh season info
# Cycle through tv shows
with itemtypes.TVShows() as TVshow:
for tvShowId in allPlexTvShowsId:
2016-03-25 04:52:02 +11:00
XMLtvshow = PF.GetPlexMetadata(tvShowId)
2016-01-11 17:55:22 +11:00
TVshow.refreshSeasonEntry(XMLtvshow, tvShowId)
2016-01-11 18:10:36 +11:00
self.logMsg("Season info refreshed", 1)
2016-01-11 17:55:22 +11:00
# Update viewstate:
for view in views:
self.PlexUpdateWatched(view['id'], itemType)
2016-01-11 01:16:59 +11:00
if self.compare:
# Manual sync, process deletes
with itemtypes.TVShows() as TVShow:
for kodiTvElement in self.allKodiElementsId:
if kodiTvElement not in self.allPlexElementsId:
TVShow.remove(kodiTvElement)
2016-01-11 18:10:36 +11:00
self.logMsg("%s sync is finished." % itemType, 1)
return True
2016-03-03 03:27:21 +11:00
@utils.LogTime
def PlexMusic(self):
itemType = 'Music'
2015-12-25 07:07:00 +11:00
views = [x for x in self.views if x['itemtype'] == 'artist']
self.logMsg("Media folders for %s: %s" % (itemType, views), 1)
2015-12-25 07:07:00 +11:00
methods = {
'MusicArtist': 'add_updateArtist',
'MusicAlbum': 'add_updateAlbum',
'Audio': 'add_updateSong'
}
urlArgs = {
'MusicArtist': {'type': 8},
'MusicAlbum': {'type': 9},
'Audio': {'type': 10}
2015-12-25 07:07:00 +11:00
}
2016-01-23 22:05:56 +11:00
2016-03-01 22:10:09 +11:00
# Process artist, then album and tracks last to minimize overhead
for kind in ('MusicArtist', 'MusicAlbum', 'Audio'):
if self.threadStopped():
return True
self.logMsg("Start processing music %s" % kind, 1)
self.ProcessMusic(
views, kind, urlArgs[kind], methods[kind])
self.logMsg("Processing of music %s done" % kind, 1)
self.GetAndProcessXMLs(itemType)
self.logMsg("GetAndProcessXMLs for music %s completed" % kind, 1)
# reset stuff
self.allKodiElementsId = {}
self.allPlexElementsId = {}
self.updatelist = []
self.logMsg("%s sync is finished." % itemType, 1)
return True
2015-12-25 07:07:00 +11:00
def ProcessMusic(self, views, kind, urlArgs, method):
self.allKodiElementsId = {}
self.allPlexElementsId = {}
self.updatelist = []
2015-12-25 07:07:00 +11:00
# Get a list of items already existing in Kodi db
if self.compare:
with embydb.GetEmbyDB() as emby_db:
# Pull the list of items already in Kodi
try:
elements = dict(emby_db.getChecksum(kind))
self.allKodiElementsId.update(elements)
# Yet empty/nothing yet synched
except ValueError:
pass
2015-12-25 07:07:00 +11:00
for view in views:
if self.threadStopped():
return True
# Get items per view
viewId = view['id']
viewName = view['name']
2016-03-25 04:52:02 +11:00
itemsXML = PF.GetPlexSectionResults(
viewId, args=urlArgs, containerSize=self.limitindex)
if itemsXML is None:
self.logMsg("Error downloading xml for view %s"
% viewId, -1)
continue
# Populate self.updatelist and self.allPlexElementsId
self.GetUpdatelist(itemsXML,
'Music',
method,
viewName,
viewId)
2015-12-25 07:07:00 +11:00
def compareDBVersion(self, current, minimum):
# It returns True is database is up to date. False otherwise.
self.logMsg("current: %s minimum: %s" % (current, minimum), 1)
2016-03-04 01:28:44 +11:00
try:
currMajor, currMinor, currPatch = current.split(".")
except ValueError:
# there WAS no current DB, e.g. deleted.
return True
2015-12-25 07:07:00 +11:00
minMajor, minMinor, minPatch = minimum.split(".")
2016-03-15 04:10:36 +11:00
currMajor = int(currMajor)
currMinor = int(currMinor)
currPatch = int(currPatch)
minMajor = int(minMajor)
minMinor = int(minMinor)
minPatch = int(minPatch)
2015-12-25 07:07:00 +11:00
if currMajor > minMajor:
return True
2016-03-01 22:10:09 +11:00
elif (currMajor == minMajor and (currMinor > minMinor or
(currMinor == minMinor and currPatch >= minPatch))):
2015-12-25 07:07:00 +11:00
return True
else:
# Database out of date.
return False
2016-03-25 04:52:02 +11:00
def processMessage(self, message):
"""
processes json.loads() messages from websocket. Triage what we need to
do with "process_" methods
"""
typus = message.get('type')
if typus is None:
self.logMsg('No type, dropping message: %s' % message, -1)
return
if typus == 'playing':
self.process_playing(message['_children'])
elif typus == 'timeline':
self.process_timeline(message['_children'])
else:
self.logMsg('Dropping message: %s' % message, -1)
def multi_delete(self, liste, deleteListe):
"""
Deletes the list items of liste at the positions in deleteListe
"""
indexes = sorted(deleteListe, reverse=True)
for index in indexes:
del liste[index]
return liste
def process_newitems(self):
"""
Periodically called to process new/updated PMS items
PMS needs a while to download info from internet AFTER it
showed up under 'timeline' websocket messages
"""
videoLibUpdate = False
now = utils.getUnixTimestamp()
deleteListe = []
for i, item in enumerate(self.itemsToProcess):
ratingKey = item['ratingKey']
timestamp = item['timestamp']
if now - timestamp < self.safteyMargin:
# We haven't waited long enough for the PMS to finish
# processing the item
continue
xml = PF.GetPlexMetadata(ratingKey)
if xml is None:
self.logMsg('Could not download metadata for %s'
% ratingKey, -1)
continue
deleteListe.append(i)
self.logMsg("Adding new PMS item: %s" % ratingKey, 1)
viewtag = xml.attrib.get('librarySectionTitle')
viewid = xml.attrib.get('librarySectionID')
mediatype = xml[0].attrib.get('type')
if mediatype == 'movie':
# Movie
videoLibUpdate = True
with itemtypes.Movies() as movie:
movie.add_update(xml[0],
viewtag=viewtag,
viewid=viewid)
elif mediatype == 'episode':
# Episode
videoLibUpdate = True
with itemtypes.TVShows() as show:
show.add_updateEpisode(xml[0],
viewtag=viewtag,
viewid=viewid)
# Get rid of the items we just processed
if len(deleteListe) > 0:
self.itemsToProcess = self.multi_delete(
self.itemsToProcess, deleteListe)
# Let Kodi know of the change
if videoLibUpdate is True:
self.logMsg("Doing Kodi Video Lib update", 1)
xbmc.executebuiltin('UpdateLibrary(video)')
def process_timeline(self, data):
"""
PMS is messing with the library items
data['type']:
1: movie
2: tv show??
3: season??
4: episode
12: trailer, extras?
"""
videoLibUpdate = False
for item in data:
if item.get('state') == 9:
# Item was deleted.
# Only care for playable type
# For some reason itemID and not ratingKey
if item.get('type') == 1:
# Movie
self.logMsg("Removing movie %s" % item.get('itemID'), 1)
videoLibUpdate = True
with itemtypes.Movies() as movie:
movie.remove(item.get('itemID'))
elif item.get('type') == 4:
# Episode
self.logMsg("Removing episode %s" % item.get('itemID'), 1)
videoLibUpdate = True
with itemtypes.TVShows() as show:
show.remove(item.get('itemID'))
elif item.get('state') == 5 and item.get('type') in (1, 4):
# Item added or changed
# Need to process later because PMS needs to be done first
self.logMsg('New/changed PMS item: %s' % item.get('itemID'), 1)
self.itemsToProcess.append({
'ratingKey': item.get('itemID'),
'timestamp': utils.getUnixTimestamp()
})
# Let Kodi know of the change
if videoLibUpdate is True:
self.logMsg("Doing Kodi Video Lib update", 1)
xbmc.executebuiltin('UpdateLibrary(video)')
def process_playing(self, data):
items = []
with embydb.GetEmbyDB() as emby_db:
for item in data:
# Drop buffering messages
state = item.get('state')
if state == 'buffering':
continue
ratingKey = item.get('ratingKey')
kodiInfo = emby_db.getItem_byId(ratingKey)
if kodiInfo is None:
# Item not (yet) in Kodi library
continue
items.append({
'ratingKey': ratingKey,
'kodi_id': kodiInfo[0],
'file_id': kodiInfo[1],
'kodi_type': kodiInfo[4],
'viewOffset': PF.ConvertPlexToKodiTime(
item.get('viewOffset')),
'state': state,
'duration': PF.ConvertPlexToKodiTime(
item.get('duration')),
'viewCount': item.get('viewCount'),
'lastViewedAt': utils.DateToKodi(utils.getUnixTimestamp())
})
for item in items:
itemFkt = getattr(itemtypes,
PF.GetItemClassFromType(item['kodi_type']))
with itemFkt() as Fkt:
Fkt.updatePlaystate(item)
2015-12-25 07:07:00 +11:00
def run(self):
try:
self.run_internal()
except Exception as e:
utils.window('emby_dbScan', clear=True)
self.logMsg('LibrarySync thread crashed', -1)
self.logMsg('Error message: %s' % e, -1)
2016-03-08 21:47:46 +11:00
# Library sync thread has crashed
2015-12-25 07:07:00 +11:00
xbmcgui.Dialog().ok(
heading=self.addonName,
2016-03-08 21:47:46 +11:00
line1=self.__language__(39400))
2015-12-25 07:07:00 +11:00
raise
def run_internal(self):
2016-03-08 21:20:11 +11:00
# Re-assign handles to have faster calls
2016-02-20 06:03:06 +11:00
window = utils.window
settings = utils.settings
log = self.logMsg
2016-03-08 21:20:11 +11:00
threadStopped = self.threadStopped
threadSuspended = self.threadSuspended
installSyncDone = self.installSyncDone
enableBackgroundSync = self.enableBackgroundSync
fullSync = self.fullSync
2016-03-25 04:52:02 +11:00
processMessage = self.processMessage
2016-03-08 21:47:46 +11:00
string = self.__language__
2016-03-08 21:20:11 +11:00
dialog = xbmcgui.Dialog()
2015-12-25 07:07:00 +11:00
2016-03-25 04:52:02 +11:00
queue = self.queue
2015-12-25 07:07:00 +11:00
startupComplete = False
2016-01-28 06:41:28 +11:00
self.views = []
count = 0
2016-03-03 03:27:21 +11:00
errorcount = 0
2015-12-25 07:07:00 +11:00
2016-03-11 04:34:11 +11:00
# Initialize self.processed
self.resetProcessedItems()
2016-03-08 21:20:11 +11:00
log("---===### Starting LibrarySync ###===---", 0)
while not threadStopped():
2015-12-25 07:07:00 +11:00
2016-02-11 20:56:01 +11:00
# In the event the server goes offline, or an item is playing
2016-03-08 21:20:11 +11:00
while threadSuspended():
2015-12-25 07:07:00 +11:00
# Set in service.py
2016-03-08 21:20:11 +11:00
if threadStopped():
2015-12-25 07:07:00 +11:00
# Abort was requested while waiting. We should exit
2016-02-20 06:03:06 +11:00
log("###===--- LibrarySync Stopped ---===###", 0)
2016-01-28 06:41:28 +11:00
return
2016-02-11 20:56:01 +11:00
xbmc.sleep(1000)
2015-12-25 07:07:00 +11:00
2016-03-08 21:20:11 +11:00
if (window('emby_dbCheck') != "true" and installSyncDone):
2015-12-25 07:07:00 +11:00
# Verify the validity of the database
2016-02-20 06:03:06 +11:00
currentVersion = settings('dbCreatedWithVersion')
minVersion = window('emby_minDBVersion')
2015-12-25 07:07:00 +11:00
uptoDate = self.compareDBVersion(currentVersion, minVersion)
if not uptoDate:
2016-02-20 06:03:06 +11:00
log("Db version out of date: %s minimum version required: "
"%s" % (currentVersion, minVersion), 0)
2016-03-08 21:47:46 +11:00
# DB out of date. Proceed to recreate?
resp = dialog.yesno(heading=self.addonName,
line1=string(39401))
2015-12-25 07:07:00 +11:00
if not resp:
2016-02-20 06:03:06 +11:00
log("Db version out of date! USER IGNORED!", 0)
2016-03-08 21:47:46 +11:00
# PKC may not work correctly until reset
dialog.ok(heading=self.addonName,
line1=(self.addonName + string(39402)))
2015-12-25 07:07:00 +11:00
else:
utils.reset()
break
2016-03-03 03:27:21 +11:00
window('emby_dbCheck', value="true")
2015-12-25 07:07:00 +11:00
if not startupComplete:
2016-03-02 02:52:09 +11:00
# Also runs when first installed
2015-12-25 07:07:00 +11:00
# Verify the video database can be found
videoDb = utils.getKodiVideoDBPath()
if not xbmcvfs.exists(videoDb):
# Database does not exists
2016-02-20 06:03:06 +11:00
log("The current Kodi version is incompatible "
2016-03-08 21:47:46 +11:00
"to know which Kodi versions are supported.", -1)
log('Current Kodi version: %s' % xbmc.getInfoLabel(
'System.BuildVersion').decode('utf-8'))
# "Current Kodi version is unsupported, cancel lib sync"
dialog.ok(heading=self.addonName,
line1=string(39403))
2015-12-25 07:07:00 +11:00
break
# Run start up sync
2016-02-20 06:03:06 +11:00
window('emby_dbScan', value="true")
log("Db version: %s" % settings('dbCreatedWithVersion'), 0)
2016-03-03 03:27:21 +11:00
log("Initial start-up full sync starting", 0)
2016-03-08 21:20:11 +11:00
librarySync = fullSync(manualrun=True)
2016-03-12 00:42:14 +11:00
# Initialize time offset Kodi - PMS
self.syncPMStime()
2016-02-20 06:03:06 +11:00
window('emby_dbScan', clear=True)
2016-03-03 03:27:21 +11:00
if librarySync:
log("Initial start-up full sync successful", 0)
startupComplete = True
settings('SyncInstallRunDone', value="true")
settings("dbCreatedWithVersion",
self.clientInfo.getVersion())
2016-03-08 21:20:11 +11:00
installSyncDone = True
2016-03-03 03:27:21 +11:00
else:
log("Initial start-up full sync unsuccessful", -1)
errorcount += 1
if errorcount > 2:
log("Startup full sync failed. Stopping sync", -1)
2016-03-08 21:47:46 +11:00
# "Startup syncing process failed repeatedly"
# "Please restart"
dialog.ok(heading=self.addonName,
line1=string(39404))
2016-03-03 03:27:21 +11:00
break
2016-01-28 06:41:28 +11:00
# Currently no db scan, so we can start a new scan
2016-02-20 06:03:06 +11:00
elif window('emby_dbScan') != "true":
2016-01-28 06:41:28 +11:00
# Full scan was requested from somewhere else, e.g. userclient
if window('plex_runLibScan') == "full":
2016-02-20 06:03:06 +11:00
log('Full library scan requested, starting', 0)
window('emby_dbScan', value="true")
window('plex_runLibScan', clear=True)
2016-03-08 21:20:11 +11:00
fullSync(manualrun=True)
2016-02-20 06:03:06 +11:00
window('emby_dbScan', clear=True)
2016-01-28 06:41:28 +11:00
count = 0
# Full library sync finished
self.showKodiNote(string(39407), forced=True)
# Reset views was requested from somewhere else
elif window('plex_runLibScan') == "views":
log('Refresh playlist and nodes requested, starting', 0)
window('emby_dbScan', value="true")
window('plex_runLibScan', clear=True)
# First remove playlists
utils.deletePlaylists()
# Remove video nodes
utils.deleteNodes()
# Kick off refresh
if self.maintainViews():
2016-03-08 21:20:11 +11:00
# Ran successfully
2016-03-08 21:47:46 +11:00
log("Refresh playlists/nodes completed", 0)
# "Plex playlists/nodes refreshed"
self.showKodiNote(string(39405), forced=True)
else:
2016-03-08 21:20:11 +11:00
# Failed
log("Refresh playlists/nodes failed", -1)
2016-03-08 21:47:46 +11:00
# "Plex playlists/nodes refresh failed"
self.showKodiNote(string(39406),
forced=True,
icon="error")
window('emby_dbScan', clear=True)
2016-03-08 21:20:11 +11:00
elif enableBackgroundSync:
2016-02-11 20:56:01 +11:00
# Run full lib scan approx every 30min
if count >= 1800:
2016-03-12 00:42:14 +11:00
count = 0
2016-03-11 04:34:11 +11:00
# Also reset self.processed, just in case
self.resetProcessedItems()
2016-03-12 00:42:14 +11:00
# Recalculate time offset Kodi - PMS
self.syncPMStime()
2016-02-20 06:03:06 +11:00
window('emby_dbScan', value="true")
2016-03-08 21:20:11 +11:00
log('Running background full lib scan', 0)
fullSync(manualrun=True)
2016-02-20 06:03:06 +11:00
window('emby_dbScan', clear=True)
# Full library sync finished
self.showKodiNote(string(39407), forced=False)
2016-03-25 04:52:02 +11:00
elif count % 300 == 0:
count += 1
self.process_newitems()
else:
2016-03-25 04:52:02 +11:00
count += 1
# See if there is a PMS message we need to handle
try:
message = queue.get(block=False)
# Empty queue
except Queue.Empty:
xbmc.sleep(100)
continue
# Got a message from PMS; process it
else:
window('emby_dbScan', value="true")
processMessage(message)
window('emby_dbScan', clear=True)
# NO sleep!
continue
xbmc.sleep(100)
2016-01-28 06:41:28 +11:00
count += 1
2015-12-25 07:07:00 +11:00
2016-02-20 06:03:06 +11:00
log("###===--- LibrarySync Stopped ---===###", 0)