Merge branch 'hotfixes' into translations

This commit is contained in:
Croneter 2018-04-29 14:40:04 +02:00
commit fae3647407
17 changed files with 308 additions and 195 deletions

View file

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

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.0.19" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.0.20" provider-name="croneter">
<requires> <requires>
<import addon="xbmc.python" version="2.1.0"/> <import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.requests" version="2.9.1" /> <import addon="script.module.requests" version="2.9.1" />
<import addon="plugin.video.plexkodiconnect.movies" version="2.0.2" /> <import addon="plugin.video.plexkodiconnect.movies" version="2.0.4" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.3" /> <import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.4" />
</requires> </requires>
<extension point="xbmc.python.pluginsource" library="default.py"> <extension point="xbmc.python.pluginsource" library="default.py">
<provides>video audio image</provides> <provides>video audio image</provides>
@ -67,7 +67,14 @@
<summary lang="ru_RU">Нативная интеграция сервера Plex в Kodi</summary> <summary lang="ru_RU">Нативная интеграция сервера Plex в Kodi</summary>
<description lang="ru_RU">Подключите Kodi к своему серверу Plex. Плагин предполагает что вы управляете своими видео с помощью Plex (а не в Kodi). Вы можете потерять текущие базы данных музыки и видео в Kodi (так как плагин напрямую их изменяет). Используйте на свой страх и риск</description> <description lang="ru_RU">Подключите Kodi к своему серверу Plex. Плагин предполагает что вы управляете своими видео с помощью Plex (а не в Kodi). Вы можете потерять текущие базы данных музыки и видео в Kodi (так как плагин напрямую их изменяет). Используйте на свой страх и риск</description>
<disclaimer lang="ru_RU">Используйте на свой страх и риск</disclaimer> <disclaimer lang="ru_RU">Используйте на свой страх и риск</disclaimer>
<news>version 2.0.19 (beta only): <news>version 2.0.20 (beta only):
- Fix missing episode poster in certain views. You will have to manually reset your Kodi database to benefit
- Fix episode artwork sometimes not being complete
- Fix IndexError for certain Plex channels
- Kodi Leia: Fix playback failing
- Fix TV On Deck direct paths asking to choose between different media
version 2.0.19 (beta only):
- Fix PKC playback startup getting caught in infinity loop - Fix PKC playback startup getting caught in infinity loop
- Rewire library sync, suspend sync during playback - Rewire library sync, suspend sync during playback
- Fix playback failing in certain cases - Fix playback failing in certain cases

View file

@ -1,3 +1,10 @@
version 2.0.20 (beta only):
- Fix missing episode poster in certain views. You will have to manually reset your Kodi database to benefit
- Fix episode artwork sometimes not being complete
- Fix IndexError for certain Plex channels
- Kodi Leia: Fix playback failing
- Fix TV On Deck direct paths asking to choose between different media
version 2.0.19 (beta only): version 2.0.19 (beta only):
- Fix PKC playback startup getting caught in infinity loop - Fix PKC playback startup getting caught in infinity loop
- Rewire library sync, suspend sync during playback - Rewire library sync, suspend sync during playback

View file

@ -41,6 +41,26 @@ msgctxt "#30005"
msgid "Username: " msgid "Username: "
msgstr "" msgstr ""
# Sync notification displayed if there is still artwork to be cached to Kodi
msgctxt "#30006"
msgid "Caching %s images"
msgstr ""
# Sync notification displayed if syncing of major artwork is done
msgctxt "#30007"
msgid "Major image caching done"
msgstr ""
# PKC settings artwork: Enable notifications for artwork image sync
msgctxt "#30008"
msgid "Enable notifications for image caching"
msgstr ""
# PKC settings artwork: Enable image caching during Kodi playback
msgctxt "#30009"
msgid "Enable image caching during Kodi playback (restart Kodi!)"
msgstr ""
# Button text # Button text
msgctxt "#30012" msgctxt "#30012"
msgid "OK" msgid "OK"

View file

@ -124,8 +124,12 @@ class API(object):
# Local path # Local path
filename = ans.rsplit("\\", 1)[1] filename = ans.rsplit("\\", 1)[1]
else: else:
try:
# Network share # Network share
filename = ans.rsplit("/", 1)[1] filename = ans.rsplit("/", 1)[1]
except IndexError:
# E.g. certain Plex channels
filename = None
return filename return filename
def file_path(self, force_first_media=False): def file_path(self, force_first_media=False):
@ -772,14 +776,22 @@ class API(object):
# Artwork lookup for episodes is broken for addon paths # Artwork lookup for episodes is broken for addon paths
# Episodes is a bit special, only get the thumb, because all # Episodes is a bit special, only get the thumb, because all
# the other artwork will be saved under season and show # the other artwork will be saved under season and show
# EXCEPT if you're constructing a listitem
if not full_artwork:
art = self._one_artwork('thumb') art = self._one_artwork('thumb')
if art: if art:
artworks['thumb'] = art artworks['thumb'] = art
if full_artwork: return artworks
for kodi_artwork, plex_artwork in \
v.KODI_TO_PLEX_ARTWORK_EPISODE.iteritems():
art = self._one_artwork(plex_artwork)
if art:
artworks[kodi_artwork] = art
if not full_artwork:
return artworks
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
db_item = plex_db.getItem_byId(self.plex_id())
try: try:
season_id = db_item[3] season_id = plex_db.getItem_byId(self.plex_id())[3]
except TypeError: except TypeError:
return artworks return artworks
# Grab artwork from the season # Grab artwork from the season
@ -789,9 +801,9 @@ class API(object):
artworks['season.%s' % kodi_art] = season_art[kodi_art] artworks['season.%s' % kodi_art] = season_art[kodi_art]
# Get the show id # Get the show id
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
db_item = plex_db.getItem_byId(self.grandparent_id())
try: try:
show_id = db_item[0] show_id = plex_db.getItem_byKodiId(season_id,
v.KODI_TYPE_SEASON)[1]
except TypeError: except TypeError:
return artworks return artworks
# Grab more artwork from the show # Grab more artwork from the show
@ -844,7 +856,6 @@ class API(object):
external_id = self.retrieve_external_item_id() external_id = self.retrieve_external_item_id()
if external_id is not None: if external_id is not None:
artworks = self.lookup_fanart_tv(external_id[0], artworks) artworks = self.lookup_fanart_tv(external_id[0], artworks)
LOG.debug('fanart artworks: %s', artworks)
return artworks return artworks
def retrieve_external_item_id(self, collection=False): def retrieve_external_item_id(self, collection=False):

View file

@ -22,6 +22,10 @@ LOG = getLogger("PLEX." + __name__)
# Disable annoying requests warnings # Disable annoying requests warnings
requests.packages.urllib3.disable_warnings() requests.packages.urllib3.disable_warnings()
ARTWORK_QUEUE = Queue() ARTWORK_QUEUE = Queue()
IMAGE_CACHING_SUSPENDS = ['SUSPEND_LIBRARY_THREAD', 'DB_SCAN', 'STOP_SYNC']
if not settings('imageSyncDuringPlayback') == 'true':
IMAGE_CACHING_SUSPENDS.append('SUSPEND_SYNC')
############################################################################### ###############################################################################
@ -33,11 +37,9 @@ def double_urldecode(text):
return unquote(unquote(text)) return unquote(unquote(text))
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', @thread_methods(add_suspends=IMAGE_CACHING_SUSPENDS)
'DB_SCAN',
'STOP_SYNC'])
class Image_Cache_Thread(Thread): class Image_Cache_Thread(Thread):
sleep_between = 50 sleep_between = 200
# Potentially issues with limited number of threads # Potentially issues with limited number of threads
# Hence let Kodi wait till download is successful # Hence let Kodi wait till download is successful
timeout = (35.1, 35.1) timeout = (35.1, 35.1)
@ -47,6 +49,7 @@ class Image_Cache_Thread(Thread):
Thread.__init__(self) Thread.__init__(self)
def run(self): def run(self):
LOG.info("---===### Starting Image_Cache_Thread ###===---")
stopped = self.stopped stopped = self.stopped
suspended = self.suspended suspended = self.suspended
queue = self.queue queue = self.queue
@ -65,6 +68,16 @@ class Image_Cache_Thread(Thread):
except Empty: except Empty:
sleep(1000) sleep(1000)
continue continue
if isinstance(url, ArtworkSyncMessage):
if state.IMAGE_SYNC_NOTIFICATIONS:
dialog('notification',
heading=lang(29999),
message=url.message,
icon='{plex}',
sound=False)
queue.task_done()
continue
url = double_urlencode(try_encode(url))
sleeptime = 0 sleeptime = 0
while True: while True:
try: try:
@ -116,6 +129,44 @@ class Artwork():
if enableTextureCache: if enableTextureCache:
queue = ARTWORK_QUEUE queue = ARTWORK_QUEUE
def cache_major_artwork(self):
"""
Takes the existing Kodi library and caches posters and fanart.
Necessary because otherwise PKC caches artwork e.g. from fanart.tv
which basically blocks Kodi from getting needed artwork fast (e.g.
while browsing the library)
"""
if not self.enableTextureCache:
return
artworks = list()
# Get all posters and fanart/background for video and music
for kind in ('video', 'music'):
connection = kodi_sql(kind)
cursor = connection.cursor()
for typus in ('poster', 'fanart'):
cursor.execute('SELECT url FROM art WHERE type == ?',
(typus, ))
artworks.extend(cursor.fetchall())
connection.close()
artworks_to_cache = list()
connection = kodi_sql('texture')
cursor = connection.cursor()
for url in artworks:
query = 'SELECT url FROM texture WHERE url == ? LIMIT 1'
cursor.execute(query, (url[0], ))
if not cursor.fetchone():
artworks_to_cache.append(url)
connection.close()
if not artworks_to_cache:
LOG.info('Caching of major images to Kodi texture cache done')
return
LOG.info('Caching has not been completed - caching %s major images',
len(artworks_to_cache))
self.queue.put(ArtworkSyncMessage(lang(30006) % len(artworks_to_cache)))
for url in artworks_to_cache:
self.queue.put(url[0])
self.queue.put(ArtworkSyncMessage(lang(30007)))
def fullTextureCacheSync(self): def fullTextureCacheSync(self):
""" """
This method will sync all Kodi artwork to textures13.db This method will sync all Kodi artwork to textures13.db
@ -174,10 +225,10 @@ class Artwork():
def cache_texture(self, url): def cache_texture(self, url):
''' '''
Cache a single image url to the texture cache Cache a single image url to the texture cache. url: unicode
''' '''
if url and self.enableTextureCache: if url and self.enableTextureCache:
self.queue.put(double_urlencode(try_encode(url))) self.queue.put(url)
def modify_artwork(self, artworks, kodi_id, kodi_type, cursor): def modify_artwork(self, artworks, kodi_id, kodi_type, cursor):
""" """
@ -266,3 +317,11 @@ class Artwork():
for path in paths: for path in paths:
makedirs(try_decode(translatePath("special://thumbnails/%s" makedirs(try_decode(translatePath("special://thumbnails/%s"
% path))) % path)))
class ArtworkSyncMessage(object):
"""
Put in artwork queue to display the message as a Kodi notification
"""
def __init__(self, message):
self.message = message

View file

@ -369,7 +369,7 @@ def getRecentEpisodes(viewid, mediatype, tagname, limit):
append_show_title = settings('RecentTvAppendShow') == 'true' append_show_title = settings('RecentTvAppendShow') == 'true'
append_sxxexx = settings('RecentTvAppendSeason') == 'true' append_sxxexx = settings('RecentTvAppendSeason') == 'true'
# First we get a list of all the TV shows - filtered by tag # First we get a list of all the TV shows - filtered by tag
allshowsIds = set() allshowsIds = list()
params = { params = {
'sort': {'order': "descending", 'method': "dateadded"}, 'sort': {'order': "descending", 'method': "dateadded"},
'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname}, 'filter': {'operator': "is", 'field': "tag", 'value': "%s" % tagname},
@ -553,7 +553,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
append_show_title=append_show_title, append_show_title=append_show_title,
append_sxxexx=append_sxxexx) append_sxxexx=append_sxxexx)
if directpaths: if directpaths:
url = api.file_path() url = api.file_path(force_first_media=True)
else: else:
url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play&filename=%s' url = ('plugin://%s.tvshows/?plex_id=%s&plex_type=%s&mode=play&filename=%s'
% (v.ADDON_ID, % (v.ADDON_ID,
@ -561,8 +561,7 @@ def getOnDeck(viewid, mediatype, tagname, limit):
api.plex_type(), api.plex_type(),
api.file_name(force_first_media=True))) api.file_name(force_first_media=True)))
if api.resume_point(): if api.resume_point():
listitem.setProperty('resumetime', listitem.setProperty('resumetime', str(api.resume_point()))
str(api.resume_point()))
xbmcplugin.addDirectoryItem( xbmcplugin.addDirectoryItem(
handle=HANDLE, handle=HANDLE,
url=url, url=url,

View file

@ -141,7 +141,7 @@ class InitialSetup(object):
""" """
def __init__(self): def __init__(self):
LOG.debug('Entering initialsetup class') LOG.debug('Entering initialsetup class')
self.server = UserClient().getServer() self.server = UserClient().get_server()
self.serverid = settings('plex_machineIdentifier') self.serverid = 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()

View file

@ -1217,6 +1217,7 @@ class Music(Items):
""" """
Adds a single music album Adds a single music album
children: list of child xml's, so in this case songs children: list of child xml's, so in this case songs
scan_children: set to False if you don't want to add children
""" """
kodicursor = self.kodicursor kodicursor = self.kodicursor
plex_db = self.plex_db plex_db = self.plex_db
@ -1224,20 +1225,18 @@ class Music(Items):
api = API(item) api = API(item)
update_item = True update_item = True
itemid = api.plex_id() plex_id = api.plex_id()
if not itemid: if not plex_id:
LOG.error('Error processing Album, skipping') LOG.error('Error processing Album, skipping')
return return
plex_dbitem = plex_db.getItem_byId(itemid) plex_dbitem = plex_db.getItem_byId(plex_id)
try: try:
albumid = plex_dbitem[0] album_id = plex_dbitem[0]
except TypeError: except TypeError:
# Albumid not found
update_item = False update_item = False
# The album details ##### # The album details #####
lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S') lastScraped = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
dateadded = api.date_created()
userdata = api.userdata() userdata = api.userdata()
checksum = api.checksum() checksum = api.checksum()
@ -1269,22 +1268,22 @@ class Music(Items):
# UPDATE THE ALBUM ##### # UPDATE THE ALBUM #####
if update_item: if update_item:
LOG.info("UPDATE album itemid: %s - Name: %s", itemid, name) LOG.info("UPDATE album plex_id: %s - Name: %s", plex_id, name)
# Update the checksum in plex table # Update the checksum in plex table
plex_db.updateReference(itemid, checksum) plex_db.updateReference(plex_id, checksum)
# OR ADD THE ALBUM ##### # OR ADD THE ALBUM #####
else: else:
LOG.info("ADD album itemid: %s - Name: %s", itemid, name) LOG.info("ADD album plex_id: %s - Name: %s", plex_id, name)
# safety checks: It looks like plex supports the same artist # safety checks: It looks like plex supports the same artist
# multiple times. # multiple times.
# 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.
albumid = self.kodi_db.addAlbum(name, musicBrainzId) album_id = self.kodi_db.addAlbum(name, musicBrainzId)
# Create the reference in plex table # Create the reference in plex table
plex_db.addReference(itemid, plex_db.addReference(plex_id,
v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_ALBUM,
albumid, album_id,
v.KODI_TYPE_ALBUM, v.KODI_TYPE_ALBUM,
view_id=viewid, view_id=viewid,
checksum=checksum) checksum=checksum)
@ -1302,7 +1301,7 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, self.genre, bio, kodicursor.execute(query, (artistname, year, self.genre, bio,
thumb, rating, lastScraped, thumb, rating, lastScraped,
v.KODI_TYPE_ALBUM, studio, v.KODI_TYPE_ALBUM, studio,
self.compilation, albumid)) self.compilation, album_id))
elif v.KODIVERSION == 17: elif v.KODIVERSION == 17:
# Kodi Krypton # Kodi Krypton
query = ''' query = '''
@ -1315,7 +1314,7 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, self.genre, bio, kodicursor.execute(query, (artistname, year, self.genre, bio,
thumb, rating, lastScraped, thumb, rating, lastScraped,
v.KODI_TYPE_ALBUM, studio, v.KODI_TYPE_ALBUM, studio,
self.compilation, albumid)) self.compilation, album_id))
elif v.KODIVERSION == 16: elif v.KODIVERSION == 16:
# Kodi Jarvis # Kodi Jarvis
query = ''' query = '''
@ -1328,71 +1327,46 @@ class Music(Items):
kodicursor.execute(query, (artistname, year, self.genre, bio, kodicursor.execute(query, (artistname, year, self.genre, bio,
thumb, rating, lastScraped, thumb, rating, lastScraped,
v.KODI_TYPE_ALBUM, studio, v.KODI_TYPE_ALBUM, studio,
self.compilation, albumid)) self.compilation, album_id))
# Associate the parentid for plex reference # Associate the parentid for plex reference
parent_id = api.parent_plex_id() parent_id = api.parent_plex_id()
artist_id = None
if parent_id is not None: if parent_id is not None:
try:
artist_id = plex_db.getItem_byId(parent_id)[0]
except TypeError:
LOG.info('Artist %s does not yet exist in Plex DB', parent_id)
artist = GetPlexMetadata(parent_id)
try:
artist[0].attrib
except (TypeError, IndexError, AttributeError):
LOG.error('Could not get artist xml for %s', parent_id)
else:
self.add_updateArtist(artist[0])
plex_dbartist = plex_db.getItem_byId(parent_id) plex_dbartist = plex_db.getItem_byId(parent_id)
try: try:
artistid = plex_dbartist[0] artist_id = plex_dbartist[0]
except TypeError: except TypeError:
LOG.info('Artist %s does not exist in plex database', LOG.error('Adding artist failed for %s', parent_id)
parent_id) # Update plex reference with the artist_id
artist = GetPlexMetadata(parent_id) plex_db.updateParentId(plex_id, artist_id)
# Item may not be an artist, verification necessary.
if artist is not None and artist != 401:
if artist[0].attrib.get('type') == v.PLEX_TYPE_ARTIST:
# Update with the parent_id, for remove reference
plex_db.addReference(parent_id,
v.PLEX_TYPE_ARTIST,
parent_id,
v.KODI_TYPE_ARTIST,
view_id=viewid)
plex_db.updateParentId(itemid, parent_id)
else:
# Update plex reference with the artistid
plex_db.updateParentId(itemid, artistid)
# Assign main artists to album
# Plex unfortunately only supports 1 artist :-(
artist_id = parent_id
plex_dbartist = plex_db.getItem_byId(artist_id)
try:
artistid = plex_dbartist[0]
except TypeError:
# Artist does not exist in plex database, create the reference
LOG.info('Artist %s does not exist in Plex database', artist_id)
artist = GetPlexMetadata(artist_id)
if artist is not None and artist != 401:
self.add_updateArtist(artist[0])
plex_dbartist = plex_db.getItem_byId(artist_id)
artistid = plex_dbartist[0]
else:
# Best take this name over anything else.
query = "UPDATE artist SET strArtist = ? WHERE idArtist = ?"
kodicursor.execute(query, (artistname, artistid,))
LOG.info("UPDATE artist: strArtist: %s, idArtist: %s",
artistname, artistid)
# Add artist to album # Add artist to album
query = ''' query = '''
INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist)
VALUES (?, ?, ?) VALUES (?, ?, ?)
''' '''
kodicursor.execute(query, (artistid, albumid, artistname)) kodicursor.execute(query, (artist_id, album_id, artistname))
# Update discography # Update discography
query = ''' query = '''
INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear)
VALUES (?, ?, ?) VALUES (?, ?, ?)
''' '''
kodicursor.execute(query, (artistid, name, year)) kodicursor.execute(query, (artist_id, name, year))
# Update plex reference with parentid
plex_db.updateParentId(artist_id, albumid)
if v.KODIVERSION < 18: if v.KODIVERSION < 18:
self.kodi_db.addMusicGenres(albumid, self.genres, v.KODI_TYPE_ALBUM) self.kodi_db.addMusicGenres(album_id, self.genres, v.KODI_TYPE_ALBUM)
# Update artwork # Update artwork
artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) artwork.modify_artwork(artworks, album_id, v.KODI_TYPE_ALBUM, kodicursor)
# Add all children - all tracks # Add all children - all tracks
if scan_children: if scan_children:
for child in children: for child in children:

View file

@ -50,7 +50,8 @@ STATE_SETTINGS = {
'remapSMBphotoNew': 'remapSMBphotoNew', 'remapSMBphotoNew': 'remapSMBphotoNew',
'enableMusic': 'ENABLE_MUSIC', 'enableMusic': 'ENABLE_MUSIC',
'forceReloadSkinOnPlaybackStop': 'FORCE_RELOAD_SKIN', 'forceReloadSkinOnPlaybackStop': 'FORCE_RELOAD_SKIN',
'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER' 'fetch_pms_item_number': 'FETCH_PMS_ITEM_NUMBER',
'imageSyncNotifications': 'IMAGE_SYNC_NOTIFICATIONS'
} }
############################################################################### ###############################################################################

View file

@ -16,6 +16,7 @@ from downloadutils import DownloadUtils as DU
import itemtypes import itemtypes
import plexdb_functions as plexdb import plexdb_functions as plexdb
import kodidb_functions as kodidb import kodidb_functions as kodidb
import artwork
import videonodes import videonodes
import variables as v import variables as v
@ -1453,7 +1454,6 @@ class LibrarySync(Thread):
elif state.RUN_LIB_SCAN == 'textures': elif state.RUN_LIB_SCAN == 'textures':
state.DB_SCAN = True state.DB_SCAN = True
window('plex_dbScan', value="true") window('plex_dbScan', value="true")
import artwork
artwork.Artwork().fullTextureCacheSync() artwork.Artwork().fullTextureCacheSync()
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False state.DB_SCAN = False
@ -1517,8 +1517,6 @@ class LibrarySync(Thread):
last_time_sync = utils.unix_timestamp() last_time_sync = utils.unix_timestamp()
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)
state.DB_SCAN = False state.DB_SCAN = False
# Start the fanart download thread
self.fanartthread.start()
while not self.stopped(): while not self.stopped():
# In the event the server goes offline # In the event the server goes offline
@ -1544,6 +1542,7 @@ class LibrarySync(Thread):
initial_sync_done = True initial_sync_done = True
kodi_db_version_checked = True kodi_db_version_checked = True
last_sync = utils.unix_timestamp() last_sync = utils.unix_timestamp()
self.fanartthread.start()
else: else:
LOG.error('Initial start-up full sync unsuccessful') LOG.error('Initial start-up full sync unsuccessful')
xbmc.executebuiltin('InhibitIdleShutdown(false)') xbmc.executebuiltin('InhibitIdleShutdown(false)')
@ -1587,6 +1586,8 @@ class LibrarySync(Thread):
if settings('FanartTV') == 'true': if settings('FanartTV') == 'true':
self.sync_fanart() self.sync_fanart()
LOG.info('Done initial sync on Kodi startup') LOG.info('Done initial sync on Kodi startup')
artwork.Artwork().cache_major_artwork()
self.fanartthread.start()
else: else:
LOG.info('Startup sync has not yet been successful') LOG.info('Startup sync has not yet been successful')
window('plex_dbScan', clear=True) window('plex_dbScan', clear=True)

View file

@ -4,12 +4,31 @@ from cPickle import dumps, loads
from xbmcgui import Window from xbmcgui import Window
from xbmc import log, LOGDEBUG from xbmc import log, LOGDEBUG
############################################################################### ###############################################################################
WINDOW = Window(10000) WINDOW = Window(10000)
PREFIX = 'PLEX.%s: ' % __name__ PREFIX = 'PLEX.%s: ' % __name__
############################################################################### ###############################################################################
def try_encode(input_str, encoding='utf-8'):
"""
Will try to encode input_str (in unicode) to encoding. This possibly
fails with e.g. Android TV's Python, which does not accept arguments for
string.encode()
COPY to avoid importing utils on calling default.py
"""
if isinstance(input_str, str):
# already encoded
return input_str
try:
input_str = input_str.encode(encoding, "ignore")
except TypeError:
input_str = input_str.encode()
return input_str
def pickl_window(property, value=None, clear=False): def pickl_window(property, value=None, clear=False):
""" """
Get or set window property - thread safe! For use with Pickle Get or set window property - thread safe! For use with Pickle
@ -20,7 +39,7 @@ def pickl_window(property, value=None, clear=False):
elif value is not None: elif value is not None:
WINDOW.setProperty(property, value) WINDOW.setProperty(property, value)
else: else:
return WINDOW.getProperty(property) return try_encode(WINDOW.getProperty(property))
def pickle_me(obj, window_var='plex_result'): def pickle_me(obj, window_var='plex_result'):

View file

@ -39,6 +39,8 @@ FORCE_RELOAD_SKIN = True
# Stemming from the PKC settings.xml # Stemming from the PKC settings.xml
# Shall we show Kodi dialogs when synching? # Shall we show Kodi dialogs when synching?
SYNC_DIALOG = True SYNC_DIALOG = True
# Shall Kodi show dialogs for syncing/caching images? (e.g. images left to sync)
IMAGE_SYNC_NOTIFICATIONS = True
# Is synching of Plex music enabled? # Is synching of Plex music enabled?
ENABLE_MUSIC = True ENABLE_MUSIC = True
# How often shall we sync? # How often shall we sync?

View file

@ -22,7 +22,9 @@ LOG = getLogger("PLEX." + __name__)
@thread_methods(add_suspends=['SUSPEND_USER_CLIENT']) @thread_methods(add_suspends=['SUSPEND_USER_CLIENT'])
class UserClient(Thread): class UserClient(Thread):
"""
Manage Plex users
"""
# Borg - multiple instances, shared state # Borg - multiple instances, shared state
__shared_state = {} __shared_state = {}
@ -32,100 +34,98 @@ class UserClient(Thread):
self.auth = True self.auth = True
self.retry = 0 self.retry = 0
self.currUser = None self.user = None
self.currServer = None self.has_access = True
self.currToken = None
self.HasAccess = True
self.AdditionalUser = []
self.userSettings = None self.server = None
self.server_name = None
self.machine_identifier = None
self.token = None
self.ssl = None
self.sslcert = None
self.addon = xbmcaddon.Addon() self.addon = xbmcaddon.Addon()
self.doUtils = DU() self.do_utils = None
Thread.__init__(self) Thread.__init__(self)
def getUsername(self): def get_server(self):
""" """
Returns username as unicode Get the current PMS' URL
""" """
username = settings('username')
if not username:
LOG.debug("No username saved, trying to get Plex username")
username = settings('plexLogin')
if not username:
LOG.debug("Also no Plex username found")
return ""
return username
def getServer(self, prefix=True):
# Original host # Original host
self.servername = settings('plex_servername') self.server_name = settings('plex_servername')
HTTPS = settings('https') == "true" https = settings('https') == "true"
host = settings('ipaddress') host = settings('ipaddress')
port = settings('port') port = settings('port')
self.machineIdentifier = settings('plex_machineIdentifier') self.machine_identifier = settings('plex_machineIdentifier')
server = host + ":" + port
if not host: if not host:
LOG.debug("No server information saved.") LOG.debug("No server information saved.")
return False return False
server = host + ":" + port
# If https is true # If https is true
if prefix and HTTPS: if https:
server = "https://%s" % server server = "https://%s" % server
# If https is false # If https is false
elif prefix and not HTTPS: else:
server = "http://%s" % server server = "http://%s" % server
# User entered IP; we need to get the machineIdentifier # User entered IP; we need to get the machineIdentifier
if self.machineIdentifier == '' and prefix is True: if not self.machine_identifier:
self.machineIdentifier = PF.GetMachineIdentifier(server) self.machine_identifier = PF.GetMachineIdentifier(server)
if self.machineIdentifier is None: if not self.machine_identifier:
self.machineIdentifier = '' self.machine_identifier = ''
settings('plex_machineIdentifier', value=self.machineIdentifier) settings('plex_machineIdentifier', value=self.machine_identifier)
LOG.debug('Returning active server: %s', server) LOG.debug('Returning active server: %s', server)
return server return server
def getSSLverify(self): @staticmethod
# Verify host certificate def get_ssl_verify():
"""
Do we need to verify the SSL certificate? Return None if that is the
case, else False
"""
return None if settings('sslverify') == 'true' else False return None if settings('sslverify') == 'true' else False
def getSSL(self): @staticmethod
# Client side certificate def get_ssl_certificate():
"""
Client side certificate
"""
return None if settings('sslcert') == 'None' \ return None if settings('sslcert') == 'None' \
else settings('sslcert') else settings('sslcert')
def setUserPref(self): def set_user_prefs(self):
"""
Load a user's profile picture
"""
LOG.debug('Setting user preferences') LOG.debug('Setting user preferences')
# Only try to get user avatar if there is a token # Only try to get user avatar if there is a token
if self.currToken: if self.token:
url = PF.GetUserArtworkURL(self.currUser) url = PF.GetUserArtworkURL(self.user)
if url: if url:
window('PlexUserImage', value=url) window('PlexUserImage', value=url)
# Set resume point max
# url = "{server}/emby/System/Configuration?format=json"
# result = doUtils.downloadUrl(url)
def hasAccess(self): @staticmethod
def check_access():
# Plex: always return True for now # Plex: always return True for now
return True return True
def loadCurrUser(self, username, userId, usertoken, authenticated=False): def load_user(self, username, user_id, usertoken, authenticated=False):
"""
Load the current user's details for PKC
"""
LOG.debug('Loading current user') LOG.debug('Loading current user')
doUtils = self.doUtils self.token = usertoken
self.server = self.get_server()
self.currToken = usertoken self.ssl = self.get_ssl_verify()
self.currServer = self.getServer() self.sslcert = self.get_ssl_certificate()
self.ssl = self.getSSLverify()
self.sslcert = self.getSSL()
if authenticated is False: if authenticated is False:
if self.currServer is None: if self.server is None:
return False return False
LOG.debug('Testing validity of current token') LOG.debug('Testing validity of current token')
res = PF.check_connection(self.currServer, res = PF.check_connection(self.server,
token=self.currToken, token=self.token,
verifySSL=self.ssl) verifySSL=self.ssl)
if res is False: if res is False:
# PMS probably offline # PMS probably offline
@ -138,7 +138,7 @@ class UserClient(Thread):
return False return False
# Set to windows property # Set to windows property
state.PLEX_USER_ID = userId or None state.PLEX_USER_ID = user_id or None
state.PLEX_USERNAME = username state.PLEX_USERNAME = username
# This is the token for the current PMS (might also be '') # This is the token for the current PMS (might also be '')
window('pms_token', value=usertoken) window('pms_token', value=usertoken)
@ -150,9 +150,9 @@ class UserClient(Thread):
window('plex_restricteduser', value=settings('plex_restricteduser')) window('plex_restricteduser', value=settings('plex_restricteduser'))
state.RESTRICTED_USER = True \ state.RESTRICTED_USER = True \
if settings('plex_restricteduser') == 'true' else False if settings('plex_restricteduser') == 'true' else False
window('pms_server', value=self.currServer) window('pms_server', value=self.server)
window('plex_machineIdentifier', value=self.machineIdentifier) window('plex_machineIdentifier', value=self.machine_identifier)
window('plex_servername', value=self.servername) window('plex_servername', value=self.server_name)
window('plex_authenticated', value='true') window('plex_authenticated', value='true')
state.AUTHENTICATED = True state.AUTHENTICATED = True
@ -166,19 +166,22 @@ class UserClient(Thread):
if settings('force_transcode_pix') == "1" else 'false') if settings('force_transcode_pix') == "1" else 'false')
# Start DownloadUtils session # Start DownloadUtils session
doUtils.startSession(reset=True) self.do_utils = DU()
# self.getAdditionalUsers() self.do_utils.startSession(reset=True)
# Set user preferences in settings # Set user preferences in settings
self.currUser = username self.user = username
self.setUserPref() self.set_user_prefs()
# Writing values to settings file # Writing values to settings file
settings('username', value=username) settings('username', value=username)
settings('userid', value=userId) settings('userid', value=user_id)
settings('accessToken', value=usertoken) settings('accessToken', value=usertoken)
return True return True
def authenticate(self): def authenticate(self):
"""
Authenticate the current user
"""
LOG.debug('Authenticating user') LOG.debug('Authenticating user')
# Give attempts at entering password / selecting user # Give attempts at entering password / selecting user
@ -198,7 +201,7 @@ class UserClient(Thread):
LOG.error("Error, no settings.xml found.") LOG.error("Error, no settings.xml found.")
self.auth = False self.auth = False
return False return False
server = self.getServer() server = self.get_server()
# If there is no server we can connect to # If there is no server we can connect to
if not server: if not server:
LOG.info("Missing server information.") LOG.info("Missing server information.")
@ -213,7 +216,7 @@ class UserClient(Thread):
# Found a user in the settings, try to authenticate # Found a user in the settings, try to authenticate
if username and enforceLogin == 'false': if username and enforceLogin == 'false':
LOG.debug('Trying to authenticate with old settings') LOG.debug('Trying to authenticate with old settings')
answ = self.loadCurrUser(username, answ = self.load_user(username,
userId, userId,
usertoken, usertoken,
authenticated=False) authenticated=False)
@ -248,7 +251,7 @@ class UserClient(Thread):
userId = '' userId = ''
usertoken = '' usertoken = ''
if self.loadCurrUser(username, userId, usertoken, authenticated=False): if self.load_user(username, userId, usertoken, authenticated=False):
# SUCCESS: loaded a user from the settings # SUCCESS: loaded a user from the settings
return True return True
# Something went wrong, try again # Something went wrong, try again
@ -256,9 +259,12 @@ class UserClient(Thread):
self.retry += 1 self.retry += 1
return False return False
def resetClient(self): def reset_client(self):
"""
Reset all user settings
"""
LOG.debug("Reset UserClient authentication.") LOG.debug("Reset UserClient authentication.")
self.doUtils.stopSession() self.do_utils.stopSession()
window('plex_authenticated', clear=True) window('plex_authenticated', clear=True)
state.AUTHENTICATED = False state.AUTHENTICATED = False
@ -279,13 +285,16 @@ class UserClient(Thread):
settings('userid', value='') settings('userid', value='')
settings('accessToken', value='') settings('accessToken', value='')
self.currToken = None self.token = None
self.auth = True self.auth = True
self.currUser = None self.user = None
self.retry = 0 self.retry = 0
def run(self): def run(self):
"""
Do the work
"""
LOG.info("----===## Starting UserClient ##===----") LOG.info("----===## Starting UserClient ##===----")
stopped = self.stopped stopped = self.stopped
suspended = self.suspended suspended = self.suspended
@ -299,19 +308,14 @@ class UserClient(Thread):
sleep(500) sleep(500)
continue continue
# Verify the connection status to server
elif state.PMS_STATUS == "restricted":
# Parental control is restricting access
self.HasAccess = False
elif state.PMS_STATUS == "401": elif state.PMS_STATUS == "401":
# Unauthorized access, revoke token # Unauthorized access, revoke token
state.PMS_STATUS = 'Auth' state.PMS_STATUS = 'Auth'
window('plex_serverStatus', value='Auth') window('plex_serverStatus', value='Auth')
self.resetClient() self.reset_client()
sleep(3000) sleep(3000)
if self.auth and (self.currUser is None): if self.auth and (self.user is None):
# Try to authenticate user # Try to authenticate user
if not state.PMS_STATUS or state.PMS_STATUS == "Auth": if not state.PMS_STATUS or state.PMS_STATUS == "Auth":
# Set auth flag because we no longer need # Set auth flag because we no longer need
@ -320,16 +324,16 @@ class UserClient(Thread):
if self.authenticate(): if self.authenticate():
# Successfully authenticated and loaded a user # Successfully authenticated and loaded a user
LOG.info("Successfully authenticated!") LOG.info("Successfully authenticated!")
LOG.info("Current user: %s", self.currUser) LOG.info("Current user: %s", self.user)
LOG.info("Current userId: %s", state.PLEX_USER_ID) LOG.info("Current userId: %s", state.PLEX_USER_ID)
self.retry = 0 self.retry = 0
state.SUSPEND_LIBRARY_THREAD = False state.SUSPEND_LIBRARY_THREAD = False
window('plex_serverStatus', clear=True) window('plex_serverStatus', clear=True)
state.PMS_STATUS = False state.PMS_STATUS = False
if not self.auth and (self.currUser is None): if not self.auth and (self.user is None):
# Loop if no server found # Loop if no server found
server = self.getServer() server = self.get_server()
# The status Stop is for when user cancelled password dialog. # The status Stop is for when user cancelled password dialog.
# Or retried too many times # Or retried too many times

View file

@ -325,6 +325,13 @@ KODI_TO_PLEX_ARTWORK = {
'fanart': 'art' 'fanart': 'art'
} }
KODI_TO_PLEX_ARTWORK_EPISODE = {
'thumb': 'thumb',
'poster': 'grandparentThumb',
'banner': 'banner',
'fanart': 'art'
}
# Might be implemented in the future: 'icon', 'landscape' (16:9) # Might be implemented in the future: 'icon', 'landscape' (16:9)
ALL_KODI_ARTWORK = ( ALL_KODI_ARTWORK = (
'thumb', 'thumb',

View file

@ -125,6 +125,8 @@
<setting id="enableTextureCache" label="30512" type="bool" default="true" /> <!-- Force Artwork Caching --> <setting id="enableTextureCache" label="30512" type="bool" default="true" /> <!-- Force Artwork Caching -->
<setting id="FanartTV" label="30539" type="bool" default="false" /><!-- Download additional art from FanArtTV --> <setting id="FanartTV" label="30539" type="bool" default="false" /><!-- Download additional art from FanArtTV -->
<setting label="39222" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=fanart)" option="close" visible="eq(-1,true)" subsetting="true" /> <!-- Look for missing fanart on FanartTV now --> <setting label="39222" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=fanart)" option="close" visible="eq(-1,true)" subsetting="true" /> <!-- Look for missing fanart on FanartTV now -->
<setting id="imageSyncNotifications" label="30008" type="bool" default="true" /><!-- Enable notifications for image caching -->
<setting id="imageSyncDuringPlayback" label="30009" type="bool" default="true" /><!-- Enable image caching during Kodi playback (restart Kodi!) -->
<setting label="39020" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=texturecache)" option="close" /> <!-- Cache all images to Kodi texture cache now --> <setting label="39020" type="action" action="RunPlugin(plugin://plugin.video.plexkodiconnect/?mode=texturecache)" option="close" /> <!-- Cache all images to Kodi texture cache now -->
</category> </category>
<!-- <!--

View file

@ -135,7 +135,7 @@ class Service():
if window('plex_online') == "true": if window('plex_online') == "true":
# Plex server is online # Plex server is online
# Verify if user is set and has access to the server # Verify if user is set and has access to the server
if (self.user.currUser is not None) and self.user.HasAccess: if (self.user.user is not None) and self.user.has_access:
if not self.kodimonitor_running: if not self.kodimonitor_running:
# Start up events # Start up events
self.warn_auth = True self.warn_auth = True
@ -145,7 +145,7 @@ class Service():
dialog('notification', dialog('notification',
lang(29999), lang(29999),
"%s %s" % (lang(33000), "%s %s" % (lang(33000),
self.user.currUser), self.user.user),
icon='{plex}', icon='{plex}',
time=2000, time=2000,
sound=False) sound=False)
@ -178,7 +178,7 @@ class Service():
self.image_cache_thread_running = True self.image_cache_thread_running = True
self.image_cache_thread.start() self.image_cache_thread.start()
else: else:
if (self.user.currUser is None) and self.warn_auth: if (self.user.user is None) and self.warn_auth:
# Alert user is not authenticated and suppress future # Alert user is not authenticated and suppress future
# warning # warning
self.warn_auth = False self.warn_auth = False
@ -187,9 +187,9 @@ class Service():
# User access is restricted. # User access is restricted.
# Keep verifying until access is granted # Keep verifying until access is granted
# unless server goes offline or Kodi is shut down. # unless server goes offline or Kodi is shut down.
while self.user.HasAccess is False: while self.user.has_access is False:
# Verify access with an API call # Verify access with an API call
self.user.hasAccess() self.user.check_access()
if window('plex_online') != "true": if window('plex_online') != "true":
# Server went offline # Server went offline
@ -202,7 +202,7 @@ class Service():
# 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(): while not self.__stop_PKC():
server = self.user.getServer() server = self.user.get_server()
if server is False: if server is False:
# No server info set in add-on settings # No server info set in add-on settings
pass pass