Esempio n. 1
0
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)
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)
Esempio n. 4
0
    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)
Esempio n. 5
0
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()
Esempio n. 6
0
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()
Esempio n. 7
0
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("")
Esempio n. 8
0
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 ""
Esempio n. 9
0
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
Esempio n. 10
0
    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)
Esempio n. 11
0
    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)
Esempio n. 12
0
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)
Esempio n. 14
0
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
    ]
Esempio n. 15
0
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
Esempio n. 16
0
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'}))
Esempio n. 17
0
    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')
Esempio n. 18
0
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
Esempio n. 19
0
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
Esempio n. 20
0
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()
Esempio n. 21
0
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
Esempio n. 22
0
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
Esempio n. 24
0
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")