Merge pull request #122 from meisnate12/develop

v1.5.1
pull/144/head v1.5.1
meisnate12 4 years ago committed by GitHub
commit bddaa35733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
# Plex Meta Manager
#### Version 1.5.0
#### Version 1.5.1
The original concept for Plex Meta Manager is [Plex Auto Collections](https://github.com/mza921/Plex-Auto-Collections), but this is rewritten from the ground up to be able to include a scheduler, metadata edits, multiple libraries, and logging. Plex Meta Manager is a Python 3 script that can be continuously run using YAML configuration files to update on a schedule the metadata of the movies, shows, and collections in your libraries as well as automatically build collections based on various methods all detailed in the wiki. Some collection examples that the script can automatically build and update daily include Plex Based Searches like actor, genre, or studio collections or Collections based on TMDb, IMDb, Trakt, TVDb, AniDB, or MyAnimeList lists and various other services.

@ -1290,7 +1290,7 @@ collections:
plex_collectionless:
exclude_prefix:
- +
- "~"
- ~
sort_title: ~_Collectionless
collection_order: alpha
collection_order: alpha

@ -41,12 +41,11 @@ class AniListAPI:
count = 0
page_num = 0
if variables is None:
variables = {"page": page_num}
else:
variables["page"] = page_num
variables = {}
next_page = True
while next_page:
page_num += 1
variables["page"] = page_num
json_obj = self.send_request(query, variables)
next_page = json_obj["data"]["Page"]["pageInfo"]["hasNextPage"]
for media in json_obj["data"]["Page"]["media"]:
@ -84,7 +83,7 @@ class AniListAPI:
def season(self, season, year, sort, limit):
query = """
query ($page: Int, $season: String, $year: Int, $sort: String) {
query ($page: Int, $season: MediaSeason, $year: Int, $sort: [MediaSort]) {
Page(page: $page){
pageInfo {hasNextPage}
media(season: $season, seasonYear: $year, type: ANIME, sort: $sort){idMal}

@ -358,12 +358,12 @@ class CollectionBuilder:
elif method_name in util.dictionary_lists:
if isinstance(data[m], dict):
def get_int(parent, method, data_in, default_in, minimum=1, maximum=None):
if method not in data_in: logger.warning(f"Collection Warning: {parent} {method} attribute not found using {default} as default")
elif not data_in[method]: logger.warning(f"Collection Warning: {parent} {method} attribute is blank using {default} as default")
if method not in data_in: logger.warning(f"Collection Warning: {parent} {method} attribute not found using {default_in} as default")
elif not data_in[method]: logger.warning(f"Collection Warning: {parent} {method} attribute is blank using {default_in} as default")
elif isinstance(data_in[method], int) and data_in[method] >= minimum:
if maximum is None or data_in[method] <= maximum: return data_in[method]
else: logger.warning(f"Collection Warning: {parent} {method} attribute {data_in[method]} invalid must an integer <= {maximum} using {default} as default")
else: logger.warning(f"Collection Warning: {parent} {method} attribute {data_in[method]} invalid must an integer >= {minimum} using {default} as default")
else: logger.warning(f"Collection Warning: {parent} {method} attribute {data_in[method]} invalid must an integer <= {maximum} using {default_in} as default")
else: logger.warning(f"Collection Warning: {parent} {method} attribute {data_in[method]} invalid must an integer >= {minimum} using {default_in} as default")
return default_in
if method_name == "filters":
for f in data[m]:
@ -406,9 +406,6 @@ class CollectionBuilder:
if isinstance(data[m]["exclude"], list): exact_list.extend(data[m]["exclude"])
else: exact_list.append(str(data[m]["exclude"]))
if len(prefix_list) == 0 and len(exact_list) == 0: raise Failed("Collection Error: you must have at least one exclusion")
self.details["add_to_arr"] = False
self.details["collection_mode"] = "hide"
self.sync = True
new_dictionary["exclude_prefix"] = prefix_list
new_dictionary["exclude"] = exact_list
self.methods.append((method_name, [new_dictionary]))
@ -641,6 +638,11 @@ class CollectionBuilder:
if self.library.Sonarr:
self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Sonarr.add
if self.collectionless:
self.details["add_to_arr"] = False
self.details["collection_mode"] = "hide"
self.sync = True
def run_methods(self, collection_obj, collection_name, rating_key_map, movie_map, show_map):
items_found = 0
for method, values in self.methods:
@ -718,20 +720,21 @@ class CollectionBuilder:
if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)):
keep_collection = False
break
for ext in value["exclude"]:
if col.title == ext or (col.titleSort and col.titleSort == ext):
keep_collection = False
break
if keep_collection:
good_collections.append(col.title.lower())
for ext in value["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.library.Plex.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.tag.lower() in good_collections:
if collection.id in good_collections:
add_item = False
break
if add_item:

@ -122,6 +122,8 @@ class PlexAPI:
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
@ -397,7 +399,7 @@ class PlexAPI:
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")
output = match.group(0)[1:].split("E" if "E" in match.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}...")

@ -112,9 +112,11 @@ class TraktAPI:
return requests.get(url, headers={"Content-Type": "application/json", "trakt-api-version": "2", "trakt-api-key": self.client_id}).json()
def get_pagenation(self, pagenation, amount, is_movie):
items = self.send_request(f"{self.base_url}/{'movies' if is_movie else 'shows'}/{pagenation}?limit={amount}")
if is_movie: return [item["ids"]["tmdb"] for item in items], []
else: return [], [item["ids"]["tvdb"] for item in items]
items = self.send_request(f"{self.base_url}/{'movies' if not is_movie else 'shows'}/{pagenation}?limit={amount}")
if pagenation == "popular" and is_movie: return [item["ids"]["tmdb"] for item in items], []
elif pagenation == "popular": return [], [item["ids"]["tvdb"] for item in items]
elif is_movie: return [item["movie"]["ids"]["tmdb"] for item in items], []
else: return [], [item["show"]["ids"]["tvdb"] for item in items]
def validate_trakt_list(self, values):
trakt_values = []

@ -15,23 +15,45 @@ parser.add_argument("-c", "--config", dest="config", help="Run with desired *.ym
parser.add_argument("-t", "--time", dest="time", help="Time to update each day use format HH:MM (Default: 03:00)", default="03:00", type=str)
parser.add_argument("-r", "--run", dest="run", help="Run without the scheduler", action="store_true", default=False)
parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False)
parser.add_argument("-cl", "--collection", "--collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str, default="")
parser.add_argument("-cl", "--collection", "--collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str)
parser.add_argument("-d", "--divider", dest="divider", help="Character that divides the sections (Default: '=')", default="=", type=str)
parser.add_argument("-w", "--width", dest="width", help="Screen Width (Default: 100)", default=100, type=int)
args = parser.parse_args()
if not re.match("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$", args.time):
raise util.Failed(f"Argument Error: time argument invalid: {args.time} must be in the HH:MM format")
def check_bool(env_str, default):
env_var = os.environ.get(env_str)
if env_var is not None:
if env_var is True or env_var is False:
return env_var
elif env_var.lower() in ["t", "true"]:
return True
else:
return False
else:
return default
my_tests = check_bool("PMM_TESTS", args.tests)
test = check_bool("PMM_TEST", args.test)
debug = check_bool("PMM_DEBUG", args.debug)
run = check_bool("PMM_RUN", args.run)
collections = os.environ.get("PMM_COLLECTIONS") if os.environ.get("PMM_COLLECTIONS") else args.collections
time_to_run = os.environ.get("PMM_TIME") if os.environ.get("PMM_TIME") else args.time
if not re.match("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$", time_to_run):
raise util.Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format")
util.separating_character = os.environ.get("PMM_DIVIDER")[0] if os.environ.get("PMM_DIVIDER") else args.divider[0]
util.separating_character = args.divider[0]
if 90 <= args.width <= 300:
util.screen_width = args.width
screen_width = os.environ.get("PMM_WIDTH") if os.environ.get("PMM_WIDTH") else args.width
if 90 <= screen_width <= 300:
util.screen_width = screen_width
else:
raise util.Failed(f"Argument Error: width argument invalid: {args.width} must be an integer between 90 and 300")
raise util.Failed(f"Argument Error: width argument invalid: {screen_width} must be an integer between 90 and 300")
config_file = os.environ.get("PMM_CONFIG") if os.environ.get("PMM_CONFIG") else args.config
default_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config")
if args.config and os.path.exists(args.config): default_dir = os.path.join(os.path.dirname(os.path.abspath(args.config)))
elif args.config and not os.path.exists(args.config): raise util.Failed(f"Config Error: config not found at {os.path.abspath(args.config)}")
if config_file and os.path.exists(config_file): default_dir = os.path.join(os.path.dirname(os.path.abspath(config_file)))
elif config_file and not os.path.exists(config_file): raise util.Failed(f"Config Error: config not found at {os.path.abspath(config_file)}")
elif not os.path.exists(os.path.join(default_dir, "config.yml")): raise util.Failed(f"Config Error: config not found at {os.path.abspath(default_dir)}")
os.makedirs(os.path.join(default_dir, "logs"), exist_ok=True)
@ -50,7 +72,7 @@ file_handler.setFormatter(logging.Formatter("[%(asctime)s] %(filename)-27s %(lev
cmd_handler = logging.StreamHandler()
cmd_handler.setFormatter(logging.Formatter("| %(message)-100s |"))
cmd_handler.setLevel(logging.DEBUG if args.tests or args.test or args.debug else logging.INFO)
cmd_handler.setLevel(logging.DEBUG if tests or test or debug else logging.INFO)
logger.addHandler(cmd_handler)
logger.addHandler(file_handler)
@ -65,23 +87,23 @@ logger.info(util.get_centered_text("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _`
logger.info(util.get_centered_text("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | "))
logger.info(util.get_centered_text("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| "))
logger.info(util.get_centered_text(" |___/ "))
logger.info(util.get_centered_text(" Version: 1.5.0 "))
logger.info(util.get_centered_text(" Version: 1.5.1 "))
util.separator()
if args.tests:
if my_tests:
tests.run_tests(default_dir)
sys.exit(0)
def start(config_path, test, daily, collections):
def start(config_path, is_test, daily, collections):
if daily: start_type = "Daily "
elif test: start_type = "Test "
elif is_test: start_type = "Test "
elif collections: start_type = "Collections "
else: start_type = ""
start_time = datetime.now()
util.separator(f"Starting {start_type}Run")
try:
config = Config(default_dir, config_path)
config.update_libraries(test, collections)
config.update_libraries(is_test, collections)
except Exception as e:
util.print_stacktrace()
logger.critical(e)
@ -89,15 +111,15 @@ def start(config_path, test, daily, collections):
util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}")
try:
if args.run or args.test or args.collections:
start(args.config, args.test, False, args.collections)
if run or test or collections:
start(config_file, test, False, collections)
else:
length = 0
schedule.every().day.at(args.time).do(start, args.config, False, True, None)
schedule.every().day.at(time_to_run).do(start, config_file, False, True, None)
while True:
schedule.run_pending()
current = datetime.now().strftime("%H:%M")
seconds = (datetime.strptime(args.time, "%H:%M") - datetime.strptime(current, "%H:%M")).total_seconds()
seconds = (datetime.strptime(time_to_run, "%H:%M") - datetime.strptime(current, "%H:%M")).total_seconds()
hours = int(seconds // 3600)
if hours < 0:
hours += 24
@ -105,7 +127,7 @@ try:
time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else ""
time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}"
length = util.print_return(length, f"Current Time: {current} | {time_str} until the daily run at {args.time}")
length = util.print_return(length, f"Current Time: {current} | {time_str} until the daily run at {time_to_run}")
time.sleep(1)
except KeyboardInterrupt:
util.separator("Exiting Plex Meta Manager")

Loading…
Cancel
Save