Пример #1
0
def getPlexTracks(plex: PlexServer, spotifyTracks: []) -> List[Track]:
    plexTracks = []
    for spotifyTrack in spotifyTracks:
        if spotifyTrack['track'] == None:
            continue
        track = spotifyTrack['track']

        try:
            musicTracks = plex.search(track['artist'], mediatype='artist')
        except:
            try:
                musicTracks = plex.search(track['name'], mediatype='artist')
            except:
                logging.info("Issue making plex request")
                continue

        if len(musicTracks) > 0:
            plexMusic = filterPlexArray(musicTracks, track['name'],
                                        track['artists'][0]['name'])
            if len(plexMusic) > 0:
                plexTracks.append(plexMusic[0])
            else:
                logging.info("Missing Plex Song: %s by %s" %
                             (track['name'], track['artists'][0]['name']))

    return plexTracks
def getPlexTracks(plex: PlexServer, spotifyTracks: []) -> List[Track]:
    plexTracks = []
    for spotifyTrack in spotifyTracks:
        track = spotifyTrack['track']
        logging.info("Searching Plex for: %s by %s" %
                     (track['name'], track['artists'][0]['name']))

        try:
            musicTracks = plex.search(track['name'], mediatype='track')
        except:
            try:
                musicTracks = plex.search(track['name'], mediatype='track')
            except:
                logging.info("Issue making plex request")
                continue

        if len(musicTracks) > 0:
            plexMusic = filterPlexArray(musicTracks, track['name'],
                                        track['artists'][0]['name'])
            if len(plexMusic) > 0:
                logging.info("Found Plex Song: %s by %s" %
                             (track['name'], track['artists'][0]['name']))
                plexTracks.append(plexMusic[0])
            else:
                logging.info("Couldn't find Spotify Song: %s by %s" %
                             (track['name'], track['artists'][0]['name']))
    return plexTracks
Пример #3
0
        def _do_search():
            from plexapi.server import PlexServer
            plex = PlexServer(self._server, None)

            matches = plex.search(search_term)
            if matches:
                match = matches[0]
                self.searches[match.title] = match.unwatched()
            else:
                _LOGGER.warning('No matches for %s', search_term)
Пример #4
0
    def get_needed_data(self, data: dict, data_needed: list) -> (bool, dict):
        server = PlexServer(
            'http://{0}:{1}'.format(self.config.PLEX_HOST,
                                    self.config.PLEX_PORT),
            self.config.PLEX_TOKEN)
        plex_data = server.search(data['title'])
        if data['media_type'] == MediaType.FILMS:
            result = self.check_film(plex_data, data)
        else:
            result = self.check_serials(plex_data, data)

        return not len(result) == 0, {'media_in_plex': not len(result) == 0}
Пример #5
0
class PlexHook(_ServerHook):
    def __init__(self, plex_host, plex_token):
        self._plex = PlexServer(plex_host, plex_token)

    def find_movie(self, criteria, mediatype='movie'):
        ret = []
        for video in self._plex.search(criteria, mediatype=mediatype):
            print(video)
            ret.append(video)
        return ret

    def update_library(self, name='Recommended'):
        library = self._plex.library.section(name)
        library.update()
Пример #6
0
    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}'
                    )
                    if self.server_info is True:
                        print(
                            f'Plex version: {this_server_connection.version}\n"'
                            f"OS: {this_server_connection.platform} {this_server_connection.platformVersion}"
                        )

                    # 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
Пример #7
0
def data():
    baseurl = 'http://' + SERVER_IP + ':' + PORT
    plex = PlexServer(baseurl, TOKEN)
    account = plex.myPlexAccount()

    player = request.form.get('player')

    for client in plex.clients():
        if (player == client.name):
            media = plex.search(request.form.get('title'))
            player.playMedia(media)
            return render_template('success.html',
                                   art=request.form.get('art'),
                                   player=player,
                                   title=request.form.get('title'))

    return render_template('notfound.html', art=request.form.get('art'))
Пример #8
0
    def parse_data(self, data: dict):
        try:
            server = PlexServer(
                'http://{0}:{1}'.format(self.config.PLEX_HOST,
                                        self.config.PLEX_PORT),
                self.config.PLEX_TOKEN)

            plex_data = server.search(data['title'])
            self.next_data = data.copy()

            if 'serial' in data.keys() and data['serial']:
                result = self.check_serials(plex_data, data)
            else:
                result = self.check_film(plex_data, data)

            return len(result) == 0
        except Exception as ex:
            logger.debug(ex)
            self.next_data = data.copy()
            return True
Пример #9
0
class PlexAPI:
    def __init__(self, params):
        try:                                                                    self.PlexServer = PlexServer(params["plex"]["url"], params["plex"]["token"], timeout=600)
        except Unauthorized:                                                    raise Failed("Plex Error: Plex token is invalid")
        except ValueError as e:                                                 raise Failed("Plex Error: {}".format(e))
        except requests.exceptions.ConnectionError as e:
            util.print_stacktrace()
            raise Failed("Plex Error: Plex url is invalid")
        self.is_movie = params["library_type"] == "movie"
        self.is_show = params["library_type"] == "show"
        self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"] and ((self.is_movie and isinstance(s, MovieSection)) or (self.is_show and isinstance(s, ShowSection)))), None)
        if not self.Plex:                                                       raise Failed("Plex Error: Plex Library {} not found".format(params["name"]))
        try:                                                                    self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8"))
        except yaml.scanner.ScannerError as e:                                  raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t      ")))

        self.metadata = None
        if "metadata" in self.data:
            if self.data["metadata"]:                                               self.metadata = self.data["metadata"]
            else:                                                                   logger.warning("Config Warning: metadata attribute is blank")
        else:                                                                   logger.warning("Config Warning: metadata attribute not found")

        self.collections = None
        if "collections" in self.data:
            if self.data["collections"]:                                            self.collections = self.data["collections"]
            else:                                                                   logger.warning("Config Warning: collections attribute is blank")
        else:                                                                   logger.warning("Config Warning: collections attribute not found")

        if self.metadata is None and self.collections is None:
            raise Failed("YAML Error: metadata attributes or collections attribute required")

        if params["asset_directory"]:
            logger.info("Using Asset Directory: {}".format(params["asset_directory"]))

        self.Radarr = None
        if params["tmdb"] and params["radarr"]:
            logger.info("Connecting to {} library's Radarr...".format(params["name"]))
            try:                                                                    self.Radarr = RadarrAPI(params["tmdb"], params["radarr"])
            except Failed as e:                                                     logger.error(e)
            logger.info("{} library's Radarr Connection {}".format(params["name"], "Failed" if self.Radarr is None else "Successful"))

        self.Sonarr = None
        if params["tvdb"] and params["sonarr"]:
            logger.info("Connecting to {} library's Sonarr...".format(params["name"]))
            try:                                                                    self.Sonarr = SonarrAPI(params["tvdb"], params["sonarr"], self.Plex.language)
            except Failed as e:                                                     logger.error(e)
            logger.info("{} library's Sonarr Connection {}".format(params["name"], "Failed" if self.Sonarr is None else "Successful"))

        self.Tautulli = None
        if params["tautulli"]:
            logger.info("Connecting to {} library's Tautulli...".format(params["name"]))
            try:                                                                    self.Tautulli = TautulliAPI(params["tautulli"])
            except Failed as e:                                                     logger.error(e)
            logger.info("{} library's Tautulli Connection {}".format(params["name"], "Failed" if self.Tautulli is None else "Successful"))

        self.name = params["name"]

        self.missing_path = os.path.join(os.path.dirname(os.path.abspath(params["metadata_path"])), "{}_missing.yml".format(os.path.splitext(os.path.basename(params["metadata_path"]))[0]))
        self.metadata_path = params["metadata_path"]
        self.asset_directory = params["asset_directory"]
        self.sync_mode = params["sync_mode"]
        self.plex = params["plex"]
        self.radarr = params["radarr"]
        self.sonarr = params["sonarr"]
        self.tautulli = params["tautulli"]

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def search(self, title, libtype=None, year=None):
        if libtype is not None and year is not None:        return self.Plex.search(title=title, year=year, libtype=libtype)
        elif libtype is not None:                           return self.Plex.search(title=title, libtype=libtype)
        elif year is not None:                              return self.Plex.search(title=title, year=year)
        else:                                               return self.Plex.search(title=title)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def fetchItem(self, data):
        return self.PlexServer.fetchItem(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def server_search(self, data):
        return self.PlexServer.search(data)

    def get_all_collections(self):
        return self.Plex.search(libtype="collection")

    def get_collection(self, data):
        collection = util.choose_from_list(self.search(str(data), libtype="collection"), "collection", str(data), exact=True)
        if collection:                              return collection
        else:                                       raise Failed("Plex Error: Collection {} not found".format(data))

    def get_item(self, data, year=None):
        if isinstance(data, (int, Movie, Show)):
            try:                                        return self.fetchItem(data.ratingKey if isinstance(data, (Movie, Show)) else data)
            except BadRequest:                          raise Failed("Plex Error: Item {} not found".format(data))
        else:
            item_list = self.search(title=data) if year is None else self.search(data, year=year)
            item = util.choose_from_list(item_list, "movie" if self.is_movie else "show", data)
            if item:                                    return item
            else:                                       raise Failed("Plex Error: Item {} not found".format(data))

    def validate_collections(self, collections):
        valid_collections = []
        for collection in collections:
            try:                                        valid_collections.append(self.get_collection(collection))
            except Failed as e:                         logger.error(e)
        if len(valid_collections) == 0:
            raise Failed("Collection Error: No valid Plex Collections in {}".format(collections[c][m]))
        return valid_collections

    def get_actor_rating_key(self, data):
        movie_rating_key = None
        for result in self.server_search(data):
            entry = str(result).split(":")
            entry[0] = entry[0][1:]
            if entry[0] == "Movie":
                movie_rating_key = int(entry[1])
                break
        if movie_rating_key:
            for role in self.fetchItem(movie_rating_key).roles:
                role = str(role).split(":")
                if data.upper().replace(" ", "-") == role[2][:-1].upper():
                    return int(role[1])
        raise Failed("Plex Error: Actor: {} not found".format(data))

    def get_ids(self, movie):
        tmdb_id = None
        imdb_id = None
        for guid_tag in self.send_request("{}{}".format(self.plex["url"], movie.key)).xpath("//guid/@id"):
            parsed_url = requests.utils.urlparse(guid_tag)
            if parsed_url.scheme == "tmdb":                 tmdb_id = parsed_url.netloc
            elif parsed_url.scheme == "imdb":               imdb_id = parsed_url.netloc
        return tmdb_id, imdb_id

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def send_request(self, url):
        return html.fromstring(requests.get(url, headers={"X-Plex-Token": self.token, "User-Agent": "Mozilla/5.0 x64"}).content)

    def del_collection_if_empty(self, collection):
        missing_data = {}
        if not os.path.exists(self.missing_path):
            with open(self.missing_path, "w"): pass
        try:
            missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path))
            if not missing_data:
                missing_data = {}
            if collection in missing_data and len(missing_data[collection]) == 0:
                del missing_data[collection]
            yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi)
        except yaml.scanner.ScannerError as e:
            logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t      ")))

    def clear_collection_missing(self, collection):
        missing_data = {}
        if not os.path.exists(self.missing_path):
            with open(self.missing_path, "w"): pass
        try:
            missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path))
            if not missing_data:
                missing_data = {}
            if collection in missing_data:
                missing_data[collection.encode("ascii", "replace").decode()] = {}
            yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi)
        except yaml.scanner.ScannerError as e:
            logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t      ")))

    def save_missing(self, collection, items, is_movie):
        missing_data = {}
        if not os.path.exists(self.missing_path):
            with open(self.missing_path, "w"): pass
        try:
            missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path))
            if not missing_data:
                missing_data = {}
            col_name = collection.encode("ascii", "replace").decode()
            if col_name not in missing_data:
                missing_data[col_name] = {}
            section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)"
            if section not in missing_data[col_name]:
                missing_data[col_name][section] = {}
            for title, item_id in items:
                missing_data[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode()
            yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi)
        except yaml.scanner.ScannerError as e:
            logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t      ")))

    def add_to_collection(self, collection, items, filters, map={}):
        name = collection.title if isinstance(collection, Collections) else collection
        collection_items = collection.children if isinstance(collection, Collections) else []

        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            current = self.get_item(item)
            match = True
            if filters:
                length = util.print_return(length, "Filtering {}/{} {}".format((" " * (max_length - len(str(i)))) + str(i), total, current.title))
                for f in filters:
                    modifier = f[0][-4:]
                    method = util.filter_alias[f[0][:-4]] if modifier in [".not", ".lte", ".gte"] else util.filter_alias[f[0]]
                    if method == "max_age":
                        threshold_date = datetime.now() - timedelta(days=f[1])
                        attr = getattr(current, "originallyAvailableAt")
                        if attr is None or attr < threshold_date:
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method == "originallyAvailableAt":
                            threshold_date = datetime.strptime(f[1], "%m/%d/%y")
                            attr = getattr(current, "originallyAvailableAt")
                            if (modifier == ".lte" and attr > threshold_date) or (modifier == ".gte" and attr < threshold_date):
                                match = False
                                break
                        elif method in ["year", "rating"]:
                            attr = getattr(current, method)
                            if (modifier == ".lte" and attr > f[1]) or (modifier == ".gte" and attr < f[1]):
                                match = False
                                break
                    else:
                        terms = f[1] if isinstance(f[1], list) else str(f[1]).split(", ")
                        if method in ["video_resolution", "audio_language", "subtitle_language"]:
                            for media in current.media:
                                if method == "video_resolution":                                                                attrs = [media.videoResolution]
                                for part in media.parts:
                                    if method == "audio_language":                                                                  attrs = ([a.language for a in part.audioStreams()])
                                    if method == "subtitle_language":                                                               attrs = ([s.language for s in part.subtitleStreams()])
                        elif method in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]:          attrs = [str(getattr(current, method))]
                        elif method in ["actors", "countries", "directors", "genres", "writers", "collections"]:        attrs = [getattr(x, "tag") for x in getattr(current, method)]

                        if (not list(set(terms) & set(attrs)) and modifier != ".not") or (list(set(terms) & set(attrs)) and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(length, "Filtering {}/{} {}".format((" " * (max_length - len(str(i)))) + str(i), total, current.title))
            if match:
                util.print_end(length, "{} Collection | {} | {}".format(name, "=" if current in collection_items else "+", current.title))
                if current in collection_items:             map[current.ratingKey] = None
                else:                                       current.addCollection(name)
        media_type = "{}{}".format("Movie" if self.is_movie else "Show", "s" if total > 1 else "")
        util.print_end(length, "{} {} Processed".format(total, media_type))
        return map

    def update_metadata(self):
        logger.info("")
        util.seperator("{} Library Metadata".format(self.name))
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for m in self.metadata:
            logger.info("")
            util.seperator()
            logger.info("")
            year = None
            if "year" in self.metadata[m]:
                now = datetime.datetime.now()
                if self.metadata[m]["year"] is None:                                    logger.error("Metadata Error: year attribute is blank")
                elif not isinstance(self.metadata[m]["year"], int):                     logger.error("Metadata Error: year attribute must be an integer")
                elif self.metadata[m]["year"] not in range(1800, now.year + 2):         logger.error("Metadata Error: year attribute must be between 1800-{}".format(now.year + 1))
                else:                                                                   year = self.metadata[m]["year"]

            alt_title = None
            used_alt = False
            if "alt_title" in self.metadata[m]:
                if self.metadata[m]["alt_title"] is None:                               logger.error("Metadata Error: alt_title attribute is blank")
                else:                                                                   alt_title = self.metadata[m]["alt_title"]

            try:
                item = self.get_item(m, year=year)
            except Failed as e:
                if alt_title:
                    try:
                        item = self.get_item(alt_title, year=year)
                        used_alt = True
                    except Failed as alt_e:
                        logger.error(alt_e)
                        logger.error("Skipping {}".format(m))
                        continue
                else:
                    logger.error(e)
                    logger.error("Skipping {}".format(m))
                    continue

            logger.info("Updating {}: {}...".format("Movie" if self.is_movie else "Show", alt_title if used_alt else m))
            edits = {}
            def add_edit(name, group, key=None, value=None, sub=None):
                if value or name in group:
                    if value or group[name]:
                        if key is None:         key = name
                        if value is None:       value = group[name]
                        if sub and "sub" in group:
                            if group["sub"]:
                                if group["sub"] is True and "(SUB)" not in value:       value = "{} (SUB)".format(value)
                                elif group["sub"] is False and " (SUB)" in value:       value = value[:-6]
                            else:
                                logger.error("Metadata Error: sub attribute is blank")
                        edits["{}.value".format(key)] = value
                        edits["{}.locked".format(key)] = 1
                    else:
                        logger.error("Metadata Error: {} attribute is blank".format(name))
            if used_alt or "sub" in self.metadata[m]:
                add_edit("title", self.metadata[m], value=m, sub=True)
            add_edit("sort_title", self.metadata[m], key="titleSort")
            add_edit("originally_available", self.metadata[m], key="originallyAvailableAt")
            add_edit("rating", self.metadata[m])
            add_edit("content_rating", self.metadata[m], key="contentRating")
            add_edit("original_title", self.metadata[m], key="originalTitle")
            add_edit("studio", self.metadata[m])
            add_edit("tagline", self.metadata[m])
            add_edit("summary", self.metadata[m])
            try:
                item.edit(**edits)
                item.reload()
                logger.info("{}: {} Details Update Successful".format("Movie" if self.is_movie else "Show", m))
            except BadRequest:
                logger.error("{}: {} Details Update Failed".format("Movie" if self.is_movie else "Show", m))
                logger.debug("Details Update: {}".format(edits))
                util.print_stacktrace()

            if "genre" in self.metadata[m]:
                if self.metadata[m]["genre"]:
                    genre_sync = False
                    if "genre_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["genre_sync_mode"] is None:                         logger.error("Metadata Error: genre_sync_mode attribute is blank defaulting to append")
                        elif self.metadata[m]["genre_sync_mode"] not in ["append", "sync"]:     logger.error("Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
                        elif self.metadata[m]["genre_sync_mode"] == "sync":                     genre_sync = True
                    genres = [genre.tag for genre in item.genres]
                    values = util.get_list(self.metadata[m]["genre"])
                    if genre_sync:
                        for genre in (g for g in genres if g not in values):
                            item.removeGenre(genre)
                            logger.info("Detail: Genre {} removed".format(genre))
                    for value in (v for v in values if v not in genres):
                        item.addGenre(value)
                        logger.info("Detail: Genre {} added".format(value))
                else:
                    logger.error("Metadata Error: genre attribute is blank")

            if "label" in self.metadata[m]:
                if self.metadata[m]["label"]:
                    label_sync = False
                    if "label_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["label_sync_mode"] is None:                         logger.error("Metadata Error: label_sync_mode attribute is blank defaulting to append")
                        elif self.metadata[m]["label_sync_mode"] not in ["append", "sync"]:     logger.error("Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
                        elif self.metadata[m]["label_sync_mode"] == "sync":                     label_sync = True
                    labels = [label.tag for label in item.labels]
                    values = util.get_list(self.metadata[m]["label"])
                    if label_sync:
                        for label in (l for l in labels if l not in values):
                            item.removeLabel(label)
                            logger.info("Detail: Label {} removed".format(label))
                    for value in (v for v in values if v not in labels):
                        item.addLabel(v)
                        logger.info("Detail: Label {} added".format(v))
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in self.metadata[m] and self.is_show:
                if self.metadata[m]["seasons"]:
                    for season_id in self.metadata[m]["seasons"]:
                        logger.info("")
                        logger.info("Updating season {} of {}...".format(season_id, alt_title if used_alt else m))
                        if isinstance(season_id, int):
                            try:                                season = item.season(season_id)
                            except NotFound:                    logger.error("Metadata Error: Season: {} not found".format(season_id))
                            else:
                                edits = {}
                                add_edit("title", self.metadata[m]["seasons"][season_id], sub=True)
                                add_edit("summary", self.metadata[m]["seasons"][season_id])
                                try:
                                    season.edit(**edits)
                                    season.reload()
                                    logger.info("Season: {} Details Update Successful".format(season_id))
                                except BadRequest:
                                    logger.debug("Season: {} Details Update: {}".format(season_id, edits))
                                    logger.error("Season: {} Details Update Failed".format(season_id))
                                    util.print_stacktrace()
                        else:
                            logger.error("Metadata Error: Season: {} invalid, it must be an integer".format(season_id))
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in self.metadata[m] and self.is_show:
                if self.metadata[m]["episodes"]:
                    for episode_str in self.metadata[m]["episodes"]:
                        logger.info("")
                        match = re.search("[Ss]{1}\d+[Ee]{1}\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split("E" if "E" in m.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info("Updating episode S{}E{} of {}...".format(episode_id, season_id, alt_title if used_alt else m))
                            try:                                episode = item.episode(season=season_id, episode=episode_id)
                            except NotFound:                    logger.error("Metadata Error: episode {} of season {} not found".format(episode_id, season_id))
                            else:
                                edits = {}
                                add_edit("title", self.metadata[m]["episodes"][episode_str], sub=True)
                                add_edit("sort_title", self.metadata[m]["episodes"][episode_str], key="titleSort")
                                add_edit("rating", self.metadata[m]["episodes"][episode_str])
                                add_edit("originally_available", self.metadata[m]["episodes"][episode_str], key="originallyAvailableAt")
                                add_edit("summary", self.metadata[m]["episodes"][episode_str])
                                try:
                                    episode.edit(**edits)
                                    episode.reload()
                                    logger.info("Season: {} Episode: {} Details Update Successful".format(season_id, episode_id))
                                except BadRequest:
                                    logger.debug("Season: {} Episode: {} Details Update: {}".format(season_id, episode_id, edits))
                                    logger.error("Season: {} Episode: {} Details Update Failed".format(season_id, episode_id))
                                    util.print_stacktrace()
                        else:
                            logger.error("Metadata Error: episode {} invlaid must have S##E## format".format(episode_str))
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
Пример #10
0
class Remote:

    def __init__(self, language, plex_url, client_name):
        self.lang = language
        self.formatHelper = FormatHelper(language)
        self.plex = PlexServer(plex_url)
        self.client = self.plex.client(client_name)

        # last results for context awareness
        self.found_media = []
        self.found_actions = []
        self.last_request = []
        self.last_picked = None

        # filter configuration
        self.filterable_in_episode = ['director', 'writer', 'year', 'decade']
        self.filterable_in_show = ['actor', 'genre', 'contentRating']
        self.filter_config = {'actor': self.formatHelper.filter_person,
                              'director': self.formatHelper.filter_person,
                              'writer': self.formatHelper.filter_person,
                              'producer': self.formatHelper.filter_person,
                              'year': self.formatHelper.filter_year,
                              'decade': self.formatHelper.filter_year,
                              'genre': self.formatHelper.filter_case_insensitive,
                              'country': self.formatHelper.filter_case_insensitive,
                              'contentRating': self.formatHelper.filter_case_insensitive}

        for section in self.plex.library.sections():
            if section.TYPE == 'movie':
                self.movies = section
            elif section.TYPE == 'show':
                self.shows = section

    def execute(self, text):
        print(text)
        commands = self.lang.match(text.lower())
        self.search(commands)
        self.filter_highest_priority()
        return self.execute_actions()

    def search(self, commands):
        self.found_media = []
        self.found_actions = []
        threads = []
        for priority, matched in commands:
            search_actions = [action for action in ['play', 'navigate', 'follow_up'] if action in matched]
            direct_actions = [action for action in ['another_one', 'play_it', 'main_menu', 'subtitle_on',
                                                    'subtitle_off', 'subtitle_toggle', 'language_toggle',
                                                    'osd', 'jump_forward', 'jump_backward',
                                                    'pause', 'play_after_pause'] if action in matched]

            if direct_actions:
                self.found_media.append((priority, direct_actions, None))
            else:
                if 'movie' in matched:
                    function = self.search_movies
                elif 'tv' in matched:
                    function = self.search_episodes
                else:
                    function = self.search_general
                thr = threading.Thread(target=function, args=[matched, priority, search_actions])
                threads.append(thr)
                thr.start()
        for thr in threads:
            thr.join()

    def search_movies(self, matched, priority, actions):
        title = matched.get('title')

        if 'unseen' in matched:
            movie_filter = 'unwatched'
        else:
            movie_filter = 'all'

        multi_filters = self.create_filter(self.movies, matched)
        if multi_filters:
            results = self.movies.search(title, filter=movie_filter, **multi_filters.pop())
            for filters in multi_filters:
                filter_results = self.movies.search(title, filter=movie_filter, **filters)
                results = [result for result in results if result in filter_results]
            for video in self.post_filter(matched, results):
                self.found_media.append((priority, actions, video))

    def search_episodes(self, matched, priority, actions):
        title = matched.get('title')
        season = matched.get('season')

        if 'unseen' in matched:
            watched_filter = 'unwatched'
            matched['oldest'] = None
        else:
            watched_filter = 'all'

        show_multi_filters = self.create_filter(self.shows, filter_dict(matched, self.filterable_in_show))
        episode_multi_filters = self.create_filter(self.shows, filter_dict(matched, self.filterable_in_episode))
        if show_multi_filters is None or episode_multi_filters is None:
            return

        episode_set = []
        used_episode_filter = False
        if episode_multi_filters[0]:
            used_episode_filter = True
            episode_set = self.shows.searchEpisodes(None, filter=watched_filter, **episode_multi_filters.pop())
            for filters in episode_multi_filters:
                filter_episode_set = self.shows.searchEpisodes(title, filter=watched_filter, **filters)
                episode_set = [result for result in episode_set if result in filter_episode_set]

        results = []
        used_show_filter = False
        if show_multi_filters[0] or season or not used_episode_filter or title or watched_filter == 'unwatched':
            used_show_filter = True
            show_set = self.shows.search(title, filter=watched_filter, **show_multi_filters.pop())
            for filters in show_multi_filters:
                filter_show_set = self.shows.search(title, filter=watched_filter, **filters)
                show_set = [result for result in show_set if result in filter_show_set]
            for show in show_set:
                if season:
                    show = show.season(self.formatHelper.season_format(season))
                res = show.episodes(watched='unseen' not in matched)
                results += self.post_filter(matched, res)

        if used_episode_filter:
            if used_show_filter:
                results = [result for result in results if result in episode_set]
            else:
                results = episode_set

        results = self.post_filter(matched, results)
        for video in results:
            self.found_media.append((priority, actions, video))

    def search_general(self, matched, priority, actions):
        title = matched.get('title')
        results = self.plex.search(title)
        results = self.post_filter(matched, results)
        for video in results:
            self.found_media.append((priority, actions, video))

    def create_filter(self, library, matched):
        multi_filter = [{}]
        for key, filter_method in self.filter_config.iteritems():
            entities = matched.get(key)
            if entities:
                server_entities = getattr(library, 'get_' + key)()
                for index, entity in enumerate(entities.split(self.lang.and_phrase())):
                    res = filter_method(server_entities, entity)
                    if res:
                        if len(multi_filter) <= index:
                            multi_filter.append(multi_filter[0].copy())
                        filters = multi_filter[index]
                        filters[key] = res
                    else:
                        return None

        return multi_filter

    @staticmethod
    def post_filter(matched, results):
        if 'higher_rating' in matched and results:
            border = float(matched['higher_rating'])
            results = [video for video in results if hasattr(video, 'rating') and float(video.rating) > border]

        if 'lower_rating' in matched and results:
            border = float(matched['lower_rating'])
            results = [video for video in results if hasattr(video, 'rating') and float(video.rating) < border]

        if 'newest' in matched and len(results) > 1:
            newest = results[0]
            newest_date = datetime(1, 1, 1)
            for video in results:
                if hasattr(video, 'originallyAvailableAt') and video.originallyAvailableAt > newest_date:
                    newest_date = video.originallyAvailableAt
                    newest = video
            return [newest]

        if 'oldest' in matched and len(results) > 1:
            oldest = results[0]
            oldest_date = datetime(9999, 1, 1)
            for video in results:
                if hasattr(video, 'originallyAvailableAt') and video.originallyAvailableAt < oldest_date:
                    oldest_date = video.originallyAvailableAt
                    oldest = video
            return [oldest]
        return results

    def filter_highest_priority(self):
        highest_priority = -1
        for priority, actions, media in self.found_media:
            highest_priority = max(priority, highest_priority)

        filtered = []
        highest_priority_actions = []
        for priority, actions, media in self.found_media:
            if priority == highest_priority:
                highest_priority_actions += actions
                filtered.append(media)
        self.found_actions = highest_priority_actions
        self.found_media = filtered

    def execute_actions(self):
        if self.found_actions:
            print(self.found_actions[0])

        # direct actions
        if 'main_menu' in self.found_actions:
            self.client.stop()
        if 'subtitle_on' in self.found_actions:
            self.client.subtitle('on')
        if 'subtitle_off' in self.found_actions:
            self.client.subtitle('off')
        if 'subtitle_toggle' in self.found_actions:
            self.client.subtitle('next')
        if 'language_toggle' in self.found_actions:
            self.client.switch_language()
        if 'osd' in self.found_actions:
            self.client.toggleOSD()
        if 'jump_forward' in self.found_actions:
            self.client.stepForward()
        if 'jump_backward' in self.found_actions:
            self.client.stepBack()
        if 'pause' in self.found_actions:
            self.client.pause()
        if 'play_after_pause' in self.found_actions:
            self.client.play()
        if 'another_one' in self.found_actions:
            self.last_picked = self.pick_another_one()
            if self.last_picked:
                self.client.navigate(self.last_picked)
        if 'play_it' in self.found_actions:
            if self.last_picked:
                self.client.playMedia(self.last_picked)

        # search actions
        if 'follow_up' in self.found_actions or 'play' in self.found_actions or 'navigate' in self.found_actions:
            if 'follow_up' in self.found_actions:
                self.found_media = [f for f in self.found_media if f in self.last_request]

            self.last_picked = self.pick_one()
            self.last_request = self.found_media
            if self.last_picked:
                if 'play' in self.found_actions:
                    print('play ' + str(self.last_picked))
                    self.client.playMedia(self.last_picked)
                elif 'navigate' in self.found_actions:
                    print('go to ' + str(self.last_picked))
                    self.client.navigate(self.last_picked)
        return len(self.found_actions) > 0

    def pick_one(self):
        if len(self.found_media) == 0:
            return None
        pos = Random().randint(0, len(self.found_media) - 1)
        return self.found_media[pos]

    def pick_another_one(self):
        if len(self.last_request) == 0:
            return None
        if len(self.last_request) == 1:
            return self.last_request[0]

        video = None
        while not video or video == self.last_picked:
            video = self.last_request[Random().randint(0, len(self.last_request) - 1)]
        return video
Пример #11
0
from plexapi.server import PlexServer
import random

baseurl = input("Type ip and port of your Plex server:")
token = input("Type your authentication token (https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/):")
plex = PlexServer("http://" + baseurl, token)


TVShows = []
Episodes = []
PlayList = []
fileoftvshows = open("Shows.txt", "r")

for show in fileoftvshows:
    found = plex.search(show, mediatype="show")
    TVShows.append(found[0])


for i in range(len(TVShows)):
    reversedepisodes = TVShows[i].episodes()
    reversedepisodes.reverse()
    Episodes.append(reversedepisodes)


while len(Episodes) != 0:
    show = random.randint(0, len(Episodes)-1)
    PlayList.append(Episodes[show].pop())
    if len(Episodes[show]) == 0:
        del Episodes[show]

nameofplaylist = input("Type name of playlist:")
Пример #12
0
class PlexAPI:
    def __init__(self, params, TMDb, TVDb):
        try:
            self.PlexServer = PlexServer(params["plex"]["url"], params["plex"]["token"], timeout=params["plex"]["timeout"])
        except Unauthorized:
            raise Failed("Plex Error: Plex token is invalid")
        except ValueError as e:
            raise Failed(f"Plex Error: {e}")
        except requests.exceptions.ConnectionError:
            util.print_stacktrace()
            raise Failed("Plex Error: Plex url is invalid")
        self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"]), None)
        if not self.Plex:
            raise Failed(f"Plex Error: Plex Library {params['name']} not found")
        if self.Plex.type not in ["movie", "show"]:
            raise Failed(f"Plex Error: Plex Library must be a Movies or TV Shows library")

        self.agent = self.Plex.agent
        self.is_movie = self.Plex.type == "movie"
        self.is_show = self.Plex.type == "show"

        logger.info(f"Using Metadata File: {params['metadata_path']}")
        try:
            self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8"))
        except yaml.scanner.ScannerError as ye:
            raise Failed(f"YAML Error: {util.tab_new_lines(ye)}")
        except Exception as e:
            util.print_stacktrace()
            raise Failed(f"YAML Error: {e}")

        def get_dict(attribute):
            if attribute in self.data:
                if self.data[attribute]:
                    if isinstance(self.data[attribute], dict):
                        return self.data[attribute]
                    else:
                        logger.warning(f"Config Warning: {attribute} must be a dictionary")
                else:
                    logger.warning(f"Config Warning: {attribute} attribute is blank")
            return None

        self.metadata = get_dict("metadata")
        self.templates = get_dict("templates")
        self.collections = get_dict("collections")

        if self.metadata is None and self.collections is None:
            raise Failed("YAML Error: metadata attributes or collections attribute required")

        if params["asset_directory"]:
            for ad in params["asset_directory"]:
                logger.info(f"Using Asset Directory: {ad}")

        self.TMDb = TMDb
        self.TVDb = TVDb
        self.Radarr = None
        self.Sonarr = None
        self.Tautulli = None
        self.name = params["name"]
        self.missing_path = os.path.join(os.path.dirname(os.path.abspath(params["metadata_path"])), f"{os.path.splitext(os.path.basename(params['metadata_path']))[0]}_missing.yml")
        self.metadata_path = params["metadata_path"]
        self.asset_directory = params["asset_directory"]
        self.asset_folders = params["asset_folders"]
        self.assets_for_all = params["assets_for_all"]
        self.sync_mode = params["sync_mode"]
        self.show_unmanaged = params["show_unmanaged"]
        self.show_filtered = params["show_filtered"]
        self.show_missing = params["show_missing"]
        self.save_missing = params["save_missing"]
        self.mass_genre_update = params["mass_genre_update"]
        self.plex = params["plex"]
        self.url = params["plex"]["url"]
        self.token = params["plex"]["token"]
        self.timeout = params["plex"]["timeout"]
        self.missing = {}
        self.run_again = []

    def get_all_collections(self):
        return self.search(libtype="collection")

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def search(self, title=None, libtype=None, sort=None, maxresults=None, **kwargs):
        return self.Plex.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def fetchItem(self, data):
        return self.PlexServer.fetchItem(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def get_all(self):
        return self.Plex.all()

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def server_search(self, data):
        return self.PlexServer.search(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def add_collection(self, item, name):
        item.addCollection(name)

    @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
    def get_search_choices(self, search_name):
        try:
            choices = {}
            for choice in self.Plex.listFilterChoices(search_name):
                choices[choice.title.lower()] = choice.title
                choices[choice.key.lower()] = choice.title
            return choices
        except NotFound:
            raise Failed(f"Collection Error: plex search attribute: {search_name} only supported with Plex's New TV Agent")

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def refresh_item(self, rating_key):
        requests.put(f"{self.url}/library/metadata/{rating_key}/refresh?X-Plex-Token={self.token}")

    def validate_search_list(self, data, search_name):
        final_search = search_translation[search_name] if search_name in search_translation else search_name
        search_choices = self.get_search_choices(final_search)
        valid_list = []
        for value in util.get_list(data):
            if str(value).lower() in search_choices:
                valid_list.append(search_choices[str(value).lower()])
            else:
                logger.error(f"Plex Error: {search_name}: {value} not found")
        return valid_list

    def get_collection(self, data):
        collection = util.choose_from_list(self.search(title=str(data), libtype="collection"), "collection", str(data), exact=True)
        if collection:                              return collection
        else:                                       raise Failed(f"Plex Error: Collection {data} not found")

    def validate_collections(self, collections):
        valid_collections = []
        for collection in collections:
            try:                                        valid_collections.append(self.get_collection(collection))
            except Failed as e:                         logger.error(e)
        if len(valid_collections) == 0:
            raise Failed(f"Collection Error: No valid Plex Collections in {collections}")
        return valid_collections

    def get_items(self, method, data, status_message=True):
        if status_message:
            logger.debug(f"Data: {data}")
        pretty = util.pretty_names[method] if method in util.pretty_names else method
        media_type = "Movie" if self.is_movie else "Show"
        items = []
        if method == "plex_all":
            if status_message:
                logger.info(f"Processing {pretty} {media_type}s")
            items = self.get_all()
        elif method == "plex_collection":
            if status_message:
                logger.info(f"Processing {pretty} {data}")
            items = data.items()
        elif method == "plex_search":
            search_terms = {}
            has_processed = False
            search_limit = None
            search_sort = None
            for search_method, search_data in data.items():
                if search_method == "limit":
                    search_limit = search_data
                elif search_method == "sort_by":
                    search_sort = search_data
                else:
                    search, modifier = os.path.splitext(str(search_method).lower())
                    final_search = search_translation[search] if search in search_translation else search
                    if search in ["added", "originally_available"] and modifier == "":
                        final_mod = ">>"
                    elif search in ["added", "originally_available"] and modifier == ".not":
                        final_mod = "<<"
                    elif search in ["critic_rating", "audience_rating"] and modifier == ".greater":
                        final_mod = "__gte"
                    elif search in ["critic_rating", "audience_rating"] and modifier == ".less":
                        final_mod = "__lt"
                    else:
                        final_mod = modifiers[modifier] if modifier in modifiers else ""
                    final_method = f"{final_search}{final_mod}"

                    if search == "duration":
                        search_terms[final_method] = search_data * 60000
                    elif search in ["added", "originally_available"] and modifier in ["", ".not"]:
                        search_terms[final_method] = f"{search_data}d"
                    else:
                        search_terms[final_method] = search_data

                    if status_message:
                        if search in ["added", "originally_available"] or modifier in [".greater", ".less", ".before", ".after"]:
                            ors = f"{search_method}({search_data}"
                        else:
                            ors = ""
                            conjunction = " AND " if final_mod == "&" else " OR "
                            for o, param in enumerate(search_data):
                                or_des = conjunction if o > 0 else f"{search_method}("
                                ors += f"{or_des}{param}"
                        if has_processed:
                            logger.info(f"\t\t      AND {ors})")
                        else:
                            logger.info(f"Processing {pretty}: {ors})")
                            has_processed = True
            if status_message:
                if search_sort:
                    logger.info(f"\t\t      SORT BY {search_sort})")
                if search_limit:
                    logger.info(f"\t\t      LIMIT {search_limit})")
                logger.debug(f"Search: {search_terms}")
            return self.search(sort=sorts[search_sort], maxresults=search_limit, **search_terms)
        elif method == "plex_collectionless":
            good_collections = []
            for col in self.get_all_collections():
                keep_collection = True
                for pre in data["exclude_prefix"]:
                    if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)):
                        keep_collection = False
                        break
                if keep_collection:
                    for ext in data["exclude"]:
                        if col.title == ext or (col.titleSort and col.titleSort == ext):
                            keep_collection = False
                            break
                if keep_collection:
                    good_collections.append(col.index)
            all_items = self.get_all()
            length = 0
            for i, item in enumerate(all_items, 1):
                length = util.print_return(length, f"Processing: {i}/{len(all_items)} {item.title}")
                add_item = True
                item.reload()
                for collection in item.collections:
                    if collection.id in good_collections:
                        add_item = False
                        break
                if add_item:
                    items.append(item)
            util.print_end(length, f"Processed {len(all_items)} {'Movies' if self.is_movie else 'Shows'}")
        else:
            raise Failed(f"Plex Error: Method {method} not supported")
        if len(items) > 0:
            return items
        else:
            raise Failed("Plex Error: No Items found in Plex")

    def add_missing(self, collection, items, is_movie):
        col_name = collection.encode("ascii", "replace").decode()
        if col_name not in self.missing:
            self.missing[col_name] = {}
        section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)"
        if section not in self.missing[col_name]:
            self.missing[col_name][section] = {}
        for title, item_id in items:
            self.missing[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode()
        with open(self.missing_path, "w"): pass
        try:
            yaml.round_trip_dump(self.missing, open(self.missing_path, "w"))
        except yaml.scanner.ScannerError as e:
            logger.error(f"YAML Error: {util.tab_new_lines(e)}")

    def add_to_collection(self, collection, items, filters, show_filtered, rating_key_map, movie_map, show_map):
        name = collection.title if isinstance(collection, Collections) else collection
        collection_items = collection.items() if isinstance(collection, Collections) else []
        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            try:
                current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item))
                if not isinstance(current, (Movie, Show)):
                    raise NotFound
            except (BadRequest, NotFound):
                logger.error(f"Plex Error: Item {item} not found")
                continue
            match = True
            if filters:
                length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}")
                for filter_method, filter_data in filters:
                    modifier = filter_method[-4:]
                    method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method
                    method_name = filter_alias[method] if method in filter_alias else method
                    if method_name == "max_age":
                        threshold_date = datetime.now() - timedelta(days=filter_data)
                        if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date:
                            match = False
                            break
                    elif method_name == "original_language":
                        movie = None
                        for key, value in movie_map.items():
                            if current.ratingKey in value:
                                try:
                                    movie = self.TMDb.get_movie(key)
                                    break
                                except Failed:
                                    pass
                        if movie is None:
                            logger.warning(f"Filter Error: No TMDb ID found for {current.title}")
                            continue
                        if (modifier == ".not" and movie.original_language in filter_data) or (modifier != ".not" and movie.original_language not in filter_data):
                            match = False
                            break
                    elif method_name == "audio_track_title":
                        jailbreak = False
                        for media in current.media:
                            for part in media.parts:
                                for audio in part.audioStreams():
                                    for check_title in filter_data:
                                        title = audio.title if audio.title else ""
                                        if check_title.lower() in title.lower():
                                            jailbreak = True
                                            break
                                    if jailbreak: break
                                if jailbreak: break
                            if jailbreak: break
                        if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"):
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method_name == "vote_count":
                            tmdb_item = None
                            for key, value in movie_map.items():
                                if current.ratingKey in value:
                                    try:
                                        tmdb_item = self.TMDb.get_movie(key) if self.is_movie else self.TMDb.get_show(key)
                                        break
                                    except Failed:
                                        pass
                            if tmdb_item is None:
                                logger.warning(f"Filter Error: No TMDb ID found for {current.title}")
                                continue
                            attr = tmdb_item.vote_count
                        else:
                            attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name)
                        if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data):
                            match = False
                            break
                    else:
                        attrs = []
                        if method_name in ["video_resolution", "audio_language", "subtitle_language"]:
                            for media in current.media:
                                if method_name == "video_resolution":
                                    attrs.extend([media.videoResolution])
                                for part in media.parts:
                                    if method_name == "audio_language":
                                        attrs.extend([a.language for a in part.audioStreams()])
                                    if method_name == "subtitle_language":
                                        attrs.extend([s.language for s in part.subtitleStreams()])
                        elif method_name in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]:
                            attrs = [str(getattr(current, method_name))]
                        elif method_name in ["actors", "countries", "directors", "genres", "writers", "collections"]:
                            attrs = [getattr(x, "tag") for x in getattr(current, method_name)]
                        else:
                            raise Failed(f"Filter Error: filter: {method_name} not supported")

                        if (not list(set(filter_data) & set(attrs)) and modifier != ".not") or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}")
            if match:
                util.print_end(length, f"{name} Collection | {'=' if current in collection_items else '+'} | {current.title}")
                if current in collection_items:             rating_key_map[current.ratingKey] = None
                else:                                       self.add_collection(current, name)
            elif show_filtered is True:
                logger.info(f"{name} Collection | X | {current.title}")
        media_type = f"{'Movie' if self.is_movie else 'Show'}{'s' if total > 1 else ''}"
        util.print_end(length, f"{total} {media_type} Processed")
        return rating_key_map

    def search_item(self, data, year=None):
        kwargs = {}
        if year is not None:
            kwargs["year"] = year
        return util.choose_from_list(self.search(title=str(data), **kwargs), "movie" if self.is_movie else "show", str(data), exact=True)

    def edit_item(self, item, name, item_type, edits, advanced=False):
        if len(edits) > 0:
            logger.debug(f"Details Update: {edits}")
            try:
                if advanced:
                    item.editAdvanced(**edits)
                else:
                    item.edit(**edits)
                item.reload()
                if advanced and "languageOverride" in edits:
                    self.refresh_item(item.ratingKey)
                logger.info(f"{item_type}: {name}{' Advanced' if advanced else ''} Details Update Successful")
            except BadRequest:
                util.print_stacktrace()
                logger.error(f"{item_type}: {name}{' Advanced' if advanced else ''} Details Update Failed")

    def update_metadata(self, TMDb, test):
        logger.info("")
        util.separator(f"{self.name} Library Metadata")
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for mapping_name, meta in self.metadata.items():
            methods = {mm.lower(): mm for mm in meta}
            if test and ("test" not in methods or meta[methods["test"]] is not True):
                continue

            updated = False
            edits = {}
            advance_edits = {}
            def add_edit(name, current, group, alias, key=None, value=None, var_type="str"):
                if value or name in alias:
                    if value or group[alias[name]]:
                        if key is None:         key = name
                        if value is None:       value = group[alias[name]]
                        try:
                            if var_type == "date":
                                final_value = util.check_date(value, name, return_string=True, plex_date=True)
                            elif var_type == "float":
                                final_value = util.check_number(value, name, number_type="float", minimum=0, maximum=10)
                            else:
                                final_value = value
                            if str(current) != str(final_value):
                                edits[f"{key}.value"] = final_value
                                edits[f"{key}.locked"] = 1
                                logger.info(f"Detail: {name} updated to {final_value}")
                        except Failed as ee:
                            logger.error(ee)
                    else:
                        logger.error(f"Metadata Error: {name} attribute is blank")

            def add_advanced_edit(attr, obj, group, alias, show_library=False, new_agent=False):
                key, options = advance_keys[attr]
                if attr in alias:
                    if new_agent and self.agent not in new_plex_agents:
                        logger.error(f"Metadata Error: {attr} attribute only works for with the New Plex Movie Agent and New Plex TV Agent")
                    elif show_library and not self.is_show:
                        logger.error(f"Metadata Error: {attr} attribute only works for show libraries")
                    elif group[alias[attr]]:
                        method_data = str(group[alias[attr]]).lower()
                        if method_data not in options:
                            logger.error(f"Metadata Error: {group[alias[attr]]} {attr} attribute invalid")
                        elif getattr(obj, key) != options[method_data]:
                            advance_edits[key] = options[method_data]
                            logger.info(f"Detail: {attr} updated to {method_data}")
                    else:
                        logger.error(f"Metadata Error: {attr} attribute is blank")

            def edit_tags(attr, obj, group, alias, key=None, extra=None, movie_library=False):
                if key is None:
                    key = f"{attr}s"
                if attr in alias and f"{attr}.sync" in alias:
                    logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
                elif attr in alias or f"{attr}.sync" in alias:
                    attr_key = attr if attr in alias else f"{attr}.sync"
                    if movie_library and not self.is_movie:
                        logger.error(f"Metadata Error: {attr_key} attribute only works for movie libraries")
                    elif group[alias[attr_key]] or extra:
                        item_tags = [item_tag.tag for item_tag in getattr(obj, key)]
                        input_tags = []
                        if group[alias[attr_key]]:
                            input_tags.extend(util.get_list(group[alias[attr_key]]))
                        if extra:
                            input_tags.extend(extra)
                        if f"{attr}.sync" in alias:
                            remove_method = getattr(obj, f"remove{attr.capitalize()}")
                            for tag in (t for t in item_tags if t not in input_tags):
                                updated = True
                                remove_method(tag)
                                logger.info(f"Detail: {attr.capitalize()} {tag} removed")
                        add_method = getattr(obj, f"add{attr.capitalize()}")
                        for tag in (t for t in input_tags if t not in item_tags):
                            updated = True
                            add_method(tag)
                            logger.info(f"Detail: {attr.capitalize()} {tag} added")
                    else:
                        logger.error(f"Metadata Error: {attr} attribute is blank")

            def set_image(attr, obj, group, alias, is_background=False):
                if group[alias[attr]]:
                    message = f"{'background' if is_background else 'poster'} to [{'File' if attr.startswith('file') else 'URL'}] {group[alias[attr]]}"
                    if group[alias[attr]] and attr.startswith("url") and is_background:
                        obj.uploadArt(url=group[alias[attr]])
                    elif group[alias[attr]] and attr.startswith("url"):
                        obj.uploadPoster(url=group[alias[attr]])
                    elif group[alias[attr]] and attr.startswith("file") and is_background:
                        obj.uploadArt(filepath=group[alias[attr]])
                    elif group[alias[attr]] and attr.startswith("file"):
                        obj.uploadPoster(filepath=group[alias[attr]])
                    logger.info(f"Detail: {attr} updated {message}")
                else:
                    logger.error(f"Metadata Error: {attr} attribute is blank")

            def set_images(obj, group, alias):
                if "url_poster" in alias:
                    set_image("url_poster", obj, group, alias)
                elif "file_poster" in alias:
                    set_image("file_poster", obj, group, alias)
                if "url_background" in alias:
                    set_image("url_background", obj, group, alias, is_background=True)
                elif "file_background" in alias:
                    set_image("file_background", obj, group, alias, is_background=True)

            logger.info("")
            util.separator()
            logger.info("")
            year = None
            if "year" in methods:
                year = util.check_number(meta[methods["year"]], "year", minimum=1800, maximum=datetime.now().year + 1)

            title = mapping_name
            if "title" in methods:
                if meta[methods["title"]] is None:              logger.error("Metadata Error: title attribute is blank")
                else:                                           title = meta[methods["title"]]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item(f"{title} (SUB)", year=year)

            if item is None and "alt_title" in methods:
                if meta[methods["alt_title"]] is None:
                    logger.error("Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = meta["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error(f"Plex Error: Item {mapping_name} not found")
                logger.error(f"Skipping {mapping_name}")
                continue

            item_type = "Movie" if self.is_movie else "Show"
            logger.info(f"Updating {item_type}: {title}...")

            tmdb_item = None
            tmdb_is_movie = None
            if ("tmdb_show" in methods or "tmdb_id" in methods) and "tmdb_movie" in methods:
                logger.error("Metadata Error: Cannot use tmdb_movie and tmdb_show when editing the same metadata item")

            if "tmdb_show" in methods or "tmdb_id" in methods or "tmdb_movie" in methods:
                try:
                    if "tmdb_show" in methods or "tmdb_id" in methods:
                        data = meta[methods["tmdb_show" if "tmdb_show" in methods else "tmdb_id"]]
                        if data is None:
                            logger.error("Metadata Error: tmdb_show attribute is blank")
                        else:
                            tmdb_is_movie = False
                            tmdb_item = TMDb.get_show(util.regex_first_int(data, "Show"))
                    elif "tmdb_movie" in methods:
                        if meta[methods["tmdb_movie"]] is None:
                            logger.error("Metadata Error: tmdb_movie attribute is blank")
                        else:
                            tmdb_is_movie = True
                            tmdb_item = TMDb.get_movie(util.regex_first_int(meta[methods["tmdb_movie"]], "Movie"))
                except Failed as e:
                    logger.error(e)

            originally_available = None
            original_title = None
            rating = None
            studio = None
            tagline = None
            summary = None
            genres = []
            if tmdb_item:
                originally_available = tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date
                if tmdb_item and tmdb_is_movie is True and tmdb_item.original_title != tmdb_item.title:
                    original_title = tmdb_item.original_title
                elif tmdb_item and tmdb_is_movie is False and tmdb_item.original_name != tmdb_item.name:
                    original_title = tmdb_item.original_name
                rating = tmdb_item.vote_average
                if tmdb_is_movie is True and tmdb_item.production_companies:
                    studio = tmdb_item.production_companies[0].name
                elif tmdb_is_movie is False and tmdb_item.networks:
                    studio = tmdb_item.networks[0].name
                tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None
                summary = tmdb_item.overview
                genres = [genre.name for genre in tmdb_item.genres]

            edits = {}
            add_edit("title", item.title, meta, methods, value=title)
            add_edit("sort_title", item.titleSort, meta, methods, key="titleSort")
            add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date")
            add_edit("critic_rating", item.rating, meta, methods, value=rating, key="rating", var_type="float")
            add_edit("audience_rating", item.audienceRating, meta, methods, key="audienceRating", var_type="float")
            add_edit("content_rating", item.contentRating, meta, methods, key="contentRating")
            add_edit("original_title", item.originalTitle, meta, methods, key="originalTitle", value=original_title)
            add_edit("studio", item.studio, meta, methods, value=studio)
            add_edit("tagline", item.tagline, meta, methods, value=tagline)
            add_edit("summary", item.summary, meta, methods, value=summary)
            self.edit_item(item, mapping_name, item_type, edits)

            advance_edits = {}
            add_advanced_edit("episode_sorting", item, meta, methods, show_library=True)
            add_advanced_edit("keep_episodes", item, meta, methods, show_library=True)
            add_advanced_edit("delete_episodes", item, meta, methods, show_library=True)
            add_advanced_edit("season_display", item, meta, methods, show_library=True)
            add_advanced_edit("episode_ordering", item, meta, methods, show_library=True)
            add_advanced_edit("metadata_language", item, meta, methods, new_agent=True)
            add_advanced_edit("use_original_title", item, meta, methods, new_agent=True)
            self.edit_item(item, mapping_name, item_type, advance_edits, advanced=True)

            edit_tags("genre", item, meta, methods, extra=genres)
            edit_tags("label", item, meta, methods)
            edit_tags("collection", item, meta, methods)
            edit_tags("country", item, meta, methods, key="countries", movie_library=True)
            edit_tags("director", item, meta, methods, movie_library=True)
            edit_tags("producer", item, meta, methods, movie_library=True)
            edit_tags("writer", item, meta, methods, movie_library=True)

            logger.info(f"{item_type}: {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")

            set_images(item, meta, methods)

            if "seasons" in methods and self.is_show:
                if meta[methods["seasons"]]:
                    for season_id in meta[methods["seasons"]]:
                        updated = False
                        logger.info("")
                        logger.info(f"Updating season {season_id} of {mapping_name}...")
                        if isinstance(season_id, int):
                            season = None
                            for s in item.seasons():
                                if s.index == season_id:
                                    season = s
                                    break
                            if season is None:
                                logger.error(f"Metadata Error: Season: {season_id} not found")
                            else:
                                season_dict = meta[methods["seasons"]][season_id]
                                season_methods = {sm.lower(): sm for sm in season_dict}

                                if "title" in season_methods and season_dict[season_methods["title"]]:
                                    title = season_dict[season_methods["title"]]
                                else:
                                    title = season.title
                                if "sub" in season_methods:
                                    if season_dict[season_methods["sub"]] is None:
                                        logger.error("Metadata Error: sub attribute is blank")
                                    elif season_dict[season_methods["sub"]] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif season_dict[season_methods["sub"]] is False and title.endswith(" (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error("Metadata Error: sub attribute must be True or False")

                                edits = {}
                                add_edit("title", season.title, season_dict, season_methods, value=title)
                                add_edit("summary", season.summary, season_dict, season_methods)
                                self.edit_item(season, season_id, "Season", edits)
                                set_images(season, season_dict, season_methods)
                        else:
                            logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer")
                        logger.info(f"Season {season_id} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
                else:
                    logger.error("Metadata Error: seasons attribute is blank")
            elif "seasons" in methods:
                logger.error("Metadata Error: seasons attribute only works for show libraries")

            if "episodes" in methods and self.is_show:
                if meta[methods["episodes"]]:
                    for episode_str in meta[methods["episodes"]]:
                        updated = False
                        logger.info("")
                        match = re.search("[Ss]\\d+[Ee]\\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split("E" if "E" in match.group(0) else "e")
                            season_id = int(output[0])
                            episode_id = int(output[1])
                            logger.info(f"Updating episode S{season_id}E{episode_id} of {mapping_name}...")
                            try:                                episode = item.episode(season=season_id, episode=episode_id)
                            except NotFound:                    logger.error(f"Metadata Error: episode {episode_id} of season {season_id} not found")
                            else:
                                episode_dict = meta[methods["episodes"]][episode_str]
                                episode_methods = {em.lower(): em for em in episode_dict}

                                if "title" in episode_methods and episode_dict[episode_methods["title"]]:
                                    title = episode_dict[episode_methods["title"]]
                                else:
                                    title = episode.title
                                if "sub" in episode_dict:
                                    if episode_dict[episode_methods["sub"]] is None:
                                        logger.error("Metadata Error: sub attribute is blank")
                                    elif episode_dict[episode_methods["sub"]] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif episode_dict[episode_methods["sub"]] is False and title.endswith(" (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error("Metadata Error: sub attribute must be True or False")
                                edits = {}
                                add_edit("title", episode.title, episode_dict, episode_methods, value=title)
                                add_edit("sort_title", episode.titleSort, episode_dict, episode_methods, key="titleSort")
                                add_edit("rating", episode.rating, episode_dict, episode_methods)
                                add_edit("originally_available", str(episode.originallyAvailableAt)[:-9], episode_dict, episode_methods, key="originallyAvailableAt")
                                add_edit("summary", episode.summary, episode_dict, episode_methods)
                                self.edit_item(episode, f"{season_id} Episode: {episode_id}", "Season", edits)
                                edit_tags("director", episode, episode_dict, episode_methods)
                                edit_tags("writer", episode, episode_dict, episode_methods)
                                set_images(episode, episode_dict, episode_methods)
                            logger.info(f"Episode S{episode_id}E{season_id}  of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
                        else:
                            logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
            elif "episodes" in methods:
                logger.error("Metadata Error: episodes attribute only works for show libraries")
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
Пример #14
0
class PlexAPI:
    def __init__(self, params, TMDb, TVDb):
        try:
            self.PlexServer = PlexServer(params["plex"]["url"],
                                         params["plex"]["token"],
                                         timeout=params["plex"]["timeout"])
        except Unauthorized:
            raise Failed("Plex Error: Plex token is invalid")
        except ValueError as e:
            raise Failed(f"Plex Error: {e}")
        except requests.exceptions.ConnectionError:
            util.print_stacktrace()
            raise Failed("Plex Error: Plex url is invalid")
        self.is_movie = params["library_type"] == "movie"
        self.is_show = params["library_type"] == "show"
        self.Plex = next(
            (s for s in self.PlexServer.library.sections()
             if s.title == params["name"] and (
                 (self.is_movie and isinstance(s, MovieSection)) or
                 (self.is_show and isinstance(s, ShowSection)))), None)
        if not self.Plex:
            raise Failed(
                f"Plex Error: Plex Library {params['name']} not found")
        try:
            self.data, ind, bsi = yaml.util.load_yaml_guess_indent(
                open(params["metadata_path"], encoding="utf-8"))
        except yaml.scanner.ScannerError as e:
            raise Failed(f"YAML Error: {util.tab_new_lines(e)}")

        def get_dict(attribute):
            if attribute in self.data:
                if self.data[attribute]:
                    if isinstance(self.data[attribute], dict):
                        return self.data[attribute]
                    else:
                        logger.warning(
                            f"Config Warning: {attribute} must be a dictionary"
                        )
                else:
                    logger.warning(
                        f"Config Warning: {attribute} attribute is blank")
            return None

        self.metadata = get_dict("metadata")
        self.templates = get_dict("templates")
        self.collections = get_dict("collections")

        if self.metadata is None and self.collections is None:
            raise Failed(
                "YAML Error: metadata attributes or collections attribute required"
            )

        if params["asset_directory"]:
            for ad in params["asset_directory"]:
                logger.info(f"Using Asset Directory: {ad}")

        self.TMDb = TMDb
        self.TVDb = TVDb
        self.Radarr = None
        self.Sonarr = None
        self.Tautulli = None
        self.name = params["name"]
        self.missing_path = os.path.join(
            os.path.dirname(os.path.abspath(params["metadata_path"])),
            f"{os.path.splitext(os.path.basename(params['metadata_path']))[0]}_missing.yml"
        )
        self.metadata_path = params["metadata_path"]
        self.asset_directory = params["asset_directory"]
        self.sync_mode = params["sync_mode"]
        self.show_unmanaged = params["show_unmanaged"]
        self.show_filtered = params["show_filtered"]
        self.show_missing = params["show_missing"]
        self.save_missing = params["save_missing"]
        self.mass_genre_update = params["mass_genre_update"]
        self.plex = params["plex"]
        self.timeout = params["plex"]["timeout"]
        self.missing = {}
        self.run_again = []

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def search(self, title, libtype=None, year=None):
        if libtype is not None and year is not None:
            return self.Plex.search(title=title, year=year, libtype=libtype)
        elif libtype is not None:
            return self.Plex.search(title=title, libtype=libtype)
        elif year is not None:
            return self.Plex.search(title=title, year=year)
        else:
            return self.Plex.search(title=title)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def fetchItem(self, data):
        return self.PlexServer.fetchItem(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def server_search(self, data):
        return self.PlexServer.search(data)

    def get_all_collections(self):
        return self.Plex.search(libtype="collection")

    def get_collection(self, data):
        collection = util.choose_from_list(self.search(str(data),
                                                       libtype="collection"),
                                           "collection",
                                           str(data),
                                           exact=True)
        if collection: return collection
        else: raise Failed(f"Plex Error: Collection {data} not found")

    def validate_collections(self, collections):
        valid_collections = []
        for collection in collections:
            try:
                valid_collections.append(self.get_collection(collection))
            except Failed as e:
                logger.error(e)
        if len(valid_collections) == 0:
            raise Failed(
                f"Collection Error: No valid Plex Collections in {collections}"
            )
        return valid_collections

    def add_missing(self, collection, items, is_movie):
        col_name = collection.encode("ascii", "replace").decode()
        if col_name not in self.missing:
            self.missing[col_name] = {}
        section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)"
        if section not in self.missing[col_name]:
            self.missing[col_name][section] = {}
        for title, item_id in items:
            self.missing[col_name][section][int(item_id)] = str(title).encode(
                "ascii", "replace").decode()
        with open(self.missing_path, "w"):
            pass
        try:
            yaml.round_trip_dump(self.missing, open(self.missing_path, "w"))
        except yaml.scanner.ScannerError as e:
            logger.error(f"YAML Error: {util.tab_new_lines(e)}")

    def add_to_collection(self, collection, items, filters, show_filtered,
                          rating_key_map, movie_map, show_map):
        name = collection.title if isinstance(collection,
                                              Collections) else collection
        collection_items = collection.items() if isinstance(
            collection, Collections) else []
        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            try:
                current = self.fetchItem(
                    item.ratingKey if isinstance(item, (Movie,
                                                        Show)) else int(item))
            except (BadRequest, NotFound):
                logger.error(f"Plex Error: Item {item} not found")
                continue
            match = True
            if filters:
                length = util.print_return(
                    length,
                    f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}"
                )
                for filter_method, filter_data in filters:
                    modifier = filter_method[-4:]
                    method = filter_method[:-4] if modifier in [
                        ".not", ".lte", ".gte"
                    ] else filter_method
                    if method in util.method_alias:
                        method_name = util.method_alias[method]
                        logger.warning(
                            f"Collection Warning: {method} attribute will run as {method_name}"
                        )
                    else:
                        method_name = method
                    if method_name == "max_age":
                        threshold_date = datetime.now() - timedelta(
                            days=filter_data)
                        if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date:
                            match = False
                            break
                    elif method_name == "original_language":
                        movie = None
                        for key, value in movie_map.items():
                            if current.ratingKey == value:
                                try:
                                    movie = self.TMDb.get_movie(key)
                                    break
                                except Failed:
                                    pass
                        if movie is None:
                            logger.warning(
                                f"Filter Error: No TMDb ID found for {current.title}"
                            )
                            continue
                        if (modifier == ".not" and movie.original_language
                                in filter_data) or (modifier != ".not"
                                                    and movie.original_language
                                                    not in filter_data):
                            match = False
                            break
                    elif method_name == "audio_track_title":
                        jailbreak = False
                        for media in current.media:
                            for part in media.parts:
                                for audio in part.audioStreams():
                                    for check_title in filter_data:
                                        title = audio.title if audio.title else ""
                                        if check_title.lower() in title.lower(
                                        ):
                                            jailbreak = True
                                            break
                                    if jailbreak: break
                                if jailbreak: break
                            if jailbreak: break
                        if (jailbreak and modifier == ".not") or (
                                not jailbreak and modifier != ".not"):
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method_name == "vote_count":
                            tmdb_item = None
                            for key, value in movie_map.items():
                                if current.ratingKey == value:
                                    try:
                                        tmdb_item = self.TMDb.get_movie(
                                            key
                                        ) if self.is_movie else self.TMDb.get_show(
                                            key)
                                        break
                                    except Failed:
                                        pass
                            if tmdb_item is None:
                                logger.warning(
                                    f"Filter Error: No TMDb ID found for {current.title}"
                                )
                                continue
                            attr = tmdb_item.vote_count
                        else:
                            attr = getattr(
                                current, method_name
                            ) / 60000 if method_name == "duration" else getattr(
                                current, method_name)
                        if (modifier == ".lte" and attr > filter_data) or (
                                modifier == ".gte" and attr < filter_data):
                            match = False
                            break
                    else:
                        attrs = []
                        if method_name in [
                                "video_resolution", "audio_language",
                                "subtitle_language"
                        ]:
                            for media in current.media:
                                if method_name == "video_resolution":
                                    attrs.extend([media.videoResolution])
                                for part in media.parts:
                                    if method_name == "audio_language":
                                        attrs.extend([
                                            a.language
                                            for a in part.audioStreams()
                                        ])
                                    if method_name == "subtitle_language":
                                        attrs.extend([
                                            s.language
                                            for s in part.subtitleStreams()
                                        ])
                        elif method_name in [
                                "contentRating", "studio", "year", "rating",
                                "originallyAvailableAt"
                        ]:
                            attrs = [str(getattr(current, method_name))]
                        elif method_name in [
                                "actors", "countries", "directors", "genres",
                                "writers", "collections"
                        ]:
                            attrs = [
                                getattr(x, "tag")
                                for x in getattr(current, method_name)
                            ]

                        if (not list(set(filter_data) & set(attrs))
                                and modifier != ".not") or (
                                    list(set(filter_data) & set(attrs))
                                    and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(
                    length,
                    f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}"
                )
            if match:
                util.print_end(
                    length,
                    f"{name} Collection | {'=' if current in collection_items else '+'} | {current.title}"
                )
                if current in collection_items:
                    rating_key_map[current.ratingKey] = None
                else:
                    current.addCollection(name)
            elif show_filtered is True:
                logger.info(f"{name} Collection | X | {current.title}")
        media_type = f"{'Movie' if self.is_movie else 'Show'}{'s' if total > 1 else ''}"
        util.print_end(length, f"{total} {media_type} Processed")
        return rating_key_map

    def search_item(self, data, year=None):
        return util.choose_from_list(self.search(data, year=year),
                                     "movie" if self.is_movie else "show",
                                     str(data),
                                     exact=True)

    def update_metadata(self, TMDb, test):
        logger.info("")
        util.separator(f"{self.name} Library Metadata")
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for m in self.metadata:
            if test and ("test" not in self.metadata[m]
                         or self.metadata[m]["test"] is not True):
                continue
            logger.info("")
            util.separator()
            logger.info("")
            year = None
            if "year" in self.metadata[m]:
                year = util.check_number(self.metadata[m]["year"],
                                         "year",
                                         minimum=1800,
                                         maximum=datetime.now().year + 1)

            title = m
            if "title" in self.metadata[m]:
                if self.metadata[m]["title"] is None:
                    logger.error("Metadata Error: title attribute is blank")
                else:
                    title = self.metadata[m]["title"]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item(f"{title} (SUB)", year=year)

            if item is None and "alt_title" in self.metadata[m]:
                if self.metadata[m]["alt_title"] is None:
                    logger.error(
                        "Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = self.metadata[m]["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error(f"Plex Error: Item {m} not found")
                logger.error(f"Skipping {m}")
                continue

            item_type = "Movie" if self.is_movie else "Show"
            logger.info(f"Updating {item_type}: {title}...")

            tmdb_item = None
            try:
                if "tmdb_id" in self.metadata[m]:
                    if self.metadata[m]["tmdb_id"] is None:
                        logger.error(
                            "Metadata Error: tmdb_id attribute is blank")
                    elif self.is_show:
                        logger.error(
                            "Metadata Error: tmdb_id attribute only works with movie libraries"
                        )
                    else:
                        tmdb_item = TMDb.get_show(
                            util.regex_first_int(self.metadata[m]["tmdb_id"],
                                                 "Show"))
            except Failed as e:
                logger.error(e)

            originally_available = tmdb_item.first_air_date if tmdb_item else None
            rating = tmdb_item.vote_average if tmdb_item else None
            original_title = tmdb_item.original_name if tmdb_item and tmdb_item.original_name != tmdb_item.name else None
            studio = tmdb_item.networks[0].name if tmdb_item else None
            tagline = tmdb_item.tagline if tmdb_item and len(
                tmdb_item.tagline) > 0 else None
            summary = tmdb_item.overview if tmdb_item else None

            edits = {}

            def add_edit(name, current, group, key=None, value=None):
                if value or name in group:
                    if value or group[name]:
                        if key is None: key = name
                        if value is None: value = group[name]
                        if str(current) != str(value):
                            edits[f"{key}.value"] = value
                            edits[f"{key}.locked"] = 1
                            logger.info(f"Detail: {name} updated to {value}")
                    else:
                        logger.error(
                            f"Metadata Error: {name} attribute is blank")

            add_edit("title", item.title, self.metadata[m], value=title)
            add_edit("sort_title",
                     item.titleSort,
                     self.metadata[m],
                     key="titleSort")
            add_edit("originally_available",
                     str(item.originallyAvailableAt)[:-9],
                     self.metadata[m],
                     key="originallyAvailableAt",
                     value=originally_available)
            add_edit("rating", item.rating, self.metadata[m], value=rating)
            add_edit("content_rating",
                     item.contentRating,
                     self.metadata[m],
                     key="contentRating")
            add_edit("original_title",
                     item.originalTitle,
                     self.metadata[m],
                     key="originalTitle",
                     value=original_title)
            add_edit("studio", item.studio, self.metadata[m], value=studio)
            add_edit("tagline", item.tagline, self.metadata[m], value=tagline)
            add_edit("summary", item.summary, self.metadata[m], value=summary)
            if len(edits) > 0:
                logger.debug(f"Details Update: {edits}")
                try:
                    item.edit(**edits)
                    item.reload()
                    logger.info(f"{item_type}: {m} Details Update Successful")
                except BadRequest:
                    util.print_stacktrace()
                    logger.error(f"{item_type}: {m} Details Update Failed")
            else:
                logger.info(f"{item_type}: {m} Details Update Not Needed")

            genres = []

            if tmdb_item:
                genres.extend([genre.name for genre in tmdb_item.genres])

            if "genre" in self.metadata[m]:
                if self.metadata[m]["genre"]:
                    genres.extend(util.get_list(self.metadata[m]["genre"]))
                else:
                    logger.error("Metadata Error: genre attribute is blank")

            if len(genres) > 0:
                item_genres = [genre.tag for genre in item.genres]
                if "genre_sync_mode" in self.metadata[m]:
                    if self.metadata[m]["genre_sync_mode"] is None:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute is blank defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] not in [
                            "append", "sync"
                    ]:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] == "sync":
                        for genre in (g for g in item_genres
                                      if g not in genres):
                            item.removeGenre(genre)
                            logger.info(f"Detail: Genre {genre} removed")
                for genre in (g for g in genres if g not in item_genres):
                    item.addGenre(genre)
                    logger.info(f"Detail: Genre {genre} added")

            if "label" in self.metadata[m]:
                if self.metadata[m]["label"]:
                    item_labels = [label.tag for label in item.labels]
                    labels = util.get_list(self.metadata[m]["label"])
                    if "label_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["label_sync_mode"] is None:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute is blank defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] not in [
                                "append", "sync"
                        ]:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] == "sync":
                            for label in (la for la in item_labels
                                          if la not in labels):
                                item.removeLabel(label)
                                logger.info(f"Detail: Label {label} removed")
                    for label in (la for la in labels
                                  if la not in item_labels):
                        item.addLabel(label)
                        logger.info(f"Detail: Label {label} added")
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in self.metadata[m] and self.is_show:
                if self.metadata[m]["seasons"]:
                    for season_id in self.metadata[m]["seasons"]:
                        logger.info("")
                        logger.info(f"Updating season {season_id} of {m}...")
                        if isinstance(season_id, int):
                            try:
                                season = item.season(season_id)
                            except NotFound:
                                logger.error(
                                    f"Metadata Error: Season: {season_id} not found"
                                )
                            else:

                                if "title" in self.metadata[m]["seasons"][
                                        season_id] and self.metadata[m][
                                            "seasons"][season_id]["title"]:
                                    title = self.metadata[m]["seasons"][
                                        season_id]["title"]
                                else:
                                    title = season.title
                                if "sub" in self.metadata[m]["seasons"][
                                        season_id]:
                                    if self.metadata[m]["seasons"][season_id][
                                            "sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is False and title.endswith(
                                                " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )

                                edits = {}
                                add_edit(
                                    "title",
                                    season.title,
                                    self.metadata[m]["seasons"][season_id],
                                    value=title)
                                add_edit(
                                    "summary", season.summary,
                                    self.metadata[m]["seasons"][season_id])
                                if len(edits) > 0:
                                    logger.debug(
                                        f"Season: {season_id} Details Update: {edits}"
                                    )
                                    try:
                                        season.edit(**edits)
                                        season.reload()
                                        logger.info(
                                            f"Season: {season_id} Details Update Successful"
                                        )
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            f"Season: {season_id} Details Update Failed"
                                        )
                                else:
                                    logger.info(
                                        f"Season: {season_id} Details Update Not Needed"
                                    )
                        else:
                            logger.error(
                                f"Metadata Error: Season: {season_id} invalid, it must be an integer"
                            )
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in self.metadata[m] and self.is_show:
                if self.metadata[m]["episodes"]:
                    for episode_str in self.metadata[m]["episodes"]:
                        logger.info("")
                        match = re.search("[Ss]\\d+[Ee]\\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split(
                                "E" if "E" in m.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info(
                                f"Updating episode S{episode_id}E{season_id} of {m}..."
                            )
                            try:
                                episode = item.episode(season=season_id,
                                                       episode=episode_id)
                            except NotFound:
                                logger.error(
                                    f"Metadata Error: episode {episode_id} of season {season_id} not found"
                                )
                            else:
                                if "title" in self.metadata[m]["episodes"][
                                        episode_str] and self.metadata[m][
                                            "episodes"][episode_str]["title"]:
                                    title = self.metadata[m]["episodes"][
                                        episode_str]["title"]
                                else:
                                    title = episode.title
                                if "sub" in self.metadata[m]["episodes"][
                                        episode_str]:
                                    if self.metadata[m]["episodes"][
                                            episode_str]["sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["episodes"][episode_str][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif self.metadata[m]["episodes"][
                                            episode_str][
                                                "sub"] is False and title.endswith(
                                                    " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )
                                edits = {}
                                add_edit(
                                    "title",
                                    episode.title,
                                    self.metadata[m]["episodes"][episode_str],
                                    value=title)
                                add_edit(
                                    "sort_title",
                                    episode.titleSort,
                                    self.metadata[m]["episodes"][episode_str],
                                    key="titleSort")
                                add_edit(
                                    "rating", episode.rating,
                                    self.metadata[m]["episodes"][episode_str])
                                add_edit(
                                    "originally_available",
                                    str(episode.originallyAvailableAt)[:-9],
                                    self.metadata[m]["episodes"][episode_str],
                                    key="originallyAvailableAt")
                                add_edit(
                                    "summary", episode.summary,
                                    self.metadata[m]["episodes"][episode_str])
                                if len(edits) > 0:
                                    logger.debug(
                                        f"Season: {season_id} Episode: {episode_id} Details Update: {edits}"
                                    )
                                    try:
                                        episode.edit(**edits)
                                        episode.reload()
                                        logger.info(
                                            f"Season: {season_id} Episode: {episode_id} Details Update Successful"
                                        )
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            f"Season: {season_id} Episode: {episode_id} Details Update Failed"
                                        )
                                else:
                                    logger.info(
                                        f"Season: {season_id} Episode: {episode_id} Details Update Not Needed"
                                    )
                        else:
                            logger.error(
                                f"Metadata Error: episode {episode_str} invalid must have S##E## format"
                            )
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
Пример #15
0
		continue
	elif searchTerm.lower() == 'ondeck':
		onDeckItems = plex.library.onDeck()[:10]
		for onDeckItem in onDeckItems:
			if onDeckItem.TYPE=='episode':
				downloadEpisode(onDeckItem)
			elif onDeckItem.TYPE=='movie':
				downloadMovie(onDeckItem)
		raw_input('Done! Press enter to continue...')
		continue


	print 'Searching...'
	print ''

	searchList = plex.search(searchTerm)


	if len(searchList) == 0:
		print 'No items found'
		continue

	actualSearchList = []
	for item in searchList:
		if isinstance(item, plexapi.video.Show) or isinstance(item, plexapi.video.Movie) or isinstance(item, plexapi.video.Episode):
			actualSearchList.append(item)
			

	searching = True

	while searching:
Пример #16
0
class PlexAPI:
    def __init__(self, params, TMDb, TVDb):
        try:
            self.PlexServer = PlexServer(params["plex"]["url"],
                                         params["plex"]["token"],
                                         timeout=params["plex"]["timeout"])
        except Unauthorized:
            raise Failed("Plex Error: Plex token is invalid")
        except ValueError as e:
            raise Failed("Plex Error: {}".format(e))
        except requests.exceptions.ConnectionError as e:
            util.print_stacktrace()
            raise Failed("Plex Error: Plex url is invalid")
        self.is_movie = params["library_type"] == "movie"
        self.is_show = params["library_type"] == "show"
        self.Plex = next(
            (s for s in self.PlexServer.library.sections()
             if s.title == params["name"] and (
                 (self.is_movie and isinstance(s, MovieSection)) or
                 (self.is_show and isinstance(s, ShowSection)))), None)
        if not self.Plex:
            raise Failed("Plex Error: Plex Library {} not found".format(
                params["name"]))
        try:
            self.data, ind, bsi = yaml.util.load_yaml_guess_indent(
                open(params["metadata_path"], encoding="utf-8"))
        except yaml.scanner.ScannerError as e:
            raise Failed("YAML Error: {}".format(
                str(e).replace("\n", "\n|\t      ")))

        def get_dict(attribute):
            if attribute in self.data:
                if self.data[attribute]:
                    if isinstance(self.data[attribute], dict):
                        return self.data[attribute]
                    else:
                        logger.waring(
                            "Config Warning: {} must be a dictionary".format(
                                attribute))
                else:
                    logger.warning(
                        "Config Warning: {} attribute is blank".format(
                            attribute))
            return None

        self.metadata = get_dict("metadata")
        self.templates = get_dict("templates")
        self.collections = get_dict("collections")

        if self.metadata is None and self.collections is None:
            raise Failed(
                "YAML Error: metadata attributes or collections attribute required"
            )

        if params["asset_directory"]:
            for ad in params["asset_directory"]:
                logger.info("Using Asset Directory: {}".format(ad))

        self.TMDb = TMDb
        self.TVDb = TVDb
        self.Radarr = None
        self.Sonarr = None
        self.Tautulli = None
        self.name = params["name"]
        self.missing_path = os.path.join(
            os.path.dirname(os.path.abspath(params["metadata_path"])),
            "{}_missing.yml".format(
                os.path.splitext(os.path.basename(
                    params["metadata_path"]))[0]))
        self.metadata_path = params["metadata_path"]
        self.asset_directory = params["asset_directory"]
        self.sync_mode = params["sync_mode"]
        self.show_unmanaged = params["show_unmanaged"]
        self.show_filtered = params["show_filtered"]
        self.show_missing = params["show_missing"]
        self.save_missing = params["save_missing"]
        self.plex = params["plex"]
        self.timeout = params["plex"]["timeout"]
        self.missing = {}

    def add_Radarr(self, Radarr):
        self.Radarr = Radarr

    def add_Sonarr(self, Sonarr):
        self.Sonarr = Sonarr

    def add_Tautulli(self, Tautulli):
        self.Tautulli = Tautulli

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def search(self, title, libtype=None, year=None):
        if libtype is not None and year is not None:
            return self.Plex.search(title=title, year=year, libtype=libtype)
        elif libtype is not None:
            return self.Plex.search(title=title, libtype=libtype)
        elif year is not None:
            return self.Plex.search(title=title, year=year)
        else:
            return self.Plex.search(title=title)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def fetchItem(self, data):
        return self.PlexServer.fetchItem(data)

    @retry(stop_max_attempt_number=6, wait_fixed=10000)
    def server_search(self, data):
        return self.PlexServer.search(data)

    def get_all_collections(self):
        return self.Plex.search(libtype="collection")

    def get_collection(self, data):
        collection = util.choose_from_list(self.search(str(data),
                                                       libtype="collection"),
                                           "collection",
                                           str(data),
                                           exact=True)
        if collection: return collection
        else: raise Failed("Plex Error: Collection {} not found".format(data))

    def validate_collections(self, collections):
        valid_collections = []
        for collection in collections:
            try:
                valid_collections.append(self.get_collection(collection))
            except Failed as e:
                logger.error(e)
        if len(valid_collections) == 0:
            raise Failed(
                "Collection Error: No valid Plex Collections in {}".format(
                    collections[c][m]))
        return valid_collections

    def add_missing(self, collection, items, is_movie):
        col_name = collection.encode("ascii", "replace").decode()
        if col_name not in self.missing:
            self.missing[col_name] = {}
        section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)"
        if section not in self.missing[col_name]:
            self.missing[col_name][section] = {}
        for title, item_id in items:
            self.missing[col_name][section][int(item_id)] = str(title).encode(
                "ascii", "replace").decode()
        with open(self.missing_path, "w"):
            pass
        try:
            yaml.round_trip_dump(self.missing, open(self.missing_path, "w"))
        except yaml.scanner.ScannerError as e:
            logger.error("YAML Error: {}".format(
                str(e).replace("\n", "\n|\t      ")))

    def add_to_collection(self, collection, items, filters, show_filtered, map,
                          movie_map, show_map):
        name = collection.title if isinstance(collection,
                                              Collections) else collection
        collection_items = collection.items() if isinstance(
            collection, Collections) else []
        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            try:
                current = self.fetchItem(
                    item.ratingKey if isinstance(item, (Movie,
                                                        Show)) else int(item))
            except (BadRequest, NotFound):
                logger.error("Plex Error: Item {} not found".format(item))
                continue
            match = True
            if filters:
                length = util.print_return(
                    length, "Filtering {}/{} {}".format(
                        (" " * (max_length - len(str(i)))) + str(i), total,
                        current.title))
                for f in filters:
                    modifier = f[0][-4:]
                    method = util.filter_alias[f[0][:-4]] if modifier in [
                        ".not", ".lte", ".gte"
                    ] else util.filter_alias[f[0]]
                    if method == "max_age":
                        threshold_date = datetime.now() - timedelta(days=f[1])
                        attr = getattr(current, "originallyAvailableAt")
                        if attr is None or attr < threshold_date:
                            match = False
                            break
                    elif method == "original_language":
                        terms = util.get_list(f[1], lower=True)
                        tmdb_id = None
                        movie = None
                        for key, value in movie_map.items():
                            if current.ratingKey == value:
                                try:
                                    movie = self.TMDb.get_movie(key)
                                    break
                                except Failed:
                                    pass
                        if movie is None:
                            logger.warning(
                                "Filter Error: No TMDb ID found for {}".format(
                                    current.title))
                            continue
                        if (modifier == ".not"
                                and movie.original_language in terms) or (
                                    modifier != ".not"
                                    and movie.original_language not in terms):
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method == "originallyAvailableAt":
                            threshold_date = datetime.strptime(
                                f[1], "%m/%d/%y")
                            attr = getattr(current, "originallyAvailableAt")
                            if (modifier == ".lte" and attr > threshold_date
                                ) or (modifier == ".gte"
                                      and attr < threshold_date):
                                match = False
                                break
                        elif method in ["year", "rating"]:
                            attr = getattr(current, method)
                            if (modifier == ".lte"
                                    and attr > f[1]) or (modifier == ".gte"
                                                         and attr < f[1]):
                                match = False
                                break
                    else:
                        terms = util.get_list(f[1])
                        if method in [
                                "video_resolution", "audio_language",
                                "subtitle_language"
                        ]:
                            for media in current.media:
                                if method == "video_resolution":
                                    attrs = [media.videoResolution]
                                for part in media.parts:
                                    if method == "audio_language":
                                        attrs = ([
                                            a.language
                                            for a in part.audioStreams()
                                        ])
                                    if method == "subtitle_language":
                                        attrs = ([
                                            s.language
                                            for s in part.subtitleStreams()
                                        ])
                        elif method in [
                                "contentRating", "studio", "year", "rating",
                                "originallyAvailableAt"
                        ]:
                            attrs = [str(getattr(current, method))]
                        elif method in [
                                "actors", "countries", "directors", "genres",
                                "writers", "collections"
                        ]:
                            attrs = [
                                getattr(x, "tag")
                                for x in getattr(current, method)
                            ]

                        if (not list(set(terms) & set(attrs)) and modifier !=
                                ".not") or (list(set(terms) & set(attrs))
                                            and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(
                    length, "Filtering {}/{} {}".format(
                        (" " * (max_length - len(str(i)))) + str(i), total,
                        current.title))
            if match:
                util.print_end(
                    length, "{} Collection | {} | {}".format(
                        name, "=" if current in collection_items else "+",
                        current.title))
                if current in collection_items: map[current.ratingKey] = None
                else: current.addCollection(name)
            elif show_filtered is True:
                logger.info("{} Collection | X | {}".format(
                    name, current.title))
        media_type = "{}{}".format("Movie" if self.is_movie else "Show",
                                   "s" if total > 1 else "")
        util.print_end(length, "{} {} Processed".format(total, media_type))
        return map

    def search_item(self, data, year=None):
        return util.choose_from_list(self.search(data, year=year),
                                     "movie" if self.is_movie else "show",
                                     str(data),
                                     exact=True)

    def update_metadata(self, TMDb, test):
        logger.info("")
        util.seperator("{} Library Metadata".format(self.name))
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for m in self.metadata:
            if test and ("test" not in self.metadata[m]
                         or self.metadata[m]["test"] is not True):
                continue
            logger.info("")
            util.seperator()
            logger.info("")
            year = None
            if "year" in self.metadata[m]:
                now = datetime.datetime.now()
                if self.metadata[m]["year"] is None:
                    logger.error("Metadata Error: year attribute is blank")
                elif not isinstance(self.metadata[m]["year"], int):
                    logger.error(
                        "Metadata Error: year attribute must be an integer")
                elif self.metadata[m]["year"] not in range(1800, now.year + 2):
                    logger.error(
                        "Metadata Error: year attribute must be between 1800-{}"
                        .format(now.year + 1))
                else:
                    year = self.metadata[m]["year"]

            title = m
            if "title" in self.metadata[m]:
                if self.metadata[m]["title"] is None:
                    logger.error("Metadata Error: title attribute is blank")
                else:
                    title = self.metadata[m]["title"]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item("{} (SUB)".format(title), year=year)

            if item is None and "alt_title" in self.metadata[m]:
                if self.metadata[m]["alt_title"] is None:
                    logger.error(
                        "Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = self.metadata[m]["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error("Plex Error: Item {} not found".format(m))
                logger.error("Skipping {}".format(m))
                continue

            logger.info("Updating {}: {}...".format(
                "Movie" if self.is_movie else "Show", title))

            tmdb_item = None
            try:
                if "tmdb_id" in self.metadata[m]:
                    if self.metadata[m]["tmdb_id"] is None:
                        logger.error(
                            "Metadata Error: tmdb_id attribute is blank")
                    elif self.is_show:
                        logger.error(
                            "Metadata Error: tmdb_id attribute only works with movie libraries"
                        )
                    else:
                        tmdb_item = TMDb.get_show(
                            util.regex_first_int(self.metadata[m]["tmdb_id"],
                                                 "Show"))
            except Failed as e:
                logger.error(e)

            originally_available = tmdb_item.first_air_date if tmdb_item else None
            rating = tmdb_item.vote_average if tmdb_item else None
            original_title = tmdb_item.original_name if tmdb_item and tmdb_item.original_name != tmdb_item.name else None
            studio = tmdb_item.networks[0].name if tmdb_item else None
            tagline = tmdb_item.tagline if tmdb_item and len(
                tmdb_item.tagline) > 0 else None
            summary = tmdb_item.overview if tmdb_item else None

            edits = {}

            def add_edit(name, current, group, key=None, value=None):
                if value or name in group:
                    if value or group[name]:
                        if key is None: key = name
                        if value is None: value = group[name]
                        if str(current) != str(value):
                            edits["{}.value".format(key)] = value
                            edits["{}.locked".format(key)] = 1
                    else:
                        logger.error(
                            "Metadata Error: {} attribute is blank".format(
                                name))

            add_edit("title", item.title, self.metadata[m], value=title)
            add_edit("sort_title",
                     item.titleSort,
                     self.metadata[m],
                     key="titleSort")
            add_edit("originally_available",
                     str(item.originallyAvailableAt)[:-9],
                     self.metadata[m],
                     key="originallyAvailableAt",
                     value=originally_available)
            add_edit("rating", item.rating, self.metadata[m], value=rating)
            add_edit("content_rating",
                     item.contentRating,
                     self.metadata[m],
                     key="contentRating")
            item_original_title = item.originalTitle if self.is_movie else item._data.attrib.get(
                "originalTitle")
            add_edit("original_title",
                     item_original_title,
                     self.metadata[m],
                     key="originalTitle",
                     value=original_title)
            add_edit("studio", item.studio, self.metadata[m], value=studio)
            item_tagline = item.tagline if self.is_movie else item._data.attrib.get(
                "tagline")
            add_edit("tagline", item_tagline, self.metadata[m], value=tagline)
            add_edit("summary", item.summary, self.metadata[m], value=summary)
            if len(edits) > 0:
                logger.debug("Details Update: {}".format(edits))
                try:
                    item.edit(**edits)
                    item.reload()
                    logger.info("{}: {} Details Update Successful".format(
                        "Movie" if self.is_movie else "Show", m))
                except BadRequest:
                    util.print_stacktrace()
                    logger.error("{}: {} Details Update Failed".format(
                        "Movie" if self.is_movie else "Show", m))
            else:
                logger.info("{}: {} Details Update Not Needed".format(
                    "Movie" if self.is_movie else "Show", m))

            genres = []

            if tmdb_item:
                genres.extend([genre.name for genre in tmdb_item.genres])

            if "genre" in self.metadata[m]:
                if self.metadata[m]["genre"]:
                    genres.extend(util.get_list(self.metadata[m]["genre"]))
                else:
                    logger.error("Metadata Error: genre attribute is blank")

            if len(genres) > 0:
                item_genres = [genre.tag for genre in item.genres]
                if "genre_sync_mode" in self.metadata[m]:
                    if self.metadata[m]["genre_sync_mode"] is None:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute is blank defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] not in [
                            "append", "sync"
                    ]:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] == "sync":
                        for genre in (g for g in item_genres
                                      if g not in genres):
                            item.removeGenre(genre)
                            logger.info(
                                "Detail: Genre {} removed".format(genre))
                for genre in (g for g in genres if g not in item_genres):
                    item.addGenre(genre)
                    logger.info("Detail: Genre {} added".format(genre))

            if "label" in self.metadata[m]:
                if self.metadata[m]["label"]:
                    item_labels = [label.tag for label in item.labels]
                    labels = util.get_list(self.metadata[m]["label"])
                    if "label_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["label_sync_mode"] is None:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute is blank defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] not in [
                                "append", "sync"
                        ]:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] == "sync":
                            for label in (l for l in item_labels
                                          if l not in labels):
                                item.removeLabel(label)
                                logger.info(
                                    "Detail: Label {} removed".format(label))
                    for label in (l for l in labels if l not in item_labels):
                        item.addLabel(label)
                        logger.info("Detail: Label {} added".format(label))
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in self.metadata[m] and self.is_show:
                if self.metadata[m]["seasons"]:
                    for season_id in self.metadata[m]["seasons"]:
                        logger.info("")
                        logger.info("Updating season {} of {}...".format(
                            season_id, m))
                        if isinstance(season_id, int):
                            try:
                                season = item.season(season_id)
                            except NotFound:
                                logger.error(
                                    "Metadata Error: Season: {} not found".
                                    format(season_id))
                            else:

                                if "title" in self.metadata[m]["seasons"][
                                        season_id] and self.metadata[m][
                                            "seasons"][season_id]["title"]:
                                    title = self.metadata[m]["seasons"][
                                        season_id]["title"]
                                else:
                                    title = season.title
                                if "sub" in self.metadata[m]["seasons"][
                                        season_id]:
                                    if self.metadata[m]["seasons"][season_id][
                                            "sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = "{} (SUB)".format(title)
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is False and title.endswith(
                                                " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )

                                edits = {}
                                add_edit(
                                    "title",
                                    season.title,
                                    self.metadata[m]["seasons"][season_id],
                                    value=title)
                                add_edit(
                                    "summary", season.summary,
                                    self.metadata[m]["seasons"][season_id])
                                if len(edits) > 0:
                                    logger.debug(
                                        "Season: {} Details Update: {}".format(
                                            season_id, edits))
                                    try:
                                        season.edit(**edits)
                                        season.reload()
                                        logger.info(
                                            "Season: {} Details Update Successful"
                                            .format(season_id))
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            "Season: {} Details Update Failed".
                                            format(season_id))
                                else:
                                    logger.info(
                                        "Season: {} Details Update Not Needed".
                                        format(season_id))
                        else:
                            logger.error(
                                "Metadata Error: Season: {} invalid, it must be an integer"
                                .format(season_id))
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in self.metadata[m] and self.is_show:
                if self.metadata[m]["episodes"]:
                    for episode_str in self.metadata[m]["episodes"]:
                        logger.info("")
                        match = re.search("[Ss]{1}\d+[Ee]{1}\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split(
                                "E" if "E" in m.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info(
                                "Updating episode S{}E{} of {}...".format(
                                    episode_id, season_id, m))
                            try:
                                episode = item.episode(season=season_id,
                                                       episode=episode_id)
                            except NotFound:
                                logger.error(
                                    "Metadata Error: episode {} of season {} not found"
                                    .format(episode_id, season_id))
                            else:
                                if "title" in self.metadata[m]["episodes"][
                                        episode_str] and self.metadata[m][
                                            "episodes"][episode_str]["title"]:
                                    title = self.metadata[m]["episodes"][
                                        episode_str]["title"]
                                else:
                                    title = episode.title
                                if "sub" in self.metadata[m]["episodes"][
                                        episode_str]:
                                    if self.metadata[m]["episodes"][
                                            episode_str]["sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["episodes"][episode_str][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = "{} (SUB)".format(title)
                                    elif self.metadata[m]["episodes"][
                                            episode_str][
                                                "sub"] is False and title.endswith(
                                                    " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )
                                edits = {}
                                add_edit(
                                    "title",
                                    episode.title,
                                    self.metadata[m]["episodes"][episode_str],
                                    value=title)
                                add_edit(
                                    "sort_title",
                                    episode.titleSort,
                                    self.metadata[m]["episodes"][episode_str],
                                    key="titleSort")
                                add_edit(
                                    "rating", episode.rating,
                                    self.metadata[m]["episodes"][episode_str])
                                add_edit(
                                    "originally_available",
                                    str(episode.originallyAvailableAt)[:-9],
                                    self.metadata[m]["episodes"][episode_str],
                                    key="originallyAvailableAt")
                                add_edit(
                                    "summary", episode.summary,
                                    self.metadata[m]["episodes"][episode_str])
                                if len(edits) > 0:
                                    logger.debug(
                                        "Season: {} Episode: {} Details Update: {}"
                                        .format(season_id, episode_id, edits))
                                    try:
                                        episode.edit(**edits)
                                        episode.reload()
                                        logger.info(
                                            "Season: {} Episode: {} Details Update Successful"
                                            .format(season_id, episode_id))
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            "Season: {} Episode: {} Details Update Failed"
                                            .format(season_id, episode_id))
                                else:
                                    logger.info(
                                        "Season: {} Episode: {} Details Update Not Needed"
                                        .format(season_id, episode_id))
                        else:
                            logger.error(
                                "Metadata Error: episode {} invlaid must have S##E## format"
                                .format(episode_str))
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
Пример #17
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.signin()  # MyPlexAccount instance
        self.plex = PlexServer()  # PlexServer instance
        self.attrs = defaultdict(dict)  # Attrs result set

    def run(self):
        # MyPlex
        self._load_attrs(self.account)
        self._load_attrs(self.account.devices())
        for resource in self.account.resources():
            self._load_attrs(resource)
            self._load_attrs(resource.connections)
        self._load_attrs(self.account.users())
        # Server
        self._load_attrs(self.plex)
        self._load_attrs(self.plex.account())
        self._load_attrs(self.plex.history()[:20])
        self._load_attrs(self.plex.playlists())
        for search in ('cre', 'ani', 'mik', 'she'):
            self._load_attrs(self.plex.search('cre'))
        self._load_attrs(self.plex.sessions())
        # Library
        self._load_attrs(self.plex.library)
        self._load_attrs(self.plex.library.sections())
        self._load_attrs(self.plex.library.all()[:20])
        self._load_attrs(self.plex.library.onDeck()[:20])
        self._load_attrs(self.plex.library.recentlyAdded()[:20])
        for search in ('cat', 'dog', 'rat'):
            self._load_attrs(self.plex.library.search(search)[:20])
        # Client
        self._load_attrs(self.plex.clients())
        return self

    def _load_attrs(self, obj):
        if isinstance(obj, (list, tuple)):
            return [self._parse_objects(x) for x in obj]
        return self._parse_objects(obj)

    def _parse_objects(self, obj):
        clsname = '%s.%s' % (obj.__module__, obj.__class__.__name__)
        clsname = clsname.replace('plexapi.', '')
        if self.clsnames and clsname not in self.clsnames:
            return None
        sys.stdout.write('.')
        sys.stdout.flush()
        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._load_obj_attrs(clsname, obj, self.attrs[clsname]['obj'])

    def _load_xml_attrs(self, clsname, elem, attrs, examples):
        if elem in (None, NA): return None
        for attr in sorted(elem.attrib.keys()):
            attrs[attr] += 1
            if elem.attrib[attr] and len(examples[attr]) <= self.opts.examples:
                examples[attr].add(elem.attrib[attr])

    def _load_obj_attrs(self, clsname, obj, attrs):
        for attr, value in obj.__dict__.items():
            if value in (None, NA) or isinstance(
                    value, (str, bool, float, int, datetime)):
                if not attr.startswith('_') and attr not in IGNORES.get(
                        clsname, []):
                    attrs[attr] += 1

    def print_report(self):
        total_attrs = 0
        for clsname in sorted(self.attrs.keys()):
            meta = self.attrs[clsname]
            count = meta['total']
            print(
                _('\n%s (%s)\n%s' % (clsname, count, '-' * (len(clsname) + 8)),
                  'yellow'))
            attrs = sorted(
                set(list(meta['xml'].keys()) + list(meta['obj'].keys())))
            for attr in attrs:
                state = self._attr_state(attr, meta)
                count = meta['xml'].get(attr, 0)
                example = list(meta['examples'].get(attr, ['--']))[0][:80]
                print('%-4s %4s  %-30s  %s' % (state, count, attr, example))
                total_attrs += count
        print(_('\nSUMMARY\n------------', 'yellow'))
        print('Plex Version     %s' % self.plex.version)
        print('PlexAPI Version  %s' % plexapi.VERSION)
        print('Total Objects    %s\n' %
              sum([x['total'] for x in self.attrs.values()]))
        for clsname in sorted(self.attrs.keys()):
            print('%-34s %s' % (clsname, self.attrs[clsname]['total']))
        print()

    def _attr_state(self, attr, meta):
        if attr in meta['xml'].keys() and attr not in meta['obj'].keys():
            return _('new', 'blue')
        if attr not in meta['xml'].keys() and attr in meta['obj'].keys():
            return _('old', 'red')
        return _('   ', 'green')
Пример #18
0
if __name__ == '__main__':
    args = parse_args()
    if args.verbose:
        logger.setLevel(logging.DEBUG)

    existing_browsers = set()

    plex = PlexServer(baseurl=args.baseurl,
                      token=args.token)  # Defaults to localhost:32400

    run_event = threading.Event()
    run_event.set()
    threads = set()

    for (i, video) in enumerate(plex.search('the')):
        if i == args.concurrency:
            break
        url = video.getStreamURL(videoResolution='800x600')
        t = threading.Thread(target=play_movie, args=(url, i, run_event))
        threads.add(t)
        t.start()

    try:
        while 1:
            time.sleep(.1)
    except KeyboardInterrupt:
        logger.info("exit")
        run_event.clear()
        for t in threads:
            t.join()
Пример #19
0
class SpotiPlex:
    def __init__(self, spotifyInfo, plexInfo):
        self.credManager = SpotifyClientCredentials(
            client_id=spotifyInfo['clientId'],
            client_secret=spotifyInfo['clientSecret'])
        self.user = spotifyInfo['user']
        self.sp = spotipy.Spotify(client_credentials_manager=self.credManager)
        self.plex = PlexServer(plexInfo['url'], plexInfo['token'])

    def getSpotifyPlaylist(self, plId):
        'Generate and return a list of tracks of the Spotify playlist'

        #playlist = self.sp.user_playlist(self.user, plId)
        playlist = self.sp.user_playlist_tracks(self.user, playlist_id=plId)

        tracks = playlist['items']
        while playlist['next']:
            playlist = self.sp.next(playlist)
            tracks.extend(playlist['items'])

        items = []
        for item in tracks:
            items.append({
                'title': item['track']['name'],
                'album': item['track']['album']['name'],
                'artist': item['track']['artists'][0]['name'],
                'isrc': item['track']['external_ids']['isrc']
                #'number': item['track']['track_number'],
                #'img': item['track']['album']['images'][0]['url']
            })

        return items

    def checkPlexFiles(self, playlist):
        'Check if the songs in the playlist are present on the Plex server. Returns list of found and missing items'

        tracks = []
        missing = []

        for item in playlist:
            results = self.plex.search(item['title'], mediatype='track')
            if not results:
                missing.append(item)
                continue

            for result in results:
                if type(result) != plexapi.audio.Track:
                    continue
                else:
                    if result.grandparentTitle.lower() == item['artist'].lower(
                    ):  # and result.parentTitle == item['album']:
                        tracks.append(result)
                        break
                    else:
                        if result == results[-1]:
                            missing.append(item)
                            break

        return tracks, missing

    def checkForPlexPlaylist(self, name):
        'Check if a playlist with this name exists in Plex. Returns the playlist if valid, else None'

        try:
            return self.plex.playlist(name)
        except plexapi.exceptions.NotFound:
            return None

    def comparePlaylists(self, sPlaylist, pPlaylist):
        'Compares the extracted Spotify playlist with the existing Plex playlist. Returns list of tracks to create the new playlist version from and missing songs in Plex'

        tracksToAdd = sPlaylist
        plexTracks = pPlaylist.items()
        plexOnlyItems = []
        temp = []
        for track in plexTracks:
            # remove any tracks from Spotify list that are already in Plex
            lastLen = len(temp)
            temp = list(
                filter(lambda item: not item['title'] == track.title,
                       tracksToAdd))
            if not len(temp) == lastLen:
                tracksToAdd = temp
            else:
                plexOnlyItems.append(track)

        return tracksToAdd, plexOnlyItems

    def createPlexPlaylist(self, name, playlist=None):
        'Create the playlist on the Plex server from given name and a item list'

        newPlaylist = self.plex.createPlaylist(name, items=playlist)
        return

    def addToPlexPlaylist(self, plexPlaylist, newItems):
        'Add more items to a Plex playlist'

        return plexPlaylist.addItems(newItems)

    def removeFromPlexPlaylist(self, plexPlaylist, itemsToRemove):
        'Remove given items from a Plex playlist'

        ## Seems not to work properly yet

        for item in itemsToRemove:
            plexPlaylist.removeItem(item)
Пример #20
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