def channel(channel_id: str): """Load stream for the required channel id""" log('Loading channel {}'.format(channel_id), LogLevel.INFO) stream = get_provider().get_stream_info(channel_id) if not stream: ok_dialog(localize(30900)) return is_helper = inputstreamhelper.Helper(stream['manifest_type'], drm=stream['drm']) if not is_helper.check_inputstream(): ok_dialog(localize(30901)) return listitem = xbmcgui.ListItem(path=stream['path']) listitem.setMimeType(stream['mime_type']) listitem.setProperty('inputstream', 'inputstream.adaptive') listitem.setProperty('inputstream.adaptive.manifest_type', stream['manifest_type']) listitem.setProperty('inputstream.adaptive.manifest_update_parameter', 'full') listitem.setProperty('inputstream.adaptive.license_type', stream['license_type']) listitem.setProperty('inputstream.adaptive.license_key', stream['license_key']) xbmcplugin.setResolvedUrl(plugin.handle, True, listitem=listitem)
def discoverProvider(handle: int, options: dict): """Prompt user for Plex authentication type, perform server discovery based on their choice, register the provider :param handle: Handle id from input :type handle: int :param options: Options/parameters passed in with the call :type options: dict """ dialog = xbmcgui.Dialog() authenticationChoices = [ localize(32013), # local only localize(32014) # MyPlex ] authenticationChoice = dialog.select(localize(32050), authenticationChoices) if authenticationChoice == 0: # local only provider = discoverProviderLocally(handle, options) elif authenticationChoice == 1: # MyPlex provider = discoverProviderWithMyPlex(handle, options) else: return if not provider: return xbmcmediaimport.setDiscoveredProvider(handle, True, provider)
def forceSync(handle: int, _options: dict): """Confirm if user wants to force a full sync and update the settings hash :param handle: Handle id from input :type handle: int :param _options: Options/parameters passed in with the call, Unused :type _options: dict """ # ask the user whether he is sure force = xbmcgui.Dialog().yesno(localize(32022), localize(32065)) if not force: 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 importSettings = mediaImport.prepareSettings() if not importSettings: log('cannot prepare media import settings', xbmc.LOGERROR) return # reset the synchronization hash setting to force a full synchronization SynchronizationSettings.ResetHash(importSettings, save=False)
def linkMyPlexAccount(handle: int, _options: dict): """Have user sign into MyPlex account, find servers on the account, and save authenticatino details for the server :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 # get the media provider settings providerSettings = mediaProvider.prepareSettings() if not providerSettings: return plexAccount = linkToMyPlexAccount() if not plexAccount: return username = plexAccount.username if not username: log('no valid username available for the linked MyPlex account', xbmc.LOGWARNING) return # make sure the configured Plex Media Server is still accessible serverUrl = mediaProvider.getBasePath() matchingServer = None serverId = getServerId(mediaProvider.getIdentifier()) # get all connected server resources serverResources = getServerResources(plexAccount) for server in serverResources: if server.clientIdentifier == serverId: matchingServer = server break if not matchingServer: log(f"no Plex Media Server matching {serverUrl} found", xbmc.LOGWARNING) xbmcgui.Dialog().ok(localize(32015), localize(32058)) return xbmcgui.Dialog().ok(localize(32015), localize(32059, username)) # change the settings providerSettings.setString(plex.constants.SETTINGS_PROVIDER_USERNAME, username) providerSettings.setString(plex.constants.SETTINGS_PROVIDER_TOKEN, matchingServer.accessToken)
def changeUrl(handle: int, _options: dict): """Change the URL of PMS :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 # get the media provider settings providerSettings = mediaProvider.prepareSettings() if not providerSettings: log("cannot prepare provider settings", xbmc.LOGERROR) return urlCurrent = ProviderSettings.GetUrl(providerSettings) if not urlCurrent: log("cannot retrieve current URL from provider settings", xbmc.LOGERROR) return # ask the user for a new URL urlNew = xbmcgui.Dialog().input(localize(32068), urlCurrent) if not urlNew: return # store the new URL in the settings ProviderSettings.SetUrl(providerSettings, urlNew) # try to connect and authenticate with the new URL success = False try: success = Server(mediaProvider).Authenticate() except: pass dialog = xbmcgui.Dialog() title = mediaProvider.getFriendlyName() if success: dialog.ok(title, localize(32018)) else: # ask the user whether to change the URL anyway changeUrlAnyway = dialog.yesno(title, localize(32069)) if not changeUrlAnyway: # revert the settings to the previous / old URL ProviderSettings.SetUrl(providerSettings, urlCurrent)
def testConnection(handle: int, _options: dict): """Test connection to the user provided PMS and display results to user :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 success = False try: success = Server(mediaProvider).Authenticate() except: pass title = mediaProvider.getFriendlyName() line = 32019 if success: line = 32018 xbmcgui.Dialog().ok(title, localize(line))
def __init__(self, *args, label, **kwargs): if not label: raise ValueError('invalid label') super(WindowXMLDialog, self).__init__(*args, **kwargs) if not isinstance(label, str): label = localize(label) self.setProperty('skiplabel', label) self._isOpen = False self._skipConfirmed = False
def discoverProviderLocally(handle: int, _options: dict) -> xbmcmediaimport.MediaProvider: """Set up a Plex server provider from user-provided URL :param handle: Handle id from input :type handle: int :param _options: Options/parameters passed in with the call, unused :type _options: dict :return: Fully setup and populated mediaProvider object for the PMS :rtype: xbmcmediaimport.MediaProvider """ dialog = xbmcgui.Dialog() baseUrl = dialog.input(localize(32051)) if not baseUrl: return None plexServer = PlexServer(baseUrl, timeout=plex.constants.REQUEST_TIMEOUT) if not plexServer: return None providerId = Server.BuildProviderId(plexServer.machineIdentifier) providerIconUrl = Server.BuildIconUrl(baseUrl) provider = xbmcmediaimport.MediaProvider( identifier=providerId, basePath=baseUrl, friendlyName=plexServer.friendlyName, iconUrl=providerIconUrl, mediaTypes=plex.constants.SUPPORTED_MEDIA_TYPES, handle=handle) # store local authentication in settings providerSettings = provider.prepareSettings() if not providerSettings: return None providerSettings.setInt( plex.constants.SETTINGS_PROVIDER_AUTHENTICATION, plex.constants.SETTINGS_PROVIDER_AUTHENTICATION_OPTION_LOCAL) providerSettings.save() return provider
def linkToMyPlexAccount() -> MyPlexAccount: """Log into MyPlex with user-provided settings and get authentication token :return: Returns authenticated MyPlexAccount object :rtype: :class:`MyPlexAccount` """ dialog = xbmcgui.Dialog() pinLogin = MyPlexPinLogin() if not pinLogin.pin: dialog.ok(localize(32015), localize(32052)) log('failed to get PIN to link MyPlex account', xbmc.LOGWARNING) return None # show the user the pin dialog.ok( localize(32015), localize(32053) + normalizeString(f" [COLOR FFE5A00D]{pinLogin.pin}[/COLOR]")) # check the status of the authentication while not pinLogin.finished: if pinLogin.checkLogin(): break if pinLogin.expired: dialog.ok(localize(32015), localize(32054)) log("linking the MyPlex account has expiried", xbmc.LOGWARNING) return None if not pinLogin.token: log("no valid token received from the linked MyPlex account", xbmc.LOGWARNING) return None # login to MyPlex try: plexAccount = MyPlexAccount(token=pinLogin.token, timeout=plex.constants.REQUEST_TIMEOUT) except Exception as e: log(f"failed to connect to the linked MyPlex account: {e}", xbmc.LOGWARNING) return None if not plexAccount: log("failed to connect to the linked MyPlex account", xbmc.LOGWARNING) return None return plexAccount
def index(): """Addon index""" ok_dialog(localize(30902))
def play(item, itemId, mediaProvider): if item.isFolder(): contextLog(f"cannot play folder item {listItem2str(item, itemId)}", xbmc.LOGERROR, entry='play') return # create a Plex server instance server = Server(mediaProvider) if not server.Authenticate(): contextLog( f"failed to connect to Plex Media Server for {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='sync') return plexItemClass = Api.getPlexMediaClassFromListItem(item) # cannot play folders if plexItemClass in (collection.Collection, video.Show, video.Season): contextLog(f"cannot play folder item {listItem2str(item, itemId)}", xbmc.LOGERROR, entry='play') return # get the Plex item with all its details plexItem = Api.getPlexItemDetails(server.PlexServer(), itemId, plexItemClass=plexItemClass) if not plexItem: contextLog( f"failed to determine Plex item for {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGWARNING, entry='refresh') return # cannot play folders if not Api.canPlay(plexItem): contextLog(f"cannot play item {listItem2str(item, itemId)}", xbmc.LOGERROR, entry='play') return playChoices = [] playChoicesUrl = [] # determine whether Direct Play is allowed mediaProviderSettings = mediaProvider.getSettings() allowDirectPlay = mediaProviderSettings.getBool(SETTINGS_PROVIDER_PLAYBACK_ALLOW_DIRECT_PLAY) # check if the item supports Direct Play if allowDirectPlay: directPlayUrl = Api.getDirectPlayUrlFromPlexItem(plexItem) if directPlayUrl: playChoices.append(localize(32103)) playChoicesUrl.append(directPlayUrl) # check if the item supports streaming directStreamUrl = Api.getStreamUrlFromPlexItem(plexItem, server.PlexServer()) if directStreamUrl: playChoices.append(localize(32104)) playChoicesUrl.append(directStreamUrl) # check if the item has multiple versions multipleVersions = [] if len(plexItem.media) > 1: for mediaStream in plexItem.media: url = None if allowDirectPlay: directPlayUrl = Api.getDirectPlayUrlFromMedia(mediaStream) if directPlayUrl: url = directPlayUrl if not url: url = Api.getStreamUrlFromMedia(mediaStream, server.PlexServer()) # get the display title of the first videostream for mediaPart in mediaStream.parts: # get all video streams videoStreams = (stream for stream in mediaPart.streams if isinstance(stream, media.VideoStream)) # extract the first non-empty display resolution displayResolution = next( ( stream.displayTitle or stream.extendedDisplayTitle for stream in videoStreams if stream.displayTitle or stream.extendedDisplayTitle ), None) if displayResolution: break # fall back to the basic video resolution of the stream if not displayResolution: displayResolution = mediaStream.videoResolution multipleVersions.append((url, mediaStream.bitrate, displayResolution)) if len(multipleVersions) > 1: playChoices.append(localize(32105)) playChoicesUrl.append(PLAY_MULTIPLE_VERSIONS_KEY) # if there are no options something went wrong if not playChoices: contextLog( f"cannot play {listItem2str(item, itemId)} from {mediaProvider2str(mediaProvider)}", xbmc.LOGERROR, entry='play') return # ask the user how to play playChoice = Dialog().contextmenu(playChoices) if playChoice < 0 or playChoice >= len(playChoices): return playUrl = playChoicesUrl[playChoice] # check if the user chose to choose which version to play if playUrl == PLAY_MULTIPLE_VERSIONS_KEY: playChoices.clear() playChoicesUrl.clear() # sort the available versions by bitrate (second field) multipleVersions.sort(key=lambda version: version[1], reverse=True) for version in multipleVersions: playChoices.append( localize(32106, bitrate=bitrate2str(version[1]), resolution=version[2])) playChoicesUrl.append(version[0]) # ask the user which version to play playChoice = Dialog().contextmenu(playChoices) if playChoice < 0 or playChoice >= len(playChoices): return playUrl = playChoicesUrl[playChoice] # play the item contextLog( ( f'playing {listItem2str(item, itemId)} using "{playChoices[playChoice]}" ({playUrl}) ' f'from {mediaProvider2str(mediaProvider)}' ), entry='play') # overwrite the dynamic path of the ListItem item.setDynamicPath(playUrl) xbmc.Player().play(playUrl, item)
def exec_import(handle, options): # parse all necessary options media_types = media_types_from_options(options) if not media_types: log('cannot execute "import" without media types', xbmc.LOGERROR) return # retrieve the media import media_import = xbmcmediaimport.getImport(handle) if not media_import: log("cannot retrieve media import", xbmc.LOGERROR) return # prepare and get the media import settings import_settings = media_import.prepareSettings() if not import_settings: log("cannot prepare media import settings", xbmc.LOGERROR) return # retrieve the media provider media_provider = media_import.getProvider() if not media_provider: log("cannot retrieve media provider", xbmc.LOGERROR) return log(f"importing {media_types} items from { provider2str(media_provider)}..." ) # TODO(stub): prepare collecting ListItems # loop over all media types to be imported progress = 0 progress_total = len(media_types) for media_type in media_types: # check if we need to cancel importing items if xbmcmediaimport.shouldCancel(handle, progress, progress_total): return progress += 1 log(f"importing {media_type} items from {provider2str(media_provider)}..." ) # report the progress status xbmcmediaimport.setProgressStatus(handle, localize(32001).format(media_type)) items = [] # TODO(stub): collect ListItems to import # adjust and use lib.kodi.Api.to_file_item() if items: # pass the imported items back to Kodi log(f"{len(items)} {media_type} items imported from {provider2str(media_provider)}" ) # TODO(stub): for partial imports use the following constants as an optional fourth argument: # xbmcmediaimport.MediaImportChangesetTypeNone: let Kodi decide # xbmcmediaimport.MediaImportChangesetTypeAdded: the item is new and should be added # xbmcmediaimport.MediaImportChangesetTypeChanged: the item has been imported before and has changed # xbmcmediaimport.MediaImportChangesetTypeRemoved: the item has to be removed xbmcmediaimport.addImportItems(handle, items, media_type) # TODO(stub): tell Kodi whether the provided items is a full or partial import partial_import = False # finish the import xbmcmediaimport.finishImport(handle, partial_import)
def execImport(handle: int, options: dict): """Perform library update/import of all configured items from a configured PMS into Kodi :param handle: Handle id from input :type handle: int :param options: Options/parameters passed in with the call, required mediatypes or mediatypes[] :type options: dict """ if 'path' not in options: log("cannot execute 'import' without path", xbmc.LOGERROR) return # parse all necessary options mediaTypes = mediaTypesFromOptions(options) if not mediaTypes: log("cannot execute 'import' without media types", xbmc.LOGERROR) return # retrieve the media import mediaImport = xbmcmediaimport.getImport(handle) if not mediaImport: log("cannot retrieve media import", xbmc.LOGERROR) return # prepare and get the media import settings importSettings = mediaImport.prepareSettings() if not importSettings: log("cannot prepare media import settings", xbmc.LOGERROR) return # retrieve the media provider mediaProvider = mediaImport.getProvider() if not mediaProvider: log("cannot retrieve media provider", xbmc.LOGERROR) return # prepare and get the media provider settings providerSettings = mediaProvider.prepareSettings() if not providerSettings: log("cannot prepare provider settings", xbmc.LOGERROR) return # create a Plex Media Server instance server = Server(mediaProvider) plexServer = server.PlexServer() plexLibrary = plexServer.library # get all (matching) library sections selectedLibrarySections = getLibrarySectionsFromSettings(importSettings) librarySections = getMatchingLibrarySections(plexServer, mediaTypes, selectedLibrarySections) if not librarySections: log(f"cannot retrieve {mediaTypes} items without any library section", xbmc.LOGERROR) return # Decide if doing fast sync or not, if so set filter string to include updatedAt fastSync = True lastSync = mediaImport.getLastSynced() # Check if import settings have changed, or if this is the first time we are importing this library type if not lastSync: fastSync = False SynchronizationSettings.CalculateHash( importSettings=importSettings, providerSettings=providerSettings, save=True) log("first time syncronizing library, forcing a full syncronization", xbmc.LOGINFO) elif SynchronizationSettings.HaveChanged(importSettings=importSettings, providerSettings=providerSettings, save=True): fastSync = False log( "library import settings have changed, forcing a full syncronization", xbmc.LOGINFO) if SynchronizationSettings.HaveChanged(importSettings=importSettings, providerSettings=providerSettings, save=True): fastSync = False log( "library import settings have changed, forcing a full syncronization", xbmc.LOGINFO) if fastSync: log(f"performing fast syncronization of items viewed or updated since {str(lastSync)}" ) lastSyncEpoch = parser.parse(lastSync).strftime('%s') updatedFilter = {'updatedAt>': lastSyncEpoch} watchedFilter = {'lastViewedAt>': lastSyncEpoch} # loop over all media types to be imported progressTotal = len(mediaTypes) for progress, mediaType in enumerate(mediaTypes): if xbmcmediaimport.shouldCancel(handle, progress, progressTotal): return mappedMediaType = Api.getPlexMediaType(mediaType) if not mappedMediaType: log(f"cannot import unsupported media type '{mediaType}'", xbmc.LOGERROR) continue plexLibType = mappedMediaType['libtype'] localizedMediaType = localize(mappedMediaType['label']).decode() xbmcmediaimport.setProgressStatus(handle, localize(32001, localizedMediaType)) log( f"importing {mediaType} items from {mediaProvider2str(mediaProvider)}", xbmc.LOGINFO) # handle library sections itemsToImport = [] sectionsProgressTotal = len(librarySections) for sectionsProgress, librarySection in enumerate(librarySections): if xbmcmediaimport.shouldCancel(handle, sectionsProgress, sectionsProgressTotal): return # get the library section from the Plex Media Server section = plexLibrary.sectionByID(librarySection['key']) if not section: log( f"cannot import {mediaType} items from unknown library section {librarySection}", xbmc.LOGWARNING) continue # get all matching items from the library section and turn them into ListItems sectionProgress = 0 sectionProgressTotal = ITEM_REQUEST_LIMIT while sectionProgress < sectionProgressTotal: if xbmcmediaimport.shouldCancel(handle, sectionProgress, sectionProgressTotal): return maxResults = min(ITEM_REQUEST_LIMIT, sectionProgressTotal - sectionProgress) try: if fastSync: updatedPlexItems = section.search( libtype=plexLibType, container_start=sectionProgress, container_size=maxResults, maxresults=maxResults, **updatedFilter) log(f"discovered {len(updatedPlexItems)} updated items from {mediaProvider2str(mediaProvider)}" ) watchedPlexItems = section.search( libtype=plexLibType, container_start=sectionProgress, container_size=maxResults, maxresults=maxResults, **watchedFilter) log(f"discovered {len(watchedPlexItems)} new watched items from {mediaProvider2str(mediaProvider)}" ) plexItems = updatedPlexItems plexItems.extend([ item for item in watchedPlexItems if item.key not in [item.key for item in plexItems] ]) else: plexItems = section.search( libtype=plexLibType, container_start=sectionProgress, container_size=maxResults, maxresults=maxResults, ) except plexapi.exceptions.BadRequest as e: log( f"failed to fetch {mediaType} items from {mediaProvider2str(mediaProvider)}: {e}", xbmc.LOGINFO) return # Update sectionProgressTotal now that search has run and totalSize has been updated sectionProgressTotal = section.totalSize plexItemsProgressTotal = len(plexItems) for plexItemsProgress, plexItem in enumerate(plexItems): if xbmcmediaimport.shouldCancel(handle, plexItemsProgress, plexItemsProgressTotal): return sectionProgress += 1 try: item = Api.toFileItem(plexServer, plexItem, mediaType, plexLibType) if not item: continue itemsToImport.append(item) except plexapi.exceptions.BadRequest as e: # Api.convertDateTimeToDbDateTime may return (404) not_found for orphaned items in the library log(( f"failed to retrieve item {plexItem.title} with key {plexItem.key} " f"from {mediaProvider2str(mediaProvider)}: {e}"), xbmc.LOGWARNING) continue if itemsToImport: log( f"{len(itemsToImport)} {mediaType} items imported from {mediaProvider2str(mediaProvider)}", xbmc.LOGINFO) xbmcmediaimport.addImportItems(handle, itemsToImport, mediaType) xbmcmediaimport.finishImport(handle, fastSync)
def discoverProviderWithMyPlex( handle: int, _options: dict) -> xbmcmediaimport.MediaProvider: """ Prompts user to sign into their Plex account using the MyPlex pin link Finds a list of all servers connected to the account and prompts user to pick one Prompt for a local discovery if no servers found within their Plex account Setup and store the Plex server as a MediaProvider and store MyPlex auth information :param handle: Handle id from input :type handle: int :param _options: Options/parameters passed in with the call, unused :type _options: dict :return: Fully configured and authenticated Plex media provider :rtype: :class:`xbmcmediaimport.MediaProvider` """ plexAccount = linkToMyPlexAccount() if not plexAccount: return None username = plexAccount.username if not username: log("no valid username available for the linked MyPlex account", xbmc.LOGWARNING) return None dialog = xbmcgui.Dialog() # get all connected server resources serverResources = getServerResources(plexAccount) if not serverResources: log(f"no servers available for MyPlex account {username}", xbmc.LOGWARNING) return None if len(serverResources) == 1: server = serverResources[0] else: # ask the user which server to use servers = [resource.name for resource in serverResources] serversChoice = dialog.select(localize(32055), servers) if serversChoice < 0 or serversChoice >= len(servers): return None server = serverResources[serversChoice] if not server: return None if not server.connections: # try to connect to the server plexServer = server.connect(timeout=plex.constants.REQUEST_TIMEOUT) if not plexServer: log(f"failed to connect to the Plex Media Server '{server.name}'", xbmc.LOGWARNING) return None baseUrl = plexServer.url('', includeToken=False) else: isLocal = False localConnections = [ connection for connection in server.connections if connection.local ] remoteConnections = [ connection for connection in server.connections if not connection.local and not connection.relay ] remoteRelayConnections = [ connection for connection in server.connections if not connection.local and connection.relay ] if localConnections: # ask the user whether to use a local or remote connection isLocal = dialog.yesno(localize(32056), localize(32057, server.name)) urls = [] if isLocal: urls.append((localConnections[0].httpuri, False)) else: urls.extend([(conn.uri, False) for conn in remoteConnections]) urls.extend([(conn.uri, True) for conn in remoteRelayConnections]) urls.extend([(conn.uri, False) for conn in localConnections]) baseUrl = None connectViaRelay = True # find a working connection / base URL for (url, isRelay) in urls: try: # don't try to connect via relay if the user has already declined it before if isRelay and not connectViaRelay: log( f"ignoring relay connection to the Plex Media Server '{server.name}' at {url}", xbmc.LOGDEBUG) continue # try to connect to the server _ = PlexServer(baseurl=url, token=server.accessToken, timeout=plex.constants.REQUEST_TIMEOUT) # if this is a relay ask the user if using it is ok if isRelay: connectViaRelay = dialog.yesno( localize(32056), localize(32061, server.name)) if not connectViaRelay: log( f"ignoring relay connection to the Plex Media Server '{server.name}' at {url}", xbmc.LOGDEBUG) continue baseUrl = url break except: log(f"failed to connect to '{server.name}' at {url}", xbmc.LOGDEBUG) continue if not baseUrl: dialog.ok(localize(32056), localize(32060, server.name)) log( f"failed to connect to the Plex Media Server '{server.name}' for MyPlex account {username}", xbmc.LOGWARNING) return None if not baseUrl: log( f"failed to find the URL to access the Plex Media Server '{server.name}' for MyPlex account {username}", xbmc.LOGWARNING) return None log( f"successfully connected to Plex Media Server '{server.name}' for MyPlex account {username} at {baseUrl}", xbmc.LOGINFO) providerId = plex.server.Server.BuildProviderId(server.clientIdentifier) providerIconUrl = plex.server.Server.BuildIconUrl(baseUrl) provider = xbmcmediaimport.MediaProvider( providerId, baseUrl, server.name, providerIconUrl, plex.constants.SUPPORTED_MEDIA_TYPES, handle=handle) # store MyPlex account details and token in settings providerSettings = provider.prepareSettings() if not providerSettings: return None providerSettings.setInt( plex.constants.SETTINGS_PROVIDER_AUTHENTICATION, plex.constants.SETTINGS_PROVIDER_AUTHENTICATION_OPTION_MYPLEX) providerSettings.setString(plex.constants.SETTINGS_PROVIDER_USERNAME, username) providerSettings.setString(plex.constants.SETTINGS_PROVIDER_TOKEN, server.accessToken) providerSettings.save() return provider
import xbmc # pylint: disable=import-error import xbmcmediaimport # pylint: disable=import-error from plexapi.server import PlexServer from plex.api import Api from plex.constants import ( PLEX_PROTOCOL, PLEX_PLAYER_PLAYING, PLEX_PLAYER_PAUSED, PLEX_PLAYER_STOPPED, SETTINGS_PROVIDER_PLAYBACK_ENABLE_EXTERNAL_SUBTITLES) from plex.server import Server from lib.utils import log, mediaProvider2str, toMilliseconds, localize REPORTING_INTERVAL = 5 # seconds SUBTITLE_UNKNOWN = localize(32064).decode() class Player(xbmc.Player): """Class with customization of the xmbc Player class to handle working with Plex""" def __init__(self): """Initializes the player""" super(Player, self).__init__() self._providers = {} self._lock = threading.Lock() self._state = {'playbacktime': 0, 'state': None, 'lastreport': 0} self._duration = None self._file = None
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)