PlexKodiConnect/resources/lib/library_sync/full_sync.py

228 lines
9 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
import xbmc
2018-10-20 23:49:04 +11:00
from .get_metadata import GetMetadataTask, reset_collections
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-11-02 01:43:43 +11:00
LOG = getLogger('PLEX.sync.full_sync')
2018-10-20 23:49:04 +11:00
class FullSync(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._canceled = False
2018-10-20 23:49:04 +11:00
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
2018-11-05 02:53:42 +11:00
self.current_sync = None
2018-10-29 02:14:37 +11:00
self.plexdb = None
2018-10-22 01:56:13 +11:00
self.plex_type = None
2018-11-05 02:53:42 +11:00
self.section_type = None
2018-10-22 03:32:11 +11:00
self.processing_thread = None
2018-10-25 22:27:12 +11:00
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
self.threader = backgroundthread.ThreaderManager(
worker=backgroundthread.NonstoppingBackgroundWorker)
2018-10-20 23:49:04 +11:00
super(FullSync, self).__init__()
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-26 00:57:12 +11:00
plex_id = int(xml_item.get('ratingKey'))
2018-11-05 02:53:42 +11:00
if not self.repair and self.plexdb.checksum(plex_id, self.plex_type) == \
int('%s%s' % (plex_id,
xml_item.get('updatedAt',
xml_item.get('addedAt', 1541572987)))):
2018-11-05 02:53:42 +11:00
# Already got EXACTLY this item in our DB
self.plexdb.update_last_sync(plex_id,
self.plex_type,
self.current_sync)
return
2018-10-26 02:50:59 +11:00
task = GetMetadataTask()
task.setup(self.queue, plex_id, self.plex_type, self.get_children)
self.threader.addTask(task)
2018-10-20 23:49:04 +11:00
2018-10-22 01:56:13 +11:00
def process_delete(self):
"""
2018-11-05 02:53:42 +11:00
Removes all the items that have NOT been updated (last_sync timestamp
is different)
2018-10-22 01:56:13 +11:00
"""
2018-11-05 02:53:42 +11:00
with self.context(self.current_sync) as c:
2018-10-29 02:14:37 +11:00
for plex_id in self.plexdb.plex_id_by_last_sync(self.plex_type,
2018-11-05 02:53:42 +11:00
self.current_sync):
2018-10-22 01:56:13 +11:00
if self.isCanceled():
return
c.remove(plex_id, plex_type=self.plex_type)
2018-11-05 02:53:42 +11:00
@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)
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-11-05 02:53:42 +11:00
successful = True
LOG.debug('Start processing %ss', self.section_type)
2018-10-25 22:25:25 +11:00
sects = (x for x in sections.SECTIONS
2018-11-05 02:53:42 +11:00
if x['plex_type'] == self.section_type)
2018-10-25 22:25:25 +11:00
for section in sects:
2018-10-20 23:49:04 +11:00
LOG.debug('Processing library section %s', section)
if self.isCanceled():
return False
if not self.install_sync_done:
state.PATH_VERIFIED = False
try:
2018-11-05 02:53:42 +11:00
# Sync new, updated and deleted items
2018-10-26 00:54:22 +11:00
iterator = PF.SectionItems(section['section_id'],
2018-11-05 02:53:42 +11:00
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)),
2018-10-26 03:28:41 +11:00
iterator.get('librarySectionTitle'),
2018-11-05 02:53:42 +11:00
section['section_id'])
2018-10-21 21:03:21 +11:00
self.queue.put(queue_info)
2018-10-29 02:14:37 +11:00
with PlexDB() as self.plexdb:
for xml_item in iterator:
if self.isCanceled():
return False
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-11-05 02:53:42 +11:00
successful = False
2018-10-21 21:03:21 +11:00
continue
LOG.debug('Waiting for download threads to finish')
while self.threader.threader.working():
xbmc.sleep(100)
2018-11-05 02:53:42 +11:00
LOG.debug('Waiting for processing thread to finish section')
2018-10-23 22:54:09 +11:00
self.queue.join()
reset_collections()
2018-11-05 02:53:42 +11:00
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)
# Ensure that the DB connection is closed to commit the
# changes above - avoids "Item not yet synced" error
self.queue.join()
2018-11-05 02:53:42 +11:00
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')
2018-10-22 01:56:13 +11:00
LOG.debug('Finished processing %ss', self.plex_type)
2018-11-05 02:53:42 +11:00
return successful
2018-10-20 23:49:04 +11:00
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-11-05 02:53:42 +11:00
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE, itemtypes.Movie, False),
(v.PLEX_TYPE_SHOW, v.PLEX_TYPE_SHOW, itemtypes.Show, False),
(v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW, itemtypes.Season, False),
(v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW, itemtypes.Episode, False)
2018-10-21 21:03:21 +11:00
]
2018-10-23 22:54:09 +11:00
if state.ENABLE_MUSIC:
2018-10-25 22:22:34 +11:00
kinds.extend([
2018-11-05 02:53:42 +11:00
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST, itemtypes.Album, True),
2018-11-04 04:47:51 +11:00
])
2018-10-29 02:14:37 +11:00
with PlexDB() as self.plexdb:
for kind in kinds:
# Setup our variables
self.plex_type = kind[0]
2018-11-05 02:53:42 +11:00
self.section_type = kind[1]
self.context = kind[2]
self.get_children = kind[3]
2018-10-29 02:14:37 +11:00
# Now do the heavy lifting
2018-10-23 22:54:09 +11:00
if self.isCanceled() or not self.process_kind():
return False
2018-11-05 02:53:42 +11:00
# Delete movies that are not on Plex anymore
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-11-05 02:53:42 +11:00
self.current_sync = utils.unix_timestamp()
2018-10-24 16:08:32 +11:00
# 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
2018-11-05 02:53:42 +11:00
self.queue = backgroundthread.Queue.Queue(maxsize=400)
2018-10-22 03:32:11 +11:00
self.processing_thread = process_metadata.ProcessMetadata(
2018-11-05 02:53:42 +11:00
self.queue, self.current_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
2018-11-05 02:53:42 +11:00
LOG.info('Running full_library_sync with repair=%s',
2018-10-23 22:54:09 +11:00
self.repair)
2018-10-20 23:49:04 +11:00
if not self.full_library_sync():
return
2018-11-05 02:53:42 +11:00
# Tell the processing thread to exit with one last element None
self.queue.put(None)
2018-10-20 23:49:04 +11:00
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-11-05 02:53:42 +11:00
# This will block until the processing thread really exits
2018-10-22 03:32:11 +11:00
LOG.debug('Waiting for processing thread to exit')
self.processing_thread.join()
common.update_kodi_library(video=True, music=True)
self.threader.shutdown()
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
"""
"""
# FullSync(repair, callback, show_dialog).start()
FullSync(repair, callback, show_dialog).run()