def findplayingmedia(): # Connect Remotely but slower #account = MyPlexAccount(credentials.login['username'], credentials.login['password']) #plex = account.resource(credentials.login['plexhost']).connect() # returns a PlexServer instance # Connect faster locally with token credential plex = PlexServer(credentials.login['baseurl'], credentials.login['token']) mediainfos = [] for session in plex.sessions(): mediadict = {} playheadseconds = session.viewOffset // 1000 #Display all instances of the players. #for player in session.players: # print("Player: " + player.device) # this checks if the media being returned is a movie or TV show. if (session.type == "episode"): mediadict["title"] = session.grandparentTitle + " - " + session.title mediadict["type"] = session.type elif (session.type == "movie"): mediadict["title"] = session.title mediadict["type"] = session.type else: print ("not the correct media type playing: " + session.type) mediadict["title"] = "Music" mediadict["type"] = session.type mediadict["convert"] = False if (session.type == "episode") or (session.type == "movie"): # Only for printing out the playehead, checking for accuracy, not otherwise necessary. #minutes, seconds = divmod(int(playheadseconds), 60) #print("Current Playhead Location: " + str(minutes) + ":" + str(seconds)) #searches plex's library for media that matches the key of the session. #this is because I couldn't find a way to consistently get the file path. Sometimes #it would work, sometimes it wouldn't. This made it always work. playingmedia = plex.fetchItem(session.key) playingmediafilepath = playingmedia.media[0].parts[0].file mediadict["playhead"] = playheadseconds mediadict["path"] = playingmediafilepath mediadict["convert"] = True else: mediadict["playhead"] = 0 mediadict["path"] = "No Path" mediadict["id"] = id_generator(8) mediainfos.append(mediadict) return (mediainfos)
class PlexWrapper(object): def __init__(self): baseurl = os.environ.get("PLEX_BASE_URL") token = os.environ.get("PLEX_TOKEN") verify_ssl = os.environ.get("BYPASS_SSL_VERIFY", "0") != "1" session = requests.Session() session.verify = verify_ssl self.plex = PlexServer(baseurl, token, session=session, timeout=(60 * 60)) def get_dupe_movies(self): dupes = [] section = self.plex.library.section(title="4K Movies") for movie in section.search(duplicate=True): if len(movie.media) > 1: dupes.append(self.movie_to_dict(movie)) return dupes def get_movie_sample_files(self): movies = [] section = self.plex.library.section(title="4K Movies") for movie in section.all(): samples = [] for media in movie.media: if media.duration is None or media.duration < (5 * 60 * 1000): samples.append(self.media_to_dict(media)) if len(samples) > 0: _movie = self.movie_to_dict(movie) _movie['media'] = samples movies.append(_movie) return movies def get_movie(self, media_id): return self.plex.fetchItem(media_id) @classmethod def video_to_dict(cls, video: Video) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Video return { 'addedAt': str(video.addedAt), 'key': video.key, 'lastViewedAt': str(video.lastViewedAt), 'librarySectionID': video.librarySectionID, 'summary': video.summary, 'thumb': video.thumb, 'title': video.title, 'titleSort': video.titleSort, 'type': video.type, 'updatedAt': str(video.updatedAt), 'viewCount': str(video.viewCount), } @classmethod def movie_to_dict(cls, movie: Movie) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Movie return { **cls.video_to_dict(movie), 'duration': movie.duration, 'guid': movie.guid, 'originalTitle': movie.originalTitle, 'originallyAvailableAt': str(movie.originallyAvailableAt), 'rating': movie.rating, 'ratingImage': movie.ratingImage, 'studio': movie.studio, 'tagline': movie.tagline, 'userRating': movie.userRating, 'year': movie.year, 'media': [cls.media_to_dict(media) for media in movie.media] } @classmethod def media_to_dict(cls, media: Media) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.Media return { 'id': media.id, # 'initpath': media.initpath, # 'video': media.video, 'aspectRatio': media.aspectRatio, 'audioChannels': media.audioChannels, 'audioCodec': media.audioCodec, 'bitrate': media.bitrate, 'container': media.container, 'duration': media.duration, 'width': media.width, 'height': media.height, 'has64bitOffsets': media.has64bitOffsets, 'optimizedForStreaming': media.optimizedForStreaming, 'target': media.target, 'title': media.title, 'videoCodec': media.videoCodec, 'videoFrameRate': media.videoFrameRate, 'videoResolution': media.videoResolution, 'videoProfile': media.videoProfile, 'parts': [cls.media_part_to_dict(media_part) for media_part in media.parts] } @classmethod def media_part_to_dict(cls, media_part: MediaPart) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPart return { 'id': media_part.id, # 'media_id': media_part.media.id, # 'initpath': media_part.initpath, 'container': media_part.container, 'duration': media_part.duration, 'file': media_part.file, 'indexes': media_part.indexes, 'key': media_part.key, 'size': media_part.size, 'exists': media_part.exists, 'accessible': media_part.accessible, 'streams': [ cls.media_part_stream_to_dict(media_part_stream) for media_part_stream in media_part.videoStreams() ] } @classmethod def media_part_stream_to_dict(cls, media_part_stream: MediaPartStream) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPartStream return { 'id': media_part_stream.id, # 'media_id': media_part.media.id, # 'initpath': media_part.initpath, 'codec': media_part_stream.codec, 'codecID': media_part_stream.codecID, 'language': media_part_stream.language, 'languageCode': media_part_stream.languageCode, 'selected': media_part_stream.selected, 'type': media_part_stream.type }
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")
PLEX_URL = '' PLEX_TOKEN = '' # Environmental Variables PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) PLEX_TOKEN = os.getenv('PLEX_TOKEN', PLEX_TOKEN) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--rating_key', required=True, type=int) parser.add_argument('--collection', required=True) parser.add_argument('--days', required=True, type=int) opts = parser.parse_args() threshold_date = datetime.now() - timedelta(days=opts.days) plex = PlexServer(PLEX_URL, PLEX_TOKEN) movie = plex.fetchItem(opts.rating_key) if movie.originallyAvailableAt >= threshold_date: movie.addCollection(opts.collection) print("Added collection '{}' to '{}'.".format( opts.collection, movie.title.encode('UTF-8'))) for m in movie.section().search(collection=opts.collection): if m.originallyAvailableAt < threshold_date: m.removeCollection(opts.collection) print("Removed collection '{}' from '{}'.".format( opts.collection, m.title.encode('UTF-8')))
import os from plexapi.server import PlexServer PLEX_URL = '' PLEX_TOKEN = '' # Environmental Variables PLEX_URL = os.getenv('PLEX_URL', PLEX_URL) PLEX_USER_TOKEN = os.getenv('PLEX_USER_TOKEN', PLEX_TOKEN) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('--rating_key', required=True, type=int) parser.add_argument('--filename', required=True) opts = parser.parse_args() plex = PlexServer(PLEX_URL, PLEX_USER_TOKEN) for episode in plex.fetchItem(opts.rating_key).season().episodes(): if episode.ratingKey == opts.rating_key: continue if any(opts.filename in part.file for media in episode.media for part in media.parts): print( "Marking multi-episode file '{grandparentTitle} - S{parentIndex}E{index}' as watched." .format( grandparentTitle=episode.grandparentTitle.encode('UTF-8'), parentIndex=str(episode.parentIndex).zfill(2), index=str(episode.index).zfill(2))) episode.markWatched()
from plexapi.server import PlexServer PLEX_URL = '' PLEX_TOKEN = '' plex = PlexServer(PLEX_URL, PLEX_TOKEN) COLLECTIONAME = 'My Fav Series' TOPLEVELFOLDERNAME = 'Series Name' LIBRARYNAME = 'Audio Books' abLibrary = plex.library.section(LIBRARYNAME) albums = [] for folder in abLibrary.folders(): if folder.title == TOPLEVELFOLDERNAME: for series in folder.allSubfolders(): trackKey = series.key try: track = plex.fetchItem(trackKey) albumKey = track.parentKey album = plex.fetchItem(albumKey) albums.append(album) except Exception: # print('{} contains additional subfolders that were likely captured. \n[{}].' # .format(series.title, ', '.join([x.title for x in series.allSubfolders()]))) pass for album in list(set(albums)): print('Adding {} to collection {}.'.format(album.title, COLLECTIONAME)) album.addCollection(COLLECTIONAME)
else: logger.error("Missing --libraries or --allLibraries") exit() if opts.action == 'update': logger.info("Deleting the playlist(s)...") for data in playlist_dict['data']: delete_playlist(data, title) logger.info('Creating playlist(s)...') for data in playlist_dict['data']: create_playlist(title, keys_list, data['server'], data['user']) if opts.action == 'add': if opts.jbop == 'collection': logger.info('Creating collection(s)...') for key in keys_list: item = plex.fetchItem(int(key)) item.addCollection([title]) elif opts.jbop == 'label': logger.info('Creating label(s)...') for key in keys_list: item = plex.fetchItem(int(key)) item.addLabel([title]) else: logger.info('Creating playlist(s)...') for data in playlist_dict['data']: create_playlist(title, keys_list, data['server'], data['user']) logger.info("Done.")
class PlexWrapper(object): def __init__(self): self.baseurl = os.environ.get("PLEX_BASE_URL") token = os.environ.get("PLEX_TOKEN") self.maxresults = int(os.environ.get("MAXRESULTS", 50)) verify_ssl = os.environ.get("BYPASS_SSL_VERIFY", "0") != "1" self.libraries = [ x.strip() for x in os.environ.get("LIBRARY_NAMES", "Movies").split(";") if x.strip() != "" ] session = requests.Session() session.verify = verify_ssl self.plex = PlexServer(self.baseurl, token, session=session, timeout=(60 * 60)) def _get_sections(self): return [ self.plex.library.section(title=library) for library in self.libraries ] def get_server_info(self): return { 'name': self.plex.friendlyName, 'url': self.baseurl + '/web/index.html' } def get_dupe_content(self): dupes = [] for section in self._get_sections(): for movie in section.search(duplicate=True, maxresults=self.maxresults, libtype='movie'): if len(movie.media) > 1: dupes.append(self.movie_to_dict(movie, section.title)) for episode in section.search(duplicate=True, maxresults=self.maxresults, libtype='episode'): if len(episode.media) > 1: dupes.append(self.episode_to_dict(episode, section.title)) return dupes def get_content_sample_files(self): content = [] for section in self._get_sections(): for mediaContent in section.all(): samples = [] if mediaContent.TYPE != 'movie' or mediaContent.TYPE != 'episode': continue for media in mediaContent.media: if media.duration is None or media.duration < (5 * 60 * 1000): samples.append(self.media_to_dict(media)) if len(samples) > 0: _media = dict() if mediaContent.TYPE == 'movie': _media = self.movie_to_dict(mediaContent, section.title) elif mediaContent.TYPE == 'episode': _media = self.episode_to_dict(mediaContent, section.title) _media["media"] = samples content.append(_media) return content def get_content(self, media_id): return self.plex.fetchItem(media_id) def video_to_dict(self, video: Video) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Video return { "addedAt": str(video.addedAt), "key": video.key, "lastViewedAt": str(video.lastViewedAt), "librarySectionID": video.librarySectionID, "summary": video.summary, "thumbUrl": video.thumbUrl, "title": video.title, "titleSort": video.titleSort, "type": video.type, "updatedAt": str(video.updatedAt), "viewCount": str(video.viewCount), "url": self.baseurl + '/web/index.html#!/server/' + self.plex.machineIdentifier + '/details?key=' + urllib.parse.quote_plus(video.key) } def movie_to_dict(self, movie: Movie, library: str) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Movie return { **self.video_to_dict(movie), "contentType": 'movie', "library": library, "duration": movie.duration, "guid": movie.guid, "originalTitle": movie.originalTitle, "originallyAvailableAt": str(movie.originallyAvailableAt), "rating": movie.rating, "ratingImage": movie.ratingImage, "studio": movie.studio, "tagline": movie.tagline, "userRating": movie.userRating, "year": movie.year, "media": [self.media_to_dict(media) for media in movie.media], } def episode_to_dict(self, episode: Episode, library: str) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/video.html#plexapi.video.Movie return { **self.video_to_dict(episode), "contentType": 'episode', "library": library, "duration": episode.duration, "guid": episode.guid, "originalTitle": episode.title, "originallyAvailableAt": str(episode.originallyAvailableAt), "rating": episode.rating, "year": episode.year, "seasonNumber": episode.seasonNumber, "seasonEpisode": episode.seasonEpisode, "seriesTitle": episode.grandparentTitle, "media": [self.media_to_dict(media) for media in episode.media], } @classmethod def media_to_dict(cls, media: Media) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.Media return { "id": media.id, # 'initpath': media.initpath, # 'video': media.video, "aspectRatio": media.aspectRatio, "audioChannels": media.audioChannels, "audioCodec": media.audioCodec, "bitrate": media.bitrate, "container": media.container, "duration": media.duration, "width": media.width, "height": media.height, "has64bitOffsets": media.has64bitOffsets, "optimizedForStreaming": media.optimizedForStreaming, "target": media.target, "title": media.title, "videoCodec": media.videoCodec, "videoFrameRate": media.videoFrameRate, "videoResolution": media.videoResolution, "videoProfile": media.videoProfile, "parts": [cls.media_part_to_dict(media_part) for media_part in media.parts], } @classmethod def media_part_to_dict(cls, media_part: MediaPart) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPart return { "id": media_part.id, # 'media_id': media_part.media.id, # 'initpath': media_part.initpath, "container": media_part.container, "duration": media_part.duration, "file": media_part.file, "indexes": media_part.indexes, "key": media_part.key, "size": media_part.size, "exists": media_part.exists, "accessible": media_part.accessible, "streams": [ cls.media_part_stream_to_dict(media_part_stream) for media_part_stream in media_part.videoStreams() ], } @classmethod def media_part_stream_to_dict(cls, media_part_stream: MediaPartStream) -> dict: # https://python-plexapi.readthedocs.io/en/latest/modules/media.html#plexapi.media.MediaPartStream return { "id": media_part_stream.id, # 'media_id': media_part.media.id, # 'initpath': media_part.initpath, "codec": media_part_stream.codec, "codecID": media_part_stream.codecID, "language": media_part_stream.language, "languageCode": media_part_stream.languageCode, "selected": media_part_stream.selected, "type": media_part_stream.type, }
'Chromecast': 'Chromecast message' } USER_IGNORE = ('') # ('Username','User2') PLEXPY_LOG = 'Killing {user}\'s stream of {title} due to video transcoding of {original} content' ## sess = requests.Session() sess.verify = False plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess) if __name__ == '__main__': rating_key = sys.argv[1] item = plex.fetchItem(int(rating_key)) orig_quality = item.media[0].videoResolution # print(orig_quality) for session in plex.sessions(): username = session.usernames[0] media_type = session.type if username not in USER_IGNORE and media_type != 'track': title = session.title sess_rating = session.key.split('/')[3] trans_dec = session.transcodeSessions[0].videoDecision if sess_rating == str( rating_key ) and orig_quality in TARGET_QUALITY and trans_dec == 'transcode': reason = DEVICES.get(session.players[0].platform, DEFAULT_REASON)
def cli(plex_url, plex_token, plex_section, myshows_api_url, myshows_username, myshows_password, cache_dir, what_if): try: myshows = MyShows(myshows_api_url, myshows_username, myshows_password) except Exception as exc: print(exc) log.error(exc) sys.exit(1) try: plex_instance = PlexServer(plex_url, plex_token) except Exception as exc: print('No Plex Media Server found at {}'.format(plex_url)) log.error(exc) sys.exit(1) plex = Plex(plex_instance) try: watched_episodes = plex.get_watched_episodes(plex_section) myshows_series_id = {} try: with open('{}/series_cache'.format(cache_dir), 'rb') as cache_file: series_cache = pickle.load(cache_file) except EOFError: series_cache = [] series_cache_size = len(series_cache) for entry in watched_episodes: if entry.ratingKey in series_cache: continue entry_key = (entry.grandparentTitle, plex_instance.fetchItem( entry.grandparentRatingKey).year) if entry_key not in myshows_series_id: series_id = myshows.get_series_id( entry.grandparentTitle, plex_instance.fetchItem(entry.grandparentRatingKey).year) myshows_series_id.update({entry_key: series_id}) else: series_id = myshows_series_id[entry_key] if series_id: episode_id = myshows.get_episode_id(series_id, entry.parentIndex, entry.index) if episode_id: myshows_watched_episodes = myshows.get_watched_episodes_id( series_id) if not myshows_watched_episodes or episode_id not in myshows_watched_episodes: info = myshows.get_episode_info(episode_id) if what_if: if info: print( '{} season {} episode {} will be marked as watched' .format(info['series_title'], info['season'], info['episode'])) log.info( '{} season {} episode {} will be marked as watched' .format(info['series_title'], info['season'], info['episode'])) else: print('Episode with id {} not found'.format( episode_id)) log.warning( 'Episode with id {} not found'.format( episode_id)) else: if myshows.mark_episode_as_watch(episode_id): print( '{} season {} episode {} mark as watched'. format(info['series_title'], info['season'], info['episode'])) log.info( '{} season {} episode {} mark as watched'. format(info['series_title'], info['season'], info['episode'])) series_cache.append(entry.ratingKey) else: series_cache.append(entry.ratingKey) else: print('{} season {} episode {} not found'.format( entry.grandparentTitle, entry.parentIndex, entry.index)) log.warning('{} season {} episode {} not found'.format( entry.grandparentTitle, entry.parentIndex, entry.index)) else: print('Series {} not found'.format(entry.grandparentTitle)) log.warning('Series {} not found'.format( entry.grandparentTitle)) if len(series_cache) != series_cache_size: with open('{}/series_cache'.format(cache_dir), 'wb+') as cache_file: pickle.dump(series_cache, cache_file) except Exception as exc: print(exc) log.error(exc) sys.exit(1)
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")