More hacking

This commit is contained in:
croneter 2018-11-04 16:53:42 +01:00
parent 2fcb43b9d9
commit 48a530b49a
6 changed files with 117 additions and 119 deletions

View file

@ -80,34 +80,29 @@ class ItemBase(object):
kodi_type, kodi_type,
self.kodicursor) self.kodicursor)
def updateUserdata(self, xml): def update_userdata(self, xml_element, plex_type):
""" """
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: api = API(xml_element)
api = API(mediaitem) # Get key and db entry on the Kodi db side
# Get key and db entry on the Kodi db side db_item = self.plexdb.item_by_id(api.plex_id(), plex_type)
db_item = self.plexdb.getItem_byId(api.plex_id()) if not db_item:
try: LOG.error('Item not yet synced: %s', xml_element.attrib)
fileid = db_item[1] return
except TypeError: # Grab the user's viewcount, resume points etc. from PMS' answer
continue userdata = api.userdata()
# Grab the user's viewcount, resume points etc. from PMS' answer # Write to Kodi DB
userdata = api.userdata() self.kodi_db.set_resume(db_item['kodi_fileid'],
# Write to Kodi DB userdata['Resume'],
self.kodi_db.set_resume(fileid, userdata['Runtime'],
userdata['Resume'], userdata['PlayCount'],
userdata['Runtime'], userdata['LastPlayedDate'],
userdata['PlayCount'], plex_type)
userdata['LastPlayedDate'], self.kodi_db.update_userrating(db_item['kodi_id'],
api.plex_type()) db_item['kodi_type'],
if v.KODIVERSION >= 17: userdata['UserRating'])
self.kodi_db.update_userrating(db_item[0],
db_item[4],
userdata['UserRating'])
def update_playstate(self, mark_played, view_count, resume, duration, def update_playstate(self, mark_played, view_count, resume, duration,
kodi_fileid, lastViewedAt, plex_type): kodi_fileid, lastViewedAt, plex_type):

View file

@ -9,7 +9,6 @@ from .. import utils, backgroundthread, variables as v, state
from .. import plex_functions as PF, itemtypes from .. import plex_functions as PF, itemtypes
from ..plex_db import PlexDB from ..plex_db import PlexDB
if (v.PLATFORM != 'Microsoft UWP' and if (v.PLATFORM != 'Microsoft UWP' and
utils.settings('enablePlaylistSync') == 'true'): utils.settings('enablePlaylistSync') == 'true'):
# Xbox cannot use watchdog, a dependency for PKC playlist features # Xbox cannot use watchdog, a dependency for PKC playlist features
@ -18,7 +17,6 @@ if (v.PLATFORM != 'Microsoft UWP' and
else: else:
PLAYLIST_SYNC_ENABLED = False PLAYLIST_SYNC_ENABLED = False
LOG = getLogger('PLEX.sync.full_sync') LOG = getLogger('PLEX.sync.full_sync')
@ -33,84 +31,63 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
self.show_dialog = show_dialog self.show_dialog = show_dialog
self.queue = None self.queue = None
self.process_thread = None self.process_thread = None
self.last_sync = None self.current_sync = None
self.plexdb = None self.plexdb = None
self.plex_type = None self.plex_type = None
self.section_type = None
self.processing_thread = None self.processing_thread = None
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true' self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
super(FullSync, self).__init__() super(FullSync, self).__init__()
def plex_update_watched(self, viewId, item_class, lastViewedAt=None,
updatedAt=None):
"""
YET to implement
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
"""
if self.new_items_only is False:
# Only do this once for fullsync: the first run where new items are
# added to Kodi
return
xml = PF.GetAllPlexLeaves(viewId,
lastViewedAt=lastViewedAt,
updatedAt=updatedAt)
# Return if there are no items in PMS reply - it's faster
try:
xml[0].attrib
except (TypeError, AttributeError, IndexError):
LOG.error('Error updating watch status. Could not get viewId: '
'%s of item_class %s with lastViewedAt: %s, updatedAt: '
'%s', viewId, item_class, lastViewedAt, updatedAt)
return
if item_class in ('Movies', 'TVShows'):
self.update_kodi_video_library = True
elif item_class == 'Music':
self.update_kodi_music_library = True
with getattr(itemtypes, item_class)() as itemtype:
itemtype.updateUserdata(xml)
def process_item(self, xml_item): def process_item(self, xml_item):
""" """
Processes a single library item Processes a single library item
""" """
plex_id = int(xml_item.get('ratingKey')) plex_id = int(xml_item.get('ratingKey'))
if self.new_items_only: if not self.repair and self.plexdb.checksum(plex_id, self.plex_type) == \
if self.plexdb.is_recorded(plex_id, self.plex_type): int('%s%s' % (plex_id,
return xml_item.get('updatedAt'))):
else: # Already got EXACTLY this item in our DB
if self.plexdb.checksum(plex_id, self.plex_type) == \ self.plexdb.update_last_sync(plex_id,
int('%s%s' % (plex_id, self.plex_type,
xml_item.get('updatedAt'))): self.current_sync)
self.plexdb.update_last_sync(plex_id, return
self.plex_type,
self.last_sync)
return
task = GetMetadataTask() task = GetMetadataTask()
task.setup(self.queue, plex_id, self.get_children) task.setup(self.queue, plex_id, self.get_children)
backgroundthread.BGThreader.addTask(task) backgroundthread.BGThreader.addTask(task)
def process_delete(self): def process_delete(self):
""" """
Removes all the items that have NOT been updated (last_sync timestamp) Removes all the items that have NOT been updated (last_sync timestamp
is different is different)
""" """
with self.context(self.last_sync) as c: with self.context(self.current_sync) as c:
for plex_id in self.plexdb.plex_id_by_last_sync(self.plex_type, for plex_id in self.plexdb.plex_id_by_last_sync(self.plex_type,
self.last_sync): self.current_sync):
if self.isCanceled(): if self.isCanceled():
return return
c.remove(plex_id, plex_type=self.plex_type) c.remove(plex_id, plex_type=self.plex_type)
@utils.log_time
def process_playstate(self, iterator):
"""
Updates the playstate (resume point, number of views, userrating, last
played date, etc.) for all elements in the (xml-)iterator
"""
with self.context(self.current_sync) as c:
for xml_item in iterator:
if self.isCanceled():
return False
c.update_userdata(xml_item, self.plex_type)
@utils.log_time @utils.log_time
def process_kind(self): def process_kind(self):
""" """
""" """
LOG.debug('Start processing %ss', self.plex_type) successful = True
LOG.debug('Start processing %ss', self.section_type)
sects = (x for x in sections.SECTIONS sects = (x for x in sections.SECTIONS
if x['plex_type'] == self.plex_type) if x['plex_type'] == self.section_type)
for section in sects: for section in sects:
LOG.debug('Processing library section %s', section) LOG.debug('Processing library section %s', section)
if self.isCanceled(): if self.isCanceled():
@ -118,15 +95,15 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
if not self.install_sync_done: if not self.install_sync_done:
state.PATH_VERIFIED = False state.PATH_VERIFIED = False
try: try:
# Sync new, updated and deleted items
iterator = PF.SectionItems(section['section_id'], iterator = PF.SectionItems(section['section_id'],
{'type': self.plex_type}) plex_type=self.plex_type)
# Tell the processing thread about this new section # Tell the processing thread about this new section
queue_info = process_metadata.InitNewSection( queue_info = process_metadata.InitNewSection(
self.context, self.context,
utils.cast(int, iterator.get('totalSize', 0)), utils.cast(int, iterator.get('totalSize', 0)),
iterator.get('librarySectionTitle'), iterator.get('librarySectionTitle'),
section['section_id'], section['section_id'])
section['plex_type'])
self.queue.put(queue_info) self.queue.put(queue_info)
with PlexDB() as self.plexdb: with PlexDB() as self.plexdb:
for xml_item in iterator: for xml_item in iterator:
@ -135,39 +112,58 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
self.process_item(xml_item) self.process_item(xml_item)
except RuntimeError: except RuntimeError:
LOG.error('Could not entirely process section %s', section) LOG.error('Could not entirely process section %s', section)
successful = False
continue continue
LOG.debug('Waiting for processing thread to finish section')
self.queue.join() self.queue.join()
try:
# Sync playstate of every item
iterator = PF.SectionItems(section['section_id'],
plex_type=self.plex_type)
# Tell the processing thread that we're syncing playstate
queue_info = process_metadata.InitNewSection(
self.context,
utils.cast(int, iterator.get('totalSize', 0)),
iterator.get('librarySectionTitle'),
section['section_id'])
self.queue.put(queue_info)
self.process_playstate(iterator)
except RuntimeError:
LOG.error('Could not process playstate for section %s', section)
successful = False
continue
LOG.debug('Done processing playstate for section')
LOG.debug('Finished processing %ss', self.plex_type) LOG.debug('Finished processing %ss', self.plex_type)
return True return successful
def full_library_sync(self): def full_library_sync(self):
""" """
""" """
kinds = [ kinds = [
(v.PLEX_TYPE_MOVIE, itemtypes.Movie, False), (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE, itemtypes.Movie, False),
(v.PLEX_TYPE_SHOW, itemtypes.Show, False), (v.PLEX_TYPE_SHOW, v.PLEX_TYPE_SHOW, itemtypes.Show, False),
(v.PLEX_TYPE_SEASON, itemtypes.Season, False), (v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW, itemtypes.Season, False),
(v.PLEX_TYPE_EPISODE, itemtypes.Episode, False) (v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW, itemtypes.Episode, False)
] ]
if state.ENABLE_MUSIC: if state.ENABLE_MUSIC:
kinds.extend([ kinds.extend([
(v.PLEX_TYPE_ARTIST, itemtypes.Artist, False), (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
(v.PLEX_TYPE_ALBUM, itemtypes.Album, True), (v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST, itemtypes.Album, True),
(v.PLEX_TYPE_SONG, itemtypes.Song, False) (v.PLEX_TYPE_SONG, v.PLEX_TYPE_ARTIST, itemtypes.Song, False)
]) ])
with PlexDB() as self.plexdb: with PlexDB() as self.plexdb:
for kind in kinds: for kind in kinds:
# Setup our variables # Setup our variables
self.plex_type = kind[0] self.plex_type = kind[0]
self.context = kind[1] self.section_type = kind[1]
self.get_children = kind[2] self.context = kind[2]
self.get_children = kind[3]
# Now do the heavy lifting # Now do the heavy lifting
if self.isCanceled() or not self.process_kind(): if self.isCanceled() or not self.process_kind():
return False return False
if self.new_items_only: # Delete movies that are not on Plex anymore
# Delete movies that are not on Plex anymore - do this only once self.process_delete()
self.process_delete()
return True return True
@utils.log_time @utils.log_time
@ -175,7 +171,7 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
if self.isCanceled(): if self.isCanceled():
return return
successful = False successful = False
self.last_sync = utils.unix_timestamp() self.current_sync = utils.unix_timestamp()
# Delete playlist and video node files from Kodi # Delete playlist and video node files from Kodi
utils.delete_playlists() utils.delete_playlists()
utils.delete_nodes() utils.delete_nodes()
@ -184,26 +180,18 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
return return
try: try:
# Fire up our single processing thread # Fire up our single processing thread
self.queue = backgroundthread.Queue.Queue(maxsize=200) self.queue = backgroundthread.Queue.Queue(maxsize=400)
self.processing_thread = process_metadata.ProcessMetadata( self.processing_thread = process_metadata.ProcessMetadata(
self.queue, self.last_sync, self.show_dialog) self.queue, self.current_sync, self.show_dialog)
self.processing_thread.start() self.processing_thread.start()
# Actual syncing - do only new items first # Actual syncing - do only new items first
LOG.info('Running fullsync for **NEW** items with repair=%s', LOG.info('Running full_library_sync with repair=%s',
self.repair) self.repair)
self.new_items_only = True
# This will also update playstates and userratings!
if not self.full_library_sync():
return
if self.isCanceled():
return
# This will NOT update playstates and userratings!
LOG.info('Running fullsync for **CHANGED** items with repair=%s',
self.repair)
self.new_items_only = False
if not self.full_library_sync(): if not self.full_library_sync():
return return
# Tell the processing thread to exit with one last element None
self.queue.put(None)
if self.isCanceled(): if self.isCanceled():
return return
if PLAYLIST_SYNC_ENABLED and not playlists.full_sync(): if PLAYLIST_SYNC_ENABLED and not playlists.full_sync():
@ -212,10 +200,7 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
except: except:
utils.ERROR(txt='full_sync.py crashed', notify=True) utils.ERROR(txt='full_sync.py crashed', notify=True)
finally: finally:
# Last element will kill the processing thread (if not already # This will block until the processing thread really exits
# done so, e.g. quitting Kodi)
self.queue.put(None)
# This will block until the processing thread exits
LOG.debug('Waiting for processing thread to exit') LOG.debug('Waiting for processing thread to exit')
self.processing_thread.join() self.processing_thread.join()
if self.callback: if self.callback:

View file

@ -17,12 +17,11 @@ class InitNewSection(object):
context: itemtypes.Movie, itemtypes.Episode, etc. context: itemtypes.Movie, itemtypes.Episode, etc.
""" """
def __init__(self, context, total_number_of_items, section_name, def __init__(self, context, total_number_of_items, section_name,
section_id, plex_type): section_id):
self.context = context self.context = context
self.total = total_number_of_items self.total = total_number_of_items
self.name = section_name self.name = section_name
self.id = section_id self.id = section_id
self.plex_type = plex_type
class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin): class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):

View file

@ -47,7 +47,7 @@ def sync_pms_time():
continue continue
library_id = section.attrib['key'] library_id = section.attrib['key']
try: try:
iterator = PF.SectionItems(library_id, {'type': plex_type}) iterator = PF.SectionItems(library_id, plex_type=plex_type)
for item in iterator: for item in iterator:
if item.get('viewCount'): if item.get('viewCount'):
# Don't want to mess with items that have playcount>0 # Don't want to mess with items that have playcount>0

View file

@ -534,8 +534,18 @@ class DownloadGen(object):
Yields XML etree children or raises RuntimeError Yields XML etree children or raises RuntimeError
""" """
def __init__(self, url, args=None): def __init__(self, url, plex_type=None, last_viewed_at=None,
updated_at=None, args=None):
self._args = args or {} self._args = args or {}
url += '?'
if plex_type:
url = '%stype=%s&' % (url, v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[plex_type])
if last_viewed_at:
url = '%slastViewedAt>=%s&' % (url, last_viewed_at)
if updated_at:
url = '%supdatedAt>=%s&' % (url, updated_at)
if url.endswith('?') or url.endswith('&'):
url = url[:-1]
self._url = url self._url = url
self._pos = 0 self._pos = 0
self._exhausted = False self._exhausted = False
@ -549,6 +559,7 @@ class DownloadGen(object):
'sort': 'id', # Entries are sorted by plex_id 'sort': 'id', # Entries are sorted by plex_id
'excludeAllLeaves': 1 # PMS wont attach a first summary child 'excludeAllLeaves': 1 # PMS wont attach a first summary child
}) })
LOG.debug('DownloadGen url: %s, args: %s', self._url, self._args)
self.xml = DU().downloadUrl(self._url, parameters=self._args) self.xml = DU().downloadUrl(self._url, parameters=self._args)
try: try:
self.xml.attrib self.xml.attrib
@ -588,11 +599,11 @@ class SectionItems(DownloadGen):
""" """
Iterator object to get all items of a Plex library section Iterator object to get all items of a Plex library section
""" """
def __init__(self, section_id, args=None): def __init__(self, section_id, plex_type=None, last_viewed_at=None,
if args and 'type' in args: updated_at=None, args=None):
args['type'] = v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[args['type']] url = '{server}/library/sections/%s/all' % section_id
super(SectionItems, self).__init__( super(SectionItems, self).__init__(url, plex_type, last_viewed_at,
'{server}/library/sections/%s/all' % section_id, args) updated_at, args)
class Children(DownloadGen): class Children(DownloadGen):

View file

@ -446,6 +446,14 @@ def unix_date_to_kodi(stamp):
return localdate return localdate
def kodi_time_to_plex(stamp):
"""
Returns a Kodi timestamp (int/float) in Plex time (subtracting the
KODI_PLEX_TIME_OFFSET)
"""
return stamp - state.KODI_PLEX_TIME_OFFSET
def unix_timestamp(seconds_into_the_future=None): def unix_timestamp(seconds_into_the_future=None):
""" """
Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as