commit
0e64f50b95
48 changed files with 1106 additions and 658 deletions
|
@ -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)
|
||||
|
|
32
addon.xml
32
addon.xml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)')):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ##===----")
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 ---===###")
|
||||
|
|
|
@ -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)):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue