def getLibrarySections(plexServer: PlexServer, mediaTypes: List[str]) -> List[dict]: """Get a list of Plex library sections with types matching the provided list of media types :param plexServer: Plex server to pull list of libraries from :type plexServer: :class:`PlexServer` :param mediaTypes: List of media type strings to pull matching libraries of :type mediaTypes: list :return: List of matching library sections, dict with 'key' and 'title' :rtype: list """ if not plexServer: raise ValueError('invalid plexServer') if not mediaTypes: raise ValueError('invalid mediaTypes') # get all library sections librarySections = [] for section in plexServer.library.sections(): plexMediaType = section.type kodiMediaTypes = Api.getKodiMediaTypes(plexMediaType) if not kodiMediaTypes: continue if not any(kodiMediaType['kodi'] in mediaTypes for kodiMediaType in kodiMediaTypes): continue librarySections.append({'key': section.key, 'title': section.title}) return librarySections
def refreshMetadata(item: ListItem, itemId: int, mediaProvider: xbmcmediaimport.MediaProvider): # create a Plex server instance server = Server(mediaProvider) if not server.Authenticate(): contextLog( f"failed to connect to Plex Media Server for {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='refresh') return plexItemClass = Api.getPlexMediaClassFromListItem(item) # get the Plex item with all its details plexItem = Api.getPlexItemDetails(server.PlexServer(), itemId, plexItemClass=plexItemClass) if not plexItem: contextLog( f"failed to determine Plex item for {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='refresh') return # trigger a metadata refresh on the Plex server plexItem.refresh() contextLog( f"triggered metadata refresh for {listItem2str(item, itemId)} on {mediaProvider2str(mediaProvider)}", entry="refresh")
def synchronizeItem( item: ListItem, itemId: int, mediaProvider: xbmcmediaimport.MediaProvider, plexServer: plexapi.server.PlexServer, plexItemClass: video.Video = None, allowDirectPlay: bool = True) -> ListItem: # retrieve all details of the item fullItem = Api.getPlexItemAsListItem(plexServer, itemId, plexItemClass=plexItemClass, allowDirectPlay=allowDirectPlay) if not fullItem: contextLog(f"cannot retrieve details of {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGERROR, entry='sync') return None return fullItem
def run(action): item = sys.listitem # pylint: disable=no-member if not item: contextLog('missing ListItem', xbmc.LOGERROR) return itemId = Api.getItemIdFromListItem(item) if not itemId: contextLog(f'cannot determine the Emby identifier of "{item.getLabel()}"', xbmc.LOGERROR) return mediaProviderId = item.getMediaProviderId() if not mediaProviderId: contextLog(f"cannot determine the media provider identifier of {listItem2str(item, itemId)}", xbmc.LOGERROR) return # get the media provider mediaProvider = xbmcmediaimport.getProviderById(mediaProviderId) if not mediaProvider: contextLog( f"cannot determine the media provider ({mediaProviderId}) of {listItem2str(item, itemId)}", xbmc.LOGERROR) return # prepare the media provider settings if not mediaProvider.prepareSettings(): contextLog( f"cannot prepare media provider ({mediaProvider2str(mediaProvider)}) settings of {listItem2str(item, itemId)}", xbmc.LOGERROR) return if action == ContextAction.Play: play(item, itemId, mediaProvider) elif action == ContextAction.Synchronize: synchronize(item, itemId, mediaProvider) elif action == ContextAction.RefreshMetadata: refreshMetadata(item, itemId, mediaProvider) else: raise ValueError(f"unknown action {action}")
def getLibrarySections(plexServer, mediaTypes): if not plexServer: raise ValueError('invalid plexServer') if not mediaTypes: raise ValueError('invalid mediaTypes') # get all library sections librarySections = [] for section in plexServer.library.sections(): plexMediaType = section.type kodiMediaTypes = Api.getKodiMediaTypes(plexMediaType) if not kodiMediaTypes: continue if not any(kodiMediaType['kodi'] in mediaTypes for kodiMediaType in kodiMediaTypes): continue librarySections.append({ 'key': section.key, 'title': section.title }) return librarySections
def synchronize(item: ListItem, itemId: int, mediaProvider): # find the matching media import mediaImport = getMediaImport(mediaProvider, item) if not mediaImport: contextLog( f"cannot find the media import of {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGERROR, entry='sync') return # determine whether Direct Play is allowed mediaProviderSettings = mediaProvider.getSettings() allowDirectPlay = mediaProviderSettings.getBool(SETTINGS_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY) # create a Plex server instance server = Server(mediaProvider) if not server.Authenticate(): contextLog( f"failed to connect to Plex Media Server for {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='sync') return plexItemClass = Api.getPlexMediaClassFromListItem(item) # synchronize the active item syncedItem = synchronizeItem(item, itemId, mediaProvider, server.PlexServer(), plexItemClass=plexItemClass, allowDirectPlay=allowDirectPlay) if not syncedItem: return syncedItems = [(xbmcmediaimport.MediaImportChangesetTypeChanged, syncedItem)] if xbmcmediaimport.changeImportedItems(mediaImport, syncedItems): contextLog(f"synchronized {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", entry='sync') else: contextLog( f"failed to synchronize {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='sync')
def play(item, itemId, mediaProvider): if item.isFolder(): contextLog(f"cannot play folder item {listItem2str(item, itemId)}", xbmc.LOGERROR, entry='play') return # create a Plex server instance server = Server(mediaProvider) if not server.Authenticate(): contextLog( f"failed to connect to Plex Media Server for {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='sync') return plexItemClass = Api.getPlexMediaClassFromListItem(item) # cannot play folders if plexItemClass in (collection.Collection, video.Show, video.Season): contextLog(f"cannot play folder item {listItem2str(item, itemId)}", xbmc.LOGERROR, entry='play') return # get the Plex item with all its details plexItem = Api.getPlexItemDetails(server.PlexServer(), itemId, plexItemClass=plexItemClass) if not plexItem: contextLog( f"failed to determine Plex item for {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='refresh') return # cannot play folders if not Api.canPlay(plexItem): contextLog(f"cannot play item {listItem2str(item, itemId)}", xbmc.LOGERROR, entry='play') return playChoices = [] playChoicesUrl = [] # determine whether Direct Play is allowed mediaProviderSettings = mediaProvider.getSettings() allowDirectPlay = mediaProviderSettings.getBool(SETTINGS_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY) # check if the item supports Direct Play if allowDirectPlay: directPlayUrl = Api.getDirectPlayUrlFromPlexItem(plexItem) if directPlayUrl: playChoices.append(localize(32103)) playChoicesUrl.append(directPlayUrl) # check if the item supports streaming directStreamUrl = Api.getStreamUrlFromPlexItem(plexItem, server.PlexServer()) if directStreamUrl: playChoices.append(localize(32104)) playChoicesUrl.append(directStreamUrl) # check if the item has multiple versions multipleVersions = [] if len(plexItem.media) > 1: for mediaStream in plexItem.media: url = None if allowDirectPlay: directPlayUrl = Api.getDirectPlayUrlFromMedia(mediaStream) if directPlayUrl: url = directPlayUrl if not url: url = Api.getStreamUrlFromMedia(mediaStream, server.PlexServer()) # get the display title of the first videostream for mediaPart in mediaStream.parts: # get all video streams videoStreams = (stream for stream in mediaPart.streams if isinstance(stream, media.VideoStream)) # extract the first non-empty display resolution displayResolution = next( ( stream.displayTitle or stream.extendedDisplayTitle for stream in videoStreams if stream.displayTitle or stream.extendedDisplayTitle ), None) if displayResolution: break # fall back to the basic video resolution of the stream if not displayResolution: displayResolution = mediaStream.videoResolution multipleVersions.append((url, mediaStream.bitrate, displayResolution)) if len(multipleVersions) > 1: playChoices.append(localize(32105)) playChoicesUrl.append(PLAY_MULTIPLE_VERSIONS_KEY) # if there are no options something went wrong if not playChoices: contextLog( f"cannot play {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGERROR, entry='play') return # ask the user how to play playChoice = Dialog().contextmenu(playChoices) if playChoice < 0 or playChoice >= len(playChoices): return playUrl = playChoicesUrl[playChoice] # check if the user chose to choose which version to play if playUrl == PLAY_MULTIPLE_VERSIONS_KEY: playChoices.clear() playChoicesUrl.clear() # sort the available versions by bitrate (second field) multipleVersions.sort(key=lambda version: version[1], reverse=True) for version in multipleVersions: playChoices.append( localize(32106, bitrate=bitrate2str(version[1]), resolution=version[2])) playChoicesUrl.append(version[0]) # ask the user which version to play playChoice = Dialog().contextmenu(playChoices) if playChoice < 0 or playChoice >= len(playChoices): return playUrl = playChoicesUrl[playChoice] # play the item contextLog( ( f'playing {listItem2str(item, itemId)} using "{playChoices[playChoice]}" ({playUrl}) ' f'from {mediaProvider2str(mediaProvider)}' ), entry='play') # overwrite the dynamic path of the ListItem item.setDynamicPath(playUrl) xbmc.Player().play(playUrl, item)
def updateOnProvider(handle, options): # retrieve the media import mediaImport = xbmcmediaimport.getImport(handle) if not mediaImport: log('cannot retrieve media import', xbmc.LOGERROR) return # retrieve the media provider mediaProvider = mediaImport.getProvider() if not mediaProvider: log('cannot retrieve media provider', xbmc.LOGERROR) return # prepare the media provider settings if not mediaProvider.prepareSettings(): log('cannot prepare media provider settings', xbmc.LOGERROR) return # prepare and get the media import settings importSettings = mediaImport.prepareSettings() if not importSettings: log('cannot prepare media import settings', xbmc.LOGERROR) return item = xbmcmediaimport.getUpdatedItem(handle) if not item: log('cannot retrieve updated item', xbmc.LOGERROR) return itemVideoInfoTag = item.getVideoInfoTag() if not itemVideoInfoTag: log('updated item is not a video item', xbmc.LOGERROR) return # determine the item's identifier / ratingKey itemId = Api.getItemIdFromListItem(item) if not itemId: log('cannot determine the identifier of the updated item: {}'.format(itemVideoInfoTag.getPath()), xbmc.LOGERROR) return # create a Plex server instance server = Server(mediaProvider) if not server.Authenticate(): log('failed to connect to Plex Media Server for {}'.format(mediaProvider2str(mediaProvider)), xbmc.LOGWARNING) return plexItem = Api.getPlexItemDetails(server.PlexServer(), itemId, Api.getPlexMediaClassFromMediaType(itemVideoInfoTag.getMediaType())) if not plexItem: log('cannot retrieve details of updated item {} with id {}'.format(itemVideoInfoTag.getPath(), itemId), xbmc.LOGERROR) return # check / update watched state playcount = itemVideoInfoTag.getPlayCount() watched = playcount > 0 if watched != plexItem.isWatched: if watched: plexItem.markWatched() else: plexItem.markUnwatched() # TODO(Montellese): check / update last played # TODO(Montellese): check / update resume point xbmcmediaimport.finishUpdateOnProvider(handle)
def execImport(handle, options): if not 'path' in options: log('cannot execute "import" without path', xbmc.LOGERROR) return # parse all necessary options mediaTypes = mediaTypesFromOptions(options) if not mediaTypes: log('cannot execute "import" without media types', xbmc.LOGERROR) return # retrieve the media import mediaImport = xbmcmediaimport.getImport(handle) if not mediaImport: log('cannot retrieve media import', xbmc.LOGERROR) return # prepare and get the media import settings importSettings = mediaImport.prepareSettings() if not importSettings: log('cannot prepare media import settings', xbmc.LOGERROR) return # retrieve the media provider mediaProvider = mediaImport.getProvider() if not mediaProvider: log('cannot retrieve media provider', xbmc.LOGERROR) return # prepare the media provider settings if not mediaProvider.prepareSettings(): log('cannot prepare media provider settings', xbmc.LOGERROR) return # create a Plex Media Server instance server = Server(mediaProvider) plexServer = server.PlexServer() plexLibrary = plexServer.library # get all (matching) library sections selectedLibrarySections = getLibrarySectionsFromSettings(importSettings) librarySections = getMatchingLibrarySections(plexServer, mediaTypes, selectedLibrarySections) if not librarySections: log('cannot retrieve {} items without any library section'.format(mediaTypes), xbmc.LOGERROR) return # loop over all media types to be imported progressTotal = len(mediaTypes) for progress, mediaType in enumerate(mediaTypes): if xbmcmediaimport.shouldCancel(handle, progress, progressTotal): return mappedMediaType = Api.getPlexMediaType(mediaType) if not mappedMediaType: log('cannot import unsupported media type "{}"'.format(mediaType), xbmc.LOGERROR) continue plexLibType = mappedMediaType['libtype'] localizedMediaType = localise(mappedMediaType['label']) xbmcmediaimport.setProgressStatus(handle, localise(32001).format(localizedMediaType)) log('importing {} items from {}'.format(mediaType, mediaProvider2str(mediaProvider))) # handle library sections plexItems = [] sectionsProgressTotal = len(librarySections) for sectionsProgress, librarySection in enumerate(librarySections): if xbmcmediaimport.shouldCancel(handle, sectionsProgress, sectionsProgressTotal): return # get the library section from the Plex Media Server section = plexLibrary.sectionByID(librarySection['key']) if not section: log('cannot import {} items from unknown library section {}'.format(mediaType, librarySection), xbmc.LOGWARNING) continue # get all matching items from the library section try: plexSectionItems = section.search(libtype=plexLibType) plexItems.extend(plexSectionItems) except plexapi.exceptions.BadRequest as err: log('failed to retrieve {} items from {}: {}'.format(mediaType, mediaProvider2str(mediaProvider), err)) return # parse all items items = [] itemsProgressTotal = len(plexItems) for itemsProgress, plexItem in enumerate(plexItems): if xbmcmediaimport.shouldCancel(handle, itemsProgress, itemsProgressTotal): return item = Api.toFileItem(plexServer, plexItem, mediaType, plexLibType) if not item: continue items.append(item) if items: log('{} {} items imported from {}'.format(len(items), mediaType, mediaProvider2str(mediaProvider))) xbmcmediaimport.addImportItems(handle, items, mediaType) xbmcmediaimport.finishImport(handle)
def _startPlayback(self): '''Identifies the item (if from Plex) and initializes the player state''' if not self._file: return if not self.isPlayingVideo(): return playingItem = self.getPlayingItem() if not playingItem: return # check if the item has been imported from a media provider mediaProviderId = playingItem.getMediaProviderId() if not mediaProviderId: return if not mediaProviderId in self._providers: log('currently playing item {} ({}) has been imported from an unknown media provider {}' \ .format(playingItem.getLabel(), self._file, mediaProviderId), xbmc.LOGWARNING) return self._mediaProvider = self._providers[mediaProviderId] videoInfoTag = self.getVideoInfoTag() if not videoInfoTag: return itemId = videoInfoTag.getUniqueID(PLEX_PROTOCOL) if not itemId: return if not itemId.isdigit(): log('invalid item id plex://{} (non digit). Kodi will not report playback state to Plex Media Server' \ .format(itemId), xbmc.LOGERROR) return self._itemId = int(itemId) if self._mediaProvider: # save item plexServer = Server(self._mediaProvider) self._item = Api.getPlexItemDetails( plexServer.PlexServer(), self._itemId, Api.getPlexMediaClassFromMediaType( videoInfoTag.getMediaType())) self._duration = toMilliseconds(self.getTotalTime()) # register settings settings = self._mediaProvider.prepareSettings() if not settings: log('failed to load settings for {} ({}) playing from {}' \ .format(self._item.title, self._file, mediaProvider2str(self._mediaProvider)), xbmc.LOGWARNING) self._reset() return # load external subtitles if settings.getBool( SETTINGS_PROVIDER_PLAYBACK_ENABLE_EXTERNAL_SUBTITLES): self._addExternalSubtitles(plexServer.PlexServer()) else: self._reset()
def execImport(handle: int, options: dict): """Perform library update/import of all configured items from a configured PMS into Kodi :param handle: Handle id from input :type handle: int :param options: Options/parameters passed in with the call, required mediatypes or mediatypes[] :type options: dict """ if 'path' not in options: log("cannot execute 'import' without path", xbmc.LOGERROR) return # parse all necessary options mediaTypes = mediaTypesFromOptions(options) if not mediaTypes: log("cannot execute 'import' without media types", xbmc.LOGERROR) return # retrieve the media import mediaImport = xbmcmediaimport.getImport(handle) if not mediaImport: log("cannot retrieve media import", xbmc.LOGERROR) return # prepare and get the media import settings importSettings = mediaImport.prepareSettings() if not importSettings: log("cannot prepare media import settings", xbmc.LOGERROR) return # retrieve the media provider mediaProvider = mediaImport.getProvider() if not mediaProvider: log("cannot retrieve media provider", xbmc.LOGERROR) return # prepare and get the media provider settings providerSettings = mediaProvider.prepareSettings() if not providerSettings: log("cannot prepare provider settings", xbmc.LOGERROR) return # create a Plex Media Server instance server = Server(mediaProvider) plexServer = server.PlexServer() plexLibrary = plexServer.library # get all (matching) library sections selectedLibrarySections = getLibrarySectionsFromSettings(importSettings) librarySections = getMatchingLibrarySections(plexServer, mediaTypes, selectedLibrarySections) if not librarySections: log(f"cannot retrieve {mediaTypes} items without any library section", xbmc.LOGERROR) return # Decide if doing fast sync or not, if so set filter string to include updatedAt fastSync = True lastSync = mediaImport.getLastSynced() # Check if import settings have changed, or if this is the first time we are importing this library type if not lastSync: fastSync = False SynchronizationSettings.CalculateHash( importSettings=importSettings, providerSettings=providerSettings, save=True) log("first time syncronizing library, forcing a full syncronization", xbmc.LOGINFO) elif SynchronizationSettings.HaveChanged(importSettings=importSettings, providerSettings=providerSettings, save=True): fastSync = False log( "library import settings have changed, forcing a full syncronization", xbmc.LOGINFO) if SynchronizationSettings.HaveChanged(importSettings=importSettings, providerSettings=providerSettings, save=True): fastSync = False log( "library import settings have changed, forcing a full syncronization", xbmc.LOGINFO) if fastSync: log(f"performing fast syncronization of items viewed or updated since {str(lastSync)}" ) lastSyncEpoch = parser.parse(lastSync).strftime('%s') updatedFilter = {'updatedAt>': lastSyncEpoch} watchedFilter = {'lastViewedAt>': lastSyncEpoch} # loop over all media types to be imported progressTotal = len(mediaTypes) for progress, mediaType in enumerate(mediaTypes): if xbmcmediaimport.shouldCancel(handle, progress, progressTotal): return mappedMediaType = Api.getPlexMediaType(mediaType) if not mappedMediaType: log(f"cannot import unsupported media type '{mediaType}'", xbmc.LOGERROR) continue plexLibType = mappedMediaType['libtype'] localizedMediaType = localize(mappedMediaType['label']).decode() xbmcmediaimport.setProgressStatus(handle, localize(32001, localizedMediaType)) log( f"importing {mediaType} items from {mediaProvider2str(mediaProvider)}", xbmc.LOGINFO) # handle library sections itemsToImport = [] sectionsProgressTotal = len(librarySections) for sectionsProgress, librarySection in enumerate(librarySections): if xbmcmediaimport.shouldCancel(handle, sectionsProgress, sectionsProgressTotal): return # get the library section from the Plex Media Server section = plexLibrary.sectionByID(librarySection['key']) if not section: log( f"cannot import {mediaType} items from unknown library section {librarySection}", xbmc.LOGWARNING) continue # get all matching items from the library section and turn them into ListItems sectionProgress = 0 sectionProgressTotal = ITEM_REQUEST_LIMIT while sectionProgress < sectionProgressTotal: if xbmcmediaimport.shouldCancel(handle, sectionProgress, sectionProgressTotal): return maxResults = min(ITEM_REQUEST_LIMIT, sectionProgressTotal - sectionProgress) try: if fastSync: updatedPlexItems = section.search( libtype=plexLibType, container_start=sectionProgress, container_size=maxResults, maxresults=maxResults, **updatedFilter) log(f"discovered {len(updatedPlexItems)} updated items from {mediaProvider2str(mediaProvider)}" ) watchedPlexItems = section.search( libtype=plexLibType, container_start=sectionProgress, container_size=maxResults, maxresults=maxResults, **watchedFilter) log(f"discovered {len(watchedPlexItems)} new watched items from {mediaProvider2str(mediaProvider)}" ) plexItems = updatedPlexItems plexItems.extend([ item for item in watchedPlexItems if item.key not in [item.key for item in plexItems] ]) else: plexItems = section.search( libtype=plexLibType, container_start=sectionProgress, container_size=maxResults, maxresults=maxResults, ) except plexapi.exceptions.BadRequest as e: log( f"failed to fetch {mediaType} items from {mediaProvider2str(mediaProvider)}: {e}", xbmc.LOGINFO) return # Update sectionProgressTotal now that search has run and totalSize has been updated sectionProgressTotal = section.totalSize plexItemsProgressTotal = len(plexItems) for plexItemsProgress, plexItem in enumerate(plexItems): if xbmcmediaimport.shouldCancel(handle, plexItemsProgress, plexItemsProgressTotal): return sectionProgress += 1 try: item = Api.toFileItem(plexServer, plexItem, mediaType, plexLibType) if not item: continue itemsToImport.append(item) except plexapi.exceptions.BadRequest as e: # Api.convertDateTimeToDbDateTime may return (404) not_found for orphaned items in the library log(( f"failed to retrieve item {plexItem.title} with key {plexItem.key} " f"from {mediaProvider2str(mediaProvider)}: {e}"), xbmc.LOGWARNING) continue if itemsToImport: log( f"{len(itemsToImport)} {mediaType} items imported from {mediaProvider2str(mediaProvider)}", xbmc.LOGINFO) xbmcmediaimport.addImportItems(handle, itemsToImport, mediaType) xbmcmediaimport.finishImport(handle, fastSync)
def updateOnProvider(handle: int, _options: dict): """Perform update/export of library items from Kodi into conifigured PMS (watch status, resume points, etc.) :param handle: Handle id from input :type handle: int :param _options: Options/parameters passed in with the call, Unused :type _options: dict """ # retrieve the media import mediaImport = xbmcmediaimport.getImport(handle) if not mediaImport: log("cannot retrieve media import", xbmc.LOGERROR) return # retrieve the media provider mediaProvider = mediaImport.getProvider() if not mediaProvider: log("cannot retrieve media provider", xbmc.LOGERROR) return # prepare the media provider settings if not mediaProvider.prepareSettings(): log("cannot prepare media provider settings", xbmc.LOGERROR) return # prepare and get the media import settings importSettings = mediaImport.prepareSettings() if not importSettings: log("cannot prepare media import settings", xbmc.LOGERROR) return item = xbmcmediaimport.getUpdatedItem(handle) if not item: log("cannot retrieve updated item", xbmc.LOGERROR) return itemVideoInfoTag = item.getVideoInfoTag() if not itemVideoInfoTag: log("updated item is not a video item", xbmc.LOGERROR) return # determine the item's identifier / ratingKey itemId = Api.getItemIdFromListItem(item) if not itemId: log( f"cannot determine the identifier of the updated item: {itemVideoInfoTag.getPath()}", xbmc.LOGERROR) return # create a Plex server instance server = Server(mediaProvider) if not server.Authenticate(): log( f"failed to connect to Plex Media Server for {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING) return plexItem = Api.getPlexItemDetails( server.PlexServer(), itemId, Api.getPlexMediaClassFromMediaType(itemVideoInfoTag.getMediaType())) if not plexItem: log( f"cannot retrieve details of updated item {itemVideoInfoTag.getPath()} with id {itemId}", xbmc.LOGERROR) return # check / update watched state playcount = itemVideoInfoTag.getPlayCount() watched = playcount > 0 if watched != plexItem.isWatched: if watched: plexItem.markWatched() else: plexItem.markUnwatched() # TODO(Montellese): check / update last played # TODO(Montellese): check / update resume point xbmcmediaimport.finishUpdateOnProvider(handle)
def run(self): numRetriesOnTimeout = ImportSettings.GetNumberOfRetriesOnTimeout( self._media_import) numSecondsBetweenRetries = ImportSettings.GetNumberOfSecondsBetweenRetries( self._media_import) while not self.should_stop(): while not self.should_stop(): plex_item = None try: plex_item = self._items_to_process_queue.get_nowait() except Empty: # if the queue is empty and we should finish, return completely if self.should_finish(): return break converted_item = None retries = numRetriesOnTimeout while retries > 0: try: # manually reload the item's metadata if isinstance(plex_item, PlexPartialObject ) and not plex_item.isFullObject(): plex_item.reload( **ToFileItemConverterThread.INCLUDES) # convert the plex item to a ListItem converted_item = Api.toFileItem( self._plex_server, plex_item, mediaType=self._media_type, plexLibType=self._plex_lib_type, allowDirectPlay=self._allow_direct_play) # get out of the retry loop break except Exception as e: # Api.convertDateTimeToDbDateTime may return (404) not_found for orphaned items in the library log(( f"failed to retrieve item {plex_item.title} with key {plex_item.key} " f"from {mediaProvider2str(self._media_provider)}: {e}" ), LOGWARNING) # retry after timeout retries -= 1 # check if there are any more retries left # if not skip the item if retries == 0: log(( f"retrieving item {plex_item.title} with key {plex_item.key} from " f"{mediaProvider2str(self._media_provider)} failed after " f"{numRetriesOnTimeout} retries"), LOGWARNING) else: # otherwise wait before trying again log(( f"retrying to retrieve {plex_item.title} with key {plex_item.key} from " f"{mediaProvider2str(self._media_provider)} in " f"{numSecondsBetweenRetries} seconds")) sleep(float(numSecondsBetweenRetries)) # let the input queue know that the plex item has been processed self._items_to_process_queue.task_done() if converted_item: # put the converted item into the output queue self._processed_items_queue.put(converted_item) else: log(( f"failed to convert item {plex_item.title} with key {plex_item.key} " f"from {mediaProvider2str(self._media_provider)}"), LOGWARNING) self._count_items_to_process -= 1 self._count_processed_items += 1 # wait for the stop event self._stop_event.wait(0.1)
def _startPlayback(self): """Identifies the item (if from Plex) and initializes the player state""" if not self._file: return if not self.isPlayingVideo(): return playingItem = self.getPlayingItem() if not playingItem: return # check if the item has been imported from a media provider mediaProviderId = playingItem.getMediaProviderId() if not mediaProviderId: return if mediaProviderId not in self._providers: log( ( f"currently playing item {playingItem.getLabel()} ({self._file}) " f"has been imported from an unknown media provider {mediaProviderId}" ), xbmc.LOGWARNING ) return self._mediaProvider = self._providers[mediaProviderId] if not self._mediaProvider: return videoInfoTag = self.getVideoInfoTag() if not videoInfoTag: return itemId = videoInfoTag.getUniqueID(PLEX_PROTOCOL) if not itemId: return if not itemId.isdigit(): log( f"Item id is not a digit: plex://{itemId}. Kodi will not report playback state to Plex Media Server", xbmc.LOGERROR ) return self._itemId = int(itemId) # save item plexServer = Server(self._mediaProvider) self._item = Api.getPlexItemDetails( plexServer.PlexServer(), self._itemId, Api.getPlexMediaClassFromMediaType(videoInfoTag.getMediaType()) ) if not self._item: log( ( f"failed to retrieve details for item {self._itemId} ({self._file}) " f"playing from {mediaProvider2str(self._mediaProvider)}" ), xbmc.LOGWARNING ) self._reset() return self._duration = toMilliseconds(self.getTotalTime()) # handle any provider specific settings self._handleProviderSettings(plexServer.PlexServer()) # get the matching media import self._mediaImport = self._mediaProvider.getImportByMediaType(videoInfoTag.getMediaType()) if self._mediaImport: # handle any import specific settings self._handleImportSettings() else: log( ( f"failed to determine import for {self._item.title} ({self._file}) " f"playing from {mediaProvider2str(self._mediaProvider)}" ), xbmc.LOGWARNING )
def execImport(handle: int, options: dict): """Perform library update/import of all configured items from a configured PMS into Kodi :param handle: Handle id from input :type handle: int :param options: Options/parameters passed in with the call, required mediatypes or mediatypes[] :type options: dict """ # parse all necessary options mediaTypes = mediaTypesFromOptions(options) if not mediaTypes: log("cannot execute 'import' without media types", xbmc.LOGERROR) return # retrieve the media import mediaImport = xbmcmediaimport.getImport(handle) if not mediaImport: log("cannot retrieve media import", xbmc.LOGERROR) return # prepare and get the media import settings importSettings = mediaImport.prepareSettings() if not importSettings: log("cannot prepare media import settings", xbmc.LOGERROR) return # retrieve the media provider mediaProvider = mediaImport.getProvider() if not mediaProvider: log("cannot retrieve media provider", xbmc.LOGERROR) return # prepare and get the media provider settings providerSettings = mediaProvider.prepareSettings() if not providerSettings: log("cannot prepare provider settings", xbmc.LOGERROR) return # get direct play settings from provider allowDirectPlay = providerSettings.getBool(SETTINGS_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY) # create a Plex Media Server instance server = Server(mediaProvider) plexServer = server.PlexServer() plexLibrary = plexServer.library # get all (matching) library sections selectedLibrarySections = getLibrarySectionsFromSettings(importSettings) librarySections = getMatchingLibrarySections(plexServer, mediaTypes, selectedLibrarySections) if not librarySections: log(f"cannot retrieve {mediaTypes} items without any library section", xbmc.LOGERROR) return numDownloadThreads = ImportSettings.GetNumberOfDownloadThreads(importSettings) numRetriesOnTimeout = ImportSettings.GetNumberOfRetriesOnTimeout(importSettings) numSecondsBetweenRetries = ImportSettings.GetNumberOfSecondsBetweenRetries(importSettings) # Decide if doing fast sync or not, if so set filter string to include updatedAt fastSync = True lastSync = mediaImport.getLastSynced() # Check if import settings have changed, or if this is the first time we are importing this library type if not lastSync: fastSync = False SynchronizationSettings.CalculateHash( importSettings=importSettings, providerSettings=providerSettings, save=True ) log("first time syncronizing library, forcing a full syncronization", xbmc.LOGINFO) elif SynchronizationSettings.HaveChanged( importSettings=importSettings, providerSettings=providerSettings, save=True ): fastSync = False log("library import settings have changed, forcing a full syncronization", xbmc.LOGINFO) if SynchronizationSettings.HaveChanged(importSettings=importSettings, providerSettings=providerSettings, save=True): fastSync = False log("library import settings have changed, forcing a full syncronization", xbmc.LOGINFO) if fastSync: log(f"performing fast syncronization of items viewed or updated since {str(lastSync)}") # loop over all media types to be imported progressTotal = len(mediaTypes) for progress, mediaType in enumerate(mediaTypes): if xbmcmediaimport.shouldCancel(handle, progress, progressTotal): return mappedMediaType = Api.getPlexMediaType(mediaType) if not mappedMediaType: log(f"cannot import unsupported media type '{mediaType}'", xbmc.LOGERROR) continue plexLibType = mappedMediaType['libtype'] localizedMediaType = localize(mappedMediaType['label']).decode() # prepare and start the converter threads converterThreads = [] for _ in range(0, numDownloadThreads): converterThreads.append(ToFileItemConverterThread(mediaProvider, mediaImport, plexServer, media_type=mediaType, plex_lib_type=plexLibType, allow_direct_play=allowDirectPlay)) log(( f"starting {len(converterThreads)} threads to import {mediaType} items " f"from {mediaProvider2str(mediaProvider)}..."), xbmc.LOGDEBUG) for converterThread in converterThreads: converterThread.start() # prepare function to stop all converter threads def stopConverterThreads(converterThreads): log(( f"stopping {len(converterThreads)} threads importing {mediaType} items " f"from {mediaProvider2str(mediaProvider)}..."), xbmc.LOGDEBUG) for converterThread in converterThreads: converterThread.stop() xbmcmediaimport.setProgressStatus(handle, localize(32001, localizedMediaType)) log(f"importing {mediaType} items from {mediaProvider2str(mediaProvider)}", xbmc.LOGINFO) # handle library sections sectionsProgressTotal = len(librarySections) for sectionsProgress, librarySection in enumerate(librarySections): if xbmcmediaimport.shouldCancel(handle, sectionsProgress, sectionsProgressTotal): stopConverterThreads(converterThreads) return # get the library section from the Plex Media Server section = plexLibrary.sectionByID(librarySection['key']) if not section: log(f"cannot import {mediaType} items from unknown library section {librarySection}", xbmc.LOGWARNING) continue # get all matching items from the library section and turn them into ListItems sectionRetrievalProgress = 0 sectionConversionProgress = 0 sectionProgressTotal = ITEM_REQUEST_LIMIT while sectionRetrievalProgress < sectionProgressTotal: if xbmcmediaimport.shouldCancel(handle, sectionConversionProgress, sectionProgressTotal): stopConverterThreads(converterThreads) return maxResults = min(ITEM_REQUEST_LIMIT, sectionProgressTotal - sectionRetrievalProgress) retries = numRetriesOnTimeout while retries > 0: try: if fastSync: lastSyncDatetime = parser.parse(lastSync).astimezone(timezone.utc) prefix = '' if mediaType in (xbmcmediaimport.MediaTypeTvShow, xbmcmediaimport.MediaTypeEpisode): prefix = Api.getPlexMediaType(mediaType)['libtype'] + '.' elif mediaType == xbmcmediaimport.MediaTypeSeason: prefix = Api.getPlexMediaType(xbmcmediaimport.MediaTypeEpisode)['libtype'] + '.' updatedFilter = {prefix + 'updatedAt>>': lastSyncDatetime} watchedFilter = {prefix + 'lastViewedAt>>': lastSyncDatetime} updatedPlexItems = section.search( libtype=plexLibType, container_start=sectionRetrievalProgress, container_size=maxResults, maxresults=maxResults, filters=updatedFilter ) log(f"discovered {len(updatedPlexItems)} updated {mediaType} items from {mediaProvider2str(mediaProvider)}") watchedPlexItems = section.search( libtype=plexLibType, container_start=sectionRetrievalProgress, container_size=maxResults, maxresults=maxResults, filters=watchedFilter ) log(f"discovered {len(watchedPlexItems)} newly watched {mediaType} items from {mediaProvider2str(mediaProvider)}") plexItems = updatedPlexItems plexItems.extend( [item for item in watchedPlexItems if item.key not in [item.key for item in plexItems]] ) else: plexItems = section.search( libtype=plexLibType, container_start=sectionRetrievalProgress, container_size=maxResults, maxresults=maxResults, ) # get out of the retry loop break except Exception as e: log(f"failed to fetch {mediaType} items from {mediaProvider2str(mediaProvider)}: {e}", xbmc.LOGWARNING) # retry after timeout retries -= 1 # check if there are any more retries left # if not abort the import process if retries == 0: log( ( f"fetching {mediaType} items from {mediaProvider2str(mediaProvider)} failed " f"after {numRetriesOnTimeout} retries" ), xbmc.LOGWARNING) stopConverterThreads(converterThreads) return else: # otherwise wait before trying again log( ( f"retrying to fetch {mediaType} items from {mediaProvider2str(mediaProvider)} in " f"{numSecondsBetweenRetries} seconds" ) ) time.sleep(float(numSecondsBetweenRetries)) # Update sectionProgressTotal now that search has run and totalSize has been updated # TODO(Montellese): fix access of private LibrarySection._totalViewSize sectionProgressTotal = section._totalViewSize # nothing to do if no items have been retrieved from Plex if not plexItems: continue # automatically determine how to distribute the retrieved items across the available converter threads plexItemsPerConverter, remainingPlexItems = divmod(len(plexItems), len(converterThreads)) plexItemsPerConverters = [plexItemsPerConverter] * len(converterThreads) plexItemsPerConverters[0:remainingPlexItems] = [plexItemsPerConverter + 1] * remainingPlexItems converterThreadIndex = 0 splitItems = [] for plexItem in plexItems: if xbmcmediaimport.shouldCancel(handle, sectionConversionProgress, sectionProgressTotal): stopConverterThreads(converterThreads) return sectionRetrievalProgress += 1 # copllect the plex items for the next converter thread splitItems.append(plexItem) plexItemsPerConverters[converterThreadIndex] -= 1 # move to the next converter thread if necessary if not plexItemsPerConverters[converterThreadIndex]: converterThreads[converterThreadIndex].add_items_to_convert(splitItems) splitItems.clear() converterThreadIndex += 1 # retrieve and combine the progress of all converter threads sectionConversionProgress = \ sum(converterThread.get_converted_items_count() for converterThread in converterThreads) if splitItems: log(f"forgot to process {len(splitItems)} {mediaType} items", xbmc.LOGWARNING) # retrieve converted items from the converter threads totalItemsToImport = 0 countFinishedConverterThreads = 0 while countFinishedConverterThreads < len(converterThreads): if xbmcmediaimport.shouldCancel(handle, sectionConversionProgress, sectionProgressTotal): stopConverterThreads(converterThreads) return sectionConversionProgress = 0 itemsToImport = [] for converterThread in converterThreads: # update the progress sectionConversionProgress += converterThread.get_converted_items_count() # ignore finished converter threads if converterThread.should_finish(): continue # retrieve the converted items convertedItems = converterThread.get_converted_items() itemsToImport.extend(convertedItems) # check if all items have been converted if not converterThread.get_items_to_convert_count(): converterThread.finish() countFinishedConverterThreads += 1 if itemsToImport: totalItemsToImport += len(itemsToImport) xbmcmediaimport.addImportItems(handle, itemsToImport, mediaType) time.sleep(0.1) if totalItemsToImport: log(f"{totalItemsToImport} {mediaType} items imported from {mediaProvider2str(mediaProvider)}", xbmc.LOGINFO) xbmcmediaimport.finishImport(handle, fastSync)