Speed up sync - download art in the background
This commit is contained in:
parent
24d6514b7f
commit
2fbdd54324
5 changed files with 259 additions and 22 deletions
|
@ -1841,16 +1841,14 @@ class API():
|
||||||
'Backdrop' : LIST with the first entry xml key "art"
|
'Backdrop' : LIST with the first entry xml key "art"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
item = self.item.attrib
|
|
||||||
|
|
||||||
allartworks = {
|
allartworks = {
|
||||||
'Primary': "",
|
'Primary': "", # corresponds to Plex poster ('thumb')
|
||||||
'Art': "",
|
'Art': "",
|
||||||
'Banner': "",
|
'Banner': "", # corresponds to Plex banner ('banner') for series
|
||||||
'Logo': "",
|
'Logo': "",
|
||||||
'Thumb': "",
|
'Thumb': "", # corresponds to Plex (grand)parent posters (thumb)
|
||||||
'Disc': "",
|
'Disc': "",
|
||||||
'Backdrop': []
|
'Backdrop': [] # Corresponds to Plex fanart ('art')
|
||||||
}
|
}
|
||||||
# Process backdrops
|
# Process backdrops
|
||||||
# Get background artwork URL
|
# Get background artwork URL
|
||||||
|
@ -1870,11 +1868,23 @@ class API():
|
||||||
self.__getOneArtwork('parentArt'))
|
self.__getOneArtwork('parentArt'))
|
||||||
if not allartworks['Primary']:
|
if not allartworks['Primary']:
|
||||||
allartworks['Primary'] = self.__getOneArtwork('parentThumb')
|
allartworks['Primary'] = self.__getOneArtwork('parentThumb')
|
||||||
|
return allartworks
|
||||||
|
|
||||||
# Plex does not get much artwork - go ahead and get the rest from
|
def getFanartArtwork(self, allartworks, parentInfo=False):
|
||||||
# fanart tv only for movie or tv show
|
"""
|
||||||
if settings('FanartTV') == 'true':
|
Downloads additional fanart from third party sources (well, link to
|
||||||
if item.get('type') in ('movie', 'show'):
|
fanart only).
|
||||||
|
|
||||||
|
allartworks = {
|
||||||
|
'Primary': "",
|
||||||
|
'Art': "",
|
||||||
|
'Banner': "",
|
||||||
|
'Logo': "",
|
||||||
|
'Thumb': "",
|
||||||
|
'Disc': "",
|
||||||
|
'Backdrop': []
|
||||||
|
}
|
||||||
|
"""
|
||||||
externalId = self.getExternalItemId()
|
externalId = self.getExternalItemId()
|
||||||
if externalId is not None:
|
if externalId is not None:
|
||||||
allartworks = self.getFanartTVArt(externalId, allartworks)
|
allartworks = self.getFanartTVArt(externalId, allartworks)
|
||||||
|
|
|
@ -373,3 +373,30 @@ class Embydb_Functions():
|
||||||
query = "DELETE FROM emby WHERE emby_id LIKE ?"
|
query = "DELETE FROM emby WHERE emby_id LIKE ?"
|
||||||
self.embycursor.execute(query, (plexid+"%",))
|
self.embycursor.execute(query, (plexid+"%",))
|
||||||
|
|
||||||
|
def itemsByType(self, plextype):
|
||||||
|
"""
|
||||||
|
Returns a list of dictionaries for all Kodi DB items present for
|
||||||
|
plextype. One dict is of the type
|
||||||
|
|
||||||
|
{
|
||||||
|
'plexId': the Plex id
|
||||||
|
'kodiId': the Kodi id
|
||||||
|
'kodi_type': e.g. 'movie', 'tvshow'
|
||||||
|
'plex_type': e.g. 'Movie', 'Series', the input plextype
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT emby_id, kodi_id, media_type",
|
||||||
|
"FROM emby",
|
||||||
|
"WHERE emby_type = ?",
|
||||||
|
))
|
||||||
|
self.embycursor.execute(query, (plextype, ))
|
||||||
|
result = []
|
||||||
|
for row in self.embycursor.fetchall():
|
||||||
|
result.append({
|
||||||
|
'plexId': row[0],
|
||||||
|
'kodiId': row[1],
|
||||||
|
'kodi_type': row[2],
|
||||||
|
'plex_type': plextype
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
|
@ -65,6 +65,28 @@ class Items(object):
|
||||||
self.kodiconn.close()
|
self.kodiconn.close()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@CatchExceptions(warnuser=True)
|
||||||
|
def getfanart(self, item, kodiId, mediaType, allartworks=None):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
API = PlexAPI.API(item)
|
||||||
|
if allartworks is None:
|
||||||
|
allartworks = API.getAllArtwork()
|
||||||
|
self.artwork.addArtwork(API.getFanartArtwork(allartworks),
|
||||||
|
kodiId,
|
||||||
|
mediaType,
|
||||||
|
self.kodicursor)
|
||||||
|
# Also get artwork for collections/movie sets
|
||||||
|
if mediaType == 'movie':
|
||||||
|
for setname in API.getCollections():
|
||||||
|
log.debug('Getting artwork for movie set %s' % setname)
|
||||||
|
setid = self.kodi_db.createBoxset(setname)
|
||||||
|
self.artwork.addArtwork(API.getSetArtwork(),
|
||||||
|
setid,
|
||||||
|
"set",
|
||||||
|
self.kodicursor)
|
||||||
|
self.kodi_db.assignBoxset(setid, kodiId)
|
||||||
|
|
||||||
def itemsbyId(self, items, process, pdialog=None):
|
def itemsbyId(self, items, process, pdialog=None):
|
||||||
# Process items by itemid. Process can be added, update, userdata, remove
|
# Process items by itemid. Process can be added, update, userdata, remove
|
||||||
embycursor = self.embycursor
|
embycursor = self.embycursor
|
||||||
|
@ -485,7 +507,7 @@ class Movies(Items):
|
||||||
tags.append("Favorite movies")
|
tags.append("Favorite movies")
|
||||||
self.kodi_db.addTags(movieid, tags, "movie")
|
self.kodi_db.addTags(movieid, tags, "movie")
|
||||||
# Add any sets from Plex collection tags
|
# Add any sets from Plex collection tags
|
||||||
self.kodi_db.addSets(movieid, collections, kodicursor, API)
|
self.kodi_db.addSets(movieid, collections, kodicursor)
|
||||||
# Process playstates
|
# Process playstates
|
||||||
self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
|
self.kodi_db.addPlaystate(fileid, resume, runtime, playcount, dateplayed)
|
||||||
|
|
||||||
|
|
|
@ -508,6 +508,48 @@ class Kodidb_Functions():
|
||||||
|
|
||||||
self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor)
|
self.artwork.addOrUpdateArt(thumb, actorid, arttype, "thumb", self.cursor)
|
||||||
|
|
||||||
|
def existingArt(self, kodiId, mediaType, refresh=False):
|
||||||
|
"""
|
||||||
|
For kodiId, returns an artwork dict with already existing art from
|
||||||
|
the Kodi db
|
||||||
|
"""
|
||||||
|
# Only get EITHER poster OR thumb (should have same URL)
|
||||||
|
kodiToPKC = {
|
||||||
|
'banner': 'Banner',
|
||||||
|
'clearart': 'Art',
|
||||||
|
'clearlogo': 'Logo',
|
||||||
|
'discart': 'Disc',
|
||||||
|
'landscape': 'Thumb',
|
||||||
|
'thumb': 'Primary'
|
||||||
|
}
|
||||||
|
# BoxRear yet unused
|
||||||
|
result = {'BoxRear': ''}
|
||||||
|
for art in kodiToPKC:
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT url",
|
||||||
|
"FROM art",
|
||||||
|
"WHERE media_id = ?",
|
||||||
|
"AND media_type = ?",
|
||||||
|
"AND type = ?"
|
||||||
|
))
|
||||||
|
self.cursor.execute(query, (kodiId, mediaType, art,))
|
||||||
|
try:
|
||||||
|
url = self.cursor.fetchone()[0]
|
||||||
|
except TypeError:
|
||||||
|
url = ""
|
||||||
|
result[kodiToPKC[art]] = url
|
||||||
|
# There may be several fanart URLs saved
|
||||||
|
query = ' '.join((
|
||||||
|
"SELECT url",
|
||||||
|
"FROM art",
|
||||||
|
"WHERE media_id = ?",
|
||||||
|
"AND media_type = ?",
|
||||||
|
"AND type LIKE ?"
|
||||||
|
))
|
||||||
|
data = self.cursor.execute(query, (kodiId, mediaType, "fanart%",))
|
||||||
|
result['Backdrop'] = [d[0] for d in data]
|
||||||
|
return result
|
||||||
|
|
||||||
def addGenres(self, kodiid, genres, mediatype):
|
def addGenres(self, kodiid, genres, mediatype):
|
||||||
|
|
||||||
|
|
||||||
|
@ -1180,15 +1222,9 @@ class Kodidb_Functions():
|
||||||
))
|
))
|
||||||
self.cursor.execute(query, (kodiid, mediatype, tag_id,))
|
self.cursor.execute(query, (kodiid, mediatype, tag_id,))
|
||||||
|
|
||||||
def addSets(self, movieid, collections, kodicursor, API):
|
def addSets(self, movieid, collections, kodicursor):
|
||||||
for setname in collections:
|
for setname in collections:
|
||||||
setid = self.createBoxset(setname)
|
setid = self.createBoxset(setname)
|
||||||
# Process artwork
|
|
||||||
if settings('setFanartTV') == 'true':
|
|
||||||
self.artwork.addArtwork(API.getSetArtwork(),
|
|
||||||
setid,
|
|
||||||
"set",
|
|
||||||
kodicursor)
|
|
||||||
self.assignBoxset(setid, movieid)
|
self.assignBoxset(setid, movieid)
|
||||||
|
|
||||||
def createBoxset(self, boxsetname):
|
def createBoxset(self, boxsetname):
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import logging
|
import logging
|
||||||
from threading import Thread, Lock
|
from threading import Thread, Lock
|
||||||
import Queue
|
import Queue
|
||||||
|
from random import shuffle
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
@ -255,6 +256,100 @@ class ThreadedShowSyncInfo(Thread):
|
||||||
log.debug('Dialog Infobox thread terminated')
|
log.debug('Dialog Infobox thread terminated')
|
||||||
|
|
||||||
|
|
||||||
|
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
||||||
|
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
||||||
|
@ThreadMethods
|
||||||
|
class ProcessFanartThread(Thread):
|
||||||
|
"""
|
||||||
|
Threaded download of additional fanart in the background
|
||||||
|
|
||||||
|
Input:
|
||||||
|
queue Queue.Queue() object that you will need to fill with
|
||||||
|
dicts of the following form:
|
||||||
|
{
|
||||||
|
'itemId': the Plex id as a string
|
||||||
|
'class': the itemtypes class, e.g. 'Movies'
|
||||||
|
'mediaType': the kodi media type, e.g. 'movie'
|
||||||
|
'refresh': True/False if true, will overwrite any 3rd party
|
||||||
|
fanart. If False, will only get missing
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
def __init__(self, queue):
|
||||||
|
self.queue = queue
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
threadStopped = self.threadStopped
|
||||||
|
threadSuspended = self.threadSuspended
|
||||||
|
queue = self.queue
|
||||||
|
log.debug('Started Fanart thread')
|
||||||
|
while not threadStopped():
|
||||||
|
# In the event the server goes offline
|
||||||
|
while threadSuspended():
|
||||||
|
# Set in service.py
|
||||||
|
if threadStopped():
|
||||||
|
# Abort was requested while waiting. We should exit
|
||||||
|
log.debug('Fanart thread terminated while suspended')
|
||||||
|
break
|
||||||
|
xbmc.sleep(1000)
|
||||||
|
# grabs Plex item from queue
|
||||||
|
try:
|
||||||
|
item = queue.get(block=False)
|
||||||
|
except Queue.Empty:
|
||||||
|
xbmc.sleep(50)
|
||||||
|
continue
|
||||||
|
if item['refresh'] is True:
|
||||||
|
# Leave the Plex art untouched
|
||||||
|
allartworks = None
|
||||||
|
else:
|
||||||
|
with embydb.GetEmbyDB() as emby_db:
|
||||||
|
try:
|
||||||
|
kodiId = emby_db.getItem_byId(item['itemId'])[0]
|
||||||
|
except TypeError:
|
||||||
|
log.error('Could not get Kodi id for plex id %s'
|
||||||
|
% item['itemId'])
|
||||||
|
queue.task_done()
|
||||||
|
continue
|
||||||
|
with kodidb.GetKodiDB('video') as kodi_db:
|
||||||
|
allartworks = kodi_db.existingArt(kodiId,
|
||||||
|
item['mediaType'])
|
||||||
|
# Check if we even need to get additional art
|
||||||
|
needsupdate = False
|
||||||
|
for key, value in allartworks.iteritems():
|
||||||
|
if not value and not key == 'BoxRear':
|
||||||
|
needsupdate = True
|
||||||
|
if needsupdate is False:
|
||||||
|
log.debug('Already got all art for Plex id %s'
|
||||||
|
% item['itemId'])
|
||||||
|
queue.task_done()
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.debug('Getting additional fanart for Plex id %s'
|
||||||
|
% item['itemId'])
|
||||||
|
# Download Metadata
|
||||||
|
xml = PF.GetPlexMetadata(item['itemId'])
|
||||||
|
if xml is None:
|
||||||
|
# Did not receive a valid XML - skip that item for now
|
||||||
|
log.warn("Could not get metadata for %s. Skipping that item "
|
||||||
|
"for now" % item['itemId'])
|
||||||
|
queue.task_done()
|
||||||
|
continue
|
||||||
|
elif xml == 401:
|
||||||
|
log.warn('HTTP 401 returned by PMS. Too much strain? '
|
||||||
|
'Cancelling sync for now')
|
||||||
|
# Kill remaining items in queue (for main thread to cont.)
|
||||||
|
queue.task_done()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Do the work
|
||||||
|
with getattr(itemtypes, item['class'])() as cls:
|
||||||
|
cls.getfanart(xml[0], kodiId, item['mediaType'], allartworks)
|
||||||
|
# signals to queue job is done
|
||||||
|
log.debug('Done getting fanart for Plex id %s' % item['itemId'])
|
||||||
|
queue.task_done()
|
||||||
|
log.debug('Fanart thread terminated')
|
||||||
|
|
||||||
|
|
||||||
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
@ThreadMethodsAdditionalSuspend('suspend_LibraryThread')
|
||||||
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
@ThreadMethodsAdditionalStop('plex_shouldStop')
|
||||||
@ThreadMethods
|
@ThreadMethods
|
||||||
|
@ -275,6 +370,9 @@ class LibrarySync(Thread):
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.itemsToProcess = []
|
self.itemsToProcess = []
|
||||||
self.sessionKeys = []
|
self.sessionKeys = []
|
||||||
|
if settings('FanartTV') == 'true':
|
||||||
|
self.fanartqueue = Queue.Queue()
|
||||||
|
self.fanartthread = ProcessFanartThread(self.fanartqueue)
|
||||||
# How long should we wait at least to process new/changed PMS items?
|
# How long should we wait at least to process new/changed PMS items?
|
||||||
self.saftyMargin = int(settings('saftyMargin'))
|
self.saftyMargin = int(settings('saftyMargin'))
|
||||||
|
|
||||||
|
@ -887,6 +985,16 @@ class LibrarySync(Thread):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
log.info("Sync threads finished")
|
log.info("Sync threads finished")
|
||||||
|
if settings('FanartTV') == 'true':
|
||||||
|
# Save to queue for later processing
|
||||||
|
typus = {'Movies': 'movie', 'TVShows': 'tvshow'}[itemType]
|
||||||
|
for item in self.updatelist:
|
||||||
|
self.fanartqueue.put({
|
||||||
|
'itemId': item['itemId'],
|
||||||
|
'class': itemType,
|
||||||
|
'mediaType': typus,
|
||||||
|
'refresh': False
|
||||||
|
})
|
||||||
self.updatelist = []
|
self.updatelist = []
|
||||||
|
|
||||||
@LogTime
|
@LogTime
|
||||||
|
@ -1484,6 +1592,30 @@ class LibrarySync(Thread):
|
||||||
with itemFkt() as Fkt:
|
with itemFkt() as Fkt:
|
||||||
Fkt.updatePlaystate(item)
|
Fkt.updatePlaystate(item)
|
||||||
|
|
||||||
|
def fanartSync(self, refresh=False):
|
||||||
|
"""
|
||||||
|
Checks all Plex movies and TV shows whether they still need fanart
|
||||||
|
|
||||||
|
refresh=True Force refresh all external fanart
|
||||||
|
"""
|
||||||
|
items = []
|
||||||
|
typus = {
|
||||||
|
'Movie': 'Movies',
|
||||||
|
'Series': 'TVShows'
|
||||||
|
}
|
||||||
|
with embydb.GetEmbyDB() as emby_db:
|
||||||
|
for plextype in typus:
|
||||||
|
items.extend(emby_db.itemsByType(plextype))
|
||||||
|
# Shuffle the list to not always start out identically
|
||||||
|
items = shuffle(items)
|
||||||
|
for item in items:
|
||||||
|
self.fanartqueue.put({
|
||||||
|
'itemId': item['plexId'],
|
||||||
|
'mediaType': item['kodi_type'],
|
||||||
|
'class': typus[item['plex_type']],
|
||||||
|
'refresh': refresh
|
||||||
|
})
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
self.run_internal()
|
self.run_internal()
|
||||||
|
@ -1508,6 +1640,7 @@ class LibrarySync(Thread):
|
||||||
fullSyncInterval = self.fullSyncInterval
|
fullSyncInterval = self.fullSyncInterval
|
||||||
lastSync = 0
|
lastSync = 0
|
||||||
lastTimeSync = 0
|
lastTimeSync = 0
|
||||||
|
lastFanartSync = 0
|
||||||
lastProcessing = 0
|
lastProcessing = 0
|
||||||
oneDay = 60*60*24
|
oneDay = 60*60*24
|
||||||
|
|
||||||
|
@ -1527,6 +1660,9 @@ class LibrarySync(Thread):
|
||||||
if self.enableMusic:
|
if self.enableMusic:
|
||||||
advancedSettingsXML()
|
advancedSettingsXML()
|
||||||
|
|
||||||
|
if settings('FanartTV') == 'true':
|
||||||
|
self.fanartthread.start()
|
||||||
|
|
||||||
while not threadStopped():
|
while not threadStopped():
|
||||||
|
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
|
@ -1661,6 +1797,12 @@ class LibrarySync(Thread):
|
||||||
window('plex_dbScan', value="true")
|
window('plex_dbScan', value="true")
|
||||||
self.syncPMStime()
|
self.syncPMStime()
|
||||||
window('plex_dbScan', clear=True)
|
window('plex_dbScan', clear=True)
|
||||||
|
elif (now - lastFanartSync > oneDay and
|
||||||
|
settings('FanartTV') == 'true'):
|
||||||
|
lastFanartSync = now
|
||||||
|
log.info('Starting daily fanart sync')
|
||||||
|
self.fanartSync()
|
||||||
|
log.info('Finished init of daily fanart sync')
|
||||||
elif enableBackgroundSync:
|
elif enableBackgroundSync:
|
||||||
# Check back whether we should process something
|
# Check back whether we should process something
|
||||||
# Only do this once every 10 seconds
|
# Only do this once every 10 seconds
|
||||||
|
|
Loading…
Reference in a new issue