Rather big change of PKC's plumbing
This commit is contained in:
parent
b2615c19bd
commit
a045063769
53 changed files with 1246 additions and 1663 deletions
13
default.py
13
default.py
|
@ -94,9 +94,6 @@ class Main():
|
||||||
elif mode == 'togglePlexTV':
|
elif mode == 'togglePlexTV':
|
||||||
entrypoint.toggle_plex_tv_sign_in()
|
entrypoint.toggle_plex_tv_sign_in()
|
||||||
|
|
||||||
elif mode == 'resetauth':
|
|
||||||
entrypoint.reset_authorization()
|
|
||||||
|
|
||||||
elif mode == 'passwords':
|
elif mode == 'passwords':
|
||||||
utils.passwords_xml()
|
utils.passwords_xml()
|
||||||
|
|
||||||
|
@ -111,14 +108,14 @@ class Main():
|
||||||
else:
|
else:
|
||||||
if mode == 'repair':
|
if mode == 'repair':
|
||||||
log.info('Requesting repair lib sync')
|
log.info('Requesting repair lib sync')
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'repair')
|
utils.plex_command('repair-scan')
|
||||||
elif mode == 'manualsync':
|
elif mode == 'manualsync':
|
||||||
log.info('Requesting full library scan')
|
log.info('Requesting full library scan')
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'full')
|
utils.plex_command('full-scan')
|
||||||
|
|
||||||
elif mode == 'texturecache':
|
elif mode == 'texturecache':
|
||||||
log.info('Requesting texture caching of all textures')
|
log.info('Requesting texture caching of all textures')
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'textures')
|
utils.plex_command('textures-scan')
|
||||||
|
|
||||||
elif mode == 'chooseServer':
|
elif mode == 'chooseServer':
|
||||||
entrypoint.choose_pms_server()
|
entrypoint.choose_pms_server()
|
||||||
|
@ -128,7 +125,7 @@ class Main():
|
||||||
|
|
||||||
elif mode == 'fanart':
|
elif mode == 'fanart':
|
||||||
log.info('User requested fanarttv refresh')
|
log.info('User requested fanarttv refresh')
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'fanart')
|
utils.plex_command('fanart-scan')
|
||||||
|
|
||||||
elif '/extrafanart' in path:
|
elif '/extrafanart' in path:
|
||||||
plexpath = arguments[1:]
|
plexpath = arguments[1:]
|
||||||
|
@ -158,7 +155,7 @@ class Main():
|
||||||
"""
|
"""
|
||||||
request = '%s&handle=%s' % (argv[2], HANDLE)
|
request = '%s&handle=%s' % (argv[2], HANDLE)
|
||||||
# Put the request into the 'queue'
|
# Put the request into the 'queue'
|
||||||
utils.plex_command('PLAY', request)
|
utils.plex_command('PLAY-%s' % request)
|
||||||
if HANDLE == -1:
|
if HANDLE == -1:
|
||||||
# Handle -1 received, not waiting for main thread
|
# Handle -1 received, not waiting for main thread
|
||||||
return
|
return
|
||||||
|
|
27
resources/lib/app/__init__.py
Normal file
27
resources/lib/app/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Used to save PKC's application state and share between modules. Be careful
|
||||||
|
if you invoke another PKC Python instance (!!) when e.g. PKC.movies is called
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from .account import Account
|
||||||
|
from .application import App
|
||||||
|
from .connection import Connection
|
||||||
|
from .libsync import Sync
|
||||||
|
from .playstate import PlayState
|
||||||
|
|
||||||
|
ACCOUNT = None
|
||||||
|
APP = None
|
||||||
|
CONN = None
|
||||||
|
SYNC = None
|
||||||
|
PLAYSTATE = None
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
global ACCOUNT, APP, CONN, SYNC, PLAYSTATE
|
||||||
|
ACCOUNT = Account()
|
||||||
|
APP = App()
|
||||||
|
CONN = Connection()
|
||||||
|
SYNC = Sync()
|
||||||
|
PLAYSTATE = PlayState()
|
86
resources/lib/app/account.py
Normal file
86
resources/lib/app/account.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.account')
|
||||||
|
|
||||||
|
|
||||||
|
class Account(object):
|
||||||
|
def __init__(self):
|
||||||
|
# Along with window('plex_authenticated')
|
||||||
|
self.authenticated = False
|
||||||
|
self._session = None
|
||||||
|
utils.window('plex_authenticated', clear=True)
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def set_authenticated(self):
|
||||||
|
self.authenticated = True
|
||||||
|
utils.window('plex_authenticated', value='true')
|
||||||
|
# Start download session
|
||||||
|
from .. import downloadutils
|
||||||
|
self._session = downloadutils.DownloadUtils()
|
||||||
|
self._session.startSession(reset=True)
|
||||||
|
|
||||||
|
def set_unauthenticated(self):
|
||||||
|
self.authenticated = False
|
||||||
|
utils.window('plex_authenticated', clear=True)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
LOG.debug('Loading account settings')
|
||||||
|
# plex.tv username
|
||||||
|
self.plex_username = utils.settings('username') or None
|
||||||
|
# Plex ID of that user (e.g. for plex.tv) as a STRING
|
||||||
|
self.plex_user_id = utils.settings('userid') or None
|
||||||
|
# Token for that user for plex.tv
|
||||||
|
self.plex_token = utils.settings('plexToken') or None
|
||||||
|
# Plex token for the active PMS for the active user
|
||||||
|
# (might be diffent to plex_token)
|
||||||
|
self.pms_token = utils.settings('accessToken') or None
|
||||||
|
self.avatar = utils.settings('plexAvatar') or None
|
||||||
|
self.myplexlogin = utils.settings('myplexlogin') == 'true'
|
||||||
|
|
||||||
|
# Plex home user? Then "False"
|
||||||
|
self.restricted_user = True \
|
||||||
|
if utils.settings('plex_restricteduser') == 'true' else False
|
||||||
|
# Force user to enter Pin if set?
|
||||||
|
self.force_login = utils.settings('enforceUserLogin') == 'true'
|
||||||
|
|
||||||
|
# Also load these settings to Kodi window variables - they'll be
|
||||||
|
# available for other PKC Python instances
|
||||||
|
utils.window('plex_restricteduser',
|
||||||
|
value='true' if self.restricted_user else 'false')
|
||||||
|
utils.window('plex_token', value=self.plex_token or '')
|
||||||
|
utils.window('pms_token', value=self.pms_token or '')
|
||||||
|
utils.window('plexAvatar', value=self.avatar or '')
|
||||||
|
LOG.debug('Loaded user %s, %s with plex token %s... and pms token %s...',
|
||||||
|
self.plex_username, self.plex_user_id,
|
||||||
|
self.plex_token[:5] if self.plex_token else None,
|
||||||
|
self.pms_token[:5] if self.pms_token else None)
|
||||||
|
LOG.debug('User is restricted Home user: %s', self.restricted_user)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
LOG.debug('Clearing account settings')
|
||||||
|
self.plex_username = None
|
||||||
|
self.plex_user_id = None
|
||||||
|
self.plex_token = None
|
||||||
|
self.pms_token = None
|
||||||
|
self.avatar = None
|
||||||
|
self.restricted_user = None
|
||||||
|
self.authenticated = False
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
utils.settings('username', value='')
|
||||||
|
utils.settings('userid', value='')
|
||||||
|
utils.settings('plex_restricteduser', value='')
|
||||||
|
utils.settings('plexToken', value='')
|
||||||
|
utils.settings('accessToken', value='')
|
||||||
|
utils.settings('plexAvatar', value='')
|
||||||
|
|
||||||
|
utils.window('plex_restricteduser', clear=True)
|
||||||
|
utils.window('plex_token', clear=True)
|
||||||
|
utils.window('pms_token', clear=True)
|
||||||
|
utils.window('plexAvatar', clear=True)
|
||||||
|
utils.window('plex_authenticated', clear=True)
|
43
resources/lib/app/application.py
Normal file
43
resources/lib/app/application.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
import Queue
|
||||||
|
from threading import Lock, RLock
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
|
|
||||||
|
|
||||||
|
class App(object):
|
||||||
|
"""
|
||||||
|
This class is used to store variables across PKC modules
|
||||||
|
"""
|
||||||
|
def __init__(self, only_reload_settings=False):
|
||||||
|
self.load_settings()
|
||||||
|
if only_reload_settings:
|
||||||
|
return
|
||||||
|
# Quit PKC?
|
||||||
|
self.stop_pkc = 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
|
||||||
|
self.lock_playqueues = RLock()
|
||||||
|
# Necessary to temporarily hold back librarysync/websocket listener when doing
|
||||||
|
# a full sync
|
||||||
|
self.lock_playlists = Lock()
|
||||||
|
|
||||||
|
# Plex Companion Queue()
|
||||||
|
self.companion_queue = Queue.Queue(maxsize=100)
|
||||||
|
# Command Pipeline Queue()
|
||||||
|
self.command_pipeline_queue = Queue.Queue()
|
||||||
|
# Websocket_client queue to communicate with librarysync
|
||||||
|
self.websocket_queue = Queue.Queue()
|
||||||
|
|
||||||
|
def load_settings(self):
|
||||||
|
# Number of items to fetch and display in widgets
|
||||||
|
self.fetch_pms_item_number = int(utils.settings('fetch_pms_item_number'))
|
||||||
|
# Hack to force Kodi widget for "in progress" to show up if it was empty
|
||||||
|
# before
|
||||||
|
self.force_reload_skin = utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
|
||||||
|
# Stemming from the PKC settings.xml
|
||||||
|
self.kodi_plex_time_offset = float(utils.settings('kodiplextimeoffset'))
|
75
resources/lib/app/connection.py
Normal file
75
resources/lib/app/connection.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from .. import utils, json_rpc as js
|
||||||
|
|
||||||
|
LOG = getLogger('PLEX.connection')
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(object):
|
||||||
|
def __init__(self, only_reload_settings=False):
|
||||||
|
self.load_webserver()
|
||||||
|
self.load()
|
||||||
|
if only_reload_settings:
|
||||||
|
return
|
||||||
|
# TODO: Delete
|
||||||
|
self.pms_server = None
|
||||||
|
# Plex Media Server Status - along with window('plex_serverStatus')
|
||||||
|
# Values:
|
||||||
|
# 'Stop': set if e.g.
|
||||||
|
# '401': Token has been revoked - PKC yet to delete tokens
|
||||||
|
# 'Auth':
|
||||||
|
self.pms_status = False
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def load_webserver(self):
|
||||||
|
"""
|
||||||
|
PKC needs Kodi webserver to work correctly
|
||||||
|
"""
|
||||||
|
LOG.debug('Loading Kodi webserver details')
|
||||||
|
# Kodi webserver details
|
||||||
|
if js.get_setting('services.webserver') in (None, False):
|
||||||
|
# Enable the webserver, it is disabled
|
||||||
|
js.set_setting('services.webserver', True)
|
||||||
|
self.webserver_host = 'localhost'
|
||||||
|
self.webserver_port = js.get_setting('services.webserverport')
|
||||||
|
self.webserver_username = js.get_setting('services.webserverusername')
|
||||||
|
self.webserver_password = js.get_setting('services.webserverpassword')
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
LOG.debug('Loading connection settings')
|
||||||
|
# Shall we verify SSL certificates? "None" will leave SSL enabled
|
||||||
|
self.verify_ssl_cert = None if utils.settings('sslverify') == 'true' \
|
||||||
|
else False
|
||||||
|
# Do we have an ssl certificate for PKC we need to use?
|
||||||
|
self.ssl_cert_path = utils.settings('sslcert') \
|
||||||
|
if utils.settings('sslcert') != 'None' else None
|
||||||
|
|
||||||
|
self.machine_identifier = utils.settings('plex_machineIdentifier') or None
|
||||||
|
self.server_name = utils.settings('plex_servername') or None
|
||||||
|
self.https = utils.settings('https') == 'true'
|
||||||
|
self.host = utils.settings('ipaddress') or None
|
||||||
|
self.port = int(utils.settings('port')) if utils.settings('port') else None
|
||||||
|
if not self.host:
|
||||||
|
self.server = None
|
||||||
|
elif self.https:
|
||||||
|
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)
|
||||||
|
LOG.debug('Set server %s (%s) to %s',
|
||||||
|
self.server_name, self.machine_identifier, self.server)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
LOG.debug('Clearing connection settings')
|
||||||
|
self.machine_identifier = None
|
||||||
|
self.server_name = None
|
||||||
|
self.http = None
|
||||||
|
self.host = None
|
||||||
|
self.port = None
|
||||||
|
self.server = None
|
||||||
|
utils.window('pms_server', clear=True)
|
75
resources/lib/app/libsync.py
Normal file
75
resources/lib/app/libsync.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
|
|
||||||
|
|
||||||
|
class Sync(object):
|
||||||
|
def __init__(self, only_reload_settings=False):
|
||||||
|
self.load_settings()
|
||||||
|
if only_reload_settings:
|
||||||
|
return
|
||||||
|
# Do we need to run a special library scan?
|
||||||
|
self.run_lib_scan = None
|
||||||
|
# Usually triggered by another Python instance - will have to be set (by
|
||||||
|
# polling window) through e.g. librarysync thread
|
||||||
|
self.suspend_library_thread = False
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def load_settings(self):
|
||||||
|
# Direct Paths (True) or Addon Paths (False)? Along with
|
||||||
|
# window('useDirectPaths')
|
||||||
|
self.direct_paths = True if utils.settings('useDirectPaths') == '1' \
|
||||||
|
else False
|
||||||
|
# Is synching of Plex music enabled?
|
||||||
|
self.enable_music = utils.settings('enableMusic') == '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'
|
||||||
|
# Mappings for REMAP_PATH:
|
||||||
|
self.remapSMBmovieOrg = utils.settings('remapSMBmovieOrg')
|
||||||
|
self.remapSMBmovieNew = utils.settings('remapSMBmovieNew')
|
||||||
|
self.remapSMBtvOrg = utils.settings('remapSMBtvOrg')
|
||||||
|
self.remapSMBtvNew = utils.settings('remapSMBtvNew')
|
||||||
|
self.remapSMBmusicOrg = utils.settings('remapSMBmusicOrg')
|
||||||
|
self.remapSMBmusicNew = utils.settings('remapSMBmusicNew')
|
||||||
|
self.remapSMBphotoOrg = utils.settings('remapSMBphotoOrg')
|
||||||
|
self.remapSMBphotoNew = utils.settings('remapSMBphotoNew')
|
||||||
|
# Shall we replace custom user ratings with the number of versions available?
|
||||||
|
self.indicate_media_versions = True \
|
||||||
|
if utils.settings('indicate_media_versions') == "true" else False
|
||||||
|
# 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'
|
71
resources/lib/app/playstate.py
Normal file
71
resources/lib/app/playstate.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
class PlayState(object):
|
||||||
|
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
|
||||||
|
template = {
|
||||||
|
'type': None,
|
||||||
|
'time': {
|
||||||
|
'hours': 0,
|
||||||
|
'minutes': 0,
|
||||||
|
'seconds': 0,
|
||||||
|
'milliseconds': 0},
|
||||||
|
'totaltime': {
|
||||||
|
'hours': 0,
|
||||||
|
'minutes': 0,
|
||||||
|
'seconds': 0,
|
||||||
|
'milliseconds': 0},
|
||||||
|
'speed': 0,
|
||||||
|
'shuffled': False,
|
||||||
|
'repeat': 'off',
|
||||||
|
'position': None,
|
||||||
|
'playlistid': None,
|
||||||
|
'currentvideostream': -1,
|
||||||
|
'currentaudiostream': -1,
|
||||||
|
'subtitleenabled': False,
|
||||||
|
'currentsubtitle': -1,
|
||||||
|
'file': None,
|
||||||
|
'kodi_id': None,
|
||||||
|
'kodi_type': None,
|
||||||
|
'plex_id': None,
|
||||||
|
'plex_type': None,
|
||||||
|
'container_key': None,
|
||||||
|
'volume': 100,
|
||||||
|
'muted': False,
|
||||||
|
'playmethod': None,
|
||||||
|
'playcount': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Kodi player states - here, initial values are set
|
||||||
|
self.player_states = {
|
||||||
|
0: {},
|
||||||
|
1: {},
|
||||||
|
2: {}
|
||||||
|
}
|
||||||
|
# The LAST playstate once playback is finished
|
||||||
|
self.old_player_states = {
|
||||||
|
0: {},
|
||||||
|
1: {},
|
||||||
|
2: {}
|
||||||
|
}
|
||||||
|
self.played_info = {}
|
||||||
|
|
||||||
|
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
||||||
|
# beginning?
|
||||||
|
self.resume_playback = False
|
||||||
|
# Was the playback initiated by the user using the Kodi context menu?
|
||||||
|
self.context_menu_play = False
|
||||||
|
# Set by context menu - shall we force-transcode the next playing item?
|
||||||
|
self.force_transcode = False
|
||||||
|
# Which Kodi player is/has been active? (either int 1, 2 or 3)
|
||||||
|
self.active_players = set()
|
||||||
|
|
||||||
|
# Failsafe for throwing an empty video back to Kodi's setResolvedUrl to set
|
||||||
|
# up our own playlist from the very beginning
|
||||||
|
self.pkc_caused_stop = False
|
||||||
|
# Flag if the 0 length PKC video has already failed so we can start resolving
|
||||||
|
# playback (set in player.py)
|
||||||
|
self.pkc_caused_stop_done = True
|
|
@ -7,8 +7,7 @@ import requests
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB
|
from .kodi_db import KodiVideoDB, KodiMusicDB, KodiTextureDB
|
||||||
from . import backgroundthread, utils
|
from . import app, backgroundthread, utils
|
||||||
from . import state
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.artwork')
|
LOG = getLogger('PLEX.artwork')
|
||||||
|
|
||||||
|
@ -19,13 +18,7 @@ requests.packages.urllib3.disable_warnings()
|
||||||
# download is successful
|
# download is successful
|
||||||
TIMEOUT = (35.1, 35.1)
|
TIMEOUT = (35.1, 35.1)
|
||||||
|
|
||||||
IMAGE_CACHING_SUSPENDS = [
|
IMAGE_CACHING_SUSPENDS = []
|
||||||
state.SUSPEND_LIBRARY_THREAD,
|
|
||||||
state.STOP_SYNC,
|
|
||||||
state.DB_SCAN
|
|
||||||
]
|
|
||||||
if not utils.settings('imageSyncDuringPlayback') == 'true':
|
|
||||||
IMAGE_CACHING_SUSPENDS.append(state.SUSPEND_SYNC)
|
|
||||||
|
|
||||||
|
|
||||||
def double_urlencode(text):
|
def double_urlencode(text):
|
||||||
|
@ -37,19 +30,9 @@ def double_urldecode(text):
|
||||||
|
|
||||||
|
|
||||||
class ImageCachingThread(backgroundthread.KillableThread):
|
class ImageCachingThread(backgroundthread.KillableThread):
|
||||||
def __init__(self):
|
|
||||||
self._canceled = False
|
|
||||||
super(ImageCachingThread, self).__init__()
|
|
||||||
|
|
||||||
def isCanceled(self):
|
|
||||||
return self._canceled or state.STOP_PKC
|
|
||||||
|
|
||||||
def isSuspended(self):
|
def isSuspended(self):
|
||||||
return any(IMAGE_CACHING_SUSPENDS)
|
return any(IMAGE_CACHING_SUSPENDS)
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
self._canceled = True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _art_url_generator():
|
def _art_url_generator():
|
||||||
for kind in (KodiVideoDB, KodiMusicDB):
|
for kind in (KodiVideoDB, KodiMusicDB):
|
||||||
|
@ -88,18 +71,18 @@ def cache_url(url):
|
||||||
try:
|
try:
|
||||||
requests.head(
|
requests.head(
|
||||||
url="http://%s:%s/image/image://%s"
|
url="http://%s:%s/image/image://%s"
|
||||||
% (state.WEBSERVER_HOST,
|
% (app.CONN.webserver_host,
|
||||||
state.WEBSERVER_PORT,
|
app.CONN.webserver_port,
|
||||||
url),
|
url),
|
||||||
auth=(state.WEBSERVER_USERNAME,
|
auth=(app.CONN.webserver_username,
|
||||||
state.WEBSERVER_PASSWORD),
|
app.CONN.webserver_password),
|
||||||
timeout=TIMEOUT)
|
timeout=TIMEOUT)
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
# We don't need the result, only trigger Kodi to start the
|
# We don't need the result, only trigger Kodi to start the
|
||||||
# download. All is well
|
# download. All is well
|
||||||
break
|
break
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
if state.STOP_PKC:
|
if app.APP.stop_pkc:
|
||||||
# Kodi terminated
|
# Kodi terminated
|
||||||
break
|
break
|
||||||
# Server thinks its a DOS attack, ('error 10053')
|
# Server thinks its a DOS attack, ('error 10053')
|
||||||
|
|
|
@ -14,7 +14,6 @@ LOG = getLogger('PLEX.' + __name__)
|
||||||
|
|
||||||
|
|
||||||
class KillableThread(threading.Thread):
|
class KillableThread(threading.Thread):
|
||||||
pass
|
|
||||||
'''A thread class that supports raising exception in the thread from
|
'''A thread class that supports raising exception in the thread from
|
||||||
another thread.
|
another thread.
|
||||||
'''
|
'''
|
||||||
|
@ -77,6 +76,45 @@ class KillableThread(threading.Thread):
|
||||||
# except KillThreadException:
|
# except KillThreadException:
|
||||||
# self.onKilled()
|
# self.onKilled()
|
||||||
|
|
||||||
|
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||||
|
self._canceled = False
|
||||||
|
self._suspended = False
|
||||||
|
super(KillableThread, self).__init__(group, target, name, args, kwargs)
|
||||||
|
|
||||||
|
def isCanceled(self):
|
||||||
|
"""
|
||||||
|
Returns True if the thread is stopped
|
||||||
|
"""
|
||||||
|
if self._canceled or xbmc.abortRequested:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"""
|
||||||
|
Call to stop this thread
|
||||||
|
"""
|
||||||
|
self._canceled = True
|
||||||
|
|
||||||
|
def suspend(self):
|
||||||
|
"""
|
||||||
|
Call to suspend this thread
|
||||||
|
"""
|
||||||
|
self._suspended = True
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
"""
|
||||||
|
Call to revive a suspended thread back to life
|
||||||
|
"""
|
||||||
|
self._suspended = False
|
||||||
|
|
||||||
|
def isSuspended(self):
|
||||||
|
"""
|
||||||
|
Returns True if the thread is suspended
|
||||||
|
"""
|
||||||
|
if self._suspended:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Tasks(list):
|
class Tasks(list):
|
||||||
def add(self, task):
|
def add(self, task):
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
import logging
|
|
||||||
from threading import Thread
|
|
||||||
from xbmc import sleep
|
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from . import state
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
LOG = logging.getLogger('PLEX.command_pipeline')
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
@utils.thread_methods
|
|
||||||
class Monitor_Window(Thread):
|
|
||||||
"""
|
|
||||||
Monitors window('plex_command') for new entries that we need to take care
|
|
||||||
of, e.g. for new plays initiated on the Kodi side with addon paths.
|
|
||||||
|
|
||||||
Adjusts state.py accordingly
|
|
||||||
"""
|
|
||||||
def run(self):
|
|
||||||
stopped = self.stopped
|
|
||||||
queue = state.COMMAND_PIPELINE_QUEUE
|
|
||||||
LOG.info("----===## Starting Kodi_Play_Client ##===----")
|
|
||||||
while not stopped():
|
|
||||||
if utils.window('plex_command'):
|
|
||||||
value = utils.window('plex_command')
|
|
||||||
utils.window('plex_command', clear=True)
|
|
||||||
if value.startswith('PLAY-'):
|
|
||||||
queue.put(value.replace('PLAY-', ''))
|
|
||||||
elif value == 'SUSPEND_LIBRARY_THREAD-True':
|
|
||||||
state.SUSPEND_LIBRARY_THREAD = True
|
|
||||||
elif value == 'SUSPEND_LIBRARY_THREAD-False':
|
|
||||||
state.SUSPEND_LIBRARY_THREAD = False
|
|
||||||
elif value == 'STOP_SYNC-True':
|
|
||||||
state.STOP_SYNC = True
|
|
||||||
elif value == 'STOP_SYNC-False':
|
|
||||||
state.STOP_SYNC = False
|
|
||||||
elif value == 'PMS_STATUS-Auth':
|
|
||||||
state.PMS_STATUS = 'Auth'
|
|
||||||
elif value == 'PMS_STATUS-401':
|
|
||||||
state.PMS_STATUS = '401'
|
|
||||||
elif value == 'SUSPEND_USER_CLIENT-True':
|
|
||||||
state.SUSPEND_USER_CLIENT = True
|
|
||||||
elif value == 'SUSPEND_USER_CLIENT-False':
|
|
||||||
state.SUSPEND_USER_CLIENT = False
|
|
||||||
elif value.startswith('PLEX_TOKEN-'):
|
|
||||||
state.PLEX_TOKEN = value.replace('PLEX_TOKEN-', '') or None
|
|
||||||
elif value.startswith('PLEX_USERNAME-'):
|
|
||||||
state.PLEX_USERNAME = \
|
|
||||||
value.replace('PLEX_USERNAME-', '') or None
|
|
||||||
elif value.startswith('RUN_LIB_SCAN-'):
|
|
||||||
state.RUN_LIB_SCAN = value.replace('RUN_LIB_SCAN-', '')
|
|
||||||
elif value.startswith('CONTEXT_menu?'):
|
|
||||||
queue.put('dummy?mode=context_menu&%s'
|
|
||||||
% value.replace('CONTEXT_menu?', ''))
|
|
||||||
elif value.startswith('NAVIGATE'):
|
|
||||||
queue.put(value.replace('NAVIGATE-', ''))
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('%s not implemented' % value)
|
|
||||||
else:
|
|
||||||
sleep(50)
|
|
||||||
# Put one last item into the queue to let playback_starter end
|
|
||||||
queue.put(None)
|
|
||||||
LOG.info("----===## Kodi_Play_Client stopped ##===----")
|
|
|
@ -7,11 +7,8 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from xbmc import Player
|
from xbmc import Player
|
||||||
|
|
||||||
from . import playqueue as PQ
|
from . import playqueue as PQ, plex_functions as PF
|
||||||
from . import plex_functions as PF
|
from . import json_rpc as js, variables as v, app
|
||||||
from . import json_rpc as js
|
|
||||||
from . import variables as v
|
|
||||||
from . import state
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -68,12 +65,12 @@ def process_command(request_path, params):
|
||||||
if request_path == 'player/playback/playMedia':
|
if request_path == 'player/playback/playMedia':
|
||||||
# We need to tell service.py
|
# We need to tell service.py
|
||||||
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
action = 'alexa' if params.get('deviceName') == 'Alexa' else 'playlist'
|
||||||
state.COMPANION_QUEUE.put({
|
app.APP.companion_queue.put({
|
||||||
'action': action,
|
'action': action,
|
||||||
'data': params
|
'data': params
|
||||||
})
|
})
|
||||||
elif request_path == 'player/playback/refreshPlayQueue':
|
elif request_path == 'player/playback/refreshPlayQueue':
|
||||||
state.COMPANION_QUEUE.put({
|
app.APP.companion_queue.put({
|
||||||
'action': 'refreshPlayQueue',
|
'action': 'refreshPlayQueue',
|
||||||
'data': params
|
'data': params
|
||||||
})
|
})
|
||||||
|
@ -115,7 +112,7 @@ def process_command(request_path, params):
|
||||||
elif request_path == "player/navigation/back":
|
elif request_path == "player/navigation/back":
|
||||||
js.input_back()
|
js.input_back()
|
||||||
elif request_path == "player/playback/setStreams":
|
elif request_path == "player/playback/setStreams":
|
||||||
state.COMPANION_QUEUE.put({
|
app.APP.companion_queue.put({
|
||||||
'action': 'setStreams',
|
'action': 'setStreams',
|
||||||
'data': params
|
'data': params
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,8 +43,8 @@ class ContextMenu(xbmcgui.WindowXMLDialog):
|
||||||
return self.selected_option
|
return self.selected_option
|
||||||
|
|
||||||
def onInit(self):
|
def onInit(self):
|
||||||
if utils.window('PlexUserImage'):
|
if utils.window('plexAvatar'):
|
||||||
self.getControl(USER_IMAGE).setImage(utils.window('PlexUserImage'))
|
self.getControl(USER_IMAGE).setImage(utils.window('plexAvatar'))
|
||||||
height = 479 + (len(self._options) * 55)
|
height = 479 + (len(self._options) * 55)
|
||||||
LOG.debug("options: %s", self._options)
|
LOG.debug("options: %s", self._options)
|
||||||
self.list_ = self.getControl(LIST)
|
self.list_ = self.getControl(LIST)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import xbmcgui
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
from . import context, plex_functions as PF, playqueue as PQ
|
from . import context, plex_functions as PF, playqueue as PQ
|
||||||
from . import utils, variables as v, state
|
from . import utils, variables as v, app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class ContextMenu(object):
|
||||||
# if user uses direct paths, give option to initiate playback via PMS
|
# if user uses direct paths, give option to initiate playback via PMS
|
||||||
if self.api and self.api.extras():
|
if self.api and self.api.extras():
|
||||||
options.append(OPTIONS['Extras'])
|
options.append(OPTIONS['Extras'])
|
||||||
if state.DIRECT_PATHS and self.kodi_type in v.KODI_VIDEOTYPES:
|
if app.PLAYSTATE.direct_paths and self.kodi_type in v.KODI_VIDEOTYPES:
|
||||||
options.append(OPTIONS['PMS_Play'])
|
options.append(OPTIONS['PMS_Play'])
|
||||||
if self.kodi_type in v.KODI_VIDEOTYPES:
|
if self.kodi_type in v.KODI_VIDEOTYPES:
|
||||||
options.append(OPTIONS['Transcode'])
|
options.append(OPTIONS['Transcode'])
|
||||||
|
@ -112,7 +112,7 @@ class ContextMenu(object):
|
||||||
"""
|
"""
|
||||||
selected = self._selected_option
|
selected = self._selected_option
|
||||||
if selected == OPTIONS['Transcode']:
|
if selected == OPTIONS['Transcode']:
|
||||||
state.FORCE_TRANSCODE = True
|
app.PLAYSTATE.force_transcode = True
|
||||||
self._PMS_play()
|
self._PMS_play()
|
||||||
elif selected == OPTIONS['PMS_Play']:
|
elif selected == OPTIONS['PMS_Play']:
|
||||||
self._PMS_play()
|
self._PMS_play()
|
||||||
|
@ -146,7 +146,7 @@ class ContextMenu(object):
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
v.KODI_PLAYLIST_TYPE_FROM_KODI_TYPE[self.kodi_type])
|
||||||
playqueue.clear()
|
playqueue.clear()
|
||||||
state.CONTEXT_MENU_PLAY = True
|
app.PLAYSTATE.context_menu_play = True
|
||||||
handle = self.api.path(force_first_media=False, force_addon=True)
|
handle = self.api.path(force_first_media=False, force_addon=True)
|
||||||
xbmc.executebuiltin('RunPlugin(%s)' % handle)
|
xbmc.executebuiltin('RunPlugin(%s)' % handle)
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from . import utils
|
from . import utils, clientinfo, app
|
||||||
from . import clientinfo
|
|
||||||
from . import state
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -14,7 +12,7 @@ from . import state
|
||||||
import requests.packages.urllib3
|
import requests.packages.urllib3
|
||||||
requests.packages.urllib3.disable_warnings()
|
requests.packages.urllib3.disable_warnings()
|
||||||
|
|
||||||
LOG = getLogger('PLEX.downloadutils')
|
LOG = getLogger('PLEX.download')
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -39,25 +37,16 @@ class DownloadUtils():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.__dict__ = self._shared_state
|
self.__dict__ = self._shared_state
|
||||||
|
|
||||||
def setServer(self, server):
|
|
||||||
"""
|
|
||||||
Reserved for userclient only
|
|
||||||
"""
|
|
||||||
self.server = server
|
|
||||||
LOG.debug("Set server: %s", server)
|
|
||||||
|
|
||||||
def setSSL(self, verifySSL=None, certificate=None):
|
def setSSL(self, verifySSL=None, certificate=None):
|
||||||
"""
|
"""
|
||||||
Reserved for userclient only
|
|
||||||
|
|
||||||
verifySSL must be 'true' to enable certificate validation
|
verifySSL must be 'true' to enable certificate validation
|
||||||
|
|
||||||
certificate must be path to certificate or 'None'
|
certificate must be path to certificate or 'None'
|
||||||
"""
|
"""
|
||||||
if verifySSL is None:
|
if verifySSL is None:
|
||||||
verifySSL = state.VERIFY_SSL_CERT
|
verifySSL = app.CONN.verify_ssl_cert
|
||||||
if certificate is None:
|
if certificate is None:
|
||||||
certificate = state.SSL_CERT_PATH
|
certificate = app.CONN.ssl_cert_path
|
||||||
# Set the session's parameters
|
# Set the session's parameters
|
||||||
self.s.verify = verifySSL
|
self.s.verify = verifySSL
|
||||||
if certificate:
|
if certificate:
|
||||||
|
@ -67,8 +56,7 @@ class DownloadUtils():
|
||||||
|
|
||||||
def startSession(self, reset=False):
|
def startSession(self, reset=False):
|
||||||
"""
|
"""
|
||||||
User should be authenticated when this method is called (via
|
User should be authenticated when this method is called
|
||||||
userclient)
|
|
||||||
"""
|
"""
|
||||||
# Start session
|
# Start session
|
||||||
self.s = requests.Session()
|
self.s = requests.Session()
|
||||||
|
@ -80,9 +68,6 @@ class DownloadUtils():
|
||||||
# Set SSL settings
|
# Set SSL settings
|
||||||
self.setSSL()
|
self.setSSL()
|
||||||
|
|
||||||
# Set other stuff
|
|
||||||
self.setServer(utils.window('pms_server'))
|
|
||||||
|
|
||||||
# Counters to declare PMS dead or unauthorized
|
# Counters to declare PMS dead or unauthorized
|
||||||
# Use window variables because start of movies will be called with a
|
# Use window variables because start of movies will be called with a
|
||||||
# new plugin instance - it's impossible to share data otherwise
|
# new plugin instance - it's impossible to share data otherwise
|
||||||
|
@ -94,7 +79,7 @@ class DownloadUtils():
|
||||||
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||||
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
|
||||||
|
|
||||||
LOG.info("Requests session started on: %s", self.server)
|
LOG.info("Requests session started on: %s", app.CONN.server)
|
||||||
|
|
||||||
def stopSession(self):
|
def stopSession(self):
|
||||||
try:
|
try:
|
||||||
|
@ -155,7 +140,7 @@ class DownloadUtils():
|
||||||
self.startSession()
|
self.startSession()
|
||||||
s = self.s
|
s = self.s
|
||||||
# Replace for the real values
|
# Replace for the real values
|
||||||
url = url.replace("{server}", self.server)
|
url = url.replace("{server}", app.CONN.server)
|
||||||
else:
|
else:
|
||||||
# User is not (yet) authenticated. Used to communicate with
|
# User is not (yet) authenticated. Used to communicate with
|
||||||
# plex.tv and to check for PMS servers
|
# plex.tv and to check for PMS servers
|
||||||
|
@ -164,9 +149,9 @@ class DownloadUtils():
|
||||||
headerOptions = self.getHeader(options=headerOptions)
|
headerOptions = self.getHeader(options=headerOptions)
|
||||||
else:
|
else:
|
||||||
headerOptions = headerOverride
|
headerOptions = headerOverride
|
||||||
kwargs['verify'] = state.VERIFY_SSL_CERT
|
kwargs['verify'] = app.CONN.verify_ssl_cert
|
||||||
if state.SSL_CERT_PATH:
|
if app.CONN.ssl_cert_path:
|
||||||
kwargs['cert'] = state.SSL_CERT_PATH
|
kwargs['cert'] = app.CONN.ssl_cert_path
|
||||||
|
|
||||||
# Set the variables we were passed (fallback to request session
|
# Set the variables we were passed (fallback to request session
|
||||||
# otherwise - faster)
|
# otherwise - faster)
|
||||||
|
@ -252,12 +237,11 @@ class DownloadUtils():
|
||||||
self.unauthorizedAttempts):
|
self.unauthorizedAttempts):
|
||||||
LOG.warn('We seem to be truly unauthorized for PMS'
|
LOG.warn('We seem to be truly unauthorized for PMS'
|
||||||
' %s ', url)
|
' %s ', url)
|
||||||
if state.PMS_STATUS not in ('401', 'Auth'):
|
if app.CONN.pms_status not in ('401', 'Auth'):
|
||||||
# Tell userclient token has been revoked.
|
# Tell others token has been revoked.
|
||||||
LOG.debug('Setting PMS server status to '
|
LOG.debug('Setting PMS server status to '
|
||||||
'unauthorized')
|
'unauthorized')
|
||||||
state.PMS_STATUS = '401'
|
app.CONN.pms_status = '401'
|
||||||
utils.window('plex_serverStatus', value="401")
|
|
||||||
utils.dialog('notification',
|
utils.dialog('notification',
|
||||||
utils.lang(29999),
|
utils.lang(29999),
|
||||||
utils.lang(30017),
|
utils.lang(30017),
|
||||||
|
|
|
@ -20,8 +20,8 @@ from .plex_api import API
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
# Be careful - your using state in another Python instance!
|
# Be careful - your using app in another Python instance!
|
||||||
from . import state
|
from . import app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.entrypoint')
|
LOG = getLogger('PLEX.entrypoint')
|
||||||
|
@ -34,34 +34,7 @@ def choose_pms_server():
|
||||||
Lets user choose from list of PMS
|
Lets user choose from list of PMS
|
||||||
"""
|
"""
|
||||||
LOG.info("Choosing PMS server requested, starting")
|
LOG.info("Choosing PMS server requested, starting")
|
||||||
|
utils.plex_command('choose_pms_server')
|
||||||
setup = initialsetup.InitialSetup()
|
|
||||||
server = setup.pick_pms(showDialog=True)
|
|
||||||
if server is None:
|
|
||||||
LOG.error('We did not connect to a new PMS, aborting')
|
|
||||||
utils.plex_command('SUSPEND_USER_CLIENT', 'False')
|
|
||||||
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False')
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.info("User chose server %s", server['name'])
|
|
||||||
setup.write_pms_to_settings(server)
|
|
||||||
|
|
||||||
if not _log_out():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Wipe Kodi and Plex database as well as playlists and video nodes
|
|
||||||
utils.wipe_database()
|
|
||||||
|
|
||||||
# Log in again
|
|
||||||
_log_in()
|
|
||||||
LOG.info("Choosing new PMS complete")
|
|
||||||
# '<PMS> connected'
|
|
||||||
utils.dialog('notification',
|
|
||||||
utils.lang(29999),
|
|
||||||
'%s %s' % (server['name'], utils.lang(39220)),
|
|
||||||
icon='{plex}',
|
|
||||||
time=3000,
|
|
||||||
sound=False)
|
|
||||||
|
|
||||||
|
|
||||||
def toggle_plex_tv_sign_in():
|
def toggle_plex_tv_sign_in():
|
||||||
|
@ -69,37 +42,8 @@ def toggle_plex_tv_sign_in():
|
||||||
Signs out of Plex.tv if there was a token saved and thus deletes the token.
|
Signs out of Plex.tv if there was a token saved and thus deletes the token.
|
||||||
Or signs in to plex.tv if the user was not logged in before.
|
Or signs in to plex.tv if the user was not logged in before.
|
||||||
"""
|
"""
|
||||||
if utils.settings('plexToken'):
|
LOG.info('Toggle of Plex.tv sign-in requested')
|
||||||
LOG.info('Reseting plex.tv credentials in settings')
|
utils.plex_command('toggle_plex_tv_sign_in')
|
||||||
utils.settings('plexLogin', value="")
|
|
||||||
utils.settings('plexToken', value="")
|
|
||||||
utils.settings('plexid', value="")
|
|
||||||
utils.settings('plexAvatar', value="")
|
|
||||||
utils.settings('plex_status', value=utils.lang(39226))
|
|
||||||
|
|
||||||
utils.window('plex_token', clear=True)
|
|
||||||
utils.plex_command('PLEX_TOKEN', '')
|
|
||||||
utils.plex_command('PLEX_USERNAME', '')
|
|
||||||
else:
|
|
||||||
LOG.info('Login to plex.tv')
|
|
||||||
initialsetup.InitialSetup().plex_tv_sign_in()
|
|
||||||
utils.dialog('notification',
|
|
||||||
utils.lang(29999),
|
|
||||||
utils.lang(39221),
|
|
||||||
icon='{plex}',
|
|
||||||
time=3000,
|
|
||||||
sound=False)
|
|
||||||
|
|
||||||
|
|
||||||
def reset_authorization():
|
|
||||||
"""
|
|
||||||
User tried login and failed too many times. Reset # of logins
|
|
||||||
"""
|
|
||||||
if utils.yesno_dialog(utils.lang(29999), utils.lang(39206)):
|
|
||||||
LOG.info("Reset login attempts.")
|
|
||||||
utils.plex_command('PMS_STATUS', 'Auth')
|
|
||||||
else:
|
|
||||||
executebuiltin('Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
|
||||||
|
|
||||||
|
|
||||||
def directory_item(label, path, folder=True):
|
def directory_item(label, path, folder=True):
|
||||||
|
@ -185,13 +129,7 @@ def switch_plex_user():
|
||||||
# position = 0
|
# position = 0
|
||||||
# utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
|
# utils.window('EmbyAdditionalUserImage.%s' % position, clear=True)
|
||||||
LOG.info("Plex home user switch requested")
|
LOG.info("Plex home user switch requested")
|
||||||
if not _log_out():
|
utils.plex_command('switch_plex_user')
|
||||||
return
|
|
||||||
# First remove playlists of old user
|
|
||||||
utils.delete_playlists()
|
|
||||||
# Remove video nodes
|
|
||||||
utils.delete_nodes()
|
|
||||||
_log_in()
|
|
||||||
|
|
||||||
|
|
||||||
def create_listitem(item, append_show_title=False, append_sxxexx=False):
|
def create_listitem(item, append_show_title=False, append_sxxexx=False):
|
||||||
|
@ -573,13 +511,13 @@ def on_deck_episodes(viewid, tagname, limit):
|
||||||
return
|
return
|
||||||
# We're using another python instance - need to load some vars
|
# We're using another python instance - need to load some vars
|
||||||
if utils.settings('useDirectPaths') == '1':
|
if utils.settings('useDirectPaths') == '1':
|
||||||
state.DIRECT_PATHS = True
|
app.SYNC.direct_paths = True
|
||||||
state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true'
|
app.SYNC.replace_smb_path = utils.settings('replaceSMB') == 'true'
|
||||||
state.REMAP_PATH = utils.settings('remapSMB') == 'true'
|
app.SYNC.remap_path = utils.settings('remapSMB') == 'true'
|
||||||
if state.REMAP_PATH:
|
if app.SYNC.remap_path:
|
||||||
initialsetup.set_replace_paths()
|
initialsetup.set_replace_paths()
|
||||||
# Let's NOT check paths for widgets!
|
# Let's NOT check paths for widgets!
|
||||||
state.PATH_VERIFIED = True
|
app.SYNC.path_verified = True
|
||||||
counter = 0
|
counter = 0
|
||||||
for item in xml:
|
for item in xml:
|
||||||
api = API(item)
|
api = API(item)
|
||||||
|
@ -964,107 +902,5 @@ def create_new_pms():
|
||||||
"""
|
"""
|
||||||
Opens dialogs for the user the plug in the PMS details
|
Opens dialogs for the user the plug in the PMS details
|
||||||
"""
|
"""
|
||||||
# "Enter your Plex Media Server's IP or URL. Examples are:"
|
LOG.info('Request to manually enter new PMS address')
|
||||||
utils.messageDialog(utils.lang(29999),
|
utils.plex_command('enter_new_pms_address')
|
||||||
'%s\n%s\n%s' % (utils.lang(39215),
|
|
||||||
'192.168.1.2',
|
|
||||||
'plex.myServer.org'))
|
|
||||||
address = utils.dialog('input', "Enter PMS IP or URL")
|
|
||||||
if address == '':
|
|
||||||
return
|
|
||||||
port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}')
|
|
||||||
if port == '':
|
|
||||||
return
|
|
||||||
url = '%s:%s' % (address, port)
|
|
||||||
# "Does your Plex Media Server support SSL connections?
|
|
||||||
# (https instead of http)"
|
|
||||||
https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217))
|
|
||||||
if https:
|
|
||||||
url = 'https://%s' % url
|
|
||||||
else:
|
|
||||||
url = 'http://%s' % url
|
|
||||||
https = 'true' if https else 'false'
|
|
||||||
machine_identifier = PF.GetMachineIdentifier(url)
|
|
||||||
if machine_identifier is None:
|
|
||||||
# "Error contacting url
|
|
||||||
# Abort (Yes) or save address anyway (No)"
|
|
||||||
if utils.yesno_dialog(utils.lang(29999),
|
|
||||||
'%s %s. %s' % (utils.lang(39218),
|
|
||||||
url,
|
|
||||||
utils.lang(39219))):
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
utils.settings('plex_machineIdentifier', '')
|
|
||||||
else:
|
|
||||||
utils.settings('plex_machineIdentifier', machine_identifier)
|
|
||||||
LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s',
|
|
||||||
https, address, port, machine_identifier)
|
|
||||||
utils.settings('https', value=https)
|
|
||||||
utils.settings('ipaddress', value=address)
|
|
||||||
utils.settings('port', value=port)
|
|
||||||
# Chances are this is a local PMS, so disable SSL certificate check
|
|
||||||
utils.settings('sslverify', value='false')
|
|
||||||
|
|
||||||
# Sign out to trigger new login
|
|
||||||
if _log_out():
|
|
||||||
# Only login again if logout was successful
|
|
||||||
_log_in()
|
|
||||||
|
|
||||||
|
|
||||||
def _log_in():
|
|
||||||
"""
|
|
||||||
Resets (clears) window properties to enable (re-)login
|
|
||||||
|
|
||||||
SUSPEND_LIBRARY_THREAD is set to False in service.py if user was signed
|
|
||||||
out!
|
|
||||||
"""
|
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'full')
|
|
||||||
# Restart user client
|
|
||||||
utils.plex_command('SUSPEND_USER_CLIENT', 'False')
|
|
||||||
|
|
||||||
|
|
||||||
def _log_out():
|
|
||||||
"""
|
|
||||||
Finishes lib scans, logs out user.
|
|
||||||
|
|
||||||
Returns True if successfully signed out, False otherwise
|
|
||||||
"""
|
|
||||||
# Resetting, please wait
|
|
||||||
utils.dialog('notification',
|
|
||||||
utils.lang(29999),
|
|
||||||
utils.lang(39207),
|
|
||||||
icon='{plex}',
|
|
||||||
time=3000,
|
|
||||||
sound=False)
|
|
||||||
# Pause library sync thread
|
|
||||||
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'True')
|
|
||||||
# Wait max for 10 seconds for all lib scans to shutdown
|
|
||||||
counter = 0
|
|
||||||
while utils.window('plex_dbScan') == 'true':
|
|
||||||
if counter > 200:
|
|
||||||
# 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
|
|
||||||
utils.plex_command('SUSPEND_LIBRARY_THREAD', 'False')
|
|
||||||
LOG.error("Could not stop library sync, aborting")
|
|
||||||
return False
|
|
||||||
counter += 1
|
|
||||||
sleep(50)
|
|
||||||
LOG.debug("Successfully stopped library sync")
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
# Log out currently signed in user:
|
|
||||||
utils.window('plex_serverStatus', value='401')
|
|
||||||
utils.plex_command('PMS_STATUS', '401')
|
|
||||||
# Above method needs to have run its course! Hence wait
|
|
||||||
while utils.window('plex_serverStatus') == "401":
|
|
||||||
if counter > 100:
|
|
||||||
# 'Failed to reset PKC. Try to restart Kodi.'
|
|
||||||
utils.messageDialog(utils.lang(29999), utils.lang(39208))
|
|
||||||
LOG.error("Could not sign out user, aborting")
|
|
||||||
return False
|
|
||||||
counter += 1
|
|
||||||
sleep(50)
|
|
||||||
# Suspend the user client during procedure
|
|
||||||
utils.plex_command('SUSPEND_USER_CLIENT', 'True')
|
|
||||||
return True
|
|
||||||
|
|
|
@ -2,22 +2,18 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
from xbmc import executebuiltin, translatePath
|
from xbmc import executebuiltin
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from .utils import etree
|
from .utils import etree
|
||||||
from . import path_ops
|
from . import path_ops
|
||||||
from . import migration
|
from . import migration
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import userclient
|
|
||||||
from . import clientinfo
|
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
from . import plex_tv
|
from . import plex_tv
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import playqueue as PQ
|
from . import app
|
||||||
from . import state
|
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -30,96 +26,6 @@ if not path_ops.exists(v.EXTERNAL_SUBTITLE_TEMP_PATH):
|
||||||
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
|
path_ops.makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
|
||||||
|
|
||||||
|
|
||||||
WINDOW_PROPERTIES = (
|
|
||||||
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
|
|
||||||
"plex_customplayqueue", "plex_playbackProps",
|
|
||||||
"pms_token", "plex_token", "pms_server", "plex_machineIdentifier",
|
|
||||||
"plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths",
|
|
||||||
"countError", "countUnauthorized", "plex_restricteduser",
|
|
||||||
"plex_allows_mediaDeletion", "plex_command", "plex_result",
|
|
||||||
"plex_force_transcode_pix"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def reload_pkc():
|
|
||||||
"""
|
|
||||||
Will reload state.py entirely and then initiate some values from the Kodi
|
|
||||||
settings file
|
|
||||||
"""
|
|
||||||
LOG.info('Start (re-)loading PKC settings')
|
|
||||||
# Reset state.py
|
|
||||||
reload(state)
|
|
||||||
# Reset window props
|
|
||||||
for prop in WINDOW_PROPERTIES:
|
|
||||||
utils.window(prop, clear=True)
|
|
||||||
# Clear video nodes properties
|
|
||||||
from .library_sync import videonodes
|
|
||||||
videonodes.VideoNodes().clearProperties()
|
|
||||||
|
|
||||||
# Initializing
|
|
||||||
state.VERIFY_SSL_CERT = utils.settings('sslverify') == 'true'
|
|
||||||
state.SSL_CERT_PATH = utils.settings('sslcert') \
|
|
||||||
if utils.settings('sslcert') != 'None' else None
|
|
||||||
state.FULL_SYNC_INTERVALL = int(utils.settings('fullSyncInterval')) * 60
|
|
||||||
state.SYNC_THREAD_NUMBER = int(utils.settings('syncThreadNumber'))
|
|
||||||
state.SYNC_DIALOG = utils.settings('dbSyncIndicator') == 'true'
|
|
||||||
state.ENABLE_MUSIC = utils.settings('enableMusic') == 'true'
|
|
||||||
state.BACKGROUND_SYNC_DISABLED = utils.settings(
|
|
||||||
'enableBackgroundSync') == 'false'
|
|
||||||
state.BACKGROUNDSYNC_SAFTYMARGIN = int(
|
|
||||||
utils.settings('backgroundsync_saftyMargin'))
|
|
||||||
state.REPLACE_SMB_PATH = utils.settings('replaceSMB') == 'true'
|
|
||||||
state.REMAP_PATH = utils.settings('remapSMB') == 'true'
|
|
||||||
state.KODI_PLEX_TIME_OFFSET = float(utils.settings('kodiplextimeoffset'))
|
|
||||||
state.FETCH_PMS_ITEM_NUMBER = utils.settings('fetch_pms_item_number')
|
|
||||||
state.FORCE_RELOAD_SKIN = \
|
|
||||||
utils.settings('forceReloadSkinOnPlaybackStop') == 'true'
|
|
||||||
# Init some Queues()
|
|
||||||
state.COMMAND_PIPELINE_QUEUE = Queue()
|
|
||||||
state.COMPANION_QUEUE = Queue(maxsize=100)
|
|
||||||
state.WEBSOCKET_QUEUE = Queue()
|
|
||||||
set_replace_paths()
|
|
||||||
set_webserver()
|
|
||||||
# To detect Kodi profile switches
|
|
||||||
utils.window('plex_kodiProfile',
|
|
||||||
value=utils.try_decode(translatePath("special://profile")))
|
|
||||||
clientinfo.getDeviceId()
|
|
||||||
# Initialize the PKC playqueues
|
|
||||||
PQ.init_playqueues()
|
|
||||||
LOG.info('Done (re-)loading PKC settings')
|
|
||||||
|
|
||||||
|
|
||||||
def set_replace_paths():
|
|
||||||
"""
|
|
||||||
Sets our values for direct paths correctly (including using lower-case
|
|
||||||
protocols like smb:// and NOT SMB://)
|
|
||||||
"""
|
|
||||||
for typus in v.REMAP_TYPE_FROM_PLEXTYPE.values():
|
|
||||||
for arg in ('Org', 'New'):
|
|
||||||
key = 'remapSMB%s%s' % (typus, arg)
|
|
||||||
value = utils.settings(key)
|
|
||||||
if '://' in value:
|
|
||||||
protocol = value.split('://', 1)[0]
|
|
||||||
value = value.replace(protocol, protocol.lower())
|
|
||||||
setattr(state, key, value)
|
|
||||||
|
|
||||||
|
|
||||||
def set_webserver():
|
|
||||||
"""
|
|
||||||
Set the Kodi webserver details - used to set the texture cache
|
|
||||||
"""
|
|
||||||
if js.get_setting('services.webserver') in (None, False):
|
|
||||||
# Enable the webserver, it is disabled
|
|
||||||
js.set_setting('services.webserver', True)
|
|
||||||
# Set standard port and username
|
|
||||||
# set_setting('services.webserverport', 8080)
|
|
||||||
# set_setting('services.webserverusername', 'kodi')
|
|
||||||
# Webserver already enabled
|
|
||||||
state.WEBSERVER_PORT = js.get_setting('services.webserverport')
|
|
||||||
state.WEBSERVER_USERNAME = js.get_setting('services.webserverusername')
|
|
||||||
state.WEBSERVER_PASSWORD = js.get_setting('services.webserverpassword')
|
|
||||||
|
|
||||||
|
|
||||||
def _write_pms_settings(url, token):
|
def _write_pms_settings(url, token):
|
||||||
"""
|
"""
|
||||||
Sets certain settings for server by asking for the PMS' settings
|
Sets certain settings for server by asking for the PMS' settings
|
||||||
|
@ -145,11 +51,8 @@ class InitialSetup(object):
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
LOG.debug('Entering initialsetup class')
|
LOG.debug('Entering initialsetup class')
|
||||||
self.server = userclient.UserClient().get_server()
|
|
||||||
self.serverid = utils.settings('plex_machineIdentifier')
|
|
||||||
# Get Plex credentials from settings file, if they exist
|
# Get Plex credentials from settings file, if they exist
|
||||||
plexdict = PF.GetPlexLoginFromSettings()
|
plexdict = PF.GetPlexLoginFromSettings()
|
||||||
self.myplexlogin = plexdict['myplexlogin'] == 'true'
|
|
||||||
self.plex_login = plexdict['plexLogin']
|
self.plex_login = plexdict['plexLogin']
|
||||||
self.plex_token = plexdict['plexToken']
|
self.plex_token = plexdict['plexToken']
|
||||||
self.plexid = plexdict['plexid']
|
self.plexid = plexdict['plexid']
|
||||||
|
@ -158,6 +61,48 @@ class InitialSetup(object):
|
||||||
if self.plex_token:
|
if self.plex_token:
|
||||||
LOG.debug('Found a plex.tv token in the settings')
|
LOG.debug('Found a plex.tv token in the settings')
|
||||||
|
|
||||||
|
def enter_new_pms_address(self):
|
||||||
|
# "Enter your Plex Media Server's IP or URL. Examples are:"
|
||||||
|
utils.messageDialog(utils.lang(29999),
|
||||||
|
'%s\n%s\n%s' % (utils.lang(39215),
|
||||||
|
'192.168.1.2',
|
||||||
|
'plex.myServer.org'))
|
||||||
|
address = utils.dialog('input', "Enter PMS IP or URL")
|
||||||
|
if address == '':
|
||||||
|
return False
|
||||||
|
port = utils.dialog('input', "Enter PMS port", '32400', type='{numeric}')
|
||||||
|
if port == '':
|
||||||
|
return False
|
||||||
|
url = '%s:%s' % (address, port)
|
||||||
|
# "Does your Plex Media Server support SSL connections?
|
||||||
|
# (https instead of http)"
|
||||||
|
https = utils.yesno_dialog(utils.lang(29999), utils.lang(39217))
|
||||||
|
if https:
|
||||||
|
url = 'https://%s' % url
|
||||||
|
else:
|
||||||
|
url = 'http://%s' % url
|
||||||
|
https = 'true' if https else 'false'
|
||||||
|
machine_identifier = PF.GetMachineIdentifier(url)
|
||||||
|
if machine_identifier is None:
|
||||||
|
# "Error contacting url
|
||||||
|
# Abort (Yes) or save address anyway (No)"
|
||||||
|
if utils.yesno_dialog(utils.lang(29999),
|
||||||
|
'%s %s. %s' % (utils.lang(39218),
|
||||||
|
url,
|
||||||
|
utils.lang(39219))):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
utils.settings('plex_machineIdentifier', '')
|
||||||
|
else:
|
||||||
|
utils.settings('plex_machineIdentifier', machine_identifier)
|
||||||
|
LOG.info('Set new PMS to https %s, address %s, port %s, machineId %s',
|
||||||
|
https, address, port, machine_identifier)
|
||||||
|
utils.settings('https', value=https)
|
||||||
|
utils.settings('ipaddress', value=address)
|
||||||
|
utils.settings('port', value=port)
|
||||||
|
# Chances are this is a local PMS, so disable SSL certificate check
|
||||||
|
utils.settings('sslverify', value='false')
|
||||||
|
|
||||||
def plex_tv_sign_in(self):
|
def plex_tv_sign_in(self):
|
||||||
"""
|
"""
|
||||||
Signs (freshly) in to plex.tv (will be saved to file settings)
|
Signs (freshly) in to plex.tv (will be saved to file settings)
|
||||||
|
@ -227,26 +172,26 @@ class InitialSetup(object):
|
||||||
not set before
|
not set before
|
||||||
"""
|
"""
|
||||||
answer = True
|
answer = True
|
||||||
chk = PF.check_connection(self.server, verifySSL=False)
|
chk = PF.check_connection(app.CONN.server, verifySSL=False)
|
||||||
if chk is False:
|
if chk is False:
|
||||||
LOG.warn('Could not reach PMS %s', self.server)
|
LOG.warn('Could not reach PMS %s', app.CONN.server)
|
||||||
answer = False
|
answer = False
|
||||||
if answer is True and not self.serverid:
|
if answer is True and not app.CONN.machine_identifier:
|
||||||
LOG.info('No PMS machineIdentifier found for %s. Trying to '
|
LOG.info('No PMS machineIdentifier found for %s. Trying to '
|
||||||
'get the PMS unique ID', self.server)
|
'get the PMS unique ID', app.CONN.server)
|
||||||
self.serverid = PF.GetMachineIdentifier(self.server)
|
app.CONN.machine_identifier = PF.GetMachineIdentifier(app.CONN.server)
|
||||||
if self.serverid is None:
|
if app.CONN.machine_identifier is None:
|
||||||
LOG.warn('Could not retrieve machineIdentifier')
|
LOG.warn('Could not retrieve machineIdentifier')
|
||||||
answer = False
|
answer = False
|
||||||
else:
|
else:
|
||||||
utils.settings('plex_machineIdentifier', value=self.serverid)
|
utils.settings('plex_machineIdentifier', value=app.CONN.machine_identifier)
|
||||||
elif answer is True:
|
elif answer is True:
|
||||||
temp_server_id = PF.GetMachineIdentifier(self.server)
|
temp_server_id = PF.GetMachineIdentifier(app.CONN.server)
|
||||||
if temp_server_id != self.serverid:
|
if temp_server_id != app.CONN.machine_identifier:
|
||||||
LOG.warn('The current PMS %s was expected to have a '
|
LOG.warn('The current PMS %s was expected to have a '
|
||||||
'unique machineIdentifier of %s. But we got '
|
'unique machineIdentifier of %s. But we got '
|
||||||
'%s. Pick a new server to be sure',
|
'%s. Pick a new server to be sure',
|
||||||
self.server, self.serverid, temp_server_id)
|
app.CONN.server, app.CONN.machine_identifier, temp_server_id)
|
||||||
answer = False
|
answer = False
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
|
@ -305,7 +250,7 @@ class InitialSetup(object):
|
||||||
"""
|
"""
|
||||||
server = None
|
server = None
|
||||||
# If no server is set, let user choose one
|
# If no server is set, let user choose one
|
||||||
if not self.server or not self.serverid:
|
if not app.CONN.server or not app.CONN.machine_identifier:
|
||||||
showDialog = True
|
showDialog = True
|
||||||
if showDialog is True:
|
if showDialog is True:
|
||||||
server = self._user_pick_pms()
|
server = self._user_pick_pms()
|
||||||
|
@ -328,13 +273,13 @@ class InitialSetup(object):
|
||||||
if https_updated is False:
|
if https_updated is False:
|
||||||
serverlist = PF.discover_pms(self.plex_token)
|
serverlist = PF.discover_pms(self.plex_token)
|
||||||
for item in serverlist:
|
for item in serverlist:
|
||||||
if item.get('machineIdentifier') == self.serverid:
|
if item.get('machineIdentifier') == app.CONN.machine_identifier:
|
||||||
server = item
|
server = item
|
||||||
if server is None:
|
if server is None:
|
||||||
name = utils.settings('plex_servername')
|
name = utils.settings('plex_servername')
|
||||||
LOG.warn('The PMS you have used before with a unique '
|
LOG.warn('The PMS you have used before with a unique '
|
||||||
'machineIdentifier of %s and name %s is '
|
'machineIdentifier of %s and name %s is '
|
||||||
'offline', self.serverid, name)
|
'offline', app.CONN.machine_identifier, name)
|
||||||
return
|
return
|
||||||
chk = self._check_pms_connectivity(server)
|
chk = self._check_pms_connectivity(server)
|
||||||
if chk == 504 and https_updated is False:
|
if chk == 504 and https_updated is False:
|
||||||
|
@ -535,7 +480,8 @@ class InitialSetup(object):
|
||||||
# Do we need to migrate stuff?
|
# Do we need to migrate stuff?
|
||||||
migration.check_migration()
|
migration.check_migration()
|
||||||
# Reload the server IP cause we might've deleted it during migration
|
# Reload the server IP cause we might've deleted it during migration
|
||||||
self.server = userclient.UserClient().get_server()
|
app.CONN.load()
|
||||||
|
app.CONN.server = app.CONN.server
|
||||||
|
|
||||||
# Display a warning if Kodi puts ALL movies into the queue, basically
|
# Display a warning if Kodi puts ALL movies into the queue, basically
|
||||||
# breaking playback reporting for PKC
|
# breaking playback reporting for PKC
|
||||||
|
@ -556,19 +502,19 @@ class InitialSetup(object):
|
||||||
|
|
||||||
# If a Plex server IP has already been set
|
# If a Plex server IP has already been set
|
||||||
# return only if the right machine identifier is found
|
# return only if the right machine identifier is found
|
||||||
if self.server:
|
if app.CONN.server:
|
||||||
LOG.info("PMS is already set: %s. Checking now...", self.server)
|
LOG.info("PMS is already set: %s. Checking now...", app.CONN.server)
|
||||||
if self.check_existing_pms():
|
if self.check_existing_pms():
|
||||||
LOG.info("Using PMS %s with machineIdentifier %s",
|
LOG.info("Using PMS %s with machineIdentifier %s",
|
||||||
self.server, self.serverid)
|
app.CONN.server, app.CONN.machine_identifier)
|
||||||
_write_pms_settings(self.server, self.pms_token)
|
_write_pms_settings(app.CONN.server, self.pms_token)
|
||||||
if reboot is True:
|
if reboot is True:
|
||||||
utils.reboot_kodi()
|
utils.reboot_kodi()
|
||||||
return
|
return
|
||||||
|
|
||||||
# If not already retrieved myplex info, optionally let user sign in
|
# If not already retrieved myplex info, optionally let user sign in
|
||||||
# to plex.tv. This DOES get called on very first install run
|
# to plex.tv. This DOES get called on very first install run
|
||||||
if not self.plex_token and self.myplexlogin:
|
if not self.plex_token and app.ACCOUNT.myplexlogin:
|
||||||
self.plex_tv_sign_in()
|
self.plex_tv_sign_in()
|
||||||
|
|
||||||
server = self.pick_pms()
|
server = self.pick_pms()
|
||||||
|
|
|
@ -7,7 +7,7 @@ from ntpath import dirname
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from ..kodi_db import KodiVideoDB
|
from ..kodi_db import KodiVideoDB
|
||||||
from .. import utils
|
from .. import utils, timing
|
||||||
|
|
||||||
LOG = getLogger('PLEX.itemtypes.common')
|
LOG = getLogger('PLEX.itemtypes.common')
|
||||||
|
|
||||||
|
@ -135,5 +135,5 @@ class ItemBase(object):
|
||||||
resume,
|
resume,
|
||||||
duration,
|
duration,
|
||||||
view_count,
|
view_count,
|
||||||
utils.unix_date_to_kodi(lastViewedAt),
|
timing.plex_date_to_kodi(lastViewedAt),
|
||||||
plex_type)
|
plex_type)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
|
|
||||||
from .common import ItemBase
|
from .common import ItemBase
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import state, variables as v, plex_functions as PF
|
from .. import app, variables as v, plex_functions as PF
|
||||||
|
|
||||||
LOG = getLogger('PLEX.movies')
|
LOG = getLogger('PLEX.movies')
|
||||||
|
|
||||||
|
@ -50,8 +50,8 @@ class Movie(ItemBase):
|
||||||
studios = api.music_studio_list()
|
studios = api.music_studio_list()
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
do_indirect = not state.DIRECT_PATHS
|
do_indirect = not app.SYNC.direct_paths
|
||||||
if state.DIRECT_PATHS:
|
if app.SYNC.direct_paths:
|
||||||
# Direct paths is set the Kodi way
|
# Direct paths is set the Kodi way
|
||||||
playurl = api.file_path(force_first_media=True)
|
playurl = api.file_path(force_first_media=True)
|
||||||
if playurl is None:
|
if playurl is None:
|
||||||
|
|
|
@ -7,7 +7,7 @@ from .common import ItemBase
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from ..kodi_db import KodiMusicDB
|
from ..kodi_db import KodiMusicDB
|
||||||
from .. import plex_functions as PF, utils, state, variables as v
|
from .. import plex_functions as PF, utils, timing, app, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.music')
|
LOG = getLogger('PLEX.music')
|
||||||
|
|
||||||
|
@ -165,12 +165,11 @@ class Artist(MusicMixin, ItemBase):
|
||||||
# Kodi doesn't allow that. In case that happens we just merge the
|
# Kodi doesn't allow that. In case that happens we just merge the
|
||||||
# artist entries.
|
# artist entries.
|
||||||
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
|
kodi_id = self.kodidb.add_artist(api.title(), musicBrainzId)
|
||||||
# Create the reference in plex table
|
|
||||||
self.kodidb.update_artist(api.list_to_string(api.genre_list()),
|
self.kodidb.update_artist(api.list_to_string(api.genre_list()),
|
||||||
api.plot(),
|
api.plot(),
|
||||||
thumb,
|
thumb,
|
||||||
fanart,
|
fanart,
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
kodi_id)
|
kodi_id)
|
||||||
# Update artwork
|
# Update artwork
|
||||||
self.kodidb.modify_artwork(artworks,
|
self.kodidb.modify_artwork(artworks,
|
||||||
|
@ -256,7 +255,7 @@ class Album(MusicMixin, ItemBase):
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.music_studio(),
|
||||||
userdata['UserRating'],
|
userdata['UserRating'],
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album',
|
'album',
|
||||||
kodi_id)
|
kodi_id)
|
||||||
else:
|
else:
|
||||||
|
@ -270,7 +269,7 @@ class Album(MusicMixin, ItemBase):
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.music_studio(),
|
||||||
userdata['UserRating'],
|
userdata['UserRating'],
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album',
|
'album',
|
||||||
kodi_id)
|
kodi_id)
|
||||||
# OR ADD THE ALBUM #####
|
# OR ADD THE ALBUM #####
|
||||||
|
@ -289,7 +288,7 @@ class Album(MusicMixin, ItemBase):
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.music_studio(),
|
||||||
userdata['UserRating'],
|
userdata['UserRating'],
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album')
|
'album')
|
||||||
else:
|
else:
|
||||||
self.kodidb.add_album_17(kodi_id,
|
self.kodidb.add_album_17(kodi_id,
|
||||||
|
@ -303,7 +302,7 @@ class Album(MusicMixin, ItemBase):
|
||||||
thumb,
|
thumb,
|
||||||
api.music_studio(),
|
api.music_studio(),
|
||||||
userdata['UserRating'],
|
userdata['UserRating'],
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'album')
|
'album')
|
||||||
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
self.kodidb.add_albumartist(artist_id, kodi_id, api.artist_name())
|
||||||
self.kodidb.add_discography(artist_id, name, api.year())
|
self.kodidb.add_discography(artist_id, name, api.year())
|
||||||
|
@ -397,7 +396,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'single')
|
'single')
|
||||||
else:
|
else:
|
||||||
self.kodidb.add_album_17(kodi_id,
|
self.kodidb.add_album_17(kodi_id,
|
||||||
|
@ -411,7 +410,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
utils.unix_date_to_kodi(self.last_sync),
|
timing.unix_date_to_kodi(self.last_sync),
|
||||||
'single')
|
'single')
|
||||||
else:
|
else:
|
||||||
album = self.plexdb.album(album_id)
|
album = self.plexdb.album(album_id)
|
||||||
|
@ -469,8 +468,8 @@ class Song(MusicMixin, ItemBase):
|
||||||
mood = api.list_to_string(moods)
|
mood = api.list_to_string(moods)
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
do_indirect = not state.DIRECT_PATHS
|
do_indirect = not app.SYNC.direct_paths
|
||||||
if state.DIRECT_PATHS:
|
if app.SYNC.direct_paths:
|
||||||
# Direct paths is set the Kodi way
|
# Direct paths is set the Kodi way
|
||||||
playurl = api.file_path(force_first_media=True)
|
playurl = api.file_path(force_first_media=True)
|
||||||
if playurl is None:
|
if playurl is None:
|
||||||
|
@ -489,8 +488,7 @@ class Song(MusicMixin, ItemBase):
|
||||||
path = playurl.replace(filename, "")
|
path = playurl.replace(filename, "")
|
||||||
if do_indirect:
|
if do_indirect:
|
||||||
# Plex works a bit differently
|
# Plex works a bit differently
|
||||||
path = "%s%s" % (utils.window('pms_server'),
|
path = "%s%s" % (app.CONN.server, xml[0][0].get('key'))
|
||||||
xml[0][0].get('key'))
|
|
||||||
path = api.attach_plex_token_to_url(path)
|
path = api.attach_plex_token_to_url(path)
|
||||||
filename = path.rsplit('/', 1)[1]
|
filename = path.rsplit('/', 1)[1]
|
||||||
path = path.replace(filename, '')
|
path = path.replace(filename, '')
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
|
|
||||||
from .common import ItemBase, process_path
|
from .common import ItemBase, process_path
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import plex_functions as PF, state, variables as v
|
from .. import plex_functions as PF, app, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.tvshows')
|
LOG = getLogger('PLEX.tvshows')
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ class Show(ItemBase, TvShowMixin):
|
||||||
studio = api.list_to_string(studios)
|
studio = api.list_to_string(studios)
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
if state.DIRECT_PATHS:
|
if app.SYNC.direct_paths:
|
||||||
# Direct paths is set the Kodi way
|
# Direct paths is set the Kodi way
|
||||||
playurl = api.validate_playurl(api.tv_show_path(),
|
playurl = api.validate_playurl(api.tv_show_path(),
|
||||||
api.plex_type(),
|
api.plex_type(),
|
||||||
|
@ -378,8 +378,8 @@ class Episode(ItemBase, TvShowMixin):
|
||||||
parent_id = season['kodi_id']
|
parent_id = season['kodi_id']
|
||||||
|
|
||||||
# GET THE FILE AND PATH #####
|
# GET THE FILE AND PATH #####
|
||||||
do_indirect = not state.DIRECT_PATHS
|
do_indirect = not app.SYNC.direct_paths
|
||||||
if state.DIRECT_PATHS:
|
if app.SYNC.direct_paths:
|
||||||
playurl = api.file_path(force_first_media=True)
|
playurl = api.file_path(force_first_media=True)
|
||||||
if playurl is None:
|
if playurl is None:
|
||||||
do_indirect = True
|
do_indirect = True
|
||||||
|
@ -513,7 +513,7 @@ class Episode(ItemBase, TvShowMixin):
|
||||||
userdata['PlayCount'],
|
userdata['PlayCount'],
|
||||||
userdata['LastPlayedDate'],
|
userdata['LastPlayedDate'],
|
||||||
None) # Do send None, we check here
|
None) # Do send None, we check here
|
||||||
if not state.DIRECT_PATHS:
|
if not app.SYNC.direct_paths:
|
||||||
# need to set a SECOND file entry for a path without plex show id
|
# need to set a SECOND file entry for a path without plex show id
|
||||||
filename = api.file_name(force_first_media=True)
|
filename = api.file_name(force_first_media=True)
|
||||||
path = 'plugin://%s.tvshows/' % v.ADDON_ID
|
path = 'plugin://%s.tvshows/' % v.ADDON_ID
|
||||||
|
|
|
@ -8,7 +8,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from json import loads, dumps
|
from json import loads, dumps
|
||||||
from xbmc import executeJSONRPC
|
from xbmc import executeJSONRPC
|
||||||
|
|
||||||
from . import utils
|
from . import timing
|
||||||
|
|
||||||
|
|
||||||
class JsonRPC(object):
|
class JsonRPC(object):
|
||||||
|
@ -156,7 +156,7 @@ def seek_to(offset):
|
||||||
for playerid in get_player_ids():
|
for playerid in get_player_ids():
|
||||||
JsonRPC("Player.Seek").execute(
|
JsonRPC("Player.Seek").execute(
|
||||||
{"playerid": playerid,
|
{"playerid": playerid,
|
||||||
"value": utils.millis_to_kodi_time(offset)})
|
"value": timing.millis_to_kodi_time(offset)})
|
||||||
|
|
||||||
|
|
||||||
def smallforward():
|
def smallforward():
|
||||||
|
|
|
@ -7,7 +7,7 @@ from .video import KodiVideoDB
|
||||||
from .music import KodiMusicDB
|
from .music import KodiMusicDB
|
||||||
from .texture import KodiTextureDB
|
from .texture import KodiTextureDB
|
||||||
|
|
||||||
from .. import path_ops, utils, variables as v
|
from .. import path_ops, utils, timing, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodi_db')
|
LOG = getLogger('PLEX.kodi_db')
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ def setup_kodi_default_entries():
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
''', (v.DB_MUSIC_VERSION[v.KODIVERSION],
|
''', (v.DB_MUSIC_VERSION[v.KODIVERSION],
|
||||||
0,
|
0,
|
||||||
utils.unix_date_to_kodi(utils.unix_timestamp())))
|
timing.kodi_now()))
|
||||||
|
|
||||||
|
|
||||||
def reset_cached_images():
|
def reset_cached_images():
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
from . import common
|
from . import common
|
||||||
from .. import path_ops, utils, variables as v, state
|
from .. import path_ops, timing, variables as v, app
|
||||||
|
|
||||||
LOG = getLogger('PLEX.kodi_db.video')
|
LOG = getLogger('PLEX.kodi_db.video')
|
||||||
|
|
||||||
|
@ -74,12 +74,11 @@ class KodiVideoDB(common.KodiDBBase):
|
||||||
if pathid is None:
|
if pathid is None:
|
||||||
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
|
self.cursor.execute("SELECT COALESCE(MAX(idPath),0) FROM path")
|
||||||
pathid = self.cursor.fetchone()[0] + 1
|
pathid = self.cursor.fetchone()[0] + 1
|
||||||
datetime = utils.unix_date_to_kodi(utils.unix_timestamp())
|
|
||||||
self.cursor.execute('''
|
self.cursor.execute('''
|
||||||
INSERT INTO path(idPath, strPath, dateAdded)
|
INSERT INTO path(idPath, strPath, dateAdded)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
''',
|
''',
|
||||||
(pathid, parentpath, datetime))
|
(pathid, parentpath, timing.kodi_now()))
|
||||||
if parentpath != path:
|
if parentpath != path:
|
||||||
# In case we end up having media in the filesystem root, C:\
|
# In case we end up having media in the filesystem root, C:\
|
||||||
parent_id = self.parent_path_id(parentpath)
|
parent_id = self.parent_path_id(parentpath)
|
||||||
|
@ -198,7 +197,7 @@ class KodiVideoDB(common.KodiDBBase):
|
||||||
Passing plex_type = v.PLEX_TYPE_EPISODE deletes any secondary files for
|
Passing plex_type = v.PLEX_TYPE_EPISODE deletes any secondary files for
|
||||||
add-on paths
|
add-on paths
|
||||||
"""
|
"""
|
||||||
if not state.DIRECT_PATHS and plex_type == v.PLEX_TYPE_EPISODE:
|
if not app.SYNC.direct_paths and plex_type == v.PLEX_TYPE_EPISODE:
|
||||||
# Hack for the 2 entries for episodes for addon paths
|
# Hack for the 2 entries for episodes for addon paths
|
||||||
self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1',
|
self.cursor.execute('SELECT strFilename FROM files WHERE idFile = ? LIMIT 1',
|
||||||
(file_id, ))
|
(file_id, ))
|
||||||
|
@ -598,7 +597,7 @@ class KodiVideoDB(common.KodiDBBase):
|
||||||
Adds a resume marker for a video library item. Will even set 2,
|
Adds a resume marker for a video library item. Will even set 2,
|
||||||
considering add-on path widget hacks.
|
considering add-on path widget hacks.
|
||||||
"""
|
"""
|
||||||
if not state.DIRECT_PATHS and plex_type == v.PLEX_TYPE_EPISODE:
|
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,
|
# Need to make sure to set a SECOND bookmark entry for another,
|
||||||
# second file_id that points to the path .tvshows instead of
|
# second file_id that points to the path .tvshows instead of
|
||||||
# .tvshows/<plex show id/!
|
# .tvshows/<plex show id/!
|
||||||
|
|
|
@ -6,23 +6,16 @@ PKC Kodi Monitoring implementation
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from json import loads
|
from json import loads
|
||||||
from threading import Thread
|
|
||||||
import copy
|
import copy
|
||||||
import xbmc
|
import xbmc
|
||||||
from xbmcgui import Window
|
from xbmcgui import Window
|
||||||
|
|
||||||
from .plex_db import PlexDB
|
from .plex_db import PlexDB
|
||||||
from . import kodi_db
|
from . import kodi_db
|
||||||
from . import utils
|
|
||||||
from . import plex_functions as PF
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import playback
|
from . import utils, timing, plex_functions as PF, playback, initialsetup
|
||||||
from . import initialsetup
|
from . import json_rpc as js, playqueue as PQ, playlist_func as PL
|
||||||
from . import playqueue as PQ
|
from . import backgroundthread, app, variables as v
|
||||||
from . import json_rpc as js
|
|
||||||
from . import playlist_func as PL
|
|
||||||
from . import state
|
|
||||||
from . import variables as v
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -68,9 +61,9 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
self._already_slept = False
|
self._already_slept = False
|
||||||
self.hack_replay = None
|
self.hack_replay = None
|
||||||
xbmc.Monitor.__init__(self)
|
xbmc.Monitor.__init__(self)
|
||||||
for playerid in state.PLAYER_STATES:
|
for playerid in app.PLAYSTATE.player_states:
|
||||||
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
LOG.info("Kodi monitor started.")
|
LOG.info("Kodi monitor started.")
|
||||||
|
|
||||||
def onScanStarted(self, library):
|
def onScanStarted(self, library):
|
||||||
|
@ -142,8 +135,8 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
self.hack_replay = None
|
self.hack_replay = None
|
||||||
|
|
||||||
if method == "Player.OnPlay":
|
if method == "Player.OnPlay":
|
||||||
state.SUSPEND_SYNC = True
|
app.SYNC.suspend_sync = True
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self.PlayBackStart(data)
|
self.PlayBackStart(data)
|
||||||
elif method == "Player.OnStop":
|
elif method == "Player.OnStop":
|
||||||
# Should refresh our video nodes, e.g. on deck
|
# Should refresh our video nodes, e.g. on deck
|
||||||
|
@ -152,27 +145,27 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
self.hack_replay == data['item']):
|
self.hack_replay == data['item']):
|
||||||
# Hack for add-on paths
|
# Hack for add-on paths
|
||||||
self.hack_replay = None
|
self.hack_replay = None
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self._hack_addon_paths_replay_video()
|
self._hack_addon_paths_replay_video()
|
||||||
elif data.get('end'):
|
elif data.get('end'):
|
||||||
if state.PKC_CAUSED_STOP is True:
|
if app.PLAYSTATE.pkc_caused_stop is True:
|
||||||
state.PKC_CAUSED_STOP = False
|
app.PLAYSTATE.pkc_caused_stop = False
|
||||||
LOG.debug('PKC caused this playback stop - ignoring')
|
LOG.debug('PKC caused this playback stop - ignoring')
|
||||||
else:
|
else:
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
_playback_cleanup(ended=True)
|
_playback_cleanup(ended=True)
|
||||||
else:
|
else:
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
_playback_cleanup()
|
_playback_cleanup()
|
||||||
state.PKC_CAUSED_STOP_DONE = True
|
app.PLAYSTATE.pkc_caused_stop_done = True
|
||||||
state.SUSPEND_SYNC = False
|
app.SYNC.suspend_sync = False
|
||||||
elif method == 'Playlist.OnAdd':
|
elif method == 'Playlist.OnAdd':
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self._playlist_onadd(data)
|
self._playlist_onadd(data)
|
||||||
elif method == 'Playlist.OnRemove':
|
elif method == 'Playlist.OnRemove':
|
||||||
self._playlist_onremove(data)
|
self._playlist_onremove(data)
|
||||||
elif method == 'Playlist.OnClear':
|
elif method == 'Playlist.OnClear':
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self._playlist_onclear(data)
|
self._playlist_onclear(data)
|
||||||
elif method == "VideoLibrary.OnUpdate":
|
elif method == "VideoLibrary.OnUpdate":
|
||||||
# Manually marking as watched/unwatched
|
# Manually marking as watched/unwatched
|
||||||
|
@ -214,7 +207,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
utils.plex_command('RUN_LIB_SCAN', 'full')
|
utils.plex_command('RUN_LIB_SCAN', 'full')
|
||||||
elif method == "System.OnQuit":
|
elif method == "System.OnQuit":
|
||||||
LOG.info('Kodi OnQuit detected - shutting down')
|
LOG.info('Kodi OnQuit detected - shutting down')
|
||||||
state.STOP_PKC = True
|
app.APP.stop_pkc = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _hack_addon_paths_replay_video():
|
def _hack_addon_paths_replay_video():
|
||||||
|
@ -237,7 +230,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
(within the same micro-second!)
|
(within the same micro-second!)
|
||||||
"""
|
"""
|
||||||
LOG.info('Detected re-start of playback of last item')
|
LOG.info('Detected re-start of playback of last item')
|
||||||
old = state.OLD_PLAYER_STATES[1]
|
old = app.PLAYSTATE.old_player_states[1]
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'plex_id': old['plex_id'],
|
'plex_id': old['plex_id'],
|
||||||
'plex_type': old['plex_type'],
|
'plex_type': old['plex_type'],
|
||||||
|
@ -261,8 +254,8 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
"""
|
"""
|
||||||
if 'id' not in data['item']:
|
if 'id' not in data['item']:
|
||||||
return
|
return
|
||||||
old = state.OLD_PLAYER_STATES[data['playlistid']]
|
old = app.PLAYSTATE.old_player_states[data['playlistid']]
|
||||||
if (not state.DIRECT_PATHS and
|
if (not app.SYNC.direct_paths and
|
||||||
data['position'] == 0 and data['playlistid'] == 1 and
|
data['position'] == 0 and data['playlistid'] == 1 and
|
||||||
not PQ.PLAYQUEUES[data['playlistid']].items and
|
not PQ.PLAYQUEUES[data['playlistid']].items and
|
||||||
data['item']['type'] == old['kodi_type'] and
|
data['item']['type'] == old['kodi_type'] and
|
||||||
|
@ -399,7 +392,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
else:
|
else:
|
||||||
pos = info['position'] if info['position'] != -1 else 0
|
pos = info['position'] if info['position'] != -1 else 0
|
||||||
LOG.debug('Detected position %s for %s', pos, playqueue)
|
LOG.debug('Detected position %s for %s', pos, playqueue)
|
||||||
status = state.PLAYER_STATES[playerid]
|
status = app.PLAYSTATE.player_states[playerid]
|
||||||
try:
|
try:
|
||||||
item = playqueue.items[pos]
|
item = playqueue.items[pos]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
@ -431,7 +424,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path)
|
plex_id, plex_type = self._get_ids(kodi_id, kodi_type, path)
|
||||||
if not plex_id:
|
if not plex_id:
|
||||||
LOG.debug('No Plex id obtained - aborting playback report')
|
LOG.debug('No Plex id obtained - aborting playback report')
|
||||||
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
return
|
return
|
||||||
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
item = PL.init_plex_playqueue(playqueue, plex_id=plex_id)
|
||||||
item.file = path
|
item.file = path
|
||||||
|
@ -455,7 +448,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
else:
|
else:
|
||||||
container_key = '/library/metadata/%s' % plex_id
|
container_key = '/library/metadata/%s' % plex_id
|
||||||
# Remember that this player has been active
|
# Remember that this player has been active
|
||||||
state.ACTIVE_PLAYERS.add(playerid)
|
app.PLAYSTATE.active_players.add(playerid)
|
||||||
status.update(info)
|
status.update(info)
|
||||||
LOG.debug('Set the Plex container_key to: %s', container_key)
|
LOG.debug('Set the Plex container_key to: %s', container_key)
|
||||||
status['container_key'] = container_key
|
status['container_key'] = container_key
|
||||||
|
@ -469,8 +462,7 @@ class KodiMonitor(xbmc.Monitor):
|
||||||
LOG.debug('Set the player state: %s', status)
|
LOG.debug('Set the player state: %s', status)
|
||||||
|
|
||||||
|
|
||||||
@utils.thread_methods
|
class SpecialMonitor(backgroundthread.KillableThread):
|
||||||
class SpecialMonitor(Thread):
|
|
||||||
"""
|
"""
|
||||||
Detect the resume dialog for widgets.
|
Detect the resume dialog for widgets.
|
||||||
Could also be used to detect external players (see Emby implementation)
|
Could also be used to detect external players (see Emby implementation)
|
||||||
|
@ -480,15 +472,15 @@ class SpecialMonitor(Thread):
|
||||||
# "Start from beginning", "Play from beginning"
|
# "Start from beginning", "Play from beginning"
|
||||||
strings = (utils.try_encode(utils.lang(12021)),
|
strings = (utils.try_encode(utils.lang(12021)),
|
||||||
utils.try_encode(utils.lang(12023)))
|
utils.try_encode(utils.lang(12023)))
|
||||||
while not self.stopped():
|
while not self.isCanceled():
|
||||||
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
|
if xbmc.getCondVisibility('Window.IsVisible(DialogContextMenu.xml)'):
|
||||||
if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings:
|
if xbmc.getInfoLabel('Control.GetLabel(1002)') in strings:
|
||||||
# Remember that the item IS indeed resumable
|
# Remember that the item IS indeed resumable
|
||||||
control = int(Window(10106).getFocusId())
|
control = int(Window(10106).getFocusId())
|
||||||
state.RESUME_PLAYBACK = True if control == 1001 else False
|
app.PLAYSTATE.resume_playback = True if control == 1001 else False
|
||||||
else:
|
else:
|
||||||
# Different context menu is displayed
|
# Different context menu is displayed
|
||||||
state.RESUME_PLAYBACK = False
|
app.PLAYSTATE.resume_playback = False
|
||||||
if xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)'):
|
if xbmc.getCondVisibility('Window.IsVisible(MyVideoNav.xml)'):
|
||||||
path = xbmc.getInfoLabel('container.folderpath')
|
path = xbmc.getInfoLabel('container.folderpath')
|
||||||
if (isinstance(path, str) and
|
if (isinstance(path, str) and
|
||||||
|
@ -507,14 +499,14 @@ def _playback_cleanup(ended=False):
|
||||||
timing data otherwise)
|
timing data otherwise)
|
||||||
"""
|
"""
|
||||||
LOG.debug('playback_cleanup called. Active players: %s',
|
LOG.debug('playback_cleanup called. Active players: %s',
|
||||||
state.ACTIVE_PLAYERS)
|
app.PLAYSTATE.active_players)
|
||||||
# We might have saved a transient token from a user flinging media via
|
# We might have saved a transient token from a user flinging media via
|
||||||
# Companion (if we could not use the playqueue to store the token)
|
# Companion (if we could not use the playqueue to store the token)
|
||||||
state.PLEX_TRANSIENT_TOKEN = None
|
app.CONN.plex_transient_token = None
|
||||||
for playerid in state.ACTIVE_PLAYERS:
|
for playerid in app.PLAYSTATE.active_players:
|
||||||
status = state.PLAYER_STATES[playerid]
|
status = app.PLAYSTATE.player_states[playerid]
|
||||||
# Remember the last played item later
|
# Remember the last played item later
|
||||||
state.OLD_PLAYER_STATES[playerid] = copy.deepcopy(status)
|
app.PLAYSTATE.old_player_states[playerid] = copy.deepcopy(status)
|
||||||
# Stop transcoding
|
# Stop transcoding
|
||||||
if status['playmethod'] == 'Transcode':
|
if status['playmethod'] == 'Transcode':
|
||||||
LOG.debug('Tell the PMS to stop transcoding')
|
LOG.debug('Tell the PMS to stop transcoding')
|
||||||
|
@ -527,9 +519,9 @@ def _playback_cleanup(ended=False):
|
||||||
# started playback via PMS
|
# started playback via PMS
|
||||||
_record_playstate(status, ended)
|
_record_playstate(status, ended)
|
||||||
# Reset the player's status
|
# Reset the player's status
|
||||||
state.PLAYER_STATES[playerid] = copy.deepcopy(state.PLAYSTATE)
|
app.PLAYSTATE.player_states[playerid] = copy.deepcopy(app.PLAYSTATE.template)
|
||||||
# As all playback has halted, reset the players that have been active
|
# As all playback has halted, reset the players that have been active
|
||||||
state.ACTIVE_PLAYERS = set()
|
app.PLAYSTATE.active_players = set()
|
||||||
LOG.info('Finished PKC playback cleanup')
|
LOG.info('Finished PKC playback cleanup')
|
||||||
|
|
||||||
|
|
||||||
|
@ -543,12 +535,12 @@ def _record_playstate(status, ended):
|
||||||
# Item not (yet) in Kodi library
|
# Item not (yet) in Kodi library
|
||||||
LOG.debug('No playstate update due to Plex id not found: %s', status)
|
LOG.debug('No playstate update due to Plex id not found: %s', status)
|
||||||
return
|
return
|
||||||
totaltime = float(utils.kodi_time_to_millis(status['totaltime'])) / 1000
|
totaltime = float(timing.kodi_time_to_millis(status['totaltime'])) / 1000
|
||||||
if ended:
|
if ended:
|
||||||
progress = 0.99
|
progress = 0.99
|
||||||
time = v.IGNORE_SECONDS_AT_START + 1
|
time = v.IGNORE_SECONDS_AT_START + 1
|
||||||
else:
|
else:
|
||||||
time = float(utils.kodi_time_to_millis(status['time'])) / 1000
|
time = float(timing.kodi_time_to_millis(status['time'])) / 1000
|
||||||
try:
|
try:
|
||||||
progress = time / totaltime
|
progress = time / totaltime
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
|
@ -556,7 +548,7 @@ def _record_playstate(status, ended):
|
||||||
LOG.debug('Playback progress %s (%s of %s seconds)',
|
LOG.debug('Playback progress %s (%s of %s seconds)',
|
||||||
progress, time, totaltime)
|
progress, time, totaltime)
|
||||||
playcount = status['playcount']
|
playcount = status['playcount']
|
||||||
last_played = utils.unix_date_to_kodi(utils.unix_timestamp())
|
last_played = timing.now()
|
||||||
if playcount is None:
|
if playcount is None:
|
||||||
LOG.debug('playcount not found, looking it up in the Kodi DB')
|
LOG.debug('playcount not found, looking it up in the Kodi DB')
|
||||||
with kodi_db.KodiVideoDB() as kodidb:
|
with kodi_db.KodiVideoDB() as kodidb:
|
||||||
|
@ -582,7 +574,7 @@ def _record_playstate(status, ended):
|
||||||
last_played,
|
last_played,
|
||||||
status['plex_type'])
|
status['plex_type'])
|
||||||
# Hack to force "in progress" widget to appear if it wasn't visible before
|
# Hack to force "in progress" widget to appear if it wasn't visible before
|
||||||
if (state.FORCE_RELOAD_SKIN and
|
if (app.APP.force_reload_skin and
|
||||||
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
|
xbmc.getCondVisibility('Window.IsVisible(Home.xml)')):
|
||||||
LOG.debug('Refreshing skin to update widgets')
|
LOG.debug('Refreshing skin to update widgets')
|
||||||
xbmc.executebuiltin('ReloadSkin()')
|
xbmc.executebuiltin('ReloadSkin()')
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from .. import state
|
from .. import app
|
||||||
|
|
||||||
|
|
||||||
class libsync_mixin(object):
|
class libsync_mixin(object):
|
||||||
def isCanceled(self):
|
def isCanceled(self):
|
||||||
return (self._canceled or state.STOP_PKC or state.STOP_SYNC or
|
return (self._canceled or app.APP.stop_pkc or app.SYNC.stop_sync or
|
||||||
state.SUSPEND_LIBRARY_THREAD or state.SUSPEND_SYNC)
|
app.SYNC.suspend_library_thread or app.SYNC.suspend_sync)
|
||||||
|
|
||||||
|
|
||||||
def update_kodi_library(video=True, music=True):
|
def update_kodi_library(video=True, music=True):
|
||||||
|
|
|
@ -8,7 +8,7 @@ from ..plex_api import API
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from ..kodi_db import KodiVideoDB
|
from ..kodi_db import KodiVideoDB
|
||||||
from .. import backgroundthread, utils
|
from .. import backgroundthread, utils
|
||||||
from .. import itemtypes, plex_functions as PF, variables as v, state
|
from .. import itemtypes, plex_functions as PF, variables as v, app
|
||||||
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync.fanart')
|
LOG = getLogger('PLEX.sync.fanart')
|
||||||
|
@ -27,14 +27,11 @@ class FanartThread(backgroundthread.KillableThread):
|
||||||
self.refresh = refresh
|
self.refresh = refresh
|
||||||
super(FanartThread, self).__init__()
|
super(FanartThread, self).__init__()
|
||||||
|
|
||||||
def isCanceled(self):
|
|
||||||
return state.STOP_PKC
|
|
||||||
|
|
||||||
def isSuspended(self):
|
def isSuspended(self):
|
||||||
return (state.SUSPEND_LIBRARY_THREAD or
|
return (app.SYNC.suspend_library_thread or
|
||||||
state.STOP_SYNC or
|
app.SYNC.stop_sync or
|
||||||
state.DB_SCAN or
|
app.SYNC.db_scan or
|
||||||
state.SUSPEND_SYNC)
|
app.SYNC.suspend_sync)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
|
@ -80,8 +77,6 @@ class FanartTask(backgroundthread.Task, common.libsync_mixin):
|
||||||
self.refresh = refresh
|
self.refresh = refresh
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if self.isCanceled():
|
|
||||||
return
|
|
||||||
process_fanart(self.plex_id, self.plex_type, self.refresh)
|
process_fanart(self.plex_id, self.plex_type, self.refresh)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import xbmc
|
||||||
from .get_metadata import GetMetadataTask, reset_collections
|
from .get_metadata import GetMetadataTask, reset_collections
|
||||||
from .process_metadata import InitNewSection, UpdateLastSync, ProcessMetadata
|
from .process_metadata import InitNewSection, UpdateLastSync, ProcessMetadata
|
||||||
from . import common, sections
|
from . import common, sections
|
||||||
from .. import utils, backgroundthread, variables as v, state
|
from .. import utils, timing, backgroundthread, variables as v, app
|
||||||
from .. import plex_functions as PF, itemtypes
|
from .. import plex_functions as PF, itemtypes
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class FullSync(common.libsync_mixin):
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return False
|
return False
|
||||||
if not self.install_sync_done:
|
if not self.install_sync_done:
|
||||||
state.PATH_VERIFIED = False
|
app.SYNC.path_verified = False
|
||||||
try:
|
try:
|
||||||
# Sync new, updated and deleted items
|
# Sync new, updated and deleted items
|
||||||
iterator = PF.SectionItems(section['section_id'],
|
iterator = PF.SectionItems(section['section_id'],
|
||||||
|
@ -157,7 +157,7 @@ class FullSync(common.libsync_mixin):
|
||||||
(v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW, itemtypes.Season, False),
|
(v.PLEX_TYPE_SEASON, v.PLEX_TYPE_SHOW, itemtypes.Season, False),
|
||||||
(v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW, itemtypes.Episode, False)
|
(v.PLEX_TYPE_EPISODE, v.PLEX_TYPE_SHOW, itemtypes.Episode, False)
|
||||||
]
|
]
|
||||||
if state.ENABLE_MUSIC:
|
if app.SYNC.enable_music:
|
||||||
kinds.extend([
|
kinds.extend([
|
||||||
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
|
(v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ARTIST, itemtypes.Artist, False),
|
||||||
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST, itemtypes.Album, True),
|
(v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ARTIST, itemtypes.Album, True),
|
||||||
|
@ -181,7 +181,7 @@ class FullSync(common.libsync_mixin):
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
return
|
return
|
||||||
successful = False
|
successful = False
|
||||||
self.current_sync = utils.unix_timestamp()
|
self.current_sync = timing.unix_timestamp()
|
||||||
# Delete playlist and video node files from Kodi
|
# Delete playlist and video node files from Kodi
|
||||||
utils.delete_playlists()
|
utils.delete_playlists()
|
||||||
utils.delete_nodes()
|
utils.delete_nodes()
|
||||||
|
|
|
@ -102,8 +102,8 @@ class ProcessMetadata(backgroundthread.KillableThread, common.libsync_mixin):
|
||||||
while not self.isCanceled():
|
while not self.isCanceled():
|
||||||
if section is None:
|
if section is None:
|
||||||
break
|
break
|
||||||
LOG.debug('Start processing section %s: %s',
|
LOG.debug('Start processing section %s (%ss)',
|
||||||
section.plex_type, section.name)
|
section.name, section.plex_type)
|
||||||
self.current = 1
|
self.current = 1
|
||||||
self.processed = 0
|
self.processed = 0
|
||||||
self.total = section.total
|
self.total = section.total
|
||||||
|
|
|
@ -9,7 +9,7 @@ from ..utils import cast
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from .. import kodi_db
|
from .. import kodi_db
|
||||||
from .. import itemtypes
|
from .. import itemtypes
|
||||||
from .. import plex_functions as PF, music, utils, state, variables as v
|
from .. import plex_functions as PF, music, utils, variables as v, app
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync.sections')
|
LOG = getLogger('PLEX.sync.sections')
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ def sync_from_pms():
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
LOG.error("Error download PMS sections, abort")
|
LOG.error("Error download PMS sections, abort")
|
||||||
return False
|
return False
|
||||||
if state.DIRECT_PATHS is True and state.ENABLE_MUSIC is True:
|
if app.SYNC.direct_paths is True and app.SYNC.enable_music is True:
|
||||||
# Will reboot Kodi is new library detected
|
# Will reboot Kodi is new library detected
|
||||||
music.excludefromscan_music_folders(xml=sections)
|
music.excludefromscan_music_folders(xml=sections)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from logging import getLogger
|
||||||
|
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from .. import plex_functions as PF, utils, variables as v, state
|
from .. import plex_functions as PF, utils, timing, variables as v
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync.time')
|
LOG = getLogger('PLEX.sync.time')
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ def sync_pms_time():
|
||||||
LOG.debug('No timestamp; using 0')
|
LOG.debug('No timestamp; using 0')
|
||||||
timestamp = utils.cast(int, timestamp)
|
timestamp = utils.cast(int, timestamp)
|
||||||
# Set the timer
|
# Set the timer
|
||||||
koditime = utils.unix_timestamp()
|
koditime = timing.unix_timestamp()
|
||||||
# Toggle watched state
|
# Toggle watched state
|
||||||
PF.scrobble(plex_id, 'watched')
|
PF.scrobble(plex_id, 'watched')
|
||||||
# Let the PMS process this first!
|
# Let the PMS process this first!
|
||||||
|
@ -100,9 +100,9 @@ def sync_pms_time():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Calculate time offset Kodi-PMS
|
# Calculate time offset Kodi-PMS
|
||||||
state.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
|
timing.KODI_PLEX_TIME_OFFSET = float(koditime) - float(plextime)
|
||||||
utils.settings('kodiplextimeoffset',
|
utils.settings('kodiplextimeoffset',
|
||||||
value=str(state.KODI_PLEX_TIME_OFFSET))
|
value=str(timing.KODI_PLEX_TIME_OFFSET))
|
||||||
LOG.info("Time offset Koditime - Plextime in seconds: %s",
|
LOG.info("Time offset Koditime - Plextime in seconds: %s",
|
||||||
state.KODI_PLEX_TIME_OFFSET)
|
timing.KODI_PLEX_TIME_OFFSET)
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from ..utils import etree
|
from ..utils import etree
|
||||||
from .. import utils, path_ops, variables as v, state
|
from .. import utils, path_ops, variables as v, app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class VideoNodes(object):
|
||||||
delete=False):
|
delete=False):
|
||||||
# Plex: reassign mediatype due to Kodi inner workings
|
# Plex: reassign mediatype due to Kodi inner workings
|
||||||
# How many items do we get at most?
|
# How many items do we get at most?
|
||||||
limit = state.FETCH_PMS_ITEM_NUMBER
|
limit = unicode(app.APP.fetch_pms_item_number)
|
||||||
mediatypes = {
|
mediatypes = {
|
||||||
'movie': 'movies',
|
'movie': 'movies',
|
||||||
'show': 'tvshows',
|
'show': 'tvshows',
|
||||||
|
|
|
@ -10,7 +10,7 @@ from ..plex_api import API
|
||||||
from ..plex_db import PlexDB
|
from ..plex_db import PlexDB
|
||||||
from .. import kodi_db
|
from .. import kodi_db
|
||||||
from .. import backgroundthread, playlists, plex_functions as PF, itemtypes
|
from .. import backgroundthread, playlists, plex_functions as PF, itemtypes
|
||||||
from .. import artwork, utils, variables as v, state
|
from .. import artwork, utils, timing, variables as v, app
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync.websocket')
|
LOG = getLogger('PLEX.sync.websocket')
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ PLAYSTATE_SESSIONS = {}
|
||||||
|
|
||||||
|
|
||||||
def interrupt_processing():
|
def interrupt_processing():
|
||||||
return state.STOP_PKC or state.SUSPEND_LIBRARY_THREAD or state.STOP_SYNC
|
return app.APP.stop_pkc or app.SYNC.suspend_library_thread or app.SYNC.stop_sync
|
||||||
|
|
||||||
|
|
||||||
def multi_delete(input_list, delete_list):
|
def multi_delete(input_list, delete_list):
|
||||||
|
@ -75,7 +75,7 @@ def process_websocket_messages():
|
||||||
9: 'deleted'
|
9: 'deleted'
|
||||||
"""
|
"""
|
||||||
global WEBSOCKET_MESSAGES
|
global WEBSOCKET_MESSAGES
|
||||||
now = utils.unix_timestamp()
|
now = timing.unix_timestamp()
|
||||||
update_kodi_video_library, update_kodi_music_library = False, False
|
update_kodi_video_library, update_kodi_music_library = False, False
|
||||||
delete_list = []
|
delete_list = []
|
||||||
for i, message in enumerate(WEBSOCKET_MESSAGES):
|
for i, message in enumerate(WEBSOCKET_MESSAGES):
|
||||||
|
@ -84,7 +84,7 @@ def process_websocket_messages():
|
||||||
break
|
break
|
||||||
if message['state'] == 9:
|
if message['state'] == 9:
|
||||||
successful, video, music = process_delete_message(message)
|
successful, video, music = process_delete_message(message)
|
||||||
elif now - message['timestamp'] < state.BACKGROUNDSYNC_SAFTYMARGIN:
|
elif now - message['timestamp'] < app.SYNC.backgroundsync_saftymargin:
|
||||||
# We haven't waited long enough for the PMS to finish processing the
|
# We haven't waited long enough for the PMS to finish processing the
|
||||||
# item. Do it later (excepting deletions)
|
# item. Do it later (excepting deletions)
|
||||||
continue
|
continue
|
||||||
|
@ -127,7 +127,7 @@ def process_new_item_message(message):
|
||||||
LOG.error('Could not download metadata for %s', message['plex_id'])
|
LOG.error('Could not download metadata for %s', message['plex_id'])
|
||||||
return False, False, False
|
return False, False, False
|
||||||
LOG.debug("Processing new/updated PMS item: %s", message['plex_id'])
|
LOG.debug("Processing new/updated PMS item: %s", message['plex_id'])
|
||||||
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](utils.unix_timestamp()) as typus:
|
with itemtypes.ITEMTYPE_FROM_PLEXTYPE[plex_type](timing.unix_timestamp()) as typus:
|
||||||
typus.add_update(xml[0],
|
typus.add_update(xml[0],
|
||||||
section_name=xml.get('librarySectionTitle'),
|
section_name=xml.get('librarySectionTitle'),
|
||||||
section_id=xml.get('librarySectionID'))
|
section_id=xml.get('librarySectionID'))
|
||||||
|
@ -169,7 +169,7 @@ def store_timeline_message(data):
|
||||||
'state': status,
|
'state': status,
|
||||||
'plex_type': typus,
|
'plex_type': typus,
|
||||||
'plex_id': utils.cast(int, message['itemID']),
|
'plex_id': utils.cast(int, message['itemID']),
|
||||||
'timestamp': utils.unix_timestamp(),
|
'timestamp': timing.unix_timestamp(),
|
||||||
'attempt': 0
|
'attempt': 0
|
||||||
})
|
})
|
||||||
elif typus in (v.PLEX_TYPE_MOVIE,
|
elif typus in (v.PLEX_TYPE_MOVIE,
|
||||||
|
@ -186,7 +186,7 @@ def store_timeline_message(data):
|
||||||
'state': status,
|
'state': status,
|
||||||
'plex_type': typus,
|
'plex_type': typus,
|
||||||
'plex_id': plex_id,
|
'plex_id': plex_id,
|
||||||
'timestamp': utils.unix_timestamp(),
|
'timestamp': timing.unix_timestamp(),
|
||||||
'attempt': 0
|
'attempt': 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ def store_activity_message(data):
|
||||||
'state': None, # Don't need a state here
|
'state': None, # Don't need a state here
|
||||||
'plex_type': typus['plex_type'],
|
'plex_type': typus['plex_type'],
|
||||||
'plex_id': plex_id,
|
'plex_id': plex_id,
|
||||||
'timestamp': utils.unix_timestamp(),
|
'timestamp': timing.unix_timestamp(),
|
||||||
'attempt': 0
|
'attempt': 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ def process_playing(data):
|
||||||
plex_id = utils.cast(int, message['ratingKey'])
|
plex_id = utils.cast(int, message['ratingKey'])
|
||||||
skip = False
|
skip = False
|
||||||
for pid in (0, 1, 2):
|
for pid in (0, 1, 2):
|
||||||
if plex_id == state.PLAYER_STATES[pid]['plex_id']:
|
if plex_id == app.PLAYSTATE.player_states[pid]['plex_id']:
|
||||||
# Kodi is playing this message - no need to set the playstate
|
# Kodi is playing this message - no need to set the playstate
|
||||||
skip = True
|
skip = True
|
||||||
if skip:
|
if skip:
|
||||||
|
@ -265,7 +265,7 @@ def process_playing(data):
|
||||||
PLAYSTATE_SESSIONS[session_key] = {}
|
PLAYSTATE_SESSIONS[session_key] = {}
|
||||||
else:
|
else:
|
||||||
# PMS is ours - get all current sessions
|
# PMS is ours - get all current sessions
|
||||||
PLAYSTATE_SESSIONS.update(PF.GetPMSStatus(state.PLEX_TOKEN))
|
PLAYSTATE_SESSIONS.update(PF.GetPMSStatus(app.CONN.plex_token))
|
||||||
LOG.debug('Updated current sessions. They are: %s',
|
LOG.debug('Updated current sessions. They are: %s',
|
||||||
PLAYSTATE_SESSIONS)
|
PLAYSTATE_SESSIONS)
|
||||||
if session_key not in PLAYSTATE_SESSIONS:
|
if session_key not in PLAYSTATE_SESSIONS:
|
||||||
|
@ -281,18 +281,18 @@ def process_playing(data):
|
||||||
# Identify the user - same one as signed on with PKC? Skip
|
# Identify the user - same one as signed on with PKC? Skip
|
||||||
# update if neither session's username nor userid match
|
# update if neither session's username nor userid match
|
||||||
# (Owner sometime's returns id '1', not always)
|
# (Owner sometime's returns id '1', not always)
|
||||||
if not state.PLEX_TOKEN and session['userId'] == '1':
|
if not app.CONN.plex_token and session['userId'] == '1':
|
||||||
# PKC not signed in to plex.tv. Plus owner of PMS is
|
# PKC not signed in to plex.tv. Plus owner of PMS is
|
||||||
# playing (the '1').
|
# playing (the '1').
|
||||||
# Hence must be us (since several users require plex.tv
|
# Hence must be us (since several users require plex.tv
|
||||||
# token for PKC)
|
# token for PKC)
|
||||||
pass
|
pass
|
||||||
elif not (session['userId'] == state.PLEX_USER_ID or
|
elif not (session['userId'] == app.CONN.plex_user_id or
|
||||||
session['username'] == state.PLEX_USERNAME):
|
session['username'] == app.CONN.plex_username):
|
||||||
LOG.debug('Our username %s, userid %s did not match '
|
LOG.debug('Our username %s, userid %s did not match '
|
||||||
'the session username %s with userid %s',
|
'the session username %s with userid %s',
|
||||||
state.PLEX_USERNAME,
|
app.CONN.plex_username,
|
||||||
state.PLEX_USER_ID,
|
app.CONN.plex_user_id,
|
||||||
session['username'],
|
session['username'],
|
||||||
session['userId'])
|
session['userId'])
|
||||||
continue
|
continue
|
||||||
|
@ -334,7 +334,7 @@ def process_playing(data):
|
||||||
mark_played = False
|
mark_played = False
|
||||||
LOG.debug('Update playstate for user %s for %s with plex id %s to '
|
LOG.debug('Update playstate for user %s for %s with plex id %s to '
|
||||||
'viewCount %s, resume %s, mark_played %s',
|
'viewCount %s, resume %s, mark_played %s',
|
||||||
state.PLEX_USERNAME, session['kodi_type'], plex_id,
|
app.CONN.plex_username, session['kodi_type'], plex_id,
|
||||||
session['viewCount'], resume, mark_played)
|
session['viewCount'], resume, mark_played)
|
||||||
func = itemtypes.ITEMTYPE_FROM_KODITYPE[session['kodi_type']]
|
func = itemtypes.ITEMTYPE_FROM_KODITYPE[session['kodi_type']]
|
||||||
with func(None) as fkt:
|
with func(None) as fkt:
|
||||||
|
@ -343,7 +343,7 @@ def process_playing(data):
|
||||||
resume,
|
resume,
|
||||||
session['duration'],
|
session['duration'],
|
||||||
session['file_id'],
|
session['file_id'],
|
||||||
utils.unix_timestamp(),
|
timing.unix_timestamp(),
|
||||||
v.PLEX_TYPE_FROM_KODI_TYPE[session['kodi_type']])
|
v.PLEX_TYPE_FROM_KODI_TYPE[session['kodi_type']])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ from . import pickler
|
||||||
from .playutils import PlayUtils
|
from .playutils import PlayUtils
|
||||||
from .pkc_listitem import PKCListItem
|
from .pkc_listitem import PKCListItem
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playback')
|
LOG = getLogger('PLEX.playback')
|
||||||
|
@ -51,14 +51,14 @@ def playback_triage(plex_id=None, plex_type=None, path=None, resolve=True):
|
||||||
'resolve %s', plex_id, plex_type, path, resolve)
|
'resolve %s', plex_id, plex_type, path, resolve)
|
||||||
global RESOLVE
|
global RESOLVE
|
||||||
# If started via Kodi context menu, we never resolve
|
# If started via Kodi context menu, we never resolve
|
||||||
RESOLVE = resolve if not state.CONTEXT_MENU_PLAY else False
|
RESOLVE = resolve if not app.PLAYSTATE.context_menu_play else False
|
||||||
if not state.AUTHENTICATED:
|
if not app.CONN.authenticated:
|
||||||
LOG.error('Not yet authenticated for PMS, abort starting playback')
|
LOG.error('Not yet authenticated for PMS, abort starting playback')
|
||||||
# "Unauthorized for PMS"
|
# "Unauthorized for PMS"
|
||||||
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
|
utils.dialog('notification', utils.lang(29999), utils.lang(30017))
|
||||||
_ensure_resolve(abort=True)
|
_ensure_resolve(abort=True)
|
||||||
return
|
return
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
playqueue = PQ.get_playqueue_from_type(
|
playqueue = PQ.get_playqueue_from_type(
|
||||||
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
v.KODI_PLAYLIST_TYPE_FROM_PLEX_TYPE[plex_type])
|
||||||
try:
|
try:
|
||||||
|
@ -208,7 +208,7 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
if RESOLVE:
|
if RESOLVE:
|
||||||
# Sleep a bit to let setResolvedUrl do its thing - bit ugly
|
# Sleep a bit to let setResolvedUrl do its thing - bit ugly
|
||||||
sleep_timer = 0
|
sleep_timer = 0
|
||||||
while not state.PKC_CAUSED_STOP_DONE:
|
while not app.PLAYSTATE.pkc_caused_stop_done:
|
||||||
sleep(50)
|
sleep(50)
|
||||||
sleep_timer += 1
|
sleep_timer += 1
|
||||||
if sleep_timer > 100:
|
if sleep_timer > 100:
|
||||||
|
@ -229,19 +229,19 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
|
||||||
utils.lang(30128),
|
utils.lang(30128),
|
||||||
icon='{error}')
|
icon='{error}')
|
||||||
# Do NOT use _ensure_resolve() because we resolved above already
|
# Do NOT use _ensure_resolve() because we resolved above already
|
||||||
state.CONTEXT_MENU_PLAY = False
|
app.PLAYSTATE.context_menu_play = False
|
||||||
state.FORCE_TRANSCODE = False
|
app.PLAYSTATE.force_transcode = False
|
||||||
state.RESUME_PLAYBACK = False
|
app.PLAYSTATE.resume_playback = False
|
||||||
return
|
return
|
||||||
PL.get_playlist_details_from_xml(playqueue, xml)
|
PL.get_playlist_details_from_xml(playqueue, xml)
|
||||||
stack = _prep_playlist_stack(xml)
|
stack = _prep_playlist_stack(xml)
|
||||||
_process_stack(playqueue, stack)
|
_process_stack(playqueue, stack)
|
||||||
# Always resume if playback initiated via PMS and there IS a resume
|
# Always resume if playback initiated via PMS and there IS a resume
|
||||||
# point
|
# point
|
||||||
offset = api.resume_point() * 1000 if state.CONTEXT_MENU_PLAY else None
|
offset = api.resume_point() * 1000 if app.PLAYSTATE.context_menu_play else None
|
||||||
# Reset some playback variables
|
# Reset some playback variables
|
||||||
state.CONTEXT_MENU_PLAY = False
|
app.PLAYSTATE.context_menu_play = False
|
||||||
state.FORCE_TRANSCODE = False
|
app.PLAYSTATE.force_transcode = False
|
||||||
# New thread to release this one sooner (e.g. harddisk spinning up)
|
# New thread to release this one sooner (e.g. harddisk spinning up)
|
||||||
thread = Thread(target=threaded_playback,
|
thread = Thread(target=threaded_playback,
|
||||||
args=(playqueue.kodi_pl, pos, offset))
|
args=(playqueue.kodi_pl, pos, offset))
|
||||||
|
@ -272,8 +272,8 @@ def _ensure_resolve(abort=False):
|
||||||
LOG.debug('Passing dummy path to Kodi')
|
LOG.debug('Passing dummy path to Kodi')
|
||||||
# if not state.CONTEXT_MENU_PLAY:
|
# if not state.CONTEXT_MENU_PLAY:
|
||||||
# Because playback won't start with context menu play
|
# Because playback won't start with context menu play
|
||||||
state.PKC_CAUSED_STOP = True
|
app.PLAYSTATE.pkc_caused_stop = True
|
||||||
state.PKC_CAUSED_STOP_DONE = False
|
app.PLAYSTATE.pkc_caused_stop_done = False
|
||||||
if not abort:
|
if not abort:
|
||||||
result = pickler.Playback_Successful()
|
result = pickler.Playback_Successful()
|
||||||
result.listitem = PKCListItem(path=v.NULL_VIDEO)
|
result.listitem = PKCListItem(path=v.NULL_VIDEO)
|
||||||
|
@ -283,9 +283,9 @@ def _ensure_resolve(abort=False):
|
||||||
pickler.pickle_me(None)
|
pickler.pickle_me(None)
|
||||||
if abort:
|
if abort:
|
||||||
# Reset some playback variables
|
# Reset some playback variables
|
||||||
state.CONTEXT_MENU_PLAY = False
|
app.PLAYSTATE.context_menu_play = False
|
||||||
state.FORCE_TRANSCODE = False
|
app.PLAYSTATE.force_transcode = False
|
||||||
state.RESUME_PLAYBACK = False
|
app.PLAYSTATE.resume_playback = False
|
||||||
|
|
||||||
|
|
||||||
def _init_existing_kodi_playlist(playqueue, pos):
|
def _init_existing_kodi_playlist(playqueue, pos):
|
||||||
|
@ -299,7 +299,7 @@ def _init_existing_kodi_playlist(playqueue, pos):
|
||||||
LOG.error('No Kodi items returned')
|
LOG.error('No Kodi items returned')
|
||||||
raise PL.PlaylistError('No Kodi items returned')
|
raise PL.PlaylistError('No Kodi items returned')
|
||||||
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
|
item = PL.init_plex_playqueue(playqueue, kodi_item=kodi_items[pos])
|
||||||
item.force_transcode = state.FORCE_TRANSCODE
|
item.force_transcode = app.PLAYSTATE.force_transcode
|
||||||
# playqueue.py will add the rest - this will likely put the PMS under
|
# playqueue.py will add the rest - this will likely put the PMS under
|
||||||
# a LOT of strain if the following Kodi setting is enabled:
|
# a LOT of strain if the following Kodi setting is enabled:
|
||||||
# Settings -> Player -> Videos -> Play next video automatically
|
# Settings -> Player -> Videos -> Play next video automatically
|
||||||
|
@ -310,7 +310,7 @@ def _prep_playlist_stack(xml):
|
||||||
stack = []
|
stack = []
|
||||||
for item in xml:
|
for item in xml:
|
||||||
api = API(item)
|
api = API(item)
|
||||||
if (state.CONTEXT_MENU_PLAY is False and
|
if (app.PLAYSTATE.context_menu_play is False and
|
||||||
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
api.plex_type() not in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_EPISODE)):
|
||||||
# If user chose to play via PMS or force transcode, do not
|
# If user chose to play via PMS or force transcode, do not
|
||||||
# use the item path stored in the Kodi DB
|
# use the item path stored in the Kodi DB
|
||||||
|
@ -378,7 +378,7 @@ def _process_stack(playqueue, stack):
|
||||||
playlist_item.offset = item['offset']
|
playlist_item.offset = item['offset']
|
||||||
playlist_item.part = item['part']
|
playlist_item.part = item['part']
|
||||||
playlist_item.id = item['id']
|
playlist_item.id = item['id']
|
||||||
playlist_item.force_transcode = state.FORCE_TRANSCODE
|
playlist_item.force_transcode = app.PLAYSTATE.force_transcode
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -413,7 +413,7 @@ def _conclude_playback(playqueue, pos):
|
||||||
playurl = item.file
|
playurl = item.file
|
||||||
if not playurl:
|
if not playurl:
|
||||||
LOG.info('Did not get a playurl, aborting playback silently')
|
LOG.info('Did not get a playurl, aborting playback silently')
|
||||||
state.RESUME_PLAYBACK = False
|
app.PLAYSTATE.resume_playback = False
|
||||||
pickler.pickle_me(result)
|
pickler.pickle_me(result)
|
||||||
return
|
return
|
||||||
listitem.setPath(utils.try_encode(playurl))
|
listitem.setPath(utils.try_encode(playurl))
|
||||||
|
@ -422,8 +422,8 @@ def _conclude_playback(playqueue, pos):
|
||||||
elif item.playmethod == 'Transcode':
|
elif item.playmethod == 'Transcode':
|
||||||
playutils.audio_subtitle_prefs(listitem)
|
playutils.audio_subtitle_prefs(listitem)
|
||||||
|
|
||||||
if state.RESUME_PLAYBACK is True:
|
if app.PLAYSTATE.resume_playback is True:
|
||||||
state.RESUME_PLAYBACK = False
|
app.PLAYSTATE.resume_playback = False
|
||||||
if (item.offset is None and
|
if (item.offset is None and
|
||||||
item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)):
|
item.plex_type not in (v.PLEX_TYPE_SONG, v.PLEX_TYPE_CLIP)):
|
||||||
with PlexDB() as plexdb:
|
with PlexDB() as plexdb:
|
||||||
|
|
|
@ -10,7 +10,7 @@ from . import playback
|
||||||
from . import context_entry
|
from . import context_entry
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import pickler
|
from . import pickler
|
||||||
from . import state
|
from . import app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class PlaybackStarter(Thread):
|
||||||
kodi_type=params.get('kodi_type'))
|
kodi_type=params.get('kodi_type'))
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
queue = state.COMMAND_PIPELINE_QUEUE
|
queue = app.APP.command_pipeline_queue
|
||||||
LOG.info("----===## Starting PlaybackStarter ##===----")
|
LOG.info("----===## Starting PlaybackStarter ##===----")
|
||||||
while True:
|
while True:
|
||||||
item = queue.get()
|
item = queue.get()
|
||||||
|
|
|
@ -16,7 +16,7 @@ from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -357,7 +357,7 @@ def verify_kodi_item(plex_id, kodi_item):
|
||||||
# Got all the info we need
|
# Got all the info we need
|
||||||
return kodi_item
|
return kodi_item
|
||||||
# Special case playlist startup - got type but no id
|
# Special case playlist startup - got type but no id
|
||||||
if (not state.DIRECT_PATHS and state.ENABLE_MUSIC and
|
if (not app.SYNC.direct_paths and app.SYNC.enable_music and
|
||||||
kodi_item.get('type') == v.KODI_TYPE_SONG and
|
kodi_item.get('type') == v.KODI_TYPE_SONG and
|
||||||
kodi_item['file'].startswith('http')):
|
kodi_item['file'].startswith('http')):
|
||||||
kodi_item['id'], _ = kodiid_from_filename(kodi_item['file'],
|
kodi_item['id'], _ = kodiid_from_filename(kodi_item['file'],
|
||||||
|
|
|
@ -19,7 +19,7 @@ from . import pms, db, kodi_pl, plex_pl
|
||||||
|
|
||||||
from ..watchdog import events
|
from ..watchdog import events
|
||||||
from ..plex_api import API
|
from ..plex_api import API
|
||||||
from .. import utils, path_ops, variables as v, state
|
from .. import utils, path_ops, variables as v, app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playlists')
|
LOG = getLogger('PLEX.playlists')
|
||||||
|
@ -92,7 +92,7 @@ def websocket(plex_id, status):
|
||||||
* 9: 'deleted'
|
* 9: 'deleted'
|
||||||
"""
|
"""
|
||||||
create = False
|
create = False
|
||||||
with state.LOCK_PLAYLISTS:
|
with app.APP.lock_playlists:
|
||||||
playlist = db.get_playlist(plex_id=plex_id)
|
playlist = db.get_playlist(plex_id=plex_id)
|
||||||
if plex_id in IGNORE_PLEX_PLAYLIST_CHANGE:
|
if plex_id in IGNORE_PLEX_PLAYLIST_CHANGE:
|
||||||
LOG.debug('Ignoring detected Plex playlist change for %s',
|
LOG.debug('Ignoring detected Plex playlist change for %s',
|
||||||
|
@ -155,7 +155,7 @@ def full_sync():
|
||||||
fetch the PMS playlists)
|
fetch the PMS playlists)
|
||||||
"""
|
"""
|
||||||
LOG.info('Starting playlist full sync')
|
LOG.info('Starting playlist full sync')
|
||||||
with state.LOCK_PLAYLISTS:
|
with app.APP.lock_playlists:
|
||||||
# Need to lock because we're messing with playlists
|
# Need to lock because we're messing with playlists
|
||||||
return _full_sync()
|
return _full_sync()
|
||||||
|
|
||||||
|
@ -283,7 +283,7 @@ def sync_kodi_playlist(path):
|
||||||
return False
|
return False
|
||||||
if extension not in SUPPORTED_FILETYPES:
|
if extension not in SUPPORTED_FILETYPES:
|
||||||
return False
|
return False
|
||||||
if not state.SYNC_SPECIFIC_KODI_PLAYLISTS:
|
if not app.SYNC.sync_specific_kodi_playlists:
|
||||||
return True
|
return True
|
||||||
playlist = Playlist()
|
playlist = Playlist()
|
||||||
playlist.kodi_path = path
|
playlist.kodi_path = path
|
||||||
|
@ -341,10 +341,10 @@ def sync_plex_playlist(playlist=None, xml=None, plex_id=None):
|
||||||
return False
|
return False
|
||||||
name = api.title()
|
name = api.title()
|
||||||
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
|
typus = v.KODI_PLAYLIST_TYPE_FROM_PLEX[api.playlist_type()]
|
||||||
if (not state.ENABLE_MUSIC and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
|
if (not app.SYNC.enable_music and typus == v.PLEX_PLAYLIST_TYPE_AUDIO):
|
||||||
LOG.debug('Not synching Plex audio playlist')
|
LOG.debug('Not synching Plex audio playlist')
|
||||||
return False
|
return False
|
||||||
if not state.SYNC_SPECIFIC_PLEX_PLAYLISTS:
|
if not app.SYNC.sync_specific_plex_playlists:
|
||||||
return True
|
return True
|
||||||
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
|
prefix = utils.settings('syncSpecificPlexPlaylistsPrefix').lower()
|
||||||
if name and name.lower().startswith(prefix):
|
if name and name.lower().startswith(prefix):
|
||||||
|
@ -387,7 +387,7 @@ class PlaylistEventhandler(events.FileSystemEventHandler):
|
||||||
events.EVENT_TYPE_CREATED: self.on_created,
|
events.EVENT_TYPE_CREATED: self.on_created,
|
||||||
events.EVENT_TYPE_DELETED: self.on_deleted,
|
events.EVENT_TYPE_DELETED: self.on_deleted,
|
||||||
}
|
}
|
||||||
with state.LOCK_PLAYLISTS:
|
with app.APP.lock_playlists:
|
||||||
_method_map[event.event_type](event)
|
_method_map[event.event_type](event)
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
|
|
|
@ -5,16 +5,11 @@ Monitors the Kodi playqueue and adjusts the Plex playqueue accordingly
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from threading import Thread
|
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from . import utils
|
|
||||||
from . import playlist_func as PL
|
|
||||||
from . import plex_functions as PF
|
|
||||||
from .plex_api import API
|
from .plex_api import API
|
||||||
from . import json_rpc as js
|
from . import playlist_func as PL, plex_functions as PF
|
||||||
from . import variables as v
|
from . import backgroundthread, utils, json_rpc as js, app, variables as v
|
||||||
from . import state
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.playqueue')
|
LOG = getLogger('PLEX.playqueue')
|
||||||
|
@ -35,7 +30,7 @@ def init_playqueues():
|
||||||
LOG.debug('Playqueues have already been initialized')
|
LOG.debug('Playqueues have already been initialized')
|
||||||
return
|
return
|
||||||
# Initialize Kodi playqueues
|
# Initialize Kodi playqueues
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
for i in (0, 1, 2):
|
for i in (0, 1, 2):
|
||||||
# Just in case the Kodi response is not sorted correctly
|
# Just in case the Kodi response is not sorted correctly
|
||||||
for queue in js.get_playlists():
|
for queue in js.get_playlists():
|
||||||
|
@ -96,13 +91,18 @@ def init_playqueue_from_plex_children(plex_id, transient_token=None):
|
||||||
return playqueue
|
return playqueue
|
||||||
|
|
||||||
|
|
||||||
@utils.thread_methods(add_suspends=['PMS_STATUS'])
|
class PlayqueueMonitor(backgroundthread.KillableThread):
|
||||||
class PlayqueueMonitor(Thread):
|
|
||||||
"""
|
"""
|
||||||
Unfortunately, Kodi does not tell if items within a Kodi playqueue
|
Unfortunately, Kodi does not tell if items within a Kodi playqueue
|
||||||
(playlist) are swapped. This is what this monitor is for. Don't replace
|
(playlist) are swapped. This is what this monitor is for. Don't replace
|
||||||
this mechanism till Kodi's implementation of playlists has improved
|
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.CONN.pms_status
|
||||||
|
|
||||||
def _compare_playqueues(self, playqueue, new):
|
def _compare_playqueues(self, playqueue, new):
|
||||||
"""
|
"""
|
||||||
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
Used to poll the Kodi playqueue and update the Plex playqueue if needed
|
||||||
|
@ -117,7 +117,7 @@ class PlayqueueMonitor(Thread):
|
||||||
# Ignore new media added by other addons
|
# Ignore new media added by other addons
|
||||||
continue
|
continue
|
||||||
for j, old_item in enumerate(old):
|
for j, old_item in enumerate(old):
|
||||||
if self.stopped():
|
if self.isCanceled():
|
||||||
# Chances are that we got an empty Kodi playlist due to
|
# Chances are that we got an empty Kodi playlist due to
|
||||||
# Kodi exit
|
# Kodi exit
|
||||||
return
|
return
|
||||||
|
@ -178,7 +178,7 @@ class PlayqueueMonitor(Thread):
|
||||||
for j in range(i, len(index)):
|
for j in range(i, len(index)):
|
||||||
index[j] += 1
|
index[j] += 1
|
||||||
for i in reversed(index):
|
for i in reversed(index):
|
||||||
if self.stopped():
|
if self.isCanceled():
|
||||||
# Chances are that we got an empty Kodi playlist due to
|
# Chances are that we got an empty Kodi playlist due to
|
||||||
# Kodi exit
|
# Kodi exit
|
||||||
return
|
return
|
||||||
|
@ -192,20 +192,18 @@ class PlayqueueMonitor(Thread):
|
||||||
LOG.debug('Done comparing playqueues')
|
LOG.debug('Done comparing playqueues')
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
stopped = self.stopped
|
|
||||||
suspended = self.suspended
|
|
||||||
LOG.info("----===## Starting PlayqueueMonitor ##===----")
|
LOG.info("----===## Starting PlayqueueMonitor ##===----")
|
||||||
while not stopped():
|
while not self.isCanceled():
|
||||||
while suspended():
|
while self.isSuspended():
|
||||||
if stopped():
|
if self.isCanceled():
|
||||||
break
|
break
|
||||||
xbmc.sleep(1000)
|
xbmc.sleep(1000)
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
for playqueue in PLAYQUEUES:
|
for playqueue in PLAYQUEUES:
|
||||||
kodi_pl = js.playlist_get_items(playqueue.playlistid)
|
kodi_pl = js.playlist_get_items(playqueue.playlistid)
|
||||||
if playqueue.old_kodi_pl != kodi_pl:
|
if playqueue.old_kodi_pl != kodi_pl:
|
||||||
if playqueue.id is None and (not state.DIRECT_PATHS or
|
if playqueue.id is None and (not app.PLAYSTATE.direct_paths or
|
||||||
state.CONTEXT_MENU_PLAY):
|
app.PLAYSTATE.context_menu_play):
|
||||||
# Only initialize if directly fired up using direct
|
# Only initialize if directly fired up using direct
|
||||||
# paths. Otherwise let default.py do its magic
|
# paths. Otherwise let default.py do its magic
|
||||||
LOG.debug('Not yet initiating playback')
|
LOG.debug('Not yet initiating playback')
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils
|
from . import utils, app
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -304,7 +304,7 @@ class PlayUtils():
|
||||||
# We don't know the language - no need to download
|
# We don't know the language - no need to download
|
||||||
else:
|
else:
|
||||||
path = self.api.attach_plex_token_to_url(
|
path = self.api.attach_plex_token_to_url(
|
||||||
"%s%s" % (utils.window('pms_server'),
|
"%s%s" % (app.CONN.server,
|
||||||
stream.attrib['key']))
|
stream.attrib['key']))
|
||||||
downloadable_streams.append(index)
|
downloadable_streams.append(index)
|
||||||
download_subs.append(utils.try_encode(path))
|
download_subs.append(utils.try_encode(path))
|
||||||
|
|
|
@ -42,11 +42,11 @@ from .kodi_db import KodiVideoDB, KodiMusicDB
|
||||||
from .utils import cast
|
from .utils import cast
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import clientinfo
|
from . import clientinfo
|
||||||
from . import utils
|
from . import utils, timing
|
||||||
from . import path_ops
|
from . import path_ops
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.plex_api')
|
LOG = getLogger('PLEX.plex_api')
|
||||||
|
@ -121,7 +121,7 @@ class API(object):
|
||||||
Pass direct_path=True if you're calling from another Plex python
|
Pass direct_path=True if you're calling from another Plex python
|
||||||
instance - because otherwise direct paths will evaluate to False!
|
instance - because otherwise direct paths will evaluate to False!
|
||||||
"""
|
"""
|
||||||
direct_paths = direct_paths or state.DIRECT_PATHS
|
direct_paths = direct_paths or app.SYNC.direct_paths
|
||||||
filename = self.file_path(force_first_media=force_first_media)
|
filename = self.file_path(force_first_media=force_first_media)
|
||||||
if (not direct_paths or force_addon or
|
if (not direct_paths or force_addon or
|
||||||
self.plex_type() == v.PLEX_TYPE_CLIP):
|
self.plex_type() == v.PLEX_TYPE_CLIP):
|
||||||
|
@ -219,15 +219,15 @@ class API(object):
|
||||||
extension not in v.KODI_SUPPORTED_IMAGES):
|
extension not in v.KODI_SUPPORTED_IMAGES):
|
||||||
# Let Plex transcode
|
# Let Plex transcode
|
||||||
# max width/height supported by plex image transcoder is 1920x1080
|
# max width/height supported by plex image transcoder is 1920x1080
|
||||||
path = state.PMS_SERVER + PF.transcode_image_path(
|
path = app.CONN.server + PF.transcode_image_path(
|
||||||
self.item[0][0].get('key'),
|
self.item[0][0].get('key'),
|
||||||
state.PMS_TOKEN,
|
app.CONN.pms_token,
|
||||||
"%s%s" % (state.PMS_SERVER, self.item[0][0].get('key')),
|
"%s%s" % (app.CONN.server, self.item[0][0].get('key')),
|
||||||
1920,
|
1920,
|
||||||
1080)
|
1080)
|
||||||
else:
|
else:
|
||||||
path = self.attach_plex_token_to_url(
|
path = self.attach_plex_token_to_url(
|
||||||
'%s%s' % (state.PMS_SERVER, self.item[0][0].attrib['key']))
|
'%s%s' % (app.CONN.server, self.item[0][0].attrib['key']))
|
||||||
# Attach Plex id to url to let it be picked up by our playqueue agent
|
# Attach Plex id to url to let it be picked up by our playqueue agent
|
||||||
# later
|
# later
|
||||||
return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id()))
|
return utils.try_encode('%s&plex_id=%s' % (path, self.plex_id()))
|
||||||
|
@ -263,10 +263,9 @@ class API(object):
|
||||||
"""
|
"""
|
||||||
res = self.item.get('addedAt')
|
res = self.item.get('addedAt')
|
||||||
if res is not None:
|
if res is not None:
|
||||||
res = utils.unix_date_to_kodi(res)
|
return timing.plex_date_to_kodi(res)
|
||||||
else:
|
else:
|
||||||
res = '2000-01-01 10:00:00'
|
return '2000-01-01 10:00:00'
|
||||||
return res
|
|
||||||
|
|
||||||
def viewcount(self):
|
def viewcount(self):
|
||||||
"""
|
"""
|
||||||
|
@ -300,11 +299,11 @@ class API(object):
|
||||||
played = True if playcount else False
|
played = True if playcount else False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
last_played = utils.unix_date_to_kodi(int(item['lastViewedAt']))
|
last_played = utils.plex_date_to_kodi(int(item['lastViewedAt']))
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
last_played = None
|
last_played = None
|
||||||
|
|
||||||
if state.INDICATE_MEDIA_VERSIONS is True:
|
if app.SYNC.indicate_media_versions is True:
|
||||||
userrating = 0
|
userrating = 0
|
||||||
for _ in self.item.findall('./Media'):
|
for _ in self.item.findall('./Media'):
|
||||||
userrating += 1
|
userrating += 1
|
||||||
|
@ -685,12 +684,12 @@ class API(object):
|
||||||
|
|
||||||
url may or may not already contain a '?'
|
url may or may not already contain a '?'
|
||||||
"""
|
"""
|
||||||
if not state.PMS_TOKEN:
|
if not app.CONN.pms_token:
|
||||||
return url
|
return url
|
||||||
if '?' not in url:
|
if '?' not in url:
|
||||||
url = "%s?X-Plex-Token=%s" % (url, state.PMS_TOKEN)
|
url = "%s?X-Plex-Token=%s" % (url, app.CONN.pms_token)
|
||||||
else:
|
else:
|
||||||
url = "%s&X-Plex-Token=%s" % (url, state.PMS_TOKEN)
|
url = "%s&X-Plex-Token=%s" % (url, app.CONN.pms_token)
|
||||||
return url
|
return url
|
||||||
|
|
||||||
def item_id(self):
|
def item_id(self):
|
||||||
|
@ -770,7 +769,7 @@ class API(object):
|
||||||
for extras in self.item.iterfind('Extras'):
|
for extras in self.item.iterfind('Extras'):
|
||||||
# There will always be only 1 extras element
|
# There will always be only 1 extras element
|
||||||
if (len(extras) > 0 and
|
if (len(extras) > 0 and
|
||||||
state.SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER):
|
app.SYNC.show_extras_instead_of_playing_trailer):
|
||||||
return ('plugin://%s?mode=route_to_extras&plex_id=%s'
|
return ('plugin://%s?mode=route_to_extras&plex_id=%s'
|
||||||
% (v.ADDON_ID, self.plex_id()))
|
% (v.ADDON_ID, self.plex_id()))
|
||||||
for extra in extras:
|
for extra in extras:
|
||||||
|
@ -888,7 +887,7 @@ class API(object):
|
||||||
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
artwork = '%s?width=%s&height=%s' % (artwork, width, height)
|
||||||
artwork = ('%s/photo/:/transcode?width=3840&height=3840&'
|
artwork = ('%s/photo/:/transcode?width=3840&height=3840&'
|
||||||
'minSize=1&upscale=0&url=%s'
|
'minSize=1&upscale=0&url=%s'
|
||||||
% (state.PMS_SERVER, quote(artwork)))
|
% (app.CONN.server, quote(artwork)))
|
||||||
artwork = self.attach_plex_token_to_url(artwork)
|
artwork = self.attach_plex_token_to_url(artwork)
|
||||||
return artwork
|
return artwork
|
||||||
|
|
||||||
|
@ -1406,7 +1405,7 @@ class API(object):
|
||||||
# trailers are 'clip' with PMS xmls
|
# trailers are 'clip' with PMS xmls
|
||||||
if action == "DirectStream":
|
if action == "DirectStream":
|
||||||
path = self.item[self.mediastream][self.part].attrib['key']
|
path = self.item[self.mediastream][self.part].attrib['key']
|
||||||
url = state.PMS_SERVER + path
|
url = app.CONN.server + path
|
||||||
# e.g. Trailers already feature an '?'!
|
# e.g. Trailers already feature an '?'!
|
||||||
if '?' in url:
|
if '?' in url:
|
||||||
url += '&' + urlencode(xargs)
|
url += '&' + urlencode(xargs)
|
||||||
|
@ -1423,7 +1422,7 @@ class API(object):
|
||||||
}
|
}
|
||||||
# Path/key to VIDEO item of xml PMS response is needed, not part
|
# Path/key to VIDEO item of xml PMS response is needed, not part
|
||||||
path = self.item.attrib['key']
|
path = self.item.attrib['key']
|
||||||
transcode_path = state.PMS_SERVER + \
|
transcode_path = app.CONN.server + \
|
||||||
'/video/:/transcode/universal/start.m3u8?'
|
'/video/:/transcode/universal/start.m3u8?'
|
||||||
args = {
|
args = {
|
||||||
'audioBoost': utils.settings('audioBoost'),
|
'audioBoost': utils.settings('audioBoost'),
|
||||||
|
@ -1481,7 +1480,7 @@ class API(object):
|
||||||
# We don't know the language - no need to download
|
# We don't know the language - no need to download
|
||||||
else:
|
else:
|
||||||
path = self.attach_plex_token_to_url(
|
path = self.attach_plex_token_to_url(
|
||||||
"%s%s" % (state.PMS_SERVER, key))
|
"%s%s" % (app.CONN.server, key))
|
||||||
externalsubs.append(path)
|
externalsubs.append(path)
|
||||||
kodiindex += 1
|
kodiindex += 1
|
||||||
LOG.info('Found external subs: %s', externalsubs)
|
LOG.info('Found external subs: %s', externalsubs)
|
||||||
|
@ -1735,16 +1734,16 @@ class API(object):
|
||||||
if path is None:
|
if path is None:
|
||||||
return
|
return
|
||||||
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
|
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
|
||||||
if state.REMAP_PATH is True:
|
if app.SYNC.remap_path is True:
|
||||||
path = path.replace(getattr(state, 'remapSMB%sOrg' % typus),
|
path = path.replace(getattr(app.SYNC, 'remapSMB%sOrg' % typus),
|
||||||
getattr(state, 'remapSMB%sNew' % typus),
|
getattr(app.SYNC, 'remapSMB%sNew' % typus),
|
||||||
1)
|
1)
|
||||||
# There might be backslashes left over:
|
# There might be backslashes left over:
|
||||||
path = path.replace('\\', '/')
|
path = path.replace('\\', '/')
|
||||||
elif state.REPLACE_SMB_PATH is True:
|
elif app.SYNC.replace_smb_path is True:
|
||||||
if path.startswith('\\\\'):
|
if path.startswith('\\\\'):
|
||||||
path = 'smb:' + path.replace('\\', '/')
|
path = 'smb:' + path.replace('\\', '/')
|
||||||
if ((state.PATH_VERIFIED and force_check is False) or
|
if ((app.SYNC.path_verified and force_check is False) or
|
||||||
omit_check is True):
|
omit_check is True):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
@ -1769,14 +1768,14 @@ class API(object):
|
||||||
if force_check is False:
|
if force_check is False:
|
||||||
# Validate the path is correct with user intervention
|
# Validate the path is correct with user intervention
|
||||||
if self.ask_to_validate(path):
|
if self.ask_to_validate(path):
|
||||||
state.STOP_SYNC = True
|
app.SYNC.stop_sync = True
|
||||||
path = None
|
path = None
|
||||||
state.PATH_VERIFIED = True
|
app.SYNC.path_verified = True
|
||||||
else:
|
else:
|
||||||
path = None
|
path = None
|
||||||
elif force_check is False:
|
elif force_check is False:
|
||||||
# Only set the flag if we were not force-checking the path
|
# Only set the flag if we were not force-checking the path
|
||||||
state.PATH_VERIFIED = True
|
app.SYNC.path_verified = True
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -20,7 +20,8 @@ from . import playback
|
||||||
from . import json_rpc as js
|
from . import json_rpc as js
|
||||||
from . import playqueue as PQ
|
from . import playqueue as PQ
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import backgroundthread
|
||||||
|
from . import app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ def update_playqueue_from_PMS(playqueue,
|
||||||
# Safe transient token from being deleted
|
# Safe transient token from being deleted
|
||||||
if transient_token is None:
|
if transient_token is None:
|
||||||
transient_token = playqueue.plex_transient_token
|
transient_token = playqueue.plex_transient_token
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
xml = PL.get_PMS_playlist(playqueue, playqueue_id)
|
||||||
try:
|
try:
|
||||||
xml.attrib
|
xml.attrib
|
||||||
|
@ -64,8 +65,7 @@ def update_playqueue_from_PMS(playqueue,
|
||||||
playback.play_xml(playqueue, xml, offset)
|
playback.play_xml(playqueue, xml, offset)
|
||||||
|
|
||||||
|
|
||||||
@utils.thread_methods(add_suspends=['PMS_STATUS'])
|
class PlexCompanion(backgroundthread.KillableThread):
|
||||||
class PlexCompanion(Thread):
|
|
||||||
"""
|
"""
|
||||||
Plex Companion monitoring class. Invoke only once
|
Plex Companion monitoring class. Invoke only once
|
||||||
"""
|
"""
|
||||||
|
@ -80,7 +80,13 @@ class PlexCompanion(Thread):
|
||||||
self.player = Player()
|
self.player = Player()
|
||||||
self.httpd = False
|
self.httpd = False
|
||||||
self.subscription_manager = None
|
self.subscription_manager = None
|
||||||
Thread.__init__(self)
|
super(PlexCompanion, self).__init__()
|
||||||
|
|
||||||
|
def isSuspended(self):
|
||||||
|
"""
|
||||||
|
Returns True if the thread is suspended
|
||||||
|
"""
|
||||||
|
return self._suspended or app.CONN.pms_status
|
||||||
|
|
||||||
def _process_alexa(self, data):
|
def _process_alexa(self, data):
|
||||||
xml = PF.GetPlexMetadata(data['key'])
|
xml = PF.GetPlexMetadata(data['key'])
|
||||||
|
@ -116,10 +122,9 @@ class PlexCompanion(Thread):
|
||||||
offset = None
|
offset = None
|
||||||
playback.play_xml(playqueue, xml, offset)
|
playback.play_xml(playqueue, xml, offset)
|
||||||
else:
|
else:
|
||||||
state.PLEX_TRANSIENT_TOKEN = data.get('token')
|
app.CONN.plex_transient_token = data.get('token')
|
||||||
if data.get('offset') != '0':
|
if data.get('offset') != '0':
|
||||||
state.RESUMABLE = True
|
app.PLAYSTATE.resume_playback = True
|
||||||
state.RESUME_PLAYBACK = True
|
|
||||||
playback.playback_triage(api.plex_id(),
|
playback.playback_triage(api.plex_id(),
|
||||||
api.plex_type(),
|
api.plex_type(),
|
||||||
resolve=False)
|
resolve=False)
|
||||||
|
@ -129,7 +134,7 @@ class PlexCompanion(Thread):
|
||||||
"""
|
"""
|
||||||
E.g. watch later initiated by Companion. Basically navigating Plex
|
E.g. watch later initiated by Companion. Basically navigating Plex
|
||||||
"""
|
"""
|
||||||
state.PLEX_TRANSIENT_TOKEN = data.get('key')
|
app.CONN.plex_transient_token = data.get('key')
|
||||||
params = {
|
params = {
|
||||||
'mode': 'plex_node',
|
'mode': 'plex_node',
|
||||||
'key': '{server}%s' % data.get('key'),
|
'key': '{server}%s' % data.get('key'),
|
||||||
|
@ -221,16 +226,16 @@ class PlexCompanion(Thread):
|
||||||
LOG.debug('Processing: %s', task)
|
LOG.debug('Processing: %s', task)
|
||||||
data = task['data']
|
data = task['data']
|
||||||
if task['action'] == 'alexa':
|
if task['action'] == 'alexa':
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self._process_alexa(data)
|
self._process_alexa(data)
|
||||||
elif (task['action'] == 'playlist' and
|
elif (task['action'] == 'playlist' and
|
||||||
data.get('address') == 'node.plexapp.com'):
|
data.get('address') == 'node.plexapp.com'):
|
||||||
self._process_node(data)
|
self._process_node(data)
|
||||||
elif task['action'] == 'playlist':
|
elif task['action'] == 'playlist':
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self._process_playlist(data)
|
self._process_playlist(data)
|
||||||
elif task['action'] == 'refreshPlayQueue':
|
elif task['action'] == 'refreshPlayQueue':
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
self._process_refresh(data)
|
self._process_refresh(data)
|
||||||
elif task['action'] == 'setStreams':
|
elif task['action'] == 'setStreams':
|
||||||
try:
|
try:
|
||||||
|
@ -260,8 +265,6 @@ class PlexCompanion(Thread):
|
||||||
httpd = self.httpd
|
httpd = self.httpd
|
||||||
# Cache for quicker while loops
|
# Cache for quicker while loops
|
||||||
client = self.client
|
client = self.client
|
||||||
stopped = self.stopped
|
|
||||||
suspended = self.suspended
|
|
||||||
|
|
||||||
# Start up instances
|
# Start up instances
|
||||||
request_mgr = httppersist.RequestMgr()
|
request_mgr = httppersist.RequestMgr()
|
||||||
|
@ -298,12 +301,12 @@ class PlexCompanion(Thread):
|
||||||
if httpd:
|
if httpd:
|
||||||
thread = Thread(target=httpd.handle_request)
|
thread = Thread(target=httpd.handle_request)
|
||||||
|
|
||||||
while not stopped():
|
while not self.isCanceled():
|
||||||
# If we are not authorized, sleep
|
# If we are not authorized, sleep
|
||||||
# Otherwise, we trigger a download which leads to a
|
# Otherwise, we trigger a download which leads to a
|
||||||
# re-authorizations
|
# re-authorizations
|
||||||
while suspended():
|
while self.isSuspended():
|
||||||
if stopped():
|
if self.isCanceled():
|
||||||
break
|
break
|
||||||
sleep(1000)
|
sleep(1000)
|
||||||
try:
|
try:
|
||||||
|
@ -335,13 +338,13 @@ class PlexCompanion(Thread):
|
||||||
LOG.warn(traceback.format_exc())
|
LOG.warn(traceback.format_exc())
|
||||||
# See if there's anything we need to process
|
# See if there's anything we need to process
|
||||||
try:
|
try:
|
||||||
task = state.COMPANION_QUEUE.get(block=False)
|
task = app.APP.companion_queue.get(block=False)
|
||||||
except Empty:
|
except Empty:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# Got instructions, process them
|
# Got instructions, process them
|
||||||
self._process_tasks(task)
|
self._process_tasks(task)
|
||||||
state.COMPANION_QUEUE.task_done()
|
app.APP.companion_queue.task_done()
|
||||||
# Don't sleep
|
# Don't sleep
|
||||||
continue
|
continue
|
||||||
sleep(50)
|
sleep(50)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from .. import variables as v
|
|
||||||
|
|
||||||
|
|
||||||
class Playlists(object):
|
class Playlists(object):
|
||||||
|
|
|
@ -5,10 +5,9 @@ from logging import getLogger
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import xbmc
|
import xbmc
|
||||||
import xbmcgui
|
|
||||||
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import utils, variables as v, state
|
from . import utils, app
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
LOG = getLogger('PLEX.plex_tv')
|
LOG = getLogger('PLEX.plex_tv')
|
||||||
|
@ -87,7 +86,7 @@ def switch_home_user(userid, pin, token, machine_identifier):
|
||||||
utils.settings('plex_restricteduser',
|
utils.settings('plex_restricteduser',
|
||||||
'true' if xml.get('restricted', '0') == '1'
|
'true' if xml.get('restricted', '0') == '1'
|
||||||
else 'false')
|
else 'false')
|
||||||
state.RESTRICTED_USER = True if \
|
app.CONN.restricted_user = True if \
|
||||||
xml.get('restricted', '0') == '1' else False
|
xml.get('restricted', '0') == '1' else False
|
||||||
|
|
||||||
# Get final token to the PMS we've chosen
|
# Get final token to the PMS we've chosen
|
||||||
|
@ -174,7 +173,7 @@ class PinLogin(object):
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while (not self._abort and
|
while (not self._abort and
|
||||||
time.time() - start < 300 and
|
time.time() - start < 300 and
|
||||||
not state.STOP_PKC):
|
not app.APP.stop_pkc):
|
||||||
xml = DU().downloadUrl(self.POLL.format(self.id),
|
xml = DU().downloadUrl(self.POLL.format(self.id),
|
||||||
authenticate=False)
|
authenticate=False)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -31,7 +31,7 @@ import time
|
||||||
from xbmc import sleep
|
from xbmc import sleep
|
||||||
|
|
||||||
from ..downloadutils import DownloadUtils as DU
|
from ..downloadutils import DownloadUtils as DU
|
||||||
from .. import utils
|
from .. import utils, app
|
||||||
from .. import variables as v
|
from .. import variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -228,7 +228,7 @@ class plexgdm:
|
||||||
return self.server_list
|
return self.server_list
|
||||||
|
|
||||||
def discover(self):
|
def discover(self):
|
||||||
currServer = utils.window('pms_server')
|
currServer = app.CONN.server
|
||||||
if not currServer:
|
if not currServer:
|
||||||
return
|
return
|
||||||
currServerProt, currServerIP, currServerPort = \
|
currServerProt, currServerIP, currServerPort = \
|
||||||
|
|
|
@ -9,8 +9,8 @@ from logging import getLogger
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from ..downloadutils import DownloadUtils as DU
|
from ..downloadutils import DownloadUtils as DU
|
||||||
from .. import utils
|
from .. import utils, timing
|
||||||
from .. import state
|
from .. import app
|
||||||
from .. import variables as v
|
from .. import variables as v
|
||||||
from .. import json_rpc as js
|
from .. import json_rpc as js
|
||||||
from .. import playqueue as PQ
|
from .. import playqueue as PQ
|
||||||
|
@ -101,9 +101,9 @@ def update_player_info(playerid):
|
||||||
"""
|
"""
|
||||||
Updates all player info for playerid [int] in state.py.
|
Updates all player info for playerid [int] in state.py.
|
||||||
"""
|
"""
|
||||||
state.PLAYER_STATES[playerid].update(js.get_player_props(playerid))
|
app.PLAYSTATE.playerstates[playerid].update(js.get_player_props(playerid))
|
||||||
state.PLAYER_STATES[playerid]['volume'] = js.get_volume()
|
app.PLAYSTATE.playerstates[playerid]['volume'] = js.get_volume()
|
||||||
state.PLAYER_STATES[playerid]['muted'] = js.get_muted()
|
app.PLAYSTATE.playerstates[playerid]['muted'] = js.get_muted()
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionMgr(object):
|
class SubscriptionMgr(object):
|
||||||
|
@ -189,9 +189,9 @@ class SubscriptionMgr(object):
|
||||||
return answ
|
return answ
|
||||||
|
|
||||||
def _timeline_dict(self, player, ptype):
|
def _timeline_dict(self, player, ptype):
|
||||||
with state.LOCK_PLAYQUEUES:
|
with app.APP.lock_playqueues:
|
||||||
playerid = player['playerid']
|
playerid = player['playerid']
|
||||||
info = state.PLAYER_STATES[playerid]
|
info = app.PLAYSTATE.player_states[playerid]
|
||||||
playqueue = PQ.PLAYQUEUES[playerid]
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
position = self._get_correct_position(info, playqueue)
|
position = self._get_correct_position(info, playqueue)
|
||||||
try:
|
try:
|
||||||
|
@ -208,12 +208,12 @@ class SubscriptionMgr(object):
|
||||||
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
|
if ptype in (v.PLEX_PLAYLIST_TYPE_VIDEO,
|
||||||
v.PLEX_PLAYLIST_TYPE_PHOTO):
|
v.PLEX_PLAYLIST_TYPE_PHOTO):
|
||||||
self.location = 'fullScreenVideo'
|
self.location = 'fullScreenVideo'
|
||||||
pbmc_server = utils.window('pms_server')
|
pbmc_server = app.CONN.server
|
||||||
if pbmc_server:
|
if pbmc_server:
|
||||||
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
(self.protocol, self.server, self.port) = pbmc_server.split(':')
|
||||||
self.server = self.server.replace('/', '')
|
self.server = self.server.replace('/', '')
|
||||||
status = 'paused' if int(info['speed']) == 0 else 'playing'
|
status = 'paused' if int(info['speed']) == 0 else 'playing'
|
||||||
duration = utils.kodi_time_to_millis(info['totaltime'])
|
duration = timing.kodi_time_to_millis(info['totaltime'])
|
||||||
shuffle = '1' if info['shuffled'] else '0'
|
shuffle = '1' if info['shuffled'] else '0'
|
||||||
mute = '1' if info['muted'] is True else '0'
|
mute = '1' if info['muted'] is True else '0'
|
||||||
answ = {
|
answ = {
|
||||||
|
@ -225,7 +225,7 @@ class SubscriptionMgr(object):
|
||||||
'state': status,
|
'state': status,
|
||||||
'type': ptype,
|
'type': ptype,
|
||||||
'itemType': ptype,
|
'itemType': ptype,
|
||||||
'time': utils.kodi_time_to_millis(info['time']),
|
'time': timing.kodi_time_to_millis(info['time']),
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'seekRange': '0-%s' % duration,
|
'seekRange': '0-%s' % duration,
|
||||||
'shuffle': shuffle,
|
'shuffle': shuffle,
|
||||||
|
@ -254,8 +254,8 @@ class SubscriptionMgr(object):
|
||||||
if playqueue.items[position].guid:
|
if playqueue.items[position].guid:
|
||||||
answ['guid'] = item.guid
|
answ['guid'] = item.guid
|
||||||
# Temp. token set?
|
# Temp. token set?
|
||||||
if state.PLEX_TRANSIENT_TOKEN:
|
if app.CONN.plex_transient_token:
|
||||||
answ['token'] = state.PLEX_TRANSIENT_TOKEN
|
answ['token'] = app.CONN.plex_transient_token
|
||||||
elif playqueue.plex_transient_token:
|
elif playqueue.plex_transient_token:
|
||||||
answ['token'] = playqueue.plex_transient_token
|
answ['token'] = playqueue.plex_transient_token
|
||||||
# Process audio and subtitle streams
|
# Process audio and subtitle streams
|
||||||
|
@ -301,7 +301,7 @@ class SubscriptionMgr(object):
|
||||||
stream_type: 'video', 'audio', 'subtitle'
|
stream_type: 'video', 'audio', 'subtitle'
|
||||||
"""
|
"""
|
||||||
playqueue = PQ.PLAYQUEUES[playerid]
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
info = state.PLAYER_STATES[playerid]
|
info = app.PLAYSTATE.player_states[playerid]
|
||||||
position = self._get_correct_position(info, playqueue)
|
position = self._get_correct_position(info, playqueue)
|
||||||
if info[STREAM_DETAILS[stream_type]] == -1:
|
if info[STREAM_DETAILS[stream_type]] == -1:
|
||||||
kodi_stream_index = -1
|
kodi_stream_index = -1
|
||||||
|
@ -315,7 +315,7 @@ class SubscriptionMgr(object):
|
||||||
Updates the Plex Companien client with the machine identifier uuid with
|
Updates the Plex Companien client with the machine identifier uuid with
|
||||||
command_id
|
command_id
|
||||||
"""
|
"""
|
||||||
with state.LOCK_SUBSCRIBER:
|
with app.APP.lock_subscriber:
|
||||||
if command_id and self.subscribers.get(uuid):
|
if command_id and self.subscribers.get(uuid):
|
||||||
self.subscribers[uuid].command_id = int(command_id)
|
self.subscribers[uuid].command_id = int(command_id)
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ class SubscriptionMgr(object):
|
||||||
playqueues.
|
playqueues.
|
||||||
"""
|
"""
|
||||||
for player in players.values():
|
for player in players.values():
|
||||||
info = state.PLAYER_STATES[player['playerid']]
|
info = app.PLAYSTATE.player_states[player['playerid']]
|
||||||
playqueue = PQ.PLAYQUEUES[player['playerid']]
|
playqueue = PQ.PLAYQUEUES[player['playerid']]
|
||||||
position = self._get_correct_position(info, playqueue)
|
position = self._get_correct_position(info, playqueue)
|
||||||
try:
|
try:
|
||||||
|
@ -345,7 +345,7 @@ class SubscriptionMgr(object):
|
||||||
Causes PKC to tell the PMS and Plex Companion players to receive a
|
Causes PKC to tell the PMS and Plex Companion players to receive a
|
||||||
notification what's being played.
|
notification what's being played.
|
||||||
"""
|
"""
|
||||||
with state.LOCK_SUBSCRIBER:
|
with app.APP.lock_subscriber:
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
# Get all the active/playing Kodi players (video, audio, pictures)
|
# Get all the active/playing Kodi players (video, audio, pictures)
|
||||||
players = js.get_players()
|
players = js.get_players()
|
||||||
|
@ -378,7 +378,7 @@ class SubscriptionMgr(object):
|
||||||
self._send_pms_notification(player['playerid'], self.last_params)
|
self._send_pms_notification(player['playerid'], self.last_params)
|
||||||
|
|
||||||
def _get_pms_params(self, playerid):
|
def _get_pms_params(self, playerid):
|
||||||
info = state.PLAYER_STATES[playerid]
|
info = app.PLAYSTATE.player_states[playerid]
|
||||||
playqueue = PQ.PLAYQUEUES[playerid]
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
position = self._get_correct_position(info, playqueue)
|
position = self._get_correct_position(info, playqueue)
|
||||||
try:
|
try:
|
||||||
|
@ -390,8 +390,8 @@ class SubscriptionMgr(object):
|
||||||
'state': status,
|
'state': status,
|
||||||
'ratingKey': item.plex_id,
|
'ratingKey': item.plex_id,
|
||||||
'key': '/library/metadata/%s' % item.plex_id,
|
'key': '/library/metadata/%s' % item.plex_id,
|
||||||
'time': utils.kodi_time_to_millis(info['time']),
|
'time': timing.kodi_time_to_millis(info['time']),
|
||||||
'duration': utils.kodi_time_to_millis(info['totaltime'])
|
'duration': timing.kodi_time_to_millis(info['totaltime'])
|
||||||
}
|
}
|
||||||
if info['container_key'] is not None:
|
if info['container_key'] is not None:
|
||||||
# params['containerKey'] = info['container_key']
|
# params['containerKey'] = info['container_key']
|
||||||
|
@ -407,12 +407,12 @@ class SubscriptionMgr(object):
|
||||||
playqueue = PQ.PLAYQUEUES[playerid]
|
playqueue = PQ.PLAYQUEUES[playerid]
|
||||||
xargs = params_pms()
|
xargs = params_pms()
|
||||||
xargs.update(params)
|
xargs.update(params)
|
||||||
if state.PLEX_TRANSIENT_TOKEN:
|
if app.CONN.plex_transient_token:
|
||||||
xargs['X-Plex-Token'] = state.PLEX_TRANSIENT_TOKEN
|
xargs['X-Plex-Token'] = app.CONN.plex_transient_token
|
||||||
elif playqueue.plex_transient_token:
|
elif playqueue.plex_transient_token:
|
||||||
xargs['X-Plex-Token'] = playqueue.plex_transient_token
|
xargs['X-Plex-Token'] = playqueue.plex_transient_token
|
||||||
elif state.PMS_TOKEN:
|
elif app.ACCOUNT.pms_token:
|
||||||
xargs['X-Plex-Token'] = state.PMS_TOKEN
|
xargs['X-Plex-Token'] = app.ACCOUNT.pms_token
|
||||||
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
url = '%s://%s:%s/:/timeline' % (serv.get('protocol', 'http'),
|
||||||
serv.get('server', 'localhost'),
|
serv.get('server', 'localhost'),
|
||||||
serv.get('port', '32400'))
|
serv.get('port', '32400'))
|
||||||
|
@ -434,7 +434,7 @@ class SubscriptionMgr(object):
|
||||||
command_id,
|
command_id,
|
||||||
self,
|
self,
|
||||||
self.request_mgr)
|
self.request_mgr)
|
||||||
with state.LOCK_SUBSCRIBER:
|
with app.APP.lock_subscriber:
|
||||||
self.subscribers[subscriber.uuid] = subscriber
|
self.subscribers[subscriber.uuid] = subscriber
|
||||||
return subscriber
|
return subscriber
|
||||||
|
|
||||||
|
@ -444,7 +444,7 @@ class SubscriptionMgr(object):
|
||||||
uuid from PKC notifications.
|
uuid from PKC notifications.
|
||||||
(Calls the cleanup() method of the subscriber)
|
(Calls the cleanup() method of the subscriber)
|
||||||
"""
|
"""
|
||||||
with state.LOCK_SUBSCRIBER:
|
with app.APP.lock_subscriber:
|
||||||
for subscriber in self.subscribers.values():
|
for subscriber in self.subscribers.values():
|
||||||
if subscriber.uuid == uuid or subscriber.host == uuid:
|
if subscriber.uuid == uuid or subscriber.host == uuid:
|
||||||
subscriber.cleanup()
|
subscriber.cleanup()
|
||||||
|
|
|
@ -5,38 +5,107 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from . import utils
|
from . import utils, clientinfo, timing
|
||||||
from . import userclient
|
from . import initialsetup, artwork
|
||||||
from . import initialsetup
|
|
||||||
from . import kodimonitor
|
from . import kodimonitor
|
||||||
from . import sync
|
from . import sync
|
||||||
from . import websocket_client
|
from . import websocket_client
|
||||||
from . import plex_companion
|
from . import plex_companion
|
||||||
from . import plex_functions as PF
|
from . import plex_functions as PF, playqueue as PQ
|
||||||
from . import command_pipeline
|
|
||||||
from . import playback_starter
|
from . import playback_starter
|
||||||
from . import playqueue
|
from . import playqueue
|
||||||
from . import variables as v
|
from . import variables as v
|
||||||
from . import state
|
from . import app
|
||||||
from . import loghandler
|
from . import loghandler
|
||||||
|
from .windows import userselect
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
loghandler.config()
|
loghandler.config()
|
||||||
LOG = logging.getLogger("PLEX.service_entry")
|
LOG = logging.getLogger("PLEX.service")
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
WINDOW_PROPERTIES = (
|
||||||
|
"plex_online", "plex_command_processed", "plex_shouldStop", "plex_dbScan",
|
||||||
|
"plex_customplayqueue", "plex_playbackProps",
|
||||||
|
"pms_token", "plex_token", "pms_server", "plex_machineIdentifier",
|
||||||
|
"plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths",
|
||||||
|
"countError", "countUnauthorized", "plex_restricteduser",
|
||||||
|
"plex_allows_mediaDeletion", "plex_command", "plex_result",
|
||||||
|
"plex_force_transcode_pix"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate():
|
||||||
|
"""
|
||||||
|
Authenticate the current user or prompt to log-in
|
||||||
|
|
||||||
|
Returns True if successful, False if not. 'aborted' if user chose to
|
||||||
|
abort
|
||||||
|
"""
|
||||||
|
LOG.info('Authenticating user')
|
||||||
|
if app.ACCOUNT.plex_username and not app.ACCOUNT.force_login:
|
||||||
|
# Found a user in the settings, try to authenticate
|
||||||
|
LOG.info('Trying to authenticate with old settings')
|
||||||
|
res = PF.check_connection(app.CONN.server,
|
||||||
|
token=app.ACCOUNT.pms_token,
|
||||||
|
verifySSL=app.CONN.verify_ssl_cert)
|
||||||
|
if res is False:
|
||||||
|
LOG.error('Something went wrong while checking connection')
|
||||||
|
return False
|
||||||
|
elif res == 401:
|
||||||
|
LOG.error('User token no longer valid. Sign user out')
|
||||||
|
app.ACCOUNT.clear()
|
||||||
|
return False
|
||||||
|
elif res >= 400:
|
||||||
|
LOG.error('Answer from PMS is not as expected')
|
||||||
|
return False
|
||||||
|
LOG.info('Successfully authenticated using old settings')
|
||||||
|
app.ACCOUNT.set_authenticated()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Could not use settings - try to get Plex user list from plex.tv
|
||||||
|
if app.ACCOUNT.plex_token:
|
||||||
|
LOG.info("Trying to connect to plex.tv to get a user list")
|
||||||
|
user, _ = userselect.start()
|
||||||
|
if not user:
|
||||||
|
LOG.info('No user received')
|
||||||
|
app.CONN.pms_status = 'Stop'
|
||||||
|
return False
|
||||||
|
username = user.title
|
||||||
|
user_id = user.id
|
||||||
|
token = user.authToken
|
||||||
|
else:
|
||||||
|
LOG.info("Trying to authenticate without a token")
|
||||||
|
username = ''
|
||||||
|
user_id = ''
|
||||||
|
token = ''
|
||||||
|
res = PF.check_connection(app.CONN.server,
|
||||||
|
token=token,
|
||||||
|
verifySSL=app.CONN.verify_ssl_cert)
|
||||||
|
if res is False:
|
||||||
|
LOG.error('Something went wrong while checking connection')
|
||||||
|
return False
|
||||||
|
elif res == 401:
|
||||||
|
LOG.error('Token not valid')
|
||||||
|
return False
|
||||||
|
elif res >= 400:
|
||||||
|
LOG.error('Answer from PMS is not as expected')
|
||||||
|
return False
|
||||||
|
LOG.info('Successfully authenticated')
|
||||||
|
# Got new values that need to be saved
|
||||||
|
utils.settings('username', value=username)
|
||||||
|
utils.settings('userid', value=user_id)
|
||||||
|
utils.settings('accessToken', value=token)
|
||||||
|
app.ACCOUNT.load()
|
||||||
|
app.ACCOUNT.set_authenticated()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Service():
|
class Service():
|
||||||
|
|
||||||
server_online = True
|
|
||||||
warn_auth = True
|
|
||||||
|
|
||||||
user = None
|
|
||||||
ws = None
|
ws = None
|
||||||
sync = None
|
sync = None
|
||||||
plexcompanion = None
|
plexcompanion = None
|
||||||
|
|
||||||
user_running = False
|
|
||||||
ws_running = False
|
ws_running = False
|
||||||
alexa_running = False
|
alexa_running = False
|
||||||
sync_running = False
|
sync_running = False
|
||||||
|
@ -67,28 +136,110 @@ class Service():
|
||||||
utils.settings('syncSpecificPlexPlaylistsPrefix'))
|
utils.settings('syncSpecificPlexPlaylistsPrefix'))
|
||||||
LOG.info('XML decoding being used: %s', utils.ETREE)
|
LOG.info('XML decoding being used: %s', utils.ETREE)
|
||||||
LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion'))
|
LOG.info("Db version: %s", utils.settings('dbCreatedWithVersion'))
|
||||||
|
|
||||||
|
# Reset window props
|
||||||
|
for prop in WINDOW_PROPERTIES:
|
||||||
|
utils.window(prop, clear=True)
|
||||||
|
|
||||||
|
# To detect Kodi profile switches
|
||||||
|
utils.window('plex_kodiProfile',
|
||||||
|
value=utils.try_decode(xbmc.translatePath("special://profile")))
|
||||||
|
|
||||||
self.monitor = xbmc.Monitor()
|
self.monitor = xbmc.Monitor()
|
||||||
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
# Load/Reset PKC entirely - important for user/Kodi profile switch
|
||||||
initialsetup.reload_pkc()
|
# Clear video nodes properties
|
||||||
|
from .library_sync import videonodes
|
||||||
|
videonodes.VideoNodes().clearProperties()
|
||||||
|
clientinfo.getDeviceId()
|
||||||
|
# Init time-offset between Kodi and Plex
|
||||||
|
timing.KODI_PLEX_TIME_OFFSET = utils.settings('kodiplextimeoffset') or 0.0
|
||||||
|
|
||||||
def _stop_pkc(self):
|
def isCanceled(self):
|
||||||
return xbmc.abortRequested or state.STOP_PKC
|
return xbmc.abortRequested or app.APP.stop_pkc
|
||||||
|
|
||||||
|
def log_out(self):
|
||||||
|
"""
|
||||||
|
Ensures that lib sync threads are suspended; signs out user
|
||||||
|
"""
|
||||||
|
LOG.info('Log-out requested')
|
||||||
|
app.SYNC.suspend_library_thread = True
|
||||||
|
i = 0
|
||||||
|
while app.SYNC.db_scan:
|
||||||
|
i += 1
|
||||||
|
xbmc.sleep(50)
|
||||||
|
if i > 100:
|
||||||
|
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.SYNC.suspend_library_thread = False
|
||||||
|
return False
|
||||||
|
LOG.info('Successfully stopped library sync')
|
||||||
|
app.ACCOUNT.clear()
|
||||||
|
LOG.info('User has been logged out')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def choose_pms_server(self, manual=False):
|
||||||
|
LOG.info("Choosing PMS server requested, starting")
|
||||||
|
if manual:
|
||||||
|
if not self.setup.enter_new_pms_address():
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
server = self.setup.pick_pms(showDialog=True)
|
||||||
|
if server is None:
|
||||||
|
LOG.info('We did not connect to a new PMS, aborting')
|
||||||
|
return False
|
||||||
|
LOG.info("User chose server %s", server['name'])
|
||||||
|
if server['baseURL'] == app.CONN.server:
|
||||||
|
LOG.info('User chose old PMS to connect to')
|
||||||
|
return False
|
||||||
|
self.setup.write_pms_to_settings(server)
|
||||||
|
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()
|
||||||
|
LOG.info("Choosing new PMS complete")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def switch_plex_user(self):
|
||||||
|
if not self.log_out():
|
||||||
|
return False
|
||||||
|
# First remove playlists of old user
|
||||||
|
utils.delete_playlists()
|
||||||
|
# Remove video nodes
|
||||||
|
utils.delete_nodes()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def toggle_plex_tv(self):
|
||||||
|
if utils.settings('plexToken'):
|
||||||
|
LOG.info('Reseting plex.tv credentials in settings')
|
||||||
|
app.ACCOUNT.clear()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
LOG.info('Login to plex.tv')
|
||||||
|
return self.setup.plex_tv_sign_in()
|
||||||
|
|
||||||
def ServiceEntryPoint(self):
|
def ServiceEntryPoint(self):
|
||||||
# Important: Threads depending on abortRequest will not trigger
|
# Important: Threads depending on abortRequest will not trigger
|
||||||
# if profile switch happens more than once.
|
# if profile switch happens more than once.
|
||||||
_stop_pkc = self._stop_pkc
|
app.init()
|
||||||
monitor = self.monitor
|
# Some plumbing
|
||||||
|
artwork.IMAGE_CACHING_SUSPENDS = [
|
||||||
|
app.SYNC.suspend_library_thread,
|
||||||
|
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()
|
||||||
|
|
||||||
# Server auto-detect
|
# Server auto-detect
|
||||||
initialsetup.InitialSetup().setup()
|
self.setup = initialsetup.InitialSetup()
|
||||||
|
self.setup.setup()
|
||||||
|
|
||||||
# Detect playback start early on
|
# Initialize important threads
|
||||||
self.command_pipeline = command_pipeline.Monitor_Window()
|
|
||||||
self.command_pipeline.start()
|
|
||||||
|
|
||||||
# Initialize important threads, handing over self for callback purposes
|
|
||||||
self.user = userclient.UserClient()
|
|
||||||
self.ws = websocket_client.PMS_Websocket()
|
self.ws = websocket_client.PMS_Websocket()
|
||||||
self.alexa = websocket_client.Alexa_Websocket()
|
self.alexa = websocket_client.Alexa_Websocket()
|
||||||
self.sync = sync.Sync()
|
self.sync = sync.Sync()
|
||||||
|
@ -97,9 +248,10 @@ class Service():
|
||||||
self.playback_starter = playback_starter.PlaybackStarter()
|
self.playback_starter = playback_starter.PlaybackStarter()
|
||||||
self.playqueue = playqueue.PlayqueueMonitor()
|
self.playqueue = playqueue.PlayqueueMonitor()
|
||||||
|
|
||||||
|
server_online = True
|
||||||
welcome_msg = True
|
welcome_msg = True
|
||||||
counter = 0
|
counter = 0
|
||||||
while not _stop_pkc():
|
while not self.isCanceled():
|
||||||
|
|
||||||
if utils.window('plex_kodiProfile') != v.KODI_PROFILE:
|
if utils.window('plex_kodiProfile') != v.KODI_PROFILE:
|
||||||
# Profile change happened, terminate this thread and others
|
# Profile change happened, terminate this thread and others
|
||||||
|
@ -108,149 +260,167 @@ class Service():
|
||||||
v.KODI_PROFILE, utils.window('plex_kodiProfile'))
|
v.KODI_PROFILE, utils.window('plex_kodiProfile'))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
plex_command = utils.window('plex_command')
|
||||||
|
if plex_command:
|
||||||
|
# Commands/user interaction received from other PKC Python
|
||||||
|
# instances (default.py and context.py instead of service.py)
|
||||||
|
utils.window('plex_command', clear=True)
|
||||||
|
if plex_command.startswith('PLAY-'):
|
||||||
|
# Add-on path playback!
|
||||||
|
app.APP.command_pipeline_queue.put(
|
||||||
|
plex_command.replace('PLAY-', ''))
|
||||||
|
elif plex_command.startswith('NAVIGATE-'):
|
||||||
|
app.APP.command_pipeline_queue.put(
|
||||||
|
plex_command.replace('NAVIGATE-', ''))
|
||||||
|
elif plex_command.startswith('CONTEXT_menu?'):
|
||||||
|
app.APP.command_pipeline_queue.put(
|
||||||
|
'dummy?mode=context_menu&%s'
|
||||||
|
% plex_command.replace('CONTEXT_menu?', ''))
|
||||||
|
elif plex_command == 'choose_pms_server':
|
||||||
|
if self.choose_pms_server():
|
||||||
|
utils.window('plex_online', clear=True)
|
||||||
|
app.ACCOUNT.set_unauthenticated()
|
||||||
|
server_online = False
|
||||||
|
welcome_msg = False
|
||||||
|
elif plex_command == 'switch_plex_user':
|
||||||
|
if self.switch_plex_user():
|
||||||
|
app.ACCOUNT.set_unauthenticated()
|
||||||
|
elif plex_command == 'enter_new_pms_address':
|
||||||
|
if self.setup.enter_new_pms_address():
|
||||||
|
if self.log_out():
|
||||||
|
utils.window('plex_online', clear=True)
|
||||||
|
app.ACCOUNT.set_unauthenticated()
|
||||||
|
server_online = False
|
||||||
|
welcome_msg = False
|
||||||
|
elif plex_command == 'toggle_plex_tv_sign_in':
|
||||||
|
if self.toggle_plex_tv():
|
||||||
|
app.ACCOUNT.set_unauthenticated()
|
||||||
|
elif plex_command == 'repair-scan':
|
||||||
|
app.SYNC.run_lib_scan = 'repair'
|
||||||
|
elif plex_command == 'full-scan':
|
||||||
|
app.SYNC.run_lib_scan = 'full'
|
||||||
|
elif plex_command == 'fanart-scan':
|
||||||
|
app.SYNC.run_lib_scan = 'fanart'
|
||||||
|
elif plex_command == 'textures-scan':
|
||||||
|
app.SYNC.run_lib_scan = 'textures'
|
||||||
|
continue
|
||||||
|
|
||||||
# Before proceeding, need to make sure:
|
# Before proceeding, need to make sure:
|
||||||
# 1. Server is online
|
# 1. Server is online
|
||||||
# 2. User is set
|
# 2. User is set
|
||||||
# 3. User has access to the server
|
# 3. User has access to the server
|
||||||
|
|
||||||
if utils.window('plex_online') == "true":
|
if utils.window('plex_online') == "true":
|
||||||
# Plex server is online
|
# Plex server is online
|
||||||
# Verify if user is set and has access to the server
|
if app.CONN.pms_status == 'Stop':
|
||||||
if (self.user.user is not None) and self.user.has_access:
|
xbmc.sleep(500)
|
||||||
|
continue
|
||||||
|
elif app.CONN.pms_status == '401':
|
||||||
|
# Unauthorized access, revoke token
|
||||||
|
LOG.info('401 received - revoking token')
|
||||||
|
app.ACCOUNT.clear()
|
||||||
|
app.CONN.pms_status = 'Auth'
|
||||||
|
utils.window('plex_serverStatus', value='Auth')
|
||||||
|
continue
|
||||||
|
if not app.ACCOUNT.authenticated:
|
||||||
|
LOG.info('Not yet authenticated')
|
||||||
|
# Do authentication
|
||||||
|
if not authenticate():
|
||||||
|
continue
|
||||||
|
# Start up events
|
||||||
|
if welcome_msg is True:
|
||||||
|
# Reset authentication warnings
|
||||||
|
welcome_msg = False
|
||||||
|
utils.dialog('notification',
|
||||||
|
utils.lang(29999),
|
||||||
|
"%s %s" % (utils.lang(33000),
|
||||||
|
app.ACCOUNT.plex_username),
|
||||||
|
icon='{plex}',
|
||||||
|
time=2000,
|
||||||
|
sound=False)
|
||||||
|
# Start monitoring kodi events
|
||||||
if not self.kodimonitor_running:
|
if not self.kodimonitor_running:
|
||||||
# Start up events
|
|
||||||
self.warn_auth = True
|
|
||||||
if welcome_msg is True:
|
|
||||||
# Reset authentication warnings
|
|
||||||
welcome_msg = False
|
|
||||||
utils.dialog('notification',
|
|
||||||
utils.lang(29999),
|
|
||||||
"%s %s" % (utils.lang(33000),
|
|
||||||
self.user.user),
|
|
||||||
icon='{plex}',
|
|
||||||
time=2000,
|
|
||||||
sound=False)
|
|
||||||
# Start monitoring kodi events
|
|
||||||
self.kodimonitor_running = kodimonitor.KodiMonitor()
|
self.kodimonitor_running = kodimonitor.KodiMonitor()
|
||||||
self.specialmonitor.start()
|
self.specialmonitor.start()
|
||||||
# Start the Websocket Client
|
# Start the Websocket Client
|
||||||
if not self.ws_running:
|
if not self.ws_running:
|
||||||
self.ws_running = True
|
self.ws_running = True
|
||||||
self.ws.start()
|
self.ws.start()
|
||||||
# Start the Alexa thread
|
# Start the Alexa thread
|
||||||
if (not self.alexa_running and
|
if (not self.alexa_running and
|
||||||
utils.settings('enable_alexa') == 'true'):
|
utils.settings('enable_alexa') == 'true'):
|
||||||
self.alexa_running = True
|
self.alexa_running = True
|
||||||
self.alexa.start()
|
self.alexa.start()
|
||||||
# Start the syncing thread
|
# Start the syncing thread
|
||||||
if not self.sync_running:
|
if not self.sync_running:
|
||||||
self.sync_running = True
|
self.sync_running = True
|
||||||
self.sync.start()
|
self.sync.start()
|
||||||
# Start the Plex Companion thread
|
# Start the Plex Companion thread
|
||||||
if not self.plexcompanion_running:
|
if not self.plexcompanion_running:
|
||||||
self.plexcompanion_running = True
|
self.plexcompanion_running = True
|
||||||
self.plexcompanion.start()
|
self.plexcompanion.start()
|
||||||
if not self.playback_starter_running:
|
if not self.playback_starter_running:
|
||||||
self.playback_starter_running = True
|
self.playback_starter_running = True
|
||||||
self.playback_starter.start()
|
self.playback_starter.start()
|
||||||
self.playqueue.start()
|
self.playqueue.start()
|
||||||
else:
|
|
||||||
if (self.user.user is None) and self.warn_auth:
|
|
||||||
# Alert user is not authenticated and suppress future
|
|
||||||
# warning
|
|
||||||
self.warn_auth = False
|
|
||||||
LOG.warn("Not authenticated yet.")
|
|
||||||
|
|
||||||
# User access is restricted.
|
|
||||||
# Keep verifying until access is granted
|
|
||||||
# unless server goes offline or Kodi is shut down.
|
|
||||||
while self.user.has_access is False:
|
|
||||||
# Verify access with an API call
|
|
||||||
self.user.check_access()
|
|
||||||
|
|
||||||
if utils.window('plex_online') != "true":
|
|
||||||
# Server went offline
|
|
||||||
break
|
|
||||||
|
|
||||||
if monitor.waitForAbort(3):
|
|
||||||
# Abort was requested while waiting. We should exit
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
# Wait until Plex server is online
|
# Wait until Plex server is online
|
||||||
# or Kodi is shut down.
|
# or Kodi is shut down.
|
||||||
while not self._stop_pkc():
|
server = app.CONN.server
|
||||||
server = self.user.get_server()
|
if not server:
|
||||||
if server is False:
|
# No server info set in add-on settings
|
||||||
# No server info set in add-on settings
|
pass
|
||||||
pass
|
elif PF.check_connection(server, verifySSL=True) is False:
|
||||||
elif PF.check_connection(server, verifySSL=True) is False:
|
# Server is offline or cannot be reached
|
||||||
# Server is offline or cannot be reached
|
# Alert the user and suppress future warning
|
||||||
# Alert the user and suppress future warning
|
if server_online:
|
||||||
if self.server_online:
|
server_online = False
|
||||||
self.server_online = False
|
utils.window('plex_online', value="false")
|
||||||
utils.window('plex_online', value="false")
|
# Suspend threads
|
||||||
# Suspend threads
|
app.SYNC.suspend_library_thread = True
|
||||||
state.SUSPEND_LIBRARY_THREAD = True
|
LOG.warn("Plex Media Server went offline")
|
||||||
LOG.error("Plex Media Server went offline")
|
if utils.settings('show_pms_offline') == 'true':
|
||||||
if utils.settings('show_pms_offline') == 'true':
|
utils.dialog('notification',
|
||||||
utils.dialog('notification',
|
utils.lang(33001),
|
||||||
utils.lang(33001),
|
"%s %s" % (utils.lang(29999),
|
||||||
"%s %s" % (utils.lang(29999),
|
utils.lang(33002)),
|
||||||
utils.lang(33002)),
|
icon='{plex}',
|
||||||
icon='{plex}',
|
sound=False)
|
||||||
sound=False)
|
counter += 1
|
||||||
counter += 1
|
# Periodically check if the IP changed, e.g. per minute
|
||||||
# Periodically check if the IP changed, e.g. per minute
|
if counter > 20:
|
||||||
if counter > 20:
|
|
||||||
counter = 0
|
|
||||||
setup = initialsetup.InitialSetup()
|
|
||||||
tmp = setup.pick_pms()
|
|
||||||
if tmp is not None:
|
|
||||||
setup.write_pms_to_settings(tmp)
|
|
||||||
else:
|
|
||||||
# Server is online
|
|
||||||
counter = 0
|
counter = 0
|
||||||
if not self.server_online:
|
setup = initialsetup.InitialSetup()
|
||||||
# Server was offline when Kodi started.
|
tmp = setup.pick_pms()
|
||||||
# Wait for server to be fully established.
|
if tmp:
|
||||||
if monitor.waitForAbort(5):
|
setup.write_pms_to_settings(tmp)
|
||||||
# Abort was requested while waiting.
|
app.CONN.load()
|
||||||
break
|
else:
|
||||||
self.server_online = True
|
# Server is online
|
||||||
# Alert the user that server is online.
|
counter = 0
|
||||||
if (welcome_msg is False and
|
if not server_online:
|
||||||
utils.settings('show_pms_offline') == 'true'):
|
# Server was offline when Kodi started.
|
||||||
utils.dialog('notification',
|
server_online = True
|
||||||
utils.lang(29999),
|
# Alert the user that server is online.
|
||||||
utils.lang(33003),
|
if (welcome_msg is False and
|
||||||
icon='{plex}',
|
utils.settings('show_pms_offline') == 'true'):
|
||||||
time=5000,
|
utils.dialog('notification',
|
||||||
sound=False)
|
utils.lang(29999),
|
||||||
LOG.info("Server %s is online and ready.", server)
|
utils.lang(33003),
|
||||||
utils.window('plex_online', value="true")
|
icon='{plex}',
|
||||||
if state.AUTHENTICATED:
|
time=5000,
|
||||||
# Server got offline when we were authenticated.
|
sound=False)
|
||||||
# Hence resume threads
|
LOG.info("Server %s is online and ready.", server)
|
||||||
state.SUSPEND_LIBRARY_THREAD = False
|
utils.window('plex_online', value="true")
|
||||||
|
if app.ACCOUNT.authenticated:
|
||||||
|
# Server got offline when we were authenticated.
|
||||||
|
# Hence resume threads
|
||||||
|
app.SYNC.suspend_library_thread = False
|
||||||
|
|
||||||
# Start the userclient thread
|
if self.monitor.waitForAbort(0.05):
|
||||||
if not self.user_running:
|
|
||||||
self.user_running = True
|
|
||||||
self.user.start()
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
if monitor.waitForAbort(3):
|
|
||||||
# Abort was requested while waiting.
|
|
||||||
break
|
|
||||||
|
|
||||||
if monitor.waitForAbort(0.05):
|
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
break
|
break
|
||||||
# Terminating PlexKodiConnect
|
|
||||||
|
|
||||||
# Tell all threads to terminate (e.g. several lib sync threads)
|
# Tell all threads to terminate (e.g. several lib sync threads)
|
||||||
state.STOP_PKC = True
|
app.APP.stop_pkc = True
|
||||||
utils.window('plex_service_started', clear=True)
|
utils.window('plex_service_started', clear=True)
|
||||||
LOG.info("======== STOP %s ========", v.ADDON_NAME)
|
LOG.info("======== STOP %s ========", v.ADDON_NAME)
|
||||||
|
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
THREAD SAFE
|
|
||||||
"""
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
from threading import Lock, RLock
|
|
||||||
|
|
||||||
|
|
||||||
# LOCKS
|
|
||||||
####################
|
|
||||||
# Need to lock all methods and functions messing with Plex Companion subscribers
|
|
||||||
LOCK_SUBSCRIBER = RLock()
|
|
||||||
# Need to lock everything messing with Kodi/PKC playqueues
|
|
||||||
LOCK_PLAYQUEUES = RLock()
|
|
||||||
# Necessary to temporarily hold back librarysync/websocket listener when doing
|
|
||||||
# a full sync
|
|
||||||
LOCK_PLAYLISTS = Lock()
|
|
||||||
|
|
||||||
# Quit PKC
|
|
||||||
STOP_PKC = False
|
|
||||||
|
|
||||||
# URL of our current PMS
|
|
||||||
PMS_SERVER = None
|
|
||||||
# Usually triggered by another Python instance - will have to be set (by
|
|
||||||
# polling window) through e.g. librarysync thread
|
|
||||||
SUSPEND_LIBRARY_THREAD = False
|
|
||||||
# Set if user decided to cancel sync
|
|
||||||
STOP_SYNC = False
|
|
||||||
# Set during media playback if PKC should not do any syncs. Will NOT
|
|
||||||
# suspend synching of playstate progress
|
|
||||||
SUSPEND_SYNC = False
|
|
||||||
# Could we access the paths?
|
|
||||||
PATH_VERIFIED = False
|
|
||||||
# Set if a Plex-Kodi DB sync is being done - along with
|
|
||||||
# window('plex_dbScan') set to 'true'
|
|
||||||
DB_SCAN = False
|
|
||||||
# Plex Media Server Status - along with window('plex_serverStatus')
|
|
||||||
PMS_STATUS = False
|
|
||||||
# When the userclient needs to wait
|
|
||||||
SUSPEND_USER_CLIENT = False
|
|
||||||
# Plex home user? Then "False". Along with window('plex_restricteduser')
|
|
||||||
RESTRICTED_USER = False
|
|
||||||
# Direct Paths (True) or Addon Paths (False)? Along with
|
|
||||||
# window('useDirectPaths')
|
|
||||||
DIRECT_PATHS = False
|
|
||||||
# Shall we replace custom user ratings with the number of versions available?
|
|
||||||
INDICATE_MEDIA_VERSIONS = False
|
|
||||||
# Will sync movie trailer differently: either play trailer directly or show
|
|
||||||
# all the Plex extras for the user to choose
|
|
||||||
SHOW_EXTRAS_INSTEAD_OF_PLAYING_TRAILER = False
|
|
||||||
# Do we need to run a special library scan?
|
|
||||||
RUN_LIB_SCAN = None
|
|
||||||
# Number of items to fetch and display in widgets
|
|
||||||
FETCH_PMS_ITEM_NUMBER = None
|
|
||||||
# Hack to force Kodi widget for "in progress" to show up if it was empty before
|
|
||||||
FORCE_RELOAD_SKIN = True
|
|
||||||
|
|
||||||
# Stemming from the PKC settings.xml
|
|
||||||
# Shall we show Kodi dialogs when synching?
|
|
||||||
SYNC_DIALOG = True
|
|
||||||
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left to sync)
|
|
||||||
IMAGE_SYNC_NOTIFICATIONS = True
|
|
||||||
# Only sync specific Plex playlists to Kodi?
|
|
||||||
SYNC_SPECIFIC_PLEX_PLAYLISTS = False
|
|
||||||
# Only sync specific Kodi playlists to Plex?
|
|
||||||
SYNC_SPECIFIC_KODI_PLAYLISTS = False
|
|
||||||
# Is synching of Plex music enabled?
|
|
||||||
ENABLE_MUSIC = True
|
|
||||||
# How often shall we sync?
|
|
||||||
FULL_SYNC_INTERVALL = 0
|
|
||||||
# Background Sync disabled?
|
|
||||||
BACKGROUND_SYNC_DISABLED = False
|
|
||||||
# How long shall we wait with synching a new item to make sure Plex got all
|
|
||||||
# metadata?
|
|
||||||
BACKGROUNDSYNC_SAFTYMARGIN = 0
|
|
||||||
# How many threads to download Plex metadata on sync?
|
|
||||||
SYNC_THREAD_NUMBER = 0
|
|
||||||
# What's the time offset between the PMS and Kodi?
|
|
||||||
KODI_PLEX_TIME_OFFSET = 0.0
|
|
||||||
|
|
||||||
# Path remapping mechanism (e.g. smb paths)
|
|
||||||
# Do we replace \\myserver\path to smb://myserver/path?
|
|
||||||
REPLACE_SMB_PATH = False
|
|
||||||
# Do we generally remap?
|
|
||||||
REMAP_PATH = False
|
|
||||||
# Mappings for REMAP_PATH:
|
|
||||||
remapSMBmovieOrg = None
|
|
||||||
remapSMBmovieNew = None
|
|
||||||
remapSMBtvOrg = None
|
|
||||||
remapSMBtvNew = None
|
|
||||||
remapSMBmusicOrg = None
|
|
||||||
remapSMBmusicNew = None
|
|
||||||
remapSMBphotoOrg = None
|
|
||||||
remapSMBphotoNew = None
|
|
||||||
|
|
||||||
# Shall we verify SSL certificates?
|
|
||||||
VERIFY_SSL_CERT = False
|
|
||||||
# Do we have an ssl certificate for PKC we need to use?
|
|
||||||
SSL_CERT_PATH = None
|
|
||||||
# Along with window('plex_authenticated')
|
|
||||||
AUTHENTICATED = False
|
|
||||||
# plex.tv username
|
|
||||||
PLEX_USERNAME = None
|
|
||||||
# Token for that user for plex.tv
|
|
||||||
PLEX_TOKEN = None
|
|
||||||
# Plex token for the active PMS for the active user
|
|
||||||
# (might be diffent to PLEX_TOKEN)
|
|
||||||
PMS_TOKEN = None
|
|
||||||
# Plex ID of that user (e.g. for plex.tv) as a STRING
|
|
||||||
PLEX_USER_ID = None
|
|
||||||
# Token passed along, e.g. if playback initiated by Plex Companion. Might be
|
|
||||||
# another user playing something! Token identifies user
|
|
||||||
PLEX_TRANSIENT_TOKEN = None
|
|
||||||
|
|
||||||
# Plex Companion Queue()
|
|
||||||
COMPANION_QUEUE = None
|
|
||||||
# Command Pipeline Queue()
|
|
||||||
COMMAND_PIPELINE_QUEUE = None
|
|
||||||
# Websocket_client queue to communicate with librarysync
|
|
||||||
WEBSOCKET_QUEUE = None
|
|
||||||
|
|
||||||
# Which Kodi player is/has been active? (either int 1, 2 or 3)
|
|
||||||
ACTIVE_PLAYERS = set()
|
|
||||||
# Failsafe for throwing an empty video back to Kodi's setResolvedUrl to set
|
|
||||||
# up our own playlist from the very beginning
|
|
||||||
PKC_CAUSED_STOP = False
|
|
||||||
# Flag if the 0 length PKC video has already failed so we can start resolving
|
|
||||||
# playback (set in player.py)
|
|
||||||
PKC_CAUSED_STOP_DONE = True
|
|
||||||
|
|
||||||
# Kodi player states - here, initial values are set
|
|
||||||
PLAYER_STATES = {
|
|
||||||
0: {},
|
|
||||||
1: {},
|
|
||||||
2: {}
|
|
||||||
}
|
|
||||||
# The LAST playstate once playback is finished
|
|
||||||
OLD_PLAYER_STATES = {
|
|
||||||
0: {},
|
|
||||||
1: {},
|
|
||||||
2: {}
|
|
||||||
}
|
|
||||||
# "empty" dict for the PLAYER_STATES above. Use copy.deepcopy to duplicate!
|
|
||||||
PLAYSTATE = {
|
|
||||||
'type': None,
|
|
||||||
'time': {
|
|
||||||
'hours': 0,
|
|
||||||
'minutes': 0,
|
|
||||||
'seconds': 0,
|
|
||||||
'milliseconds': 0},
|
|
||||||
'totaltime': {
|
|
||||||
'hours': 0,
|
|
||||||
'minutes': 0,
|
|
||||||
'seconds': 0,
|
|
||||||
'milliseconds': 0},
|
|
||||||
'speed': 0,
|
|
||||||
'shuffled': False,
|
|
||||||
'repeat': 'off',
|
|
||||||
'position': None,
|
|
||||||
'playlistid': None,
|
|
||||||
'currentvideostream': -1,
|
|
||||||
'currentaudiostream': -1,
|
|
||||||
'subtitleenabled': False,
|
|
||||||
'currentsubtitle': -1,
|
|
||||||
'file': None,
|
|
||||||
'kodi_id': None,
|
|
||||||
'kodi_type': None,
|
|
||||||
'plex_id': None,
|
|
||||||
'plex_type': None,
|
|
||||||
'container_key': None,
|
|
||||||
'volume': 100,
|
|
||||||
'muted': False,
|
|
||||||
'playmethod': None,
|
|
||||||
'playcount': None
|
|
||||||
}
|
|
||||||
PLAYED_INFO = {}
|
|
||||||
# Set by SpecialMonitor - did user choose to resume playback or start from the
|
|
||||||
# beginning?
|
|
||||||
RESUME_PLAYBACK = False
|
|
||||||
# Was the playback initiated by the user using the Kodi context menu?
|
|
||||||
CONTEXT_MENU_PLAY = False
|
|
||||||
# Set by context menu - shall we force-transcode the next playing item?
|
|
||||||
FORCE_TRANSCODE = False
|
|
||||||
|
|
||||||
# Kodi webserver details
|
|
||||||
WEBSERVER_PORT = 8080
|
|
||||||
WEBSERVER_USERNAME = 'kodi'
|
|
||||||
WEBSERVER_PASSWORD = ''
|
|
||||||
WEBSERVER_HOST = 'localhost'
|
|
|
@ -5,8 +5,8 @@ from logging import getLogger
|
||||||
import xbmc
|
import xbmc
|
||||||
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
from .downloadutils import DownloadUtils as DU
|
||||||
from . import library_sync
|
from . import library_sync, timing
|
||||||
from . import backgroundthread, utils, path_ops, artwork, variables as v, state
|
from . import backgroundthread, utils, path_ops, artwork, variables as v, app
|
||||||
from . import plex_db, kodi_db
|
from . import plex_db, kodi_db
|
||||||
|
|
||||||
LOG = getLogger('PLEX.sync')
|
LOG = getLogger('PLEX.sync')
|
||||||
|
@ -18,10 +18,10 @@ def set_library_scan_toggle(boolean=True):
|
||||||
"""
|
"""
|
||||||
if not boolean:
|
if not boolean:
|
||||||
# Deactivate
|
# Deactivate
|
||||||
state.DB_SCAN = False
|
app.SYNC.db_scan = False
|
||||||
utils.window('plex_dbScan', clear=True)
|
utils.window('plex_dbScan', clear=True)
|
||||||
else:
|
else:
|
||||||
state.DB_SCAN = True
|
app.SYNC.db_scan = True
|
||||||
utils.window('plex_dbScan', value="true")
|
utils.window('plex_dbScan', value="true")
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,11 +40,8 @@ class Sync(backgroundthread.KillableThread):
|
||||||
# self.lock = backgroundthread.threading.Lock()
|
# self.lock = backgroundthread.threading.Lock()
|
||||||
super(Sync, self).__init__()
|
super(Sync, self).__init__()
|
||||||
|
|
||||||
def isCanceled(self):
|
|
||||||
return state.STOP_PKC
|
|
||||||
|
|
||||||
def isSuspended(self):
|
def isSuspended(self):
|
||||||
return state.SUSPEND_LIBRARY_THREAD
|
return self._suspended or app.SYNC.suspend_library_thread
|
||||||
|
|
||||||
def show_kodi_note(self, message, icon="plex", force=False):
|
def show_kodi_note(self, message, icon="plex", force=False):
|
||||||
"""
|
"""
|
||||||
|
@ -54,7 +51,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
icon: "plex": shows Plex icon
|
icon: "plex": shows Plex icon
|
||||||
"error": shows Kodi error icon
|
"error": shows Kodi error icon
|
||||||
"""
|
"""
|
||||||
if not force and state.SYNC_DIALOG is not True and self.force_dialog is not True:
|
if not force and app.SYNC.sync_dialog is not True and self.force_dialog is not True:
|
||||||
return
|
return
|
||||||
if icon == "plex":
|
if icon == "plex":
|
||||||
utils.dialog('notification',
|
utils.dialog('notification',
|
||||||
|
@ -70,14 +67,14 @@ class Sync(backgroundthread.KillableThread):
|
||||||
|
|
||||||
def triage_lib_scans(self):
|
def triage_lib_scans(self):
|
||||||
"""
|
"""
|
||||||
Decides what to do if state.RUN_LIB_SCAN has been set. E.g. manually
|
Decides what to do if app.SYNC.run_lib_scan has been set. E.g. manually
|
||||||
triggered full or repair syncs
|
triggered full or repair syncs
|
||||||
"""
|
"""
|
||||||
if state.RUN_LIB_SCAN in ("full", "repair"):
|
if app.SYNC.run_lib_scan in ("full", "repair"):
|
||||||
set_library_scan_toggle()
|
set_library_scan_toggle()
|
||||||
LOG.info('Full library scan requested, starting')
|
LOG.info('Full library scan requested, starting')
|
||||||
self.start_library_sync(show_dialog=True,
|
self.start_library_sync(show_dialog=True,
|
||||||
repair=state.RUN_LIB_SCAN == 'repair',
|
repair=app.SYNC.run_lib_scan == 'repair',
|
||||||
block=True)
|
block=True)
|
||||||
if self.sync_successful:
|
if self.sync_successful:
|
||||||
# Full library sync finished
|
# Full library sync finished
|
||||||
|
@ -85,7 +82,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
elif not self.isSuspended() and not self.isCanceled():
|
elif not self.isSuspended() and not self.isCanceled():
|
||||||
# ERROR in library sync
|
# ERROR in library sync
|
||||||
self.show_kodi_note(utils.lang(39410), icon='error')
|
self.show_kodi_note(utils.lang(39410), icon='error')
|
||||||
elif state.RUN_LIB_SCAN == 'fanart':
|
elif app.SYNC.run_lib_scan == 'fanart':
|
||||||
# Only look for missing fanart (No) or refresh all fanart (Yes)
|
# Only look for missing fanart (No) or refresh all fanart (Yes)
|
||||||
from .windows import optionsdialog
|
from .windows import optionsdialog
|
||||||
refresh = optionsdialog.show(utils.lang(29999),
|
refresh = optionsdialog.show(utils.lang(29999),
|
||||||
|
@ -99,7 +96,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
message=utils.lang(30015),
|
message=utils.lang(30015),
|
||||||
icon='{plex}',
|
icon='{plex}',
|
||||||
sound=False)
|
sound=False)
|
||||||
elif state.RUN_LIB_SCAN == 'textures':
|
elif app.SYNC.run_lib_scan == 'textures':
|
||||||
LOG.info("Caching of images requested")
|
LOG.info("Caching of images requested")
|
||||||
if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)):
|
if not utils.yesno_dialog("Image Texture Cache", utils.lang(39250)):
|
||||||
return
|
return
|
||||||
|
@ -113,7 +110,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
Hit this after the full sync has finished
|
Hit this after the full sync has finished
|
||||||
"""
|
"""
|
||||||
self.sync_successful = successful
|
self.sync_successful = successful
|
||||||
self.last_full_sync = utils.unix_timestamp()
|
self.last_full_sync = timing.unix_timestamp()
|
||||||
set_library_scan_toggle(boolean=False)
|
set_library_scan_toggle(boolean=False)
|
||||||
if successful:
|
if successful:
|
||||||
self.show_kodi_note(utils.lang(39407))
|
self.show_kodi_note(utils.lang(39407))
|
||||||
|
@ -129,7 +126,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
|
|
||||||
def start_library_sync(self, show_dialog=None, repair=False, block=False):
|
def start_library_sync(self, show_dialog=None, repair=False, block=False):
|
||||||
set_library_scan_toggle(boolean=True)
|
set_library_scan_toggle(boolean=True)
|
||||||
show_dialog = show_dialog if show_dialog is not None else state.SYNC_DIALOG
|
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)
|
library_sync.start(show_dialog, repair, self.on_library_scan_finished)
|
||||||
# if block:
|
# if block:
|
||||||
# self.lock.acquire()
|
# self.lock.acquire()
|
||||||
|
@ -153,7 +150,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
|
|
||||||
def on_fanart_download_finished(self, successful):
|
def on_fanart_download_finished(self, successful):
|
||||||
# FanartTV lookup completed
|
# FanartTV lookup completed
|
||||||
if successful and state.SYNC_DIALOG:
|
if successful and app.SYNC.sync_dialog:
|
||||||
utils.dialog('notification',
|
utils.dialog('notification',
|
||||||
heading='{plex}',
|
heading='{plex}',
|
||||||
message=utils.lang(30019),
|
message=utils.lang(30019),
|
||||||
|
@ -174,7 +171,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
try:
|
try:
|
||||||
self._run_internal()
|
self._run_internal()
|
||||||
except:
|
except:
|
||||||
state.DB_SCAN = False
|
app.SYNC.db_scan = False
|
||||||
utils.window('plex_dbScan', clear=True)
|
utils.window('plex_dbScan', clear=True)
|
||||||
utils.ERROR(txt='sync.py crashed', notify=True)
|
utils.ERROR(txt='sync.py crashed', notify=True)
|
||||||
raise
|
raise
|
||||||
|
@ -188,12 +185,12 @@ class Sync(backgroundthread.KillableThread):
|
||||||
last_time_sync = 0
|
last_time_sync = 0
|
||||||
one_day_in_seconds = 60 * 60 * 24
|
one_day_in_seconds = 60 * 60 * 24
|
||||||
# Link to Websocket queue
|
# Link to Websocket queue
|
||||||
queue = state.WEBSOCKET_QUEUE
|
queue = app.APP.websocket_queue
|
||||||
|
|
||||||
# Kodi Version supported by PKC?
|
# Kodi Version supported by PKC?
|
||||||
if (not path_ops.exists(v.DB_VIDEO_PATH) or
|
if (not path_ops.exists(v.DB_VIDEO_PATH) or
|
||||||
not path_ops.exists(v.DB_TEXTURE_PATH) or
|
not path_ops.exists(v.DB_TEXTURE_PATH) or
|
||||||
(state.ENABLE_MUSIC and not path_ops.exists(v.DB_MUSIC_PATH))):
|
(app.SYNC.enable_music and not path_ops.exists(v.DB_MUSIC_PATH))):
|
||||||
# Database does not exists
|
# Database does not exists
|
||||||
LOG.error('The current Kodi version is incompatible')
|
LOG.error('The current Kodi version is incompatible')
|
||||||
LOG.error('Current Kodi version: %s', utils.try_decode(
|
LOG.error('Current Kodi version: %s', utils.try_decode(
|
||||||
|
@ -242,7 +239,7 @@ class Sync(backgroundthread.KillableThread):
|
||||||
self.force_dialog = True
|
self.force_dialog = True
|
||||||
# Initialize time offset Kodi - PMS
|
# Initialize time offset Kodi - PMS
|
||||||
library_sync.sync_pms_time()
|
library_sync.sync_pms_time()
|
||||||
last_time_sync = utils.unix_timestamp()
|
last_time_sync = timing.unix_timestamp()
|
||||||
LOG.info('Initial start-up full sync starting')
|
LOG.info('Initial start-up full sync starting')
|
||||||
xbmc.executebuiltin('InhibitIdleShutdown(true)')
|
xbmc.executebuiltin('InhibitIdleShutdown(true)')
|
||||||
# This call will block until scan is completed
|
# This call will block until scan is completed
|
||||||
|
@ -268,9 +265,9 @@ class Sync(backgroundthread.KillableThread):
|
||||||
# First sync upon PKC restart. Skipped if very first sync upon
|
# First sync upon PKC restart. Skipped if very first sync upon
|
||||||
# PKC installation has been completed
|
# PKC installation has been completed
|
||||||
LOG.info('Doing initial sync on Kodi startup')
|
LOG.info('Doing initial sync on Kodi startup')
|
||||||
if state.SUSPEND_SYNC:
|
if app.SYNC.suspend_sync:
|
||||||
LOG.warning('Forcing startup sync even if Kodi is playing')
|
LOG.warning('Forcing startup sync even if Kodi is playing')
|
||||||
state.SUSPEND_SYNC = False
|
app.SYNC.suspend_sync = False
|
||||||
self.start_library_sync(block=True)
|
self.start_library_sync(block=True)
|
||||||
if self.sync_successful:
|
if self.sync_successful:
|
||||||
initial_sync_done = True
|
initial_sync_done = True
|
||||||
|
@ -285,27 +282,27 @@ class Sync(backgroundthread.KillableThread):
|
||||||
xbmc.sleep(1000)
|
xbmc.sleep(1000)
|
||||||
|
|
||||||
# Currently no db scan, so we could start a new scan
|
# Currently no db scan, so we could start a new scan
|
||||||
elif state.DB_SCAN is False:
|
elif app.SYNC.db_scan is False:
|
||||||
# Full scan was requested from somewhere else, e.g. userclient
|
# Full scan was requested from somewhere else
|
||||||
if state.RUN_LIB_SCAN is not None:
|
if app.SYNC.run_lib_scan is not None:
|
||||||
# Force-show dialogs since they are user-initiated
|
# Force-show dialogs since they are user-initiated
|
||||||
self.force_dialog = True
|
self.force_dialog = True
|
||||||
self.triage_lib_scans()
|
self.triage_lib_scans()
|
||||||
self.force_dialog = False
|
self.force_dialog = False
|
||||||
# Reset the flag
|
# Reset the flag
|
||||||
state.RUN_LIB_SCAN = None
|
app.SYNC.run_lib_scan = None
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Standard syncs - don't force-show dialogs
|
# Standard syncs - don't force-show dialogs
|
||||||
now = utils.unix_timestamp()
|
now = timing.unix_timestamp()
|
||||||
if (now - self.last_full_sync > state.FULL_SYNC_INTERVALL):
|
if (now - self.last_full_sync > app.SYNC.full_sync_intervall):
|
||||||
LOG.info('Doing scheduled full library scan')
|
LOG.info('Doing scheduled full library scan')
|
||||||
self.start_library_sync()
|
self.start_library_sync()
|
||||||
elif now - last_time_sync > one_day_in_seconds:
|
elif now - last_time_sync > one_day_in_seconds:
|
||||||
LOG.info('Starting daily time sync')
|
LOG.info('Starting daily time sync')
|
||||||
library_sync.sync_pms_time()
|
library_sync.sync_pms_time()
|
||||||
last_time_sync = now
|
last_time_sync = now
|
||||||
elif not state.BACKGROUND_SYNC_DISABLED:
|
elif not app.SYNC.background_sync_disabled:
|
||||||
# Check back whether we should process something Only do
|
# Check back whether we should process something Only do
|
||||||
# this once a while (otherwise, potentially many screen
|
# this once a while (otherwise, potentially many screen
|
||||||
# refreshes lead to flickering)
|
# refreshes lead to flickering)
|
||||||
|
|
94
resources/lib/timing.py
Normal file
94
resources/lib/timing.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from time import localtime, strftime
|
||||||
|
|
||||||
|
EPOCH = datetime.utcfromtimestamp(0)
|
||||||
|
|
||||||
|
# What's the time offset between the PMS and Kodi?
|
||||||
|
KODI_PLEX_TIME_OFFSET = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def unix_timestamp(seconds_into_the_future=None):
|
||||||
|
"""
|
||||||
|
Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as
|
||||||
|
an integer.
|
||||||
|
|
||||||
|
Optionally, pass seconds_into_the_future: positive int's will result in a
|
||||||
|
future timestamp, negative the past
|
||||||
|
"""
|
||||||
|
if seconds_into_the_future:
|
||||||
|
future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future)
|
||||||
|
else:
|
||||||
|
future = datetime.utcnow()
|
||||||
|
return int((future - EPOCH).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
def unix_date_to_kodi(unix_kodi_time):
|
||||||
|
"""
|
||||||
|
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
||||||
|
propper, human-readable time stamp used by Kodi
|
||||||
|
|
||||||
|
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
||||||
|
"""
|
||||||
|
return strftime('%Y-%m-%d %H:%M:%S', localtime(float(unix_kodi_time)))
|
||||||
|
|
||||||
|
|
||||||
|
def plex_date_to_kodi(plex_timestamp):
|
||||||
|
"""
|
||||||
|
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
||||||
|
propper, human-readable time stamp used by Kodi
|
||||||
|
|
||||||
|
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
||||||
|
"""
|
||||||
|
return strftime('%Y-%m-%d %H:%M:%S',
|
||||||
|
localtime(float(plex_timestamp) + KODI_PLEX_TIME_OFFSET))
|
||||||
|
|
||||||
|
|
||||||
|
def kodi_timestamp(plex_timestamp):
|
||||||
|
return unix_date_to_kodi(plex_timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
def kodi_now():
|
||||||
|
return unix_date_to_kodi(unix_timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
def millis_to_kodi_time(milliseconds):
|
||||||
|
"""
|
||||||
|
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
|
||||||
|
{
|
||||||
|
'hours': [int],
|
||||||
|
'minutes': [int],
|
||||||
|
'seconds'[int],
|
||||||
|
'milliseconds': [int]
|
||||||
|
}
|
||||||
|
Pass in the time in milliseconds as an int
|
||||||
|
"""
|
||||||
|
seconds = int(milliseconds / 1000)
|
||||||
|
minutes = int(seconds / 60)
|
||||||
|
seconds = seconds % 60
|
||||||
|
hours = int(minutes / 60)
|
||||||
|
minutes = minutes % 60
|
||||||
|
milliseconds = milliseconds % 1000
|
||||||
|
return {'hours': hours,
|
||||||
|
'minutes': minutes,
|
||||||
|
'seconds': seconds,
|
||||||
|
'milliseconds': milliseconds}
|
||||||
|
|
||||||
|
|
||||||
|
def kodi_time_to_millis(time):
|
||||||
|
"""
|
||||||
|
Converts the Kodi time dict
|
||||||
|
{
|
||||||
|
'hours': [int],
|
||||||
|
'minutes': [int],
|
||||||
|
'seconds'[int],
|
||||||
|
'milliseconds': [int]
|
||||||
|
}
|
||||||
|
to milliseconds [int]. Will not return negative results but 0!
|
||||||
|
"""
|
||||||
|
ret = (time['hours'] * 3600 +
|
||||||
|
time['minutes'] * 60 +
|
||||||
|
time['seconds']) * 1000 + time['milliseconds']
|
||||||
|
return 0 if ret < 0 else ret
|
|
@ -1,356 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
|
||||||
from logging import getLogger
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from xbmc import sleep, executebuiltin
|
|
||||||
|
|
||||||
from .windows import userselect
|
|
||||||
from .downloadutils import DownloadUtils as DU
|
|
||||||
from . import utils
|
|
||||||
from . import path_ops
|
|
||||||
from . import plex_functions as PF
|
|
||||||
from . import variables as v
|
|
||||||
from . import state
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
LOG = getLogger('PLEX.userclient')
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
@utils.thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
|
|
||||||
class UserClient(Thread):
|
|
||||||
"""
|
|
||||||
Manage Plex users
|
|
||||||
"""
|
|
||||||
# Borg - multiple instances, shared state
|
|
||||||
__shared_state = {}
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__dict__ = self.__shared_state
|
|
||||||
|
|
||||||
self.auth = True
|
|
||||||
self.retry = 0
|
|
||||||
self.aborted = False
|
|
||||||
|
|
||||||
self.user = None
|
|
||||||
self.has_access = True
|
|
||||||
|
|
||||||
self.server = None
|
|
||||||
self.server_name = None
|
|
||||||
self.machine_identifier = None
|
|
||||||
self.token = None
|
|
||||||
self.ssl = None
|
|
||||||
self.sslcert = None
|
|
||||||
|
|
||||||
self.do_utils = None
|
|
||||||
|
|
||||||
Thread.__init__(self)
|
|
||||||
|
|
||||||
def get_server(self):
|
|
||||||
"""
|
|
||||||
Get the current PMS' URL
|
|
||||||
"""
|
|
||||||
# Original host
|
|
||||||
self.server_name = utils.settings('plex_servername')
|
|
||||||
https = utils.settings('https') == "true"
|
|
||||||
host = utils.settings('ipaddress')
|
|
||||||
port = utils.settings('port')
|
|
||||||
self.machine_identifier = utils.settings('plex_machineIdentifier')
|
|
||||||
if not host:
|
|
||||||
LOG.debug("No server information saved.")
|
|
||||||
return False
|
|
||||||
server = host + ":" + port
|
|
||||||
# If https is true
|
|
||||||
if https:
|
|
||||||
server = "https://%s" % server
|
|
||||||
# If https is false
|
|
||||||
else:
|
|
||||||
server = "http://%s" % server
|
|
||||||
# User entered IP; we need to get the machineIdentifier
|
|
||||||
if not self.machine_identifier:
|
|
||||||
self.machine_identifier = PF.GetMachineIdentifier(server)
|
|
||||||
if not self.machine_identifier:
|
|
||||||
self.machine_identifier = ''
|
|
||||||
utils.settings('plex_machineIdentifier',
|
|
||||||
value=self.machine_identifier)
|
|
||||||
LOG.debug('Returning active server: %s', server)
|
|
||||||
return server
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_ssl_verify():
|
|
||||||
"""
|
|
||||||
Do we need to verify the SSL certificate? Return None if that is the
|
|
||||||
case, else False
|
|
||||||
"""
|
|
||||||
return None if utils.settings('sslverify') == 'true' else False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_ssl_certificate():
|
|
||||||
"""
|
|
||||||
Client side certificate
|
|
||||||
"""
|
|
||||||
return None if utils.settings('sslcert') == 'None' \
|
|
||||||
else utils.settings('sslcert')
|
|
||||||
|
|
||||||
def set_user_prefs(self):
|
|
||||||
"""
|
|
||||||
Load a user's profile picture
|
|
||||||
"""
|
|
||||||
LOG.debug('Setting user preferences')
|
|
||||||
# Only try to get user avatar if there is a token
|
|
||||||
if self.token:
|
|
||||||
url = PF.GetUserArtworkURL(self.user)
|
|
||||||
if url:
|
|
||||||
utils.window('PlexUserImage', value=url)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_access():
|
|
||||||
# Plex: always return True for now
|
|
||||||
return True
|
|
||||||
|
|
||||||
def load_user(self, username, user_id, usertoken, authenticated=False):
|
|
||||||
"""
|
|
||||||
Load the current user's details for PKC
|
|
||||||
"""
|
|
||||||
LOG.debug('Loading current user')
|
|
||||||
self.token = usertoken
|
|
||||||
self.server = self.get_server()
|
|
||||||
self.ssl = self.get_ssl_verify()
|
|
||||||
self.sslcert = self.get_ssl_certificate()
|
|
||||||
|
|
||||||
if authenticated is False:
|
|
||||||
if self.server is None:
|
|
||||||
return False
|
|
||||||
LOG.debug('Testing validity of current token')
|
|
||||||
res = PF.check_connection(self.server,
|
|
||||||
token=self.token,
|
|
||||||
verifySSL=self.ssl)
|
|
||||||
if res is False:
|
|
||||||
# PMS probably offline
|
|
||||||
return False
|
|
||||||
elif res == 401:
|
|
||||||
LOG.error('Token is no longer valid')
|
|
||||||
return 401
|
|
||||||
elif res >= 400:
|
|
||||||
LOG.error('Answer from PMS is not as expected. Retrying')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Set to windows property
|
|
||||||
state.PLEX_USER_ID = user_id or None
|
|
||||||
state.PLEX_USERNAME = username
|
|
||||||
# This is the token for the current PMS (might also be '')
|
|
||||||
utils.window('pms_token', value=usertoken)
|
|
||||||
state.PMS_TOKEN = usertoken
|
|
||||||
# This is the token for plex.tv for the current user
|
|
||||||
# Is only '' if user is not signed in to plex.tv
|
|
||||||
utils.window('plex_token', value=utils.settings('plexToken'))
|
|
||||||
state.PLEX_TOKEN = utils.settings('plexToken') or None
|
|
||||||
utils.window('plex_restricteduser',
|
|
||||||
value=utils.settings('plex_restricteduser'))
|
|
||||||
state.RESTRICTED_USER = True \
|
|
||||||
if utils.settings('plex_restricteduser') == 'true' else False
|
|
||||||
utils.window('pms_server', value=self.server)
|
|
||||||
state.PMS_SERVER = self.server
|
|
||||||
utils.window('plex_machineIdentifier', value=self.machine_identifier)
|
|
||||||
utils.window('plex_servername', value=self.server_name)
|
|
||||||
utils.window('plex_authenticated', value='true')
|
|
||||||
state.AUTHENTICATED = True
|
|
||||||
|
|
||||||
utils.window('useDirectPaths',
|
|
||||||
value='true' if utils.settings('useDirectPaths') == "1"
|
|
||||||
else 'false')
|
|
||||||
state.DIRECT_PATHS = True if utils.settings('useDirectPaths') == "1" \
|
|
||||||
else False
|
|
||||||
state.INDICATE_MEDIA_VERSIONS = True \
|
|
||||||
if utils.settings('indicate_media_versions') == "true" else False
|
|
||||||
utils.window('plex_force_transcode_pix',
|
|
||||||
value='true' if utils.settings('force_transcode_pix') == "1"
|
|
||||||
else 'false')
|
|
||||||
|
|
||||||
# Start DownloadUtils session
|
|
||||||
self.do_utils = DU()
|
|
||||||
self.do_utils.startSession(reset=True)
|
|
||||||
# Set user preferences in settings
|
|
||||||
self.user = username
|
|
||||||
self.set_user_prefs()
|
|
||||||
|
|
||||||
# Writing values to settings file
|
|
||||||
utils.settings('username', value=username)
|
|
||||||
utils.settings('userid', value=user_id)
|
|
||||||
utils.settings('accessToken', value=usertoken)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
"""
|
|
||||||
Authenticate the current user
|
|
||||||
"""
|
|
||||||
LOG.debug('Authenticating user')
|
|
||||||
|
|
||||||
# Give attempts at entering password / selecting user
|
|
||||||
if self.retry > 0:
|
|
||||||
if not self.aborted:
|
|
||||||
LOG.error("Too many retries to login.")
|
|
||||||
state.PMS_STATUS = 'Stop'
|
|
||||||
# Failed to authenticate. Did you login to plex.tv?
|
|
||||||
utils.messageDialog(utils.lang(29999),utils.lang(39023))
|
|
||||||
executebuiltin(
|
|
||||||
'Addon.OpenSettings(plugin.video.plexkodiconnect)')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If there's no settings.xml
|
|
||||||
if not path_ops.exists("%ssettings.xml" % v.ADDON_PROFILE):
|
|
||||||
LOG.error("Error, no settings.xml found.")
|
|
||||||
self.auth = False
|
|
||||||
return False
|
|
||||||
server = self.get_server()
|
|
||||||
# If there is no server we can connect to
|
|
||||||
if not server:
|
|
||||||
LOG.info("Missing server information.")
|
|
||||||
self.auth = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If there is a username in the settings, try authenticating
|
|
||||||
username = utils.settings('username')
|
|
||||||
userId = utils.settings('userid')
|
|
||||||
usertoken = utils.settings('accessToken')
|
|
||||||
enforceLogin = utils.settings('enforceUserLogin')
|
|
||||||
# Found a user in the settings, try to authenticate
|
|
||||||
if username and enforceLogin == 'false':
|
|
||||||
LOG.debug('Trying to authenticate with old settings')
|
|
||||||
answ = self.load_user(username,
|
|
||||||
userId,
|
|
||||||
usertoken,
|
|
||||||
authenticated=False)
|
|
||||||
if answ is True:
|
|
||||||
# SUCCESS: loaded a user from the settings
|
|
||||||
return True
|
|
||||||
elif answ == 401:
|
|
||||||
LOG.error("User token no longer valid. Sign user out")
|
|
||||||
utils.settings('username', value='')
|
|
||||||
utils.settings('userid', value='')
|
|
||||||
utils.settings('accessToken', value='')
|
|
||||||
else:
|
|
||||||
LOG.debug("Could not yet authenticate user")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Could not use settings - try to get Plex user list from plex.tv
|
|
||||||
plextoken = utils.settings('plexToken')
|
|
||||||
if plextoken:
|
|
||||||
LOG.info("Trying to connect to plex.tv to get a user list")
|
|
||||||
user, self.aborted = userselect.start()
|
|
||||||
if not user:
|
|
||||||
# FAILURE: Something went wrong, try again
|
|
||||||
self.auth = True
|
|
||||||
self.retry += 1
|
|
||||||
return False
|
|
||||||
username = user.title
|
|
||||||
user_id = user.id
|
|
||||||
usertoken = user.authToken
|
|
||||||
else:
|
|
||||||
LOG.info("Trying to authenticate without a token")
|
|
||||||
username = ''
|
|
||||||
user_id = ''
|
|
||||||
usertoken = ''
|
|
||||||
|
|
||||||
if self.load_user(username, user_id, usertoken, authenticated=False):
|
|
||||||
# SUCCESS: loaded a user from the settings
|
|
||||||
return True
|
|
||||||
# Something went wrong, try again
|
|
||||||
self.auth = True
|
|
||||||
self.retry += 1
|
|
||||||
return False
|
|
||||||
|
|
||||||
def reset_client(self):
|
|
||||||
"""
|
|
||||||
Reset all user settings
|
|
||||||
"""
|
|
||||||
LOG.debug("Reset UserClient authentication.")
|
|
||||||
try:
|
|
||||||
self.do_utils.stopSession()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
utils.window('plex_authenticated', clear=True)
|
|
||||||
state.AUTHENTICATED = False
|
|
||||||
utils.window('pms_token', clear=True)
|
|
||||||
state.PLEX_TOKEN = None
|
|
||||||
state.PLEX_TRANSIENT_TOKEN = None
|
|
||||||
state.PMS_TOKEN = None
|
|
||||||
utils.window('plex_token', clear=True)
|
|
||||||
utils.window('pms_server', clear=True)
|
|
||||||
state.PMS_SERVER = None
|
|
||||||
utils.window('plex_machineIdentifier', clear=True)
|
|
||||||
utils.window('plex_servername', clear=True)
|
|
||||||
state.PLEX_USER_ID = None
|
|
||||||
state.PLEX_USERNAME = None
|
|
||||||
utils.window('plex_restricteduser', clear=True)
|
|
||||||
state.RESTRICTED_USER = False
|
|
||||||
|
|
||||||
utils.settings('username', value='')
|
|
||||||
utils.settings('userid', value='')
|
|
||||||
utils.settings('accessToken', value='')
|
|
||||||
|
|
||||||
self.token = None
|
|
||||||
self.auth = True
|
|
||||||
self.user = None
|
|
||||||
|
|
||||||
self.retry = 0
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Do the work
|
|
||||||
"""
|
|
||||||
LOG.info("----===## Starting UserClient ##===----")
|
|
||||||
stopped = self.stopped
|
|
||||||
suspended = self.suspended
|
|
||||||
while not stopped():
|
|
||||||
while suspended():
|
|
||||||
if stopped():
|
|
||||||
break
|
|
||||||
sleep(1000)
|
|
||||||
|
|
||||||
if state.PMS_STATUS == "Stop":
|
|
||||||
sleep(500)
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif state.PMS_STATUS == "401":
|
|
||||||
# Unauthorized access, revoke token
|
|
||||||
state.PMS_STATUS = 'Auth'
|
|
||||||
utils.window('plex_serverStatus', value='Auth')
|
|
||||||
self.reset_client()
|
|
||||||
sleep(3000)
|
|
||||||
|
|
||||||
if self.auth and (self.user is None):
|
|
||||||
# Try to authenticate user
|
|
||||||
if not state.PMS_STATUS or state.PMS_STATUS == "Auth":
|
|
||||||
# Set auth flag because we no longer need
|
|
||||||
# to authenticate the user
|
|
||||||
self.auth = False
|
|
||||||
if self.authenticate():
|
|
||||||
# Successfully authenticated and loaded a user
|
|
||||||
LOG.info("Successfully authenticated!")
|
|
||||||
LOG.info("Current user: %s", self.user)
|
|
||||||
LOG.info("Current userId: %s", state.PLEX_USER_ID)
|
|
||||||
self.retry = 0
|
|
||||||
state.SUSPEND_LIBRARY_THREAD = False
|
|
||||||
utils.window('plex_serverStatus', clear=True)
|
|
||||||
state.PMS_STATUS = False
|
|
||||||
|
|
||||||
if not self.auth and (self.user is None):
|
|
||||||
# Loop if no server found
|
|
||||||
server = self.get_server()
|
|
||||||
|
|
||||||
# The status Stop is for when user cancelled password dialog.
|
|
||||||
# Or retried too many times
|
|
||||||
if server and state.PMS_STATUS != "Stop":
|
|
||||||
# Only if there's information found to login
|
|
||||||
LOG.debug("Server found: %s", server)
|
|
||||||
self.auth = True
|
|
||||||
|
|
||||||
# Minimize CPU load
|
|
||||||
sleep(100)
|
|
||||||
|
|
||||||
LOG.info("##===---- UserClient Stopped ----===##")
|
|
|
@ -6,8 +6,7 @@ Various functions and decorators for PKC
|
||||||
from __future__ import absolute_import, division, unicode_literals
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlite3 import connect, OperationalError
|
from sqlite3 import connect, OperationalError
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from time import localtime, strftime
|
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
try:
|
try:
|
||||||
import xml.etree.cElementTree as etree
|
import xml.etree.cElementTree as etree
|
||||||
|
@ -19,7 +18,7 @@ except ImportError:
|
||||||
import defusedxml.ElementTree as defused_etree # etree parse unsafe
|
import defusedxml.ElementTree as defused_etree # etree parse unsafe
|
||||||
from xml.etree.ElementTree import ParseError
|
from xml.etree.ElementTree import ParseError
|
||||||
ETREE = 'ElementTree'
|
ETREE = 'ElementTree'
|
||||||
from functools import wraps, partial
|
from functools import wraps
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
@ -28,7 +27,7 @@ import xbmc
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
|
||||||
from . import path_ops, variables as v, state
|
from . import path_ops, variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -36,7 +35,6 @@ LOG = getLogger('PLEX.utils')
|
||||||
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
WINDOW = xbmcgui.Window(10000)
|
||||||
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
ADDON = xbmcaddon.Addon(id='plugin.video.plexkodiconnect')
|
||||||
EPOCH = datetime.utcfromtimestamp(0)
|
|
||||||
|
|
||||||
# Grab Plex id from '...plex_id=XXXX....'
|
# Grab Plex id from '...plex_id=XXXX....'
|
||||||
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
REGEX_PLEX_ID = re.compile(r'''plex_id=(\d+)''')
|
||||||
|
@ -107,17 +105,14 @@ def window(prop, value=None, clear=False, windowid=10000):
|
||||||
return try_decode(win.getProperty(prop))
|
return try_decode(win.getProperty(prop))
|
||||||
|
|
||||||
|
|
||||||
def plex_command(key, value):
|
def plex_command(value):
|
||||||
"""
|
"""
|
||||||
Used to funnel states between different Python instances. NOT really thread
|
Used to funnel states between different Python instances. NOT really thread
|
||||||
safe - let's hope the Kodi user can't click fast enough
|
safe - let's hope the Kodi user can't click fast enough
|
||||||
|
|
||||||
key: state.py variable
|
|
||||||
value: either 'True' or 'False'
|
|
||||||
"""
|
"""
|
||||||
while window('plex_command'):
|
while window('plex_command'):
|
||||||
xbmc.sleep(20)
|
xbmc.sleep(20)
|
||||||
window('plex_command', value='%s-%s' % (key, value))
|
window('plex_command', value=value)
|
||||||
|
|
||||||
|
|
||||||
def settings(setting, value=None):
|
def settings(setting, value=None):
|
||||||
|
@ -238,7 +233,8 @@ def ERROR(txt='', hide_tb=False, notify=False, cancel_sync=False):
|
||||||
short = str(sys.exc_info()[1])
|
short = str(sys.exc_info()[1])
|
||||||
LOG.error('Error encountered: %s - %s', txt, short)
|
LOG.error('Error encountered: %s - %s', txt, short)
|
||||||
if cancel_sync:
|
if cancel_sync:
|
||||||
state.STOP_SYNC = True
|
import app
|
||||||
|
app.SYNC.stop_sync = True
|
||||||
if hide_tb:
|
if hide_tb:
|
||||||
return short
|
return short
|
||||||
|
|
||||||
|
@ -275,47 +271,6 @@ class AttributeDict(dict):
|
||||||
return self.__unicode__().encode('utf-8')
|
return self.__unicode__().encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def millis_to_kodi_time(milliseconds):
|
|
||||||
"""
|
|
||||||
Converts time in milliseconds to the time dict used by the Kodi JSON RPC:
|
|
||||||
{
|
|
||||||
'hours': [int],
|
|
||||||
'minutes': [int],
|
|
||||||
'seconds'[int],
|
|
||||||
'milliseconds': [int]
|
|
||||||
}
|
|
||||||
Pass in the time in milliseconds as an int
|
|
||||||
"""
|
|
||||||
seconds = int(milliseconds / 1000)
|
|
||||||
minutes = int(seconds / 60)
|
|
||||||
seconds = seconds % 60
|
|
||||||
hours = int(minutes / 60)
|
|
||||||
minutes = minutes % 60
|
|
||||||
milliseconds = milliseconds % 1000
|
|
||||||
return {'hours': hours,
|
|
||||||
'minutes': minutes,
|
|
||||||
'seconds': seconds,
|
|
||||||
'milliseconds': milliseconds}
|
|
||||||
|
|
||||||
|
|
||||||
def kodi_time_to_millis(time):
|
|
||||||
"""
|
|
||||||
Converts the Kodi time dict
|
|
||||||
{
|
|
||||||
'hours': [int],
|
|
||||||
'minutes': [int],
|
|
||||||
'seconds'[int],
|
|
||||||
'milliseconds': [int]
|
|
||||||
}
|
|
||||||
to milliseconds [int]. Will not return negative results but 0!
|
|
||||||
"""
|
|
||||||
ret = (time['hours'] * 3600 +
|
|
||||||
time['minutes'] * 60 +
|
|
||||||
time['seconds']) * 1000 + time['milliseconds']
|
|
||||||
ret = 0 if ret < 0 else ret
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def cast(func, value):
|
def cast(func, value):
|
||||||
"""
|
"""
|
||||||
Cast the specified value to the specified type (returned by func). Currently this
|
Cast the specified value to the specified type (returned by func). Currently this
|
||||||
|
@ -434,47 +389,6 @@ def escape_html(string):
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
def unix_date_to_kodi(stamp):
|
|
||||||
"""
|
|
||||||
converts a Unix time stamp (seconds passed sinceJanuary 1 1970) to a
|
|
||||||
propper, human-readable time stamp used by Kodi
|
|
||||||
|
|
||||||
Output: Y-m-d h:m:s = 2009-04-05 23:16:04
|
|
||||||
|
|
||||||
None if an error was encountered
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
stamp = float(stamp) + state.KODI_PLEX_TIME_OFFSET
|
|
||||||
date_time = localtime(stamp)
|
|
||||||
localdate = strftime('%Y-%m-%d %H:%M:%S', date_time)
|
|
||||||
except:
|
|
||||||
localdate = None
|
|
||||||
return localdate
|
|
||||||
|
|
||||||
|
|
||||||
def kodi_time_to_plex(stamp):
|
|
||||||
"""
|
|
||||||
Returns a Kodi timestamp (int/float) in Plex time (subtracting the
|
|
||||||
KODI_PLEX_TIME_OFFSET)
|
|
||||||
"""
|
|
||||||
return stamp - state.KODI_PLEX_TIME_OFFSET
|
|
||||||
|
|
||||||
|
|
||||||
def unix_timestamp(seconds_into_the_future=None):
|
|
||||||
"""
|
|
||||||
Returns a Unix time stamp (seconds passed since January 1 1970) for NOW as
|
|
||||||
an integer.
|
|
||||||
|
|
||||||
Optionally, pass seconds_into_the_future: positive int's will result in a
|
|
||||||
future timestamp, negative the past
|
|
||||||
"""
|
|
||||||
if seconds_into_the_future:
|
|
||||||
future = datetime.utcnow() + timedelta(seconds=seconds_into_the_future)
|
|
||||||
else:
|
|
||||||
future = datetime.utcnow()
|
|
||||||
return int((future - EPOCH).total_seconds())
|
|
||||||
|
|
||||||
|
|
||||||
def kodi_sql(media_type=None):
|
def kodi_sql(media_type=None):
|
||||||
"""
|
"""
|
||||||
Open a connection to the Kodi database.
|
Open a connection to the Kodi database.
|
||||||
|
@ -1135,95 +1049,3 @@ def log_time(func):
|
||||||
elapsedtotal, func.__name__)
|
elapsedtotal, func.__name__)
|
||||||
return result
|
return result
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def thread_methods(cls=None, add_stops=None, add_suspends=None):
|
|
||||||
"""
|
|
||||||
Decorator to add the following methods to a threading class:
|
|
||||||
|
|
||||||
suspend(): pauses the thread
|
|
||||||
resume(): resumes the thread
|
|
||||||
stop(): stopps/kills the thread
|
|
||||||
|
|
||||||
suspended(): returns True if thread is suspended
|
|
||||||
stopped(): returns True if thread is stopped (or should stop ;-))
|
|
||||||
ALSO returns True if PKC should exit
|
|
||||||
|
|
||||||
Also adds the following class attributes:
|
|
||||||
thread_stopped
|
|
||||||
thread_suspended
|
|
||||||
stops
|
|
||||||
suspends
|
|
||||||
|
|
||||||
invoke with either
|
|
||||||
@thread_methods
|
|
||||||
class MyClass():
|
|
||||||
or
|
|
||||||
@thread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'],
|
|
||||||
add_suspends=['DB_SCAN', 'WHATEVER'])
|
|
||||||
class MyClass():
|
|
||||||
"""
|
|
||||||
# So we don't need to invoke with ()
|
|
||||||
if cls is None:
|
|
||||||
return partial(thread_methods,
|
|
||||||
add_stops=add_stops,
|
|
||||||
add_suspends=add_suspends)
|
|
||||||
# Because we need a reference, not a copy of the immutable objects in
|
|
||||||
# state, we need to look up state every time explicitly
|
|
||||||
cls.stops = ['STOP_PKC']
|
|
||||||
if add_stops is not None:
|
|
||||||
cls.stops.extend(add_stops)
|
|
||||||
cls.suspends = add_suspends or []
|
|
||||||
|
|
||||||
# Attach new attributes to class
|
|
||||||
cls.thread_stopped = False
|
|
||||||
cls.thread_suspended = False
|
|
||||||
|
|
||||||
# Define new class methods and attach them to class
|
|
||||||
def stop(self):
|
|
||||||
"""
|
|
||||||
Call to stop this thread
|
|
||||||
"""
|
|
||||||
self.thread_stopped = True
|
|
||||||
cls.stop = stop
|
|
||||||
|
|
||||||
def suspend(self):
|
|
||||||
"""
|
|
||||||
Call to suspend this thread
|
|
||||||
"""
|
|
||||||
self.thread_suspended = True
|
|
||||||
cls.suspend = suspend
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
"""
|
|
||||||
Call to revive a suspended thread back to life
|
|
||||||
"""
|
|
||||||
self.thread_suspended = False
|
|
||||||
cls.resume = resume
|
|
||||||
|
|
||||||
def suspended(self):
|
|
||||||
"""
|
|
||||||
Returns True if the thread is suspended
|
|
||||||
"""
|
|
||||||
if self.thread_suspended is True:
|
|
||||||
return True
|
|
||||||
for suspend in self.suspends:
|
|
||||||
if getattr(state, suspend):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
cls.suspended = suspended
|
|
||||||
|
|
||||||
def stopped(self):
|
|
||||||
"""
|
|
||||||
Returns True if the thread is stopped
|
|
||||||
"""
|
|
||||||
if self.thread_stopped is True:
|
|
||||||
return True
|
|
||||||
for stop in self.stops:
|
|
||||||
if getattr(state, stop):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
cls.stopped = stopped
|
|
||||||
|
|
||||||
# Return class to render this a decorator
|
|
||||||
return cls
|
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from json import loads
|
from json import loads
|
||||||
from threading import Thread
|
|
||||||
from ssl import CERT_NONE
|
from ssl import CERT_NONE
|
||||||
from xbmc import sleep
|
from xbmc import sleep
|
||||||
|
|
||||||
from . import websocket, utils, companion, state, variables as v
|
from . import backgroundthread, websocket, utils, companion, app, variables as v
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
@ -15,7 +14,7 @@ LOG = getLogger('PLEX.websocket_client')
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
|
||||||
class WebSocket(Thread):
|
class WebSocket(backgroundthread.KillableThread):
|
||||||
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
opcode_data = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -48,18 +47,15 @@ class WebSocket(Thread):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
|
LOG.info("----===## Starting %s ##===----", self.__class__.__name__)
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
stopped = self.stopped
|
while not self.isCanceled():
|
||||||
suspended = self.suspended
|
|
||||||
while not stopped():
|
|
||||||
# In the event the server goes offline
|
# In the event the server goes offline
|
||||||
while suspended():
|
while self.isSuspended():
|
||||||
# Set in service.py
|
# Set in service.py
|
||||||
if self.ws is not None:
|
if self.ws is not None:
|
||||||
self.ws.close()
|
self.ws.close()
|
||||||
self.ws = None
|
self.ws = None
|
||||||
if stopped():
|
if self.isCanceled():
|
||||||
# Abort was requested while waiting. We should exit
|
# Abort was requested while waiting. We should exit
|
||||||
LOG.info("##===---- %s Stopped ----===##",
|
LOG.info("##===---- %s Stopped ----===##",
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
|
@ -133,14 +129,20 @@ class WebSocket(Thread):
|
||||||
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
|
LOG.info("##===---- %s Stopped ----===##", self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
@utils.thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
|
|
||||||
'BACKGROUND_SYNC_DISABLED'])
|
|
||||||
class PMS_Websocket(WebSocket):
|
class PMS_Websocket(WebSocket):
|
||||||
"""
|
"""
|
||||||
Websocket connection with the PMS for Plex Companion
|
Websocket connection with the PMS for Plex Companion
|
||||||
"""
|
"""
|
||||||
|
def isSuspended(self):
|
||||||
|
"""
|
||||||
|
Returns True if the thread is suspended
|
||||||
|
"""
|
||||||
|
return (self._suspended or
|
||||||
|
app.SYNC.suspend_library_thread or
|
||||||
|
app.SYNC.background_sync_disabled)
|
||||||
|
|
||||||
def getUri(self):
|
def getUri(self):
|
||||||
server = utils.window('pms_server')
|
server = app.CONN.server
|
||||||
# Get the appropriate prefix for the websocket
|
# Get the appropriate prefix for the websocket
|
||||||
if server.startswith('https'):
|
if server.startswith('https'):
|
||||||
server = "wss%s" % server[5:]
|
server = "wss%s" % server[5:]
|
||||||
|
@ -148,8 +150,8 @@ class PMS_Websocket(WebSocket):
|
||||||
server = "ws%s" % server[4:]
|
server = "ws%s" % server[4:]
|
||||||
uri = "%s/:/websockets/notifications" % server
|
uri = "%s/:/websockets/notifications" % server
|
||||||
# Need to use plex.tv token, if any. NOT user token
|
# Need to use plex.tv token, if any. NOT user token
|
||||||
if state.PLEX_TOKEN:
|
if app.ACCOUNT.plex_token:
|
||||||
uri += '?X-Plex-Token=%s' % state.PLEX_TOKEN
|
uri += '?X-Plex-Token=%s' % app.ACCOUNT.plex_token
|
||||||
sslopt = {}
|
sslopt = {}
|
||||||
if utils.settings('sslverify') == "false":
|
if utils.settings('sslverify') == "false":
|
||||||
sslopt["cert_reqs"] = CERT_NONE
|
sslopt["cert_reqs"] = CERT_NONE
|
||||||
|
@ -185,30 +187,33 @@ class PMS_Websocket(WebSocket):
|
||||||
# Drop everything we're not interested in
|
# Drop everything we're not interested in
|
||||||
if typus not in ('playing', 'timeline', 'activity'):
|
if typus not in ('playing', 'timeline', 'activity'):
|
||||||
return
|
return
|
||||||
elif typus == 'activity' and state.DB_SCAN is True:
|
elif typus == 'activity' and app.SYNC.db_scan is True:
|
||||||
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
# Only add to processing if PKC is NOT doing a lib scan (and thus
|
||||||
# possibly causing these reprocessing messages en mass)
|
# possibly causing these reprocessing messages en mass)
|
||||||
LOG.debug('%s: Dropping message as PKC is currently synching',
|
LOG.debug('%s: Dropping message as PKC is currently synching',
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
else:
|
else:
|
||||||
# Put PMS message on queue and let libsync take care of it
|
# Put PMS message on queue and let libsync take care of it
|
||||||
state.WEBSOCKET_QUEUE.put(message)
|
app.APP.websocket_queue.put(message)
|
||||||
|
|
||||||
|
|
||||||
class Alexa_Websocket(WebSocket):
|
class Alexa_Websocket(WebSocket):
|
||||||
"""
|
"""
|
||||||
Websocket connection to talk to Amazon Alexa.
|
Websocket connection to talk to Amazon Alexa.
|
||||||
|
|
||||||
Can't use utils.thread_methods!
|
|
||||||
"""
|
"""
|
||||||
thread_stopped = False
|
def isSuspended(self):
|
||||||
thread_suspended = False
|
"""
|
||||||
|
Overwrite method since we need to check for plex token
|
||||||
|
"""
|
||||||
|
return (self._suspended or
|
||||||
|
not app.ACCOUNT.plex_token or
|
||||||
|
app.ACCOUNT.restricted_user)
|
||||||
|
|
||||||
def getUri(self):
|
def getUri(self):
|
||||||
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
uri = ('wss://pubsub.plex.tv/sub/websockets/%s/%s?X-Plex-Token=%s'
|
||||||
% (state.PLEX_USER_ID,
|
% (app.ACCOUNT.plex_user_id,
|
||||||
v.PKC_MACHINE_IDENTIFIER,
|
v.PKC_MACHINE_IDENTIFIER,
|
||||||
state.PLEX_TOKEN))
|
app.ACCOUNT.plex_token))
|
||||||
sslopt = {}
|
sslopt = {}
|
||||||
LOG.debug("%s: Uri: %s, sslopt: %s",
|
LOG.debug("%s: Uri: %s, sslopt: %s",
|
||||||
self.__class__.__name__, uri, sslopt)
|
self.__class__.__name__, uri, sslopt)
|
||||||
|
@ -238,33 +243,3 @@ class Alexa_Websocket(WebSocket):
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
return
|
return
|
||||||
companion.process_command(message.attrib['path'][1:], message.attrib)
|
companion.process_command(message.attrib['path'][1:], message.attrib)
|
||||||
|
|
||||||
# Path in utils.thread_methods
|
|
||||||
def stop(self):
|
|
||||||
self.thread_stopped = True
|
|
||||||
|
|
||||||
def suspend(self):
|
|
||||||
self.thread_suspended = True
|
|
||||||
|
|
||||||
def resume(self):
|
|
||||||
self.thread_suspended = False
|
|
||||||
|
|
||||||
def stopped(self):
|
|
||||||
if self.thread_stopped is True:
|
|
||||||
return True
|
|
||||||
if state.STOP_PKC:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# The culprit
|
|
||||||
def suspended(self):
|
|
||||||
"""
|
|
||||||
Overwrite method since we need to check for plex token
|
|
||||||
"""
|
|
||||||
if self.thread_suspended is True:
|
|
||||||
return True
|
|
||||||
if not state.PLEX_TOKEN:
|
|
||||||
return True
|
|
||||||
if state.RESTRICTED_USER:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import absolute_import, division, unicode_literals
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
|
|
||||||
from . import kodigui
|
from . import kodigui
|
||||||
from .. import utils, variables as v
|
from .. import variables as v
|
||||||
|
|
||||||
|
|
||||||
class Background(kodigui.BaseWindow):
|
class Background(kodigui.BaseWindow):
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
<setting id="enforceUserLogin" label="30536" type="bool" default="false" />
|
<setting id="enforceUserLogin" label="30536" type="bool" default="false" />
|
||||||
<setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=passwords)" option="close" /><!-- Network credentials -->
|
<setting label="30517" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=passwords)" option="close" /><!-- Network credentials -->
|
||||||
<setting label="30505" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect?mode=resetauth)" option="close" /><!-- reset connection attempts -->
|
|
||||||
<setting id="accessToken" type="text" visible="false" default="" />
|
<setting id="accessToken" type="text" visible="false" default="" />
|
||||||
</category>
|
</category>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue