Exemple #1
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 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 server
        Returns
        -------
        sections: dict
            {section title: section object}
        """
        sections = {
            section.title: section
            for section in self.server.library.sections()
        }

        return sections

    def all_sections_totals(self, library=None):
        """All sections total items
        Returns
        -------
        section_totals: dict or int
            {section title: section object} or int
        """
        section_totals = {}
        if library:
            sections = [self.all_sections()[library]]
        else:
            sections = self.all_sections()
        for section in sections:
            if section.type == 'movie':
                section_total = len(section.all())
            elif section.type == 'show':
                section_total = len(section.search(libtype='episode'))
            else:
                continue

            if library:
                return section_total

            section_totals[section.title] = section_total

        return section_totals
Exemple #2
0
def choose_managed_user(account: MyPlexAccount):
    users = [u.title for u in account.users()]
    if not users:
        return None

    click.echo(success("Managed user(s) found:"))
    users = sorted(users)
    users.insert(0, account.username)
    user = inquirer.select(
        message="Select the user you would like to use:",
        choices=users,
        default=None,
        style=style,
        qmark="",
        pointer=">",
    ).execute()

    if user == account.username:
        return None

    # Sanity check, even the user can't input invalid user
    user_account = account.user(user)
    if user_account:
        return user

    return None
Exemple #3
0
def choose_managed_user(account: MyPlexAccount):
    users = [u.title for u in account.users() if u.friend]
    if not users:
        return None

    click.echo(success("Managed user(s) found:"))
    users = sorted(users)
    for user in users:
        click.echo(f"- {user}")

    if not click.confirm(PROMPT_MANAGED_USER):
        return None

    # choice = prompt_choice(users)
    user = click.prompt(
        title("Please select:"),
        type=Choice(users),
        show_default=True,
    )

    # Sanity check, even the user can't input invalid user
    user_account = account.user(user)
    if user_account:
        return user

    return None
Exemple #4
0
def get_friends(config):
    token = config['plex']['token']
    account = MyPlexAccount(token)
    columns = ['email', 'cleaned_email']
    friends = [(user.email, clean_email(user.email))
               for user in account.users() if user.friend]
    frame = pd.DataFrame(friends, columns=columns)
    return frame
Exemple #5
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
Exemple #6
0
def get_env_data():
    trakt.core.CONFIG_PATH = pytrakt_file
    plex_needed = util.input_yesno(
        "-- Plex --\nAre you logged into this server with a Plex account?")
    if plex_needed:
        username = input("Please enter your Plex username: "******"Please enter your Plex password: "******"Now enter the server name: ")
        account = MyPlexAccount(username, password)
        plex = account.resource(
            servername).connect()  # returns a PlexServer instance
        token = plex._token
        users = account.users()
        if users:
            print("Managed user(s) found:")
            for user in users:
                if user.friend is True:
                    print(user.title)
            print("If you want to use a managed user enter its username,")
            name = input(
                "if you want to use your main account just press enter: ")
            while name:
                try:
                    useraccount = account.user(name)
                except:
                    if name != "_wrong":
                        print("Unknown username!")
                    name = input(
                        "Please enter a managed username (or just press enter to use your main account): "
                    )
                    if not name:
                        print("Ok, continuing with your account " + username)
                        break
                    continue
                try:
                    token = account.user(name).get_token(
                        plex.machineIdentifier)
                    username = name
                    break
                except:
                    print("Impossible to find the managed user \'" + name +
                          "\' on this server!")
                    name = "_wrong"
        with open(env_file, 'w') as txt:
            txt.write("PLEX_USERNAME="******"\n")
            txt.write("PLEX_TOKEN=" + token + "\n")
            txt.write("PLEX_BASEURL=" + plex._baseurl + "\n")
            txt.write("PLEX_FALLBACKURL=http://localhost:32400\n")
        print("Plex token and baseurl for {} have been added in .env file:".
              format(username))
        print("PLEX_TOKEN={}".format(token))
        print("PLEX_BASEURL={}".format(plex._baseurl))
    else:
        with open(env_file, "w") as txt:
            txt.write("PLEX_USERNAME=-\n")
            txt.write("PLEX_TOKEN=-\n")
            txt.write("PLEX_BASEURL=http://localhost:32400\n")

    trakt.core.AUTH_METHOD = trakt.core.DEVICE_AUTH
    print("-- Trakt --")
    client_id, client_secret = trakt.core._get_client_info()
    trakt.init(client_id=client_id, client_secret=client_secret, store=True)
    trakt_user = trakt.users.User('me')
    with open(env_file, "a") as txt:
        txt.write("TRAKT_USERNAME="******"\n")
    print(
        "You are now logged into Trakt. Your Trakt credentials have been added in .env and .pytrakt.json files."
    )
    print("You can enjoy sync! \nCheck config.json to adjust settings.")
    print(
        "If you want to change Plex or Trakt account, just edit or remove .env and .pytrakt.json files."
    )
Exemple #7
0
def get_env_data():
    trakt.core.CONFIG_PATH = pytrakt_file
    print("")
    #plex_needed = util.input_yesno("Are you logged into hawke.one with your Plex account? (almost certainly yes)")
    plex_needed = 1
    if plex_needed:
        username = input("    Please enter your Plex username: "******"    Please enter your Plex password: "******"\nNext we will need your server name:\n\n    The server name is displayed top left (under the library title) when viewing a library on that server:\n    https://app.plex.tv/desktop \n\n    For Hawke.one your server name will most likely be one of the following:"
        )
        print("     1) plex1.hawke.one (usually for Golden Company)")
        print("     2) plex2.hawke.one (usually for Nightswatch)")
        print("     3) plex3.hawke.one (usually for Kings Guard")
        print("     4) plex4.hawke.one (usually for Nightswatch)")
        servername = input(
            "\n    Please select you server (1-4), or enter your custom server name: "
        )
        if servername == "1": servername = "plex1.hawke.one"
        if servername == "2": servername = "plex2.hawke.one"
        if servername == "3": servername = "plex3.hawke.one"
        if servername == "4": servername = "plex4.hawke.one"
        print("    Verifying server...")
        account = MyPlexAccount(username, password)
        plex = account.resource(
            servername).connect()  # returns a PlexServer instance
        token = plex._token
        users = account.users()
        if users:
            print("\n    The following managed Plex user(s) were found:")
            for user in users:
                if user.friend is True:
                    print("     * " + user.title)
            print(" ")
            name = input(
                "    To use your " + username +
                " account, press enter (else enter the name of a managed user): "
            )
            while name:
                try:
                    useraccount = account.user(name)
                except:
                    if name != "_wrong":
                        print("Unknown username!")
                    name = input(
                        "Please enter a managed username (or just press enter to use your main account): "
                    )
                    if not name:
                        print("Ok, continuing with your account " + username)
                        break
                    continue
                try:
                    token = account.user(name).get_token(
                        plex.machineIdentifier)
                    username = name
                    break
                except:
                    print("    Impossible to find the managed user \'" + name +
                          "\' on this server!")
                    name = "_wrong"
        CONFIG["PLEX_USERNAME"] = username
        CONFIG["PLEX_TOKEN"] = token
        CONFIG["PLEX_BASEURL"] = plex._baseurl
        CONFIG["PLEX_FALLBACKURL"] = "http://localhost:32400"

        print("    User {} configured successfully.".format(username))
        # print("PLEX_TOKEN={}".format(token))
        # print("PLEX_BASEURL={}".format(plex._baseurl))
    else:
        CONFIG["PLEX_USERNAME"] = "******"
        CONFIG["PLEX_TOKEN"] = "-"
        CONFIG["PLEX_BASEURL"] = "http://localhost:32400"

    trakt.core.AUTH_METHOD = trakt.core.DEVICE_AUTH
    print("\n\n")
    print("Now we'll setup the Trakt part of things:")
    print(" ")
    print(
        "    Create the required Trakt client ID and secret by completing the following steps:"
    )

    if platform.system() == "Windows":
        print(
            "      1 - Press enter below to open http://trakt.tv/oauth/applications"
        )
        print("      2 - Login to your Trakt account")
        print("      3 - Press the NEW APPLICATION button")
        print(
            "      4 - Set the NAME field = hawke.one (or something else meaningful)"
        )
        import pyperclip
        pyperclip.copy("urn:ietf:wg:oauth:2.0:oob")
        print(
            "      5 - Set the REDIRECT URL field = urn:ietf:wg:oauth:2.0:oob (This has been copied to your clipboard for you)"
        )
        input(
            "\n    Press Enter to open http://trakt.tv/oauth/applications and complete steps 1-6: "
        )
        webbrowser.open('http://trakt.tv/oauth/applications')
    else:
        print(
            "      1 - Open http://trakt.tv/oauth/applications on any computer"
        )
        print("      2 - Login to your Trakt account")
        print("      3 - Press the NEW APPLICATION button")
        print("      4 - Set the NAME field = hawke.one")
        print(
            "      5 - Set the REDIRECT URL field = urn:ietf:wg:oauth:2.0:oob")

    print("      6 - Press the SAVE APP button")

    print(
        "\n    Once steps 1-6 are completed, please proceed to steps 7-8 below:\n"
    )

    #client_id, client_secret = trakt.core._get_client_info()
    client_id = input("      7 - Copy and paste the displayed Client ID: ")
    client_secret = input(
        "      8 - Copy and paste the displayed Client secret: ")

    if platform.system() == "Windows":
        input(
            "\n    We will now generate a user code and open https://trakt.tv/activate for you to authenticate the app.\n    Press Enter to continue..."
        )
        webbrowser.open('https://trakt.tv/activate')
        print("\n")

    trakt.init(client_id=client_id, client_secret=client_secret, store=True)
    trakt_user = trakt.users.User('me')
    CONFIG["TRAKT_USERNAME"] = trakt_user.username

    print("\n\n")
    print("You're all done!\n")
    print(
        "Plex / Trakt accounts may be altered by re-running setup, or deleting the .env and .pytrakt.json files."
    )
    print("Expert settings may also be altered within the config.json file.")
    print(" ")
    print(" ")

    CONFIG.save()
Exemple #8
0
import trakt
import trakt.core
import trakt.users

trakt.core.CONFIG_PATH = path.join(path.dirname(path.abspath(__file__)), ".pytrakt.json")
env_file = path.join(path.dirname(path.abspath(__file__)), ".env")

plex_needed = utils.input_yesno("-- Plex --\nAre you logged into this server with a Plex account?")
if plex_needed:
    username = input("Please enter your Plex username: "******"Please enter your Plex password: "******"Now enter the server name: ")
    account = MyPlexAccount(username, password)
    plex = account.resource(servername).connect()  # returns a PlexServer instance
    token = plex._token
    users = account.users()
    if users:
        print("Managed user(s) found:")
        for user in users:
            if user.friend is True:
                print(user.title)
        print("If you want to use a managed user enter its username,")
        name = input("if you want to use your main account just press enter: ")
        while name:
            try:
                useraccount = account.user(name)
            except:
                if name != "_wrong":
                    print("Unknown username!")
                name = input("Please enter a managed username (or just press enter to use your main account): ")
                if not name:
# Do not edit past this line #
account = MyPlexAccount(PLEX_USERNAME, PLEX_PASSWORD)

session = Session()
session.params = {'apikey': TAUTULLI_API_KEY}
formatted_url = f'{TAUTULLI_URL}/api/v2'

request = session.get(formatted_url, params={'cmd': 'get_user_names'})

tautulli_users = None
try:
    tautulli_users = request.json()['response']['data']
except JSONDecodeError:
    exit("Error talking to Tautulli API, please check your TAUTULLI_URL")

plex_friend_ids = [friend.id for friend in account.users()]
removed_users = [
    user for user in tautulli_users if user['user_id'] not in plex_friend_ids
]

if BACKUP_DB:
    backup = session.get(formatted_url, params={'cmd': 'backup_db'})

if removed_users:
    for user in removed_users:
        removed_user = session.get(formatted_url,
                                   params={
                                       'cmd': 'delete_user',
                                       'user_id': user['user_id']
                                   })
        print(f"Removed {user['friendly_name']} from Tautulli")
class PlexAttributes():
    def __init__(self, opts):
        self.opts = opts  # command line options
        self.clsnames = [c for c in opts.clsnames.split(',')
                         if c]  # list of clsnames to report (blank=all)
        self.account = MyPlexAccount()  # MyPlexAccount instance
        self.plex = PlexServer()  # PlexServer instance
        self.total = 0  # Total objects parsed
        self.attrs = defaultdict(dict)  # Attrs result set

    def run(self):
        starttime = time.time()
        self._parse_myplex()
        self._parse_server()
        self._parse_search()
        self._parse_library()
        self._parse_audio()
        self._parse_photo()
        self._parse_movie()
        self._parse_show()
        self._parse_client()
        self._parse_playlist()
        self._parse_sync()
        self.runtime = round((time.time() - starttime) / 60.0, 1)
        return self

    def _parse_myplex(self):
        self._load_attrs(self.account, 'myplex')
        self._load_attrs(self.account.devices(), 'myplex')
        for resource in self.account.resources():
            self._load_attrs(resource, 'myplex')
            self._load_attrs(resource.connections, 'myplex')
        self._load_attrs(self.account.users(), 'myplex')

    def _parse_server(self):
        self._load_attrs(self.plex, 'serv')
        self._load_attrs(self.plex.account(), 'serv')
        self._load_attrs(self.plex.history()[:50], 'hist')
        self._load_attrs(self.plex.history()[50:], 'hist')
        self._load_attrs(self.plex.sessions(), 'sess')

    def _parse_search(self):
        for search in ('cre', 'ani', 'mik', 'she', 'bea'):
            self._load_attrs(self.plex.search(search), 'hub')

    def _parse_library(self):
        cat = 'lib'
        self._load_attrs(self.plex.library, cat)
        # self._load_attrs(self.plex.library.all()[:50], 'all')
        self._load_attrs(self.plex.library.onDeck()[:50], 'deck')
        self._load_attrs(self.plex.library.recentlyAdded()[:50], 'add')
        for search in ('cat', 'dog', 'rat', 'gir', 'mou'):
            self._load_attrs(self.plex.library.search(search)[:50], 'srch')
        # TODO: Implement section search (remove library search?)
        # TODO: Implement section search filters

    def _parse_audio(self):
        cat = 'lib'
        for musicsection in self.plex.library.sections():
            if musicsection.TYPE == library.MusicSection.TYPE:
                self._load_attrs(musicsection, cat)
                for artist in musicsection.all():
                    self._load_attrs(artist, cat)
                    for album in artist.albums():
                        self._load_attrs(album, cat)
                        for track in album.tracks():
                            self._load_attrs(track, cat)

    def _parse_photo(self):
        cat = 'lib'
        for photosection in self.plex.library.sections():
            if photosection.TYPE == library.PhotoSection.TYPE:
                self._load_attrs(photosection, cat)
                for photoalbum in photosection.all():
                    self._load_attrs(photoalbum, cat)
                    for photo in photoalbum.photos():
                        self._load_attrs(photo, cat)

    def _parse_movie(self):
        cat = 'lib'
        for moviesection in self.plex.library.sections():
            if moviesection.TYPE == library.MovieSection.TYPE:
                self._load_attrs(moviesection, cat)
                for movie in moviesection.all():
                    self._load_attrs(movie, cat)

    def _parse_show(self):
        cat = 'lib'
        for showsection in self.plex.library.sections():
            if showsection.TYPE == library.ShowSection.TYPE:
                self._load_attrs(showsection, cat)
                for show in showsection.all():
                    self._load_attrs(show, cat)
                    for season in show.seasons():
                        self._load_attrs(season, cat)
                        for episode in season.episodes():
                            self._load_attrs(episode, cat)

    def _parse_client(self):
        for device in self.account.devices():
            client = self._safe_connect(device)
            if client is not None:
                self._load_attrs(client, 'myplex')
        for client in self.plex.clients():
            self._safe_connect(client)
            self._load_attrs(client, 'client')

    def _parse_playlist(self):
        for playlist in self.plex.playlists():
            self._load_attrs(playlist, 'pl')
            for item in playlist.items():
                self._load_attrs(item, 'pl')
            playqueue = PlayQueue.create(self.plex, playlist)
            self._load_attrs(playqueue, 'pq')

    def _parse_sync(self):
        # TODO: Get plexattrs._parse_sync() working.
        pass

    def _load_attrs(self, obj, cat=None):
        if isinstance(obj, (list, tuple)):
            return [self._parse_objects(item, cat) for item in obj]
        self._parse_objects(obj, cat)

    def _parse_objects(self, obj, cat=None):
        clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__)
        clsname = clsname.replace('plexapi.', '')
        if self.clsnames and clsname not in self.clsnames:
            return None
        self._print_the_little_dot()
        if clsname not in self.attrs:
            self.attrs[clsname] = copy.deepcopy(NAMESPACE)
        self.attrs[clsname]['total'] += 1
        self._load_xml_attrs(clsname, obj._data, self.attrs[clsname]['xml'],
                             self.attrs[clsname]['examples'],
                             self.attrs[clsname]['categories'], cat)
        self._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'],
                             self.attrs[clsname]['docs'])

    def _print_the_little_dot(self):
        self.total += 1
        if not self.total % 100:
            sys.stdout.write('.')
            if not self.total % 8000:
                sys.stdout.write('\n')
            sys.stdout.flush()

    def _load_xml_attrs(self, clsname, elem, attrs, examples, categories, cat):
        if elem is None: return None
        for attr in sorted(elem.attrib.keys()):
            attrs[attr] += 1
            if cat: categories[attr].add(cat)
            if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples:
                examples[attr].add(elem.attrib[attr])
            for subelem in elem:
                attrname = TAGATTRS.get(subelem.tag,
                                        '%ss' % subelem.tag.lower())
                attrs['%s[]' % attrname] += 1

    def _load_obj_attrs(self, clsname, obj, attrs, docs):
        if clsname in STOP_RECURSING_AT: return None
        if isinstance(obj, PlexObject) and clsname not in DONT_RELOAD:
            self._safe_reload(obj)
        alldocs = '\n\n'.join(self._all_docs(obj.__class__))
        for attr, value in obj.__dict__.items():
            if value is None or isinstance(value,
                                           (str, bool, float, int, datetime)):
                if not attr.startswith('_') and attr not in IGNORES.get(
                        clsname, []):
                    attrs[attr] += 1
                    if re.search('\s{8}%s\s\(.+?\)\:' % attr,
                                 alldocs) is not None:
                        docs[attr] += 1
            if isinstance(value, list):
                if not attr.startswith('_') and attr not in IGNORES.get(
                        clsname, []):
                    if value and isinstance(value[0], PlexObject):
                        attrs['%s[]' % attr] += 1
                        [self._parse_objects(obj) for obj in value]

    def _all_docs(self, cls, docs=None):
        import inspect
        docs = docs or []
        if cls.__doc__ is not None:
            docs.append(cls.__doc__)
        for parent in inspect.getmro(cls):
            if parent != cls:
                docs += self._all_docs(parent)
        return docs

    def print_report(self):
        total_attrs = 0
        for clsname in sorted(self.attrs.keys()):
            if self._clsname_match(clsname):
                meta = self.attrs[clsname]
                count = meta['total']
                print(_('\n%s (%s)\n%s' % (clsname, count, '-' * 30),
                        'yellow'))
                attrs = sorted(
                    set(list(meta['xml'].keys()) + list(meta['obj'].keys())))
                for attr in attrs:
                    state = self._attr_state(clsname, attr, meta)
                    count = meta['xml'].get(attr, 0)
                    categories = ','.join(meta['categories'].get(attr, ['--']))
                    examples = '; '.join(
                        list(meta['examples'].get(attr, ['--']))[:3])[:80]
                    print('%7s  %3s  %-30s  %-20s  %s' %
                          (count, state, attr, categories, examples))
                    total_attrs += count
        print(_('\nSUMMARY\n%s' % ('-' * 30), 'yellow'))
        print('%7s  %3s  %3s  %3s  %-20s  %s' %
              ('total', 'new', 'old', 'doc', 'categories', 'clsname'))
        for clsname in sorted(self.attrs.keys()):
            if self._clsname_match(clsname):
                print('%7s  %12s  %12s  %12s  %s' %
                      (self.attrs[clsname]['total'],
                       _(self.attrs[clsname]['new'] or '',
                         'cyan'), _(self.attrs[clsname]['old'] or '', 'red'),
                       _(self.attrs[clsname]['doc'] or '', 'purple'), clsname))
        print('\nPlex Version     %s' % self.plex.version)
        print('PlexAPI Version  %s' % plexapi.VERSION)
        print('Total Objects    %s' %
              sum([x['total'] for x in self.attrs.values()]))
        print('Runtime          %s min\n' % self.runtime)

    def _clsname_match(self, clsname):
        if not self.clsnames:
            return True
        for cname in self.clsnames:
            if cname.lower() in clsname.lower():
                return True
        return False

    def _attr_state(self, clsname, attr, meta):
        if attr in meta['xml'].keys() and attr not in meta['obj'].keys():
            self.attrs[clsname]['new'] += 1
            return _('new', 'blue')
        if attr not in meta['xml'].keys() and attr in meta['obj'].keys():
            self.attrs[clsname]['old'] += 1
            return _('old', 'red')
        if attr not in meta['docs'].keys() and attr in meta['obj'].keys():
            self.attrs[clsname]['doc'] += 1
            return _('doc', 'purple')
        return _('   ', 'green')

    def _safe_connect(self, elem):
        try:
            return elem.connect()
        except:
            return None

    def _safe_reload(self, elem):
        try:
            elem.reload()
        except:
            pass
class 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
    "½": "&frac12;",
    "⅓": "&frac13;"
}

today = datetime.date.today()
notification_date = today - datetime.timedelta(
    days=defaults['notification_period'])
notification_epoch = date_to_epoch("%s 00:00:00" % notification_date)

print("Logging into Plex as %s" % defaults['plex_user'])
plexAccount = MyPlexAccount(defaults['plex_user'], defaults['plex_pass'])
plexServer = PlexServer(plex_local_url, plexAccount._token)

recent = plexServer.library.recentlyAdded()
user_emails = [plexAccount.email]
users = plexAccount.users()
for user in users:
    user_emails.append(user.email)

print("Email recipients: %s" % ', '.join(user_emails))

recent_items = []
for video in recent:
    if video.type == "movie" and date_to_epoch(
            video.addedAt) > notification_epoch:
        recent_item = {}
        recent_item['title'] = "".join(
            html_escape_table.get(c, c) for c in video.title)
        #  This allows movie posters to be loaded
        recent_item['poster_url'] = video.thumbUrl.replace(
            plex_local_url, defaults['base_url'])