Rewire llibrary sync, part 5
This commit is contained in:
parent
23dada9fe5
commit
2f96749fc7
10 changed files with 258 additions and 1151 deletions
|
@ -123,10 +123,6 @@ class Main():
|
||||||
elif mode == 'chooseServer':
|
elif mode == 'chooseServer':
|
||||||
entrypoint.choose_pms_server()
|
entrypoint.choose_pms_server()
|
||||||
|
|
||||||
elif mode == 'refreshplaylist':
|
|
||||||
log.info('Requesting playlist/nodes refresh')
|
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'views')
|
|
||||||
|
|
||||||
elif mode == 'deviceid':
|
elif mode == 'deviceid':
|
||||||
self.deviceid()
|
self.deviceid()
|
||||||
|
|
||||||
|
|
|
@ -1036,10 +1036,6 @@ msgctxt "#39201"
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#39203"
|
|
||||||
msgid "Refresh Plex playlists/nodes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#39204"
|
msgctxt "#39204"
|
||||||
msgid "Perform manual library sync"
|
msgid "Perform manual library sync"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -170,8 +170,6 @@ def show_main_menu(content_type=None):
|
||||||
|
|
||||||
# some extra entries for settings and stuff
|
# some extra entries for settings and stuff
|
||||||
directory_item(utils.lang(39201), "plugin://%s?mode=settings" % v.ADDON_ID)
|
directory_item(utils.lang(39201), "plugin://%s?mode=settings" % v.ADDON_ID)
|
||||||
directory_item(utils.lang(39203),
|
|
||||||
"plugin://%s?mode=refreshplaylist" % v.ADDON_ID)
|
|
||||||
directory_item(utils.lang(39204),
|
directory_item(utils.lang(39204),
|
||||||
"plugin://%s?mode=manualsync" % v.ADDON_ID)
|
"plugin://%s?mode=manualsync" % v.ADDON_ID)
|
||||||
xbmcplugin.endOfDirectory(int(argv[1]))
|
xbmcplugin.endOfDirectory(int(argv[1]))
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
from .full_sync import start, PLAYLIST_SYNC_ENABLED
|
from .full_sync import start, PLAYLIST_SYNC_ENABLED
|
||||||
|
from .time import sync_pms_time
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import time
|
|
||||||
|
|
||||||
from .get_metadata import GetMetadataTask
|
from .get_metadata import GetMetadataTask
|
||||||
from . import common, process_metadata, sections
|
from . import common, process_metadata, sections
|
||||||
|
@ -24,12 +23,13 @@ LOG = getLogger('PLEX.library_sync.full_sync')
|
||||||
|
|
||||||
|
|
||||||
class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
|
class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
|
||||||
def __init__(self, repair, callback):
|
def __init__(self, repair, callback, show_dialog):
|
||||||
"""
|
"""
|
||||||
repair=True: force sync EVERY item
|
repair=True: force sync EVERY item
|
||||||
"""
|
"""
|
||||||
self.repair = repair
|
self.repair = repair
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
self.show_dialog = show_dialog
|
||||||
self.queue = None
|
self.queue = None
|
||||||
self.process_thread = None
|
self.process_thread = None
|
||||||
self.last_sync = None
|
self.last_sync = None
|
||||||
|
@ -141,14 +141,18 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return
|
return
|
||||||
successful = False
|
successful = False
|
||||||
self.last_sync = time.time()
|
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
|
||||||
if not sections.sync_from_pms():
|
if not sections.sync_from_pms():
|
||||||
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=200)
|
||||||
self.processing_thread = process_metadata.ProcessMetadata(
|
self.processing_thread = process_metadata.ProcessMetadata(
|
||||||
self.queue, self.last_sync)
|
self.queue, self.last_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
|
||||||
|
@ -180,12 +184,13 @@ class FullSync(backgroundthread.KillableThread, common.libsync_mixin):
|
||||||
# This will block until the processing thread exits
|
# 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()
|
||||||
self.callback(successful)
|
if self.callback:
|
||||||
|
self.callback(successful)
|
||||||
LOG.info('Done full_sync')
|
LOG.info('Done full_sync')
|
||||||
|
|
||||||
|
|
||||||
def start(repair, callback):
|
def start(show_dialog, repair=False, callback=None):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
# backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback))
|
# backgroundthread.BGThreader.addTask(FullSync().setup(repair, callback))
|
||||||
FullSync(repair, callback).start()
|
FullSync(repair, callback, show_dialog).start()
|
||||||
|
|
|
@ -36,34 +36,38 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):
|
||||||
item_class: as used to call functions in itemtypes.py e.g. 'Movies' =>
|
item_class: as used to call functions in itemtypes.py e.g. 'Movies' =>
|
||||||
itemtypes.Movies()
|
itemtypes.Movies()
|
||||||
"""
|
"""
|
||||||
def __init__(self, queue, last_sync):
|
def __init__(self, queue, last_sync, show_dialog):
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.last_sync = last_sync
|
self.last_sync = last_sync
|
||||||
|
self.show_dialog = show_dialog
|
||||||
self.total = 0
|
self.total = 0
|
||||||
self.current = 0
|
self.current = 0
|
||||||
self.title = None
|
self.title = None
|
||||||
self.section_name = None
|
self.section_name = None
|
||||||
|
self.dialog = None
|
||||||
super(ProcessMetadata, self).__init__()
|
super(ProcessMetadata, self).__init__()
|
||||||
|
|
||||||
def update_dialog(self):
|
def update(self):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
try:
|
if self.show_dialog:
|
||||||
progress = int(float(self.current) / float(self.total) * 100.0)
|
try:
|
||||||
except ZeroDivisionError:
|
progress = int(float(self.current) / float(self.total) * 100.0)
|
||||||
progress = 0
|
except ZeroDivisionError:
|
||||||
self.dialog.update(progress,
|
progress = 0
|
||||||
self.section_name,
|
self.dialog.update(progress,
|
||||||
'%s/%s: %s'
|
self.section_name,
|
||||||
% (self.current, self.total, self.title))
|
'%s/%s: %s'
|
||||||
|
% (self.current, self.total, self.title))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Do the work
|
Do the work
|
||||||
"""
|
"""
|
||||||
LOG.debug('Processing thread started')
|
LOG.debug('Processing thread started')
|
||||||
self.dialog = xbmcgui.DialogProgressBG()
|
if self.show_dialog:
|
||||||
self.dialog.create(utils.lang(39714))
|
self.dialog = xbmcgui.DialogProgressBG()
|
||||||
|
self.dialog.create(utils.lang(39714))
|
||||||
try:
|
try:
|
||||||
# Init with the very first library section. This will block!
|
# Init with the very first library section. This will block!
|
||||||
section = self.queue.get()
|
section = self.queue.get()
|
||||||
|
@ -91,13 +95,15 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):
|
||||||
children=xml.children)
|
children=xml.children)
|
||||||
except:
|
except:
|
||||||
utils.ERROR(txt='process_metadata crashed',
|
utils.ERROR(txt='process_metadata crashed',
|
||||||
notify=True)
|
notify=True,
|
||||||
|
cancel_sync=True)
|
||||||
if self.current % 20 == 0:
|
if self.current % 20 == 0:
|
||||||
self.title = utils.cast(unicode,
|
self.title = utils.cast(unicode,
|
||||||
xml[0].get('title'))
|
xml[0].get('title'))
|
||||||
self.update_dialog()
|
self.update()
|
||||||
self.current += 1
|
self.current += 1
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
finally:
|
finally:
|
||||||
self.dialog.close()
|
if self.dialog:
|
||||||
|
self.dialog.close()
|
||||||
LOG.debug('Processing thread terminated')
|
LOG.debug('Processing thread terminated')
|
||||||
|
|
108
resources/lib/library_sync/time.py
Normal file
108
resources/lib/library_sync/time.py
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
import xbmc
|
||||||
|
|
||||||
|
from .. import plex_functions as PF, utils, variables as v, state
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.library_sync.time')
|
||||||
|
|
||||||
|
|
||||||
|
def sync_pms_time():
|
||||||
|
"""
|
||||||
|
PMS does not provide a means to get a server timestamp. This is a work-
|
||||||
|
around - because the PMS might be in another time zone
|
||||||
|
|
||||||
|
In general, everything saved to Kodi shall be in Kodi time.
|
||||||
|
|
||||||
|
Any info with a PMS timestamp is in Plex time, naturally
|
||||||
|
"""
|
||||||
|
LOG.info('Synching time with PMS server')
|
||||||
|
# Find a PMS item where we can toggle the view state to enforce a
|
||||||
|
# change in lastViewedAt
|
||||||
|
|
||||||
|
# Get all Plex libraries
|
||||||
|
sections = PF.get_plex_sections()
|
||||||
|
try:
|
||||||
|
sections.attrib
|
||||||
|
except AttributeError:
|
||||||
|
LOG.error("Error download PMS views, abort sync_pms_time")
|
||||||
|
return False
|
||||||
|
|
||||||
|
plex_id = None
|
||||||
|
typus = (
|
||||||
|
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_MOVIE,),
|
||||||
|
(v.PLEX_TYPE_SHOW, v.PLEX_TYPE_EPISODE),
|
||||||
|
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_SONG)
|
||||||
|
)
|
||||||
|
for section_type, plex_type in typus:
|
||||||
|
if plex_id:
|
||||||
|
break
|
||||||
|
for section in sections:
|
||||||
|
if plex_id:
|
||||||
|
break
|
||||||
|
if not section.attrib['type'] == section_type:
|
||||||
|
continue
|
||||||
|
library_id = section.attrib['key']
|
||||||
|
try:
|
||||||
|
iterator = PF.SectionItems(library_id, {'type': plex_type})
|
||||||
|
for item in iterator:
|
||||||
|
if item.get('viewCount'):
|
||||||
|
# Don't want to mess with items that have playcount>0
|
||||||
|
continue
|
||||||
|
if item.get('viewOffset'):
|
||||||
|
# Don't mess with items with a resume point
|
||||||
|
continue
|
||||||
|
plex_id = utils.cast(int, item.get('ratingKey'))
|
||||||
|
LOG.info('Found a %s item to sync with: %s',
|
||||||
|
plex_type, plex_id)
|
||||||
|
break
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
if plex_id is None:
|
||||||
|
LOG.error("Could not find an item to sync time with")
|
||||||
|
LOG.error("Aborting PMS-Kodi time sync")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get the Plex item's metadata
|
||||||
|
xml = PF.GetPlexMetadata(plex_id)
|
||||||
|
if xml in (None, 401):
|
||||||
|
LOG.error("Could not download metadata, aborting time sync")
|
||||||
|
return False
|
||||||
|
|
||||||
|
timestamp = xml[0].get('lastViewedAt')
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = xml[0].get('updatedAt')
|
||||||
|
LOG.debug('Using items updatedAt=%s', timestamp)
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = xml[0].get('addedAt')
|
||||||
|
LOG.debug('Using items addedAt=%s', timestamp)
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = 0
|
||||||
|
LOG.debug('No timestamp; using 0')
|
||||||
|
timestamp = utils.cast(int, timestamp)
|
||||||
|
# Set the timer
|
||||||
|
koditime = utils.unix_timestamp()
|
||||||
|
# Toggle watched state
|
||||||
|
PF.scrobble(plex_id, 'watched')
|
||||||
|
# Let the PMS process this first!
|
||||||
|
xbmc.sleep(1000)
|
||||||
|
# Get updated metadata
|
||||||
|
xml = PF.GetPlexMetadata(plex_id)
|
||||||
|
# Toggle watched state back
|
||||||
|
PF.scrobble(plex_id, 'unwatched')
|
||||||
|
try:
|
||||||
|
plextime = xml[0].get('lastViewedAt')
|
||||||
|
except (IndexError, TypeError, AttributeError):
|
||||||
|
LOG.error('Could not get lastViewedAt - aborting')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Calculate time offset Kodi-PMS
|
||||||
|
state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
|
||||||
|
utils.settings('kodiplextimeoffset',
|
||||||
|
value=str(state.KODI_PLEX_TIME_OFFSET))
|
||||||
|
LOG.info("Time offset Koditime - Plextime in seconds: %s",
|
||||||
|
state.KODI_PLEX_TIME_OFFSET)
|
||||||
|
return True
|
File diff suppressed because it is too large
Load diff
|
@ -588,7 +588,7 @@ 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):
|
def __init__(self, section_id, args=None):
|
||||||
super(SectionItems, self).__init__(
|
super(SectionItems, self).__init__(
|
||||||
'{server}/library/sections/%s/all' % section_id, args)
|
'{server}/library/sections/%s/all' % section_id, args)
|
||||||
|
|
||||||
|
|
|
@ -233,10 +233,12 @@ def dialog(typus, *args, **kwargs):
|
||||||
return types[typus](*args, **kwargs)
|
return types[typus](*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def ERROR(txt='', hide_tb=False, notify=False):
|
def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False):
|
||||||
import sys
|
import sys
|
||||||
short = str(sys.exc_info()[1])
|
short = str(sys.exc_info()[1])
|
||||||
LOG.error('Error encountered: %s - %s', txt, short)
|
LOG.error('Error encountered: %s - %s', txt, short)
|
||||||
|
if cancel_sync:
|
||||||
|
state.STOP_SYNC = True
|
||||||
if hide_tb:
|
if hide_tb:
|
||||||
return short
|
return short
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue