def _addExternalSubtitles(self, plexServer: PlexServer): """Add external subtitles to the player :param plexServer: Plex server to get subtitles from :type plexServer: :class:`PlexServer` """ if not self._item: return # note: internal subtitles don't have a key provided by plexapi external_subtitles = [ sub for sub in self._item.subtitleStreams() if sub.key ] if external_subtitles: for subtitle in external_subtitles: # TODO: What to do with forced subs? self.addSubtitle( plexServer.url(subtitle.key, includeToken=True), subtitle.title if subtitle.title else SUBTITLE_UNKNOWN, subtitle.language if subtitle.language else SUBTITLE_UNKNOWN, subtitle.selected) log(( f"external subtitle '{subtitle.title}' [{subtitle.language}]" f"at index {subtitle.index} added for '{self._item.title}' ({self._file})" f"from media provider {mediaProvider2str(self._mediaProvider)}" ), xbmc.LOGINFO)
def getStreamUrl(mediaPart: media.MediaPart, plexServer: server.PlexServer) -> str: if not mediaPart: raise ValueError('invalid mediaPart') if not plexServer: raise ValueError('invalid plexServer') return plexServer.url(mediaPart.key, includeToken=True)
class PlexBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): super(PlexBackend, self).__init__(audio=audio) self.config = config self.session = get_requests_session( proxy_config=config['proxy'], user_agent='%s/%s' % (mopidy_plex.Extension.dist_name, mopidy_plex.__version__)) baseurl = (config['plex']['server']) token = (config['plex']['token']) self.plex = PlexServer(baseurl, token) self.music = [ s for s in self.plex.library.sections() if s.TYPE == MusicSection.TYPE ][0] logger.debug('Found music section on plex server %s: %s', self.plex, self.music) self.library_id = config['plex']['library_id'] self.uri_schemes = [ 'plex', ] self.library = PlexLibraryProvider(backend=self) self.playback = PlexPlaybackProvider(audio=audio, backend=self) def plex_uri(self, uri_path, prefix='plex'): '''Get a leaf uri and complete it to a mopidy plex uri. E.g. plex:artist:3434 plex:track:2323 plex:album:2323 plex:playlist:3432 ''' uri_path = str(uri_path) if not uri_path.startswith('/library/metadata/'): uri_path = '/library/metadata/' + uri_path if uri_path.startswith('/library/metadata/'): uri_path = uri_path[len('/library/metadata/'):] return '{}:{}'.format(prefix, uri_path) def resolve_uri(self, uri_path): '''Get a leaf uri and return full address to plex server''' uri_path = str(uri_path) if not uri_path.startswith('/library/metadata/'): uri_path = '/library/metadata/' + uri_path return self.plex.url(uri_path)
class Plex(GObject.Object): __gsignals__ = { 'login-status': (GObject.SignalFlags.RUN_FIRST, None, (bool, str)), 'shows-latest': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'shows-deck': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'section-shows-deck': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'download-cover': (GObject.SignalFlags.RUN_FIRST, None, (int, str)), 'download-from-url': (GObject.SignalFlags.RUN_FIRST, None, (str, str)), 'shows-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, object)), 'item-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'item-downloading': (GObject.SignalFlags.RUN_FIRST, None, (object, bool)), 'sync-status': (GObject.SignalFlags.RUN_FIRST, None, (bool, )), 'servers-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'sections-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'album-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, object)), 'artist-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, object)), 'playlists-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'section-item-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (object, )), 'search-item-retrieved': (GObject.SignalFlags.RUN_FIRST, None, (str, object)), 'connection-to-server': (GObject.SignalFlags.RUN_FIRST, None, ()), 'logout': (GObject.SignalFlags.RUN_FIRST, None, ()), 'loading': (GObject.SignalFlags.RUN_FIRST, None, (str, bool)), 'sync-items': (GObject.SignalFlags.RUN_FIRST, None, (object, )), } _config = {} _search_provider_data = {} _server = None _account = None _library = None _sync_busy = False def __init__(self, config_dir, data_dir, player, **kwargs): super().__init__(**kwargs) self._settings = Gio.Settings("nl.g4d.Girens") self._config_dir = config_dir self._data_dir = data_dir self._player = player self._player.set_plex(self) self._config = self.__open_file(self._config_dir + '/config') self._search_provider_data = self.__open_file(self._config_dir + '/search_provider_data') self._user_uuid = self._settings.get_string("user-uuid") self._token = self.get_token(self._user_uuid) self._server_uuid = self._settings.get_string("server-uuid") if (self._server_uuid is not ''): self._server_token = self.get_server_token(self._server_uuid) self._server_url = self._settings.get_string("server-url") else: self._server_token = None self._server_url = None def __open_file(self, file_path): if (os.path.isfile(file_path)): with open(file_path, 'r') as file: lines = file.readlines() return json.loads(lines[0]) return {} def has_token(self): return self._token is not None def get_server_token(self, uuid): return Secret.password_lookup_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': uuid}, None) def get_token(self, uuid): return Secret.password_lookup_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': uuid}, None) def set_server_token(self, token, server_url, server_uuid, name): self._settings.set_string("server-url", self._server._baseurl) self._settings.set_string("server-uuid", self._server.machineIdentifier) Secret.password_store( Secret.Schema.new( "nl.g4d.Girens", Secret.SchemaFlags.NONE, { 'name': Secret.SchemaAttributeType.STRING, 'url': Secret.SchemaAttributeType.STRING, 'uuid': Secret.SchemaAttributeType.STRING }), { 'name': name, 'url': server_url, 'uuid': server_uuid }, Secret.COLLECTION_DEFAULT, 'Girens server token', token, None, None) def set_token(self, token, username, email, uuid): self._settings.set_string("user-uuid", uuid) Secret.password_store( Secret.Schema.new( "nl.g4d.Girens", Secret.SchemaFlags.NONE, { 'username': Secret.SchemaAttributeType.STRING, 'email': Secret.SchemaAttributeType.STRING, 'uuid': Secret.SchemaAttributeType.STRING }), { 'username': username, 'email': email, 'uuid': uuid }, Secret.COLLECTION_DEFAULT, 'Girens token', token, None, None) def has_url(self): return self._server_url is not None def login_token(self, token): try: self._account = MyPlexAccount(token=token) self.set_token(self._account._token, self._account.username, self._account.email, self._account.uuid) self.emit('login-status', True, '') except: self.emit('login-status', False, 'Login failed') def login(self, username, password): try: self._account = MyPlexAccount(username, password) self.set_token(self._account._token, self._account.username, self._account.email, self._account.uuid) self.emit('login-status', True, '') except: self.emit('login-status', False, 'Login failed') def login_with_url(self, baseurl, token): try: self.emit('loading', _('Connecting to ') + baseurl, True) self._server = PlexServer(baseurl, token) self._account = self._server.account() self._library = self._server.library self.set_server_token(self._server._token, self._server._baseurl, self._server.machineIdentifier, self._server.friendlyName) Secret.password_clear_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': self._user_uuid}, None) self._user_uuid = None self._token = None self.emit('connection-to-server') self.emit('loading', 'Success', False) self.emit('login-status', True, '') except: self.emit('loading', _('Connecting to ') + baseurl + _(' failed.'), True) self.emit('login-status', False, 'Login failed') print('connection failed (login with url)') def __save_config(self): with open(self._config_dir + '/config', 'w') as file: file.write(json.dumps(self._config)) def __save_search_provider_data(self): with open(self._config_dir + '/search_provider_data', 'w') as file: file.write(json.dumps(self._search_provider_data)) def logout(self): self._config = {} self._server = None self._account = None self._library = None self.__remove_login() self.emit('logout') def __remove_login(self): if (os.path.isfile(self._config_dir + '/config')): os.remove(self._config_dir + '/config') Secret.password_clear_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': self._server_uuid}, None) Secret.password_clear_sync( Secret.Schema.new("nl.g4d.Girens", Secret.SchemaFlags.NONE, {'uuid': Secret.SchemaAttributeType.STRING}), {'uuid': self._user_uuid}, None) self._settings.set_string("server-url", '') self._settings.set_string("server-uuid", '') self._settings.set_string("user-uuid", '') self._user_uuid = None self._token = None self._server_uuid = None self._server_token = None self._server_url = None def get_latest(self): latest = self._library.recentlyAdded() self.emit('shows-latest', latest) def get_deck(self): deck = self._library.onDeck() self.emit('shows-deck', deck) def get_section_deck(self, section_id): deck = self._library.sectionByID(section_id).onDeck() self.emit('section-shows-deck', deck) def get_item(self, key): return self._server.fetchItem(int(key)) def get_show(self, key): show = self._server.fetchItem(int(key)) episodes = show.episodes() self.emit('shows-retrieved', show, episodes) def get_album(self, key): album = self._server.fetchItem(int(key)) tracks = album.tracks() self.emit('album-retrieved', album, tracks) def get_artist(self, key): artist = self._server.fetchItem(int(key)) albums = artist.albums() self.emit('artist-retrieved', artist, albums) def get_servers(self): servers = [] if (self.has_token()): for resource in self._account.resources(): if (resource.provides == 'server'): servers.append(resource) else: servers.append(self._server) self.emit('servers-retrieved', servers) def get_playlists(self): playlists = self._server.playlists() self.emit('playlists-retrieved', playlists) def get_sections(self): sections = self._library.sections() self.emit('sections-retrieved', sections) def get_section_filter(self, section): if ('sections' in self._config and section.uuid in self._config['sections'] and 'sort' in self._config['sections'][section.uuid]): return self._config['sections'][section.uuid] return None def get_section_items(self, section, container_start=0, container_size=10, sort=None, sort_value=None): if (sort != None): if 'sections' not in self._config: self._config['sections'] = {} if section.uuid not in self._config['sections']: self._config['sections'][section.uuid] = {} self._config['sections'][section.uuid]['sort'] = sort self._config['sections'][section.uuid]['sort_value'] = sort_value self.__save_config() sort = sort + ':' + sort_value items = section.all(container_start=container_start, container_size=container_size, sort=sort) self.emit('section-item-retrieved', items) def reload_search_provider_data(self): #section = self._library.sectionByID('22') self._search_provider_data['sections'] = {} for section in self._library.sections(): if (section.type not in ['photo', 'movie']): items = section.all() self._search_provider_data['sections'][section.uuid] = { 'key': section.key, 'server_machine_identifier': self._server.machineIdentifier, 'title': section.title } self._search_provider_data['sections'][ section.uuid]['items'] = [] for item in items: self._search_provider_data['sections'][ section.uuid]['items'].append({ 'title': item.title, 'titleSort': item.titleSort, 'ratingKey': item.ratingKey, 'type': item.type }) if (section.type == 'artist'): for item in section.albums(): self._search_provider_data['sections'][ section.uuid]['items'].append({ 'title': item.title, 'titleSort': item.titleSort, 'ratingKey': item.ratingKey, 'type': item.type }) self.__save_search_provider_data() def search_library(self, search, libtype=None): items = self._library.search(search, limit=10, libtype=libtype) self.emit('search-item-retrieved', search, items) def download_cover(self, key, thumb): url_image = self._server.transcodeImage(thumb, 300, 200) if (url_image is not None and url_image != ""): path = self.__download(url_image, 'thumb_' + str(key)) self.emit('download-cover', key, path) def download_from_url(self, name_image, url_image): if (url_image is not None and url_image != ""): path = self.__download(url_image, 'thumb_' + name_image) self.emit('download-from-url', name_image, path) def play_item(self, item, shuffle=0, from_beginning=None, sort=None): if type(item) is str: item = self._server.fetchItem(item) parent_item = None if item.TYPE == "track": parent_item = item.album() playqueue = PlayQueue.create(self._server, item, shuffle=shuffle, continuous=1, parent=parent_item, sort=sort) self._player.set_playqueue(playqueue) GLib.idle_add(self.__play_item, from_beginning) def __play_item(self, from_beginning): self._player.start(from_beginning=from_beginning) def get_sync_items(self): if 'sync' in self._config: self.emit('sync-items', self._config['sync']) def remove_from_sync(self, item_key): if str(item_key) in self._config['sync']: del self._config['sync'][item_key] self.__save_config() self.get_sync_items() def add_to_sync(self, item, converted=False, max_items=None, only_unwatched=False): if 'sync' not in self._config: self._config['sync'] = {} if str(item.ratingKey) not in self._config['sync']: self._config['sync'][str(item.ratingKey)] = {} self._config['sync'][str(item.ratingKey)]['rating_key'] = str( item.ratingKey) self._config['sync'][str(item.ratingKey)]['converted'] = converted self._config['sync'][str( item.ratingKey)]['only_unwatched'] = only_unwatched if (max_items != None): self._config['sync'][str( item.ratingKey)]['max_items'] = max_items self.__save_config() self.get_sync_items() self.sync() def sync(self): if (self._sync_busy == False): self.emit('sync-status', True) path_dir = self._data_dir + '/' + self._server.machineIdentifier download_files = [] for file in os.listdir(path_dir): if file.startswith("item_"): download_files.append(file) self._sync_busy = True if 'sync' in self._config: sync = self._config['sync'].copy() for item_keys in sync: item = self._server.fetchItem(int(item_keys)) download_items = [] if (item.TYPE == 'movie' or item.TYPE == 'episode'): download_items.append(item) elif (item.TYPE == 'album' or item.TYPE == 'artist'): download_items = item.tracks() elif (item.TYPE == 'playlist'): download_items = item.items() elif (item.TYPE == 'show'): download_items = item.episodes() count = 0 for download_item in download_items: sync_bool = False if ('only_unwatched' not in sync[item_keys]): sync_bool = True elif (sync[item_keys]['only_unwatched'] == False): sync_bool = True elif (sync[item_keys]['only_unwatched'] == True and (download_item.TYPE == 'movie' or download_item.TYPE == 'episode') and not download_item.isWatched): sync_bool = True if (sync_bool == True): count = count + 1 if ('max_items' in sync[item_keys] and count > int(sync[item_keys]['max_items'])): break if (self.get_item_download_path(download_item) == None): self.__download_item( download_item, converted=sync[item_keys]['converted']) if ('item_' + str(download_item.ratingKey) in download_files): download_files.remove( 'item_' + str(download_item.ratingKey)) for file in download_files: path_file = os.path.join(path_dir, file) if os.path.exists(path_file): os.remove(path_file) self.emit('sync-status', False) self._sync_busy = False def __download_item(self, item, converted=False): path_dir = self._data_dir + '/' + self._server.machineIdentifier filename = 'item_' + str(item.ratingKey) filename_tmp = filename + '.tmp' path = path_dir + '/' + filename path_tmp = path_dir + '/' + filename_tmp self.emit('item-downloading', item, True) if not os.path.exists(path_dir): os.makedirs(path_dir) if not os.path.exists(path): if os.path.exists(path_tmp): os.remove(path_tmp) locations = [i for i in item.iterParts() if i] if (converted == False): download_url = self._server.url('%s?download=1' % locations[0].key) else: download_url = item.getStreamURL() utils.download(download_url, self._server._token, filename=filename_tmp, savepath=path_dir, session=self._server._session) os.rename(path_tmp, path) self.emit('item-downloading', item, False) def get_item_download_path(self, item): path_dir = self._data_dir + '/' + self._server.machineIdentifier filename = 'item_' + str(item.ratingKey) path = path_dir + '/' + filename if not os.path.exists(path): return None return path def mark_as_played(self, item): item.markWatched() item.reload() self.emit('item-retrieved', item) def mark_as_unplayed(self, item): item.markUnwatched() item.reload() self.emit('item-retrieved', item) def retrieve_item(self, item_key): item = self._server.fetchItem(int(item_key)) self.emit('item-retrieved', item) def path_for_download(self, prefix): path_dir = self._data_dir + '/' + self._server.machineIdentifier path = path_dir + '/' + prefix return [path_dir, path] def __download(self, url_image, prefix): paths = self.path_for_download(prefix) path_dir = paths[0] path = paths[1] if not os.path.exists(path_dir): os.makedirs(path_dir) if not os.path.exists(path): parse = urllib.parse.urlparse(url_image) auth_user = parse.username auth_passwd = parse.password password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() password_mgr.add_password(None, parse.scheme + "://" + parse.hostname, auth_user, auth_passwd) handler = urllib.request.HTTPBasicAuthHandler(password_mgr) opener = urllib.request.build_opener(handler) port = "" if parse.port != None: port = ":" + str(parse.port) url_img_combined = parse.scheme + "://" + parse.hostname + port + parse.path + "?" + parse.query img_raw = opener.open(url_img_combined) with open(path, 'w+b') as file: file.write(img_raw.read()) return path else: return path def connect_to_server(self): if (self._server_token is not None and self._server_url is not None): try: self.emit('loading', _('Connecting to ') + self._server_url + '.', True) self._server = PlexServer(self._server_url, self._server_token) self._library = self._server.library self.set_server_token(self._server._token, self._server._baseurl, self._server.machineIdentifier, self._server.friendlyName) self.emit('connection-to-server') self.emit('loading', 'Success', False) return None except: self.emit( 'loading', _('Connecting to ') + self._server_url + _(' failed.'), True) print('custom url connection failed') servers_found = False for resource in self._account.resources(): servers_found = True if ('server' in resource.provides.split(',')): if self.connect_to_resource(resource): break if (servers_found == False): self.emit('loading', _('No servers found for this account.'), True) def connect_to_resource(self, resource): try: self.emit( 'loading', _('Connecting to ') + resource.name + '.\n' + _('There are ') + str(len(resource.connections)) + _(' connection urls.') + '\n' + _('This may take a while'), True) self._server = resource.connect(ssl=self._account.secure) self._library = self._server.library self.set_server_token(self._server._token, self._server._baseurl, self._server.machineIdentifier, self._server.friendlyName) self.emit('connection-to-server') self.emit('loading', 'Success', False) return True except: self.emit('loading', _('Connecting to ') + resource.name + _(' failed.'), True) print('connection failed (when trying to connect to resource)') return False
name="Photos", type="photo", location="/data/Photos" if opts.no_docker is False else photos_path, agent="com.plexapp.agents.none", scanner="Plex Photo Scanner", expected_media_count=has_photos, )) # Create the Plex library in our instance if sections: print("Creating the Plex libraries on %s" % server.friendlyName) for section in sections: create_section(server, section, opts) # Share this instance with the specified username if account: shared_username = os.environ.get("SHARED_USERNAME", "PKKid") try: user = account.user(shared_username) account.updateFriend(user, server) print("The server was shared with user %s" % shared_username) except NotFound: pass # Finished: Display our Plex details print("Base URL is %s" % server.url("", False)) if account and opts.show_token: print("Auth token is %s" % account.authenticationToken) print("Server %s is ready to use!" % opts.server_name)
class PlexSleep: """ Use the plexapi to monitor: - client connections - streaming sessions - transcoding sessions (for things like sync) """ config_filename = 'config_plex_sleep.yml' def __init__(self, config_filename): log.info(f'plex_sleep v{__version__}') self.load_config(config_filename) self.activity = time.time() self.baseurl = f'http://{self.server}:{self.port}' self.wait_for_resume() log.info(f"Connnecting to plex server at {self.baseurl}...") self.plex = PlexServer(self.baseurl, self.token) log.info(f"Connnected") self.pending_refreshes = {} self.watch_server() def load_config(self, filename): with open(filename) as f: self.config = yaml.load(f, Loader=yaml.FullLoader) self.user = self.config.get('user') self.server = self.config.get('server', 'localhost') self.port = self.config.get('port', '32400') self.timeout = self.config.get('timeout', 10*60) # Default to 10 minutes before sleeping self.check_interval = self.config.get('check_interval', 60) # Check every minute to update who's connected # Default scan intervals self.library_scan_interval = {'movie': 60*60*12, # 12 hours, 'show': 60*60*12, # TV shows = 12 hours 'artist': 60*60*48, # Music = 2 days 'photo': 60*60*24, # Photos = 1 day, } scan_intervals = self.config.get('scan_interval', "movie:43200") for entry in scan_intervals.split(','): lib_type, lib_interval = entry.split(':') lib_type = lib_type.strip() lib_interval = lib_interval.strip() self.library_scan_interval[lib_type] = int(lib_interval) # Catch some common errors that a user might make if lib_type == 'music' or lib_type == 'mp3': log.warn(f'For scan_interval, you have used {lib_type}, but please use "artist" for Music libraries') if lib_type == 'tv' or lib_type == 'tv shows': log.warn(f'For scan_interval, you have used {lib_type}, but please use "show" for TV libraries') for lib_type, lib_interval in self.library_scan_interval.items(): log.info(f'Scan interval for {lib_type} libraries: {self.library_scan_interval[lib_type]} seconds') # Get the token from the environment first, then look for it in the config file if 'PLEX_TOKEN' in os.environ: self.token = os.environ.get('PLEX_TOKEN') elif 'token' in self.config: self.token = self.config.get('token') else: log.error(f'No PLEX_TOKEN environment variable set or "token" statement in config file (used to connect to Plex server') log.error('Exiting') sys.exit(-1) def watch_server(self): idle_time = 0 last_client_time = time.time() log.info('Started monitoring') while True: n_clients = self.get_num_clients() n_sess = self.get_num_sessions() n_trans = self.get_num_transcode_sessions() n_activity = self.get_activity_report() n = n_clients + n_sess + n_trans + n_activity self.refresh_libraries() if n>0: # People are browsing the server last_client_time = time.time() log.debug(f'Active clients:{n_clients}|sessions:{n_sess}|transcodes:{n_trans}|scans:{n_activity}') else: idle_time = time.time() - last_client_time if idle_time > self.timeout: log.info(f'Plex server idle for {int(idle_time/60)} minutes. Suspending...') if self._is_alive(self.server): os.system(f"""ssh -o StrictHostKeyChecking=no {self.user}@{self.server} 'echo "sudo pm-suspend" | at now + 1 minute'""") self.wait_for_suspend() self.wait_for_resume() log.info('resuming...') last_client_time = time.time() else: log.debug(f'Plex server has been idle for {int(idle_time/60)} minutes') time.sleep(self.check_interval) def _is_alive(self, server): r = ping(server, count=1, timeout=1) return r.success() def wait_for_suspend(self): log.info(f'waiting for {self.server} to sleep') while True: if self._is_alive(self.server): log.debug('ping is alive') time.sleep(5) else: log.info(f'{self.server} is asleep') return def wait_for_resume(self): log.info(f'waiting for {self.server} to awaken') while True: try: if self._is_alive(self.server): log.info(f'{self.server} is awake') return else: raise OSError except OSError: log.debug('ping is dead, waiting...') time.sleep(self.check_interval) # Probably errno 64 Host is down def _json_query(self, end_point): """ Make a custom query that returns json instead of xml like Plex's default end_point: '/status/sessions' """ headers = self.plex._headers() headers['Accept'] = 'application/json' url = self.plex.url(end_point) response = requests.get(url, headers=headers) return response.text def get_num_sessions(self): # Any client/app watching a stream return self._parse_count('/status/sessions') def get_num_clients(self): # Any client/app browsing the server return self._parse_count('/clients/') def get_num_transcode_sessions(self): # Transcodes to a player or a sync return self._parse_count('/transcode/sessions') def get_activity_report(self): # Any library scans running on the server are reported here return self._parse_count('/activities') def _parse_count(self, end_point): if self._is_alive(self.server): j = self._json_query(end_point) d = json.loads(j) log.debug(json.dumps(d, indent=4) ) return int(d['MediaContainer']['size']) else: return 0 def refresh_libraries(self): """Trigger a rescan of any library that was last scanned earlier than our library_scan_interval """ # Get the library api end_point = '/library/sections' j = self._json_query(end_point) d = json.loads(j) log.debug(json.dumps(d, indent=4) ) current_time = int(time.time()) # Clear out any pending refresh marks that are older than 10 minutes # - This is for the corner case where we marked something for refresh, it started refreshing # and then it finished refreshing before this function was called again. #for r, start_time in dict(self.pending_refreshes).items(): #if (current_time - start_time) > 10*60: #del self.pending_refreshes[r] for library in d['MediaContainer']['Directory']: last_scan = library['scannedAt'] library_type = library['type'] # Don't do anything if the scan interval is zero if self.library_scan_interval[library_type] == 0: continue if not library['refreshing']: # If Plex is not currently refreshing if current_time - last_scan > self.library_scan_interval.get(library_type, 60*60*24): # Library last refreshed earlier than library scan interval, so mark for refresh if library['key'] not in self.pending_refreshes: log.info(f'Starting refresh of {library["title"]}') end_point = f'/library/sections/{library["key"]}/refresh' j = self._json_query(end_point) self.pending_refreshes[library['key']] = current_time else: # We've already queued this up for a refresh so don't do anything pass else: if library['key'] in self.pending_refreshes: log.info(f'Completed refresh of {library["title"]}') del self.pending_refreshes[library['key']]
import os from plexapi.server import PlexServer from plexapi import utils baseurl = 'https://plx.w00t.cloud' token = 'H6gqeSNE3yGthe72x1w7' plex = PlexServer(baseurl, token) playlists = [pl for pl in plex.playlists()] playlist = utils.choose('Choose Playlist', playlists, lambda pl: '%s' % pl.title) print(len(playlist.items())) for photo in playlist.items(): photomediapart = photo.media[0].parts[0] print('Download File: %s' % photomediapart.file) url = plex.url('%s?download=1' % photomediapart.key) utils.download(url, token, os.path.basename(photomediapart.file))
print('Finished with photos...') sections.append( dict(name='Photos', type='photo', location='/data/Photos', agent='com.plexapp.agents.none', scanner='Plex Photo Scanner', expected_media_count=has_photos)) if sections: print('Ok, got the media, it`s time to create a library for you!') for section in sections: create_section(server, section) if account: shared_username = os.environ.get('SHARED_USERNAME', 'PKKid') try: user = account.user(shared_username) account.updateFriend(user, server) print('The server was shared with user "%s"' % shared_username) except NotFound: pass print('Base URL is %s' % server.url('', False)) if account and opts.show_token: print('Auth token is %s' % account.authenticationToken) print('Server %s is ready to use!' % opts.server_name)
class Plex(commands.Cog): """ Discord commands pertinent to interacting with Plex Contains user commands such as play, pause, resume, stop, etc. Grabs, and parses all data from plex database. """ # pylint: disable=too-many-instance-attributes # All are necessary to detect global interactions # within the bot. def __init__(self, bot, **kwargs): """ Initializes Plex resources Connects to Plex library and sets up all asyncronous communications. Args: bot: discord.ext.command.Bot, bind for cogs base_url: str url to Plex server plex_token: str X-Token of Plex server lib_name: str name of Plex library to search through Raises: plexapi.exceptions.Unauthorized: Invalid Plex token Returns: None """ self.bot = bot self.base_url = kwargs["base_url"] self.plex_token = kwargs["plex_token"] self.library_name = kwargs["lib_name"] self.bot_prefix = bot.command_prefix if kwargs["lyrics_token"]: self.genius = lyricsgenius.Genius(kwargs["lyrics_token"]) else: plex_log.warning("No lyrics token specified, lyrics disabled") self.genius = None # Log fatal invalid plex token try: self.pms = PlexServer(self.base_url, self.plex_token) except Unauthorized: plex_log.fatal("Invalid Plex token, stopping...") raise Unauthorized("Invalid Plex token") self.music = self.pms.library.section(self.library_name) plex_log.debug("Connected to plex library: %s", self.library_name) # Initialize necessary vars self.voice_channel = None self.current_track = None self.np_message_id = None self.ctx = None # Initialize events self.play_queue = asyncio.Queue() self.play_next_event = asyncio.Event() bot_log.info("Started bot successfully") self.bot.loop.create_task(self._audio_player_task()) def _search_tracks(self, title: str): """ Search the Plex music db for track Args: title: str title of song to search for Returns: plexapi.audio.Track pointing to best matching title Raises: MediaNotFoundError: Title of track can't be found in plex db """ results = self.music.searchTracks(title=title, maxresults=1) try: return results[0] except IndexError: raise MediaNotFoundError("Track cannot be found") def _search_albums(self, title: str): """ Search the Plex music db for album Args: title: str title of album to search for Returns: plexapi.audio.Album pointing to best matching title Raises: MediaNotFoundError: Title of album can't be found in plex db """ results = self.music.searchAlbums(title=title, maxresults=1) try: return results[0] except IndexError: raise MediaNotFoundError("Album cannot be found") def _search_playlists(self, title: str): """ Search the Plex music db for playlist Args: title: str title of playlist to search for Returns: plexapi.playlist pointing to best matching title Raises: MediaNotFoundError: Title of playlist can't be found in plex db """ try: return self.pms.playlist(title) except NotFound: raise MediaNotFoundError("Playlist cannot be found") async def _play(self): """ Heavy lifting of playing songs Grabs the appropiate streaming URL, sends the `now playing` message, and initiates playback in the vc. Args: None Returns: None Raises: None """ track_url = self.current_track.getStreamURL() audio_stream = FFmpegPCMAudio(track_url) while self.voice_channel.is_playing(): asyncio.sleep(2) self.voice_channel.play(audio_stream, after=self._toggle_next) plex_log.debug("%s - URL: %s", self.current_track, track_url) embed, img = self._build_embed_track(self.current_track) self.np_message_id = await self.ctx.send(embed=embed, file=img) async def _audio_player_task(self): """ Coroutine to handle playback and queuing Always-running function awaiting new songs to be added. Auto disconnects from VC if idle for > 15 seconds. Handles auto deletion of now playing song notifications. Args: None Returns: None Raises: None """ while True: self.play_next_event.clear() if self.voice_channel: try: # Disconnect after 15 seconds idle async with timeout(15): self.current_track = await self.play_queue.get() except asyncio.TimeoutError: await self.voice_channel.disconnect() self.voice_channel = None if not self.current_track: self.current_track = await self.play_queue.get() await self._play() await self.play_next_event.wait() await self.np_message_id.delete() def _toggle_next(self, error=None): """ Callback for vc playback Clears current track, then activates _audio_player_task to play next in queue or disconnect. Args: error: Optional parameter required for discord.py callback Returns: None Raises: None """ self.current_track = None self.bot.loop.call_soon_threadsafe(self.play_next_event.set) @staticmethod def _build_embed_track(track, type_="play"): """ Creates a pretty embed card for tracks Builds a helpful status embed with the following info: Status, song title, album, artist and album art. All pertitent information is grabbed dynamically from the Plex db. Args: track: plexapi.audio.Track object of song type_: Type of card to make (play, queue). Returns: embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of album thumbnail img. Raises: ValueError: Unsupported type of embed {type_} """ # Grab the relevant thumbnail img_stream = requests.get(track.thumbUrl, stream=True).raw img = io.BytesIO(img_stream.read()) # Attach to discord embed art_file = discord.File(img, filename="image0.png") # Get appropiate status message if type_ == "play": title = f"Now Playing - {track.title}" elif type_ == "queue": title = f"Added to queue - {track.title}" else: raise ValueError(f"Unsupported type of embed {type_}") # Include song details descrip = f"{track.album().title} - {track.artist().title}" # Build the actual embed embed = discord.Embed( title=title, description=descrip, colour=discord.Color.red() ) embed.set_author(name="Plex") # Point to file attached with ctx object. embed.set_thumbnail(url="attachment://image0.png") bot_log.debug("Built embed for track - %s", track.title) return embed, art_file @staticmethod def _build_embed_album(album): """ Creates a pretty embed card for albums Builds a helpful status embed with the following info: album, artist, and album art. All pertitent information is grabbed dynamically from the Plex db. Args: album: plexapi.audio.Album object of album Returns: embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of album thumbnail img. Raises: None """ # Grab the relevant thumbnail img_stream = requests.get(album.thumbUrl, stream=True).raw img = io.BytesIO(img_stream.read()) # Attach to discord embed art_file = discord.File(img, filename="image0.png") title = "Added album to queue" descrip = f"{album.title} - {album.artist().title}" embed = discord.Embed( title=title, description=descrip, colour=discord.Color.red() ) embed.set_author(name="Plex") embed.set_thumbnail(url="attachment://image0.png") bot_log.debug("Built embed for album - %s", album.title) return embed, art_file @staticmethod def _build_embed_playlist(self, playlist): """ Creates a pretty embed card for playlists Builds a helpful status embed with the following info: playlist art. All pertitent information is grabbed dynamically from the Plex db. Args: playlist: plexapi.playlist object of playlist Returns: embed: discord.embed fully constructed payload. thumb_art: io.BytesIO of playlist thumbnail img. Raises: None """ # Grab the relevant thumbnail img_stream = requests.get(self.pms.url(playlist.composite, True), stream=True).raw img = io.BytesIO(img_stream.read()) # Attach to discord embed art_file = discord.File(img, filename="image0.png") title = "Added playlist to queue" descrip = f"{playlist.title}" embed = discord.Embed( title=title, description=descrip, colour=discord.Color.red() ) embed.set_author(name="Plex") embed.set_thumbnail(url="attachment://image0.png") bot_log.debug("Built embed for playlist - %s", playlist.title) return embed, art_file async def _validate(self, ctx): """ Ensures user is in a vc Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: VoiceChannelError: Author not in voice channel """ # Fail if user not in vc if not ctx.author.voice: await ctx.send("Join a voice channel first!") bot_log.debug("Failed to play, requester not in voice channel") raise VoiceChannelError # Connect to voice if not already if not self.voice_channel: self.voice_channel = await ctx.author.voice.channel.connect() bot_log.debug("Connected to vc.") @command() async def play(self, ctx, *args): """ User command to play song Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. Args: ctx: discord.ext.commands.Context message context from command *args: Title of song to play Returns: None Raises: None """ # Save the context to use with async callbacks self.ctx = ctx title = " ".join(args) try: track = self._search_tracks(title) except MediaNotFoundError: await ctx.send(f"Can't find song: {title}") bot_log.debug("Failed to play, can't find song - %s", title) return try: await self._validate(ctx) except VoiceChannelError: pass # Specific add to queue message if self.voice_channel.is_playing(): bot_log.debug("Added to queue - %s", title) embed, img = self._build_embed_track(track, type_="queue") await ctx.send(embed=embed, file=img) # Add the song to the async queue await self.play_queue.put(track) @command() async def album(self, ctx, *args): """ User command to play song Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. Args: ctx: discord.ext.commands.Context message context from command *args: Title of song to play Returns: None Raises: None """ # Save the context to use with async callbacks self.ctx = ctx title = " ".join(args) try: album = self._search_albums(title) except MediaNotFoundError: await ctx.send(f"Can't find album: {title}") bot_log.debug("Failed to queue album, can't find - %s", title) return try: await self._validate(ctx) except VoiceChannelError: pass bot_log.debug("Added to queue - %s", title) embed, img = self._build_embed_album(album) await ctx.send(embed=embed, file=img) for track in album.tracks(): await self.play_queue.put(track) @command() async def playlist(self, ctx, *args): """ User command to play playlist Searchs plex db and either, initiates playback, or adds to queue. Handles invalid usage from the user. Args: ctx: discord.ext.commands.Context message context from command *args: Title of playlist to play Returns: None Raises: None """ # Save the context to use with async callbacks self.ctx = ctx title = " ".join(args) try: playlist = self._search_playlists(title) except MediaNotFoundError: await ctx.send(f"Can't find playlist: {title}") bot_log.debug("Failed to queue playlist, can't find - %s", title) return try: await self._validate(ctx) except VoiceChannelError: pass bot_log.debug("Added to queue - %s", title) embed, img = self._build_embed_playlist(self, playlist) await ctx.send(embed=embed, file=img) for item in playlist.items(): if (item.TYPE == "track"): await self.play_queue.put(item) @command() async def stop(self, ctx): """ User command to stop playback Stops playback and disconnects from vc. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.voice_channel: self.voice_channel.stop() await self.voice_channel.disconnect() self.voice_channel = None self.ctx = None bot_log.debug("Stopped") await ctx.send(":stop_button: Stopped") @command() async def pause(self, ctx): """ User command to pause playback Pauses playback, but doesn't reset anything to allow playback resuming. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.voice_channel: self.voice_channel.pause() bot_log.debug("Paused") await ctx.send(":play_pause: Paused") @command() async def resume(self, ctx): """ User command to resume playback Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.voice_channel: self.voice_channel.resume() bot_log.debug("Resumed") await ctx.send(":play_pause: Resumed") @command() async def skip(self, ctx): """ User command to skip song in queue Skips currently playing song. If no other songs in queue, stops playback, otherwise moves to next song. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ bot_log.debug("Skip") if self.voice_channel: self.voice_channel.stop() bot_log.debug("Skipped") self._toggle_next() @command(name="np") async def now_playing(self, ctx): """ User command to get currently playing song. Deletes old `now playing` status message, Creates a new one with up to date information. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if self.current_track: embed, img = self._build_embed_track(self.current_track) bot_log.debug("Now playing") if self.np_message_id: await self.np_message_id.delete() bot_log.debug("Deleted old np status") bot_log.debug("Created np status") self.np_message_id = await ctx.send(embed=embed, file=img) @command() async def clear(self, ctx): """ User command to clear play queue. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ self.play_queue = asyncio.Queue() bot_log.debug("Cleared queue") await ctx.send(":boom: Queue cleared.") @command() async def lyrics(self, ctx): """ User command to get lyrics of a song. Args: ctx: discord.ext.commands.Context message context from command Returns: None Raises: None """ if not self.current_track: plex_log.info("No song currently playing") return if self.genius: plex_log.info( "Searching for %s, %s", self.current_track.title, self.current_track.artist().title, ) try: song = self.genius.search_song( self.current_track.title, self.current_track.artist().title ) except TypeError: self.genius = None plex_log.error("Invalid genius token, disabling lyrics") return try: lyrics = song.lyrics # Split into 1950 char chunks # Discord max message length is 2000 lines = [(lyrics[i : i + 1950]) for i in range(0, len(lyrics), 1950)] for i in lines: if i == "": continue # Apply code block format i = f"```{i}```" await ctx.send(i) except (IndexError, TypeError): plex_log.info("Could not find lyrics") await ctx.send("Can't find lyrics for this song.") else: plex_log.warning("Attempted lyrics without valid token")
def fillVideoInfos(plexServer: server.PlexServer, itemId: int, plexItem: video.Video, mediaType: str, item: ListItem, allowDirectPlay: bool = False): """ Populate the provided ListItem object with existing data from plexItem and additional detail pulled from the provided plexServer :param plexServer: Plex server to gather additional details from :type plexServer: server.PlexServer :param itemId: Unique ID of the plex Video object item :type itemId: int :param plexItem: Plex object populated with information about the item :type plexItem: video.Video :param mediaType: Kodi Media type object :type mediaType: str :param item: Instantiated Kodi ListItem to populate with additional details :type item: :class:`ListItem` :param allowDirectPlay: Settings definition on provider if directPlay is allowed :type allowDirectPlay: bool, optional """ videoInfoTag = item.getVideoInfoTag() videoInfoTag.setMediaType(mediaType) videoInfoTag.setTitle(item.getLabel() or '') date = None isFolder = False resumeTime = 0.0 duration = 0.0 artwork = {} collections = [] media = [] locations = [] roles = [] if isinstance(plexItem, video.Video): videoInfoTag.setSortTitle(plexItem.titleSort or '') videoInfoTag.setPlot(plexItem.summary or '') videoInfoTag.setDateAdded( Api.convertDateTimeToDbDateTime(plexItem.addedAt)) videoInfoTag.setPlaycount(plexItem.viewCount or 0) videoInfoTag.setLastPlayed( Api.convertDateTimeToDbDateTime(plexItem.lastViewedAt)) videoInfoTag.setTags([plexItem.librarySectionTitle]) if isinstance(plexItem, video.Movie): date = Api.convertDateTimeToDbDate(plexItem.originallyAvailableAt) duration = Api.MillisecondsToSeconds(plexItem.duration) resumeTime = Api.MillisecondsToSeconds(plexItem.viewOffset) collections = plexItem.collections or [] media = plexItem.media or [] roles = plexItem.roles or [] videoInfoTag.setMpaa(plexItem.contentRating or '') videoInfoTag.setDuration(int(duration)) videoInfoTag.setOriginalTitle(plexItem.originalTitle or '') videoInfoTag.setPremiered(date) videoInfoTag.setRating(plexItem.rating or 0.0) videoInfoTag.setTagLine(plexItem.tagline or '') videoInfoTag.setUserRating(int(plexItem.userRating or 0)) videoInfoTag.setYear(plexItem.year or 0) videoInfoTag.setStudios(Api.ListFromString(plexItem.studio)) videoInfoTag.setCountries(Api.ListFromMediaTags( plexItem.countries)) videoInfoTag.setGenres(Api.ListFromMediaTags(plexItem.genres)) videoInfoTag.setDirectors(Api.ListFromMediaTags( plexItem.directors)) videoInfoTag.setWriters(Api.ListFromMediaTags(plexItem.writers)) elif isinstance(plexItem, collection.Collection): # ignore empty collections if plexItem.childCount <= 0: return isFolder = True videoInfoTag.setPlot(plexItem.summary or '') videoInfoTag.setDateAdded( Api.convertDateTimeToDbDateTime(plexItem.addedAt)) elif isinstance(plexItem, video.Show): isFolder = True date = Api.convertDateTimeToDbDate(plexItem.originallyAvailableAt) duration = Api.MillisecondsToSeconds(plexItem.duration) locations = plexItem.locations or [] collections = plexItem.collections or [] roles = plexItem.roles or [] banner = plexItem.banner if banner: artwork['banner'] = plexServer.url(banner, includeToken=True) videoInfoTag.setMpaa(plexItem.contentRating or '') videoInfoTag.setDuration(int(duration)) videoInfoTag.setOriginalTitle(plexItem.originalTitle or '') videoInfoTag.setPremiered(date) videoInfoTag.setRating(plexItem.rating or 0.0) videoInfoTag.setTagLine(plexItem.tagline or '') videoInfoTag.setYear(plexItem.year or 0) videoInfoTag.setStudios(Api.ListFromString(plexItem.studio)) videoInfoTag.setGenres(Api.ListFromMediaTags(plexItem.genres)) elif isinstance(plexItem, video.Season): isFolder = True videoInfoTag.setTvShowTitle(plexItem.parentTitle or '') videoInfoTag.setSeason(plexItem.index) elif isinstance(plexItem, video.Episode): date = Api.convertDateTimeToDbDate(plexItem.originallyAvailableAt) resumeTime = Api.MillisecondsToSeconds(plexItem.viewOffset) duration = Api.MillisecondsToSeconds(plexItem.duration) media = plexItem.media or [] videoInfoTag.setTvShowTitle(plexItem.grandparentTitle or '') videoInfoTag.setSeason(int(plexItem.parentIndex)) videoInfoTag.setEpisode(plexItem.index) videoInfoTag.setMpaa(plexItem.contentRating or '') videoInfoTag.setDuration(int(duration)) videoInfoTag.setFirstAired(date) videoInfoTag.setRating(plexItem.rating or 0.0) videoInfoTag.setYear(plexItem.year or 0) videoInfoTag.setDirectors(Api.ListFromMediaTags( plexItem.directors)) videoInfoTag.setWriters(Api.ListFromMediaTags(plexItem.writers)) # handle collections / sets collections = Api.ListFromMediaTags(collections) if collections: # Kodi can only store one set per media item videoInfoTag.setSet(collections[0]) # set the item's datetime if available if date: item.setDateTime(date) # specify whether the item is a folder or not item.setIsFolder(isFolder) # add the item's ID as a unique ID belonging to Plex uniqueIDs = {PLEX_PROTOCOL: str(itemId)} # retrieve and map GUIDS from Plex if isinstance(plexItem, (video.Movie, video.Show, video.Season, video.Episode)): guids = Api._mapGuids(plexItem.guids) if guids: uniqueIDs = {**guids, **uniqueIDs} videoInfoTag.setUniqueIDs(uniqueIDs, PLEX_PROTOCOL) # handle actors / cast cast = [] for index, role in enumerate(roles): actor = xbmc.Actor(role.tag.strip(), (role.role or '').strip(), index, role.thumb) cast.append(actor) if cast: videoInfoTag.setCast(cast) # handle resume point if resumeTime > 0 and duration > 0.0: videoInfoTag.setResumePoint(resumeTime, duration) # handle stream details path = None for mediaStream in media: for part in mediaStream.parts: # pick the first MediaPart with a valid file and stream URL if not path and part.file and part.key: path = part.file for videoStream in part.videoStreams(): videoInfoTag.addVideoStream( xbmc.VideoStreamDetail(width=videoStream.width or 0, height=videoStream.height or 0, codec=videoStream.codec or '', duration=int(duration), language=videoStream.language or '')) for audioStream in part.audioStreams(): videoInfoTag.addAudioStream( xbmc.AudioStreamDetail(channels=audioStream.channels or 2, codec=audioStream.codec or '', language=audioStream.language or '')) for index, subtitleStream in enumerate(part.subtitleStreams()): videoInfoTag.addSubtitleStream( xbmc.SubtitleStreamDetail( language=subtitleStream.language or f"[{index}]")) if isFolder: # for folders use locations for the path if locations: path = locations[0] item.setPath(plexServer.url(plexItem.key, includeToken=True)) else: # determine if directPlay is enabled and possible if allowDirectPlay: directPlayUrl = Api.getDirectPlayUrlFromPlexItem(plexItem) if directPlayUrl: item.setPath(directPlayUrl) # otherwise determine the stream URL if not item.getPath(): item.setPath(Api.getStreamUrlFromPlexItem( plexItem, plexServer)) if path: videoInfoTag.setPath(path) videoInfoTag.setFilenameAndPath(item.getPath()) # handle artwork poster = None fanart = None if isinstance(plexItem, video.Video): poster = plexItem.thumbUrl fanart = plexItem.artUrl elif isinstance(plexItem, collection.Collection) and plexItem.thumb: poster = plexServer.url(plexItem.thumb, includeToken=True) if poster: artwork['poster'] = poster if fanart: artwork['fanart'] = fanart if artwork: item.setArt(artwork)
class PlexBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): super(PlexBackend, self).__init__(audio=audio) self.config = config self.session = get_requests_session( proxy_config=config['proxy'], user_agent='%s/%s' % (mopidy_plex.Extension.dist_name, mopidy_plex.__version__)) type = config['plex']['type'] library = (config['plex']['library']) self.plex = None self.music = None if type == 'myplex': server = (config['plex']['server']) user = (config['plex']['username']) password = (config['plex']['password']) account = self.myplex_login(user, password) logger.info('Connecting to plex server: %s', server) self.plex = account.resource(server).connect(ssl=False) self.music = self.plex.library.section(library) elif type == 'direct': baseurl = (config['plex']['server']) token = (config['plex']['token']) self.plex = PlexServer(baseurl, token) self.music = self.plex.library.section(library) else: logger.error('Invalid value for plex backend type: %s', type) logger.info('Connected to plex server') logger.debug('Found music section on plex server %s: %s', self.plex, self.music) self.library_id = self.music.key self.uri_schemes = [ 'plex', ] self.library = PlexLibraryProvider(backend=self) self.playback = PlexPlaybackProvider(audio=audio, backend=self) self.playlists = PlexPlaylistsProvider(backend=self) def myplex_login(self, user, password): max_attempts = 20 current_attempt = 0 account = None while account is None: try: account = MyPlexAccount(user, password, session=self.session) except Exception as e: if current_attempt > max_attempts: logger.error( 'Could not connect to MyPlex in time, exiting...') return None traceback.print_exception(*e) current_attempt += 1 logger.error('Failed to log into MyPlex, retrying... %s/%s', current_attempt, max_attempts) sleep(5) return account def plex_uri(self, uri_path, prefix='plex'): '''Get a leaf uri and complete it to a mopidy plex uri. E.g. plex:artist:3434 plex:track:2323 plex:album:2323 plex:playlist:3432 ''' uri_path = str(uri_path) if not uri_path.startswith('/library/metadata/'): uri_path = '/library/metadata/' + uri_path if uri_path.startswith('/library/metadata/'): uri_path = uri_path[len('/library/metadata/'):] return '{}:{}'.format(prefix, uri_path) def resolve_uri(self, uri_path): '''Get a leaf uri and return full address to plex server''' uri_path = str(uri_path) if not uri_path.startswith('/library/metadata/'): uri_path = '/library/metadata/' + uri_path return self.plex.url(uri_path)