class EntangledPOC: def __init__(self): username = '******' password = load_password() self.account = MyPlexAccount(username, password) self.resource: PlexClient = None def list_resources(self): resources = self.account.resources() print(resources) def connect_to(self, resource_name): self.resource = self.account.resource(resource_name).connect() def play(self): if not self.resource: return self.resource.play() def pause(self): if not self.resource: return self.resource.pause() def go_to_10_min_in_movie(self): ten_minutes_ms = 10 * 60 * 1000 self.resource.seekTo(ten_minutes_ms)
def __init__(self, name, plex_url, plex_user, plex_password, plex_server, plex_token): """Initialize the sensor.""" from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer self._name = name self._state = 0 self._media_attrs = {} self._sessions = None self._session = None self._sessioncount = 0 self._session_type = None self._plex_url = plex_url self._plex_token = plex_token """Set all Media Items to None.""" # General self._media_content_type = None self._media_title = None # TV Show self._media_episode = None self._media_season = None self._media_series_title = None if plex_token: self._server = PlexServer(plex_url, plex_token) elif plex_user and plex_password: user = MyPlexAccount(plex_user, plex_password) server = plex_server if plex_server else user.resources()[0].name self._server = user.resource(server).connect() else: self._server = PlexServer(plex_url)
def __init__(self, name, plex_url, plex_user, plex_password, plex_server, plex_token, verify_ssl): """Initialize the sensor.""" from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer from requests import Session self._name = name self._state = 0 self._now_playing = [] cert_session = None if not verify_ssl: _LOGGER.info("Ignoring SSL verification") cert_session = Session() cert_session.verify = False if plex_token: self._server = PlexServer(plex_url, plex_token, cert_session) elif plex_user and plex_password: user = MyPlexAccount(plex_user, plex_password) server = plex_server if plex_server else user.resources()[0].name self._server = user.resource(server).connect() else: self._server = PlexServer(plex_url, None, cert_session)
class PythonLibPlexApi(PlexApi): # TODO # Add a 'connect_to_account' and 'connect_to_resource' method # and call them both in 'Entangled' (test via outside-in testing # that it's been call during 'Entangled' '__init__') # Then in the 'list_all_resources' script, only call 'connect_to_account' # Boom lifecyle problem solved, and it's transparent to the higher level # using 'Entangled' def __init__(self) -> None: self.username = os.environ['PLEX_USER'] self.password = os.environ['PLEX_PASS'] self.resource_name = os.environ['PLEX_RESOURCE'] self._connect_to_resource() def _connect_to_resource(self): self.account = MyPlexAccount(self.username, self.password) if (self.resource_name): self.resource = self.account.resource(self.resource_name).connect() else: logger.info("DEBUG TEMP: Add resource to envvar") @ensure_connected_to_account def all_resources(self): return self.account.resources() @ensure_connected_to_resource def current_movie_time(self): logger.info('Getting current movie time') # TODO: Fix 'self.resource.timeline.time' can be None sometimes if the movie was paused. Find solution. print(self.resource.timeline.time) import math timestamp = math.floor(self.resource.timeline.time / 1000) hours = math.floor(timestamp/3600) min = math.floor(timestamp / 60) - hours * 60 secs = timestamp - hours*3600 - min*60 current_time = f"{hours}:{min:02}:{secs:02}" return current_time @ensure_connected_to_resource def seek_to(self, hour, minute, second): logger.info(f"Seeking to: '{hour}:{minute}:{second}") def seconds(seconds_num): return seconds_num * 1000 def minutes(minutes_num): return seconds(minutes_num * 60) def hours(hours_num): return minutes(hours_num * 60) self.resource.seekTo( hours(hour) + minutes(minute) + seconds(second) ) @ensure_connected_to_resource def play(self): logger.info('Playing') self.resource.play()
def pause_then_play(): username = '******' password = load_password() session = Session() session.verify = 'charles-ssl-proxying-certificate.pem' account = MyPlexAccount(username, password, session=session) debug = account.resources() macbook_pro: PlexClient = account.resource('TheMacbookPro').connect() duration = int(input('How long to pause for: ')) macbook_pro.pause() sleep(duration) macbook_pro.play()
def get_server_info( username: str = typer.Argument(None, envvar="PLEXDL_USERNAME"), password: str = typer.Argument(None, envvar="PLEXDL_PASSWORD"), debug: bool = typer.Argument(False, envvar="PLEXDL_DEBUG"), ): """Show info about servers available to your account.""" p = MyPlexAccount( username=username, password=password, ) servers = p.resources() for server in servers: if server.product == "Plex Media Server": print(f"{server.name=} ", "-" * 70) print(f" clientIdentifier: {server.clientIdentifier}") last_seen_diff = datetime.datetime.now() - server.lastSeenAt last_seen_time_delta = humanize.naturaldelta(last_seen_diff) print( f" lastSeenAt: {server.lastSeenAt.strftime('%FT%T%z')} ({last_seen_time_delta})" ) created_diff = datetime.datetime.now() - server.createdAt created_time_delta = humanize.naturaldelta(created_diff) print( f" createdAt: {server.createdAt.strftime('%FT%T%z')} ({created_time_delta})" ) for i, connection in enumerate(server.connections): preferred = connection.uri in server.preferred_connections() print(f" connections[{i}]:") print(f" local: {connection.local}") print(f" uri: {connection.uri}") print(f" httpuri: {connection.httpuri}") print(f" relay: {bool(connection.relay)}") print(f" preferred: {preferred}") print(f" device: {server.device}") print(f" home: {server.home}") print(f" httpsRequired: {server.httpsRequired}") print(f" name: {server.name}") print(f" owned: {server.owned}") print(f" ownerid: {server.ownerid}") print(f" platform: {server.platform}") print(f" platformVersion: {server.platformVersion}") print(f" presence: {server.presence}") print(f" product: {server.product}") print(f" productVersion: {server.productVersion}") print(f" provides: {server.provides}") print(f" publicAddressMatches: {server.publicAddressMatches}") print(f" sourceTitle: {server.sourceTitle}") print(f" synced: {server.synced}") print("")
def getPlexToken(): arg_serverurl=sys.argv[2] arg_username=sys.argv[3] arg_password=sys.argv[4] if arg_username is not None and arg_password is not None : try: account = MyPlexAccount(arg_username, arg_password) for res in account.resources() : logging.debug("PLEX------ Server name available : " +str(res.name)) plexServer = account.resource(arg_serverurl).connect() logging.debug("PLEX------ Token for reuse : " +str(account._token)) return str(account._token) except Exception as e : pass return ""
def pick_server(account: MyPlexAccount): servers = account.resources() if not servers: return None if len(servers) == 1: return servers[0] server_name = prompt_server(servers) # Sanity check, even the user can't choose invalid resource server = account.resource(server_name) if server: return server return None
def __init__(self, name, plex_url, plex_user, plex_password, plex_server, plex_token): """Initialize the sensor.""" from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer self._name = name self._state = 0 self._now_playing = [] if plex_token: self._server = PlexServer(plex_url, plex_token) elif plex_user and plex_password: user = MyPlexAccount(plex_user, plex_password) server = plex_server if plex_server else user.resources()[0].name self._server = user.resource(server).connect() else: self._server = PlexServer(plex_url)
def connect_plex(configuration): from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer if (configuration['connection']=="direct"): print ("Connecting Directly") baseurl = configuration['url'] token = configuration['token'] server = PlexServer(baseurl, token) else: account = MyPlexAccount(configuration['username'],configuration['password']) if (configuration['servername']=='choose'): servers = servers = [s for s in account.resources() if 'server' in s.provides] server_choice=utils.choose('Choose a Server', servers, 'name') configuration['servername']=server_choice.name print ("Connecting to %s" % configuration['servername']) server = account.resource(configuration['servername']).connect() # returns a PlexServer instance token=account.authenticationToken print ("Connected") return (server)
def __init__(self, name, plex_url, plex_user, plex_password, plex_server, plex_token, plex_machine_id): """Initialize the sensor.""" from plexapi.myplex import MyPlexAccount from plexapi.server import PlexServer self._name = name self._media_attrs = {} self._plex_machine_id = plex_machine_id self._player_state = 'idle' self._machineIdentifier = plex_machine_id self._device = None self._is_player_active = False self._is_player_available = False self._player = None self._make = 'AppleTV' self._session = None self._session_type = None self._session_username = None self._state = STATE_IDLE self._entity_picture = None self._plex_url = plex_url self._plex_token = plex_token self._media_content_id = None self._media_content_rating = None self._media_content_type = None self._media_duration = None self._media_image_url = None self._media_title = None self._media_ratio = None self._media_episode = None self._media_season = None self._media_series_title = None if plex_token: self._server = PlexServer(plex_url, plex_token) elif plex_user and plex_password: user = MyPlexAccount(plex_user, plex_password) server = plex_server if plex_server else user.resources()[0].name self._server = user.resource(server).connect() else: self._server = PlexServer(plex_url)
def getServerResources(plexAccount: MyPlexAccount) -> List[MyPlexResource]: """Get list of plex servers connected to the MyPlexAccount provided :param plexAccount: Authenticated PlexAccount object to pull resources from :type plexAccount: :class:`MyPlexAccount` :return: List of MyPlexResource objects (servers) connected to the plex account :rtype: list """ if not plexAccount: raise ValueError('invalid plexAccount') # get all connected resources resources = plexAccount.resources() if not resources: return [] # we are only interested in Plex Media Server resources return [ resource for resource in resources if resource.product == 'Plex Media Server' and 'server' in resource.provides ]
class Client: """A client interface to plex for finding direct download URLs.""" def __init__(self, **kwargs): """plexdl.Client __init__ method.""" self.debug = kwargs["debug"] self.item_prefix = kwargs["item_prefix"] self.metadata = kwargs["metadata"] self.password = kwargs["password"] self.ratings = kwargs["ratings"] self.relay = kwargs["relay"] self.summary = kwargs["summary"] self.title = kwargs["title"] self.username = kwargs["username"] self.account = MyPlexAccount(self.username, self.password) available_servers: List[PlexServer] = list() @staticmethod def print_item_info(self, item, access_token): """Print info about a given media item.""" log.debug(f"Filesystem locations: {item.locations}") if hasattr(item, "iterParts"): locations = [i for i in item.iterParts() if i] for location in locations: media_info = [] download_url = item._server.url( f"{location.key}?download=1&X-Plex-Token={access_token}") download_filename = "" if hasattr(item, "seasonEpisode"): download_filename += f"{item.seasonEpisode} " download_filename += f"{item.title}.{location.container}" if self.metadata is True: if item.media[0].width is not None: media_info.append( f"{item.media[0].width}x{item.media[0].height}") if item.media[0].videoCodec is not None: media_info.append(item.media[0].videoCodec) if item.media[0].audioCodec is not None: media_info.append(item.media[0].audioCodec) if item.media[0].bitrate is not None: media_info.append(f"{item.media[0].bitrate}kbps") try: length = humanfriendly.format_size( int( requests.head( download_url).headers["Content-Length"])) media_info.append(f"{length}") except ValueError: pass if media_info: print(f'({", ".join(media_info)})') print( f' {self.item_prefix} "{download_filename}" "{download_url}"' ) @staticmethod def print_all_items_for_server(self, item, access_token): """Print info about all media items discovered on a given Plex server.""" if item.TYPE in ["movie", "track"]: print("-" * 79) print(f"{item.TYPE.capitalize()}: {item.title}") if self.summary is True and len(item.summary) > 1: print(f"Year: {item.year}") print(f"Studio: {item.studio}") print(f"Summary: {item.summary}") if self.ratings is True: if item.contentRating: print(f"Content rating: {item.contentRating}") if item.audienceRating: print(f"Audience rating: {item.audienceRating}") if item.rating: print(f"Critic rating: {item.rating}") self.print_item_info(self, item, access_token) elif item.TYPE in ["show"]: print("-" * 79) if self.summary is True and len(item.summary) > 1: print( f"{item.TYPE.capitalize()}: {item.title}\nSummary: {item.summary}" ) if self.ratings is True: print( f"Audience rating: {item.audienceRating}\nCritic rating: {item.rating}\nRated: {item.contentRating}" ) for episode in item.episodes(): self.print_item_info(self, episode, access_token) # TODO: break this up into just search, with print being handled elsewhere def main(self): """Perform all search and print logic.""" for r in self.account.resources(): if r.product == "Plex Media Server": self.available_servers.append(r) for this_server in self.available_servers: if not this_server.presence: continue try: for connection in this_server.connections: if connection.local: continue this_server_connection = PlexServer( connection.uri, this_server.accessToken) relay_status = "" if connection.relay: if self.relay is False: log.debug( f"Skipping {this_server_connection.friendlyName} relay" ) continue else: relay_status = " (relay)" print("\n") print("=" * 79) print( f'Server: "{this_server_connection.friendlyName}"{relay_status}' ) # TODO: add flags for each media type to help sort down what is displayed (since /hub/seach?mediatype="foo" doesn't work) # TODO: write handlers for each type # TODO: save results to a native data structure and have different output methods (eg: json, download links, csv) for item in this_server_connection.search(self.title): self.print_all_items_for_server( self, item, this_server.accessToken) except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout) as e: print(f'ERROR: connection to "{this_server.name}" failed.') log.debug(e) continue
class VideoFinder(MycroftSkill): def __init__(self): MycroftSkill.__init__(self) # List of services. Names in config must match API names. self.services = self.settings.get('services').lower().split(',') # Some titles are only available in certain countries self.country = self.settings.get('country').lower() # RapidAPI keys for Utelly API and IMDB API self.header_apiKey = self.settings.get('x-rapidapi-key') # Plex info self.plexAccount = MyPlexAccount(self.settings.get('plexUsername'), self.settings.get('plexPassword')) self.plexServers = [] # Service host info - from Rapid API self.header_utellyHost = self.settings.get('utellyHost') self.header_imdbHost = self.settings.get('imdbHost') # URLs for each service - from Rapid API self.utellyUrl = f'https://{self.header_utellyHost}/idlookup' self.imdbUrl = f'https://{self.header_imdbHost}/title/find' # If this is set to true, then the skill will offer to download a title if it can't be found. # Is there a way for this skill to just check if the other skill is loaded? self.couchPotato = self.settings.get('couchPotato') def initialize(self): # Find all Plex resources if "plex" in self.services: resources = self.plexAccount.resources() for resource in resources: if resource.provides == "server": self.plexServers.append(resource.name) # -------- # Main movie search handler # -------- @intent_handler('movie.search.intent') def handle_movie_search(self, message): # Pull data from intent search_title = message.data.get('title') search_actor = message.data.get('actor') # Talk to the user self.speak_dialog('search.for.that') # If the user specified an actor, search for the first matching title on IMDB to get the IMDB ID if search_actor: try: self.results = self.search_imdb_actor(search_title, search_actor) # Didn't find anything in IMDB if not self.results: self.speak_dialog('what.movie') # Found one in IMDB, go find it via utelly, etc else: self.movie_search(0) # Something went wrong with IMDB search except Exception as e: self.speak_dialog('unable.to.connect', data={'service':'imdb'}) LOG.error("Error searching imdb: " + str(e)) # If no actor was specified, find the top three and let the user choose the correct option else: # Make IMDB api request and get results try: # Search IMDB for IMDB ID (top three results) self.results = self.search_imdb(search_title)[0:3] self.speak_dialog('movies.results') # List top three videos found on IMDB for the user to chose from self.list_movies() # Something went wrong talking to IMDB except Exception as e: self.speak_dialog('unable.to.connect', data={'service':'imdb'}) LOG.error("Error searching imdb: " + str(e)) # List top three results found from IMDB def list_movies(self): # Loop through results for result in self.results: title = result["title"] year = "" star = "" # Add year to response to help the user find the right title if "year" in result: year = ", " + str(result["year"]) # Add first star of the title to help the user find the right title if "principals" in result: star = "starring " + result["principals"][0]["name"] self.speak_dialog('movie.title', data={'title': title, 'year': year, 'star': star}) # Let the user respond self.speak("", expect_response=True) # Performs search on IMDB. Returns first three results def search_imdb(self, title): # Build JSON query string queryString = {"q":title} LOG.info("Searching IMDB: " + str(queryString)) # HTTP Request headers headers = { 'x-rapidapi-key': self.header_apiKey, 'x-rapidapi-host': self.header_imdbHost } # Make HTTP request response = requests.request("GET", self.imdbUrl, headers=headers, params=queryString) # Convert response to json data = response.json() return data["results"] # Performs search on IMDB with actor included. Returns first result found with the matching actor def search_imdb_actor(self, title, actor): # search IMDB and get all titles from the response results = self.search_imdb(title) LOG.info("actor: '" + actor + "'") # Loop through all titles and find the first one with the specified actor for result in results: if "principals" in result: for principal in result["principals"]: if "name" in principal: if principal["name"].lower() == actor.lower(): return result return False # ----------- # User selected a movie from the list # ----------- @intent_handler('select.movie.intent') def handle_select_movie_intent(self, message): movieNum = message.data.get("num") # Process options (first, second, third, invalid) if movieNum == "first" and len(self.results) >= 1: self.speak_dialog('search.for.that') self.movie_search(0) elif movieNum == "second" and len(self.results) >= 2: self.speak_dialog('search.for.that') self.movie_search(1) elif movieNum == "third" and len(self.results) >= 3: self.speak_dialog('search.for.that') self.movie_search(2) else: self.speak_dialog('not.valid.option') def movie_search(self, num): # Extract IMDB ID from result if "title" in self.results: # If there's only one result (search by actor) imdbId = self.results["id"].split('/')[2] else: imdbId = self.results[num]["id"].split('/')[2] found_services = [] # Search Utelly for IMDB ID try: locations = self.search_utelly(imdbId) if locations: for location in locations: # We have a positive result if location["display_name"].lower() in self.services: found_services.append(location["display_name"]) # Something went wrong taling to utelly except Exception as e: self.speak_dialog('unable.to.connect', data={'service':'utelly'}) LOG.error("Error searching utelly: " + str(e)) # If Plex is enabled (utelly doesn't support plex so we need a separate request) if "plex" in self.services: # Search plex try: plexService = self.search_plex(imdbId) # Positive result if plexService: found_services.append('Plex - ' + plexService) # Something went wrong talking to Plex except Exception as e: self.speak_dialog('unable.to.connect', data={'service':'plex'}) LOG.error("Error searching plex: " + str(e)) # If at least one service was found if found_services: self.speak_dialog('found.services') # List all discovered services with matching result for service in found_services: self.speak_dialog('service', data={'service': service}) # Didn't find the title anywhere else: self.not_found(num, imdbId) # Performs search on Utelly and returns the raw response def search_utelly(self, imdbId): # Build HTP request query string queryString = {"source_id":imdbId,"source":"imdb","country":self.country} LOG.info("Searching Utelly: " + str(queryString)) # Build HTTP request headers headers = { 'x-rapidapi-key': self.header_apiKey, 'x-rapidapi-host': self.header_utellyHost } # Make HTP request response = requests.request("GET", self.utellyUrl, headers=headers, params=queryString) # Parse response as json jsonData = response.json() # If there's a positive result, return it if "locations" in jsonData["collection"]: return jsonData["collection"]["locations"] # Didn't find the title in any service via utelly else: return None # Searches Plex def search_plex(self, imdbId): # Loop through available plex servers for server in self.plexServers: # Connect to a plex server plex = self.plexAccount.resource(server).connect() # returns a PlexServer instance # Search libraries for IMDB ID videos = plex.library.search(guid = imdbId) for video in videos: # Positive match if imdbId in video.guid: return server return False # Function is called if a title isn't found anywhere def not_found(self, num, imdbId): self.speak_dialog('could.not.find') # If couchPotato skill is enabled if self.couchPotato: answer = "" # Ask the user if they want to download the title and keep asking until they say 'yes' or 'no' while answer != 'yes' and answer != 'no': answer = self.get_response('download.it') # User said yes, so download it if answer == 'yes': # Send an utterance to the BUS to trigger couchpotato download of the IMDB ID self.bus.emit(Message("recognizer_loop:utterance", {'utterances': ["Download the movie " + self.results[num - 1]["title"] + " with imdb id " + str(imdbId)], 'lang': 'en-us'}))
log_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s %(filename)s:%(lineno)d:%(funcName)-18s %(message)s')) logger.addHandler(log_handler) authentication = netrc.netrc().authenticators('plex') if authentication is None: logger.error("can't find machine 'plex' in your ~/.netrc") sys.exit(os.EX_NOTFOUND) user = MyPlexAccount(authentication[0], authentication[2]) parser = argparse.ArgumentParser() group1 = parser.add_mutually_exclusive_group() group1.add_argument("-d", "--debug", action="store_true") group1.add_argument("-v", "--verbose", action="store_true") group1.add_argument("-q", "--quiet", action="store_true") parser.add_argument("--server", help="Plex server name to fetch files from", required=True, choices=[x.name for x in user.resources() if x.provides == 'server']) parser.add_argument("--target", help="destination folder", required=True) parser.add_argument("--section", help="section to fetch") parser.add_argument("--name", help="movie or series to fetch") parser.add_argument("--bwlimit", help="limit bandwidth in bytes/s", type=int) parser.add_argument("--progress", help="show download progress", action="store_true") parser.add_argument("--force", help="force download, even if already seen", action="store_true") parser.add_argument("--assets", help="also download other assets (subtitles, cover and fanart)", action="store_true") parser.add_argument("--refresh-assets", help="redownload all assets", action="store_true") playlist_group = parser.add_argument_group() playlist_group.add_argument("--playlist", help="playlist to fetch") playlist_group.add_argument("--playlist-remove", action="store_true", help="cleanup playlist after downloading") args = parser.parse_args() if not (args.playlist or args.section) or (args.playlist and args.section): parser.error('either --section or --playlist required')
class Plex: def __init__(self, token, url=None): if token and not url: self.account = MyPlexAccount(token) if token and url: session = Connection().session self.server = PlexServer(baseurl=url, token=token, session=session) def admin_servers(self): """All owned servers Returns ------- data: dict """ resources = {} for resource in self.account.resources(): if 'server' in [resource.provides] and resource.owned == True: resources[resource.name] = resource return resources def all_users(self): """All users Returns ------- data: dict """ users = {self.account.title: self.account} for user in self.account.users(): users[user.title] = user return users def all_sections(self): """All sections from all owned servers Returns ------- data: dict """ data = {} servers = self.admin_servers() print("Connecting to admin server(s) for access info...") for name, server in servers.items(): connect = server.connect() sections = { section.title: section for section in connect.library.sections() } data[name] = sections return data def users_access(self): """Users access across all owned servers Returns ------- data: dict """ all_users = self.all_users().values() admin_servers = self.admin_servers() all_sections = self.all_sections() data = {self.account.title: {"account": self.account}} for user in all_users: if not data.get(user.title): servers = [] for server in user.servers: if admin_servers.get(server.name): access = {} sections = { section.title: section for section in server.sections() if section.shared == True } access['server'] = { server.name: admin_servers.get(server.name) } access['sections'] = sections servers += [access] data[user.title] = {'account': user, 'access': servers} else: # Admin account servers = [] for name, server in admin_servers.items(): access = {} sections = all_sections.get(name) access['server'] = {name: server} access['sections'] = sections servers += [access] data[user.title] = {'account': user, 'access': servers} return data
class Plex: server = None resource = None account = None signed_in = False def __init__(self, username, password): self.username = username self.password = password self.sign_in() def sign_in(self): try: self.account = MyPlexAccount(self.username, self.password) self.signed_in = True except: raise SignInError("Could not sign in") def get_resources(self): if (not self.signed_in): raise NotSignedInError("Not signed in ") return self.account.resources() def get_plex_server_resources(self): if (not self.signed_in): raise NotSignedInError("Not signed in ") plex_servers = [] for resource in self.account.resources(): if (resource.product == "Plex Media Server"): plex_servers.append(resource) return plex_servers def add_resource_by_name(self, name): if (not self.signed_in): raise NotSignedInError("Not signed in ") resources = self.get_resources() for resource in resources: if (resource.name != name): continue if (resource.product != "Plex Media Server"): raise NotMediaServerError("Not media server") self.resource = resource return raise NoServerError("No server with that name") def add_resource(self, resource): if (not self.signed_in): raise NotSignedInError("Not signed in ") if (resource.product != "Plex Media Server"): raise NotMediaServerError("Not media server") self.resource = resource def connect(self): if (not self.signed_in): raise NotSignedInError("Not signed in ") if (not self.resource): raise NoServerError("No resource attached") self.server = self.resource.connect() def add_playlist(self, name: str, name_list): if (not self.signed_in): raise NotSignedInError("Not signed in ") if (not self.server): raise NoServerError("No server attached") movie_list = [] failed_movies = [] for movie in name_list: # get movie if it exists temp = self.__get_movie(movie) # loop if it can't find the movie if temp is False: failed_movies.append(movie["title"]) continue # add to list if it can find it movie_list.append(temp) # create playlist playlist = None if (len(movie_list) > 0): playlist = self.server.createPlaylist(name, movie_list) return playlist, failed_movies def __get_movie(self, movie): results = self.server.search(movie["title"]) # return movie if it exists for plex_movie in results: if plex_movie.type == "movie" and str(plex_movie.year) in str( movie["year"]): return plex_movie return False def copy_to_users(self, playlist, users): for user in users: try: playlist.copyToUser(user) except: print(f"{user} does not have access to the library.") def scrape_imdb(self, link): movie_list = imdb(link) return movie_list def get_users(self): return [ user.title for user in self.server.myPlexAccount().users() if user.friend ] def check_login(self, username, password): try: MyPlexAccount(username, password) return True except: return False
class Plex(): def __init__(self, username=None, password=None, server=None, library=None, overwrite=False, verbose=False): self.account = None self.username = username self.password = password self.servers = [] self.server = server self.libraries = [] self.library = library self.overwrite = overwrite self.verbose = verbose self.downloaded = 0 self.skipped = 0 self.getAccount() self.getServer() self.getLibrary() def getAccount(self): try: self.account = MyPlexAccount(self.username, self.password) except BadRequest as e: print('\033[91mERROR:\033[0m', 'failed to connect to Plex. Check your username and password.') sys.exit() def getServer(self): self.servers = [ _ for _ in self.account.resources() if _.product == 'Plex Media Server' ] if not self.servers: print('\033[91mERROR:\033[0m', 'no available servers.') sys.exit() if self.server == None or self.servers not in [ _.name for _ in self.servers ]: self.server = plexapi.utils.choose('Select Server', self.servers, 'name').connect() else: self.server = self.server(self.server) if self.verbose: print('\033[94mSERVER:\033[0m', self.server.friendlyName) def getLibrary(self): self.libraries = [ _ for _ in self.server.library.sections() if _.type in {'movie', 'show'} ] if not self.libraries: print('\033[91mERROR:\033[0m', 'no available libraries.') sys.exit() if self.library == None or self.library not in [ _.title for _ in self.libraries ]: self.library = plexapi.utils.choose('Select Library', self.libraries, 'title') else: self.library = self.server.library.section(self.library) if self.verbose: print('\033[94mLIBRARY:\033[0m', self.library.title) def getAll(self): return self.library.all() def getPath(self, item, season=False): if self.library.type == 'movie': for media in item.media: for part in media.parts: return part.file.rsplit('/', 1)[0] elif self.library.type == 'show': for episode in item.episodes(): for media in episode.media: for part in media.parts: if season: return part.file.rsplit('/', 1)[0] return part.file.rsplit('/', 2)[0] def download(self, url=None, filename=None, path=None): if not self.overwrite and os.path.isfile(path+'/'+filename): if self.verbose: print('\033[93mSKIPPED:\033[0m', path+'/'+filename) self.skipped += 1 else: if plexapi.utils.download(self.server._baseurl+url, self.account._token, filename=filename, savepath=path): if self.verbose: print('\033[92mDOWNLOADED:\033[0m', path+'/'+filename) self.downloaded += 1 else: print('\033[91mDOWNLOAD FAILED:\033[0m', path+'/'+filename) sys.exit()
class PlexAttributes(): def __init__(self, opts): self.opts = opts # command line options self.clsnames = [c for c in opts.clsnames.split(',') if c] # list of clsnames to report (blank=all) self.account = MyPlexAccount() # MyPlexAccount instance self.plex = PlexServer() # PlexServer instance self.total = 0 # Total objects parsed self.attrs = defaultdict(dict) # Attrs result set def run(self): starttime = time.time() self._parse_myplex() self._parse_server() self._parse_search() self._parse_library() self._parse_audio() self._parse_photo() self._parse_movie() self._parse_show() self._parse_client() self._parse_playlist() self._parse_sync() self.runtime = round((time.time() - starttime) / 60.0, 1) return self def _parse_myplex(self): self._load_attrs(self.account, 'myplex') self._load_attrs(self.account.devices(), 'myplex') for resource in self.account.resources(): self._load_attrs(resource, 'myplex') self._load_attrs(resource.connections, 'myplex') self._load_attrs(self.account.users(), 'myplex') def _parse_server(self): self._load_attrs(self.plex, 'serv') self._load_attrs(self.plex.account(), 'serv') self._load_attrs(self.plex.history()[:50], 'hist') self._load_attrs(self.plex.history()[50:], 'hist') self._load_attrs(self.plex.sessions(), 'sess') def _parse_search(self): for search in ('cre', 'ani', 'mik', 'she', 'bea'): self._load_attrs(self.plex.search(search), 'hub') def _parse_library(self): cat = 'lib' self._load_attrs(self.plex.library, cat) # self._load_attrs(self.plex.library.all()[:50], 'all') self._load_attrs(self.plex.library.onDeck()[:50], 'deck') self._load_attrs(self.plex.library.recentlyAdded()[:50], 'add') for search in ('cat', 'dog', 'rat', 'gir', 'mou'): self._load_attrs(self.plex.library.search(search)[:50], 'srch') # TODO: Implement section search (remove library search?) # TODO: Implement section search filters def _parse_audio(self): cat = 'lib' for musicsection in self.plex.library.sections(): if musicsection.TYPE == library.MusicSection.TYPE: self._load_attrs(musicsection, cat) for artist in musicsection.all(): self._load_attrs(artist, cat) for album in artist.albums(): self._load_attrs(album, cat) for track in album.tracks(): self._load_attrs(track, cat) def _parse_photo(self): cat = 'lib' for photosection in self.plex.library.sections(): if photosection.TYPE == library.PhotoSection.TYPE: self._load_attrs(photosection, cat) for photoalbum in photosection.all(): self._load_attrs(photoalbum, cat) for photo in photoalbum.photos(): self._load_attrs(photo, cat) def _parse_movie(self): cat = 'lib' for moviesection in self.plex.library.sections(): if moviesection.TYPE == library.MovieSection.TYPE: self._load_attrs(moviesection, cat) for movie in moviesection.all(): self._load_attrs(movie, cat) def _parse_show(self): cat = 'lib' for showsection in self.plex.library.sections(): if showsection.TYPE == library.ShowSection.TYPE: self._load_attrs(showsection, cat) for show in showsection.all(): self._load_attrs(show, cat) for season in show.seasons(): self._load_attrs(season, cat) for episode in season.episodes(): self._load_attrs(episode, cat) def _parse_client(self): for device in self.account.devices(): client = self._safe_connect(device) if client is not None: self._load_attrs(client, 'myplex') for client in self.plex.clients(): self._safe_connect(client) self._load_attrs(client, 'client') def _parse_playlist(self): for playlist in self.plex.playlists(): self._load_attrs(playlist, 'pl') for item in playlist.items(): self._load_attrs(item, 'pl') playqueue = PlayQueue.create(self.plex, playlist) self._load_attrs(playqueue, 'pq') def _parse_sync(self): # TODO: Get plexattrs._parse_sync() working. pass def _load_attrs(self, obj, cat=None): if isinstance(obj, (list, tuple)): return [self._parse_objects(item, cat) for item in obj] self._parse_objects(obj, cat) def _parse_objects(self, obj, cat=None): clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__) clsname = clsname.replace('plexapi.', '') if self.clsnames and clsname not in self.clsnames: return None self._print_the_little_dot() if clsname not in self.attrs: self.attrs[clsname] = copy.deepcopy(NAMESPACE) self.attrs[clsname]['total'] += 1 self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'], self.attrs[clsname]['examples'], self.attrs[clsname]['categories'], cat) self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'], self.attrs[clsname]['docs']) def _print_the_little_dot(self): self.total += 1 if not self.total % 100: sys.stdout.write('.') if not self.total % 8000: sys.stdout.write('\n') sys.stdout.flush() def _load_xml_attrs(self, clsname, elem, attrs, examples, categories, cat): if elem is None: return None for attr in sorted(elem.attrib.keys()): attrs[attr] += 1 if cat: categories[attr].add(cat) if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples: examples[attr].add(elem.attrib[attr]) for subelem in elem: attrname = TAGATTRS.get(subelem.tag, '%ss' % subelem.tag.lower()) attrs['%s[]' % attrname] += 1 def _load_obj_attrs(self, clsname, obj, attrs, docs): if clsname in STOP_RECURSING_AT: return None if isinstance(obj, PlexObject) and clsname not in DONT_RELOAD: self._safe_reload(obj) alldocs = '\n\n'.join(self._all_docs(obj.__class__)) for attr, value in obj.__dict__.items(): if value is None or isinstance(value, (str, bool, float, int, datetime)): if not attr.startswith('_') and attr not in IGNORES.get(clsname, []): attrs[attr] += 1 if re.search('\s{8}%s\s\(.+?\)\:' % attr, alldocs) is not None: docs[attr] += 1 if isinstance(value, list): if not attr.startswith('_') and attr not in IGNORES.get(clsname, []): if value and isinstance(value[0], PlexObject): attrs['%s[]' % attr] += 1 [self._parse_objects(obj) for obj in value] def _all_docs(self, cls, docs=None): import inspect docs = docs or [] if cls.__doc__ is not None: docs.append(cls.__doc__) for parent in inspect.getmro(cls): if parent != cls: docs += self._all_docs(parent) return docs def print_report(self): total_attrs = 0 for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): meta = self.attrs[clsname] count = meta['total'] print(_('\n%s (%s)\n%s' % (clsname, count, '-' * 30), 'yellow')) attrs = sorted(set(list(meta['xml'].keys()) + list(meta['obj'].keys()))) for attr in attrs: state = self._attr_state(clsname, attr, meta) count = meta['xml'].get(attr, 0) categories = ','.join(meta['categories'].get(attr, ['--'])) examples = '; '.join(list(meta['examples'].get(attr, ['--']))[:3])[:80] print('%7s %3s %-30s %-20s %s' % (count, state, attr, categories, examples)) total_attrs += count print(_('\nSUMMARY\n%s' % ('-' * 30), 'yellow')) print('%7s %3s %3s %3s %-20s %s' % ('total', 'new', 'old', 'doc', 'categories', 'clsname')) for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): print('%7s %12s %12s %12s %s' % (self.attrs[clsname]['total'], _(self.attrs[clsname]['new'] or '', 'cyan'), _(self.attrs[clsname]['old'] or '', 'red'), _(self.attrs[clsname]['doc'] or '', 'purple'), clsname)) print('\nPlex Version %s' % self.plex.version) print('PlexAPI Version %s' % plexapi.VERSION) print('Total Objects %s' % sum([x['total'] for x in self.attrs.values()])) print('Runtime %s min\n' % self.runtime) def _clsname_match(self, clsname): if not self.clsnames: return True for cname in self.clsnames: if cname.lower() in clsname.lower(): return True return False def _attr_state(self, clsname, attr, meta): if attr in meta['xml'].keys() and attr not in meta['obj'].keys(): self.attrs[clsname]['new'] += 1 return _('new', 'blue') if attr not in meta['xml'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['old'] += 1 return _('old', 'red') if attr not in meta['docs'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['doc'] += 1 return _('doc', 'purple') return _(' ', 'green') def _safe_connect(self, elem): try: return elem.connect() except: return None def _safe_reload(self, elem): try: elem.reload() except: pass
def get_servers_for_account(account: MyPlexAccount) -> PlexServer: return [s for s in account.resources() if "server" in s.provides]
class PlexAttributes(): def __init__(self, opts): self.opts = opts # command line options self.clsnames = [c for c in opts.clsnames.split(',') if c] # list of clsnames to report (blank=all) self.account = MyPlexAccount() # MyPlexAccount instance self.plex = PlexServer() # PlexServer instance self.total = 0 # Total objects parsed self.attrs = defaultdict(dict) # Attrs result set def run(self): starttime = time.time() self._parse_myplex() self._parse_server() self._parse_search() self._parse_library() self._parse_audio() self._parse_photo() self._parse_movie() self._parse_show() self._parse_client() self._parse_playlist() self._parse_sync() self.runtime = round((time.time() - starttime) / 60.0, 1) return self def _parse_myplex(self): self._load_attrs(self.account, 'myplex') self._load_attrs(self.account.devices(), 'myplex') for resource in self.account.resources(): self._load_attrs(resource, 'myplex') self._load_attrs(resource.connections, 'myplex') self._load_attrs(self.account.users(), 'myplex') def _parse_server(self): self._load_attrs(self.plex, 'serv') self._load_attrs(self.plex.account(), 'serv') self._load_attrs(self.plex.history()[:50], 'hist') self._load_attrs(self.plex.history()[50:], 'hist') self._load_attrs(self.plex.sessions(), 'sess') def _parse_search(self): for search in ('cre', 'ani', 'mik', 'she', 'bea'): self._load_attrs(self.plex.search(search), 'hub') def _parse_library(self): cat = 'lib' self._load_attrs(self.plex.library, cat) # self._load_attrs(self.plex.library.all()[:50], 'all') self._load_attrs(self.plex.library.onDeck()[:50], 'deck') self._load_attrs(self.plex.library.recentlyAdded()[:50], 'add') for search in ('cat', 'dog', 'rat', 'gir', 'mou'): self._load_attrs(self.plex.library.search(search)[:50], 'srch') # TODO: Implement section search (remove library search?) # TODO: Implement section search filters def _parse_audio(self): cat = 'lib' for musicsection in self.plex.library.sections(): if musicsection.TYPE == library.MusicSection.TYPE: self._load_attrs(musicsection, cat) for artist in musicsection.all(): self._load_attrs(artist, cat) for album in artist.albums(): self._load_attrs(album, cat) for track in album.tracks(): self._load_attrs(track, cat) def _parse_photo(self): cat = 'lib' for photosection in self.plex.library.sections(): if photosection.TYPE == library.PhotoSection.TYPE: self._load_attrs(photosection, cat) for photoalbum in photosection.all(): self._load_attrs(photoalbum, cat) for photo in photoalbum.photos(): self._load_attrs(photo, cat) def _parse_movie(self): cat = 'lib' for moviesection in self.plex.library.sections(): if moviesection.TYPE == library.MovieSection.TYPE: self._load_attrs(moviesection, cat) for movie in moviesection.all(): self._load_attrs(movie, cat) def _parse_show(self): cat = 'lib' for showsection in self.plex.library.sections(): if showsection.TYPE == library.ShowSection.TYPE: self._load_attrs(showsection, cat) for show in showsection.all(): self._load_attrs(show, cat) for season in show.seasons(): self._load_attrs(season, cat) for episode in season.episodes(): self._load_attrs(episode, cat) def _parse_client(self): for device in self.account.devices(): client = self._safe_connect(device) if client is not None: self._load_attrs(client, 'myplex') for client in self.plex.clients(): self._safe_connect(client) self._load_attrs(client, 'client') def _parse_playlist(self): for playlist in self.plex.playlists(): self._load_attrs(playlist, 'pl') for item in playlist.items(): self._load_attrs(item, 'pl') playqueue = PlayQueue.create(self.plex, playlist) self._load_attrs(playqueue, 'pq') def _parse_sync(self): # TODO: Get plexattrs._parse_sync() working. pass def _load_attrs(self, obj, cat=None): if isinstance(obj, (list, tuple)): return [self._parse_objects(item, cat) for item in obj] self._parse_objects(obj, cat) def _parse_objects(self, obj, cat=None): clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__) clsname = clsname.replace('plexapi.', '') if self.clsnames and clsname not in self.clsnames: return None self._print_the_little_dot() if clsname not in self.attrs: self.attrs[clsname] = copy.deepcopy(NAMESPACE) self.attrs[clsname]['total'] += 1 self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'], self.attrs[clsname]['examples'], self.attrs[clsname]['categories'], cat) self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'], self.attrs[clsname]['docs']) def _print_the_little_dot(self): self.total += 1 if not self.total % 100: sys.stdout.write('.') if not self.total % 8000: sys.stdout.write('\n') sys.stdout.flush() def _load_xml_attrs(self, clsname, elem, attrs, examples, categories, cat): if elem is None: return None for attr in sorted(elem.attrib.keys()): attrs[attr] += 1 if cat: categories[attr].add(cat) if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples: examples[attr].add(elem.attrib[attr]) for subelem in elem: attrname = TAGATTRS.get(subelem.tag, '%ss' % subelem.tag.lower()) attrs['%s[]' % attrname] += 1 def _load_obj_attrs(self, clsname, obj, attrs, docs): if clsname in STOP_RECURSING_AT: return None if isinstance(obj, PlexObject) and clsname not in DONT_RELOAD: self._safe_reload(obj) alldocs = '\n\n'.join(self._all_docs(obj.__class__)) for attr, value in obj.__dict__.items(): if value is None or isinstance(value, (str, bool, float, int, datetime)): if not attr.startswith('_') and attr not in IGNORES.get( clsname, []): attrs[attr] += 1 if re.search('\s{8}%s\s\(.+?\)\:' % attr, alldocs) is not None: docs[attr] += 1 if isinstance(value, list): if not attr.startswith('_') and attr not in IGNORES.get( clsname, []): if value and isinstance(value[0], PlexObject): attrs['%s[]' % attr] += 1 [self._parse_objects(obj) for obj in value] def _all_docs(self, cls, docs=None): import inspect docs = docs or [] if cls.__doc__ is not None: docs.append(cls.__doc__) for parent in inspect.getmro(cls): if parent != cls: docs += self._all_docs(parent) return docs def print_report(self): total_attrs = 0 for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): meta = self.attrs[clsname] count = meta['total'] print(_('\n%s (%s)\n%s' % (clsname, count, '-' * 30), 'yellow')) attrs = sorted( set(list(meta['xml'].keys()) + list(meta['obj'].keys()))) for attr in attrs: state = self._attr_state(clsname, attr, meta) count = meta['xml'].get(attr, 0) categories = ','.join(meta['categories'].get(attr, ['--'])) examples = '; '.join( list(meta['examples'].get(attr, ['--']))[:3])[:80] print('%7s %3s %-30s %-20s %s' % (count, state, attr, categories, examples)) total_attrs += count print(_('\nSUMMARY\n%s' % ('-' * 30), 'yellow')) print('%7s %3s %3s %3s %-20s %s' % ('total', 'new', 'old', 'doc', 'categories', 'clsname')) for clsname in sorted(self.attrs.keys()): if self._clsname_match(clsname): print('%7s %12s %12s %12s %s' % (self.attrs[clsname]['total'], _(self.attrs[clsname]['new'] or '', 'cyan'), _(self.attrs[clsname]['old'] or '', 'red'), _(self.attrs[clsname]['doc'] or '', 'purple'), clsname)) print('\nPlex Version %s' % self.plex.version) print('PlexAPI Version %s' % plexapi.VERSION) print('Total Objects %s' % sum([x['total'] for x in self.attrs.values()])) print('Runtime %s min\n' % self.runtime) def _clsname_match(self, clsname): if not self.clsnames: return True for cname in self.clsnames: if cname.lower() in clsname.lower(): return True return False def _attr_state(self, clsname, attr, meta): if attr in meta['xml'].keys() and attr not in meta['obj'].keys(): self.attrs[clsname]['new'] += 1 return _('new', 'blue') if attr not in meta['xml'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['old'] += 1 return _('old', 'red') if attr not in meta['docs'].keys() and attr in meta['obj'].keys(): self.attrs[clsname]['doc'] += 1 return _('doc', 'purple') return _(' ', 'green') def _safe_connect(self, elem): try: return elem.connect() except: return None def _safe_reload(self, elem): try: elem.reload() except: pass
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
class PlexAlertListener(threading.Thread): productName = "Plex Media Server" updateTimeoutTimerInterval = 30 connectionTimeoutTimerInterval = 60 maximumIgnores = 2 def __init__(self, token: str, serverConfig: models.config.Server): super().__init__() self.daemon = True self.token = token self.serverConfig = serverConfig self.logger = LoggerWithPrefix( f"[{self.serverConfig['name']}/{hashlib.md5(str(id(self)).encode('UTF-8')).hexdigest()[:5].upper()}] " ) self.discordRpcService = DiscordRpcService() self.updateTimeoutTimer: Optional[threading.Timer] = None self.connectionTimeoutTimer: Optional[threading.Timer] = None self.account: Optional[MyPlexAccount] = None self.server: Optional[PlexServer] = None self.alertListener: Optional[AlertListener] = None self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0 self.listenForUser, self.isServerOwner, self.ignoreCount = "", False, 0 self.start() def run(self) -> None: connected = False while not connected: try: self.logger.info("Signing into Plex") self.account = MyPlexAccount(token=self.token) self.logger.info("Signed in as Plex user \"%s\"", self.account.username) self.listenForUser = self.serverConfig.get( "listenForUser", self.account.username) self.server = None for resource in self.account.resources(): if resource.product == self.productName and resource.name.lower( ) == self.serverConfig["name"].lower(): self.logger.info("Connecting to %s \"%s\"", self.productName, self.serverConfig["name"]) self.server = resource.connect() try: self.server.account() self.isServerOwner = True except: pass self.logger.info("Connected to %s \"%s\"", self.productName, resource.name) self.alertListener = AlertListener( self.server, self.handlePlexAlert, self.reconnect) self.alertListener.start() self.logger.info( "Listening for alerts from user \"%s\"", self.listenForUser) self.connectionTimeoutTimer = threading.Timer( self.connectionTimeoutTimerInterval, self.connectionTimeout) self.connectionTimeoutTimer.start() connected = True break if not self.server: self.logger.error("%s \"%s\" not found", self.productName, self.serverConfig["name"]) break except Exception as e: self.logger.error("Failed to connect to %s \"%s\": %s", self.productName, self.serverConfig["name"], e) self.logger.error("Reconnecting in 10 seconds") time.sleep(10) def disconnect(self) -> None: if self.alertListener: try: self.alertListener.stop() except: pass self.disconnectRpc() self.account, self.server, self.alertListener, self.listenForUser, self.isServerOwner, self.ignoreCount = None, None, None, "", False, 0 self.logger.info("Stopped listening for alerts") def reconnect(self, exception: Exception) -> None: self.logger.error("Connection to Plex lost: %s", exception) self.disconnect() self.logger.error("Reconnecting") self.run() def disconnectRpc(self) -> None: self.lastState, self.lastSessionKey, self.lastRatingKey = "", 0, 0 self.discordRpcService.disconnect() self.cancelTimers() def cancelTimers(self) -> None: if self.updateTimeoutTimer: self.updateTimeoutTimer.cancel() if self.connectionTimeoutTimer: self.connectionTimeoutTimer.cancel() self.updateTimeoutTimer, self.connectionTimeoutTimer = None, None def updateTimeout(self) -> None: self.logger.debug("No recent updates from session key %s", self.lastSessionKey) self.disconnectRpc() def connectionTimeout(self) -> None: try: assert self.server self.logger.debug( "Request for list of clients to check connection: %s", self.server.clients()) except Exception as e: self.reconnect(e) else: self.connectionTimeoutTimer = threading.Timer( self.connectionTimeoutTimerInterval, self.connectionTimeout) self.connectionTimeoutTimer.start() def handlePlexAlert(self, alert: models.plex.Alert) -> None: try: if alert[ "type"] == "playing" and "PlaySessionStateNotification" in alert: stateNotification = alert["PlaySessionStateNotification"][0] state = stateNotification["state"] sessionKey = int(stateNotification["sessionKey"]) ratingKey = int(stateNotification["ratingKey"]) viewOffset = int(stateNotification["viewOffset"]) self.logger.debug("Received alert: %s", stateNotification) assert self.server item: PlexPartialObject = self.server.fetchItem(ratingKey) libraryName: str = item.section().title if "blacklistedLibraries" in self.serverConfig and libraryName in self.serverConfig[ "blacklistedLibraries"]: self.logger.debug( "Library \"%s\" is blacklisted, ignoring", libraryName) return if "whitelistedLibraries" in self.serverConfig and libraryName not in self.serverConfig[ "whitelistedLibraries"]: self.logger.debug( "Library \"%s\" is not whitelisted, ignoring", libraryName) return if self.lastSessionKey == sessionKey and self.lastRatingKey == ratingKey: if self.updateTimeoutTimer: self.updateTimeoutTimer.cancel() self.updateTimeoutTimer = None if self.lastState == state and self.ignoreCount < self.maximumIgnores: self.logger.debug("Nothing changed, ignoring") self.ignoreCount += 1 self.updateTimeoutTimer = threading.Timer( self.updateTimeoutTimerInterval, self.updateTimeout) self.updateTimeoutTimer.start() return else: self.ignoreCount = 0 if state == "stopped": self.disconnectRpc() return elif state == "stopped": self.logger.debug( "Received \"stopped\" state alert from unknown session, ignoring" ) return if self.isServerOwner: self.logger.debug("Searching sessions for session key %s", sessionKey) sessions: list[Playable] = self.server.sessions() if len(sessions) < 1: self.logger.debug("Empty session list, ignoring") return for session in sessions: self.logger.debug("%s, Session Key: %s, Usernames: %s", session, session.sessionKey, session.usernames) if session.sessionKey == sessionKey: self.logger.debug("Session found") sessionUsername: str = session.usernames[0] if sessionUsername.lower( ) == self.listenForUser.lower(): self.logger.debug( "Username \"%s\" matches \"%s\", continuing", sessionUsername, self.listenForUser) break self.logger.debug( "Username \"%s\" doesn't match \"%s\", ignoring", sessionUsername, self.listenForUser) return else: self.logger.debug( "No matching session found, ignoring") return if self.updateTimeoutTimer: self.updateTimeoutTimer.cancel() self.updateTimeoutTimer = threading.Timer( self.updateTimeoutTimerInterval, self.updateTimeout) self.updateTimeoutTimer.start() self.lastState, self.lastSessionKey, self.lastRatingKey = state, sessionKey, ratingKey mediaType: str = item.type title: str thumb: str if mediaType in ["movie", "episode"]: stateStrings: list[str] = [ formatSeconds(item.duration / 1000) ] if mediaType == "movie": title = f"{item.title} ({item.year})" stateStrings.append( f"{', '.join(genre.tag for genre in item.genres[:3])}" ) largeText = "Watching a movie" thumb = item.thumb else: title = item.grandparentTitle stateStrings.append( f"S{item.parentIndex:02}E{item.index:02}") stateStrings.append(item.title) largeText = "Watching a TV show" thumb = item.grandparentThumb if state != "playing": stateStrings.append( f"{formatSeconds(viewOffset / 1000, ':')} elapsed") stateText = " ยท ".join(stateString for stateString in stateStrings if stateString) elif mediaType == "track": title = item.title stateText = f"{item.originalTitle or item.grandparentTitle} - {item.parentTitle} ({self.server.fetchItem(item.parentRatingKey).year})" largeText = "Listening to music" thumb = item.thumb else: self.logger.debug( "Unsupported media type \"%s\", ignoring", mediaType) return thumbUrl = "" if config["display"]["posters"]["enabled"]: thumbUrl = getKey(thumb) if not thumbUrl: self.logger.debug("Uploading image") thumbUrl = uploadImage(self.server.url(thumb, True)) setKey(thumb, thumbUrl) activity: models.discord.Activity = { "details": title[:128], "state": stateText[:128], "assets": { "large_text": largeText, "large_image": thumbUrl or "logo", "small_text": state.capitalize(), "small_image": state, }, } if state == "playing": currentTimestamp = int(time.time()) if config["display"]["useRemainingTime"]: activity["timestamps"] = { "end": round(currentTimestamp + ((item.duration - viewOffset) / 1000)) } else: activity["timestamps"] = { "start": round(currentTimestamp - (viewOffset / 1000)) } if not self.discordRpcService.connected: self.discordRpcService.connect() if self.discordRpcService.connected: self.discordRpcService.setActivity(activity) except: self.logger.exception( "An unexpected error occured in the alert handler")