コード例 #1
0
        def checkLogin(self):
            if self.finished:
                return not self.expired

            url = Url.append(constants.URL_EMBY_CONNECT_BASE, constants.URL_EMBY_CONNECT_PIN)
            url = Url.addOptions(url, {
                constants.URL_QUERY_DEVICE_ID: self.deviceId,
                constants.URL_QUERY_PIN: self.pin,
            })

            resultObj = Request.GetAsJson(url)
            if not resultObj or \
               constants.PROPERTY_EMBY_CONNECT_PIN_IS_CONFIRMED not in resultObj or \
               constants.PROPERTY_EMBY_CONNECT_PIN_IS_EXPIRED not in resultObj:
                log('failed to check status of PIN {} at {}: {}'.format(self.pin, url, resultObj), xbmc.LOGWARNING)
                self.finished = True
                self.expired = True
                return False

            self.finished = resultObj.get(constants.PROPERTY_EMBY_CONNECT_PIN_IS_CONFIRMED)
            self.expired = resultObj.get(constants.PROPERTY_EMBY_CONNECT_PIN_IS_EXPIRED)
            if self.expired:
                self.finished = True

            return self.finished
コード例 #2
0
    def BuildDirectStreamUrl(self, mediaType, itemId, container):
        if not itemId:
            raise ValueError('Invalid itemId')

        embyMediaType = None
        if mediaType == 'Video':
            embyMediaType = constants.URL_PLAYBACK_MEDIA_TYPE_VIDEO
        elif mediaType == 'Audio':
            embyMediaType = constants.URL_PLAYBACK_MEDIA_TYPE_AUDIO
        else:
            raise ValueError('Invalid mediaType "{}"'.format(mediaType))

        url = self.BuildUrl(embyMediaType)
        url = Url.append(url, itemId, constants.URL_PLAYBACK_STREAM)
        if container:
            containers = container.split(',')
            # TODO(Montellese): for now pick the first container but maybe we
            # need some sanity checking / priorization
            url = '{}.{}'.format(url, containers[0])

        url = Url.addOptions(
            url, {
                constants.URL_PLAYBACK_OPTION_STATIC:
                constants.URL_PLAYBACK_OPTION_STATIC_TRUE,
                constants.URL_QUERY_API_KEY: self.AccessToken()
            })

        return url
コード例 #3
0
def importItems(handle, embyServer, url, mediaType, viewId, embyMediaType=None, viewName=None, raw=False, allowDirectPlay=True):
    items = []

    viewUrl = url
    viewUrl = Url.addOptions(viewUrl, { 'ParentId': viewId })

    # retrieve all items matching the current media type
    totalCount = 0
    startIndex = 0
    while True:
        if xbmcmediaimport.shouldCancel(handle, startIndex, max(totalCount, 1)):
            return

        # put together a paged URL
        pagedUrlOptions = {
            'StartIndex': startIndex
        }
        pagedUrl = Url.addOptions(viewUrl, pagedUrlOptions)
        resultObj = embyServer.ApiGet(pagedUrl)
        if not resultObj or not emby.constants.PROPERTY_ITEM_ITEMS in resultObj or not emby.constants.PROPERTY_ITEM_TOTAL_RECORD_COUNT in resultObj:
            log('invalid response for items of media type "{}" from {}'.format(mediaType, pagedUrl), xbmc.LOGERROR)
            return

        # retrieve the total number of items
        totalCount = int(resultObj[emby.constants.PROPERTY_ITEM_TOTAL_RECORD_COUNT])

        # parse all items
        itemsObj = resultObj[emby.constants.PROPERTY_ITEM_ITEMS]
        for itemObj in itemsObj:
            startIndex = startIndex + 1
            if xbmcmediaimport.shouldCancel(handle, startIndex, totalCount):
                return

            if raw:
                items.append(itemObj)
            else:
                item = kodi.Api.toFileItem(embyServer, itemObj, mediaType, embyMediaType, viewName, allowDirectPlay=allowDirectPlay)
                if not item:
                    continue

                items.append(item)

        # check if we have retrieved all available items
        if startIndex >= totalCount:
            break

    return items
コード例 #4
0
    def UpdateResumePoint(embyServer, itemId, positionInTicks):
        if not embyServer:
            raise ValueError('invalid embyServer')
        if not itemId:
            raise ValueError('invalid itemId')

        url = embyServer.BuildUserPlayingItemUrl(itemId)
        url = Url.addOptions(url, {'PositionTicks': positionInTicks})

        embyServer.ApiDelete(url)
        return True
コード例 #5
0
    def BuildImageUrl(self, itemId, imageType, imageTag=''):
        if not itemId:
            raise ValueError('Invalid itemId')
        if not imageType:
            raise ValueError('Invalid imageType')

        url = self.BuildItemUrl(itemId)
        url = Url.append(url, constants.URL_IMAGES, imageType)
        if imageTag:
            url = Url.addOptions(url, {constants.URL_QUERY_TAG: imageTag})

        return url
コード例 #6
0
    def BuildConnectExchangeUrl(baseUrl, userId):
        if not baseUrl:
            raise ValueError('Invalid baseUrl')
        if not userId:
            raise ValueError('Invalid userId')

        url = Url.append(Server._buildBaseUrl(baseUrl), constants.URL_CONNECT, constants.URL_CONNECT_EXCHANGE)
        url = Url.addOptions(url, {
            constants.URL_QUERY_CONNECT_EXCHANGE_FORMAT: constants.URL_QUERY_CONNECT_EXCHANGE_FORMAT_JSON,
            constants.URL_QUERY_CONNECT_EXCHANGE_USER_ID: userId,
        })

        return url
コード例 #7
0
    def GetServers(accessToken, userId):
        if not accessToken:
            raise ValueError('invalid accessToken')
        if not userId:
            raise ValueError('invalid userId')

        url = Url.append(constants.URL_EMBY_CONNECT_BASE,
                         constants.URL_EMBY_CONNECT_SERVERS)
        url = Url.addOptions(url, {
            constants.URL_QUERY_USER_ID: userId,
        })
        headers = EmbyConnect._getApplicationHeader()
        headers.update({
            constants.EMBY_CONNECT_USER_TOKEN_HEADER: accessToken,
        })

        resultObj = Request.GetAsJson(url, headers=headers)
        if not resultObj:
            log('invalid response from {}: {}'.format(url, resultObj))
            return None

        servers = []
        for server in resultObj:
            id = server.get(constants.PROPERTY_EMBY_CONNECT_SERVER_ID, None)
            systemId = server.get(
                constants.PROPERTY_EMBY_CONNECT_SERVER_SYSTEM_ID, None)
            accessKey = server.get(
                constants.PROPERTY_EMBY_CONNECT_SERVER_ACCESS_KEY, None)
            name = server.get(constants.PROPERTY_EMBY_CONNECT_SERVER_NAME,
                              None)
            remoteUrl = server.get(
                constants.PROPERTY_EMBY_CONNECT_SERVER_REMOTE_URL, None)
            localUrl = server.get(
                constants.PROPERTY_EMBY_CONNECT_SERVER_LOCAL_URL, None)

            if None in (id, accessKey, name, remoteUrl, localUrl):
                log('invalid Emby server received from {}: {}'.format(
                    url, server))
                continue

            servers.append(
                EmbyConnect.Server(id=id,
                                   systemId=systemId,
                                   accessKey=accessKey,
                                   name=name,
                                   remoteUrl=remoteUrl,
                                   localUrl=localUrl))

        return servers
コード例 #8
0
    def BuildSubtitleStreamUrl(self, itemId, sourceId, index, codec):
        if not itemId:
            raise ValueError('invalid itemId')
        if not sourceId:
            raise ValueError('invalid sourceId')
        if not index:
            raise ValueError('invalid index')
        if not codec:
            raise ValueError('invalid codec')

        # <url>/Videos/<itemId>/<sourceId>/Subtitles/<index>/Stream.<codec>?api_key=<token>
        url = Url.append(self._url, constants.URL_VIDEOS, itemId, sourceId, constants.URL_VIDEOS_SUBTITLES, str(index),
                         constants.URL_VIDEOS_SUBTITLES_STREAM)
        url = '{}.{}'.format(url, codec)
        return Url.addOptions(url, {constants.URL_QUERY_API_KEY: self._authenticator.AccessToken()})
コード例 #9
0
def getRawItemsChunked(embyServer, url, mediaType, viewId, startIndex, count):
    viewUrl = url
    viewUrl = Url.addOptions(viewUrl, {emby.constants.URL_QUERY_ITEMS_PARENT_ID: viewId})

    # put together a paged URL
    pagedUrlOptions = {
        emby.constants.URL_QUERY_ITEMS_LIMIT: count,
        emby.constants.URL_QUERY_ITEMS_START_INDEX: startIndex
    }
    pagedUrl = Url.addOptions(viewUrl, pagedUrlOptions)

    # retrieve all items matching the current media type
    resultObj = embyServer.ApiGet(pagedUrl)
    if not resultObj or emby.constants.PROPERTY_ITEM_ITEMS not in resultObj or \
        emby.constants.PROPERTY_ITEM_TOTAL_RECORD_COUNT not in resultObj:
        raise RuntimeError('invalid response for items of media type "{}" from {}'.format(mediaType, pagedUrl))

    # retrieve the total number of items
    totalCount = int(resultObj[emby.constants.PROPERTY_ITEM_TOTAL_RECORD_COUNT])

    # parse all items
    itemsObj = resultObj[emby.constants.PROPERTY_ITEM_ITEMS]

    return (totalCount, itemsObj)
コード例 #10
0
        def GetItems(embyServer, date, filters=None):
            if not embyServer:
                raise ValueError('invalid embyServer')
            if not date:
                raise ValueError('invalid date')

            # determine the endpoint based on whether we are talking to an Emby or Jellyfin server
            endpoint = KodiCompanion.SyncQueue.ENDPOINT_EMBY
            serverInfo = Server.GetInfo(embyServer.Url())
            if serverInfo and serverInfo.isJellyfinServer():
                endpoint = KodiCompanion.SyncQueue.ENDPOINT_JELLYFIN

            # build the URL to retrieve the items from the sync queue
            url = embyServer.BuildUrl(endpoint)
            url = Url.append(url, embyServer.UserId(),
                             KodiCompanion.SyncQueue.ENDPOINT_GET_ITEMS)
            url = Url.addOptions(
                url, {
                    KodiCompanion.SyncQueue.QUERY_GET_ITEMS_LAST_UPDATE: date,
                    KodiCompanion.SyncQueue.QUERY_GET_ITEMS_FILTER: filters
                })

            itemsObj = embyServer.ApiGet(url)
            if not itemsObj:
                return []

            itemsAdded = []
            itemsUpdated = []
            itemsRemoved = []
            userDataChanged = []
            if KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_ADDED in itemsObj:
                itemsAdded = itemsObj[
                    KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_ADDED]
            if KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_UPDATED in itemsObj:
                itemsUpdated = itemsObj[
                    KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_UPDATED]
            if KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_REMOVED in itemsObj:
                itemsRemoved = itemsObj[
                    KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_REMOVED]
            if KodiCompanion.SyncQueue.PROPERTY_GET_ITEMS_USER_DATA_CHANGED in itemsObj:
                userDataChanged = itemsObj[
                    KodiCompanion.SyncQueue.
                    PROPERTY_GET_ITEMS_USER_DATA_CHANGED]

            return KodiCompanion.SyncQueue(itemsAdded=itemsAdded,
                                           itemsUpdated=itemsUpdated,
                                           itemsRemoved=itemsRemoved,
                                           userDataChanged=userDataChanged)
コード例 #11
0
    def MarkAsWatched(embyServer, itemId, lastPlayed):
        if not embyServer:
            raise ValueError('invalid embyServer')
        if not itemId:
            raise ValueError('invalid itemId')

        lastPlayedDate = UserData.PreprocessLastPlayed(lastPlayed)

        url = embyServer.BuildUserPlayedItemUrl(itemId)
        url = Url.addOptions(
            url, {'DatePlayed': lastPlayedDate.strftime('%Y%m%d%H%M%S')})

        if not embyServer.ApiPost(url):
            return False

        return True
コード例 #12
0
    def BuildDirectStreamUrl(self, mediaType, itemId):
        if not itemId:
            raise ValueError('Invalid itemId')

        embyMediaType = None
        if mediaType == 'Video':
            embyMediaType = constants.URL_PLAYBACK_MEDIA_TYPE_VIDEO
        elif mediaType == 'Audio':
            embyMediaType = constants.URL_PLAYBACK_MEDIA_TYPE_AUDIO
        else:
            raise ValueError('Invalid mediaType "{}"'.format(mediaType))

        url = self.BuildUrl(embyMediaType)
        url = Url.append(url, itemId, constants.URL_PLAYBACK_STREAM)

        url = Url.addOptions(url, {
            constants.URL_PLAYBACK_OPTION_STATIC: constants.URL_PLAYBACK_OPTION_STATIC_TRUE,
            constants.URL_QUERY_API_KEY: self.AccessToken()
        })

        return url
コード例 #13
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 kodi.Api.compareMediaProviders(self._mediaProvider,
                                              mediaProvider):
                # update the media provider and settings anyway
                self._mediaProvider = mediaProvider
                self._settings = self._mediaProvider.prepareSettings()
                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')

        try:
            # create emby server instance
            self._server = Server(self._mediaProvider)

            # authenticate with the Emby server
            authenticated = self._server.Authenticate(force=True)
        except:
            authenticated = False

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

        # analyze the media provider's URL
        urlParts = urlparse(self._mediaProvider.getBasePath())
        # determine the proper scheme (ws:// or wss://) and whether or not to verify the HTTPS certificate
        websocketScheme = 'wss' if urlParts.scheme == 'https' else 'ws'
        # put the urL back together
        url = urlunparse(
            urlParts._replace(scheme=websocketScheme, path='embywebsocket'))
        url = Url.addOptions(
            url, {
                URL_QUERY_API_KEY: self._server.AccessToken(),
                URL_QUERY_DEVICE_ID: self._server.DeviceId()
            })

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

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

        # reduce the timeout
        self._websocket.settimeout(1.0)

        ProviderObserver.log(
            'successfully connected to {} to observe media imports'.format(
                mediaProvider2str(self._mediaProvider)))
        self._connected = True
        return True
コード例 #14
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

    log('importing {} items from {}...'.format(mediaTypes, mediaProvider2str(mediaProvider)))

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

    # create an Emby server instance
    embyServer = Server(mediaProvider)
    if not embyServer.Authenticate():
        log('failed to authenticate on media provider {}'.format(mediaProvider2str(mediaProvider)), xbmc.LOGERROR)
        return

    # build the base URL to retrieve items
    baseUrl = embyServer.BuildUserUrl(emby.constants.URL_ITEMS)
    baseUrlOptions = {
        'Recursive': 'true',
        'Fields': ','.join(EMBY_ITEM_FIELDS),
        'ExcludeLocationTypes': 'Virtual,Offline',
        'Limit': ITEM_REQUEST_LIMIT
    }
    baseUrl = Url.addOptions(baseUrl, baseUrlOptions)

    # get all (matching) library views
    selectedViews = getLibraryViewsFromSettings(importSettings)
    views = getMatchingLibraryViews(embyServer, mediaTypes, selectedViews)
    if not views:
        log('cannot retrieve items without any library views', xbmc.LOGERROR)
        return

    # determine whether Direct Play is allowed
    allowDirectPlay = mediaProviderSettings.getBool(emby.constants.SETTING_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY)

    # determine whether to import collections
    importCollections = importSettings.getBool(emby.constants.SETTING_IMPORT_IMPORT_COLLECTIONS)

    # loop over all media types to be imported
    progress = 0
    progressTotal = len(mediaTypes)
    for mediaType in mediaTypes:
        if xbmcmediaimport.shouldCancel(handle, progress, progressTotal):
            return
        progress += 1

        if mediaType == xbmcmediaimport.MediaTypeVideoCollection and not importCollections:
            log('importing {} items from {} is disabled'.format(mediaType, mediaProvider2str(mediaProvider)), xbmc.LOGDEBUG)
            continue

        log('importing {} items from {}...'.format(mediaType, mediaProvider2str(mediaProvider)))

        mappedMediaType = kodi.Api.getEmbyMediaType(mediaType)
        if not mappedMediaType:
            log('cannot import unsupported media type "{}"'.format(mediaType), xbmc.LOGERROR)
            continue
        (_, embyMediaType, localizedMediaType) = mappedMediaType

        xbmcmediaimport.setProgressStatus(handle, __addon__.getLocalizedString(32001).format(__addon__.getLocalizedString(localizedMediaType)))

        urlOptions = {
            'IncludeItemTypes': embyMediaType
        }
        url = Url.addOptions(baseUrl, urlOptions)

        boxsetUrlOptions = {
            'IncludeItemTypes': kodi.EMBY_MEDIATYPE_BOXSET
        }
        boxsetUrl = Url.addOptions(baseUrl, boxsetUrlOptions)

        items = []
        boxsets = {}

        # handle library views
        for view in views:
            log('importing {} items from "{}" view from {}...'.format(mediaType, view.name, mediaProvider2str(mediaProvider)))
            items.extend(importItems(handle, embyServer, url, mediaType, view.id, embyMediaType=embyMediaType, viewName=view.name, allowDirectPlay=allowDirectPlay))

            if importCollections and items and mediaType == xbmcmediaimport.MediaTypeMovie:
                # retrieve all BoxSets / collections matching the current media type
                boxsetObjs = importItems(handle, embyServer, boxsetUrl, mediaType, view.id, raw=True, allowDirectPlay=allowDirectPlay)
                for boxsetObj in boxsetObjs:
                    if not emby.constants.PROPERTY_ITEM_ID in boxsetObj or not emby.constants.PROPERTY_ITEM_NAME in boxsetObj:
                        continue

                    boxsetId = boxsetObj[emby.constants.PROPERTY_ITEM_ID]
                    boxsetName = boxsetObj[emby.constants.PROPERTY_ITEM_NAME]
                    boxsets[boxsetId] = boxsetName

        # handle BoxSets / collections
        if importCollections and items:
            for (boxsetId, boxsetName) in iteritems(boxsets):
                # get all items belonging to the BoxSet
                boxsetItems = importItems(handle, embyServer, url, mediaType, boxsetId, embyMediaType=embyMediaType, viewName=boxsetName, allowDirectPlay=allowDirectPlay)
                for boxsetItem in boxsetItems:
                    # find the matching retrieved item
                    for (index, item) in enumerate(items):
                        if boxsetItem.getPath() == item.getPath():
                            # set the BoxSet / collection
                            kodi.Api.setCollection(item, boxsetName)
                            items[index] = item

        log('{} {} items imported from {}'.format(len(items), mediaType, mediaProvider2str(mediaProvider)))
        if items:
            xbmcmediaimport.addImportItems(handle, items, mediaType)

    xbmcmediaimport.finishImport(handle)
コード例 #15
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

    log('importing {} items from {}...'.format(
        mediaTypes, mediaProvider2str(mediaProvider)))

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

    # create an Emby server instance
    embyServer = Server(mediaProvider)

    # build the base URL to retrieve items
    baseUrl = embyServer.BuildUserUrl(emby.constants.URL_ITEMS)
    baseUrlOptions = {
        emby.constants.URL_QUERY_ITEMS_RECURSIVE: 'true',
        emby.constants.URL_QUERY_ITEMS_FIELDS: ','.join(EMBY_ITEM_FIELDS),
        emby.constants.URL_QUERY_ITEMS_EXCLUDE_LOCATION_TYPES:
        'Virtual,Offline',
        emby.constants.URL_QUERY_ITEMS_LIMIT: ITEM_REQUEST_LIMIT
    }
    baseUrl = Url.addOptions(baseUrl, baseUrlOptions)

    # get all (matching) library views
    selectedViews = ImportSettings.GetLibraryViews(importSettings)
    views = getMatchingLibraryViews(embyServer, mediaTypes, selectedViews)
    if not views:
        log('cannot retrieve items without any library views', xbmc.LOGERROR)
        return

    # determine whether Direct Play is allowed
    allowDirectPlay = mediaProviderSettings.getBool(
        emby.constants.SETTING_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY)

    # determine whether to import collections
    importCollections = importSettings.getBool(
        emby.constants.SETTING_IMPORT_IMPORT_COLLECTIONS)

    # determine the last sync time and whether we should perform a fast sync
    fastSync = False
    syncUrlOptions = {}

    # check if synchronization related settings have changed; if yes we have to perform a full synchronization
    if SynchronizationSettings.HaveChanged(mediaTypes,
                                           mediaProviderSettings,
                                           importSettings,
                                           save=True):
        log('forcing a full synchronization to import {} items from {} because some related settings have changed' \
            .format(mediaTypes, mediaProvider2str(mediaProvider)))
    else:
        # check if we
        #   have already performed a (full) synchronization before
        #   should use the Kodi Companion Emby server plugin
        lastSync = mediaImport.getLastSynced()
        if lastSync and mediaProviderSettings.getBool(
                emby.constants.
                SETTING_PROVIDER_SYNCHRONIZATION_USE_KODI_COMPANION):
            if KodiCompanion.IsInstalled(embyServer):
                fastSync = True

                # convert the last sync datetime string to ISO 8601
                lastSync = parser.parse(lastSync).astimezone(utc).isoformat(
                    timespec='seconds')

                syncUrlOptions.update({
                    # only set MinDateLastSavedForUser because it already covers DateLastSaved, RatingLastModified
                    # and PlaystateLastModified. Setting both MinDateLastSaved and MinDateLastSavedForUser will
                    # cause issues, see https://emby.media/community/index.php?/topic/82258-retrieving-changeset-when-client-returns-online-mediaimport/
                    emby.constants.URL_QUERY_ITEMS_MIN_DATE_LAST_SAVED_FOR_USER:
                    lastSync
                })
                log('using fast synchronization to import {} items from {} with Kodi companion plugin' \
                    .format(mediaTypes, mediaProvider2str(mediaProvider)), xbmc.LOGDEBUG)

                # retrieving the sync queue from Kodi companion
                syncQueue = KodiCompanion.SyncQueue.GetItems(
                    embyServer, lastSync)
            else:
                log('Kodi companion usage is enabled to import {} items from {} but the server plugin is not installed' \
                    .format(mediaTypes, mediaProvider2str(mediaProvider)), xbmc.LOGWARNING)

    # loop over all media types to be imported
    progress = 0
    progressTotal = len(mediaTypes)
    for mediaType in mediaTypes:
        if xbmcmediaimport.shouldCancel(handle, progress, progressTotal):
            return
        progress += 1

        if mediaType == xbmcmediaimport.MediaTypeVideoCollection and not importCollections:
            log(
                'importing {} items from {} is disabled'.format(
                    mediaType, mediaProvider2str(mediaProvider)),
                xbmc.LOGDEBUG)
            continue

        log('importing {} items from {}...'.format(
            mediaType, mediaProvider2str(mediaProvider)))

        mappedMediaType = kodi.Api.getEmbyMediaType(mediaType)
        if not mappedMediaType:
            log('cannot import unsupported media type "{}"'.format(mediaType),
                xbmc.LOGERROR)
            continue
        (_, embyMediaType, localizedMediaType) = mappedMediaType

        xbmcmediaimport.setProgressStatus(
            handle,
            __addon__.getLocalizedString(32001).format(
                __addon__.getLocalizedString(localizedMediaType)))

        urlOptions = syncUrlOptions.copy()
        urlOptions.update(
            {emby.constants.URL_QUERY_ITEMS_INCLUDE_ITEM_TYPES: embyMediaType})
        url = Url.addOptions(baseUrl, urlOptions)

        boxsetUrlOptions = {
            emby.constants.URL_QUERY_ITEMS_INCLUDE_ITEM_TYPES:
            kodi.EMBY_MEDIATYPE_BOXSET
        }
        boxsetUrl = Url.addOptions(baseUrl, boxsetUrlOptions)

        items = []
        boxsets = {}

        # handle library views
        for view in views:
            log('importing {} items from "{}" view from {}...'.format(
                mediaType, view.name, mediaProvider2str(mediaProvider)))
            items.extend(
                importItems(handle,
                            embyServer,
                            url,
                            mediaType,
                            view.id,
                            embyMediaType=embyMediaType,
                            viewName=view.name,
                            allowDirectPlay=allowDirectPlay))

            if importCollections and items and mediaType == xbmcmediaimport.MediaTypeMovie:
                # retrieve all BoxSets / collections matching the current media type
                boxsetObjs = importItems(handle,
                                         embyServer,
                                         boxsetUrl,
                                         mediaType,
                                         view.id,
                                         raw=True,
                                         allowDirectPlay=allowDirectPlay)
                for boxsetObj in boxsetObjs:
                    if not emby.constants.PROPERTY_ITEM_ID in boxsetObj or not emby.constants.PROPERTY_ITEM_NAME in boxsetObj:
                        continue

                    boxsetId = boxsetObj[emby.constants.PROPERTY_ITEM_ID]
                    boxsetName = boxsetObj[emby.constants.PROPERTY_ITEM_NAME]
                    boxsets[boxsetId] = boxsetName

        # handle BoxSets / collections
        if importCollections and items:
            for (boxsetId, boxsetName) in iteritems(boxsets):
                # get all items belonging to the BoxSet
                boxsetItems = importItems(handle,
                                          embyServer,
                                          url,
                                          mediaType,
                                          boxsetId,
                                          embyMediaType=embyMediaType,
                                          viewName=boxsetName,
                                          allowDirectPlay=allowDirectPlay)
                for boxsetItem in boxsetItems:
                    # find the matching retrieved item
                    for index, item in enumerate(items):
                        if boxsetItem.getPath() == item.getPath():
                            # set the BoxSet / collection
                            kodi.Api.setCollection(item, boxsetName)
                            items[index] = item

        # in a fast sync we need to get the removed items from Kodi companion
        if fastSync:
            if items:
                log('{} changed {} items imported from {}'.format(
                    len(items), mediaType, mediaProvider2str(mediaProvider)))

            # handle removed items through Kodi companion's sync queue
            if syncQueue.itemsRemoved:
                # retrieve all local items matching the current media type from the current import
                localItems = xbmcmediaimport.getImportedItems(
                    handle, mediaType)

                # match the local items against the changed items
                removedItems, = kodi.Api.matchImportedItemIdsToLocalItems(
                    localItems, syncQueue.itemsRemoved)  # pylint: disable=unbalanced-tuple-unpacking

                # erase all removed items matching the current media type from the sync queue
                syncQueue.itemsRemoved = [
                    removedItem for removedItem in syncQueue.itemsRemoved
                    if removedItem in removedItems
                ]

                if removedItems:
                    log('{} previously imported {} items removed from {}'.
                        format(len(removedItems), mediaType,
                               mediaProvider2str(mediaProvider)))
                    xbmcmediaimport.addImportItems(
                        handle, removedItems, mediaType,
                        xbmcmediaimport.MediaImportChangesetTypeRemoved)
        else:
            log('{} {} items imported from {}'.format(
                len(items), mediaType, mediaProvider2str(mediaProvider)))

        # pass the imported items back to Kodi
        if items:
            xbmcmediaimport.addImportItems(handle, items, mediaType)

    xbmcmediaimport.finishImport(handle, fastSync)