Merge branch 'master' into translations

This commit is contained in:
croneter 2018-04-17 21:05:06 +02:00
commit 056285f7ae
20 changed files with 913 additions and 791 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.18-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.19-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.18" provider-name="croneter"> <addon id="plugin.video.plexkodiconnect" name="PlexKodiConnect" version="2.0.19" 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.1" /> <import addon="plugin.video.plexkodiconnect.movies" version="2.0.2" />
<import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.2" /> <import addon="plugin.video.plexkodiconnect.tvshows" version="2.0.3" />
</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,15 @@
<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.18 (beta only): <news>version 2.0.19 (beta only):
- Fix PKC playback startup getting caught in infinity loop
- Rewire library sync, suspend sync during playback
- Fix playback failing in certain cases
- Fix PKC not working anymore after using context menu on songs
- Fix deletion of Plex music items
- Code cleanup
version 2.0.18 (beta only):
- Fix some playqueue inconsistencies using Plex Companion - Fix some playqueue inconsistencies using Plex Companion
- Direct paths: fix replaying item where playback was started via PMS - Direct paths: fix replaying item where playback was started via PMS
- Fix Plex trailers screwing up playqueue - Fix Plex trailers screwing up playqueue

View file

@ -1,3 +1,11 @@
version 2.0.19 (beta only):
- Fix PKC playback startup getting caught in infinity loop
- Rewire library sync, suspend sync during playback
- Fix playback failing in certain cases
- Fix PKC not working anymore after using context menu on songs
- Fix deletion of Plex music items
- Code cleanup
version 2.0.18 (beta only): version 2.0.18 (beta only):
- Fix some playqueue inconsistencies using Plex Companion - Fix some playqueue inconsistencies using Plex Companion
- Direct paths: fix replaying item where playback was started via PMS - Direct paths: fix replaying item where playback was started via PMS

View file

@ -171,8 +171,12 @@ class Main():
""" """
Start up playback_starter in main Python thread Start up playback_starter in main Python thread
""" """
request = '%s&handle=%s' % (argv[2], HANDLE)
# Put the request into the 'queue' # Put the request into the 'queue'
plex_command('PLAY', argv[2]) plex_command('PLAY', request)
if HANDLE == -1:
# Handle -1 received, not waiting for main thread
return
# Wait for the result # Wait for the result
while not pickl_window('plex_result'): while not pickl_window('plex_result'):
sleep(50) sleep(50)

View file

@ -94,8 +94,7 @@ class PlexCompanion(Thread):
params = { params = {
'mode': 'plex_node', 'mode': 'plex_node',
'key': '{server}%s' % data.get('key'), 'key': '{server}%s' % data.get('key'),
'offset': data.get('offset'), 'offset': data.get('offset')
'play_directly': 'true'
} }
executebuiltin('RunPlugin(plugin://%s?%s)' executebuiltin('RunPlugin(plugin://%s?%s)'
% (v.ADDON_ID, urlencode(params))) % (v.ADDON_ID, urlencode(params)))

View file

@ -29,7 +29,7 @@ LOG = getLogger("PLEX." + __name__)
WINDOW_PROPERTIES = ( WINDOW_PROPERTIES = (
"plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan", "plex_online", "plex_serverStatus", "plex_shouldStop", "plex_dbScan",
"plex_initialScan", "plex_customplayqueue", "plex_playbackProps", "plex_customplayqueue", "plex_playbackProps",
"pms_token", "plex_token", "pms_server", "plex_machineIdentifier", "pms_token", "plex_token", "pms_server", "plex_machineIdentifier",
"plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths", "plex_servername", "plex_authenticated", "PlexUserImage", "useDirectPaths",
"countError", "countUnauthorized", "plex_restricteduser", "countError", "countUnauthorized", "plex_restricteduser",

View file

@ -1719,6 +1719,7 @@ class Music(Items):
# Update album artwork # Update album artwork
artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) artwork.modify_artwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor)
@catch_exceptions(warnuser=True)
def remove(self, plex_id): def remove(self, plex_id):
""" """
Completely remove the item with plex_id from the Kodi and Plex DBs. Completely remove the item with plex_id from the Kodi and Plex DBs.
@ -1728,92 +1729,124 @@ class Music(Items):
try: try:
kodi_id = plex_dbitem[0] kodi_id = plex_dbitem[0]
file_id = plex_dbitem[1] file_id = plex_dbitem[1]
path_id = plex_dbitem[2]
parent_id = plex_dbitem[3] parent_id = plex_dbitem[3]
kodi_type = plex_dbitem[4] kodi_type = plex_dbitem[4]
LOG.info("Removing %s with kodi_id: %s, parent_id: %s, file_id: %s", LOG.debug('Removing plex_id %s with kodi_type %s, kodi_id %s, '
kodi_type, kodi_id, parent_id, file_id) 'parent_id %s, file_id %s, pathid %s',
plex_id, kodi_type, kodi_id, parent_id, file_id, path_id)
except TypeError: except TypeError:
LOG.debug('Cannot delete item with plex id %s from Kodi', plex_id) LOG.debug('Cannot delete item with plex id %s from Kodi', plex_id)
return return
# Remove the plex reference # Remove the plex reference
self.plex_db.removeItem(plex_id) self.plex_db.removeItem(plex_id)
##### SONG ##### ##### SONG #####
if kodi_type == v.KODI_TYPE_SONG: if kodi_type == v.KODI_TYPE_SONG:
# Delete song # Delete song and orphaned artists and albums
self.remove_song(kodi_id) self._remove_song(kodi_id, path_id=path_id)
# Album verification # Album verification
album = self.plex_db.getItem_byKodiId(parent_id,
for item in self.plex_db.getItem_byWildId(plex_id): v.KODI_TYPE_ALBUM)
if not self.plex_db.getItem_byParentId(parent_id,
item_kid = item[0] v.KODI_TYPE_SONG):
item_kodi_type = item[1] # No song left for album - so delete the album
self.plex_db.removeItem(album[0])
if item_kodi_type == v.KODI_TYPE_ALBUM: self._remove_album(parent_id)
childs = self.plex_db.getItem_byParentId(item_kid,
v.KODI_TYPE_SONG)
if not childs:
# Delete album
self.remove_album(item_kid)
##### ALBUM ##### ##### ALBUM #####
elif kodi_type == v.KODI_TYPE_ALBUM: elif kodi_type == v.KODI_TYPE_ALBUM:
# Delete songs, album # Delete songs, album
album_songs = self.plex_db.getItem_byParentId(kodi_id, songs = self.plex_db.getItem_byParentId(kodi_id,
v.KODI_TYPE_SONG) v.KODI_TYPE_SONG)
for song in album_songs: for song in songs:
self.remove_song(song[1]) self._remove_song(song[1], path_id=song[2])
# Remove plex songs # Remove songs from Plex table
self.plex_db.removeItems_byParentId(kodi_id, self.plex_db.removeItems_byParentId(kodi_id,
v.KODI_TYPE_SONG) v.KODI_TYPE_SONG)
# Remove the album # Remove the album and associated orphaned entries
self.remove_album(kodi_id) self._remove_album(kodi_id)
##### IF ARTIST ##### ##### IF ARTIST #####
elif kodi_type == v.KODI_TYPE_ARTIST: elif kodi_type == v.KODI_TYPE_ARTIST:
# Delete songs, album, artist # Delete songs, album, artist
albums = self.plex_db.getItem_byParentId(kodi_id, v.KODI_TYPE_ALBUM) albums = self.plex_db.getItem_byParentId(kodi_id, v.KODI_TYPE_ALBUM)
for album in albums: for album in albums:
albumid = album[1] songs = self.plex_db.getItem_byParentId(album[1],
album_songs = self.plex_db.getItem_byParentId(albumid, v.KODI_TYPE_SONG)
v.KODI_TYPE_SONG) for song in songs:
for song in album_songs: self._remove_song(song[1], path_id=song[2])
self.remove_song(song[1]) # Remove entries for the songs in the Plex db
# Remove plex song self.plex_db.removeItems_byParentId(album[1], v.KODI_TYPE_SONG)
self.plex_db.removeItems_byParentId(albumid, v.KODI_TYPE_SONG)
# Remove plex artist
self.plex_db.removeItems_byParentId(albumid, v.KODI_TYPE_ARTIST)
# Remove kodi album # Remove kodi album
self.remove_album(albumid) self._remove_album(album[1])
# Remove plex albums # Remove album entries in the Plex db
self.plex_db.removeItems_byParentId(kodi_id, v.KODI_TYPE_ALBUM) self.plex_db.removeItems_byParentId(kodi_id, v.KODI_TYPE_ALBUM)
# Remove artist # Remove artist
self.remove_artist(kodi_id) self._remove_artist(kodi_id)
LOG.debug("Deleted plex_id %s from kodi database", plex_id) LOG.debug("Deleted plex_id %s from kodi database", plex_id)
def remove_song(self, kodi_id): def _remove_song(self, kodi_id, path_id=None):
""" """
Remove song, and only the song Remove song, orphaned artists and orphaned paths
""" """
if not path_id:
query = 'SELECT idPath FROM song WHERE idSong = ? LIMIT 1'
self.kodicursor.execute(query, (kodi_id, ))
try:
path_id = self.kodicursor.fetchone()[0]
except TypeError:
pass
artist_to_delete = self.kodi_db.delete_song_from_song_artist(kodi_id)
if artist_to_delete:
# Delete the artist reference in the Plex table
artist = self.plex_db.getItem_byKodiId(artist_to_delete,
v.KODI_TYPE_ARTIST)
try:
plex_id = artist[0]
except TypeError:
pass
else:
self.plex_db.removeItem(plex_id)
self._remove_artist(artist_to_delete)
self.kodicursor.execute('DELETE FROM song WHERE idSong = ?',
(kodi_id, ))
# Check whether we have orphaned path entries
query = 'SELECT idPath FROM song WHERE idPath = ? LIMIT 1'
self.kodicursor.execute(query, (path_id, ))
if not self.kodicursor.fetchone():
self.kodicursor.execute('DELETE FROM path WHERE idPath = ?',
(path_id, ))
if v.KODIVERSION < 18:
self.kodi_db.delete_song_from_song_genre(kodi_id)
query = 'DELETE FROM albuminfosong WHERE idAlbumInfoSong = ?'
self.kodicursor.execute(query, (kodi_id, ))
self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_SONG, self.kodicursor) self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_SONG, self.kodicursor)
self.kodicursor.execute("DELETE FROM song WHERE idSong = ?",
(kodi_id,))
def remove_album(self, kodi_id): def _remove_album(self, kodi_id):
""" '''
Remove an album, and only the album Remove an album
""" '''
self.kodi_db.delete_album_from_discography(kodi_id)
if v.KODIVERSION < 18:
self.kodi_db.delete_album_from_album_genre(kodi_id)
query = 'DELETE FROM albuminfosong WHERE idAlbumInfo = ?'
self.kodicursor.execute(query, (kodi_id, ))
self.kodicursor.execute('DELETE FROM album_artist WHERE idAlbum = ?',
(kodi_id, ))
self.kodicursor.execute('DELETE FROM album WHERE idAlbum = ?',
(kodi_id, ))
self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_ALBUM, self.kodicursor) self.artwork.delete_artwork(kodi_id, v.KODI_TYPE_ALBUM, self.kodicursor)
self.kodicursor.execute("DELETE FROM album WHERE idAlbum = ?",
(kodi_id,))
def remove_artist(self, kodi_id): def _remove_artist(self, kodi_id):
""" '''
Remove an artist, and only the artist Remove an artist and associated songs and albums
""" '''
self.kodicursor.execute('DELETE FROM album_artist WHERE idArtist = ?',
(kodi_id, ))
self.kodicursor.execute('DELETE FROM artist WHERE idArtist = ?',
(kodi_id, ))
self.kodicursor.execute('DELETE FROM song_artist WHERE idArtist = ?',
(kodi_id, ))
self.kodicursor.execute('DELETE FROM discography WHERE idArtist = ?',
(kodi_id, ))
self.artwork.delete_artwork(kodi_id, self.artwork.delete_artwork(kodi_id,
v.KODI_TYPE_ARTIST, v.KODI_TYPE_ARTIST,
self.kodicursor) self.kodicursor)
self.kodicursor.execute("DELETE FROM artist WHERE idArtist = ?",
(kodi_id,))

View file

@ -907,6 +907,102 @@ class KodiDBMethods(object):
self.cursor.execute(query, (name, artistid,)) self.cursor.execute(query, (name, artistid,))
return artistid return artistid
def delete_song_from_song_artist(self, song_id):
"""
Deletes son from song_artist table and possibly orphaned roles
Will returned an orphaned idArtist or None if not orphaned
"""
query = '''
SELECT idArtist, idRole FROM song_artist WHERE idSong = ? LIMIT 1
'''
self.cursor.execute(query, (song_id, ))
artist = self.cursor.fetchone()
if artist is None:
# No entry to begin with
return
# Delete the entry
self.cursor.execute('DELETE FROM song_artist WHERE idSong = ?',
(song_id, ))
# Check whether we need to delete orphaned roles
query = 'SELECT idRole FROM song_artist WHERE idRole = ? LIMIT 1'
self.cursor.execute(query, (artist[1], ))
if not self.cursor.fetchone():
# Delete orphaned role
self.cursor.execute('DELETE FROM role WHERE idRole = ?',
(artist[1], ))
# Check whether we need to delete orphaned artists
query = 'SELECT idArtist FROM song_artist WHERE idArtist = ? LIMIT 1'
self.cursor.execute(query, (artist[0], ))
if self.cursor.fetchone():
return
else:
return artist[0]
def delete_album_from_discography(self, album_id):
"""
Removes the album with id album_id from the table discography
"""
# Need to get the album name as a string first!
query = 'SELECT strAlbum, iYear FROM album WHERE idAlbum = ? LIMIT 1'
self.cursor.execute(query, (album_id, ))
try:
name, year = self.cursor.fetchone()
except TypeError:
return
query = 'SELECT idArtist FROM album_artist WHERE idAlbum = ? LIMIT 1'
self.cursor.execute(query, (album_id, ))
artist = self.cursor.fetchone()
if not artist:
return
query = '''
DELETE FROM discography
WHERE idArtist = ? AND strAlbum = ? AND strYear = ?
'''
self.cursor.execute(query, (artist[0], name, year))
def delete_song_from_song_genre(self, song_id):
"""
Deletes the one entry with id song_id from the song_genre table.
Will also delete orphaned genres from genre table
"""
query = 'SELECT idGenre FROM song_genre WHERE idSong = ?'
self.cursor.execute(query, (song_id, ))
genres = self.cursor.fetchall()
self.cursor.execute('DELETE FROM song_genre WHERE idSong = ?',
(song_id, ))
# Check for orphaned genres in both song_genre and album_genre tables
query = 'SELECT idGenre FROM song_genre WHERE idGenre = ? LIMIT 1'
query2 = 'SELECT idGenre FROM album_genre WHERE idGenre = ? LIMIT 1'
for genre in genres:
self.cursor.execute(query, (genre[0], ))
if not self.cursor.fetchone():
self.cursor.execute(query2, (genre[0], ))
if not self.cursor.fetchone():
self.cursor.execute('DELETE FROM genre WHERE idGenre = ?',
(genre[0], ))
def delete_album_from_album_genre(self, album_id):
"""
Deletes the one entry with id album_id from the album_genre table.
Will also delete orphaned genres from genre table
"""
query = 'SELECT idGenre FROM album_genre WHERE idAlbum = ?'
self.cursor.execute(query, (album_id, ))
genres = self.cursor.fetchall()
self.cursor.execute('DELETE FROM album_genre WHERE idAlbum = ?',
(album_id, ))
# Check for orphaned genres in both album_genre and song_genre tables
query = 'SELECT idGenre FROM album_genre WHERE idGenre = ? LIMIT 1'
query2 = 'SELECT idGenre FROM song_genre WHERE idGenre = ? LIMIT 1'
for genre in genres:
self.cursor.execute(query, (genre[0], ))
if not self.cursor.fetchone():
self.cursor.execute(query2, (genre[0], ))
if not self.cursor.fetchone():
self.cursor.execute('DELETE FROM genre WHERE idGenre = ?',
(genre[0], ))
def addAlbum(self, name, musicbrainz): def addAlbum(self, name, musicbrainz):
query = 'SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = ?' query = 'SELECT idAlbum FROM album WHERE strMusicBrainzAlbumID = ?'
self.cursor.execute(query, (musicbrainz,)) self.cursor.execute(query, (musicbrainz,))

View file

@ -136,6 +136,7 @@ class KodiMonitor(xbmc.Monitor):
LOG.debug("Method: %s Data: %s", method, data) LOG.debug("Method: %s Data: %s", method, data)
if method == "Player.OnPlay": if method == "Player.OnPlay":
state.SUSPEND_SYNC = True
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
@ -143,12 +144,13 @@ class KodiMonitor(xbmc.Monitor):
if data.get('end'): if data.get('end'):
if state.PKC_CAUSED_STOP is True: if state.PKC_CAUSED_STOP is True:
state.PKC_CAUSED_STOP = False state.PKC_CAUSED_STOP = False
state.PKC_CAUSED_STOP_DONE = True
LOG.debug('PKC caused this playback stop - ignoring') LOG.debug('PKC caused this playback stop - ignoring')
else: else:
_playback_cleanup(ended=True) _playback_cleanup(ended=True)
else: else:
_playback_cleanup() _playback_cleanup()
state.PKC_CAUSED_STOP_DONE = True
state.SUSPEND_SYNC = False
elif method == 'Playlist.OnAdd': elif method == 'Playlist.OnAdd':
self._playlist_onadd(data) self._playlist_onadd(data)
elif method == 'Playlist.OnRemove': elif method == 'Playlist.OnRemove':
@ -253,8 +255,8 @@ class KodiMonitor(xbmc.Monitor):
""" """
playqueue = PQ.PLAYQUEUES[data['playlistid']] playqueue = PQ.PLAYQUEUES[data['playlistid']]
if not playqueue.is_pkc_clear(): if not playqueue.is_pkc_clear():
playqueue.clear(kodi=False)
playqueue.pkc_edit = True playqueue.pkc_edit = True
playqueue.clear(kodi=False)
else: else:
LOG.debug('Detected PKC clear - ignoring') LOG.debug('Detected PKC clear - ignoring')

View file

@ -12,15 +12,16 @@ import variables as v
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
@thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD', @thread_methods(add_suspends=['SUSPEND_LIBRARY_THREAD',
'DB_SCAN', 'DB_SCAN',
'STOP_SYNC']) 'STOP_SYNC',
class Process_Fanart_Thread(Thread): 'SUSPEND_SYNC'])
class ThreadedProcessFanart(Thread):
""" """
Threaded download of additional fanart in the background Threaded download of additional fanart in the background
@ -39,21 +40,10 @@ class Process_Fanart_Thread(Thread):
Thread.__init__(self) Thread.__init__(self)
def run(self): def run(self):
"""
Catch all exceptions and log them
"""
try:
self.__run()
except Exception as e:
log.error('Exception %s' % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
def __run(self):
""" """
Do the work Do the work
""" """
log.debug("---===### Starting FanartSync ###===---") LOG.debug("---===### Starting FanartSync ###===---")
stopped = self.stopped stopped = self.stopped
suspended = self.suspended suspended = self.suspended
queue = self.queue queue = self.queue
@ -63,7 +53,7 @@ class Process_Fanart_Thread(Thread):
# Set in service.py # Set in service.py
if stopped(): if stopped():
# Abort was requested while waiting. We should exit # Abort was requested while waiting. We should exit
log.info("---===### Stopped FanartSync ###===---") LOG.info("---===### Stopped FanartSync ###===---")
return return
sleep(1000) sleep(1000)
# grabs Plex item from queue # grabs Plex item from queue
@ -73,15 +63,14 @@ class Process_Fanart_Thread(Thread):
sleep(200) sleep(200)
continue continue
log.debug('Get additional fanart for Plex id %s' % item['plex_id']) LOG.debug('Get additional fanart for Plex id %s', item['plex_id'])
with getattr(itemtypes, with getattr(itemtypes,
v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as cls: v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as item_type:
result = cls.getfanart(item['plex_id'], result = item_type.getfanart(item['plex_id'],
refresh=item['refresh']) refresh=item['refresh'])
if result is True: if result is True:
log.debug('Done getting fanart for Plex id %s' LOG.debug('Done getting fanart for Plex id %s', item['plex_id'])
% item['plex_id'])
with plexdb.Get_Plex_DB() as plex_db: with plexdb.Get_Plex_DB() as plex_db:
plex_db.set_fanart_synched(item['plex_id']) plex_db.set_fanart_synched(item['plex_id'])
queue.task_done() queue.task_done()
log.debug("---===### Stopped FanartSync ###===---") LOG.debug("---===### Stopped FanartSync ###===---")

View file

@ -11,20 +11,22 @@ import sync_info
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) @thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
class Threaded_Get_Metadata(Thread): 'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedGetMetadata(Thread):
""" """
Threaded download of Plex XML metadata for a certain library item. Threaded download of Plex XML metadata for a certain library item.
Fills the out_queue with the downloaded etree XML objects Fills the out_queue with the downloaded etree XML objects
Input: Input:
queue Queue.Queue() object that you'll need to fill up queue Queue.Queue() object that you'll need to fill up
with Plex itemIds with plex_ids
out_queue Queue() object where this thread will store out_queue Queue() object where this thread will store
the downloaded metadata XMLs as etree objects the downloaded metadata XMLs as etree objects
""" """
@ -60,21 +62,10 @@ class Threaded_Get_Metadata(Thread):
self.out_queue.task_done() self.out_queue.task_done()
def run(self): def run(self):
"""
Catch all exceptions and log them
"""
try:
self.__run()
except Exception as e:
log.error('Exception %s' % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
def __run(self):
""" """
Do the work Do the work
""" """
log.debug('Starting get metadata thread') LOG.debug('Starting get metadata thread')
# cache local variables because it's faster # cache local variables because it's faster
queue = self.queue queue = self.queue
out_queue = self.out_queue out_queue = self.out_queue
@ -88,11 +79,11 @@ class Threaded_Get_Metadata(Thread):
sleep(20) sleep(20)
continue continue
# Download Metadata # Download Metadata
xml = GetPlexMetadata(item['itemId']) xml = GetPlexMetadata(item['plex_id'])
if xml is None: if xml is None:
# Did not receive a valid XML - skip that item for now # Did not receive a valid XML - skip that item for now
log.error("Could not get metadata for %s. Skipping that item " LOG.error("Could not get metadata for %s. Skipping that item "
"for now" % item['itemId']) "for now", item['plex_id'])
# Increase BOTH counters - since metadata won't be processed # Increase BOTH counters - since metadata won't be processed
with sync_info.LOCK: with sync_info.LOCK:
sync_info.GET_METADATA_COUNT += 1 sync_info.GET_METADATA_COUNT += 1
@ -100,21 +91,21 @@ class Threaded_Get_Metadata(Thread):
queue.task_done() queue.task_done()
continue continue
elif xml == 401: elif xml == 401:
log.error('HTTP 401 returned by PMS. Too much strain? ' LOG.error('HTTP 401 returned by PMS. Too much strain? '
'Cancelling sync for now') 'Cancelling sync for now')
window('plex_scancrashed', value='401') window('plex_scancrashed', value='401')
# Kill remaining items in queue (for main thread to cont.) # Kill remaining items in queue (for main thread to cont.)
queue.task_done() queue.task_done()
break break
item['XML'] = xml item['xml'] = xml
if item.get('get_children') is True: if item.get('get_children') is True:
children_xml = GetAllPlexChildren(item['itemId']) children_xml = GetAllPlexChildren(item['plex_id'])
try: try:
children_xml[0].attrib children_xml[0].attrib
except (TypeError, IndexError, AttributeError): except (TypeError, IndexError, AttributeError):
log.error('Could not get children for Plex id %s' LOG.error('Could not get children for Plex id %s',
% item['itemId']) item['plex_id'])
item['children'] = [] item['children'] = []
else: else:
item['children'] = children_xml item['children'] = children_xml
@ -128,4 +119,4 @@ class Threaded_Get_Metadata(Thread):
queue.task_done() queue.task_done()
# Empty queue in case PKC was shut down (main thread hangs otherwise) # Empty queue in case PKC was shut down (main thread hangs otherwise)
self.terminate_now() self.terminate_now()
log.debug('Get metadata thread terminated') LOG.debug('Get metadata thread terminated')

View file

@ -10,13 +10,15 @@ import itemtypes
import sync_info import sync_info
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) @thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
class Threaded_Process_Metadata(Thread): 'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedProcessMetadata(Thread):
""" """
Not yet implemented for more than 1 thread - if ever. Only to be called by Not yet implemented for more than 1 thread - if ever. Only to be called by
ONE thread! ONE thread!
@ -25,12 +27,12 @@ class Threaded_Process_Metadata(Thread):
Input: Input:
queue: Queue.Queue() object that you'll need to fill up with queue: Queue.Queue() object that you'll need to fill up with
the downloaded XML eTree objects the downloaded XML eTree objects
item_type: as used to call functions in itemtypes.py e.g. 'Movies' => item_class: as used to call functions in itemtypes.py e.g. 'Movies' =>
itemtypes.Movies() itemtypes.Movies()
""" """
def __init__(self, queue, item_type): def __init__(self, queue, item_class):
self.queue = queue self.queue = queue
self.item_type = item_type self.item_class = item_class
Thread.__init__(self) Thread.__init__(self)
def terminate_now(self): def terminate_now(self):
@ -49,23 +51,12 @@ class Threaded_Process_Metadata(Thread):
self.queue.task_done() self.queue.task_done()
def run(self): def run(self):
"""
Catch all exceptions and log them
"""
try:
self.__run()
except Exception as e:
log.error('Exception %s' % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
def __run(self):
""" """
Do the work Do the work
""" """
log.debug('Processing thread started') LOG.debug('Processing thread started')
# Constructs the method name, e.g. itemtypes.Movies # Constructs the method name, e.g. itemtypes.Movies
item_fct = getattr(itemtypes, self.item_type) item_fct = getattr(itemtypes, self.item_class)
# cache local variables because it's faster # cache local variables because it's faster
queue = self.queue queue = self.queue
stopped = self.stopped stopped = self.stopped
@ -79,24 +70,19 @@ class Threaded_Process_Metadata(Thread):
continue continue
# Do the work # Do the work
item_method = getattr(item_class, item['method']) item_method = getattr(item_class, item['method'])
if item.get('children') is not None: if item.get('children'):
item_method(item['XML'][0], item_method(item['xml'][0],
viewtag=item['viewName'], viewtag=item['view_name'],
viewid=item['viewId'], viewid=item['view_id'],
children=item['children']) children=item['children'])
else: else:
item_method(item['XML'][0], item_method(item['xml'][0],
viewtag=item['viewName'], viewtag=item['view_name'],
viewid=item['viewId']) viewid=item['view_id'])
# Keep track of where we are at # Keep track of where we are at
try:
log.debug('found child: %s'
% item['children'].attrib)
except:
pass
with sync_info.LOCK: with sync_info.LOCK:
sync_info.PROCESS_METADATA_COUNT += 1 sync_info.PROCESS_METADATA_COUNT += 1
sync_info.PROCESSING_VIEW_NAME = item['title'] sync_info.PROCESSING_VIEW_NAME = item['title']
queue.task_done() queue.task_done()
self.terminate_now() self.terminate_now()
log.debug('Processing thread terminated') LOG.debug('Processing thread terminated')

View file

@ -2,14 +2,14 @@
from logging import getLogger from logging import getLogger
from threading import Thread, Lock from threading import Thread, Lock
from xbmc import sleep, Player from xbmc import sleep
from xbmcgui import DialogProgressBG from xbmcgui import DialogProgressBG
from utils import thread_methods, language as lang from utils import thread_methods, language as lang
############################################################################### ###############################################################################
log = getLogger("PLEX."+__name__) LOG = getLogger("PLEX." + __name__)
GET_METADATA_COUNT = 0 GET_METADATA_COUNT = 0
PROCESS_METADATA_COUNT = 0 PROCESS_METADATA_COUNT = 0
@ -19,8 +19,10 @@ LOCK = Lock()
############################################################################### ###############################################################################
@thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD', 'STOP_SYNC']) @thread_methods(add_stops=['SUSPEND_LIBRARY_THREAD',
class Threaded_Show_Sync_Info(Thread): 'STOP_SYNC',
'SUSPEND_SYNC'])
class ThreadedShowSyncInfo(Thread):
""" """
Threaded class to show the Kodi statusbar of the metadata download. Threaded class to show the Kodi statusbar of the metadata download.
@ -34,38 +36,26 @@ class Threaded_Show_Sync_Info(Thread):
Thread.__init__(self) Thread.__init__(self)
def run(self): def run(self):
"""
Catch all exceptions and log them
"""
try:
self.__run()
except Exception as e:
log.error('Exception %s' % e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
def __run(self):
""" """
Do the work Do the work
""" """
log.debug('Show sync info thread started') LOG.debug('Show sync info thread started')
# cache local variables because it's faster # cache local variables because it's faster
total = self.total total = self.total
dialog = DialogProgressBG('dialoglogProgressBG') dialog = DialogProgressBG('dialoglogProgressBG')
dialog.create("%s %s: %s %s" dialog.create("%s %s: %s %s"
% (lang(39714), self.item_type, str(total), lang(39715))) % (lang(39714), self.item_type, str(total), lang(39715)))
player = Player()
total = 2 * total total = 2 * total
totalProgress = 0 total_progress = 0
while self.stopped() is False and not player.isPlaying(): while not self.stopped():
with LOCK: with LOCK:
get_progress = GET_METADATA_COUNT get_progress = GET_METADATA_COUNT
process_progress = PROCESS_METADATA_COUNT process_progress = PROCESS_METADATA_COUNT
viewName = PROCESSING_VIEW_NAME view_name = PROCESSING_VIEW_NAME
totalProgress = get_progress + process_progress total_progress = get_progress + process_progress
try: try:
percentage = int(float(totalProgress) / float(total)*100.0) percentage = int(float(total_progress) / float(total)*100.0)
except ZeroDivisionError: except ZeroDivisionError:
percentage = 0 percentage = 0
dialog.update(percentage, dialog.update(percentage,
@ -74,8 +64,8 @@ class Threaded_Show_Sync_Info(Thread):
lang(39712), lang(39712),
process_progress, process_progress,
lang(39713), lang(39713),
viewName)) view_name))
# Sleep for x milliseconds # Sleep for x milliseconds
sleep(200) sleep(200)
dialog.close() dialog.close()
log.debug('Show sync info thread terminated') LOG.debug('Show sync info thread terminated')

File diff suppressed because it is too large Load diff

View file

@ -124,8 +124,12 @@ def _playback_init(plex_id, plex_type, playqueue, pos):
LOG.debug('Playing trailers: %s', trailers) LOG.debug('Playing trailers: %s', trailers)
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
while not state.PKC_CAUSED_STOP_DONE: while not state.PKC_CAUSED_STOP_DONE:
sleep(50) sleep(50)
sleep_timer += 1
if sleep_timer > 100:
break
playqueue.clear() playqueue.clear()
if plex_type != v.PLEX_TYPE_CLIP: if plex_type != v.PLEX_TYPE_CLIP:
# Post to the PMS to create a playqueue - in any case due to Companion # Post to the PMS to create a playqueue - in any case due to Companion

View file

@ -18,46 +18,50 @@ LOG = getLogger("PLEX." + __name__)
############################################################################### ###############################################################################
class Playback_Starter(Thread): class PlaybackStarter(Thread):
""" """
Processes new plays Processes new plays
""" """
def triage(self, item): @staticmethod
try: def _triage(item):
_, params = item.split('?', 1) _, params = item.split('?', 1)
except ValueError: params = dict(parse_qsl(params))
mode = params.get('mode')
resolve = False if params.get('handle') == '-1' else True
LOG.debug('Received mode: %s, params: %s', mode, params)
if mode == 'play':
playback.playback_triage(plex_id=params.get('plex_id'),
plex_type=params.get('plex_type'),
path=params.get('path'),
resolve=resolve)
elif mode == 'plex_node':
playback.process_indirect(params['key'],
params['offset'],
resolve=resolve)
elif mode == 'navigation':
# e.g. when plugin://...tvshows is called for entire season # e.g. when plugin://...tvshows is called for entire season
with kodidb.GetKodiDB('video') as kodi_db: with kodidb.GetKodiDB('video') as kodi_db:
show_id = kodi_db.show_id_from_path(item) show_id = kodi_db.show_id_from_path(params.get('path'))
if show_id: if show_id:
js.activate_window('videos', js.activate_window('videos',
'videodb://tvshows/titles/%s' % show_id) 'videodb://tvshows/titles/%s' % show_id)
else: else:
LOG.error('Could not find tv show id for %s', item) LOG.error('Could not find tv show id for %s', item)
pickle_me(Playback_Successful()) if resolve:
return pickle_me(Playback_Successful())
params = dict(parse_qsl(params))
mode = params.get('mode')
LOG.debug('Received mode: %s, params: %s', mode, params)
if mode == 'play':
playback.playback_triage(plex_id=params.get('plex_id'),
plex_type=params.get('plex_type'),
path=params.get('path'))
elif mode == 'plex_node':
playback.process_indirect(params['key'], params['offset'])
elif mode == 'context_menu': elif mode == 'context_menu':
ContextMenu(kodi_id=params['kodi_id'], ContextMenu(kodi_id=params.get('kodi_id'),
kodi_type=params['kodi_type']) kodi_type=params.get('kodi_type'))
def run(self): def run(self):
queue = state.COMMAND_PIPELINE_QUEUE queue = state.COMMAND_PIPELINE_QUEUE
LOG.info("----===## Starting Playback_Starter ##===----") LOG.info("----===## Starting PlaybackStarter ##===----")
while True: while True:
item = queue.get() item = queue.get()
if item is None: if item is None:
# Need to shutdown - initiated by command_pipeline # Need to shutdown - initiated by command_pipeline
break break
else: else:
self.triage(item) self._triage(item)
queue.task_done() queue.task_done()
LOG.info("----===## Playback_Starter stopped ##===----") LOG.info("----===## PlaybackStarter stopped ##===----")

View file

@ -237,6 +237,7 @@ class Plex_DB_Functions():
SELECT plex_id, parent_id, plex_type SELECT plex_id, parent_id, plex_type
FROM plex FROM plex
WHERE kodi_id = ? AND kodi_type = ? WHERE kodi_id = ? AND kodi_type = ?
LIMIT 1
''' '''
self.plexcursor.execute(query, (kodi_id, kodi_type,)) self.plexcursor.execute(query, (kodi_id, kodi_type,))
return self.plexcursor.fetchone() return self.plexcursor.fetchone()

View file

@ -10,6 +10,9 @@ STOP_PKC = False
SUSPEND_LIBRARY_THREAD = False SUSPEND_LIBRARY_THREAD = False
# Set if user decided to cancel sync # Set if user decided to cancel sync
STOP_SYNC = False STOP_SYNC = False
# Set e.g. 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? # Could we access the paths?
PATH_VERIFIED = False PATH_VERIFIED = False
# Set if a Plex-Kodi DB sync is being done - along with # Set if a Plex-Kodi DB sync is being done - along with
@ -36,8 +39,6 @@ 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
# Have we already checked the Kodi DB on consistency?
KODI_DB_CHECKED = False
# 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

@ -1050,11 +1050,11 @@ def thread_methods(cls=None, add_stops=None, add_suspends=None):
suspends suspends
invoke with either invoke with either
@Newthread_methods @thread_methods
class MyClass(): class MyClass():
or or
@Newthread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'], @thread_methods(add_stops=['SUSPEND_LIBRARY_TRHEAD'],
add_suspends=['DB_SCAN', 'WHATEVER']) add_suspends=['DB_SCAN', 'WHATEVER'])
class MyClass(): class MyClass():
""" """
# So we don't need to invoke with () # So we don't need to invoke with ()

View file

@ -38,7 +38,7 @@ from websocket_client import PMS_Websocket, Alexa_Websocket
from PlexFunctions import check_connection from PlexFunctions import check_connection
from PlexCompanion import PlexCompanion from PlexCompanion import PlexCompanion
from command_pipeline import Monitor_Window from command_pipeline import Monitor_Window
from playback_starter import Playback_Starter from playback_starter import PlaybackStarter
from playqueue import PlayqueueMonitor from playqueue import PlayqueueMonitor
from artwork import Image_Cache_Thread from artwork import Image_Cache_Thread
import variables as v import variables as v
@ -111,7 +111,7 @@ class Service():
self.library = LibrarySync() self.library = LibrarySync()
self.plexCompanion = PlexCompanion() self.plexCompanion = PlexCompanion()
self.specialMonitor = SpecialMonitor() self.specialMonitor = SpecialMonitor()
self.playback_starter = Playback_Starter() self.playback_starter = PlaybackStarter()
self.playqueue = PlayqueueMonitor() self.playqueue = PlayqueueMonitor()
if settings('enableTextureCache') == "true": if settings('enableTextureCache') == "true":
self.image_cache_thread = Image_Cache_Thread() self.image_cache_thread = Image_Cache_Thread()