Merge pull request #721 from croneter/beta-version

Update master
This commit is contained in:
croneter 2019-02-11 16:59:02 +01:00 committed by GitHub
commit 0e64f50b95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1106 additions and 658 deletions

View file

@ -1,5 +1,5 @@
[![stable version](https://img.shields.io/badge/stable_version-2.6.5-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
[![beta version](https://img.shields.io/badge/beta_version-2.6.5-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
[![stable version](https://img.shields.io/badge/stable_version-2.7.0-blue.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/stable/repository.plexkodiconnect/repository.plexkodiconnect-1.0.2.zip)
[![beta version](https://img.shields.io/badge/beta_version-2.7.0-red.svg?maxAge=60&style=flat) ](https://github.com/croneter/binary_repo/raw/master/beta/repository.plexkodiconnectbeta/repository.plexkodiconnectbeta-1.0.2.zip)
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/Installation)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-brightgreen.svg?maxAge=60&style=flat)](https://github.com/croneter/PlexKodiConnect/wiki/faq)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.6.5" provider-name="croneter">
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.7.0" provider-name="croneter">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" />
@ -77,7 +77,35 @@
<summary lang="uk_UA">Нативна інтеграція Plex в Kodi</summary>
<description lang="uk_UA">Підключає Kodi до серверу Plex. Цей плагін передбачає, що ви керуєте всіма своїми відео за допомогою Plex (і ніяк не Kodi). Ви можете втратити дані, які вже зберігаються у відео та музичних БД Kodi (оскільки цей плагін безпосередньо їх змінює). Використовуйте на свій страх і ризик!</description>
<disclaimer lang="uk_UA">Використовуйте на свій ризик</disclaimer>
<news>version 2.6.5:
<news>version 2.7.0:
- WARNING: You will need to reset the Kodi database if you're using the stable version of PKC!
- Version 2.6.6-9 for everyone
- Choose which Plex libraries get synched to Kodi
version 2.6.9 (beta only):
- Fix PKC crashing on resetting the database
version 2.6.8 (beta only):
- Choose which Plex libraries get synched to Kodi
- Fix PKC becoming unresponsive
- Fix rare case where thousands of identical playlists could be generated
- Fix movies or shows disappearing in fringe cases
- Fix processing of collections in special cases
- Implement Codacy suggestions
version 2.6.7 (beta only):
- Fix "Unauthorized for PMS" e.g. on switching Plex users
- Improve error messages when playback failes
version 2.6.6 (beta only):
- WARNING: You will need to reset the Kodi database!
- Greatly speed up sync for episodes, especially for large libraries
- Allow websocket redirects. Never allow insecure HTTPs connections for Kodi Leia
- Optimize headers for communication with PMS to appear like a Plex Media Player
- Fix PMS log entries 'Unable to find client profile for device'
- Improve sync dialog
version 2.6.5:
- Fix extras not playing
- Hide "Verify SSL certificate" setting for Kodi 18 Krypton
- Improve logging

View file

@ -1,3 +1,31 @@
version 2.7.0:
- WARNING: You will need to reset the Kodi database if you're using the stable version of PKC!
- Version 2.6.6-9 for everyone
- Choose which Plex libraries get synched to Kodi
version 2.6.9 (beta only):
- Fix PKC crashing on resetting the database
version 2.6.8 (beta only):
- Choose which Plex libraries get synched to Kodi
- Fix PKC becoming unresponsive
- Fix rare case where thousands of identical playlists could be generated
- Fix movies or shows disappearing in fringe cases
- Fix processing of collections in special cases
- Implement Codacy suggestions
version 2.6.7 (beta only):
- Fix "Unauthorized for PMS" e.g. on switching Plex users
- Improve error messages when playback failes
version 2.6.6 (beta only):
- WARNING: You will need to reset the Kodi database!
- Greatly speed up sync for episodes, especially for large libraries
- Allow websocket redirects. Never allow insecure HTTPs connections for Kodi Leia
- Optimize headers for communication with PMS to appear like a Plex Media Player
- Fix PMS log entries 'Unable to find client profile for device'
- Improve sync dialog
version 2.6.5:
- Fix extras not playing
- Hide "Verify SSL certificate" setting for Kodi 18 Krypton

View file

@ -141,6 +141,10 @@ class Main():
elif mode == 'hub':
entrypoint.hub(params.get('type'))
elif mode == 'select-libraries':
LOG.info('User requested to select Plex libraries')
transfer.plex_command('select-libraries')
else:
entrypoint.show_main_menu(content_type=params.get('content_type'))

View file

@ -558,6 +558,12 @@ msgctxt "#30523"
msgid "Also show sync progress for playstate and user data"
msgstr ""
# PKC Settings - Sync Options
msgctxt "#30524"
msgid "Select Plex libraries to sync"
msgstr ""
# PKC Settings - Playback
msgctxt "#30527"
msgid "Ignore specials in next episodes"
@ -1138,9 +1144,9 @@ msgctxt "#39211"
msgid "Watch later"
msgstr ""
# String attached at the end to get something like "PMS Name is offline"
# Error message pop-up if {0} cannot be contacted. {0} will be replaced by e.g. the PMS' name
msgctxt "#39213"
msgid "is offline"
msgid "{0} offline"
msgstr ""
msgctxt "#39215"

View file

@ -10,12 +10,22 @@ LOG = getLogger('PLEX.account')
class Account(object):
def __init__(self, entrypoint=False):
self.plex_login = None
self.plex_login_id = None
self.plex_username = None
self.plex_user_id = None
self.plex_token = None
self.pms_token = None
self.avatar = None
self.myplexlogin = None
self.restricted_user = None
self.force_login = None
self._session = None
self.authenticated = False
if entrypoint:
self.load_entrypoint()
else:
self.authenticated = False
utils.window('plex_authenticated', clear=True)
self._session = None
self.load()
def set_authenticated(self):

View file

@ -1,27 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import Queue
from threading import Lock, RLock
import xbmc
from .. import utils
LOG = getLogger('PLEX.app')
class App(object):
"""
This class is used to store variables across PKC modules
"""
def __init__(self, entrypoint=False):
self.fetch_pms_item_number = None
self.force_reload_skin = None
if entrypoint:
self.load_entrypoint()
else:
self.load()
# Quit PKC?
self.stop_pkc = False
# Shall we completely suspend PKC and our threads?
# This will suspend the main thread also
self.suspend = False
# Shall we only suspend threads?
self._suspend_threads = False
# Need to lock all methods and functions messing with Plex Companion subscribers
self.lock_subscriber = RLock()
# Need to lock everything messing with Kodi/PKC playqueues
@ -38,6 +43,130 @@ class App(object):
self.monitor = None
# xbmc.Player() instance
self.player = None
# All thread instances
self.threads = []
# Instance of FanartThread()
self.fanart_thread = None
# Instance of ImageCachingThread()
self.caching_thread = None
@property
def is_playing(self):
return self.player.isPlaying()
@property
def is_playing_video(self):
return self.player.isPlayingVideo()
def register_fanart_thread(self, thread):
self.fanart_thread = thread
self.threads.append(thread)
def deregister_fanart_thread(self, thread):
self.fanart_thread = None
self.threads.remove(thread)
def suspend_fanart_thread(self, block=True):
try:
self.fanart_thread.suspend(block=block)
except AttributeError:
pass
def resume_fanart_thread(self):
try:
self.fanart_thread.resume()
except AttributeError:
pass
def register_caching_thread(self, thread):
self.caching_thread = thread
self.threads.append(thread)
def deregister_caching_thread(self, thread):
self.caching_thread = None
self.threads.remove(thread)
def suspend_caching_thread(self, block=True):
try:
self.caching_thread.suspend(block=block)
except AttributeError:
pass
def resume_caching_thread(self):
try:
self.caching_thread.resume()
except AttributeError:
pass
def register_thread(self, thread):
"""
Hit with thread [backgroundthread.Killablethread instance] to register
any and all threads
"""
self.threads.append(thread)
def deregister_thread(self, thread):
"""
Sync thread has done it's work and is e.g. about to die
"""
self.threads.remove(thread)
def suspend_threads(self, block=True):
"""
Suspend all threads' activity with or without blocking.
Returns True only if PKC shutdown requested
"""
LOG.debug('Suspending threads: %s', self.threads)
for thread in self.threads:
thread.suspend()
if block:
while True:
for thread in self.threads:
if not thread.suspend_reached:
LOG.debug('Waiting for thread to suspend: %s', thread)
# Send suspend signal again in case self.threads
# changed
thread.suspend()
if self.monitor.waitForAbort(0.1):
return True
break
else:
break
return xbmc.abortRequested
def resume_threads(self, block=True):
"""
Resume all thread activity with or without blocking.
Returns True only if PKC shutdown requested
"""
LOG.debug('Resuming threads: %s', self.threads)
for thread in self.threads:
thread.resume()
if block:
while True:
for thread in self.threads:
if thread.suspend_reached:
LOG.debug('Waiting for thread to resume: %s', thread)
if self.monitor.waitForAbort(0.1):
return True
break
else:
break
return xbmc.abortRequested
def stop_threads(self, block=True):
"""
Stop all threads. Will block until all threads are stopped
Will NOT quit if PKC should exit!
"""
LOG.debug('Killing threads: %s', self.threads)
for thread in self.threads:
thread.abort()
if block:
while self.threads:
LOG.debug('Waiting for threads to exit: %s', self.threads)
if xbmc.sleep(100):
return True
def load(self):
# Number of items to fetch and display in widgets
@ -48,11 +177,3 @@ class App(object):
def load_entrypoint(self):
self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number'))
@property
def suspend_threads(self):
return self._suspend_threads or self.suspend
@suspend_threads.setter
def suspend_threads(self, value):
self._suspend_threads = value

View file

@ -10,13 +10,25 @@ LOG = getLogger('PLEX.connection')
class Connection(object):
def __init__(self, entrypoint=False):
self.verify_ssl_cert = None
self.ssl_cert_path = None
self.machine_identifier = None
self.server_name = None
self.https = None
self.host = None
self.port = None
self.server = None
self.online = False
self.webserver_host = None
self.webserver_port = None
self.webserver_username = None
self.webserver_password = None
if entrypoint:
self.load_entrypoint()
else:
self.load_webserver()
self.load()
# TODO: Delete
self.pms_server = None
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
# another user playing something! Token identifies user
self.plex_transient_token = None
@ -57,7 +69,6 @@ class Connection(object):
self.server = 'https://%s:%s' % (self.host, self.port)
else:
self.server = 'http://%s:%s' % (self.host, self.port)
utils.window('pms_server', value=self.server)
self.online = False
LOG.debug('Set server %s (%s) to %s',
self.server_name, self.machine_identifier, self.server)
@ -81,8 +92,7 @@ class Connection(object):
LOG.debug('Clearing connection settings')
self.machine_identifier = None
self.server_name = None
self.http = None
self.https = None
self.host = None
self.port = None
self.server = None
utils.window('pms_server', clear=True)

View file

@ -19,34 +19,71 @@ def remove_trailing_slash(path):
class Sync(object):
def __init__(self, entrypoint=False):
self.load()
# Direct Paths (True) or Addon Paths (False)?
self.direct_paths = None
# Is synching of Plex music enabled?
self.enable_music = None
# Do we sync artwork from the PMS to Kodi?
self.artwork = None
# Path remapping mechanism (e.g. smb paths)
# Do we replace \\myserver\path to smb://myserver/path?
self.replace_smb_path = None
# Do we generally remap?
self.remap_path = None
self.force_transcode_pix = None
# Mappings for REMAP_PATH:
self.remapSMBmovieOrg = None
self.remapSMBmovieNew = None
self.remapSMBtvOrg = None
self.remapSMBtvNew = None
self.remapSMBmusicOrg = None
self.remapSMBmusicNew = None
self.remapSMBphotoOrg = None
self.remapSMBphotoNew = None
# Escape path?
self.escape_path = None
# Shall we replace custom user ratings with the number of versions available?
self.indicate_media_versions = None
# Will sync movie trailer differently: either play trailer directly or show
# all the Plex extras for the user to choose
self.show_extras_instead_of_playing_trailer = None
# Only sync specific Plex playlists to Kodi?
self.sync_specific_plex_playlists = None
# Only sync specific Kodi playlists to Plex?
self.sync_specific_kodi_playlists = None
# Shall we show Kodi dialogs when synching?
self.sync_dialog = None
# How often shall we sync?
self.full_sync_intervall = None
# Background Sync disabled?
self.background_sync_disabled = None
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
self.backgroundsync_saftymargin = None
# How many threads to download Plex metadata on sync?
self.sync_thread_number = None
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left
# to sync)
self.image_sync_notifications = None
# Do we need to run a special library scan?
self.run_lib_scan = None
# Set if user decided to cancel sync
self.stop_sync = False
# Set during media playback if PKC should not do any syncs. Will NOT
# suspend synching of playstate progress
self.suspend_sync = False
# Could we access the paths?
self.path_verified = False
# Set if a Plex-Kodi DB sync is being done - along with
# window('plex_dbScan') set to 'true'
self.db_scan = False
self.load()
def load(self):
# Direct Paths (True) or Addon Paths (False)?
self.direct_paths = utils.settings('useDirectPaths') == '1'
# Is synching of Plex music enabled?
self.enable_music = utils.settings('enableMusic') == 'true'
# Do we sync artwork from the PMS to Kodi?
self.artwork = utils.settings('usePlexArtwork') == 'true'
# Path remapping mechanism (e.g. smb paths)
# Do we replace \\myserver\path to smb://myserver/path?
self.replace_smb_path = utils.settings('replaceSMB') == 'true'
# Do we generally remap?
self.remap_path = utils.settings('remapSMB') == 'true'
self.force_transcode_pix = utils.settings('force_transcode_pix') == 'true'
# Mappings for REMAP_PATH:
self.remapSMBmovieOrg = remove_trailing_slash(utils.settings('remapSMBmovieOrg'))
self.remapSMBmovieNew = remove_trailing_slash(utils.settings('remapSMBmovieNew'))
self.remapSMBtvOrg = remove_trailing_slash(utils.settings('remapSMBtvOrg'))
@ -55,30 +92,16 @@ class Sync(object):
self.remapSMBmusicNew = remove_trailing_slash(utils.settings('remapSMBmusicNew'))
self.remapSMBphotoOrg = remove_trailing_slash(utils.settings('remapSMBphotoOrg'))
self.remapSMBphotoNew = remove_trailing_slash(utils.settings('remapSMBphotoNew'))
# Escape path?
self.escape_path = utils.settings('escapePath') == 'true'
# Shall we replace custom user ratings with the number of versions available?
self.indicate_media_versions = utils.settings('indicate_media_versions') == "true"
# Will sync movie trailer differently: either play trailer directly or show
# all the Plex extras for the user to choose
self.show_extras_instead_of_playing_trailer = utils.settings('showExtrasInsteadOfTrailer') == 'true'
# Only sync specific Plex playlists to Kodi?
self.sync_specific_plex_playlists = utils.settings('syncSpecificPlexPlaylists') == 'true'
# Only sync specific Kodi playlists to Plex?
self.sync_specific_kodi_playlists = utils.settings('syncSpecificKodiPlaylists') == 'true'
# Shall we show Kodi dialogs when synching?
self.sync_dialog = utils.settings('dbSyncIndicator') == 'true'
# How often shall we sync?
self.full_sync_intervall = int(utils.settings('fullSyncInterval')) * 60
# Background Sync disabled?
self.background_sync_disabled = utils.settings('enableBackgroundSync') == 'false'
# How long shall we wait with synching a new item to make sure Plex got all
# metadata?
self.backgroundsync_saftymargin = int(utils.settings('backgroundsync_saftyMargin'))
# How many threads to download Plex metadata on sync?
self.sync_thread_number = int(utils.settings('syncThreadNumber'))
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left
# to sync)
self.image_sync_notifications = utils.settings('imageSyncNotifications') == 'true'

View file

@ -18,8 +18,6 @@ requests.packages.urllib3.disable_warnings()
TIMEOUT = (35.1, 35.1)
BATCH_SIZE = 500
IMAGE_CACHING_SUSPENDS = []
def double_urlencode(text):
return quote_plus(quote_plus(text))
@ -30,10 +28,17 @@ def double_urldecode(text):
class ImageCachingThread(backgroundthread.KillableThread):
def isSuspended(self):
return any(IMAGE_CACHING_SUSPENDS)
def __init__(self):
super(ImageCachingThread, self).__init__()
self.suspend_points = [(self, '_suspended')]
if not utils.settings('imageSyncDuringPlayback') == 'true':
self.suspend_points.append((app.APP, 'is_playing_video'))
def _url_generator(self, kind, kodi_type):
def isSuspended(self):
return any(getattr(obj, txt) for obj, txt in self.suspend_points)
@staticmethod
def _url_generator(kind, kodi_type):
"""
Main goal is to close DB connection between calls
"""
@ -60,11 +65,13 @@ class ImageCachingThread(backgroundthread.KillableThread):
def run(self):
LOG.info("---===### Starting ImageCachingThread ###===---")
app.APP.register_caching_thread(self)
try:
self._run()
except Exception:
utils.ERROR()
finally:
app.APP.deregister_caching_thread(self)
LOG.info("---===### Stopped ImageCachingThread ###===---")
def _run(self):
@ -74,14 +81,8 @@ class ImageCachingThread(backgroundthread.KillableThread):
for kind in kinds:
for kodi_type in ('poster', 'fanart'):
for url in self._url_generator(kind, kodi_type):
if self.isCanceled():
if self.wait_while_suspended():
return
while self.isSuspended():
# Set in service.py
if self.isCanceled():
# Abort was requested while waiting. We should exit
return
app.APP.monitor.waitForAbort(1)
cache_url(url)
# Toggles Image caching completed to Yes
utils.settings('plex_status_image_caching', value=utils.lang(107))

View file

@ -77,7 +77,10 @@ class KillableThread(threading.Thread):
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
self._canceled = False
# Set to True to set the thread to suspended
self._suspended = False
# Thread will return True only if suspended state is reached
self.suspend_reached = False
super(KillableThread, self).__init__(group, target, name, args, kwargs)
def isCanceled(self):
@ -94,11 +97,16 @@ class KillableThread(threading.Thread):
"""
self._canceled = True
def suspend(self):
def suspend(self, block=False):
"""
Call to suspend this thread
"""
self._suspended = True
if block:
while not self.suspend_reached:
LOG.debug('Waiting for thread to suspend: %s', self)
if app.APP.monitor.waitForAbort(0.1):
return
def resume(self):
"""
@ -106,6 +114,25 @@ class KillableThread(threading.Thread):
"""
self._suspended = False
def wait_while_suspended(self):
"""
Blocks until thread is not suspended anymore or the thread should
exit.
Returns True only if the thread should exit (=isCanceled())
"""
while self.isSuspended():
try:
self.suspend_reached = True
# Set in service.py
if self.isCanceled():
# Abort was requested while waiting. We should exit
return True
if app.APP.monitor.waitForAbort(0.1):
return True
finally:
self.suspend_reached = False
return self.isCanceled()
def isSuspended(self):
"""
Returns True if the thread is suspended
@ -198,7 +225,8 @@ class BackgroundWorker(object):
self._abort = False
self._task = None
def _runTask(self, task):
@staticmethod
def _runTask(task):
if task._canceled:
return
try:

View file

@ -32,12 +32,11 @@ def getXArgsDeviceInfo(options=None, include_token=True):
"Content-Type": "application/x-www-form-urlencoded",
# "Access-Control-Allow-Origin": "*",
# 'X-Plex-Language': 'en',
'X-Plex-Device': v.ADDON_NAME,
'X-Plex-Client-Platform': v.PLATFORM,
'X-Plex-Device': v.DEVICE,
'X-Plex-Model': v.MODEL,
'X-Plex-Device-Name': v.DEVICENAME,
'X-Plex-Platform': v.PLATFORM,
# 'X-Plex-Platform-Version': 'unknown',
# 'X-Plex-Model': 'unknown',
'X-Plex-Platform-Version': v.PLATFORM_VERSION,
'X-Plex-Product': v.ADDON_NAME,
'X-Plex-Version': v.ADDON_VERSION,
'X-Plex-Client-Identifier': getDeviceId(),

View file

@ -167,6 +167,7 @@ class DownloadUtils():
kwargs['timeout'] = timeout
# ACTUAL DOWNLOAD HAPPENING HERE
success = False
try:
r = self._doDownload(s, action_type, **kwargs)
@ -176,44 +177,37 @@ class DownloadUtils():
LOG.warn(e)
if reraise:
raise
except exceptions.ConnectionError as e:
# Connection error
LOG.warn("Server unreachable at: %s", url)
LOG.warn(e)
if reraise:
raise
except exceptions.Timeout as e:
LOG.warn("Server timeout at: %s", url)
LOG.warn(e)
if reraise:
raise
except exceptions.HTTPError as e:
LOG.warn('HTTP Error at %s', url)
LOG.warn(e)
if reraise:
raise
except exceptions.TooManyRedirects as e:
LOG.warn("Too many redirects connecting to: %s", url)
LOG.warn(e)
if reraise:
raise
except exceptions.RequestException as e:
LOG.warn("Unknown error connecting to: %s", url)
LOG.warn(e)
if reraise:
raise
except SystemExit:
LOG.info('SystemExit detected, aborting download')
self.stopSession()
if reraise:
raise
except Exception:
LOG.warn('Unknown error while downloading. Traceback:')
import traceback
@ -223,6 +217,7 @@ class DownloadUtils():
# THE RESPONSE #####
else:
success = True
# We COULD contact the PMS, hence it ain't dead
if authenticate is True:
self.count_error = 0
@ -300,12 +295,12 @@ class DownloadUtils():
url, r.status_code)
return True
# And now deal with the consequences of the exceptions
if authenticate is True:
# Make the addon aware of status
self.count_error += 1
if self.count_error >= self.connection_attempts:
LOG.warn('Failed to connect to %s too many times. '
'Declare PMS dead', url)
app.CONN.online = False
return
finally:
if not success and authenticate:
# Deal with the consequences of the exceptions
# Make the addon aware of status
self.count_error += 1
if self.count_error >= self.connection_attempts:
LOG.warn('Failed to connect to %s too many times. '
'Declare PMS dead', url)
app.CONN.online = False

View file

@ -69,7 +69,8 @@ class InitialSetup(object):
utils.settings('plex_allows_mediaDeletion', value=value)
utils.window('plex_allows_mediaDeletion', value=value)
def enter_new_pms_address(self):
@staticmethod
def enter_new_pms_address():
LOG.info('Start getting manual PMS address and port')
# "Enter your Plex Media Server's IP or URL. Examples are:"
utils.messageDialog(utils.lang(29999),
@ -196,12 +197,12 @@ class InitialSetup(object):
LOG.error('Failed to update Plex info from plex.tv')
else:
utils.settings('plexLogin', value=self.plex_login)
home = 'true' if xml.attrib.get('home') == '1' else 'false'
utils.settings('plexAvatar', value=xml.attrib.get('thumb'))
LOG.info('Updated Plex info from plex.tv')
return answer
def check_existing_pms(self):
@staticmethod
def check_existing_pms():
"""
Check the PMS that was set in file settings.
Will return False if we need to reconnect, because:

View file

@ -110,7 +110,7 @@ class ItemBase(object):
kodi_type)
def update_playstate(self, mark_played, view_count, resume, duration,
kodi_fileid, lastViewedAt, plex_type):
kodi_fileid, kodi_fileid_2, lastViewedAt):
"""
Use with websockets, not xml
"""
@ -128,5 +128,11 @@ class ItemBase(object):
resume,
duration,
view_count,
timing.plex_date_to_kodi(lastViewedAt),
plex_type)
timing.plex_date_to_kodi(lastViewedAt))
if kodi_fileid_2:
# Our dirty hack for episodes
self.kodidb.set_resume(kodi_fileid_2,
resume,
duration,
view_count,
timing.plex_date_to_kodi(lastViewedAt))

View file

@ -189,7 +189,7 @@ class Movie(ItemBase):
# e.g. when added via websocket
LOG.debug('Costly looking up Plex collection %s: %s',
plex_set_id, set_name)
for index, coll_plex_id in api.collections_match():
for index, coll_plex_id in api.collections_match(section_id):
# Get Plex artwork for collections - a pain
if index == plex_set_id:
set_xml = PF.GetPlexMetadata(coll_plex_id)
@ -214,8 +214,7 @@ class Movie(ItemBase):
resume,
runtime,
playcount,
dateplayed,
v.PLEX_TYPE_MOVIE)
dateplayed)
self.plexdb.add_movie(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
@ -279,8 +278,7 @@ class Movie(ItemBase):
userdata['Resume'],
userdata['Runtime'],
userdata['PlayCount'],
userdata['LastPlayedDate'],
plex_type)
userdata['LastPlayedDate'])
self.kodidb.update_userrating(db_item['kodi_id'],
db_item['kodi_type'],
userdata['UserRating'])

View file

@ -32,8 +32,13 @@ class TvShowMixin(object):
userdata['Resume'],
userdata['Runtime'],
userdata['PlayCount'],
userdata['LastPlayedDate'],
plex_type)
userdata['LastPlayedDate'])
if db_item['kodi_fileid_2']:
self.kodidb.set_resume(db_item['kodi_fileid_2'],
userdata['Resume'],
userdata['Runtime'],
userdata['PlayCount'],
userdata['LastPlayedDate'])
return True
def remove(self, plex_id, plex_type=None):
@ -54,7 +59,7 @@ class TvShowMixin(object):
# EPISODE #####
if db_item['plex_type'] == v.PLEX_TYPE_EPISODE:
# Delete episode, verify season and tvshow
self.remove_episode(db_item['kodi_id'], db_item['kodi_fileid'])
self.remove_episode(db_item)
# Season verification
if (db_item['season_id'] and
not self.plexdb.season_has_episodes(db_item['season_id'])):
@ -72,7 +77,7 @@ class TvShowMixin(object):
# Remove episodes, season, verify tvshow
episodes = list(self.plexdb.episode_by_season(db_item['plex_id']))
for episode in episodes:
self.remove_episode(episode['kodi_id'], episode['kodi_fileid'])
self.remove_episode(episode)
self.plexdb.remove(episode['plex_id'], v.PLEX_TYPE_EPISODE)
# Remove season
self.remove_season(db_item['kodi_id'])
@ -91,8 +96,7 @@ class TvShowMixin(object):
self.plexdb.remove(season['plex_id'], v.PLEX_TYPE_SEASON)
episodes = list(self.plexdb.episode_by_show(db_item['plex_id']))
for episode in episodes:
self.remove_episode(episode['kodi_id'],
episode['kodi_fileid'])
self.remove_episode(episode)
self.plexdb.remove(episode['plex_id'], v.PLEX_TYPE_EPISODE)
self.remove_show(db_item['kodi_id'])
@ -120,17 +124,19 @@ class TvShowMixin(object):
self.kodidb.remove_season(kodi_id)
LOG.debug("Removed season: %s", kodi_id)
def remove_episode(self, kodi_id, file_id):
def remove_episode(self, db_item):
"""
Remove an episode, and episode only from the Kodi DB (not Plex DB)
"""
self.kodidb.modify_people(kodi_id, v.KODI_TYPE_EPISODE)
self.kodidb.remove_file(file_id, plex_type=v.PLEX_TYPE_EPISODE)
self.kodidb.delete_artwork(kodi_id, v.KODI_TYPE_EPISODE)
self.kodidb.remove_episode(kodi_id)
self.kodidb.remove_uniqueid(kodi_id, v.KODI_TYPE_EPISODE)
self.kodidb.remove_ratings(kodi_id, v.KODI_TYPE_EPISODE)
LOG.debug("Removed episode: %s", kodi_id)
self.kodidb.modify_people(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
self.kodidb.remove_file(db_item['kodi_fileid'])
if db_item['kodi_fileid_2']:
self.kodidb.remove_file(db_item['kodi_fileid_2'])
self.kodidb.delete_artwork(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
self.kodidb.remove_episode(db_item['kodi_id'])
self.kodidb.remove_uniqueid(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
self.kodidb.remove_ratings(db_item['kodi_id'], v.KODI_TYPE_EPISODE)
LOG.debug("Removed episode: %s", db_item['kodi_id'])
class Show(TvShowMixin, ItemBase):
@ -367,6 +373,7 @@ class Episode(TvShowMixin, ItemBase):
update_item = True
kodi_id = episode['kodi_id']
old_kodi_fileid = episode['kodi_fileid']
old_kodi_fileid_2 = episode['kodi_fileid_2']
kodi_pathid = episode['kodi_pathid']
peoples = api.people()
@ -452,6 +459,15 @@ class Episode(TvShowMixin, ItemBase):
playurl = filename
# Root path tvshows/ already saved in Kodi DB
kodi_pathid = self.kodidb.add_path(path)
if not app.SYNC.direct_paths:
# need to set a 2nd file entry for a path without plex show id
# This fixes e.g. context menu and widgets working as they
# should
# A dirty hack, really
path_2 = 'plugin://%s.tvshows/' % v.ADDON_ID
# filename_2 is exactly the same as filename
# so WITH plex show id!
kodi_pathid_2 = self.kodidb.add_path(path_2)
# UPDATE THE EPISODE #####
if update_item:
@ -459,9 +475,17 @@ class Episode(TvShowMixin, ItemBase):
kodi_fileid = self.kodidb.modify_file(filename,
kodi_pathid,
api.date_created())
if not app.SYNC.direct_paths:
kodi_fileid_2 = self.kodidb.modify_file(filename,
kodi_pathid_2,
api.date_created())
else:
kodi_fileid_2 = None
if kodi_fileid != old_kodi_fileid:
self.kodidb.remove_file(old_kodi_fileid)
if not app.SYNC.direct_paths:
self.kodidb.remove_file(old_kodi_fileid_2)
ratingid = self.kodidb.get_ratingid(kodi_id,
v.KODI_TYPE_EPISODE)
self.kodidb.update_ratings(kodi_id,
@ -502,7 +526,7 @@ class Episode(TvShowMixin, ItemBase):
airs_before_episode,
playurl,
kodi_pathid,
kodi_fileid,
kodi_fileid, # and NOT kodi_fileid_2
parent_id,
userdata['UserRating'],
kodi_id)
@ -510,8 +534,13 @@ class Episode(TvShowMixin, ItemBase):
api.resume_point(),
api.runtime(),
userdata['PlayCount'],
userdata['LastPlayedDate'],
v.PLEX_TYPE_EPISODE)
userdata['LastPlayedDate'])
if not app.SYNC.direct_paths:
self.kodidb.set_resume(kodi_fileid_2,
api.resume_point(),
api.runtime(),
userdata['PlayCount'],
userdata['LastPlayedDate'])
self.plexdb.add_episode(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
@ -521,6 +550,7 @@ class Episode(TvShowMixin, ItemBase):
parent_id=parent_id,
kodi_id=kodi_id,
kodi_fileid=kodi_fileid,
kodi_fileid_2=kodi_fileid_2,
kodi_pathid=kodi_pathid,
last_sync=self.last_sync)
# OR ADD THE EPISODE #####
@ -529,6 +559,12 @@ class Episode(TvShowMixin, ItemBase):
kodi_fileid = self.kodidb.add_file(filename,
kodi_pathid,
api.date_created())
if not app.SYNC.direct_paths:
kodi_fileid_2 = self.kodidb.add_file(filename,
kodi_pathid_2,
api.date_created())
else:
kodi_fileid_2 = None
rating_id = self.kodidb.add_ratingid()
self.kodidb.add_ratings(rating_id,
@ -552,7 +588,7 @@ class Episode(TvShowMixin, ItemBase):
kodi_id,
v.KODI_TYPE_EPISODE)
self.kodidb.add_episode(kodi_id,
kodi_fileid,
kodi_fileid, # and NOT kodi_fileid_2
api.title(),
api.plot(),
rating_id,
@ -574,8 +610,13 @@ class Episode(TvShowMixin, ItemBase):
api.resume_point(),
api.runtime(),
userdata['PlayCount'],
userdata['LastPlayedDate'],
None) # Do send None to avoid episode loop
userdata['LastPlayedDate'])
if not app.SYNC.direct_paths:
self.kodidb.set_resume(kodi_fileid_2,
api.resume_point(),
api.runtime(),
userdata['PlayCount'],
userdata['LastPlayedDate'])
self.plexdb.add_episode(plex_id=plex_id,
checksum=api.checksum(),
section_id=section_id,
@ -585,26 +626,10 @@ class Episode(TvShowMixin, ItemBase):
parent_id=parent_id,
kodi_id=kodi_id,
kodi_fileid=kodi_fileid,
kodi_fileid_2=kodi_fileid_2,
kodi_pathid=kodi_pathid,
last_sync=self.last_sync)
if not app.SYNC.direct_paths:
# need to set a SECOND file entry for a path without plex show id
filename = api.file_name(force_first_media=True)
path = 'plugin://%s.tvshows/' % v.ADDON_ID
# Filename is exactly the same, WITH plex show id!
filename = ('%s%s/?plex_id=%s&plex_type=%s&mode=play&filename=%s'
% (path, show_id, plex_id, v.PLEX_TYPE_EPISODE,
filename))
kodi_pathid = self.kodidb.add_path(path)
second_kodi_fileid = self.kodidb.add_file(filename,
kodi_pathid,
api.date_created())
self.kodidb.set_resume(second_kodi_fileid,
api.resume_point(),
api.runtime(),
userdata['PlayCount'],
userdata['LastPlayedDate'],
None) # Do send None - 2nd entry
self.kodidb.modify_streams(kodi_fileid,
self.kodidb.modify_streams(kodi_fileid, # and NOT kodi_fileid_2
api.mediastreams(),
api.runtime())

View file

@ -9,6 +9,9 @@ from .. import path_ops, timing, variables as v, app
LOG = getLogger('PLEX.kodi_db.video')
MOVIE_PATH = 'plugin://%s.movies/' % v.ADDON_ID
SHOW_PATH = 'plugin://%s.tvshows/' % v.ADDON_ID
class KodiVideoDB(common.KodiDBBase):
db_kind = 'video'
@ -23,7 +26,7 @@ class KodiVideoDB(common.KodiDBBase):
For some reason, Kodi ignores this if done via itemtypes while e.g.
adding or updating items. (addPath method does NOT work)
"""
path_id = self.get_path('plugin://%s.movies/' % v.ADDON_ID)
path_id = self.get_path(MOVIE_PATH)
if path_id is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
path_id = self.cursor.fetchone()[0] + 1
@ -37,13 +40,13 @@ class KodiVideoDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (path_id,
'plugin://%s.movies/' % v.ADDON_ID,
MOVIE_PATH,
'movies',
'metadata.local',
1,
0))
# And TV shows
path_id = self.get_path('plugin://%s.tvshows/' % v.ADDON_ID)
path_id = self.get_path(SHOW_PATH)
if path_id is None:
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
path_id = self.cursor.fetchone()[0] + 1
@ -57,7 +60,7 @@ class KodiVideoDB(common.KodiDBBase):
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(query, (path_id,
'plugin://%s.tvshows/' % v.ADDON_ID,
SHOW_PATH,
'tvshows',
'metadata.local',
1,
@ -199,29 +202,13 @@ class KodiVideoDB(common.KodiDBBase):
pass
@common.catch_operationalerrors
def remove_file(self, file_id, remove_orphans=True, plex_type=None):
def remove_file(self, file_id, remove_orphans=True):
"""
Removes the entry for file_id from the files table. Will also delete
entries from the associated tables: bookmark, settings, streamdetails.
If remove_orphans is true, this method will delete any orphaned path
entries in the Kodi path table
Passing plex_type = v.PLEX_TYPE_EPISODE deletes any secondary files for
add-on paths
"""
if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE:
# Hack for the 2 entries for episodes for addon paths
self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1',
(file_id, ))
filename = self.cursor.fetchone()
if not filename:
LOG.error('Could not find file_id %s', file_id)
return
for new_id in self.cursor.execute('SELECT idFile FROM files WHERE strFilename = ? LIMIT 2',
(filename[0], )):
self.remove_file(new_id[0], remove_orphans=remove_orphans)
return
self.cursor.execute('SELECT idPath FROM files WHERE idFile = ? LIMIT 1',
(file_id,))
try:
@ -243,8 +230,12 @@ class KodiVideoDB(common.KodiDBBase):
self.cursor.execute('SELECT idFile FROM files WHERE idPath = ? LIMIT 1',
(path_id,))
if self.cursor.fetchone() is None:
self.cursor.execute('DELETE FROM path WHERE idPath = ?',
(path_id,))
# Make sure we're not deleting our root paths!
query = '''
DELETE FROM path
WHERE idPath = ? AND strPath NOT IN (?, ?)
'''
self.cursor.execute(query, (path_id, MOVIE_PATH, SHOW_PATH))
@common.catch_operationalerrors
def _modify_link_and_table(self, kodi_id, kodi_type, entries, link_table,
@ -612,31 +603,11 @@ class KodiVideoDB(common.KodiDBBase):
@common.catch_operationalerrors
def set_resume(self, file_id, resume_seconds, total_seconds, playcount,
dateplayed, plex_type):
dateplayed):
"""
Adds a resume marker for a video library item. Will even set 2,
considering add-on path widget hacks.
"""
if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE:
# Need to make sure to set a SECOND bookmark entry for another,
# second file_id that points to the path .tvshows instead of
# .tvshows/<plex show id/!
self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1',
(file_id, ))
try:
filename = self.cursor.fetchone()[0]
except TypeError:
LOG.error('Did not get a filename, aborting for file_id %s',
file_id)
return
self.cursor.execute('SELECT idFile FROM files WHERE strFilename = ? LIMIT 2',
(filename, ))
file_ids = self.cursor.fetchall()
for new_id in file_ids:
self.set_resume(new_id[0], resume_seconds, total_seconds,
playcount, dateplayed, None)
return
# Delete existing resume point
self.cursor.execute('DELETE FROM bookmark WHERE idFile = ?', (file_id,))
# Set watched count

View file

@ -54,7 +54,8 @@ class KodiMonitor(xbmc.Monitor):
"""
LOG.debug('PKC settings change detected')
# Assume that the user changed something so we can try to reconnect
app.APP.suspend = False
# app.APP.suspend = False
# app.APP.resume_threads(block=False)
def onNotification(self, sender, method, data):
"""
@ -69,7 +70,6 @@ class KodiMonitor(xbmc.Monitor):
self.hack_replay = None
if method == "Player.OnPlay":
app.SYNC.suspend_sync = True
with app.APP.lock_playqueues:
self.PlayBackStart(data)
elif method == "Player.OnStop":
@ -87,7 +87,6 @@ class KodiMonitor(xbmc.Monitor):
else:
with app.APP.lock_playqueues:
_playback_cleanup()
app.SYNC.suspend_sync = False
elif method == 'Playlist.OnAdd':
if 'item' in data and data['item'].get('type') == v.KODI_TYPE_SHOW:
# Hitting the "browse" button on tv show info dialog
@ -208,7 +207,8 @@ class KodiMonitor(xbmc.Monitor):
"""
pass
def _playlist_onclear(self, data):
@staticmethod
def _playlist_onclear(data):
"""
Called if a Kodi playlist is cleared. Example data dict:
{
@ -222,7 +222,8 @@ class KodiMonitor(xbmc.Monitor):
else:
LOG.debug('Detected PKC clear - ignoring')
def _get_ids(self, kodi_id, kodi_type, path):
@staticmethod
def _get_ids(kodi_id, kodi_type, path):
"""
Returns the tuple (plex_id, plex_type) or (None, None)
"""
@ -316,7 +317,7 @@ class KodiMonitor(xbmc.Monitor):
return
playerid = js.get_playlist_id(playlist_type)
if not playerid:
LOG.error('Coud not get playerid for data', data)
LOG.error('Coud not get playerid for data %s', data)
return
playqueue = PQ.PLAYQUEUES[playerid]
info = js.get_player_props(playerid)
@ -493,8 +494,14 @@ def _record_playstate(status, ended):
time,
totaltime,
playcount,
last_played,
status['plex_type'])
last_played)
if 'kodi_fileid_2' in db_item and db_item['kodi_fileid_2']:
# Dirty hack for our episodes
kodidb.set_resume(db_item['kodi_fileid_2'],
time,
totaltime,
playcount,
last_played)
# Hack to force "in progress" widget to appear if it wasn't visible before
if (app.APP.force_reload_skin and
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):

View file

@ -7,3 +7,5 @@ from .websocket import store_websocket_message, process_websocket_messages, \
WEBSOCKET_MESSAGES, PLAYSTATE_SESSIONS
from .common import update_kodi_library, PLAYLIST_SYNC_ENABLED
from .fanart import FanartThread, FanartTask
from .videonodes import VideoNodes
from .sections import force_full_sync

View file

@ -3,24 +3,38 @@
from __future__ import absolute_import, division, unicode_literals
import xbmc
from .. import app, utils, variables as v
from .. import utils, variables as v
PLAYLIST_SYNC_ENABLED = (v.PLATFORM != 'Microsoft UWP' and
PLAYLIST_SYNC_ENABLED = (v.DEVICE != 'Microsoft UWP' and
utils.settings('enablePlaylistSync') == 'true')
class libsync_mixin(object):
def isCanceled(self):
return (self._canceled or app.APP.stop_pkc or app.SYNC.stop_sync or
app.APP.suspend_threads or app.SYNC.suspend_sync)
class fullsync_mixin(object):
def __init__(self):
self._canceled = False
def abort(self):
"""Hit method to terminate the thread"""
self._canceled = True
# Let's NOT suspend sync threads but immediately terminate them
suspend = abort
@property
def suspend_reached(self):
"""Since we're not suspending, we'll never set it to True"""
return False
@suspend_reached.setter
def suspend_reached(self):
pass
def resume(self):
"""Obsolete since we're not suspending"""
pass
def isCanceled(self):
return (self._canceled or
app.APP.stop_pkc or
app.SYNC.stop_sync or
app.APP.suspend_threads)
"""Check whether we should exit this thread"""
return self._canceled
def update_kodi_library(video=True, music=True):

View file

@ -2,7 +2,6 @@
from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
from . import common
from ..plex_api import API
from ..plex_db import PlexDB
from ..kodi_db import KodiVideoDB
@ -19,13 +18,6 @@ PREFER_KODI_COLLECTION_ART = utils.settings('PreferKodiCollectionArt') == 'false
BATCH_SIZE = 500
def suspends():
return (app.APP.suspend_threads or
app.SYNC.stop_sync or
app.SYNC.db_scan or
app.SYNC.suspend_sync)
class FanartThread(backgroundthread.KillableThread):
"""
This will potentially take hours!
@ -36,16 +28,19 @@ class FanartThread(backgroundthread.KillableThread):
super(FanartThread, self).__init__()
def isSuspended(self):
return suspends()
return self._suspended or app.APP.is_playing_video
def run(self):
LOG.info('Starting FanartThread')
app.APP.register_fanart_thread(self)
try:
self._run_internal()
except Exception:
utils.ERROR(notify=True)
finally:
app.APP.deregister_fanart_thread(self)
def _run_internal(self):
LOG.info('Starting FanartThread')
finished = False
try:
for typus in SUPPORTED_TYPES:
@ -63,12 +58,8 @@ class FanartThread(backgroundthread.KillableThread):
BATCH_SIZE))
for plex_id in batch:
# Do the actual, time-consuming processing
if self.isCanceled():
if self.wait_while_suspended():
return
if self.isSuspended():
if self.isCanceled():
return
app.APP.monitor.waitForAbort(1)
process_fanart(plex_id, typus, self.refresh)
if len(batch) < BATCH_SIZE:
break
@ -80,7 +71,7 @@ class FanartThread(backgroundthread.KillableThread):
self.callback(finished)
class FanartTask(common.libsync_mixin, backgroundthread.Task):
class FanartTask(backgroundthread.Task):
"""
This task will also be executed while library sync is suspended!
"""
@ -154,11 +145,7 @@ def process_fanart(plex_id, plex_type, refresh=False):
setid,
v.KODI_TYPE_SET)
done = True
except utils.OperationalError:
# Caused if we reset the Plex database and this function has not yet
# returned
pass
finally:
if done is True and not suspends():
if done is True:
with PlexDB() as plexdb:
plexdb.set_fanart_synced(plex_id, plex_type)

View file

@ -19,7 +19,7 @@ if common.PLAYLIST_SYNC_ENABLED:
LOG = getLogger('PLEX.sync.full_sync')
# How many items will be put through the processing chain at once?
BATCH_SIZE = 200
BATCH_SIZE = 500
# Safety margin to filter PMS items - how many seconds to look into the past?
UPDATED_AT_SAFETY = 60 * 5
LAST_VIEWED_AT_SAFETY = 60 * 5
@ -44,7 +44,6 @@ class FullSync(common.fullsync_mixin):
"""
repair=True: force sync EVERY item
"""
self._canceled = False
self.repair = repair
self.callback = callback
self.queue = None
@ -69,6 +68,7 @@ class FullSync(common.fullsync_mixin):
self.context = None
self.get_children = None
self.successful = None
self.section_success = None
self.install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
self.threader = backgroundthread.ThreaderManager(
worker=backgroundthread.NonstoppingBackgroundWorker,
@ -83,9 +83,9 @@ class FullSync(common.fullsync_mixin):
progress = 0
self.dialog.update(progress,
'%s (%s)' % (self.section_name, self.section_type_text),
'%s/%s %s'
% (self.current, self.total, self.title))
if app.APP.player.isPlayingVideo():
'%s %s/%s'
% (self.title, self.current, self.total))
if app.APP.is_playing_video:
self.dialog.close()
self.dialog = None
@ -233,14 +233,14 @@ class FullSync(common.fullsync_mixin):
if not itemtype.update_userdata(xml_item, section['plex_type']):
# Somehow did not sync this item yet
itemtype.add_update(xml_item,
section['section_name'],
section['section_id'])
section_name=section['section_name'],
section_id=section['section_id'])
itemtype.plexdb.update_last_sync(int(xml_item.attrib['ratingKey']),
section['plex_type'],
self.current_sync)
self.current += 1
self.update_progressbar()
if (i + 1) % BATCH_SIZE == 0:
if (i + 1) % (10 * BATCH_SIZE) == 0:
break
if last:
break
@ -249,38 +249,39 @@ class FullSync(common.fullsync_mixin):
LOG.error('Could not entirely process section %s', section)
return False
def threaded_get_iterators(self, kinds, queue, updated_at=None,
last_viewed_at=None):
def threaded_get_iterators(self, kinds, queue, all_items=False):
"""
PF.SectionItems is costly, so let's do it asynchronous
"""
if self.repair:
updated_at = None
last_viewed_at = None
else:
updated_at = updated_at - UPDATED_AT_SAFETY if updated_at else None
last_viewed_at = last_viewed_at - LAST_VIEWED_AT_SAFETY \
if last_viewed_at else None
try:
for kind in kinds:
for section in (x for x in sections.SECTIONS
if x['plex_type'] == kind[1]):
if self.isCanceled():
return
if not section['sync_to_kodi']:
LOG.info('User chose to not sync section %s', section)
continue
element = copy.deepcopy(section)
element['section_type'] = element['plex_type']
element['plex_type'] = kind[0]
element['element_type'] = kind[1]
element['context'] = kind[2]
element['get_children'] = kind[3]
if self.repair or all_items:
updated_at = None
else:
updated_at = section['last_sync'] - UPDATED_AT_SAFETY \
if section['last_sync'] else None
try:
element['iterator'] = PF.SectionItems(section['section_id'],
plex_type=kind[0],
updated_at=updated_at,
last_viewed_at=last_viewed_at)
last_viewed_at=None)
except RuntimeError:
LOG.warn('Sync at least partially unsuccessful')
self.successful = False
self.section_success = False
else:
queue.put(element)
finally:
@ -304,14 +305,13 @@ class FullSync(common.fullsync_mixin):
# Already start setting up the iterators. We need to enforce
# syncing e.g. show before season before episode
iterator_queue = Queue.Queue()
updated_at = int(utils.settings('lastfullsync')) or None
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
None,
kinds,
iterator_queue,
updated_at=updated_at)
iterator_queue)
backgroundthread.BGThreader.addTask(task)
while True:
self.section_success = True
section = iterator_queue.get()
iterator_queue.task_done()
if section is None:
@ -324,10 +324,14 @@ class FullSync(common.fullsync_mixin):
# Now do the heavy lifting
if self.isCanceled() or not self.addupdate_section(section):
return False
if self.section_success:
# Need to check because a thread might have missed to get
# some items from the PMS
with PlexDB() as plexdb:
# Set the new time mark for the next delta sync
plexdb.update_section_last_sync(section['section_id'],
self.current_sync)
common.update_kodi_library(video=True, music=True)
if self.successful:
# Set timestamp for next sync - neglecting playstates!
utils.settings('lastfullsync', value=str(int(self.current_sync)))
# In order to not delete all your songs again
if app.SYNC.enable_music:
kinds.extend([
@ -337,6 +341,8 @@ class FullSync(common.fullsync_mixin):
# were set to unwatched). Also mark all items on the PMS to be able
# to delete the ones still in Kodi
LOG.info('Start synching playstate and userdata for every item')
# Make sure we're not showing an item's title in the sync dialog
self.title = ''
self.threader.shutdown()
self.threader = None
if not self.show_dialog_userdata and self.dialog:
@ -346,7 +352,8 @@ class FullSync(common.fullsync_mixin):
task = backgroundthread.FunctionAsTask(self.threaded_get_iterators,
None,
kinds,
iterator_queue)
iterator_queue,
all_items=True)
backgroundthread.BGThreader.addTask(task)
while True:
section = iterator_queue.get()
@ -363,7 +370,7 @@ class FullSync(common.fullsync_mixin):
return False
# Delete movies that are not on Plex anymore
LOG.info('Looking for items to delete')
LOG.debug('Looking for items to delete')
kinds = [
(v.PLEX_TYPE_MOVIE, itemtypes.Movie),
(v.PLEX_TYPE_SHOW, itemtypes.Show),
@ -392,14 +399,22 @@ class FullSync(common.fullsync_mixin):
LOG.debug('Done deleting')
return True
@utils.log_time
def run(self):
app.APP.register_thread(self)
try:
self._run()
finally:
app.APP.deregister_thread(self)
LOG.info('Done full_sync')
@utils.log_time
def _run(self):
self.current_sync = timing.plex_now()
# 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(self):
return
self.successful = True
try:
@ -434,7 +449,6 @@ class FullSync(common.fullsync_mixin):
icon='{error}')
if self.callback:
self.callback(self.successful)
LOG.info('Done full_sync')
def start(show_dialog, repair=False, callback=None):

View file

@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
from logging import getLogger
import copy
from . import common, videonodes
from . import videonodes
from ..utils import cast
from ..plex_db import PlexDB
from .. import kodi_db
@ -13,21 +13,39 @@ from .. import plex_functions as PF, music, utils, variables as v, app
LOG = getLogger('PLEX.sync.sections')
BATCH_SIZE = 200
BATCH_SIZE = 500
VNODES = videonodes.VideoNodes()
PLAYLISTS = {}
NODES = {}
SECTIONS = []
# Need a way to interrupt
IS_CANCELED = None
def isCanceled():
return app.APP.stop_pkc or app.APP.suspend_threads or app.SYNC.stop_sync
def force_full_sync():
"""
Resets the sync timestamp for all sections to 0, thus forcing a subsequent
full sync (not delta)
"""
LOG.info('Telling PKC to do a full sync instead of a delta sync')
with PlexDB() as plexdb:
plexdb.force_full_sync()
def sync_from_pms():
def sync_from_pms(parent_self):
"""
Sync the Plex library sections
"""
global IS_CANCELED
IS_CANCELED = parent_self.isCanceled
try:
return _sync_from_pms()
finally:
IS_CANCELED = None
def _sync_from_pms():
global PLAYLISTS, NODES, SECTIONS
sections = PF.get_plex_sections()
try:
sections.attrib
@ -38,7 +56,7 @@ def sync_from_pms():
# Will reboot Kodi is new library detected
music.excludefromscan_music_folders(xml=sections)
global PLAYLISTS, NODES, SECTIONS
VNODES.clearProperties()
SECTIONS = []
NODES = {
v.PLEX_TYPE_MOVIE: [],
@ -47,64 +65,51 @@ def sync_from_pms():
v.PLEX_TYPE_PHOTO: []
}
PLAYLISTS = copy.deepcopy(NODES)
sorted_sections = []
for section in sections:
if (section.attrib['type'] in
(v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO,
v.PLEX_TYPE_ARTIST)):
sorted_sections.append(section.attrib['title'])
LOG.debug('Sorted sections: %s', sorted_sections)
totalnodes = len(sorted_sections)
VNODES.clearProperties()
with PlexDB() as plexdb:
# Backup old sections to delete them later, if needed (at the end
# of this method, only unused sections will be left in old_sections)
old_sections = list(plexdb.section_ids())
old_sections = list(plexdb.all_sections())
with kodi_db.KodiVideoDB() as kodidb:
for section in sections:
for index, section in enumerate(sections):
_process_section(section,
kodidb,
plexdb,
sorted_sections,
old_sections,
totalnodes)
index,
old_sections)
if old_sections:
# Section has been deleted on the PMS
delete_sections(old_sections)
# update sections for all:
with PlexDB() as plexdb:
SECTIONS = list(plexdb.section_infos())
utils.window('Plex.nodes.total', str(totalnodes))
LOG.info("Finished processing library sections: %s", SECTIONS)
SECTIONS = list(plexdb.all_sections())
utils.window('Plex.nodes.total', str(len(sections)))
LOG.info("Finished processing %s library sections: %s", len(sections), SECTIONS)
if app.CONN.machine_identifier != utils.settings('sections_asked_for_machine_identifier'):
LOG.info('First time connecting to this PMS, choosing libraries')
if choose_libraries():
with PlexDB() as plexdb:
SECTIONS = list(plexdb.all_sections())
return True
def _process_section(section_xml, kodidb, plexdb, sorted_sections,
old_sections, totalnodes):
def _process_section(section_xml, kodidb, plexdb, index, old_sections):
global PLAYLISTS, NODES
folder = section_xml.attrib
plex_type = folder['type']
# Only process supported formats
if plex_type not in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW,
v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_PHOTO):
LOG.error('Unsupported Plex section type: %s', folder)
return totalnodes
return
section_id = cast(int, folder['key'])
section_name = folder['title']
global PLAYLISTS, NODES
# Prevent duplicate for nodes of the same type
nodes = NODES[plex_type]
# Prevent duplicate for playlists of the same type
playlists = PLAYLISTS[plex_type]
# Get current media folders from plex database
section = plexdb.section(section_id)
try:
current_sectionname = section[1]
current_sectiontype = section[2]
current_tagid = section[3]
except TypeError:
if not section:
LOG.info('Creating section id: %s in Plex database.', section_id)
tagid = kodidb.create_tag(section_name)
# Create playlist for the video library
@ -114,28 +119,30 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
playlists.append(section_name)
# Create the video node
if section_name not in nodes:
VNODES.viewNode(sorted_sections.index(section_name),
VNODES.viewNode(index,
section_name,
plex_type,
None,
section_id)
nodes.append(section_name)
totalnodes += 1
# Add view to plex database
plexdb.add_section(section_id, section_name, plex_type, tagid)
plexdb.add_section(section_id,
section_name,
plex_type,
tagid,
True, # Sync this new section for now
None)
else:
LOG.info('Found library section id %s, name %s, type %s, tagid %s',
section_id, current_sectionname, current_sectiontype,
current_tagid)
section_id, section['section_name'], section['plex_type'],
section['kodi_tagid'])
# Remove views that are still valid to delete rest later
try:
old_sections.remove(section_id)
except ValueError:
# View was just created, nothing to remove
pass
for section in old_sections:
if section['section_id'] == section_id:
old_sections.remove(section)
break
# View was modified, update with latest info
if current_sectionname != section_name:
if section['section_name'] != section_name:
LOG.info('section id: %s new sectionname: %s',
section_id, section_name)
tagid = kodidb.create_tag(section_name)
@ -144,22 +151,24 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
plexdb.add_section(section_id,
section_name,
plex_type,
tagid)
tagid,
section['sync_to_kodi'], # Use "old" setting
section['last_sync'])
if plexdb.section_id_by_name(current_sectionname) is None:
if plexdb.section_id_by_name(section['section_name']) is None:
# The tag could be a combined view. Ensure there's
# no other tags with the same name before deleting
# playlist.
utils.playlist_xsp(plex_type,
current_sectionname,
section['section_name'],
section_id,
current_sectiontype,
section['plex_type'],
True)
# Delete video node
if plex_type != "musicvideos":
VNODES.viewNode(
indexnumber=sorted_sections.index(section_name),
tagname=current_sectionname,
indexnumber=index,
tagname=section['section_name'],
mediatype=plex_type,
viewtype=None,
viewid=section_id,
@ -172,17 +181,16 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
playlists.append(section_name)
# Add new video node
if section_name not in nodes and plex_type != "musicvideos":
VNODES.viewNode(sorted_sections.index(section_name),
VNODES.viewNode(index,
section_name,
plex_type,
None,
section_id)
nodes.append(section_name)
totalnodes += 1
# Update items with new tag
for kodi_id in plexdb.kodiid_by_sectionid(section_id, plex_type):
kodidb.update_tag(
current_tagid, tagid, kodi_id, current_sectiontype)
section['kodi_tagid'], tagid, kodi_id, section['plex_type'])
else:
# Validate the playlist exists or recreate it
if (section_name not in playlists and plex_type in
@ -193,14 +201,12 @@ def _process_section(section_xml, kodidb, plexdb, sorted_sections,
playlists.append(section_name)
# Create the video node if not already exists
if section_name not in nodes and plex_type != "musicvideos":
VNODES.viewNode(sorted_sections.index(section_name),
VNODES.viewNode(index,
section_name,
plex_type,
None,
section_id)
nodes.append(section_name)
totalnodes += 1
return totalnodes
def _delete_kodi_db_items(section_id, section_type):
@ -226,7 +232,7 @@ def _delete_kodi_db_items(section_id, section_type):
with kodi_context(texture_db=True) as kodidb:
typus = context(None, plexdb=plexdb, kodidb=kodidb)
for plex_id in plex_ids:
if isCanceled():
if IS_CANCELED():
return False
typus.remove(plex_id)
if len(plex_ids) < BATCH_SIZE:
@ -239,25 +245,69 @@ def delete_sections(old_sections):
Deletes all elements for a Plex section that has been deleted. (e.g. all
TV shows, Seasons and Episodes of a Show section)
"""
try:
LOG.info("Removing entire Plex library sections: %s", old_sections)
for section in old_sections:
# "Deleting <section_name>"
utils.dialog('notification',
heading='{plex}',
message='%s %s' % (utils.lang(30052), section['section_name']),
icon='{plex}',
sound=False)
if section['plex_type'] == v.PLEX_TYPE_PHOTO:
# not synced - just remove the link in our Plex sections table
pass
else:
if not _delete_kodi_db_items(section['section_id'], section['plex_type']):
return
# Only remove Plex entry if we've removed all items first
with PlexDB() as plexdb:
old_sections = [plexdb.section(x) for x in old_sections]
LOG.info("Removing entire Plex library sections: %s", old_sections)
for section in old_sections:
# "Deleting <section_name>"
utils.dialog('notification',
heading='{plex}',
message='%s %s' % (utils.lang(30052), section[1]),
icon='{plex}',
sound=False)
if section[2] == v.PLEX_TYPE_PHOTO:
# not synced - just remove the link in our Plex sections table
pass
plexdb.remove_section(section['section_id'])
def choose_libraries():
"""
Displays a dialog for the user to select the libraries he wants synched
Returns True if this was successful, False if not
"""
# Re-set value in order to make sure we got the lastest user input
app.SYNC.enable_music = utils.settings('enableMusic') == 'true'
import xbmcgui
sections = []
preselect = []
index = 0
for section in SECTIONS:
if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST:
LOG.info('Ignoring music section: %s', section)
continue
elif section['plex_type'] == v.PLEX_TYPE_PHOTO:
continue
else:
sections.append(section['section_name'])
if section['sync_to_kodi']:
preselect.append(index)
index += 1
# "Select Plex libraries to sync"
selected = xbmcgui.Dialog().multiselect(utils.lang(30524),
sections,
preselect=preselect,
useDetails=False)
if selected is None:
# User canceled
return False
index = 0
with PlexDB() as plexdb:
for section in SECTIONS:
if not app.SYNC.enable_music and section['plex_type'] == v.PLEX_TYPE_ARTIST:
continue
elif section['plex_type'] == v.PLEX_TYPE_PHOTO:
continue
else:
if not _delete_kodi_db_items(section[0], section[2]):
return
# Only remove Plex entry if we've removed all items first
with PlexDB() as plexdb:
plexdb.remove_section(section[0])
finally:
common.update_kodi_library()
sync = True if index in selected else False
plexdb.update_section_sync(section['section_id'], sync)
index += 1
sections = list(plexdb.all_sections())
LOG.info('Plex libraries to sync: %s', sections)
utils.settings('sections_asked_for_machine_identifier',
value=app.CONN.machine_identifier)
return True

View file

@ -476,7 +476,7 @@ class VideoNodes(object):
"unwatched.content","unwatched.path",
"recent.title","recent.content","recent.path",
"recentepisodes.title","recentepisodes.content",
"recentepisodes.path","inprogressepisodes.title",
"recentepisodes.path", "inprogressepisodes.title",
"inprogressepisodes.content","inprogressepisodes.path"
]

View file

@ -23,10 +23,6 @@ WEBSOCKET_MESSAGES = []
PLAYSTATE_SESSIONS = {}
def interrupt_processing():
return app.APP.stop_pkc or app.APP.suspend_threads or app.SYNC.stop_sync
def multi_delete(input_list, delete_list):
"""
Deletes the list items of input_list at the positions in delete_list
@ -81,9 +77,6 @@ def process_websocket_messages():
update_kodi_video_library, update_kodi_music_library = False, False
delete_list = []
for i, message in enumerate(WEBSOCKET_MESSAGES):
if interrupt_processing():
# Chances are that Kodi gets shut down
break
if message['state'] == 9:
successful, video, music = process_delete_message(message)
elif now - message['timestamp'] < app.SYNC.backgroundsync_saftymargin:
@ -285,10 +278,14 @@ def process_playing(data):
PLAYSTATE_SESSIONS)
# Attach Kodi info to the session
try:
PLAYSTATE_SESSIONS[session_key]['file_id'] = typus['kodi_fileid']
PLAYSTATE_SESSIONS[session_key]['kodi_fileid'] = typus['kodi_fileid']
except KeyError:
# media type without file - no need to do anything
continue
if typus['plex_type'] == v.PLEX_TYPE_EPISODE:
PLAYSTATE_SESSIONS[session_key]['kodi_fileid_2'] = typus['kodi_fileid_2']
else:
PLAYSTATE_SESSIONS[session_key]['kodi_fileid_2'] = None
PLAYSTATE_SESSIONS[session_key]['kodi_id'] = typus['kodi_id']
PLAYSTATE_SESSIONS[session_key]['kodi_type'] = typus['kodi_type']
session = PLAYSTATE_SESSIONS[session_key]
@ -357,9 +354,9 @@ def process_playing(data):
session['viewCount'],
resume,
session['duration'],
session['file_id'],
timing.unix_timestamp(),
v.PLEX_TYPE_FROM_KODI_TYPE[session['kodi_type']])
session['kodi_fileid'],
session['kodi_fileid_2'],
timing.unix_timestamp())
def cache_artwork(plex_id, plex_type, kodi_id=None, kodi_type=None):

View file

@ -11,7 +11,6 @@ from .plex_api import API
from .plex_db import PlexDB
from . import plex_functions as PF
from . import utils
from .downloadutils import DownloadUtils as DU
from .kodi_db import KodiVideoDB
from . import playlist_func as PL
from . import playqueue as PQ
@ -50,10 +49,18 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
global RESOLVE
# If started via Kodi context menu, we never resolve
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
if not app.ACCOUNT.authenticated:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
if not app.CONN.online or not app.ACCOUNT.authenticated:
if not app.CONN.online:
LOG.error('PMS not online for playback')
# "{0} offline"
utils.dialog('notification',
utils.lang(29999),
utils.lang(39213).format(app.CONN.server_name),
icon='{plex}')
else:
LOG.error('Not yet authenticated for PMS, abort starting playback')
# "Unauthorized for PMS"
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
_ensure_resolve(abort=True)
return
with app.APP.lock_playqueues:
@ -135,16 +142,8 @@ def _playlist_playback(plex_id, plex_type):
for the next item in line :-)
(by the way: trying to get active Kodi player id will return [])
"""
xml = PF.GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (IndexError, TypeError, AttributeError):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
xml = PF.GetPlexMetadata(plex_id, reraise=True)
if xml in (None, 401):
_ensure_resolve(abort=True)
return
# Kodi bug: playqueue will ALWAYS be audio playqueue UNTIL playback
@ -164,16 +163,9 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
Playback setup if Kodi starts playing an item for the first time.
"""
LOG.info('Initializing PKC playback')
xml = PF.GetPlexMetadata(plex_id)
try:
xml[0].attrib
except (IndexError, TypeError, AttributeError):
xml = PF.GetPlexMetadata(plex_id, reraise=True)
if xml in (None, 401):
LOG.error('Could not get a PMS xml for plex id %s', plex_id)
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
if playqueue.kodi_pl.size() > 1:
@ -182,6 +174,11 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
_init_existing_kodi_playlist(playqueue, pos)
except PL.PlaylistError:
LOG.error('Playback_init for existing Kodi playlist failed')
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
_ensure_resolve(abort=True)
return
# Now we need to use setResolvedUrl for the item at position ZERO
@ -259,12 +256,10 @@ def _ensure_resolve(abort=False):
will be destroyed.
"""
if RESOLVE:
if not abort:
# Releases the other Python thread without a ListItem
transfer.send(True)
else:
# Shows PKC error message
transfer.send(None)
# Releases the other Python thread without a ListItem
transfer.send(True)
# Shows PKC error message
# transfer.send(None)
if abort:
# Reset some playback variables
app.PLAYSTATE.context_menu_play = False
@ -418,7 +413,10 @@ def _conclude_playback(playqueue, pos):
LOG.info('Resuming playback at %s', item.offset)
if v.KODIVERSION >= 18 and api:
# Kodi 18 Alpha 3 broke StartOffset
percent = item.offset / api.runtime() * 100.0
try:
percent = item.offset / api.runtime() * 100.0
except ZeroDivisionError:
percent = 0.0
LOG.debug('Resuming at %s percent', percent)
listitem.setProperty('StartPercent', str(percent))
else:
@ -446,21 +444,20 @@ def process_indirect(key, offset, resolve=True):
key, offset, resolve)
global RESOLVE
RESOLVE = resolve
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset)) if offset != '0' else None
if key.startswith('http') or key.startswith('{server}'):
xml = DU().downloadUrl(key)
xml = PF.get_playback_xml(key, app.CONN.server_name)
elif key.startswith('/system/services'):
xml = DU().downloadUrl('http://node.plexapp.com:32400%s' % key)
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s' % key,
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
else:
xml = DU().downloadUrl('{server}%s' % key)
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download PMS metadata')
xml = PF.get_playback_xml('{server}%s' % key, app.CONN.server_name)
if xml is None:
_ensure_resolve(abort=True)
return
if offset != '0':
offset = int(v.PLEX_TO_KODI_TIMEFACTOR * float(offset))
# Todo: implement offset
api = API(xml[0])
listitem = transfer.PKCListItem()
api.create_listitem(listitem)
@ -469,19 +466,31 @@ def process_indirect(key, offset, resolve=True):
playqueue.clear()
item = PL.Playlist_Item()
item.xml = xml[0]
item.offset = int(offset)
item.offset = offset
item.plex_type = v.PLEX_TYPE_CLIP
item.playmethod = 'DirectStream'
# Need to get yet another xml to get the final playback url
xml = DU().downloadUrl('http://node.plexapp.com:32400%s'
% xml[0][0][0].attrib['key'])
try:
xml[0].attrib
xml = PF.get_playback_xml('http://node.plexapp.com:32400%s'
% xml[0][0][0].attrib['key'],
'plexapp.com',
authenticate=False,
token=app.ACCOUNT.plex_token)
except (TypeError, IndexError, AttributeError):
LOG.error('Could not download last xml for playurl')
LOG.error('XML malformed: %s', xml.attrib)
xml = None
if xml is None:
_ensure_resolve(abort=True)
return
playurl = xml[0].attrib['key']
try:
playurl = xml[0].attrib['key']
except (TypeError, IndexError, AttributeError):
LOG.error('Last xml malformed: %s', xml.attrib)
_ensure_resolve(abort=True)
return
item.file = playurl
listitem.setPath(utils.try_encode(playurl))
playqueue.items.append(item)
@ -533,7 +542,7 @@ def threaded_playback(kodi_playlist, startpos, offset):
app.APP.player.play(kodi_playlist, None, False, startpos)
if offset and offset != '0':
i = 0
while not app.APP.player.isPlaying():
while not app.APP.is_playing:
app.APP.monitor.waitForAbort(0.1)
i += 1
if i > 100:

View file

@ -43,7 +43,7 @@ IGNORE_PLEX_PLAYLIST_CHANGE = list()
def isCanceled():
return app.APP.stop_pkc or app.SYNC.stop_sync or app.APP.suspend_threads
return app.APP.stop_pkc or app.SYNC.stop_sync
def kodi_playlist_monitor():

View file

@ -17,7 +17,7 @@ LOG = getLogger('PLEX.playlists.kodi_pl')
###############################################################################
REGEX_FILE_NUMBERING = re.compile(r'''_(\d+)\.\w+$''')
REGEX_FILE_NUMBERING = re.compile(r'''_(\d\d)\.\w+$''')
def create(plex_id):
@ -53,6 +53,10 @@ def create(plex_id):
'%s_01.m3u' % name[:min(len(name), 248)])
else:
number = int(occurance.group(1)) + 1
if number > 3:
LOG.error('Detected spanning tree issue, abort sync for %s',
playlist)
raise PlaylistError('Spanning tree warning')
basename = re.sub(REGEX_FILE_NUMBERING, '', path)
path = '%s_%02d.m3u' % (basename, number)
LOG.debug('Kodi playlist path: %s', path)

View file

@ -97,12 +97,6 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
(playlist) are swapped. This is what this monitor is for. Don't replace
this mechanism till Kodi's implementation of playlists has improved
"""
def isSuspended(self):
"""
Returns True if the thread is suspended
"""
return self._suspended or app.APP.suspend_threads
def _compare_playqueues(self, playqueue, new):
"""
Used to poll the Kodi playqueue and update the Plex playqueue if needed
@ -193,11 +187,17 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
def run(self):
LOG.info("----===## Starting PlayqueueMonitor ##===----")
app.APP.register_thread(self)
try:
self._run()
finally:
app.APP.deregister_thread(self)
LOG.info("----===## PlayqueueMonitor stopped ##===----")
def _run(self):
while not self.isCanceled():
while self.isSuspended():
if self.isCanceled():
break
app.APP.monitor.waitForAbort(1)
if self.wait_while_suspended():
return
with app.APP.lock_playqueues:
for playqueue in PLAYQUEUES:
kodi_pl = js.playlist_get_items(playqueue.playlistid)
@ -212,4 +212,3 @@ class PlayqueueMonitor(backgroundthread.KillableThread):
self._compare_playqueues(playqueue, kodi_pl)
playqueue.old_kodi_pl = list(kodi_pl)
app.APP.monitor.waitForAbort(0.2)
LOG.info("----===## PlayqueueMonitor stopped ##===----")

View file

@ -147,7 +147,8 @@ class PlayUtils():
return False
return True
def get_max_bitrate(self):
@staticmethod
def get_max_bitrate():
# get the addon video quality
videoQuality = utils.settings('maxVideoQualities')
bitrate = {
@ -167,7 +168,8 @@ class PlayUtils():
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
def getH265(self):
@staticmethod
def getH265():
"""
Returns the user settings for transcoding h265: boundary resolutions
of 480, 720 or 1080 as an int
@ -182,7 +184,8 @@ class PlayUtils():
}
return H265[utils.settings('transcodeH265')]
def get_bitrate(self):
@staticmethod
def get_bitrate():
"""
Get the desired transcoding bitrate from the settings
"""
@ -203,7 +206,8 @@ class PlayUtils():
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(videoQuality, 2147483)
def get_resolution(self):
@staticmethod
def get_resolution():
"""
Get the desired transcoding resolutions from the settings
"""

View file

@ -1256,7 +1256,7 @@ class API(object):
"""
return self.item.get('librarySectionID')
def collections_match(self):
def collections_match(self, section_id):
"""
Downloads one additional xml from the PMS in order to return a list of
tuples [(collection_id, plex_id), ...] for all collections of the
@ -1264,7 +1264,7 @@ class API(object):
Pass in the collection id of e.g. the movie's metadata
"""
if self.collections is None:
self.collections = PF.collections(self.library_section_id())
self.collections = PF.collections(section_id)
if self.collections is None:
LOG.error('Could not download collections for %s',
self.library_section_id())
@ -1769,7 +1769,7 @@ class API(object):
if force_check is False:
# Validate the path is correct with user intervention
if self.ask_to_validate(path):
app.SYNC.stop_sync = True
app.APP.stop_threads(block=False)
path = None
app.SYNC.path_verified = True
else:

View file

@ -80,13 +80,8 @@ class PlexCompanion(backgroundthread.KillableThread):
self.subscription_manager = None
super(PlexCompanion, self).__init__()
def isSuspended(self):
"""
Returns True if the thread is suspended
"""
return self._suspended or app.APP.suspend
def _process_alexa(self, data):
@staticmethod
def _process_alexa(data):
xml = PF.GetPlexMetadata(data['key'])
try:
xml[0].attrib
@ -141,7 +136,8 @@ class PlexCompanion(backgroundthread.KillableThread):
executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params)))
def _process_playlist(self, data):
@staticmethod
def _process_playlist(data):
# Get the playqueue ID
_, container_key, query = PF.ParseContainerKey(data['containerKey'])
try:
@ -165,7 +161,8 @@ class PlexCompanion(backgroundthread.KillableThread):
offset=data.get('offset'),
transient_token=data.get('token'))
def _process_streams(self, data):
@staticmethod
def _process_streams(data):
"""
Plex Companion client adjusted audio or subtitle stream
"""
@ -186,7 +183,8 @@ class PlexCompanion(backgroundthread.KillableThread):
else:
LOG.error('Unknown setStreams command: %s', data)
def _process_refresh(self, data):
@staticmethod
def _process_refresh(data):
"""
example data: {'playQueueID': '8475', 'commandID': '11'}
"""
@ -245,6 +243,7 @@ class PlexCompanion(backgroundthread.KillableThread):
"""
Ensure that sockets will be closed no matter what
"""
app.APP.register_thread(self)
try:
self._run()
finally:
@ -257,7 +256,8 @@ class PlexCompanion(backgroundthread.KillableThread):
self.httpd.socket.close()
except AttributeError:
pass
LOG.info("----===## Plex Companion stopped ##===----")
app.APP.deregister_thread(self)
LOG.info("----===## Plex Companion stopped ##===----")
def _run(self):
httpd = self.httpd
@ -303,10 +303,8 @@ class PlexCompanion(backgroundthread.KillableThread):
# If we are not authorized, sleep
# Otherwise, we trigger a download which leads to a
# re-authorizations
while self.isSuspended():
if self.isCanceled():
break
app.APP.monitor.waitForAbort(1)
if self.wait_while_suspended():
break
try:
message_count += 1
if httpd:

View file

@ -194,7 +194,8 @@ def initialize():
section_name TEXT,
plex_type TEXT,
kodi_tagid INTEGER,
sync_to_kodi INTEGER)
sync_to_kodi INTEGER,
last_sync INTEGER)
''')
plexdb.cursor.execute('''
CREATE TABLE IF NOT EXISTS movie(
@ -239,6 +240,7 @@ def initialize():
parent_id INTEGER,
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_fileid_2 INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER)

View file

@ -4,43 +4,39 @@ from __future__ import absolute_import, division, unicode_literals
class Sections(object):
def section_ids(self):
def all_sections(self):
"""
Returns an iterator for section Plex ids for all sections
"""
self.cursor.execute('SELECT section_id FROM sections')
return (x[0] for x in self.cursor)
def section_infos(self):
"""
Returns an iterator for dicts for all Plex libraries:
{
'section_id'
'section_name'
'plex_type'
'kodi_tagid'
'sync_to_kodi'
}
Returns an iterator for all sections
"""
self.cursor.execute('SELECT * FROM sections')
return ({'section_id': x[0],
'section_name': x[1],
'plex_type': x[2],
'kodi_tagid': x[3],
'sync_to_kodi': x[4]} for x in self.cursor)
return (self.entry_to_section(x) for x in self.cursor)
def section(self, section_id):
"""
For section_id, returns the tuple (or None)
For section_id, returns the dict
section_id INTEGER PRIMARY KEY,
section_name TEXT,
plex_type TEXT,
kodi_tagid INTEGER,
sync_to_kodi INTEGER
sync_to_kodi BOOL,
last_sync INTEGER
"""
self.cursor.execute('SELECT * FROM sections WHERE section_id = ? LIMIT 1',
(section_id, ))
return self.cursor.fetchone()
return self.entry_to_section(self.cursor.fetchone())
@staticmethod
def entry_to_section(entry):
if not entry:
return
return {
'section_id': entry[0],
'section_name': entry[1],
'plex_type': entry[2],
'kodi_tagid': entry[3],
'sync_to_kodi': entry[4] == 1,
'last_sync': entry[5]
}
def section_id_by_name(self, section_name):
"""
@ -54,22 +50,35 @@ class Sections(object):
pass
def add_section(self, section_id, section_name, plex_type, kodi_tagid,
sync_to_kodi=True):
sync_to_kodi, last_sync):
"""
Appends a Plex section to the Plex sections table
sync=False: Plex library won't be synced to Kodi
"""
query = '''
INSERT OR REPLACE INTO sections(
section_id, section_name, plex_type, kodi_tagid, sync_to_kodi)
VALUES (?, ?, ?, ?, ?)
section_id,
section_name,
plex_type,
kodi_tagid,
sync_to_kodi,
last_sync)
VALUES (?, ?, ?, ?, ?, ?)
'''
self.cursor.execute(query,
(section_id,
section_name,
plex_type,
kodi_tagid,
sync_to_kodi))
sync_to_kodi,
last_sync))
def update_section(self, section_id, section_name):
"""
Updates the section with section_id
"""
query = 'UPDATE sections SET section_name = ? WHERE section_id = ?'
self.cursor.execute(query, (section_name, section_id))
def remove_section(self, section_id):
"""
@ -77,3 +86,35 @@ class Sections(object):
"""
self.cursor.execute('DELETE FROM sections WHERE section_id = ?',
(section_id, ))
def update_section_sync(self, section_id, sync_to_kodi):
"""
Updates whether we should sync sections_id (sync=True) or not
"""
if sync_to_kodi:
query = '''
UPDATE sections
SET sync_to_kodi = ?
WHERE section_id = ?
'''
else:
# Set last_sync = 0 in order to force a full sync if reactivated
query = '''
UPDATE sections
SET sync_to_kodi = ?, last_sync = 0
WHERE section_id = ?
'''
self.cursor.execute(query, (sync_to_kodi, section_id))
def update_section_last_sync(self, section_id, last_sync):
"""
Updates the timestamp for the section
"""
self.cursor.execute('UPDATE sections SET last_sync = ? WHERE section_id = ?',
(last_sync, section_id))
def force_full_sync(self):
"""
Sets the last_sync flag to 0 for every section
"""
self.cursor.execute('UPDATE sections SET last_sync = 0')

View file

@ -59,7 +59,7 @@ class TVShows(object):
def add_episode(self, plex_id, checksum, section_id, show_id,
grandparent_id, season_id, parent_id, kodi_id, kodi_fileid,
kodi_pathid, last_sync):
kodi_fileid_2, kodi_pathid, last_sync):
"""
Appends or replaces an entry into the plex table
"""
@ -75,10 +75,11 @@ class TVShows(object):
parent_id,
kodi_id,
kodi_fileid,
kodi_fileid_2,
kodi_pathid,
fanart_synced,
last_sync)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''',
(plex_id,
checksum,
@ -89,6 +90,7 @@ class TVShows(object):
parent_id,
kodi_id,
kodi_fileid,
kodi_fileid_2,
kodi_pathid,
0,
last_sync))
@ -129,21 +131,6 @@ class TVShows(object):
return self.entry_to_season(self.cursor.fetchone())
def episode(self, plex_id):
"""
Returns the show info as a tuple for the TV show with plex_id:
plex_id INTEGER PRIMARY KEY,
checksum INTEGER UNIQUE,
section_id INTEGER,
show_id INTEGER, # plex_id of the parent show
grandparent_id INTEGER, # kodi_id of the parent show
season_id INTEGER, # plex_id of the parent season
parent_id INTEGER, # kodi_id of the parent season
kodi_id INTEGER,
kodi_fileid INTEGER,
kodi_pathid INTEGER,
fanart_synced INTEGER,
last_sync INTEGER
"""
if plex_id is None:
return
self.cursor.execute('SELECT * FROM episode WHERE plex_id = ? LIMIT 1',
@ -166,9 +153,10 @@ class TVShows(object):
'parent_id': entry[6],
'kodi_id': entry[7],
'kodi_fileid': entry[8],
'kodi_pathid': entry[9],
'fanart_synced': entry[10],
'last_sync': entry[11]
'kodi_fileid_2': entry[9],
'kodi_pathid': entry[10],
'fanart_synced': entry[11],
'last_sync': entry[12]
}
@staticmethod

View file

@ -9,7 +9,7 @@ from copy import deepcopy
from time import time
from threading import Thread
from .downloadutils import DownloadUtils as DU
from .downloadutils import DownloadUtils as DU, exceptions
from . import backgroundthread, utils, plex_tv, variables as v, app
###############################################################################
@ -454,7 +454,7 @@ def _poke_pms(pms, queue):
url, pms['uuid'], xml.get('machineIdentifier'))
def GetPlexMetadata(key):
def GetPlexMetadata(key, reraise=False):
"""
Returns raw API metadata for key as an etree XML.
@ -481,18 +481,71 @@ def GetPlexMetadata(key):
# 'includeConcerts': 1
}
url = url + '?' + urlencode(arguments)
xml = DU().downloadUrl(url)
if xml == 401:
# Either unauthorized (taken care of by doUtils) or PMS under strain
return 401
# Did we receive a valid XML?
try:
xml.attrib
# Nope we did not receive a valid XML
except AttributeError:
LOG.error("Error retrieving metadata for %s", url)
xml = None
return xml
xml = DU().downloadUrl(url, reraise=reraise)
except exceptions.RequestException:
# "PMS offline"
utils.dialog('notification',
utils.lang(29999),
utils.lang(39213).format(app.CONN.server_name),
icon='{plex}')
except Exception:
# "Error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30135),
icon='{error}')
else:
if xml == 401:
# Either unauthorized (taken care of by doUtils) or PMS under strain
return 401
# Did we receive a valid XML?
try:
xml[0].attrib
# Nope we did not receive a valid XML
except (TypeError, IndexError, AttributeError):
LOG.error("Error retrieving metadata for %s", url)
xml = None
return xml
def get_playback_xml(url, server_name, authenticate=True, token=None):
"""
Returns None if something went wrong
"""
header_options = {'X-Plex-Token': token} if not authenticate else None
try:
xml = DU().downloadUrl(url,
authenticate=authenticate,
headerOptions=header_options,
reraise=True)
except exceptions.RequestException:
# "{0} offline"
utils.dialog('notification',
utils.lang(29999),
utils.lang(39213).format(server_name),
icon='{plex}')
except Exception as e:
LOG.error(e)
import traceback
LOG.error("Traceback:\n%s", traceback.format_exc())
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
else:
try:
xml[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not get a valid xml, unfortunately')
# "Play error"
utils.dialog('notification',
utils.lang(29999),
utils.lang(30128),
icon='{error}')
else:
return xml
def GetAllPlexChildren(key):

View file

@ -40,7 +40,7 @@ RESOURCES_XML = ('%s<MediaContainer>\n'
'</MediaContainer>\n') % (v.XML_HEADER,
v.ADDON_NAME,
v.PLATFORM,
v.ADDON_VERSION)
v.PLATFORM_VERSION)
class MyHandler(BaseHTTPRequestHandler):
"""
@ -134,7 +134,7 @@ class MyHandler(BaseHTTPRequestHandler):
CLIENT_DICT[self.client_address[0]] = []
tracker = CLIENT_DICT[self.client_address[0]]
tracker.append(self.client_address[1])
while (not app.APP.player.isPlaying() and
while (not app.APP.is_playing and
not app.APP.monitor.abortRequested() and
sub_mgr.stop_sent_to_web and not
(len(tracker) >= 4 and

View file

@ -45,6 +45,9 @@ class plexgdm:
self.discover_message = 'M-SEARCH * HTTP/1.0'
self.client_header = '* HTTP/1.0'
self.client_data = None
self.update_sock = None
self.discover_t = None
self.register_t = None
self._multicast_address = '239.0.0.250'
self.discover_group = (self._multicast_address, 32414)

View file

@ -9,7 +9,7 @@ from logging import getLogger
from threading import Thread
from ..downloadutils import DownloadUtils as DU
from .. import utils, timing
from .. import timing
from .. import app
from .. import variables as v
from .. import json_rpc as js
@ -50,7 +50,7 @@ HEADERS_PMS = {
'Accept': 'text/plain, */*; q=0.01',
'Accept-Language': 'en',
'Accept-Encoding': 'gzip, deflate',
'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.PLATFORM)
'User-Agent': '%s %s (%s)' % (v.ADDON_NAME, v.ADDON_VERSION, v.DEVICE)
}
@ -64,14 +64,13 @@ def params_pms():
# 'audioDecoders=mp3,aac,dts{bitrate:800000&channels:2},'
# 'ac3{bitrate:800000&channels:2}',
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Device': v.PLATFORM,
'X-Plex-Device': v.DEVICE,
'X-Plex-Device-Name': v.DEVICENAME,
# 'X-Plex-Device-Screen-Resolution': '1916x1018,1920x1080',
'X-Plex-Model': 'unknown',
'X-Plex-Model': v.MODEL,
'X-Plex-Platform': v.PLATFORM,
'X-Plex-Platform-Version': 'unknown',
'X-Plex-Platform-Version': v.PLATFORM_VERSION,
'X-Plex-Product': v.ADDON_NAME,
'X-Plex-Provider-Version': v.ADDON_VERSION,
'X-Plex-Version': v.ADDON_VERSION,
'hasMDE': '1',
# 'X-Plex-Session-Identifier': ['vinuvirm6m20iuw9c4cx1dcx'],
@ -89,7 +88,7 @@ def headers_companion_client():
'X-Plex-Client-Identifier': v.PKC_MACHINE_IDENTIFIER,
'X-Plex-Device-Name': v.DEVICENAME,
'X-Plex-Platform': v.PLATFORM,
'X-Plex-Platform-Version': 'unknown',
'X-Plex-Platform-Version': v.PLATFORM_VERSION,
'X-Plex-Product': v.ADDON_NAME,
'X-Plex-Version': v.ADDON_VERSION,
'Accept-Encoding': 'gzip, deflate',

View file

@ -7,9 +7,9 @@ import xbmc
import xbmcgui
from . import utils, clientinfo, timing
from . import initialsetup, artwork
from . import initialsetup
from . import kodimonitor
from . import sync
from . import sync, library_sync
from . import websocket_client
from . import plex_companion
from . import plex_functions as PF, playqueue as PQ
@ -27,9 +27,8 @@ LOG = logging.getLogger("PLEX.service")
###############################################################################
WINDOW_PROPERTIES = (
"plex_dbScan", "pms_token", "plex_token", "pms_server",
"plex_authenticated", "plex_restricteduser", "plex_allows_mediaDeletion",
"plexkodiconnect.command", "plex_result")
"pms_token", "plex_token", "plex_authenticated", "plex_restricteduser",
"plex_allows_mediaDeletion", "plexkodiconnect.command", "plex_result")
# "Start from beginning", "Play from beginning"
STRINGS = (utils.try_encode(utils.lang(12021)),
@ -98,8 +97,7 @@ class Service():
# Load/Reset PKC entirely - important for user/Kodi profile switch
# Clear video nodes properties
from .library_sync import videonodes
videonodes.VideoNodes().clearProperties()
library_sync.VideoNodes().clearProperties()
clientinfo.getDeviceId()
# Init time-offset between Kodi and Plex
timing.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset') or 0.0)
@ -108,12 +106,16 @@ class Service():
self.server_has_been_online = True
self.welcome_msg = True
self.connection_check_counter = 0
self.setup = None
self.alexa = None
self.playqueue = None
# Flags for other threads
self.connection_check_running = False
self.auth_running = False
self._init_done = True
def isCanceled(self):
@staticmethod
def isCanceled():
return xbmc.abortRequested or app.APP.stop_pkc
def on_connection_check(self, result):
@ -126,9 +128,10 @@ class Service():
# Alert the user and suppress future warning
if app.CONN.online:
# PMS was online before
app.CONN.online = False
app.APP.suspend_threads = True
LOG.warn("Plex Media Server went offline")
app.CONN.online = False
app.APP.suspend_threads()
LOG.debug('Threads suspended')
if utils.settings('show_pms_offline') == 'true':
utils.dialog('notification',
utils.lang(33001),
@ -165,32 +168,21 @@ class Service():
if app.ACCOUNT.authenticated:
# Server got offline when we were authenticated.
# Hence resume threads
app.APP.suspend_threads = False
app.APP.resume_threads()
app.CONN.online = True
finally:
self.connection_check_running = False
def log_out(self):
@staticmethod
def log_out():
"""
Ensures that lib sync threads are suspended; signs out user
"""
LOG.info('Log-out requested')
app.APP.suspend_threads = True
i = 0
while app.SYNC.db_scan:
i += 1
app.APP.monitor.waitForAbort(0.1)
if i > 150:
LOG.error('Could not stop library sync, aborting log-out')
# Failed to reset PMS and plex.tv connects. Try to restart Kodi
utils.messageDialog(utils.lang(29999), utils.lang(39208))
# Resuming threads, just in case
app.APP.suspend_threads = False
return False
LOG.info('Successfully stopped library sync')
app.APP.suspend_threads()
LOG.info('Successfully suspended threads')
app.ACCOUNT.log_out()
LOG.info('User has been logged out')
return True
def choose_pms_server(self, manual=False):
LOG.info("Choosing PMS server requested, starting")
@ -202,15 +194,16 @@ class Service():
if not server:
LOG.info('We did not connect to a new PMS, aborting')
return False
LOG.info("User chose server %s", server['name'])
if server['machineIdentifier'] == app.CONN.machine_identifier:
LOG.info("User chose server %s with url %s",
server['name'], server['baseURL'])
if (server['machineIdentifier'] == app.CONN.machine_identifier and
server['baseURL'] == app.CONN.server):
LOG.info('User chose old PMS to connect to')
return False
# Save changes to to file
self.setup.save_pms_settings(server['baseURL'], server['token'])
self.setup.write_pms_to_settings(server)
if not self.log_out():
return False
self.log_out()
# Wipe Kodi and Plex database as well as playlists and video nodes
utils.wipe_database()
app.CONN.load()
@ -220,20 +213,23 @@ class Service():
self.welcome_msg = False
# Force a full sync
app.SYNC.run_lib_scan = 'full'
# Enable the main loop to continue
app.APP.suspend = False
LOG.info("Choosing new PMS complete")
return True
def switch_plex_user(self):
if not self.log_out():
return False
self.log_out()
# First remove playlists of old user
utils.delete_playlists()
# Remove video nodes
utils.delete_nodes()
app.ACCOUNT.set_unauthenticated()
# Force full sync after login
utils.settings('lastfullsync', value='0')
library_sync.force_full_sync()
app.SYNC.run_lib_scan = 'full'
# Enable the main loop to display user selection dialog
app.APP.suspend = False
return True
def toggle_plex_tv(self):
@ -246,6 +242,8 @@ class Service():
if self.setup.plex_tv_sign_in():
self.setup.write_credentials_to_settings()
app.ACCOUNT.load()
# Enable the main loop to continue
app.APP.suspend = False
def authenticate(self):
"""
@ -265,22 +263,19 @@ class Service():
icon='{plex}',
time=2000,
sound=False)
app.APP.suspend_threads = False
app.APP.resume_threads()
self.auth_running = False
def enter_new_pms_address(self):
server = self.setup.enter_new_pms_address()
if not server:
return
if not self.log_out():
return False
# Save changes to to file
self.log_out()
# Save changes to to file
self.setup.save_pms_settings(server['baseURL'], server['token'])
self.setup.write_pms_to_settings(server)
if not v.KODIVERSION >= 18:
utils.settings('sslverify', value='false')
if not self.log_out():
return False
# Wipe Kodi and Plex database as well as playlists and video nodes
utils.wipe_database()
app.CONN.load()
@ -290,9 +285,38 @@ class Service():
self.welcome_msg = False
# Force a full sync
app.SYNC.run_lib_scan = 'full'
LOG.info("Choosing new PMS complete")
# Enable the main loop to continue
app.APP.suspend = False
LOG.info("Entering PMS address complete")
return True
def choose_plex_libraries(self):
if not app.CONN.online:
LOG.error('PMS not online to choose libraries')
# "{0} offline"
utils.dialog('notification',
utils.lang(29999),
utils.lang(39213).format(app.CONN.server_name or ''),
icon='{plex}')
return
if not app.ACCOUNT.authenticated:
LOG.error('Not yet authenticated for PMS to choose libraries')
# "Unauthorized for PMS"
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
return
app.APP.suspend_threads()
from .library_sync import sections
try:
# Get newest sections from the PMS
if not sections.sync_from_pms(self):
return
if not sections.choose_libraries():
return
# Force a full sync
app.SYNC.run_lib_scan = 'full'
finally:
app.APP.resume_threads()
def _do_auth(self):
LOG.info('Authenticating user')
if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login: # Found a user in the settings, try to authenticate
@ -323,6 +347,8 @@ class Service():
if not user:
LOG.info('No user received')
app.APP.suspend = True
app.APP.suspend_threads()
LOG.debug('Threads suspended')
return False
username = user.title
user_id = user.id
@ -355,7 +381,10 @@ class Service():
app.ACCOUNT.load()
continue
else:
LOG.debug('Suspending threads')
app.APP.suspend = True
app.APP.suspend_threads()
LOG.debug('Threads suspended')
return False
elif res >= 400:
LOG.error('Answer from PMS is not as expected')
@ -378,13 +407,6 @@ class Service():
app.init()
app.APP.monitor = kodimonitor.KodiMonitor()
app.APP.player = xbmc.Player()
artwork.IMAGE_CACHING_SUSPENDS = [
app.APP.suspend_threads,
app.SYNC.stop_sync,
app.SYNC.db_scan
]
if not utils.settings('imageSyncDuringPlayback') == 'true':
artwork.IMAGE_CACHING_SUSPENDS.append(app.SYNC.suspend_sync)
# Initialize the PKC playqueues
PQ.init_playqueues()
@ -444,6 +466,8 @@ class Service():
app.SYNC.run_lib_scan = 'fanart'
elif plex_command == 'textures-scan':
app.SYNC.run_lib_scan = 'textures'
elif plex_command == 'select-libraries':
self.choose_plex_libraries()
elif plex_command == 'RESET-PKC':
utils.reset()
if task:
@ -505,7 +529,10 @@ class Service():
# EXITING PKC
# Tell all threads to terminate (e.g. several lib sync threads)
LOG.debug('Aborting all threads')
app.APP.stop_pkc = True
# Will block until threads have quit
app.APP.stop_threads()
utils.window('plex_service_started', clear=True)
LOG.info("======== STOP %s ========", v.ADDON_NAME)

View file

@ -15,19 +15,6 @@ if library_sync.PLAYLIST_SYNC_ENABLED:
LOG = getLogger('PLEX.sync')
def set_library_scan_toggle(boolean=True):
"""
Make sure to hit this function before starting large scans
"""
if not boolean:
# Deactivate
app.SYNC.db_scan = False
utils.window('plex_dbScan', clear=True)
else:
app.SYNC.db_scan = True
utils.window('plex_dbScan', value="true")
class Sync(backgroundthread.KillableThread):
"""
The one and only library sync thread. Spawn only 1!
@ -35,24 +22,18 @@ class Sync(backgroundthread.KillableThread):
def __init__(self):
self.sync_successful = False
self.last_full_sync = 0
self.fanart = None
# Show sync dialog even if user deactivated?
self.force_dialog = False
self.fanart_thread = None
self.image_cache_thread = None
# Lock used to wait on a full sync, e.g. on initial sync
# self.lock = backgroundthread.threading.Lock()
super(Sync, self).__init__()
def isSuspended(self):
return self._suspended or app.APP.suspend_threads
def triage_lib_scans(self):
"""
Decides what to do if app.SYNC.run_lib_scan has been set. E.g. manually
triggered full or repair syncs
"""
if app.SYNC.run_lib_scan in ("full", "repair"):
set_library_scan_toggle()
LOG.info('Full library scan requested, starting')
self.start_library_sync(show_dialog=True,
repair=app.SYNC.run_lib_scan == 'repair',
@ -89,23 +70,16 @@ class Sync(backgroundthread.KillableThread):
"""
self.sync_successful = successful
self.last_full_sync = timing.unix_timestamp()
set_library_scan_toggle(boolean=False)
if not successful:
LOG.warn('Could not finish scheduled full sync')
# try:
# self.lock.release()
# except backgroundthread.threading.ThreadError:
# pass
app.APP.resume_fanart_thread()
app.APP.resume_caching_thread()
def start_library_sync(self, show_dialog=None, repair=False, block=False):
set_library_scan_toggle(boolean=True)
app.APP.suspend_fanart_thread(block=True)
app.APP.suspend_caching_thread(block=True)
show_dialog = show_dialog if show_dialog is not None else app.SYNC.sync_dialog
library_sync.start(show_dialog, repair, self.on_library_scan_finished)
# if block:
# self.lock.acquire()
# Will block until scan is finished
# self.lock.acquire()
# self.lock.release()
def start_fanart_download(self, refresh):
if not utils.settings('FanartTV') == 'true':
@ -114,11 +88,11 @@ class Sync(backgroundthread.KillableThread):
if not app.SYNC.artwork:
LOG.info('Not synching Plex PMS artwork, not getting artwork')
return False
elif self.fanart is None or not self.fanart.is_alive():
elif self.fanart_thread is None or not self.fanart_thread.is_alive():
LOG.info('Start downloading additional fanart with refresh %s',
refresh)
self.fanart = library_sync.FanartThread(self.on_fanart_download_finished, refresh)
self.fanart.start()
self.fanart_thread = library_sync.FanartThread(self.on_fanart_download_finished, refresh)
self.fanart_thread.start()
return True
else:
LOG.info('Still downloading fanart')
@ -144,16 +118,21 @@ class Sync(backgroundthread.KillableThread):
self.image_cache_thread.start()
def run(self):
LOG.info("---===### Starting Sync Thread ###===---")
app.APP.register_thread(self)
try:
self._run_internal()
except Exception:
app.SYNC.db_scan = False
utils.window('plex_dbScan', clear=True)
utils.ERROR(txt='sync.py crashed', notify=True)
raise
finally:
try:
app.APP.deregister_thread(self)
except ValueError:
pass
LOG.info("###===--- Sync Thread Stopped ---===###")
def _run_internal(self):
LOG.info("---===### Starting Sync Thread ###===---")
install_sync_done = utils.settings('SyncInstallRunDone') == 'true'
playlist_monitor = None
initial_sync_done = False
@ -170,6 +149,8 @@ class Sync(backgroundthread.KillableThread):
v.MIN_DB_VERSION):
LOG.warn("Db version out of date: %s minimum version "
"required: %s", current_version, v.MIN_DB_VERSION)
# In order to not wait for this thread to suspend
app.APP.deregister_thread(self)
# DB out of date. Proceed to recreate?
if not utils.yesno_dialog(utils.lang(29999),
utils.lang(39401)):
@ -186,17 +167,10 @@ class Sync(backgroundthread.KillableThread):
while not self.isCanceled():
# In the event the server goes offline
while self.isSuspended():
if self.isCanceled():
# Abort was requested while waiting. We should exit
LOG.info("###===--- Sync Thread Stopped ---===###")
return
app.APP.monitor.waitForAbort(1)
if self.wait_while_suspended():
return
if not install_sync_done:
# Very FIRST sync ever upon installation or reset of Kodi DB
set_library_scan_toggle()
self.force_dialog = True
# Initialize time offset Kodi - PMS
library_sync.sync_pms_time()
last_time_sync = timing.unix_timestamp()
@ -217,7 +191,6 @@ class Sync(backgroundthread.KillableThread):
else:
LOG.error('Initial start-up full sync unsuccessful')
app.APP.monitor.waitForAbort(1)
self.force_dialog = False
xbmc.executebuiltin('InhibitIdleShutdown(false)')
elif not initial_sync_done:
@ -237,13 +210,10 @@ class Sync(backgroundthread.KillableThread):
app.APP.monitor.waitForAbort(1)
# Currently no db scan, so we could start a new scan
elif app.SYNC.db_scan is False:
else:
# Full scan was requested from somewhere else
if app.SYNC.run_lib_scan is not None:
# Force-show dialogs since they are user-initiated
self.force_dialog = True
self.triage_lib_scans()
self.force_dialog = False
# Reset the flag
app.SYNC.run_lib_scan = None
continue
@ -251,7 +221,7 @@ class Sync(backgroundthread.KillableThread):
# Standard syncs - don't force-show dialogs
now = timing.unix_timestamp()
if (now - self.last_full_sync > app.SYNC.full_sync_intervall and
not app.SYNC.suspend_sync):
not app.APP.is_playing_video):
LOG.info('Doing scheduled full library scan')
self.start_library_sync()
elif now - last_time_sync > one_day_in_seconds:
@ -287,4 +257,3 @@ class Sync(backgroundthread.KillableThread):
DU().stopSession()
except AttributeError:
pass
LOG.info("###===--- Sync Thread Stopped ---===###")

View file

@ -223,7 +223,7 @@ def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False):
LOG.error('Error encountered: %s - %s', txt, short)
if cancel_sync:
from . import app
app.SYNC.stop_sync = True
app.APP.stop_threads(block=False)
if hide_tb:
return short
@ -340,14 +340,14 @@ def valid_filename(text):
text = re.sub(r'(?! )\s', '', text)
# ASCII characters 0 to 31 (non-printable, just in case)
text = re.sub(u'[\x00-\x1f]', '', text)
if v.PLATFORM == 'Windows':
if v.DEVICE == 'Windows':
# Whitespace at the end of the filename is illegal
text = text.strip()
# Dot at the end of a filename is illegal
text = re.sub(r'\.+$', '', text)
# Illegal Windows characters
text = re.sub(r'[/\\:*?"<>|\^]', '', text)
elif v.PLATFORM == 'MacOSX':
elif v.DEVICE == 'MacOSX':
# Colon is illegal
text = re.sub(r':', '', text)
# Files cannot begin with a dot
@ -466,7 +466,7 @@ def wipe_database():
kodi_db.reset_cached_images()
# reset the install run flag
settings('SyncInstallRunDone', value="false")
settings('lastfullsync', value="0")
settings('sections_asked_for_machine_identifier', value='')
init_dbs()
LOG.info('Wiping done')
if settings('kodi_db_has_been_wiped_clean') != 'true':
@ -502,19 +502,7 @@ def reset(ask_user=True):
return
from . import app
# first stop any db sync
app.APP.suspend_threads = True
count = 15
while app.SYNC.db_scan:
LOG.info("Sync is running, will retry: %s...", count)
count -= 1
if count == 0:
LOG.error('Could not stop PKC syncing process to reset the DB')
# Could not stop the database from running. Please try again later.
messageDialog(lang(29999), lang(39601))
app.APP.suspend_threads = False
return
xbmc.sleep(1000)
app.APP.suspend_threads()
# Reset all PlexKodiConnect Addon settings? (this is usually NOT
# recommended and unnecessary!)
if ask_user and yesno_dialog(lang(29999), lang(39603)):

View file

@ -4,6 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
import os
import sys
import re
import platform
import xbmc
from xbmcaddon import Addon
@ -51,23 +52,28 @@ KODILONGVERSION = xbmc.getInfoLabel('System.BuildVersion')
KODI_PROFILE = try_decode(xbmc.translatePath("special://profile"))
if xbmc.getCondVisibility('system.platform.osx'):
PLATFORM = "MacOSX"
DEVICE = "MacOSX"
elif xbmc.getCondVisibility("system.platform.uwp"):
PLATFORM = "Microsoft UWP"
DEVICE = "Microsoft UWP"
elif xbmc.getCondVisibility('system.platform.atv2'):
PLATFORM = "AppleTV2"
DEVICE = "AppleTV2"
elif xbmc.getCondVisibility('system.platform.ios'):
PLATFORM = "iOS"
DEVICE = "iOS"
elif xbmc.getCondVisibility('system.platform.windows'):
PLATFORM = "Windows"
DEVICE = "Windows"
elif xbmc.getCondVisibility('system.platform.raspberrypi'):
PLATFORM = "RaspberryPi"
DEVICE = "RaspberryPi"
elif xbmc.getCondVisibility('system.platform.linux'):
PLATFORM = "Linux"
DEVICE = "Linux"
elif xbmc.getCondVisibility('system.platform.android'):
PLATFORM = "Android"
DEVICE = "Android"
else:
PLATFORM = "Unknown"
DEVICE = "Unknown"
MODEL = platform.release() or 'Unknown'
# Plex' own platform for e.g. Plex Media Player
PLATFORM = 'Konvergo'
PLATFORM_VERSION = '2.26.0.947-1e21fa2b'
DEVICENAME = try_decode(_ADDON.getSetting('deviceName'))
if not DEVICENAME:
@ -91,7 +97,7 @@ COMPANION_PORT = int(_ADDON.getSetting('companionPort'))
PKC_MACHINE_IDENTIFIER = None
# Minimal PKC version needed for the Kodi database - otherwise need to recreate
MIN_DB_VERSION = '2.6.1'
MIN_DB_VERSION = '2.6.8'
# Supported databases
SUPPORTED_VIDEO_DB = {
@ -637,7 +643,7 @@ def database_paths():
# Encoding to be used for our m3u playlist files
# m3u files do not have encoding specified by definition, unfortunately.
if PLATFORM == 'Windows':
if DEVICE == 'Windows':
M3U_ENCODING = 'mbcs'
else:
M3U_ENCODING = sys.getfilesystemencoding()

View file

@ -105,6 +105,19 @@ class WebSocketTimeoutException(WebSocketException):
pass
class WebsocketRedirect(WebSocketException):
"""
WebsocketRedirect will be raised if a status code 301 is returned
The Exception will be instantiated with a dict containing all response
headers; which should contain the redirect address under the key 'location'
Access the headers via the attribute headers
"""
def __init__(self, headers):
self.headers = headers
super(WebsocketRedirect, self).__init__()
DEFAULT_TIMEOUT = None
TRACE_ENABLED = False
@ -162,10 +175,10 @@ def _parse_url(url):
port = parsed.port
is_secure = False
if scheme == "ws":
if scheme == "ws" or scheme == 'http':
if not port:
port = 80
elif scheme == "wss":
elif scheme == "wss" or scheme == 'https':
is_secure = True
if not port:
port = 443
@ -500,6 +513,9 @@ class WebSocket(object):
LOG.debug("-----------------------")
status, resp_headers = self._read_headers()
if status == 301:
# Redirect
raise WebsocketRedirect(resp_headers)
if status != 101:
self.close()
raise WebSocketException("Handshake Status %d" % status)

View file

@ -18,6 +18,7 @@ class WebSocket(backgroundthread.KillableThread):
def __init__(self):
self.ws = None
self.redirect_uri = None
super(WebSocket, self).__init__()
def process(self, opcode, message):
@ -46,20 +47,20 @@ class WebSocket(backgroundthread.KillableThread):
def run(self):
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
app.APP.register_thread(self)
counter = 0
while not self.isCanceled():
# In the event the server goes offline
while self.isSuspended():
if self.isSuspended():
# Set in service.py
if self.ws is not None:
self.ws.close()
self.ws = None
if self.isCanceled():
if self.wait_while_suspended():
# Abort was requested while waiting. We should exit
LOG.info("##===---- %s Stopped ----===##",
self.__class__.__name__)
return
app.APP.monitor.waitForAbort(1)
try:
self.process(*self.receive(self.ws))
except websocket.WebSocketTimeoutException:
@ -91,6 +92,16 @@ class WebSocket(backgroundthread.KillableThread):
self.__class__.__name__)
self.ws = None
app.APP.monitor.waitForAbort(1)
except websocket.WebsocketRedirect as e:
LOG.info('301 redirect detected')
self.redirect_uri = e.headers.get('location', e.headers.get('Location'))
if self.redirect_uri:
self.redirect_uri.decode('utf-8')
counter += 1
if counter >= 10:
LOG.info('%s: Repeated WebsocketRedirect detected. Stopping now',
self.__class__.__name__)
break
except websocket.WebSocketException as e:
LOG.info('%s: WebSocketException: %s',
self.__class__.__name__, e)
@ -125,6 +136,7 @@ class WebSocket(backgroundthread.KillableThread):
# Close websocket connection on shutdown
if self.ws is not None:
self.ws.close()
app.APP.deregister_thread(self)
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
@ -136,23 +148,25 @@ class PMS_Websocket(WebSocket):
"""
Returns True if the thread is suspended
"""
return (self._suspended or
app.APP.suspend_threads or
app.SYNC.background_sync_disabled)
return self._suspended or app.SYNC.background_sync_disabled
def getUri(self):
server = app.CONN.server
# Get the appropriate prefix for the websocket
if server.startswith('https'):
server = "wss%s" % server[5:]
if self.redirect_uri:
uri = self.redirect_uri
self.redirect_uri = None
else:
server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server
# Need to use plex.tv token, if any. NOT user token
if app.ACCOUNT.plex_token:
uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token
server = app.CONN.server
# Get the appropriate prefix for the websocket
if server.startswith('https'):
server = "wss%s" % server[5:]
else:
server = "ws%s" % server[4:]
uri = "%s/:/websockets/notifications" % server
# Need to use plex.tv token, if any. NOT user token
if app.ACCOUNT.plex_token:
uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token
sslopt = {}
if utils.settings('sslverify') == "false":
if v.KODIVERSION == 17 and utils.settings('sslverify') == "false":
sslopt["cert_reqs"] = CERT_NONE
LOG.debug("%s: Uri: %s, sslopt: %s",
self.__class__.__name__, uri, sslopt)
@ -186,11 +200,6 @@ class PMS_Websocket(WebSocket):
# Drop everything we're not interested in
if typus not in ('playing', 'timeline', 'activity'):
return
elif typus == 'activity' and app.SYNC.db_scan is True:
# Only add to processing if PKC is NOT doing a lib scan (and thus
# possibly causing these reprocessing messages en mass)
LOG.debug('%s: Dropping message as PKC is currently synching',
self.__class__.__name__)
else:
# Put PMS message on queue and let libsync take care of it
app.APP.websocket_queue.put(message)
@ -209,10 +218,14 @@ class Alexa_Websocket(WebSocket):
app.ACCOUNT.restricted_user)
def getUri(self):
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (app.ACCOUNT.plex_user_id,
v.PKC_MACHINE_IDENTIFIER,
app.ACCOUNT.plex_token))
if self.redirect_uri:
uri = self.redirect_uri
self.redirect_uri = None
else:
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
% (app.ACCOUNT.plex_user_id,
v.PKC_MACHINE_IDENTIFIER,
app.ACCOUNT.plex_token))
sslopt = {}
LOG.debug("%s: Uri: %s, sslopt: %s",
self.__class__.__name__, uri, sslopt)

View file

@ -206,7 +206,7 @@ class ControlledWindow(ControlledBase, BaseWindow):
if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
self.doClose()
return
except:
except Exception:
traceback.print_exc()
BaseWindow.onAction(self, action)
@ -218,7 +218,7 @@ class ControlledDialog(ControlledBase, BaseDialog):
if action in (xbmcgui.ACTION_PREVIOUS_MENU, xbmcgui.ACTION_NAV_BACK):
self.doClose()
return
except:
except Exception:
traceback.print_exc()
BaseDialog.onAction(self, action)
@ -228,7 +228,8 @@ DUMMY_LIST_ITEM = xbmcgui.ListItem()
class ManagedListItem(object):
def __init__(self, label='', label2='', iconImage='', thumbnailImage='', path='', data_source=None, properties=None):
def __init__(self, label='', label2='', iconImage='', thumbnailImage='',
path='', data_source=None, properties=None):
self._listItem = xbmcgui.ListItem(label, label2, iconImage, thumbnailImage, path)
self.dataSource = data_source
self.properties = {}
@ -608,13 +609,15 @@ class ManagedControlList(object):
def getViewPosition(self):
try:
return int(xbmc.getInfoLabel('Container({0}).Position'.format(self.controlID)))
except:
except Exception:
return 0
def getViewRange(self):
viewPosition = self.getViewPosition()
selected = self.getSelectedPosition()
return range(max(selected - viewPosition, 0), min(selected + (self._maxViewIndex - viewPosition) + 1, self.size() - 1))
return range(max(selected - viewPosition, 0),
min(selected + (self._maxViewIndex - viewPosition) + 1,
self.size() - 1))
def positionIsValid(self, pos):
return 0 <= pos < self.size()
@ -809,7 +812,7 @@ class SafeControlEdit(object):
if self.processOffControlAction(action.getButtonCode()):
self._win.setFocusId(self.controlID)
return
except:
except Exception:
traceback.print_exc()
self._winOnAction(action)

View file

@ -49,9 +49,11 @@
<setting id="dbCreatedWithVersion" type="text" default="" visible="false"/>
<setting id="plexid" type="text" default="" visible="false"/>
<setting id="userid" type="text" default="" visible="false"/>
<setting id="sections_asked_for_machine_identifier" type="text" default="" visible="false"/>
</category>
<category label="30506"><!-- Sync Options -->
<setting label="[COLOR yellow]$ADDON[plugin.video.plexkodiconnect 30524][/COLOR]" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=select-libraries)" option="close" help="Choose which Plex library you want to sync"/><!-- Select Plex libraries to sync -->
<setting type="lsep" label="30537" /><!-- Restart if you make changes -->
<setting type="sep" />
<setting id="fullSyncInterval" type="number" label="39053" default="60" option="int" />
@ -84,7 +86,6 @@
<setting id="themoviedbAPIKey" type="text" default="19c90103adb9e98f2172c6a6a3d85dc4" visible="false"/>
<setting id="FanArtTVAPIKey" type="text" default="639191cb0774661597f28a47e7e2bad5" visible="false"/>
<setting id="syncEmptyShows" type="bool" label="30508" default="false" visible="false"/>
<setting id="lastfullsync" type="number" label="Time stamp when last successful full sync was conducted" default="0" visible="false" />
<setting id="kodi_db_has_been_wiped_clean" type="bool" label="PKC needs to completely clean the Kodi DB at least once, then reboot, to avoid Kodi error messages, e.g. OperationalError" default="false" visible="false" />
</category>