PlexKodiConnect/resources/lib/library_sync/full_sync.py

230 lines
8.7 KiB
Python
Raw Normal View History

2018-10-20 23:49:04 +11:00
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from .get_metadata import GetMetadataTask
2018-10-23 22:54:09 +11:00
from . import common, process_metadata, sections
from .. import utils, backgroundthread, variables as v, state
2018-10-20 23:49:04 +11:00
from .. import plex_functions as PF, itemtypes
2018-10-23 22:54:09 +11:00
from ..plex_db import PlexDB
2018-10-20 23:49:04 +11:00
2018-10-23 22:54:09 +11:00
if (v.PLATFORM != 'Microsoft UWP' and
utils.settings('enablePlaylistSync') == 'true'):
# Xbox cannot use watchdog, a dependency for PKC playlist features
from .. import playlists
PLAYLIST_SYNC_ENABLED = True
else:
PLAYLIST_SYNC_ENABLED = False
2018-10-20 23:49:04 +11:00
2018-10-23 22:54:09 +11:00
LOG = getLogger('PLEX.library_sync.full_sync')
2018-10-20 23:49:04 +11:00
2018-10-21 21:03:21 +11:00
class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
2018-10-24 16:08:32 +11:00
def __init__(self, repair, callback, show_dialog):
2018-10-20 23:49:04 +11:00
"""
repair=True: force sync EVERY item
"""
self.repair = repair
self.callback = callback
2018-10-24 16:08:32 +11:00
self.show_dialog = show_dialog
2018-10-21 21:03:21 +11:00
self.queue = None
self.process_thread = None
self.last_sync = None
self.plex_db = None
2018-10-22 01:56:13 +11:00
self.plex_type = None
2018-10-22 03:32:11 +11:00
self.processing_thread = None
2018-10-20 23:49:04 +11:00
super(FullSync, self).__init__()
2018-10-24 19:57:52 +11:00
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)
2018-10-22 01:56:13 +11:00
def process_item(self, xml_item):
2018-10-20 23:49:04 +11:00
"""
Processes a single library item
"""
2018-10-21 21:03:21 +11:00
plex_id = int(xml_item['ratingKey'])
2018-10-20 23:49:04 +11:00
if self.new_items_only:
if self.plex_db.check_plexid(plex_id) is None:
backgroundthread.BGThreader.addTask(
2018-10-21 21:03:21 +11:00
GetMetadataTask().setup(self.queue,
2018-10-20 23:49:04 +11:00
plex_id,
2018-10-22 01:56:13 +11:00
self.get_children))
2018-10-20 23:49:04 +11:00
else:
if self.plex_db.check_checksum(
2018-10-21 21:03:21 +11:00
int('%s%s' % (xml_item['ratingKey'],
xml_item['updatedAt']))) is None:
backgroundthread.BGThreader.addTask(
GetMetadataTask().setup(self.queue,
plex_id,
2018-10-22 01:56:13 +11:00
self.get_children))
2018-10-21 21:03:21 +11:00
else:
self.plex_db.update_last_sync(plex_id, self.last_sync)
2018-10-20 23:49:04 +11:00
2018-10-22 01:56:13 +11:00
def process_delete(self):
"""
Removes all the items that have NOT been updated (last_sync timestamp)
is different
"""
with self.context() as c:
for plex_id in self.plex_db.plex_id_by_last_sync(self.plex_type,
self.last_sync):
if self.isCanceled():
return
c.remove(plex_id, plex_type=self.plex_type)
2018-10-21 21:03:21 +11:00
@utils.log_time
2018-10-22 01:56:13 +11:00
def process_kind(self):
2018-10-20 23:49:04 +11:00
"""
"""
2018-10-22 01:56:13 +11:00
LOG.debug('Start processing %ss', self.plex_type)
sections = (x for x in sections.SECTIONS
if x['plex_type'] == self.plex_type)
2018-10-20 23:49:04 +11:00
for section in sections:
LOG.debug('Processing library section %s', section)
if self.isCanceled():
return False
if not self.install_sync_done:
state.PATH_VERIFIED = False
try:
2018-10-22 01:56:13 +11:00
iterator = PF.SectionItems(
section['id'],
{'type': v.PLEX_TYPE_NUMBER_FROM_PLEX_TYPE[self.plex_type]})
2018-10-21 21:03:21 +11:00
# Tell the processing thread about this new section
queue_info = process_metadata.InitNewSection(
2018-10-22 01:56:13 +11:00
self.context,
2018-10-21 21:03:21 +11:00
utils.cast(int, iterator.get('totalSize', 0)),
utils.cast(unicode, iterator.get('librarySectionTitle')),
2018-10-24 19:57:52 +11:00
section['id'],
utils.cast(unicode, section['viewGroup']))
2018-10-21 21:03:21 +11:00
self.queue.put(queue_info)
for xml_item in iterator:
2018-10-20 23:49:04 +11:00
if self.isCanceled():
return False
2018-10-22 01:56:13 +11:00
self.process_item(xml_item)
2018-10-20 23:49:04 +11:00
except RuntimeError:
LOG.error('Could not entirely process section %s', section)
2018-10-21 21:03:21 +11:00
continue
2018-10-23 22:54:09 +11:00
self.queue.join()
2018-10-22 01:56:13 +11:00
LOG.debug('Finished processing %ss', self.plex_type)
2018-10-20 23:49:04 +11:00
return True
2018-10-22 01:56:13 +11:00
def full_library_sync(self):
2018-10-20 23:49:04 +11:00
"""
"""
2018-10-21 21:03:21 +11:00
kinds = [
2018-10-22 01:56:13 +11:00
(v.PLEX_TYPE_MOVIE, itemtypes.Movie, False),
(v.PLEX_TYPE_SHOW, itemtypes.Show, False),
(v.PLEX_TYPE_SEASON, itemtypes.Season, False),
2018-10-23 22:54:09 +11:00
(v.PLEX_TYPE_EPISODE, itemtypes.Episode, False)
2018-10-21 21:03:21 +11:00
]
2018-10-23 22:54:09 +11:00
if state.ENABLE_MUSIC:
kinds.extend(
(v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
(v.PLEX_TYPE_ALBUM, itemtypes.Album, True),
(v.PLEX_TYPE_SONG, itemtypes.Song, False))
2018-10-22 01:56:13 +11:00
for kind in kinds:
# Setup our variables
self.plex_type = kind[0]
self.context = kind[1]
self.get_children = kind[2]
# Now do the heavy lifting
2018-10-23 22:54:09 +11:00
with PlexDB() as self.plex_db:
if self.isCanceled() or not self.process_kind():
return False
if self.new_items_only:
# Delete movies that are not on Plex anymore - do this only once
self.process_delete()
2018-10-20 23:49:04 +11:00
return True
@utils.log_time
def run(self):
2018-10-22 03:32:11 +11:00
if self.isCanceled():
return
2018-10-23 22:54:09 +11:00
successful = False
2018-10-24 16:08:32 +11:00
self.last_sync = utils.unix_timestamp()
# Delete playlist and video node files from Kodi
utils.delete_playlists()
utils.delete_nodes()
# Get latest Plex libraries and build playlist and video node files
2018-10-22 03:32:11 +11:00
if not sections.sync_from_pms():
return
2018-10-20 23:49:04 +11:00
try:
2018-10-22 03:32:11 +11:00
# Fire up our single processing thread
self.queue = backgroundthread.Queue.Queue(maxsize=200)
self.processing_thread = process_metadata.ProcessMetadata(
2018-10-24 16:08:32 +11:00
self.queue, self.last_sync, self.show_dialog)
2018-10-22 03:32:11 +11:00
self.processing_thread.start()
2018-10-23 22:54:09 +11:00
# Actual syncing - do only new items first
LOG.info('Running fullsync for **NEW** items with repair=%s',
self.repair)
self.new_items_only = True
2018-10-20 23:49:04 +11:00
# This will also update playstates and userratings!
2018-10-23 22:54:09 +11:00
if not self.full_library_sync():
2018-10-20 23:49:04 +11:00
return
if self.isCanceled():
return
# This will NOT update playstates and userratings!
2018-10-23 22:54:09 +11:00
LOG.info('Running fullsync for **CHANGED** items with repair=%s',
2018-10-20 23:49:04 +11:00
self.repair)
2018-10-23 22:54:09 +11:00
self.new_items_only = False
2018-10-20 23:49:04 +11:00
if not self.full_library_sync():
return
if self.isCanceled():
return
if PLAYLIST_SYNC_ENABLED and not playlists.full_sync():
return
successful = True
except:
utils.ERROR(txt='full_sync.py crashed', notify=True)
finally:
2018-10-22 03:32:11 +11:00
# Last element will kill the processing thread (if not already
# 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')
self.processing_thread.join()
2018-10-24 16:08:32 +11:00
if self.callback:
self.callback(successful)
2018-10-22 03:32:11 +11:00
LOG.info('Done full_sync')
2018-10-20 23:49:04 +11:00
2018-10-24 16:08:32 +11:00
def start(show_dialog, repair=False, callback=None):
2018-10-20 23:49:04 +11:00
"""
"""
2018-10-23 22:54:09 +11:00
# backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback))
2018-10-24 16:08:32 +11:00
FullSync(repair, callback, show_dialog).start()