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
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)
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}
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()
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
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'))
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
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")
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
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:")
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
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")
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:
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")
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')
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()
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)
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