From 0cda3563de53e3afe61867b649035044460469b0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 31 Mar 2017 16:42:39 +0200 Subject: [PATCH 1/9] Remove obsolete method --- resources/lib/itemtypes.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 5c419afd..4e853611 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1259,14 +1259,6 @@ class TVShows(Items): class Music(Items): - def __init__(self): - Items.__init__(self) - - self.directstream = settings('streamMusic') == "true" - self.enableimportsongrating = settings('enableImportSongRating') == "true" - self.enableexportsongrating = settings('enableExportSongRating') == "true" - self.enableupdatesongrating = settings('enableUpdateSongRating') == "true" - def __enter__(self): """ OVERWRITE this method, because we need to open another DB. From 498dfb67d7aaf2115c96c89f736be16332d22027 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 31 Mar 2017 16:43:05 +0200 Subject: [PATCH 2/9] Remove obsolete import --- resources/lib/itemtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 4e853611..b99a957c 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -9,7 +9,7 @@ from datetime import datetime from xbmc import sleep import artwork -from utils import tryEncode, tryDecode, settings, window, kodiSQL, \ +from utils import tryEncode, tryDecode, window, kodiSQL, \ CatchExceptions import plexdb_functions as plexdb import kodidb_functions as kodidb From a3201f8a30f53037e9fcce2851ec61ba373aeb26 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 31 Mar 2017 16:43:44 +0200 Subject: [PATCH 3/9] Beautify --- resources/lib/itemtypes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index b99a957c..2e994043 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -9,8 +9,7 @@ from datetime import datetime from xbmc import sleep import artwork -from utils import tryEncode, tryDecode, window, kodiSQL, \ - CatchExceptions +from utils import tryEncode, tryDecode, window, kodiSQL, CatchExceptions import plexdb_functions as plexdb import kodidb_functions as kodidb From 95fd016bd792a4ab88da2789f8a96a3937885c5c Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sat, 1 Apr 2017 18:28:02 +0200 Subject: [PATCH 4/9] Code refactoring of download in chunks --- resources/lib/PlexFunctions.py | 57 ++++++++++++---------------------- resources/lib/entrypoint.py | 4 +-- resources/lib/librarysync.py | 45 ++++++++++----------------- 3 files changed, 37 insertions(+), 69 deletions(-) diff --git a/resources/lib/PlexFunctions.py b/resources/lib/PlexFunctions.py index 04e1a6ff..9c1284e2 100644 --- a/resources/lib/PlexFunctions.py +++ b/resources/lib/PlexFunctions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import logging +from logging import getLogger from urllib import urlencode from ast import literal_eval from urlparse import urlparse, parse_qsl @@ -12,7 +12,9 @@ from variables import PLEX_TO_KODI_TIMEFACTOR ############################################################################### -log = logging.getLogger("PLEX."+__name__) +log = getLogger("PLEX."+__name__) + +CONTAINERSIZE = int(settings('limitindex')) ############################################################################### @@ -141,7 +143,7 @@ def GetPlexMetadata(key): return xml -def GetAllPlexChildren(key, containerSize=None): +def GetAllPlexChildren(key): """ Returns a list (raw xml API dump) of all Plex children for the key. (e.g. /library/metadata/194853/children pointing to a season) @@ -149,11 +151,10 @@ def GetAllPlexChildren(key, containerSize=None): Input: key Key to a Plex item, e.g. 12345 """ - url = "{server}/library/metadata/%s/children?" % key - return DownloadChunks(url, containerSize) + return DownloadChunks("{server}/library/metadata/%s/children?" % key) -def GetPlexSectionResults(viewId, args=None, containerSize=None): +def GetPlexSectionResults(viewId, args=None): """ Returns a list (XML API dump) of all Plex items in the Plex section with key = viewId. @@ -166,38 +167,23 @@ def GetPlexSectionResults(viewId, args=None, containerSize=None): url = "{server}/library/sections/%s/all?" % viewId if args: url += urlencode(args) + '&' - return DownloadChunks(url, containerSize) + return DownloadChunks(url) -def DownloadChunks(url, containerSize): +def DownloadChunks(url): """ - Downloads PMS url in chunks of containerSize (int). - If containerSize is None: ONE xml is fetched directly + Downloads PMS url in chunks of CONTAINERSIZE. url MUST end with '?' (if no other url encoded args are present) or '&' Returns a stitched-together xml or None. """ - if containerSize is None: - # Get rid of '?' or '&' at the end of url - xml = downloadutils.DownloadUtils().downloadUrl(url[:-1]) - if xml == 401: - return 401 - try: - xml.attrib - except AttributeError: - # Nope, not an XML, abort - log.error("Error getting url %s" % url[:-1]) - return None - else: - return xml - xml = None pos = 0 errorCounter = 0 while errorCounter < 10: args = { - 'X-Plex-Container-Size': containerSize, + 'X-Plex-Container-Size': CONTAINERSIZE, 'X-Plex-Container-Start': pos } xmlpart = downloadutils.DownloadUtils().downloadUrl( @@ -208,33 +194,32 @@ def DownloadChunks(url, containerSize): except AttributeError: log.error('Error while downloading chunks: %s' % (url + urlencode(args))) - pos += containerSize + pos += CONTAINERSIZE errorCounter += 1 continue # Very first run: starting xml (to retain data in xml's root!) if xml is None: xml = deepcopy(xmlpart) - if len(xmlpart) < containerSize: + if len(xmlpart) < CONTAINERSIZE: break else: - pos += containerSize + pos += CONTAINERSIZE continue # Build answer xml - containing the entire library for child in xmlpart: xml.append(child) # Done as soon as we don't receive a full complement of items - if len(xmlpart) < containerSize: + if len(xmlpart) < CONTAINERSIZE: break - pos += containerSize + pos += CONTAINERSIZE if errorCounter == 10: log.error('Fatal error while downloading chunks for %s' % url) return None return xml -def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None, - containerSize=None): +def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None): """ Returns a list (raw XML API dump) of all Plex subitems for the key. (e.g. /library/sections/2/allLeaves pointing to all TV shows) @@ -245,7 +230,6 @@ def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None, since that point of time until now. updatedAt Unix timestamp; only retrieves PMS items updated by the PMS since that point of time until now. - containerSize Number of items simultaneously fetched from PMS If lastViewedAt and updatedAt=None, ALL PMS items are returned. @@ -265,14 +249,13 @@ def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None, url += '?' + '&'.join(args) + '&' else: url += '?' - return DownloadChunks(url, containerSize) + return DownloadChunks(url) -def GetPlexOnDeck(viewId, containerSize=None): +def GetPlexOnDeck(viewId): """ """ - url = "{server}/library/sections/%s/onDeck?" % viewId - return DownloadChunks(url, containerSize) + return DownloadChunks("{server}/library/sections/%s/onDeck?" % viewId) def GetPlexCollections(mediatype): diff --git a/resources/lib/entrypoint.py b/resources/lib/entrypoint.py index df028666..2f373308 100644 --- a/resources/lib/entrypoint.py +++ b/resources/lib/entrypoint.py @@ -791,9 +791,7 @@ def browse_plex(key=None, plex_section_id=None): if key: xml = downloadutils.DownloadUtils().downloadUrl('{server}%s' % key) else: - xml = GetPlexSectionResults( - plex_section_id, - containerSize=int(settings('limitindex'))) + xml = GetPlexSectionResults(plex_section_id) try: xml[0].attrib except (ValueError, AttributeError, IndexError, TypeError): diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 8de3a96a..2db99558 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -346,7 +346,6 @@ class LibrarySync(Thread): self.enableMusic = settings('enableMusic') == "true" self.enableBackgroundSync = settings( 'enableBackgroundSync') == "true" - self.limitindex = int(settings('limitindex')) # Init for replacing paths window('remapSMB', value=settings('remapSMB')) @@ -422,8 +421,7 @@ class LibrarySync(Thread): if not view.attrib['type'] == mediatype: continue libraryId = view.attrib['key'] - items = GetAllPlexLeaves(libraryId, - containerSize=self.limitindex) + items = GetAllPlexLeaves(libraryId) if items in (None, 401): log.error("Could not download section %s" % view.attrib['key']) @@ -468,9 +466,7 @@ class LibrarySync(Thread): # Let the PMS process this first! xbmc.sleep(1000) # Get PMS items to find the item we just changed - items = GetAllPlexLeaves(libraryId, - lastViewedAt=timestamp, - containerSize=self.limitindex) + items = GetAllPlexLeaves(libraryId, lastViewedAt=timestamp) # Toggle watched state back scrobble(plexId, 'unwatched') if items in (None, 401): @@ -1083,8 +1079,7 @@ class LibrarySync(Thread): # Get items per view viewId = view['id'] viewName = view['name'] - all_plexmovies = GetPlexSectionResults( - viewId, args=None, containerSize=self.limitindex) + all_plexmovies = GetPlexSectionResults(viewId, args=None) if all_plexmovies is None: log.info("Couldnt get section items, aborting for view.") continue @@ -1127,8 +1122,7 @@ class LibrarySync(Thread): return xml = GetAllPlexLeaves(viewId, lastViewedAt=lastViewedAt, - updatedAt=updatedAt, - containerSize=self.limitindex) + updatedAt=updatedAt) # Return if there are no items in PMS reply - it's faster try: xml[0].attrib @@ -1178,8 +1172,7 @@ class LibrarySync(Thread): # Get items per view viewId = view['id'] viewName = view['name'] - allPlexTvShows = GetPlexSectionResults( - viewId, containerSize=self.limitindex) + allPlexTvShows = GetPlexSectionResults(viewId) if allPlexTvShows is None: log.error("Error downloading show xml for view %s" % viewId) continue @@ -1206,8 +1199,7 @@ class LibrarySync(Thread): if self.threadStopped(): return False # Grab all seasons to tvshow from PMS - seasons = GetAllPlexChildren( - tvShowId, containerSize=self.limitindex) + seasons = GetAllPlexChildren(tvShowId) if seasons is None: log.error("Error download season xml for show %s" % tvShowId) continue @@ -1232,8 +1224,7 @@ class LibrarySync(Thread): if self.threadStopped(): return False # Grab all episodes to tvshow from PMS - episodes = GetAllPlexLeaves( - view['id'], containerSize=self.limitindex) + episodes = GetAllPlexLeaves(view['id']) if episodes is None: log.error("Error downloading episod xml for view %s" % view.get('name')) @@ -1297,12 +1288,17 @@ class LibrarySync(Thread): } # Process artist, then album and tracks last to minimize overhead + # Each album needs to be processed directly with its songs + # Remaining songs without album will be processed last for kind in (v.PLEX_TYPE_ARTIST, v.PLEX_TYPE_ALBUM, v.PLEX_TYPE_SONG): if self.threadStopped(): return False log.debug("Start processing music %s" % kind) + self.allKodiElementsId = {} + self.allPlexElementsId = {} + self.updatelist = [] if self.ProcessMusic(views, kind, urlArgs[kind], @@ -1326,10 +1322,6 @@ class LibrarySync(Thread): return True def ProcessMusic(self, views, kind, urlArgs, method): - self.allKodiElementsId = {} - self.allPlexElementsId = {} - self.updatelist = [] - # Get a list of items already existing in Kodi db if self.compare: with plexdb.Get_Plex_DB() as plex_db: @@ -1340,17 +1332,13 @@ class LibrarySync(Thread): # Yet empty/nothing yet synched except ValueError: pass - for view in views: if self.threadStopped(): return False # Get items per view - viewId = view['id'] - viewName = view['name'] - itemsXML = GetPlexSectionResults( - viewId, args=urlArgs, containerSize=self.limitindex) + itemsXML = GetPlexSectionResults(view['id'], args=urlArgs) if itemsXML is None: - log.error("Error downloading xml for view %s" % viewId) + log.error("Error downloading xml for view %s" % view['id']) continue elif itemsXML == 401: return False @@ -1358,9 +1346,8 @@ class LibrarySync(Thread): self.GetUpdatelist(itemsXML, 'Music', method, - viewName, - viewId) - + view['name'], + view['id']) if self.compare: # Manual sync, process deletes with itemtypes.Music() as Music: From c049e8d05e95dae321f92a75d9af12cf6a339ac0 Mon Sep 17 00:00:00 2001 From: RickDB Date: Sun, 2 Apr 2017 14:11:13 +0200 Subject: [PATCH 5/9] Included additional info in stream selection dialog --- resources/lib/PlexAPI.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index a324f2e2..fec9e228 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -30,6 +30,7 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt (and others...) """ +import os import logging from time import time import urllib2 @@ -2186,12 +2187,31 @@ class API(): # Several streams/files available. dialoglist = [] for entry in self.item.findall('./Media'): + fileName = '' + audioLanguage = '' + + # Get additional info (filename / languages) + mediaPartEntry = self.item.find('./Media/Part') + if mediaPartEntry is not None: + # Filename + if 'file' in mediaPartEntry.attrib: + fileName = os.path.basename(mediaPartEntry.attrib['file']) + + # Languages - subtitle does not seem to be directly included in this stream info + mediaPartStreamEntry = self.item.find('./Media/Part/Stream') + if mediaPartStreamEntry is not None: + # Audio language + if 'language' in mediaPartStreamEntry.attrib: + audioLanguage = mediaPartStreamEntry.attrib['language'] + dialoglist.append( - "%sp %s - %s (%s)" - % (entry.attrib.get('videoResolution', 'unknown'), - entry.attrib.get('videoCodec', 'unknown'), - entry.attrib.get('audioProfile', 'unknown'), - entry.attrib.get('audioCodec', 'unknown')) + "%sp %s - %s (%s) (%s) %s" % + (entry.attrib.get('videoResolution', 'unknown'), + entry.attrib.get('videoCodec', 'unknown'), + entry.attrib.get('audioProfile', 'unknown'), + entry.attrib.get('audioCodec', 'unknown'), + audioLanguage, + fileName) ) media = xbmcgui.Dialog().select('Select stream', dialoglist) else: From e64746f277e24f30c13aee9dcb9954a4607c5a79 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 2 Apr 2017 17:02:41 +0200 Subject: [PATCH 6/9] Huge music overhaul - Fixes #254 --- resources/lib/itemtypes.py | 346 +++++++++--------- resources/lib/library_sync/__init__.py | 1 + resources/lib/library_sync/fanart.py | 88 +++++ resources/lib/library_sync/get_metadata.py | 140 +++++++ .../lib/library_sync/process_metadata.py | 104 ++++++ resources/lib/library_sync/sync_info.py | 81 ++++ resources/lib/librarysync.py | 341 ++--------------- 7 files changed, 623 insertions(+), 478 deletions(-) create mode 100644 resources/lib/library_sync/__init__.py create mode 100644 resources/lib/library_sync/fanart.py create mode 100644 resources/lib/library_sync/get_metadata.py create mode 100644 resources/lib/library_sync/process_metadata.py create mode 100644 resources/lib/library_sync/sync_info.py diff --git a/resources/lib/itemtypes.py b/resources/lib/itemtypes.py index 2e994043..5fa46dc9 100644 --- a/resources/lib/itemtypes.py +++ b/resources/lib/itemtypes.py @@ -1296,7 +1296,7 @@ class Music(Items): name, sortname = API.getTitle() # musicBrainzId = API.getProvider('MusicBrainzArtist') musicBrainzId = None - genres = API.joinList(API.getGenres()) + genres = ' / '.join(API.getGenres()) bio = API.getPlot() # Associate artwork @@ -1335,31 +1335,32 @@ class Music(Items): # Process the artist if v.KODIVERSION >= 16: - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?", - "WHERE idArtist = ?" - )) + query = ''' + UPDATE artist + SET strGenres = ?, strBiography = ?, strImage = ?, + strFanart = ?, lastScraped = ? + WHERE idArtist = ? + ''' kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, artistid)) else: - query = ' '.join(( - - "UPDATE artist", - "SET strGenres = ?, strBiography = ?, strImage = ?, strFanart = ?,", - "lastScraped = ?, dateAdded = ?", - "WHERE idArtist = ?" - )) + query = ''' + UPDATE artist + SET strGenres = ?, strBiography = ?, strImage = ?, + strFanart = ?, lastScraped = ?, dateAdded = ? + WHERE idArtist = ? + ''' kodicursor.execute(query, (genres, bio, thumb, fanart, lastScraped, dateadded, artistid)) # Update artwork - artwork.addArtwork(artworks, artistid, "artist", kodicursor) + artwork.addArtwork(artworks, artistid, v.KODI_TYPE_ARTIST, kodicursor) @CatchExceptions(warnuser=True) - def add_updateAlbum(self, item, viewtag=None, viewid=None): + def add_updateAlbum(self, item, viewtag=None, viewid=None, children=None): + """ + children: list of child xml's, so in this case songs + """ kodicursor = self.kodicursor plex_db = self.plex_db artwork = self.artwork @@ -1387,21 +1388,21 @@ class Music(Items): # musicBrainzId = API.getProvider('MusicBrainzAlbum') musicBrainzId = None year = API.getYear() - genres = API.getGenres() - genre = API.joinList(genres) + self.genres = API.getGenres() + self.genre = ' / '.join(self.genres) bio = API.getPlot() rating = userdata['UserRating'] studio = API.getMusicStudio() - # artists = item['AlbumArtists'] - # if not artists: - # artists = item['ArtistItems'] - # artistname = [] - # for artist in artists: - # artistname.append(artist['Name']) artistname = item.attrib.get('parentTitle') if not artistname: artistname = item.attrib.get('originalTitle') - + # See if we have a compilation - Plex does NOT feature a compilation + # flag for albums + self.compilation = 0 + for child in children: + if child.attrib.get('originalTitle') is not None: + self.compilation = 1 + break # Associate artwork artworks = API.getAllArtwork(parentInfo=True) thumb = artworks['Primary'] @@ -1433,56 +1434,54 @@ class Music(Items): # Process the album info if v.KODIVERSION >= 17: # Kodi Krypton - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iUserrating = ?, lastScraped = ?, strReleaseType = ?, " - "strLabel = ? ", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, - rating, lastScraped, "album", studio, - albumid)) + query = ''' + UPDATE album + SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, + strImage = ?, iUserrating = ?, lastScraped = ?, + strReleaseType = ?, strLabel = ?, bCompilation = ? + WHERE idAlbum = ? + ''' + kodicursor.execute(query, (artistname, year, self.genre, bio, + thumb, rating, lastScraped, + v.KODI_TYPE_ALBUM, studio, + self.compilation, albumid)) elif v.KODIVERSION == 16: # Kodi Jarvis - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, strReleaseType = ?, " - "strLabel = ? ", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, - rating, lastScraped, "album", studio, - albumid)) + query = ''' + UPDATE album + SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, + strImage = ?, iRating = ?, lastScraped = ?, + strReleaseType = ?, strLabel = ?, bCompilation = ? + WHERE idAlbum = ? + ''' + kodicursor.execute(query, (artistname, year, self.genre, bio, + thumb, rating, lastScraped, + v.KODI_TYPE_ALBUM, studio, + self.compilation, albumid)) elif v.KODIVERSION == 15: # Kodi Isengard - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?, " - "strReleaseType = ?, strLabel = ? ", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, - rating, lastScraped, dateadded, - "album", studio, albumid)) + query = ''' + UPDATE album + SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, + strImage = ?, iRating = ?, lastScraped = ?, dateAdded = ?, + strReleaseType = ?, strLabel = ? + WHERE idAlbum = ? + ''' + kodicursor.execute(query, (artistname, year, self.genre, bio, + thumb, rating, lastScraped, dateadded, + v.KODI_TYPE_ALBUM, studio, albumid)) else: # Kodi Helix - query = ' '.join(( - - "UPDATE album", - "SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, strImage = ?,", - "iRating = ?, lastScraped = ?, dateAdded = ?, " - "strLabel = ? ", - "WHERE idAlbum = ?" - )) - kodicursor.execute(query, (artistname, year, genre, bio, thumb, - rating, lastScraped, dateadded, studio, - albumid)) + query = ''' + UPDATE album + SET strArtists = ?, iYear = ?, strGenres = ?, strReview = ?, + strImage = ?, iRating = ?, lastScraped = ?, dateAdded = ?, + strLabel = ? + WHERE idAlbum = ? + ''' + kodicursor.execute(query, (artistname, year, self.genre, bio, + thumb, rating, lastScraped, dateadded, + studio, albumid)) # Associate the parentid for plex reference parentId = item.attrib.get('parentRatingKey') @@ -1496,7 +1495,7 @@ class Music(Items): artist = GetPlexMetadata(parentId) # Item may not be an artist, verification necessary. if artist is not None and artist != 401: - if artist[0].attrib.get('type') == "artist": + if artist[0].attrib.get('type') == v.PLEX_TYPE_ARTIST: # Update with the parentId, for remove reference plex_db.addReference(parentId, v.PLEX_TYPE_ARTIST, @@ -1530,29 +1529,26 @@ class Music(Items): % (artistname, artistid)) # Add artist to album - query = ( - ''' + query = ''' INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) - VALUES (?, ?, ?) - ''' - ) + ''' kodicursor.execute(query, (artistid, albumid, artistname)) # Update discography - query = ( - ''' + query = ''' INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) - VALUES (?, ?, ?) - ''' - ) + ''' kodicursor.execute(query, (artistid, name, year)) # Update plex reference with parentid plex_db.updateParentId(artistId, albumid) # Add genres - self.kodi_db.addMusicGenres(albumid, genres, "album") + self.kodi_db.addMusicGenres(albumid, self.genres, v.KODI_TYPE_ALBUM) # Update artwork - artwork.addArtwork(artworks, albumid, "album", kodicursor) + artwork.addArtwork(artworks, albumid, v.KODI_TYPE_ALBUM, kodicursor) + # Add all children - all tracks + for child in children: + self.add_updateSong(child, viewtag, viewid) @CatchExceptions(warnuser=True) def add_updateSong(self, item, viewtag=None, viewid=None): @@ -1592,9 +1588,22 @@ class Music(Items): title, sorttitle = API.getTitle() # musicBrainzId = API.getProvider('MusicBrainzTrackId') musicBrainzId = None - genres = API.getGenres() - genre = API.joinList(genres) - artists = item.attrib.get('grandparentTitle') + try: + genres = self.genres + genre = self.genre + except AttributeError: + # No parent album - hence no genre information from Plex + genres = None + genre = None + try: + if self.compilation == 0: + artists = item.attrib.get('grandparentTitle') + else: + artists = item.attrib.get('originalTitle') + except AttributeError: + # compilation not set + artists = item.attrib.get('originalTitle', + item.attrib.get('grandparentTitle')) tracknumber = int(item.attrib.get('index', 0)) disc = int(item.attrib.get('parentIndex', 1)) if disc == 1: @@ -1604,9 +1613,13 @@ class Music(Items): year = API.getYear() resume, duration = API.getRuntime() rating = userdata['UserRating'] - - hasEmbeddedCover = False comment = None + # Moods + moods = [] + for entry in item: + if entry.tag == 'Mood': + moods.append(entry.attrib['tag']) + mood = ' / '.join(moods) # GET THE FILE AND PATH ##### doIndirect = not self.directpath @@ -1644,16 +1657,18 @@ class Music(Items): kodicursor.execute(query, (path, '123', pathid)) # Update the song entry - query = ' '.join(( - "UPDATE song", - "SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, iTrack = ?,", - "iDuration = ?, iYear = ?, strFilename = ?, iTimesPlayed = ?, lastplayed = ?,", - "rating = ?, comment = ?", - "WHERE idSong = ?" - )) + query = ''' + UPDATE song + SET idAlbum = ?, strArtists = ?, strGenres = ?, strTitle = ?, + iTrack = ?, iDuration = ?, iYear = ?, strFilename = ?, + iTimesPlayed = ?, lastplayed = ?, rating = ?, comment = ?, + mood = ? + WHERE idSong = ? + ''' kodicursor.execute(query, (albumid, artists, genre, title, track, duration, year, filename, playcount, - dateplayed, rating, comment, songid)) + dateplayed, rating, comment, mood, + songid)) # Update the checksum in plex table plex_db.updateReference(itemid, checksum) @@ -1676,7 +1691,9 @@ class Music(Items): if album_name: log.info("Creating virtual music album for song: %s." % itemid) - albumid = self.kodi_db.addAlbum(album_name, API.getProvider('MusicBrainzAlbum')) + albumid = self.kodi_db.addAlbum( + album_name, + API.getProvider('MusicBrainzAlbum')) plex_db.addReference("%salbum%s" % (itemid, albumid), v.PLEX_TYPE_ALBUM, albumid, @@ -1704,54 +1721,51 @@ class Music(Items): except TypeError: # No album found, create a single's album log.info("Failed to add album. Creating singles.") - kodicursor.execute("select coalesce(max(idAlbum),0) from album") + kodicursor.execute( + "select coalesce(max(idAlbum),0) from album") albumid = kodicursor.fetchone()[0] + 1 if v.KODIVERSION >= 16: # Kodi Jarvis - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, strReleaseType) - + query = ''' + INSERT INTO album( + idAlbum, strGenres, iYear, strReleaseType) VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, "single")) + ''' + kodicursor.execute(query, + (albumid, genre, year, "single")) elif v.KODIVERSION == 15: # Kodi Isengard - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded, strReleaseType) - + query = ''' + INSERT INTO album( + idAlbum, strGenres, iYear, dateAdded, + strReleaseType) VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, dateadded, "single")) + ''' + kodicursor.execute(query, (albumid, genre, year, + dateadded, "single")) else: # Kodi Helix - query = ( - ''' - INSERT INTO album(idAlbum, strGenres, iYear, dateAdded) - + query = ''' + INSERT INTO album( + idAlbum, strGenres, iYear, dateAdded) VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (albumid, genre, year, dateadded)) + ''' + kodicursor.execute(query, (albumid, genre, year, + dateadded)) # Create the song entry - query = ( - ''' + query = ''' INSERT INTO song( - idSong, idAlbum, idPath, strArtists, strGenres, strTitle, iTrack, - iDuration, iYear, strFileName, strMusicBrainzTrackID, iTimesPlayed, lastplayed, - rating, iStartOffset, iEndOffset) - - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + idSong, idAlbum, idPath, strArtists, strGenres, strTitle, + iTrack, iDuration, iYear, strFileName, + strMusicBrainzTrackID, iTimesPlayed, lastplayed, + rating, iStartOffset, iEndOffset, mood) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''' - ) kodicursor.execute( query, (songid, albumid, pathid, artists, genre, title, track, duration, year, filename, musicBrainzId, playcount, - dateplayed, rating, 0, 0)) + dateplayed, rating, 0, 0, mood)) # Create the reference in plex table plex_db.addReference(itemid, @@ -1764,14 +1778,11 @@ class Music(Items): view_id=viewid) # Link song to album - query = ( - ''' + query = ''' INSERT OR REPLACE INTO albuminfosong( idAlbumInfoSong, idAlbumInfo, iTrack, strTitle, iDuration) - VALUES (?, ?, ?, ?, ?) - ''' - ) + ''' kodicursor.execute(query, (songid, albumid, track, title, duration)) # Link song to artists @@ -1799,29 +1810,27 @@ class Music(Items): finally: if v.KODIVERSION >= 17: # Kodi Krypton - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, idRole, iOrder, strArtist) + query = ''' + INSERT OR REPLACE INTO song_artist( + idArtist, idSong, idRole, iOrder, strArtist) VALUES (?, ?, ?, ?, ?) - ''' - ) - kodicursor.execute(query,(artistid, songid, 1, index, artist_name)) + ''' + kodicursor.execute(query, (artistid, songid, 1, index, + artist_name)) # May want to look into only doing this once? - query = ( - ''' + query = ''' INSERT OR REPLACE INTO role(idRole, strRole) VALUES (?, ?) - ''' - ) + ''' kodicursor.execute(query, (1, 'Composer')) else: - query = ( - ''' - INSERT OR REPLACE INTO song_artist(idArtist, idSong, iOrder, strArtist) + query = ''' + INSERT OR REPLACE INTO song_artist( + idArtist, idSong, iOrder, strArtist) VALUES (?, ?, ?, ?) - ''' - ) - kodicursor.execute(query, (artistid, songid, index, artist_name)) + ''' + kodicursor.execute(query, (artistid, songid, index, + artist_name)) # Verify if album artist exists album_artists = [] @@ -1843,31 +1852,28 @@ class Music(Items): artist_edb = plex_db.getItem_byId(artist_eid) artistid = artist_edb[0] finally: - query = ( - ''' - INSERT OR REPLACE INTO album_artist(idArtist, idAlbum, strArtist) + query = ''' + INSERT OR REPLACE INTO album_artist( + idArtist, idAlbum, strArtist) VALUES (?, ?, ?) - ''' - ) + ''' kodicursor.execute(query, (artistid, albumid, artist_name)) # Update discography if item.get('Album'): - query = ( - ''' - INSERT OR REPLACE INTO discography(idArtist, strAlbum, strYear) + query = ''' + INSERT OR REPLACE INTO discography( + idArtist, strAlbum, strYear) VALUES (?, ?, ?) - ''' - ) + ''' kodicursor.execute(query, (artistid, item['Album'], 0)) # else: if False: album_artists = " / ".join(album_artists) - query = ' '.join(( - - "SELECT strArtists", - "FROM album", - "WHERE idAlbum = ?" - )) + query = ''' + SELECT strArtists + FROM album + WHERE idAlbum = ? + ''' kodicursor.execute(query, (albumid,)) result = kodicursor.fetchone() if result and result[0] != album_artists: @@ -1886,18 +1892,16 @@ class Music(Items): kodicursor.execute(query, (album_artists, albumid)) # Add genres - self.kodi_db.addMusicGenres(songid, genres, "song") + if genres: + self.kodi_db.addMusicGenres(songid, genres, v.KODI_TYPE_SONG) # Update artwork allart = API.getAllArtwork(parentInfo=True) - if hasEmbeddedCover: - allart["Primary"] = "image://music@" + artwork.single_urlencode( playurl ) - artwork.addArtwork(allart, songid, "song", kodicursor) + artwork.addArtwork(allart, songid, v.KODI_TYPE_SONG, kodicursor) - # if item.get('AlbumId') is None: if item.get('parentKey') is None: # Update album artwork - artwork.addArtwork(allart, albumid, "album", kodicursor) + artwork.addArtwork(allart, albumid, v.KODI_TYPE_ALBUM, kodicursor) def remove(self, itemid): # Remove kodiid, fileid, pathid, plex reference diff --git a/resources/lib/library_sync/__init__.py b/resources/lib/library_sync/__init__.py new file mode 100644 index 00000000..b93054b3 --- /dev/null +++ b/resources/lib/library_sync/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/resources/lib/library_sync/fanart.py b/resources/lib/library_sync/fanart.py new file mode 100644 index 00000000..7f9fc074 --- /dev/null +++ b/resources/lib/library_sync/fanart.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from logging import getLogger +from threading import Thread +from Queue import Empty + +from xbmc import sleep + +from utils import ThreadMethodsAdditionalStop, ThreadMethods, window, \ + ThreadMethodsAdditionalSuspend +import plexdb_functions as plexdb +import itemtypes +import variables as v + +############################################################################### + +log = getLogger("PLEX."+__name__) + +############################################################################### + + +@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') +@ThreadMethodsAdditionalStop('plex_shouldStop') +@ThreadMethods +class Process_Fanart_Thread(Thread): + """ + Threaded download of additional fanart in the background + + Input: + queue Queue.Queue() object that you will need to fill with + dicts of the following form: + { + 'plex_id': the Plex id as a string + 'plex_type': the Plex media type, e.g. 'movie' + 'refresh': True/False if True, will overwrite any 3rd party + fanart. If False, will only get missing + } + """ + def __init__(self, queue): + self.queue = queue + Thread.__init__(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 + """ + log.debug("---===### Starting FanartSync ###===---") + threadStopped = self.threadStopped + threadSuspended = self.threadSuspended + queue = self.queue + while not threadStopped(): + # In the event the server goes offline + while threadSuspended() or window('plex_dbScan'): + # Set in service.py + if threadStopped(): + # Abort was requested while waiting. We should exit + log.info("---===### Stopped FanartSync ###===---") + return + sleep(1000) + # grabs Plex item from queue + try: + item = queue.get(block=False) + except Empty: + sleep(200) + continue + + log.debug('Get additional fanart for Plex id %s' % item['plex_id']) + with getattr(itemtypes, + v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as cls: + result = cls.getfanart(item['plex_id'], + refresh=item['refresh']) + if result is True: + log.debug('Done getting fanart for Plex id %s' + % item['plex_id']) + with plexdb.Get_Plex_DB() as plex_db: + plex_db.set_fanart_synched(item['plex_id']) + queue.task_done() + log.debug("---===### Stopped FanartSync ###===---") diff --git a/resources/lib/library_sync/get_metadata.py b/resources/lib/library_sync/get_metadata.py new file mode 100644 index 00000000..5fd25859 --- /dev/null +++ b/resources/lib/library_sync/get_metadata.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +from logging import getLogger +from threading import Thread +from Queue import Empty + +from xbmc import sleep + +from utils import ThreadMethodsAdditionalStop, ThreadMethods, window +from PlexFunctions import GetPlexMetadata, GetAllPlexChildren +import sync_info + +############################################################################### + +log = getLogger("PLEX."+__name__) + +############################################################################### + + +@ThreadMethodsAdditionalStop('suspend_LibraryThread') +@ThreadMethods +class Threaded_Get_Metadata(Thread): + """ + Threaded download of Plex XML metadata for a certain library item. + Fills the out_queue with the downloaded etree XML objects + + Input: + queue Queue.Queue() object that you'll need to fill up + with Plex itemIds + out_queue Queue() object where this thread will store + the downloaded metadata XMLs as etree objects + """ + def __init__(self, queue, out_queue): + self.queue = queue + self.out_queue = out_queue + Thread.__init__(self) + + def terminate_now(self): + """ + Needed to terminate this thread, because there might be items left in + the queue which could cause other threads to hang + """ + while not self.queue.empty(): + # Still try because remaining item might have been taken + try: + self.queue.get(block=False) + except Empty: + sleep(10) + continue + else: + self.queue.task_done() + if self.threadStopped(): + # Shutdown from outside requested; purge out_queue as well + while not self.out_queue.empty(): + # Still try because remaining item might have been taken + try: + self.out_queue.get(block=False) + except Empty: + sleep(10) + continue + else: + self.out_queue.task_done() + + 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 + """ + log.debug('Starting get metadata thread') + # cache local variables because it's faster + queue = self.queue + out_queue = self.out_queue + threadStopped = self.threadStopped + while threadStopped() is False: + # grabs Plex item from queue + try: + item = queue.get(block=False) + # Empty queue + except Empty: + sleep(20) + continue + # Download Metadata + xml = GetPlexMetadata(item['itemId']) + if xml is None: + # Did not receive a valid XML - skip that item for now + log.error("Could not get metadata for %s. Skipping that item " + "for now" % item['itemId']) + # Increase BOTH counters - since metadata won't be processed + with sync_info.LOCK: + sync_info.GET_METADATA_COUNT += 1 + sync_info.PROCESS_METADATA_COUNT += 1 + queue.task_done() + continue + elif xml == 401: + log.error('HTTP 401 returned by PMS. Too much strain? ' + 'Cancelling sync for now') + window('plex_scancrashed', value='401') + # Kill remaining items in queue (for main thread to cont.) + queue.task_done() + break + + item['XML'] = xml + if item.get('get_children') is True: + children_xml = GetAllPlexChildren(item['itemId']) + try: + children_xml[0].attrib + except (TypeError, IndexError, AttributeError): + log.error('Could not get children for Plex id %s' + % item['itemId']) + else: + item['children'] = [] + for child in children_xml: + child_xml = GetPlexMetadata(child.attrib['ratingKey']) + try: + child_xml[0].attrib + except (TypeError, IndexError, AttributeError): + log.error('Could not get child for Plex id %s' + % child.attrib['ratingKey']) + else: + item['children'].append(child_xml[0]) + + # place item into out queue + out_queue.put(item) + # Keep track of where we are at + with sync_info.LOCK: + sync_info.GET_METADATA_COUNT += 1 + # signals to queue job is done + queue.task_done() + # Empty queue in case PKC was shut down (main thread hangs otherwise) + self.terminate_now() + log.debug('Get metadata thread terminated') diff --git a/resources/lib/library_sync/process_metadata.py b/resources/lib/library_sync/process_metadata.py new file mode 100644 index 00000000..e6765b41 --- /dev/null +++ b/resources/lib/library_sync/process_metadata.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from logging import getLogger +from threading import Thread +from Queue import Empty + +from xbmc import sleep + +from utils import ThreadMethodsAdditionalStop, ThreadMethods +import itemtypes +import sync_info + +############################################################################### + +log = getLogger("PLEX."+__name__) + +############################################################################### + + +@ThreadMethodsAdditionalStop('suspend_LibraryThread') +@ThreadMethods +class Threaded_Process_Metadata(Thread): + """ + Not yet implemented for more than 1 thread - if ever. Only to be called by + ONE thread! + Processes the XML metadata in the queue + + Input: + queue: Queue.Queue() object that you'll need to fill up with + the downloaded XML eTree objects + item_type: as used to call functions in itemtypes.py e.g. 'Movies' => + itemtypes.Movies() + """ + def __init__(self, queue, item_type): + self.queue = queue + self.item_type = item_type + Thread.__init__(self) + + def terminate_now(self): + """ + Needed to terminate this thread, because there might be items left in + the queue which could cause other threads to hang + """ + while not self.queue.empty(): + # Still try because remaining item might have been taken + try: + self.queue.get(block=False) + except Empty: + sleep(10) + continue + else: + self.queue.task_done() + + 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 + """ + log.debug('Processing thread started') + # Constructs the method name, e.g. itemtypes.Movies + item_fct = getattr(itemtypes, self.item_type) + # cache local variables because it's faster + queue = self.queue + threadStopped = self.threadStopped + with item_fct() as item_class: + while threadStopped() is False: + # grabs item from queue + try: + item = queue.get(block=False) + except Empty: + sleep(20) + continue + # Do the work + item_method = getattr(item_class, item['method']) + if item.get('children') is not None: + item_method(item['XML'][0], + viewtag=item['viewName'], + viewid=item['viewId'], + children=item['children']) + else: + item_method(item['XML'][0], + viewtag=item['viewName'], + viewid=item['viewId']) + # Keep track of where we are at + try: + log.debug('found child: %s' + % item['children'].attrib) + except: + pass + with sync_info.LOCK: + sync_info.PROCESS_METADATA_COUNT += 1 + sync_info.PROCESSING_VIEW_NAME = item['title'] + queue.task_done() + self.terminate_now() + log.debug('Processing thread terminated') diff --git a/resources/lib/library_sync/sync_info.py b/resources/lib/library_sync/sync_info.py new file mode 100644 index 00000000..df14e433 --- /dev/null +++ b/resources/lib/library_sync/sync_info.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from logging import getLogger +from threading import Thread, Lock + +from xbmc import sleep + +from utils import ThreadMethodsAdditionalStop, ThreadMethods, language as lang + +############################################################################### + +log = getLogger("PLEX."+__name__) + +GET_METADATA_COUNT = 0 +PROCESS_METADATA_COUNT = 0 +PROCESSING_VIEW_NAME = '' +LOCK = Lock() + +############################################################################### + + +@ThreadMethodsAdditionalStop('suspend_LibraryThread') +@ThreadMethods +class Threaded_Show_Sync_Info(Thread): + """ + Threaded class to show the Kodi statusbar of the metadata download. + + Input: + dialog xbmcgui.DialogProgressBG() object to show progress + total: Total number of items to get + """ + def __init__(self, dialog, total, item_type): + self.total = total + self.dialog = dialog + self.item_type = item_type + Thread.__init__(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 + """ + log.debug('Show sync info thread started') + # cache local variables because it's faster + total = self.total + dialog = self.dialog + threadStopped = self.threadStopped + dialog.create("%s: Sync %s: %s items" + % (lang(29999), self.item_type, str(total)), + "Starting") + + total = 2 * total + totalProgress = 0 + while threadStopped() is False: + with LOCK: + get_progress = GET_METADATA_COUNT + process_progress = PROCESS_METADATA_COUNT + viewName = PROCESSING_VIEW_NAME + totalProgress = get_progress + process_progress + try: + percentage = int(float(totalProgress) / float(total)*100.0) + except ZeroDivisionError: + percentage = 0 + dialog.update(percentage, + message="%s downloaded. %s processed: %s" + % (get_progress, + process_progress, + viewName)) + # Sleep for x milliseconds + sleep(200) + dialog.close() + log.debug('Show sync info thread terminated') diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py index 2db99558..c22074f7 100644 --- a/resources/lib/librarysync.py +++ b/resources/lib/librarysync.py @@ -3,7 +3,7 @@ ############################################################################### import logging -from threading import Thread, Lock +from threading import Thread import Queue from random import shuffle @@ -28,6 +28,10 @@ import variables as v from PlexFunctions import GetPlexMetadata, GetAllPlexLeaves, scrobble, \ GetPlexSectionResults, GetAllPlexChildren, GetPMSStatus import PlexAPI +from library_sync.get_metadata import Threaded_Get_Metadata +from library_sync.process_metadata import Threaded_Process_Metadata +import library_sync.sync_info as sync_info +from library_sync.fanart import Process_Fanart_Thread ############################################################################### @@ -36,282 +40,6 @@ log = logging.getLogger("PLEX."+__name__) ############################################################################### -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods -class ThreadedGetMetadata(Thread): - """ - Threaded download of Plex XML metadata for a certain library item. - Fills the out_queue with the downloaded etree XML objects - - Input: - queue Queue.Queue() object that you'll need to fill up - with Plex itemIds - out_queue Queue() object where this thread will store - the downloaded metadata XMLs as etree objects - lock Lock(), used for counting where we are - """ - def __init__(self, queue, out_queue, lock, processlock): - self.queue = queue - self.out_queue = out_queue - self.lock = lock - self.processlock = processlock - Thread.__init__(self) - - def terminateNow(self): - while not self.queue.empty(): - # Still try because remaining item might have been taken - try: - self.queue.get(block=False) - except Queue.Empty: - xbmc.sleep(10) - continue - else: - self.queue.task_done() - if self.threadStopped(): - # Shutdown from outside requested; purge out_queue as well - while not self.out_queue.empty(): - # Still try because remaining item might have been taken - try: - self.out_queue.get(block=False) - except Queue.Empty: - xbmc.sleep(10) - continue - else: - self.out_queue.task_done() - - def run(self): - # cache local variables because it's faster - queue = self.queue - out_queue = self.out_queue - lock = self.lock - processlock = self.processlock - threadStopped = self.threadStopped - global getMetadataCount - global processMetadataCount - while threadStopped() is False: - # grabs Plex item from queue - try: - updateItem = queue.get(block=False) - # Empty queue - except Queue.Empty: - xbmc.sleep(10) - continue - # Download Metadata - plexXML = GetPlexMetadata(updateItem['itemId']) - if plexXML is None: - # Did not receive a valid XML - skip that item for now - log.warn("Could not get metadata for %s. Skipping that item " - "for now" % updateItem['itemId']) - # Increase BOTH counters - since metadata won't be processed - with lock: - getMetadataCount += 1 - with processlock: - processMetadataCount += 1 - queue.task_done() - continue - elif plexXML == 401: - log.warn('HTTP 401 returned by PMS. Too much strain? ' - 'Cancelling sync for now') - window('plex_scancrashed', value='401') - # Kill remaining items in queue (for main thread to cont.) - queue.task_done() - break - - updateItem['XML'] = plexXML - # place item into out queue - out_queue.put(updateItem) - # Keep track of where we are at - with lock: - getMetadataCount += 1 - # signals to queue job is done - queue.task_done() - # Empty queue in case PKC was shut down (main thread hangs otherwise) - self.terminateNow() - log.debug('Download thread terminated') - - -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods -class ThreadedProcessMetadata(Thread): - """ - Not yet implemented - if ever. Only to be called by ONE thread! - Processes the XML metadata in the queue - - Input: - queue: Queue.Queue() object that you'll need to fill up with - the downloaded XML eTree objects - itemType: as used to call functions in itemtypes.py - e.g. 'Movies' => itemtypes.Movies() - lock: Lock(), used for counting where we are - """ - def __init__(self, queue, itemType, lock): - self.queue = queue - self.lock = lock - self.itemType = itemType - Thread.__init__(self) - - def terminateNow(self): - while not self.queue.empty(): - # Still try because remaining item might have been taken - try: - self.queue.get(block=False) - except Queue.Empty: - xbmc.sleep(10) - continue - else: - self.queue.task_done() - - def run(self): - # Constructs the method name, e.g. itemtypes.Movies - itemFkt = getattr(itemtypes, self.itemType) - # cache local variables because it's faster - queue = self.queue - lock = self.lock - threadStopped = self.threadStopped - global processMetadataCount - global processingViewName - with itemFkt() as item: - while threadStopped() is False: - # grabs item from queue - try: - updateItem = queue.get(block=False) - except Queue.Empty: - xbmc.sleep(10) - continue - # Do the work - plexitem = updateItem['XML'] - method = updateItem['method'] - viewName = updateItem['viewName'] - viewId = updateItem['viewId'] - title = updateItem['title'] - itemSubFkt = getattr(item, method) - # Get the one child entry in the xml and process - for child in plexitem: - itemSubFkt(child, - viewtag=viewName, - viewid=viewId) - # Keep track of where we are at - with lock: - processMetadataCount += 1 - processingViewName = title - # signals to queue job is done - queue.task_done() - # Empty queue in case PKC was shut down (main thread hangs otherwise) - self.terminateNow() - log.debug('Processing thread terminated') - - -@ThreadMethodsAdditionalStop('suspend_LibraryThread') -@ThreadMethods -class ThreadedShowSyncInfo(Thread): - """ - Threaded class to show the Kodi statusbar of the metadata download. - - Input: - dialog xbmcgui.DialogProgressBG() object to show progress - locks = [downloadLock, processLock] Locks() to the other threads - total: Total number of items to get - """ - def __init__(self, dialog, locks, total, itemType): - self.locks = locks - self.total = total - self.dialog = dialog - self.itemType = itemType - Thread.__init__(self) - - def run(self): - # cache local variables because it's faster - total = self.total - dialog = self.dialog - threadStopped = self.threadStopped - downloadLock = self.locks[0] - processLock = self.locks[1] - dialog.create("%s: Sync %s: %s items" - % (lang(29999), self.itemType, str(total)), - "Starting") - global getMetadataCount - global processMetadataCount - global processingViewName - total = 2 * total - totalProgress = 0 - while threadStopped() is False: - with downloadLock: - getMetadataProgress = getMetadataCount - with processLock: - processMetadataProgress = processMetadataCount - viewName = processingViewName - totalProgress = getMetadataProgress + processMetadataProgress - try: - percentage = int(float(totalProgress) / float(total)*100.0) - except ZeroDivisionError: - percentage = 0 - dialog.update(percentage, - message="%s downloaded. %s processed: %s" - % (getMetadataProgress, - processMetadataProgress, - viewName)) - # Sleep for x milliseconds - xbmc.sleep(200) - dialog.close() - log.debug('Dialog Infobox thread terminated') - - -@ThreadMethodsAdditionalSuspend('suspend_LibraryThread') -@ThreadMethodsAdditionalStop('plex_shouldStop') -@ThreadMethods -class ProcessFanartThread(Thread): - """ - Threaded download of additional fanart in the background - - Input: - queue Queue.Queue() object that you will need to fill with - dicts of the following form: - { - 'plex_id': the Plex id as a string - 'plex_type': the Plex media type, e.g. 'movie' - 'refresh': True/False if True, will overwrite any 3rd party - fanart. If False, will only get missing - } - """ - def __init__(self, queue): - self.queue = queue - Thread.__init__(self) - - def run(self): - threadStopped = self.threadStopped - threadSuspended = self.threadSuspended - queue = self.queue - log.info("---===### Starting FanartSync ###===---") - while not threadStopped(): - # In the event the server goes offline - while threadSuspended() or window('plex_dbScan'): - # Set in service.py - if threadStopped(): - # Abort was requested while waiting. We should exit - log.info("---===### Stopped FanartSync ###===---") - return - xbmc.sleep(1000) - # grabs Plex item from queue - try: - item = queue.get(block=False) - except Queue.Empty: - xbmc.sleep(200) - continue - - log.debug('Get additional fanart for Plex id %s' % item['plex_id']) - with getattr(itemtypes, - v.ITEMTYPE_FROM_PLEXTYPE[item['plex_type']])() as cls: - result = cls.getfanart(item['plex_id'], - refresh=item['refresh']) - if result is True: - log.debug('Done getting fanart for Plex id %s' - % item['plex_id']) - with plexdb.Get_Plex_DB() as plex_db: - plex_db.set_fanart_synched(item['plex_id']) - queue.task_done() - log.info("---===### Stopped FanartSync ###===---") - - @ThreadMethodsAdditionalSuspend('suspend_LibraryThread') @ThreadMethodsAdditionalStop('plex_shouldStop') @ThreadMethods @@ -330,7 +58,7 @@ class LibrarySync(Thread): self.sessionKeys = [] self.fanartqueue = Queue.Queue() if settings('FanartTV') == 'true': - self.fanartthread = ProcessFanartThread(self.fanartqueue) + self.fanartthread = Process_Fanart_Thread(self.fanartqueue) # How long should we wait at least to process new/changed PMS items? self.saftyMargin = int(settings('backgroundsync_saftyMargin')) @@ -700,8 +428,8 @@ class LibrarySync(Thread): viewid=folderid, delete=True) # Added new playlist - if (foldername not in playlists and - mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): + if (foldername not in playlists and mediatype in + (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): playlistXSP(mediatype, foldername, folderid, @@ -726,8 +454,8 @@ class LibrarySync(Thread): else: # Validate the playlist exists or recreate it if mediatype != v.PLEX_TYPE_ARTIST: - if (foldername not in playlists and - mediatype in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): + if (foldername not in playlists and mediatype in + (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW)): playlistXSP(mediatype, foldername, folderid, @@ -777,7 +505,8 @@ class LibrarySync(Thread): for view in sections: itemType = view.attrib['type'] - if itemType in (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO): # NOT artist for now + if (itemType in + (v.PLEX_TYPE_MOVIE, v.PLEX_TYPE_SHOW, v.PLEX_TYPE_PHOTO)): self.sorted_views.append(view.attrib['title']) log.debug('Sorted views: %s' % self.sorted_views) @@ -859,7 +588,8 @@ class LibrarySync(Thread): with itemtypes.Music() as music: music.remove(item['plex_id']) - def GetUpdatelist(self, xml, itemType, method, viewName, viewId): + def GetUpdatelist(self, xml, itemType, method, viewName, viewId, + get_children=False): """ THIS METHOD NEEDS TO BE FAST! => e.g. no API calls @@ -872,6 +602,8 @@ class LibrarySync(Thread): see itemtypes.py viewName: Name of the Plex view (e.g. 'My TV shows') viewId: Id/Key of Plex library (e.g. '1') + get_children: will get Plex children of the item if True, + e.g. for music albums Output: self.updatelist, self.allPlexElementsId self.updatelist APPENDED(!!) list itemids (Plex Keys as @@ -906,7 +638,8 @@ class LibrarySync(Thread): 'viewName': viewName, 'viewId': viewId, 'title': item.attrib.get('title', 'Missing Title'), - 'mediaType': item.attrib.get('type') + 'mediaType': item.attrib.get('type'), + 'get_children': get_children }) self.just_processed[itemId] = now return @@ -932,7 +665,8 @@ class LibrarySync(Thread): 'viewName': viewName, 'viewId': viewId, 'title': item.attrib.get('title', 'Missing Title'), - 'mediaType': item.attrib.get('type') + 'mediaType': item.attrib.get('type'), + 'get_children': get_children }) self.just_processed[itemId] = now else: @@ -951,7 +685,8 @@ class LibrarySync(Thread): 'viewName': viewName, 'viewId': viewId, 'title': item.attrib.get('title', 'Missing Title'), - 'mediaType': item.attrib.get('type') + 'mediaType': item.attrib.get('type'), + 'get_children': get_children }) self.just_processed[itemId] = now @@ -976,49 +711,38 @@ class LibrarySync(Thread): log.info("Starting sync threads") getMetadataQueue = Queue.Queue() processMetadataQueue = Queue.Queue(maxsize=100) - getMetadataLock = Lock() - processMetadataLock = Lock() # To keep track - global getMetadataCount - getMetadataCount = 0 - global processMetadataCount - processMetadataCount = 0 - global processingViewName - processingViewName = '' + sync_info.GET_METADATA_COUNT = 0 + sync_info.PROCESS_METADATA_COUNT = 0 + sync_info.PROCESSING_VIEW_NAME = '' # Populate queue: GetMetadata for updateItem in self.updatelist: getMetadataQueue.put(updateItem) # Spawn GetMetadata threads for downloading threads = [] for i in range(min(self.syncThreadNumber, itemNumber)): - thread = ThreadedGetMetadata(getMetadataQueue, - processMetadataQueue, - getMetadataLock, - processMetadataLock) + thread = Threaded_Get_Metadata(getMetadataQueue, + processMetadataQueue) thread.setDaemon(True) thread.start() threads.append(thread) log.info("%s download threads spawned" % len(threads)) # Spawn one more thread to process Metadata, once downloaded - thread = ThreadedProcessMetadata(processMetadataQueue, - itemType, - processMetadataLock) + thread = Threaded_Process_Metadata(processMetadataQueue, + itemType) thread.setDaemon(True) thread.start() threads.append(thread) - log.info("Processing thread spawned") # Start one thread to show sync progress ONLY for new PMS items if self.new_items_only is True and window('dbSyncIndicator') == 'true': dialog = xbmcgui.DialogProgressBG() - thread = ThreadedShowSyncInfo( + thread = sync_info.Threaded_Show_Sync_Info( dialog, - [getMetadataLock, processMetadataLock], itemNumber, itemType) thread.setDaemon(True) thread.start() threads.append(thread) - log.info("Kodi Infobox thread spawned") # Wait until finished getMetadataQueue.join() @@ -1322,6 +1046,8 @@ class LibrarySync(Thread): return True def ProcessMusic(self, views, kind, urlArgs, method): + # For albums, we need to look at the album's songs simultaneously + get_children = True if kind == v.PLEX_TYPE_ALBUM else False # Get a list of items already existing in Kodi db if self.compare: with plexdb.Get_Plex_DB() as plex_db: @@ -1347,7 +1073,8 @@ class LibrarySync(Thread): 'Music', method, view['name'], - view['id']) + view['id'], + get_children=get_children) if self.compare: # Manual sync, process deletes with itemtypes.Music() as Music: From b0fe3cba2465ebd014f5a9c681ced77764e9ee85 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Sun, 2 Apr 2017 20:10:10 +0200 Subject: [PATCH 7/9] Cleanup stream selection --- resources/lib/PlexAPI.py | 58 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index fec9e228..9036b82f 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -30,7 +30,6 @@ http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-pyt (and others...) """ -import os import logging from time import time import urllib2 @@ -40,6 +39,7 @@ import xml.etree.ElementTree as etree from re import compile as re_compile, sub from json import dumps from urllib import urlencode, quote_plus, unquote +from os import path as os_path import xbmcgui from xbmc import sleep, executebuiltin @@ -2187,32 +2187,38 @@ class API(): # Several streams/files available. dialoglist = [] for entry in self.item.findall('./Media'): - fileName = '' - audioLanguage = '' - # Get additional info (filename / languages) - mediaPartEntry = self.item.find('./Media/Part') - if mediaPartEntry is not None: - # Filename - if 'file' in mediaPartEntry.attrib: - fileName = os.path.basename(mediaPartEntry.attrib['file']) - - # Languages - subtitle does not seem to be directly included in this stream info - mediaPartStreamEntry = self.item.find('./Media/Part/Stream') - if mediaPartStreamEntry is not None: - # Audio language - if 'language' in mediaPartStreamEntry.attrib: - audioLanguage = mediaPartStreamEntry.attrib['language'] - - dialoglist.append( - "%sp %s - %s (%s) (%s) %s" % - (entry.attrib.get('videoResolution', 'unknown'), - entry.attrib.get('videoCodec', 'unknown'), - entry.attrib.get('audioProfile', 'unknown'), - entry.attrib.get('audioCodec', 'unknown'), - audioLanguage, - fileName) - ) + filename = None + if 'file' in entry[0].attrib: + filename = os_path.basename(entry[0].attrib['file']) + # Languages of audio streams + languages = [] + for stream in entry[0]: + if (stream.attrib['streamType'] == '1' and + 'language' in stream.attrib): + languages.append(stream.attrib['language']) + languages = ', '.join(languages) + if filename: + option = tryEncode(filename) + if languages: + if option: + option = '%s (%s): ' % (option, tryEncode(languages)) + else: + option = '%s: ' % tryEncode(languages) + if 'videoResolution' in entry.attrib: + option = '%s%sp ' % (option, + entry.attrib.get('videoResolution')) + if 'videoCodec' in entry.attrib: + option = '%s%s' % (option, + entry.attrib.get('videoCodec')) + option = option.strip() + ' - ' + if 'audioProfile' in entry.attrib: + option = '%s%s ' % (option, + entry.attrib.get('audioProfile')) + if 'audioCodec' in entry.attrib: + option = '%s%s ' % (option, + entry.attrib.get('audioCodec')) + dialoglist.append(option) media = xbmcgui.Dialog().select('Select stream', dialoglist) else: media = 0 From f7262e737fa531b754eb7be18a4df7568eb37f4a Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 7 Apr 2017 11:32:53 +0200 Subject: [PATCH 8/9] Update Dutch translation --- resources/language/Dutch/strings.xml | 86 ++++++++++++++-------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/resources/language/Dutch/strings.xml b/resources/language/Dutch/strings.xml index 981dfd47..ea18177a 100644 --- a/resources/language/Dutch/strings.xml +++ b/resources/language/Dutch/strings.xml @@ -4,13 +4,13 @@ PlexKodiConnect Serveradres (IP) Voorkeurs methode voor afspelen - Logniveau + Log niveau Gebruikersnaam: Wachtwoord: Netwerk gebruikersnaam: Netwerk wachtwoord: Transcode: - Schakel Performance Profiling in + Schakel prestatie profilering in Lokaal caching systeem OK Nooit weergeven @@ -26,7 +26,7 @@ Bericht weergeven als PMS offline gaat Poortnummer - Dit is mijn Plex Media Server + Ik beheer deze Plex Media Server Aantal recente films tonen: Aantal recente TV-afleveringen tonen: Aantal recente muziekalbums tonen: @@ -171,7 +171,7 @@ BoxSets Trailers Muziekvideo 's - Foto 's + Foto's Onbekeken films Film Genres Filmstudios @@ -217,7 +217,7 @@ Willekeurige Items Aanbevolen - Extra 's + Extra's Synchroniseer themamuziek Synchroniseer extra Fanart Synchroniseer Film BoxSets @@ -230,19 +230,19 @@ Forceer Codecs Transcoden Inschakelen van Netflix stijl volgende notificatie - - Het aantal seconden voor het einde waneer de notificatie te tonen + - Het aantal seconden voor het einde wanneer de notificatie te tonen Toon Emby Infodialoog bij Afspelen/Selecteer actie Inschakelen van server verbinding bericht bij het opstarten - Recent toegevoegde Thuis-video 's + Recent toegevoegde Thuis-video's Recent toegevoegde foto's - Favorite Home Videos - Favorite Photos - Favorite Albums + Favoriete Thuis-Video's + Favoriete foto's + Favoriete Albums Recent toegevoegde videoclips Actieve videoclips - Onbekeken videoclips + Ongeziene videoclips Actieve @@ -254,9 +254,9 @@ Seizoenen Afleveringen Muziek artiesten - Muziekalbums - Muziekvideo 's - Muziektracks + Muziek albums + Muziek video 's + Muziek tracks Kanalen @@ -301,7 +301,7 @@ Geen confirmatie bij het verwijderen van Plex (gebruik op eigen risico) Terugspringen bij verder kijken (in seconden) Forceer transcoden h265/HEVC - Muzie metagegevens opties (niet compatible met direct stream) + Muziek metagegevens opties (niet compatibel met direct stream) Muziek ratings direct uit bestanden lezen Converteer muziek ratings naar Emby ratings Toestaan van aanpassen rating in bestanden @@ -310,10 +310,10 @@ Opstartvertraging (in seconden) Server herstart bericht inschakelen Schakel kennisgeving in nieuwe inhoud - Duur van de bibliotheek popup (in seconden) - Duur van de bibliotheek popup (in seconden) + Duur van de bibliotheek pop-up (in seconden) + Duur van de bibliotheek pop-up (in seconden) Serverberichten - [COLOR yellow]Genereer nieuw uniek apparaat id (bijvoorbeeld bij het klonen van Kodi)[/COLOR] + [COLOR yellow]Genereer nieuw uniek apparaat ID (bijvoorbeeld bij het klonen van Kodi)[/COLOR] Gebruiker moet bij elke Kodi herstart opnieuw inloggen BIJ AANPASSINGEN KODI HERSTARTEN Volledige re-sync nodig @@ -330,8 +330,8 @@ Verbindingsfout Server is onbereikbaar Server is online - items toegevoegd aan playlist - items toegevoegd aan playlist + items toegevoegd aan afspeellijst + items toegevoegd aan afspeellijst De server wordt opnieuw opgestart Toegang is verleend Wachtwoord voor gebruiker: @@ -346,10 +346,10 @@ Verzamel films van: Verzamel boxsets Verzamel muziek video's van: - Verzamel tv-shows van: + Verzamel tv-series van: Verzamelen: - Detected the database needs to be recreated for this version of Emby for Kodi. Proceed? - Emby for Kodi may not work correctly until the database is reset. + Gedetecteerd dat de database opnieuw moet worden gemaakt voor deze versie van Emby voor Kodi. Wilt u doorgaan? + Emby voor Kodi werkt mogelijk niet correct totdat de database is ge-reset. Database sync geannuleerd. De huidige versie van Kodi is niet ondersteund. voltooid in: Vergelijk films met: @@ -379,25 +379,25 @@ Nog niet ingelogd op de Plex server Log in op plex.tv. Problemen bij het verbinden met de server. Kies een andere server? - Plex muziek library uitzetten? (Het is sterk aangeraden om Plex muziek alleen met directe paden te gebruiken bij een grote library. Kodie kan anders vastlopen) + Plex muziek bibliotheek uitzetten? (Het is sterk aanbevolen om Plex muziek alleen met directe paden te gebruiken bij een grote bibliotheek. Kodi kan anders vastlopen) PKC plugin instellingen aanpassen? Kodi moet hierna herstart worden! [COLOR yellow]Lokale database herstellen (forceer verversen van alle inhoud)[/COLOR] [COLOR red]Gedeeltelijke of volledige reset van database en PKC[/COLOR] [COLOR yellow]Alle afbeeldingen van Kodi nu aan de cache toevoegen[/COLOR] - [COLOR yellow]Sync Emby Theme Media to Kodi[/COLOR] + [Kleur geel] Kodi[/COLOR] Synchroniseer Emby thema muziek naar Kodi lokaal Authenticatie mislukt. Ingelogd bij plex.tv? - Automatisch inloggen op plex.tv bij het opstarten + Automatisch inloggen bij plex.tv bij het opstarten Constante achtergrond sync inschakelen Afspeelmodus LET OP! Als u "Native" modus kiest, zullen sommige Plex mogelijkheden niet beschikbaar zijn. Bijvoorbeeld: Plex trailers en opties voor transcoden. ALLE Plex shares moeten gebruik maken van directe paden (bijvoorbeeld smb://myNAS/mymovie.mkv of \\myNAS/mymovie.mkv)! - Netwerk gegevens - Netwerk gegevens toevoegen zodat Kodi bij de inhoud kan? Opmerking: Kodi zal een bericht tonen bij de eerste sync als deze stap overgeslagen word. + Netwerk inloggegevens + Netwerk inloggegevens toevoegen zodat Kodi bij de inhoud kan? Opmerking: Kodi zal een bericht tonen bij de eerste sync als deze stap overgeslagen word. Kodi kan het bestand niet vinden: - Controleer het pad. Waarschijnlijk verkeerde netwerk gegevens of verkeerde paden. Stop synchroniseren? + Controleer het pad. Waarschijnlijk verkeerde netwerk gegevens of verkeerde paden. Stoppen met synchroniseren? Vervang Plex UNC pad \\myNas\mymovie.mkv automatisch met smb paden, smb://myNas/mymovie.mkv? (aanbevolen) Vervang Plex UNC paden \\myNas met smb://myNas @@ -418,19 +418,19 @@ [COLOR yellow]Kies Plex Server uit lijst[/COLOR] Tijd voordat nieuwe/gewijzigde items gesynct worden [s] Achtergrond Sync - Elke X minuten een volledige sync + Elke X minuten een volledige bibliotheek sync remote Plex Server zoeken Gebruikt door Sync en bij Direct Play Aanpassen van paden On Deck van TV-series laat alle series zien - Onlangs Toegevoegd: Voeg show naam toe aan afleveringen + Onlangs Toegevoegd: Voeg serie naam toe aan afleveringen Onlangs Toegevoegd: Voeg seizoen en aflevering nummer toe SxxExx Extra artwork van FanArtTV in de background downloaden? Sync als screensaver gedeactiveerd is Forceer transcoden van Hi10P Onlangs Toegevoegd: Toon ook al bekeken afleveringen - Onlangs Toegevoegd: Toon ook al bekeken films (Ververs Plex playlist/nodes!) + Onlangs Toegevoegd: Toon ook al bekeken films (Ververs Plex afspeellijst/nodes!) Huidige Plex Media Server: [COLOR yellow]Handmatig Plex Media Server adres invoeren[/COLOR] Huidig adres: @@ -438,7 +438,7 @@ Huidige status van de plex.tv: Is Kodi geïnstalleerd op een low-powered apparaat zoals een Raspberry Pi? Zo ja, dan zullen we de druk op Kodi verlagen om de kans op crashen te verminderen. Uiterlijk aanpassingen - TV shows + TV series Gebruik indien mogelijk altijd standaard Plex ondertitels Bij gebruik van meerdere plex libraries van hetzelfde type, bijvoorbeeld "Kinder Films" en "Films voor volwassenen" bekijk dan de Wiki: https://goo.gl/JFtQV9 Aantal PMS items in widgets laten zien (bijv. "On Deck") @@ -448,18 +448,18 @@ Log-out Plex Home gebruiker Instellingen - Netwerk gegevens + Netwerk inloggegevens Vernieuwen van Plex afspeellijsten/nodes Handmatige sync uitvoeren Sync is niet gelukt, de add-on is niet verbonden met de Plex Server. Plex kan uw account vergrendelen bij teveel foutieve login pogingen. Toch doorgaan? - Resetten van PMS verbindingen, een moment geduld a.u.b + PMS verbindingen aan het resetten, een moment geduld a.u.b PKC reset niet gelukt. Herstart Kodi. [COLOR yellow]Verander plex.tv login (aanmelden of afmelden)[/COLOR] Nog niet verbonden met Plex Server Later bekijken is offline - Hoewel aangemeld bij plex.tv, niet geauthorizeerd bij de PMS + Hoewel aangemeld bij plex.tv, niet geauthoriseerd bij de PMS Voer Plex Media Server adres in. Voorbeelden zijn: Ondersteunt uw Plex Media Server SSL-verbindingen? (https in plaats van http)? @@ -468,7 +468,7 @@ verbonden plex.tv aanpassing succesvol [COLOR yellow]Nu zoeken naar missende artwork op FanartTV[/COLOR] - Alleen missende artwork zoeken of alles verniewen? De scan kan lang duren en zal op de achtergrond plaatsvinden. + Alleen missende artwork zoeken of alles vernieuwen? De scan kan lang duren en zal op de achtergrond plaatsvinden. Alles vernieuwen Alleen ontbrekende @@ -482,7 +482,7 @@ Wachtwoord voor gebruiker van plex.tv Kan de gebruiker niet inloggen Probleem met verbinding naar plex.tv. Probeer het later opnieuw - Ga naar https://plex.tv/pin en voer de code in: + Ga naar https://plex.tv/pin en voer de volgende code in: Kan niet aanmelden bij plex.tv. Probeer het later opnieuw : Selecteer gebruiker Voer pincode voor gebruiker @@ -495,21 +495,21 @@ Sync is gecrashed. Herstart Kodi. Meldt dit probleem op het forum Deze versie vereist een nieuwe database. Dit kan enige tijd duren. Doorgaan? werkt mogelijk niet correct totdat de database opnieuw wordt ingesteld. - Database syncing proces geannuleerd. Huidige Kodi versie wordt niet ondersteund. Controleer uw logbestanden voor meer info. + Database synchroniseer proces geannuleerd. Huidige Kodi versie wordt niet ondersteund. Controleer uw logbestanden voor meer info. Opstarten synchronisatie proces mislukt herhaaldelijk. Start Kodi opnieuw. Sync is voor nu geannuleerd. Plex afspeellijsten/nodes vernieuwd Plex afspeellijsten/nodes vernieuwen mislukt - Volledige bibliotheek sync klaar + Volledige bibliotheek synchronisatie voltooid Sync diende een aantal bestanden over te slaan. Kodi werkt misschien niet langer stabiel. Gelieve uw Kodi log bestanden op het Plex forum te posten. Plex gaf fouten terug door de gevraagde hoeveelheid data. Verminder het aantal sync threads in de instellingen. Sommige items zijn overgeslagen. - FOUT in bibliotheek sync + FOUT in bibliotheek synchronisatie On Deck Verzamelingen - Lokale database van Kodi resetten? Een hersynchronisatie is nodig en kan enige tijd duren. + Lokale database van Kodi resetten? Een hersynchronisatie is nodig en dit kan enige tijd duren. Kon de database synchronisatie niet stoppen. Probeer het later nog eens. Verwijder artwork cache? (aanbevolen!) Reset alle instellingen van de PlexKodiConnect Addon? (dit is gewoonlijk niet aanbevolen en onnodig!) @@ -517,4 +517,4 @@ Amazon Alexa (spraakherkenning) Aktiveer Alexa Per map bladeren - \ No newline at end of file + From ed2785559c2cad6590f8327a301530030967e2d0 Mon Sep 17 00:00:00 2001 From: tomkat83 Date: Fri, 7 Apr 2017 11:33:26 +0200 Subject: [PATCH 9/9] Update German translation --- resources/language/German/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/language/German/strings.xml b/resources/language/German/strings.xml index 185b0db7..708a5272 100644 --- a/resources/language/German/strings.xml +++ b/resources/language/German/strings.xml @@ -236,7 +236,7 @@ Kürzlich hinzugefügte Home Videos Kürzlich hinzugefügte Fotos - Lieblings Heimvideos + Lieblings-Homevideos Lieblingsfotos Lieblings-Alben