예제 #1
0
def settingOptionsFillerLibrarySections(handle, options):
    # retrieve the media provider
    mediaProvider = xbmcmediaimport.getProvider(handle)
    if not mediaProvider:
        log('cannot retrieve media provider', xbmc.LOGERROR)
        return

    # retrieve the media import
    mediaImport = xbmcmediaimport.getImport(handle)
    if not mediaImport:
        log('cannot retrieve media import', xbmc.LOGERROR)
        return

    # prepare the media provider settings
    if not mediaProvider.prepareSettings():
        log('cannot prepare media provider settings', xbmc.LOGERROR)
        return

    server = Server(mediaProvider)
    if not server.Authenticate():
        log('failed to connect to Plex Media Server for {}'.format(mediaProvider2str(mediaProvider)), xbmc.LOGWARNING)
        return

    plexServer = server.PlexServer()

    # get all library sections
    mediaTypes = mediaImport.getMediaTypes()
    librarySections = getLibrarySections(plexServer, mediaTypes)
    sections = [ (section['title'], section['key']) for section in librarySections ]

    # get the import's settings
    settings = mediaImport.getSettings()

    # pass the list of views back to Kodi
    settings.setStringOptions(plex.constants.SETTINGS_IMPORT_LIBRARY_SECTIONS, sections)
예제 #2
0
def isImportReady(handle: int, _options: dict):
    """Validate that MediaImport at handle ID and associated provider are ready

    :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)
        xbmcmediaimport.setImportReady(handle, False)
        return

    # prepare and get the media import settings
    importSettings = mediaImport.prepareSettings()
    if not importSettings:
        log("cannot prepare media import settings", xbmc.LOGERROR)
        xbmcmediaimport.setImportReady(handle, False)
        return

    # retrieve the media provider
    mediaProvider = xbmcmediaimport.getProvider(handle)
    if not mediaProvider:
        log("cannot retrieve media provider", xbmc.LOGERROR)
        xbmcmediaimport.setImportReady(handle, False)
        return

    # prepare the media provider settings
    if not mediaProvider.prepareSettings():
        log("cannot prepare media provider settings", xbmc.LOGERROR)
        xbmcmediaimport.setImportReady(handle, False)
        return

    try:
        server = Server(mediaProvider)
    except:
        pass

    importReady = False
    # check if authentication works with the current provider settings
    if server.Authenticate():
        # check if the chosen library sections exist
        selectedLibrarySections = getLibrarySectionsFromSettings(importSettings)
        matchingLibrarySections = getMatchingLibrarySections(
            server.PlexServer(),
            mediaImport.getMediaTypes(),
            selectedLibrarySections
        )
        importReady = len(matchingLibrarySections) > 0

    xbmcmediaimport.setImportReady(handle, importReady)
예제 #3
0
def settingOptionsFillerLibrarySections(handle: int, _options: dict):
    """Find and set the library sections setting from Plex matching a mediaImport's media type

    :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 provider
    mediaProvider = xbmcmediaimport.getProvider(handle)
    if not mediaProvider:
        log('cannot retrieve media provider', xbmc.LOGERROR)
        return

    # retrieve the media import
    mediaImport = xbmcmediaimport.getImport(handle)
    if not mediaImport:
        log('cannot retrieve media import', xbmc.LOGERROR)
        return

    # prepare the media provider settings
    if not mediaProvider.prepareSettings():
        log('cannot prepare media provider settings', xbmc.LOGERROR)
        return

    server = Server(mediaProvider)
    if not server.Authenticate():
        log(
            f"failed to connect to Plex Media Server for {mediaProvider2str(mediaProvider)}",
            xbmc.LOGWARNING)
        return

    plexServer = server.PlexServer()

    # get all library sections
    mediaTypes = mediaImport.getMediaTypes()
    librarySections = getLibrarySections(plexServer, mediaTypes)
    sections = [(section['title'], section['key'])
                for section in librarySections]

    # get the import's settings
    settings = mediaImport.getSettings()

    # pass the list of views back to Kodi
    settings.setStringOptions(plex.constants.SETTINGS_IMPORT_LIBRARY_SECTIONS,
                              sections)
예제 #4
0
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")
예제 #5
0
def isImportReady(handle, options):
    # 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 = xbmcmediaimport.getProvider(handle)
    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

    try:
        server = Server(mediaProvider)
    except:
        pass

    importReady = False
    # check if authentication works with the current provider settings
    if server.Authenticate():
        # check if the chosen library sections exist
        selectedLibrarySections = getLibrarySectionsFromSettings(importSettings)
        matchingLibrarySections = getMatchingLibrarySections(server.PlexServer(), mediaImport.getMediaTypes(), selectedLibrarySections)
        importReady = len(matchingLibrarySections) > 0

    xbmcmediaimport.setImportReady(handle, importReady)
예제 #6
0
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')
class ProviderObserver:
    """Class for observing the PMS websocket stream for live updates and processing the messages."""
    class Action:
        Start = 0
        Stop = 1

    ENDPOINT = '/:/websockets/notifications'

    def __init__(self):
        # default values
        self._actions = []
        self._connected = False
        self._imports = []
        self._mediaProvider = None
        self._server = None
        self._settings = None

        # create the websocket
        self._websocket = websocket.WebSocket()
        self._websocket.settimeout(0.1)

    def __del__(self):
        self._StopAction()

    def AddImport(self, mediaImport: xbmcmediaimport.MediaImport):
        """Add, or update if existing, mediaImport into list of imports

        :param mediaImport: mediaImport object to update into the import list
        :type mediaImport: :class:`xbmcmediaimport.MediaImport`
        """
        if not mediaImport:
            raise ValueError('invalid mediaImport')

        # look for a matching import
        matchingImportIndices = self._FindImportIndices(mediaImport)
        # if a matching import has been found update it
        if matchingImportIndices:
            self._imports[matchingImportIndices[0]] = mediaImport
            log(f"media import {mediaImport2str(mediaImport)} updated",
                xbmc.LOGINFO)
        else:
            # other add the import to the list
            self._imports.append(mediaImport)
            log(f"media import {mediaImport2str(mediaImport)} added",
                xbmc.LOGINFO)

    def RemoveImport(self, mediaImport: xbmcmediaimport.MediaImport):
        """Remove an existing mediaImport from the list of imports

        :param mediaImport: mediaImport object to remove from the import plist
        :type mediaImport: :class:`xbmcmediaimport.MediaImport`
        """
        if not mediaImport:
            raise ValueError('invalid mediaImport')

        # look for a matching import
        matchingImportIndices = self._FindImportIndices(mediaImport)
        if not matchingImportIndices:
            return

        # remove the media import from the list
        del self._imports[matchingImportIndices[0]]
        log(f"media import {mediaImport2str(mediaImport)} removed",
            xbmc.LOGINFO)

    def Start(self, mediaProvider: xbmcmediaimport.MediaProvider):
        """Trigger start of observation for the provided mediaProvider

        :param mediaProvider: Media provider to start observing
        :type mediaProvider: :class:`xbmcmediaimport.MediaProvider`
        """
        if not mediaProvider:
            raise ValueError('invalid mediaProvider')

        self._actions.append((ProviderObserver.Action.Start, mediaProvider))

    def Stop(self):
        """End all observation tasks"""
        self._actions.append((ProviderObserver.Action.Stop, None))

    def Process(self):
        """Main trigger to process all queued messages and provider actions"""
        # process any open actions
        self._ProcessActions()
        # process any incoming messages
        self._ProcessMessages()

    def _FindImportIndices(
            self, mediaImport: xbmcmediaimport.MediaImport) -> List[int]:
        """Find the list index for the provided mediaImport object in the imports list if present

        :param mediaImport: The mediaImport object to find in the imports list
        :type mediaImport: :class:`xbmcmediaimport.MediaImport`
        :return: List of indexes where the mediaImport object was found
        :rtype: list
        """
        if not mediaImport:
            raise ValueError('invalid mediaImport')

        return [
            i for i, x in enumerate(self._imports)
            if x.getProvider().getIdentifier() == mediaImport.getProvider().getIdentifier() and \
               x.getMediaTypes() == mediaImport.getMediaTypes()
        ]

    def _ProcessActions(self):
        """Process pending provider actions in the queue (start/stop observation)"""
        for (action, data) in self._actions:
            if action == ProviderObserver.Action.Start:
                self._StartAction(data)
            elif action == ProviderObserver.Action.Stop:
                self._StopAction()
            else:
                log(f"unknown action {action} to process", xbmc.LOGWARNING)

        self._actions = []

    def _ProcessMessages(self):
        """Trigger processing of messages from the media providers websocket"""
        # nothing to do if we are not connected to a Plex server
        if not self._connected:
            return

        while True:
            try:
                message = self._websocket.recv()
                if not message:
                    break

                messageObj = json.loads(message)
                if not messageObj:
                    log((
                        f"invalid JSON message ({len(message)}) from {mediaProvider2str(self._mediaProvider)} "
                        f"received: {message}"), xbmc.LOGWARNING)
                    continue

                self._ProcessMessage(messageObj)

            except websocket.WebSocketTimeoutException:
                break
            except Exception as e:
                # TODO(Montellese): remove workaround for Kodi Python 3 issue
                if e.args and e.args[0] == 'timed out':
                    break

                log(
                    f"unknown exception when receiving data from {mediaProvider2str(self._mediaProvider)}: "
                    f"{e.args[0]}", xbmc.LOGWARNING)
                break

    def _ProcessMessage(self, message: dict):
        """Determine message type and pass to appropriate processing function

        :param message: Message data to be processed
        :type message: dict
        """
        if not message:
            return

        if constants.WS_MESSAGE_NOTIFICATION_CONTAINER not in message:
            log((
                f"message without '{constants.WS_MESSAGE_NOTIFICATION_CONTAINER}'"
                f"received from {mediaProvider2str(self._mediaProvider)}: {json.dumps(message)}"
            ), xbmc.LOGWARNING)
            return

        messageData = message[constants.WS_MESSAGE_NOTIFICATION_CONTAINER]
        if constants.WS_MESSAGE_NOTIFICATION_TYPE not in messageData:
            log((
                f"message without '{constants.WS_MESSAGE_NOTIFICATION_TYPE}'"
                f"received from {mediaProvider2str(self._mediaProvider)}: {json.dumps(message)}"
            ), xbmc.LOGWARNING)
            return

        messageType = messageData[constants.WS_MESSAGE_NOTIFICATION_TYPE]
        if messageType == constants.WS_MESSAGE_NOTIFICATION_TYPE_TIMELINE:
            self._ProcessMessageTimelineEntry(messageData)
        elif messageType == constants.WS_MESSAGE_NOTIFICATION_TYPE_PLAYING:
            self._ProcessMessagePlaySessionState(messageData)
        elif messageType == constants.WS_MESSAGE_NOTIFICATION_TYPE_ACTIVITY:
            self._ProcessMessageActivity(messageData)
        else:
            log(
                f"ignoring '{messageType}' from {mediaProvider2str(self._mediaProvider)}: {json.dumps(message)}",
                xbmc.LOGWARNING)

    def _ProcessMessageTimelineEntry(self, data: dict):
        """Gather details from a Timeline Entry message, format into a plex change and trigger further processing

        :param data: Timeline message data to process
        :type data: dict
        """
        if constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY not in data:
            log(
                f"invalid timeline message received from {mediaProvider2str(self._mediaProvider)}: {json.dumps(data)}",
                xbmc.LOGWARNING)
            return

        timelineEntries = data[
            constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY]
        if not timelineEntries:
            return

        required_keys = (
            constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_IDENTIFIER,
            constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_ITEM_ID,
            constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE,
            constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE)

        changedPlexItems = []
        for timelineEntry in timelineEntries:
            if not all(key in timelineEntry for key in required_keys):
                continue

            # we are only interested in library changes
            if (timelineEntry[
                    constants.
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_IDENTIFIER] !=
                    constants.
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_IDENTIFIER_LIBRARY):
                continue

            plexItemId = int(timelineEntry[
                constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_ITEM_ID])
            if not plexItemId:
                continue

            # filter and determine the changed item's library type / class
            plexItemType = timelineEntry[
                constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE]
            if plexItemType == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_MOVIE:
                plexItemLibraryType = api.PLEX_LIBRARY_TYPE_MOVIE
            elif plexItemType == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_TVSHOW:
                plexItemLibraryType = api.PLEX_LIBRARY_TYPE_TVSHOW
            elif plexItemType == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_SEASON:
                plexItemLibraryType = api.PLEX_LIBRARY_TYPE_SEASON
            elif plexItemType == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_EPISODE:
                plexItemLibraryType = api.PLEX_LIBRARY_TYPE_EPISODE
            else:
                continue

            plexItemMediaClass = api.Api.getPlexMediaClassFromLibraryType(
                plexItemLibraryType)

            # filter and process the changed item's state
            plexItemState = timelineEntry[
                constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE]
            if plexItemState == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE_CREATED:
                plexItemChangesetType = xbmcmediaimport.MediaImportChangesetTypeAdded
            elif plexItemState == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE_FINISHED:
                plexItemChangesetType = xbmcmediaimport.MediaImportChangesetTypeChanged
            elif plexItemState == constants.WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE_DELETED:
                plexItemChangesetType = xbmcmediaimport.MediaImportChangesetTypeRemoved
            else:
                continue

            changedPlexItems.append(
                (plexItemChangesetType, plexItemId, plexItemMediaClass))

        self._ProcessChangedPlexItems(changedPlexItems)

    def _ProcessMessagePlaySessionState(self, data: dict):
        """
        Gather details from a Play Session message, format into a plex change and trigger further processing
        Not Implemented

        :param data: Play Session message data to process
        :type data: dict
        """
        if constants.WS_MESSAGE_NOTIFICATION_PLAY_SESSION_STATE not in data:
            log(
                f"invalid playing message received from {mediaProvider2str(self._mediaProvider)}: {json.dumps(data)}",
                xbmc.LOGWARNING)
            return

        playSessionStates = data[
            constants.WS_MESSAGE_NOTIFICATION_PLAY_SESSION_STATE]
        if not playSessionStates:
            return

        # TODO(Montellese)
        # for playSessionState in playSessionStates:
        #     pass

    def _ProcessMessageActivity(self, data: dict):
        """Gather details from an Activity message, format into a plex change and trigger further processing

        :param data: Activity message data to process
        :type data: dict
        """
        if constants.WS_MESSAGE_NOTIFICATION_ACTIVITY not in data:
            log(
                f"invalid activity message received from {mediaProvider2str(self._mediaProvider)}: {json.dumps(data)}",
                xbmc.LOGWARNING)
            return

        activities = data[constants.WS_MESSAGE_NOTIFICATION_ACTIVITY]
        if not activities:
            return

        required_activity_keys = (
            constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_EVENT,
            constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY)
        required_activity_details_keys = (
            constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_TYPE,
            constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT)

        changedPlexItems = []
        for activity in activities:
            if not all(key in activity for key in required_activity_keys):
                continue
            # we are only interested in the final result
            if (activity[constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_EVENT] !=
                    constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_EVENT_ENDED):
                continue

            activityDetails = activity[
                constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY]
            if not all(key in activityDetails
                       for key in required_activity_details_keys):
                continue

            # we are only interested in changes to library items
            if (activityDetails[
                    constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_TYPE]
                    != constants.
                    WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_TYPE_REFRESH_ITEMS
                ):
                continue

            context = activityDetails[
                constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT]
            if constants.WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT_KEY not in context:
                continue

            plexItemKey = context[
                constants.
                WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT_KEY]
            plexItemId = api.Api.getItemIdFromPlexKey(plexItemKey)
            if not plexItemId:
                continue

            changedPlexItems.append(
                (xbmcmediaimport.MediaImportChangesetTypeChanged, plexItemId,
                 None))

        self._ProcessChangedPlexItems(changedPlexItems)

    def _ProcessChangedPlexItems(self, changedPlexItems: List[tuple]):
        """
        Determine if change was an add/update or remove operation,
        pull the details of the item being changed,
        format and trigger further processing

        :param changedPlexItems: List of plex change tuples parsed from websocket messages to process
        :type changedPlexItems: list
        """
        changedItems = []
        for (changesetType, plexItemId, plexItemClass) in changedPlexItems:
            item = None
            if changesetType in (
                    xbmcmediaimport.MediaImportChangesetTypeAdded,
                    xbmcmediaimport.MediaImportChangesetTypeChanged):
                # get all details for the added / changed item
                item = self._GetItemDetails(plexItemId, plexItemClass)
                if not item:
                    log(
                        f"failed to get details for changed item with id {plexItemId}",
                        xbmc.LOGWARNING)
                    continue
            else:
                # find the removed item in the list of imported items
                importedItems = xbmcmediaimport.getImportedItemsByProvider(
                    self._mediaProvider)
                matchingItems = [
                    importedItem for importedItem in importedItems if
                    api.Api.getItemIdFromListItem(importedItem) == plexItemId
                ]
                if not matchingItems:
                    log(f"failed to find removed item with id {plexItemId}",
                        xbmc.LOGWARNING)
                    continue
                if len(matchingItems) > 1:
                    log(
                        f"multiple imported items for item with id {plexItemId} found => only removing the first one",
                        xbmc.LOGWARNING)

                item = matchingItems[0]

            if not item:
                log(f"failed to process changed item with id {plexItemId}",
                    xbmc.LOGWARNING)
                continue

            changedItems.append((changesetType, item, plexItemId))

        self._ChangeItems(changedItems)

    def _ChangeItems(self, changedItems: List[tuple]):
        """Send change details to Kodi to perform library updates

        :param changedItems: List of change detail tuples to process
        :type changedItems: list
        """
        # map the changed items to their media import
        changedItemsMap = {}
        for (changesetType, item, plexItemId) in changedItems:
            if not item:
                continue

            # find a matching import for the changed item
            mediaImport = self._FindImportForItem(item)
            if not mediaImport:
                log(
                    f"failed to determine media import for changed item with id {plexItemId}",
                    xbmc.LOGWARNING)
                continue

            if mediaImport not in changedItemsMap:
                changedItemsMap[mediaImport] = []

            changedItemsMap[mediaImport].append((changesetType, item))

        # finally pass the changed items grouped by their media import to Kodi
        for (mediaImport, itemsByImport) in changedItemsMap.items():
            if xbmcmediaimport.changeImportedItems(mediaImport, itemsByImport):
                log(
                    f"changed {len(itemsByImport)} imported items for media import {mediaImport2str(mediaImport)}",
                    xbmc.LOGINFO)
            else:
                log((f"failed to change {len(itemsByImport)} imported items "
                     f"for media import {mediaImport2str(mediaImport)}"),
                    xbmc.LOGWARNING)

    def _GetItemDetails(
            self,
            plexItemId: int,
            plexItemClass: plexapi.video.Video = None) -> xbmcgui.ListItem:
        """Pull details of plex item from the PMS server as a kodi ListItem

        :param plexItemId: ID of the item in PMS to get the details of
        :type plexItemId: int
        :param plexItemClass: Plex video object media type
        :type plexItemClass: :class:`plexapi.video.Video`, optional
        :return: ListItem object populated with the details of the plex item
        :rtype: :class:`xbmc.ListItem`
        """
        return api.Api.getPlexItemAsListItem(
            self._server.PlexServer(),
            plexItemId,
            plexItemClass,
            allowDirectPlay=self._settings.getBool(
                constants.SETTINGS_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY))

    def _FindImportForItem(
            self, item: xbmcgui.ListItem) -> xbmcmediaimport.MediaImport:
        """Find a matching MediaImport in the imports list for the provided ListItem

        :param item: The plex import item in ListItem object to find a matching import for
        :type item: :class:`xbmcgui.ListItem`
        :return: The matching MediaImport item if found
        :rtype: :class:`xbmcmediaimport.MediaImport`
        """
        videoInfoTag = item.getVideoInfoTag()
        if not videoInfoTag:
            return None

        itemMediaType = videoInfoTag.getMediaType()

        matchingImports = [
            mediaImport for mediaImport in self._imports
            if itemMediaType in mediaImport.getMediaTypes()
        ]
        if not matchingImports:
            return None

        return matchingImports[0]

    def _StartAction(self,
                     mediaProvider: xbmcmediaimport.MediaProvider) -> bool:
        """Establish websocket connection to the provided MediaProvder and begin listening

        :param mediaProvider: MediaProvider with websocket to connect to
        :type mediaProvider: :class:`xbmcmediaimport.MediaProvider`
        :return: Whether the connection was successful or not
        :rtype: bool
        """
        if not mediaProvider:
            raise RuntimeError('invalid mediaProvider')

        # if we are already connected check if something important changed in the media provider
        if self._connected:
            if api.Api.compareMediaProviders(self._mediaProvider,
                                             mediaProvider):
                return True

        self._StopAction(restart=True)

        self._mediaProvider = mediaProvider

        self._settings = self._mediaProvider.prepareSettings()
        if not self._settings:
            raise RuntimeError('cannot prepare media provider settings')

        # create Plex server instance
        self._server = Server(self._mediaProvider)

        # first authenticate with the Plex Media Server
        try:
            authenticated = self._server.Authenticate()
        except:
            authenticated = False

        if not authenticated:
            log(
                f"failed to authenticate with {mediaProvider2str(self._mediaProvider)}",
                xbmc.LOGERROR)
            self._Reset()
            return False

        # prepare the URL
        url = self._server.PlexServer().url(self.ENDPOINT,
                                            includeToken=True).replace(
                                                'http', 'ws')

        # connect the websocket
        try:
            self._websocket.connect(url)
        except:
            log(f"failed to connect to {url} using a websocket", xbmc.LOGERROR)
            self._Reset()
            return False

        log(
            f"successfully connected to {mediaProvider2str(self._mediaProvider)} to observe media imports",
            xbmc.LOGINFO)
        self._connected = True
        return True

    def _StopAction(self, restart: bool = False):
        """Close the current websocket connection and reset connection details back to defaults

        :param restart: Whether the connection should be re-started or not
        :type restart: bool, optional
        """
        if not self._connected:
            return

        if not restart:
            log(
                f"stopped observing media imports from {mediaProvider2str(self._mediaProvider)}",
                xbmc.LOGINFO)

        self._websocket.close()
        self._Reset()

    def _Reset(self):
        """Reset connection state variables back to defaults"""
        self._connected = False
        self._server = None
        self._mediaProvider = None
예제 #8
0
class ProviderObserver:
    class Action:
        Start = 0
        Stop = 1

    ENDPOINT = '/:/websockets/notifications'

    def __init__(self):
        # default values
        self._actions = []
        self._connected = False
        self._imports = []
        self._mediaProvider = None
        self._server = None

        # create the websocket
        self._websocket = websocket.WebSocket()
        self._websocket.settimeout(0.1)

    def __del__(self):
        self._StopAction()

    def AddImport(self, mediaImport):
        if not mediaImport:
            raise ValueError('invalid mediaImport')

        # look for a matching import
        matchingImportIndices = self._FindImportIndices(mediaImport)
        # if a matching import has been found update it
        if matchingImportIndices:
            self._imports[matchingImportIndices[0]] = mediaImport
            log('media import {} updated'.format(mediaImport2str(mediaImport)))
        else:
            # other add the import to the list
            self._imports.append(mediaImport)
            log('media import {} added'.format(mediaImport2str(mediaImport)))

    def RemoveImport(self, mediaImport):
        if not mediaImport:
            raise ValueError('invalid mediaImport')

        # look for a matching import
        matchingImportIndices = self._FindImportIndices(mediaImport)
        if not matchingImportIndices:
            return

        # remove the media import from the list
        del self._imports[matchingImportIndices[0]]
        log('media import {} removed'.format(mediaImport2str(mediaImport)))

    def Start(self, mediaProvider):
        if not mediaProvider:
            raise ValueError('invalid mediaProvider')

        self._actions.append((ProviderObserver.Action.Start, mediaProvider))

    def Stop(self):
        self._actions.append((ProviderObserver.Action.Stop, None))

    def Process(self):
        # process any open actions
        self._ProcessActions()
        # process any incoming messages
        self._ProcessMessages()

    def _FindImportIndices(self, mediaImport):
        if not mediaImport:
            raise ValueError('invalid mediaImport')

        return [
            i for i, x in enumerate(self._imports)
            if x.getPath() == mediaImport.getPath()
            and x.getMediaTypes() == mediaImport.getMediaTypes()
        ]

    def _ProcessActions(self):
        for (action, data) in self._actions:
            if action == ProviderObserver.Action.Start:
                self._StartAction(data)
            elif action == ProviderObserver.Action.Stop:
                self._StopAction()
            else:
                log('unknown action {} to process'.format(action),
                    xbmc.LOGWARNING)

        self._actions = []

    def _ProcessMessages(self):
        # nothing to do if we are not connected to an Emby server
        if not self._connected:
            return

        while True:
            try:
                message = self._websocket.recv()
                if message is None:
                    break

                messageObj = json.loads(message)
                if not messageObj:
                    log(
                        'invalid JSON message ({}) from {} received: {}'.
                        format(len(message),
                               mediaProvider2str(self._mediaProvider),
                               message), xbmc.LOGWARNING)
                    continue

                self._ProcessMessage(messageObj)

            except websocket.WebSocketTimeoutException:
                break
            except Exception as error:
                log(
                    'unknown exception when receiving data from {}: {}'.format(
                        mediaProvider2str(self._mediaProvider), error.args[0]),
                    xbmc.LOGWARNING)
                break

    def _ProcessMessage(self, message):
        if not message:
            return

        if not WS_MESSAGE_NOTIFICATION_CONTAINER in message:
            log(
                'message without "{}" received from {}: {}'.format(
                    WS_MESSAGE_NOTIFICATION_CONTAINER,
                    mediaProvider2str(self._mediaProvider),
                    json.dumps(message)), xbmc.LOGWARNING)
            return

        messageData = message[WS_MESSAGE_NOTIFICATION_CONTAINER]
        if not WS_MESSAGE_NOTIFICATION_TYPE in messageData:
            log(
                'message without "{}" received from {}: {}'.format(
                    WS_MESSAGE_NOTIFICATION_TYPE,
                    mediaProvider2str(self._mediaProvider),
                    json.dumps(message)), xbmc.LOGWARNING)
            return

        messageType = messageData[WS_MESSAGE_NOTIFICATION_TYPE]
        if messageType == WS_MESSAGE_NOTIFICATION_TYPE_TIMELINE:
            self._ProcessMessageTimelineEntry(messageData)
        elif messageType == WS_MESSAGE_NOTIFICATION_TYPE_PLAYING:
            self._ProcessMessagePlaySessionState(messageData)
        elif messageType == WS_MESSAGE_NOTIFICATION_TYPE_ACTIVITY:
            self._ProcessMessageActivity(messageData)
        else:
            log(
                'ignoring "{}" message from {}: {}'.format(
                    messageType, mediaProvider2str(self._mediaProvider),
                    json.dumps(message)), xbmc.LOGDEBUG)

    def _ProcessMessageTimelineEntry(self, data):
        if not WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY in data:
            log(
                'invalid timeline message received from {}: {}'.format(
                    mediaProvider2str(self._mediaProvider), json.dumps(data)),
                xbmc.LOGWARNING)
            return

        timelineEntries = data[WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY]
        if not timelineEntries:
            return

        changedPlexItems = []
        for timelineEntry in timelineEntries:
            if not all(key in timelineEntry for key in (
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_IDENTIFIER,
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_ITEM_ID,
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE,
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE)):
                continue

            # we are only interested in library changes
            if timelineEntry[
                    WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_IDENTIFIER] != WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_IDENTIFIER_LIBRARY:
                continue

            plexItemId = int(
                timelineEntry[WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_ITEM_ID])
            if not plexItemId:
                continue

            # filter and determine the changed item's library type / class
            plexItemType = timelineEntry[
                WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE]
            if plexItemType == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_MOVIE:
                plexItemLibraryType = PLEX_LIBRARY_TYPE_MOVIE
            elif plexItemType == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_TVSHOW:
                plexItemLibraryType = PLEX_LIBRARY_TYPE_TVSHOW
            elif plexItemType == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_SEASON:
                plexItemLibraryType = PLEX_LIBRARY_TYPE_SEASON
            elif plexItemType == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_TYPE_EPISODE:
                plexItemLibraryType = PLEX_LIBRARY_TYPE_EPISODE
            else:
                continue
            plexItemMediaClass = Api.getPlexMediaClassFromLibraryType(
                plexItemLibraryType)

            # filter and process the changed item's state
            plexItemState = timelineEntry[
                WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE]
            if plexItemState == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE_CREATED:
                plexItemChangesetType = xbmcmediaimport.MediaImportChangesetTypeAdded
            elif plexItemState == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE_FINISHED:
                plexItemChangesetType = xbmcmediaimport.MediaImportChangesetTypeChanged
            elif plexItemState == WS_MESSAGE_NOTIFICATION_TIMELINE_ENTRY_STATE_DELETED:
                plexItemChangesetType = xbmcmediaimport.MediaImportChangesetTypeRemoved
            else:
                continue

            changedPlexItems.append(
                (plexItemChangesetType, plexItemId, plexItemMediaClass))

        self._ProcessChangedPlexItems(changedPlexItems)

    def _ProcessMessagePlaySessionState(self, data):
        if not WS_MESSAGE_NOTIFICATION_PLAY_SESSION_STATE in data:
            log(
                'invalid playing message received from {}: {}'.format(
                    mediaProvider2str(self._mediaProvider), json.dumps(data)),
                xbmc.LOGWARNING)
            return

        playSessionStates = data[WS_MESSAGE_NOTIFICATION_PLAY_SESSION_STATE]
        if not playSessionStates:
            return

        for playSessionState in playSessionStates:
            # TODO(Montellese)
            pass

    def _ProcessMessageActivity(self, data):
        if not WS_MESSAGE_NOTIFICATION_ACTIVITY in data:
            log(
                'invalid activity message received from {}: {}'.format(
                    mediaProvider2str(self._mediaProvider), json.dumps(data)),
                xbmc.LOGWARNING)
            return

        activities = data[WS_MESSAGE_NOTIFICATION_ACTIVITY]
        if not activities:
            return

        changedPlexItems = []
        for activity in activities:
            if not all(key in activity
                       for key in (WS_MESSAGE_NOTIFICATION_ACTIVITY_EVENT,
                                   WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY)):
                continue
            # we are only interested in the final result
            if activity[
                    WS_MESSAGE_NOTIFICATION_ACTIVITY_EVENT] != WS_MESSAGE_NOTIFICATION_ACTIVITY_EVENT_ENDED:
                continue

            activityDetails = activity[
                WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY]
            if not all(key in activityDetails for key in (
                    WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_TYPE,
                    WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT)):
                continue

            # we are only interested in changes to library items
            if activityDetails[
                    WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_TYPE] != WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_TYPE_REFRESH_ITEMS:
                continue

            context = activityDetails[
                WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT]
            if not WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT_KEY in context:
                continue

            plexItemKey = context[
                WS_MESSAGE_NOTIFICATION_ACTIVITY_ACTIVITY_CONTEXT_KEY]
            plexItemId = Api.getItemIdFromPlexKey(plexItemKey)
            if not plexItemId:
                continue
            changedPlexItems.append(
                (xbmcmediaimport.MediaImportChangesetTypeChanged, plexItemId,
                 None))

        self._ProcessChangedPlexItems(changedPlexItems)

    def _ProcessChangedPlexItems(self, changedPlexItems):
        changedItems = []
        for (changesetType, plexItemId, plexItemClass) in changedPlexItems:
            item = None
            if changesetType == xbmcmediaimport.MediaImportChangesetTypeAdded or \
               changesetType == xbmcmediaimport.MediaImportChangesetTypeChanged:
                # get all details for the added / changed item
                item = self._GetItemDetails(plexItemId, plexItemClass)
                if not item:
                    log(
                        'failed to get details for changed item with id {}'.
                        format(plexItemId), xbmc.LOGWARNING)
                    continue
            else:
                # find the removed item in the list of imported items
                importedItems = xbmcmediaimport.getImportedItemsByProvider(
                    self._mediaProvider)
                matchingItems = [
                    importedItem for importedItem in importedItems
                    if Api.getItemIdFromListItem(importedItem) == plexItemId
                ]
                if not matchingItems:
                    log(
                        'failed to find removed item with id {}'.format(
                            plexItemId), xbmc.LOGWARNING)
                    continue
                if len(matchingItems) > 1:
                    log(
                        'multiple imported items for item with id {} found => only removing the first one'
                        .format(plexItemId), xbmc.LOGWARNING)

                item = matchingItems[0]

            if not item:
                log(
                    'failed to process changed item with id {}'.format(
                        plexItemId), xbmc.LOGWARNING)
                continue

            changedItems.append((changesetType, item, plexItemId))

        self._ChangeItems(changedItems)

    def _ChangeItems(self, changedItems):
        # map the changed items to their media import
        changedItemsMap = {}
        for (changesetType, item, plexItemId) in changedItems:
            if not item:
                continue

            # find a matching import for the changed item
            mediaImport = self._FindImportForItem(item)
            if not mediaImport:
                log(
                    'failed to determine media import for changed item with id {}'
                    .format(plexItemId), xbmc.LOGWARNING)
                continue

            if mediaImport not in changedItemsMap:
                changedItemsMap[mediaImport] = []

            changedItemsMap[mediaImport].append((changesetType, item))

        # finally pass the changed items grouped by their media import to Kodi
        for (mediaImport, changedItems) in changedItemsMap.items():
            if xbmcmediaimport.changeImportedItems(mediaImport, changedItems):
                log('changed {} imported items for media import {}'.format(
                    len(changedItems), mediaImport2str(mediaImport)))
            else:
                log(
                    'failed to change {} imported items for media import {}'.
                    format(len(changedItems),
                           mediaImport2str(mediaImport)), xbmc.LOGWARNING)

    def _GetItemDetails(self, plexItemId, plexItemClass=None):
        return Api.getPlexItemAsListItem(self._server.PlexServer(), plexItemId,
                                         plexItemClass)

    def _FindImportForItem(self, item):
        videoInfoTag = item.getVideoInfoTag()
        if not videoInfoTag:
            return None

        itemMediaType = videoInfoTag.getMediaType()

        matchingImports = [
            mediaImport for mediaImport in self._imports
            if itemMediaType in mediaImport.getMediaTypes()
        ]
        if not matchingImports:
            return None

        return matchingImports[0]

    def _StartAction(self, mediaProvider):
        if not mediaProvider:
            raise RuntimeError('invalid mediaProvider')

        # if we are already connected check if something important changed in the media provider
        if self._connected:
            if Api.compareMediaProviders(self._mediaProvider, mediaProvider):
                return True

        self._StopAction(restart=True)

        self._mediaProvider = mediaProvider

        settings = self._mediaProvider.prepareSettings()
        if not settings:
            raise RuntimeError('cannot prepare media provider settings')

        # create Plex server instance
        self._server = Server(self._mediaProvider)

        # first authenticate with the Plex Media Server
        try:
            authenticated = self._server.Authenticate()
        except:
            authenticated = False

        if not authenticated:
            log(
                'failed to authenticate with {}'.format(
                    mediaProvider2str(self._mediaProvider)), xbmc.LOGERROR)
            self._Reset()
            return False

        # prepare the URL
        url = self._server.PlexServer().url(self.ENDPOINT,
                                            includeToken=True).replace(
                                                'http', 'ws')

        # connect the websocket
        try:
            self._websocket.connect(url)
        except:
            log('failed to connect to {} using a websocket'.format(url),
                xbmc.LOGERROR)
            self._Reset()
            return False

        log('successfully connected to {} to observe media imports'.format(
            mediaProvider2str(self._mediaProvider)))
        self._connected = True
        return True

    def _StopAction(self, restart=False):
        if not self._connected:
            return

        if not restart:
            log('stopped observing media imports from {}'.format(
                mediaProvider2str(self._mediaProvider)))

        self._websocket.close()
        self._Reset()

    def _Reset(self):
        self._connected = False
        self._server = None
        self._mediaProvider = None
예제 #9
0
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)
예제 #10
0
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)
예제 #11
0
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)
예제 #12
0
    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()
예제 #13
0
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)
예제 #14
0
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)
예제 #15
0
    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
            )
예제 #16
0
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)