From 74b5ea7b5e6a2f33b4000dfde0ca8758432a10de Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sun, 19 Jun 2022 02:56:50 -0600 Subject: [PATCH] Fix username differences in watch list. Add python version check. More error handling. --- main.py | 403 +---------------------------------------------- src/functions.py | 11 +- src/jellyfin.py | 357 ++++++++++++++++++++++------------------- src/main.py | 400 ++++++++++++++++++++++++++++++++++++++++++++++ src/plex.py | 352 ++++++++++++++++++++++------------------- 5 files changed, 795 insertions(+), 728 deletions(-) create mode 100644 src/main.py diff --git a/main.py b/main.py index 5276f08..866f866 100644 --- a/main.py +++ b/main.py @@ -1,397 +1,10 @@ -import copy, os, traceback, json -from dotenv import load_dotenv -from time import sleep +import sys +if __name__ == '__main__': + # Check python version 3.6 or higher + if not (3, 6) <= tuple(map(int, sys.version_info[:2])): + print("This script requires Python 3.6 or higher") + sys.exit(1) -from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor -from src.plex import Plex -from src.jellyfin import Jellyfin - -load_dotenv(override=True) - -def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None): - modified_watched_list_1 = copy.deepcopy(watched_list_1) - - # remove entries from plex_watched that are in jellyfin_watched - for user_1 in watched_list_1: - user_other = None - if user_mapping: - user_other = search_mapping(user_mapping, user_1) - if user_1 in modified_watched_list_1: - if user_1 in watched_list_2: - user_2 = user_1 - elif user_other in watched_list_2: - user_2 = user_other - else: - logger(f"User {user_1} and {user_other} not found in watched list 2", 1) - continue - - for library_1 in watched_list_1[user_1]: - library_other = None - if library_mapping: - library_other = search_mapping(library_mapping, library_1) - if library_1 in modified_watched_list_1[user_1]: - if library_1 in watched_list_2[user_2]: - library_2 = library_1 - elif library_other in watched_list_2[user_2]: - library_2 = library_other - else: - logger(f"library {library_1} and {library_other} not found in watched list 2", 1) - continue - - # Movies - if isinstance(watched_list_1[user_1][library_1], list): - for item in watched_list_1[user_1][library_1]: - for watch_list_1_key, watch_list_1_value in item.items(): - for watch_list_2_item in watched_list_2[user_2][library_2]: - for watch_list_2_item_key, watch_list_2_item_value in watch_list_2_item.items(): - if watch_list_1_key == watch_list_2_item_key and watch_list_1_value == watch_list_2_item_value: - if item in modified_watched_list_1[user_1][library_1]: - logger(f"Removing {item} from {library_1}", 3) - modified_watched_list_1[user_1][library_1].remove(item) - - - # TV Shows - elif isinstance(watched_list_1[user_1][library_1], dict): - # Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1 - _, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 1) - - for show_key_1 in watched_list_1[user_1][library_1].keys(): - show_key_dict = dict(show_key_1) - for season in watched_list_1[user_1][library_1][show_key_1]: - for episode in watched_list_1[user_1][library_1][show_key_1][season]: - for episode_key, episode_item in episode.items(): - # If episode_key and episode_item are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1 - if episode_key in episode_watched_list_2_keys_dict.keys(): - if episode_item in episode_watched_list_2_keys_dict[episode_key]: - if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]: - logger(f"Removing {show_key_dict['title']} {episode} from {library_1}", 3) - modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode) - - # Remove empty seasons - if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0: - if season in modified_watched_list_1[user_1][library_1][show_key_1]: - logger(f"Removing {season} from {library_1} because it is empty", 3) - del modified_watched_list_1[user_1][library_1][show_key_1][season] - - # If the show is empty, remove the show - if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0: - if show_key_1 in modified_watched_list_1[user_1][library_1]: - logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1) - del modified_watched_list_1[user_1][library_1][show_key_1] - - for user_1 in watched_list_1: - for library_1 in watched_list_1[user_1]: - if library_1 in modified_watched_list_1[user_1]: - # If library is empty then remove it - if len(modified_watched_list_1[user_1][library_1]) == 0: - logger(f"Removing {library_1} from {user_1} because it is empty", 1) - del modified_watched_list_1[user_1][library_1] - - if user_1 in modified_watched_list_1: - # If user is empty delete user - if len(modified_watched_list_1[user_1]) == 0: - logger(f"Removing {user_1} from watched list 1 because it is empty", 1) - del modified_watched_list_1[user_1] - - return modified_watched_list_1 - -def setup_black_white_lists(library_mapping=None): - blacklist_library = os.getenv("BLACKLIST_LIBRARY") - if blacklist_library: - if len(blacklist_library) > 0: - blacklist_library = blacklist_library.split(",") - blacklist_library = [x.strip() for x in blacklist_library] - if library_mapping: - temp_library = [] - for library in blacklist_library: - library_other = search_mapping(library_mapping, library) - if library_other: - temp_library.append(library_other) - - blacklist_library = blacklist_library + temp_library - else: - blacklist_library = [] - - logger(f"Blacklist Library: {blacklist_library}", 1) - - whitelist_library = os.getenv("WHITELIST_LIBRARY") - if whitelist_library: - if len(whitelist_library) > 0: - whitelist_library = whitelist_library.split(",") - whitelist_library = [x.strip() for x in whitelist_library] - if library_mapping: - temp_library = [] - for library in whitelist_library: - library_other = search_mapping(library_mapping, library) - if library_other: - temp_library.append(library_other) - - whitelist_library = whitelist_library + temp_library - else: - whitelist_library = [] - logger(f"Whitelist Library: {whitelist_library}", 1) - - blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE") - if blacklist_library_type: - if len(blacklist_library_type) > 0: - blacklist_library_type = blacklist_library_type.split(",") - blacklist_library_type = [x.lower().strip() for x in blacklist_library_type] - else: - blacklist_library_type = [] - logger(f"Blacklist Library Type: {blacklist_library_type}", 1) - - whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE") - if whitelist_library_type: - if len(whitelist_library_type) > 0: - whitelist_library_type = whitelist_library_type.split(",") - whitelist_library_type = [x.lower().strip() for x in whitelist_library_type] - else: - whitelist_library_type = [] - logger(f"Whitelist Library Type: {whitelist_library_type}", 1) - - blacklist_users = os.getenv("BLACKLIST_USERS") - if blacklist_users: - if len(blacklist_users) > 0: - blacklist_users = blacklist_users.split(",") - blacklist_users = [x.lower().strip() for x in blacklist_users] - else: - blacklist_users = [] - logger(f"Blacklist Users: {blacklist_users}", 1) - - whitelist_users = os.getenv("WHITELIST_USERS") - if whitelist_users: - if len(whitelist_users) > 0: - whitelist_users = whitelist_users.split(",") - whitelist_users = [x.lower().strip() for x in whitelist_users] - else: - whitelist_users = [] - else: - whitelist_users = [] - logger(f"Whitelist Users: {whitelist_users}", 1) - - return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users - -def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None): - - # generate list of users from server 1 and server 2 - server_1_type = server_1[0] - server_1_connection = server_1[1] - server_2_type = server_2[0] - server_2_connection = server_2[1] - - server_1_users = [] - if server_1_type == "plex": - server_1_users = [ x.title.lower() for x in server_1_connection.users ] - elif server_1_type == "jellyfin": - server_1_users = [ key.lower() for key in server_1_connection.users.keys() ] - - server_2_users = [] - if server_2_type == "plex": - server_2_users = [ x.title.lower() for x in server_2_connection.users ] - elif server_2_type == "jellyfin": - server_2_users = [ key.lower() for key in server_2_connection.users.keys() ] - - - # combined list of overlapping users from plex and jellyfin - users = {} - - for server_1_user in server_1_users: - if user_mapping: - jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user) - if jellyfin_plex_mapped_user: - users[server_1_user] = jellyfin_plex_mapped_user - continue - - if server_1_user in server_2_users: - users[server_1_user] = server_1_user - - for server_2_user in server_2_users: - if user_mapping: - plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user) - if plex_jellyfin_mapped_user: - users[plex_jellyfin_mapped_user] = server_2_user - continue - - if server_2_user in server_1_users: - users[server_2_user] = server_2_user - - logger(f"User list that exist on both servers {users}", 1) - - users_filtered = {} - for user in users: - # whitelist_user is not empty and user lowercase is not in whitelist lowercase - if len(whitelist_users) > 0: - if user not in whitelist_users and users[user] not in whitelist_users: - logger(f"{user} or {users[user]} is not in whitelist", 1) - continue - - if user not in blacklist_users and users[user] not in blacklist_users: - users_filtered[user] = users[user] - - logger(f"Filtered user list {users_filtered}", 1) - - if server_1_type == "plex": - output_server_1_users = [] - for plex_user in server_1_connection.users: - if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): - output_server_1_users.append(plex_user) - elif server_1_type == "jellyfin": - output_server_1_users = {} - for jellyfin_user, jellyfin_id in server_1_connection.users.items(): - if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): - output_server_1_users[jellyfin_user] = jellyfin_id - - if server_2_type == "plex": - output_server_2_users = [] - for plex_user in server_2_connection.users: - if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): - output_server_2_users.append(plex_user) - elif server_2_type == "jellyfin": - output_server_2_users = {} - for jellyfin_user, jellyfin_id in server_2_connection.users.items(): - if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): - output_server_2_users[jellyfin_user] = jellyfin_id - - if len(output_server_1_users) == 0: - raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}") - - if len(output_server_2_users) == 0: - raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}") - - logger(f"Server 1 users: {output_server_1_users}", 1) - logger(f"Server 2 users: {output_server_2_users}", 1) - - return output_server_1_users, output_server_2_users - -def generate_server_connections(): - servers = [] - - plex_baseurl = os.getenv("PLEX_BASEURL", None) - plex_token = os.getenv("PLEX_TOKEN", None) - plex_username = os.getenv("PLEX_USERNAME", None) - plex_password = os.getenv("PLEX_PASSWORD", None) - plex_servername = os.getenv("PLEX_SERVERNAME", None) - - if plex_baseurl and plex_token: - plex_baseurl = plex_baseurl.split(",") - plex_token = plex_token.split(",") - - if len(plex_baseurl) != len(plex_token): - raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries") - - for i, url in enumerate(plex_baseurl): - servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None))) - - if plex_username and plex_password and plex_servername: - plex_username = plex_username.split(",") - plex_password = plex_password.split(",") - plex_servername = plex_servername.split(",") - - if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername): - raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries") - - for i, username in enumerate(plex_username): - servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip()))) - - jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None) - jellyfin_token = os.getenv("JELLYFIN_TOKEN", None) - - if jellyfin_baseurl and jellyfin_token: - jellyfin_baseurl = jellyfin_baseurl.split(",") - jellyfin_token = jellyfin_token.split(",") - - if len(jellyfin_baseurl) != len(jellyfin_token): - raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries") - - for i, baseurl in enumerate(jellyfin_baseurl): - servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()))) - - return servers - -def main(): - logfile = os.getenv("LOGFILE","log.log") - # Delete logfile if it exists - if os.path.exists(logfile): - os.remove(logfile) - - dryrun = str_to_bool(os.getenv("DRYRUN", "False")) - logger(f"Dryrun: {dryrun}", 1) - - user_mapping = os.getenv("USER_MAPPING") - if user_mapping: - user_mapping = json.loads(user_mapping.lower()) - logger(f"User Mapping: {user_mapping}", 1) - - library_mapping = os.getenv("LIBRARY_MAPPING") - if library_mapping: - library_mapping = json.loads(library_mapping) - logger(f"Library Mapping: {library_mapping}", 1) - - # Create (black/white)lists - blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(library_mapping) - - # Create server connections - servers = generate_server_connections() - - for server_1 in servers: - # If server is the final server in the list, then we are done with the loop - if server_1 == servers[-1]: - break - - # Start server_2 at the next server in the list - for server_2 in servers[servers.index(server_1) + 1:]: - - server_1_connection = server_1[1] - server_2_connection = server_2[1] - - # Create users list - server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) - - args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping] - , [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]] - - results = future_thread_executor(args) - server_1_watched = results[0] - server_2_watched = results[1] - - # clone watched so it isnt modified in the cleanup function so all duplicates are actually removed - server_1_watched_filtered = copy.deepcopy(server_1_watched) - server_2_watched_filtered = copy.deepcopy(server_2_watched) - - logger("Cleaning Server 1 Watched", 1) - server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping) - - logger("Cleaning Server 2 Watched", 1) - server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping) - - logger(f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}", 1) - logger(f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}", 1) - - args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun] - , [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]] - - future_thread_executor(args) - -if __name__ == "__main__": - sleep_duration = float(os.getenv("SLEEP_DURATION", "3600")) - - while(True): - try: - main() - logger(f"Looping in {sleep_duration}") - sleep(sleep_duration) - except Exception as error: - if isinstance(error, list): - for message in error: - logger(message, log_type=2) - else: - logger(error, log_type=2) - - - logger(traceback.format_exc(), 2) - logger(f"Retrying in {sleep_duration}", log_type=0) - - except KeyboardInterrupt: - logger("Exiting", log_type=0) - os._exit(0) + from src.main import main + main() diff --git a/src/functions.py b/src/functions.py index d6511eb..0ffafe9 100644 --- a/src/functions.py +++ b/src/functions.py @@ -39,11 +39,11 @@ def search_mapping(dictionary: dict, key_value: str): if key_value in dictionary.keys(): return dictionary[key_value] elif key_value.lower() in dictionary.keys(): - return dictionary[key_value] + return dictionary[key_value.lower()] elif key_value in dictionary.values(): return list(dictionary.keys())[list(dictionary.values()).index(key_value)] elif key_value.lower() in dictionary.values(): - return list(dictionary.keys())[list(dictionary.values()).index(key_value)] + return list(dictionary.keys())[list(dictionary.values()).index(key_value.lower())] else: return None @@ -116,11 +116,14 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): return show_output_dict, episode_output_dict, movies_output_dict -def future_thread_executor(args: list): +def future_thread_executor(args: list, workers: int = -1): futures_list = [] results = [] + workers=1 + if workers == -1: + workers = min(32, os.cpu_count()*1.25) - with ThreadPoolExecutor() as executor: + with ThreadPoolExecutor(max_workers=workers) as executor: for arg in args: # * arg unpacks the list into actual arguments futures_list.append(executor.submit(*arg)) diff --git a/src/jellyfin.py b/src/jellyfin.py index a98ea8b..b0a753d 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -39,216 +39,241 @@ class Jellyfin(): response = self.session.post(self.baseurl + query, headers=headers) return response.json() + except Exception as e: - logger(e, 2) - logger(response, 2) + logger(f"Jellyfin: Query failed {e}", 2) + raise Exception(e) def get_users(self): - users = {} + try: + users = {} - query = "/Users" - response = self.query(query, "get") + query = "/Users" + response = self.query(query, "get") - # If reponse is not empty - if response: - for user in response: - users[user["Name"]] = user["Id"] + # If reponse is not empty + if response: + for user in response: + users[user["Name"]] = user["Id"] - return users + return users + except Exception as e: + logger(f"Jellyfin: Get users failed {e}", 2) + raise Exception(e) def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): - user_watched = {} - user_watched[user_name] = {} + try: + user_name = user_name.lower() + user_watched = {} + user_watched[user_name] = {} - logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0) - # Movies - if library_type == "Movie": - user_watched[user_name][library_title] = [] - watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds", "get") - for movie in watched["Items"]: - if movie["UserData"]["Played"] == True: - if movie["ProviderIds"]: - # Lowercase movie["ProviderIds"] keys - movie["ProviderIds"] = {k.lower(): v for k, v in movie["ProviderIds"].items()} - user_watched[user_name][library_title].append(movie["ProviderIds"]) + logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0) + # Movies + if library_type == "Movie": + user_watched[user_name][library_title] = [] + watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds", "get") + for movie in watched["Items"]: + if movie["UserData"]["Played"] == True: + if movie["ProviderIds"]: + # Lowercase movie["ProviderIds"] keys + movie["ProviderIds"] = {k.lower(): v for k, v in movie["ProviderIds"].items()} + user_watched[user_name][library_title].append(movie["ProviderIds"]) - # TV Shows - if library_type == "Episode": - user_watched[user_name][library_title] = {} - watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Fields=ItemCounts,ProviderIds", "get") - watched_shows = [x for x in watched["Items"] if x["Type"] == "Series"] + # TV Shows + if library_type == "Episode": + user_watched[user_name][library_title] = {} + watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Fields=ItemCounts,ProviderIds", "get") + watched_shows = [x for x in watched["Items"] if x["Type"] == "Series"] - for show in watched_shows: - show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()} - show_guids["title"] = show["Name"] - show_guids = frozenset(show_guids.items()) - seasons = self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&Fields=ItemCounts,ProviderIds", "get") - if len(seasons["Items"]) > 0: - for season in seasons["Items"]: - episodes = self.query(f"/Shows/{show['Id']}/Episodes?seasonId={season['Id']}&userId={user_id}&Fields=ItemCounts,ProviderIds", "get") - if len(episodes["Items"]) > 0: - for episode in episodes["Items"]: - if episode["UserData"]["Played"] == True: - if episode["ProviderIds"]: - if show_guids not in user_watched[user_name][library_title]: - user_watched[user_name][library_title][show_guids] = {} - if season["Name"] not in user_watched[user_name][library_title][show_guids]: - user_watched[user_name][library_title][show_guids][season["Name"]] = [] + for show in watched_shows: + show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()} + show_guids["title"] = show["Name"] + show_guids = frozenset(show_guids.items()) + seasons = self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&Fields=ItemCounts,ProviderIds", "get") + if len(seasons["Items"]) > 0: + for season in seasons["Items"]: + episodes = self.query(f"/Shows/{show['Id']}/Episodes?seasonId={season['Id']}&userId={user_id}&Fields=ItemCounts,ProviderIds", "get") + if len(episodes["Items"]) > 0: + for episode in episodes["Items"]: + if episode["UserData"]["Played"] == True: + if episode["ProviderIds"]: + if show_guids not in user_watched[user_name][library_title]: + user_watched[user_name][library_title][show_guids] = {} + if season["Name"] not in user_watched[user_name][library_title][show_guids]: + user_watched[user_name][library_title][show_guids][season["Name"]] = [] - # Lowercase episode["ProviderIds"] keys - episode["ProviderIds"] = {k.lower(): v for k, v in episode["ProviderIds"].items()} - user_watched[user_name][library_title][show_guids][season["Name"]].append(episode["ProviderIds"]) + # Lowercase episode["ProviderIds"] keys + episode["ProviderIds"] = {k.lower(): v for k, v in episode["ProviderIds"].items()} + user_watched[user_name][library_title][show_guids][season["Name"]].append(episode["ProviderIds"]) - return user_watched + return user_watched + except Exception as e: + logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2) + raise Exception(e) def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): - users_watched = {} - args = [] + try: + users_watched = {} + args = [] - for user_name, user_id in users.items(): - # Get all libraries - user_name = user_name.lower() + for user_name, user_id in users.items(): + # Get all libraries + user_name = user_name.lower() - libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] + libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] - for library in libraries: - library_title = library["Name"] - library_id = library["Id"] - watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&limit=1", "get") + for library in libraries: + library_title = library["Name"] + library_id = library["Id"] + watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&limit=1", "get") - if len(watched["Items"]) == 0: - logger(f"Jellyfin: No watched items found in library {library_title}", 1) - continue - else: - library_type = watched["Items"][0]["Type"] + if len(watched["Items"]) == 0: + logger(f"Jellyfin: No watched items found in library {library_title}", 1) + continue + else: + library_type = watched["Items"][0]["Type"] - skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) + skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) - if skip_reason: - logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) - continue + if skip_reason: + logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) + continue - args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) + args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) - for user_watched in future_thread_executor(args): - for user, user_watched_temp in user_watched.items(): - if user not in users_watched: - users_watched[user] = {} - users_watched[user].update(user_watched_temp) - - return users_watched + for user_watched in future_thread_executor(args): + for user, user_watched_temp in user_watched.items(): + if user not in users_watched: + users_watched[user] = {} + users_watched[user].update(user_watched_temp) + return users_watched + except Exception as e: + logger(f"Jellyfin: Failed to get watched, Error: {e}", 2) + raise Exception(e) def update_user_watched(self, user, user_id, library, library_id, videos, dryrun): - logger(f"Jellyfin: Updating watched for {user} in library {library}", 1) - library_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&limit=1", "get") - library_type = library_search["Items"][0]["Type"] + try: + logger(f"Jellyfin: Updating watched for {user} in library {library}", 1) + library_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&limit=1", "get") + library_type = library_search["Items"][0]["Type"] - # Movies - if library_type == "Movie": - _, _, videos_movies_ids = generate_library_guids_dict(videos, 2) + # Movies + if library_type == "Movie": + _, _, videos_movies_ids = generate_library_guids_dict(videos, 2) - jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds", "get") - for jellyfin_video in jellyfin_search["Items"]: - if str_to_bool(jellyfin_video["UserData"]["Played"]) == False: - jellyfin_video_id = jellyfin_video["Id"] + jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds", "get") + for jellyfin_video in jellyfin_search["Items"]: + if str_to_bool(jellyfin_video["UserData"]["Played"]) == False: + jellyfin_video_id = jellyfin_video["Id"] - for movie_provider_source, movie_provider_id in jellyfin_video["ProviderIds"].items(): - if movie_provider_source.lower() in videos_movies_ids: - if movie_provider_id.lower() in videos_movies_ids[movie_provider_source.lower()]: - msg = f"{jellyfin_video['Name']} as watched for {user} in {library} for Jellyfin" - if not dryrun: - logger(f"Marking {msg}", 0) - self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post") - else: - logger(f"Dryrun {msg}", 0) - break + for movie_provider_source, movie_provider_id in jellyfin_video["ProviderIds"].items(): + if movie_provider_source.lower() in videos_movies_ids: + if movie_provider_id.lower() in videos_movies_ids[movie_provider_source.lower()]: + msg = f"{jellyfin_video['Name']} as watched for {user} in {library} for Jellyfin" + if not dryrun: + logger(f"Marking {msg}", 0) + self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post") + else: + logger(f"Dryrun {msg}", 0) + break - # TV Shows - if library_type == "Episode": - videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3) + # TV Shows + if library_type == "Episode": + videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3) - jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds", "get") - jellyfin_shows = [x for x in jellyfin_search["Items"]] + jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds", "get") + jellyfin_shows = [x for x in jellyfin_search["Items"]] - for jellyfin_show in jellyfin_shows: - show_found = False - for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items(): - if show_provider_source.lower() in videos_shows_ids: - if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]: - show_found = True - jellyfin_show_id = jellyfin_show["Id"] - jellyfin_episodes = self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds", "get") - for jellyfin_episode in jellyfin_episodes["Items"]: - if str_to_bool(jellyfin_episode["UserData"]["Played"]) == False: - jellyfin_episode_id = jellyfin_episode["Id"] + for jellyfin_show in jellyfin_shows: + show_found = False + for show_provider_source, show_provider_id in jellyfin_show["ProviderIds"].items(): + if show_provider_source.lower() in videos_shows_ids: + if show_provider_id.lower() in videos_shows_ids[show_provider_source.lower()]: + show_found = True + jellyfin_show_id = jellyfin_show["Id"] + jellyfin_episodes = self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds", "get") + for jellyfin_episode in jellyfin_episodes["Items"]: + if str_to_bool(jellyfin_episode["UserData"]["Played"]) == False: + jellyfin_episode_id = jellyfin_episode["Id"] - for episode_provider_source, episode_provider_id in jellyfin_episode["ProviderIds"].items(): - if episode_provider_source.lower() in videos_episode_ids: - if episode_provider_id.lower() in videos_episode_ids[episode_provider_source.lower()]: - msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']} as watched for {user} in {library} for Jellyfin" - if not dryrun: - logger(f"Marked {msg}", 0) - self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post") - else: - logger(f"Dryrun {msg}", 0) - break + for episode_provider_source, episode_provider_id in jellyfin_episode["ProviderIds"].items(): + if episode_provider_source.lower() in videos_episode_ids: + if episode_provider_id.lower() in videos_episode_ids[episode_provider_source.lower()]: + msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']} as watched for {user} in {library} for Jellyfin" + if not dryrun: + logger(f"Marked {msg}", 0) + self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post") + else: + logger(f"Dryrun {msg}", 0) + break - if show_found: - break + if show_found: + break + + except Exception as e: + logger(f"Jellyfin: Error updating watched for {user} in library {library}", 2) + raise Exception(e) def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): - args = [] - for user, libraries in watched_list.items(): - user_other = None - if user_mapping: - if user in user_mapping.keys(): - user_other = user_mapping[user] - elif user in user_mapping.values(): - user_other = search_mapping(user_mapping, user) + try: + args = [] + for user, libraries in watched_list.items(): + user_other = None + if user_mapping: + if user in user_mapping.keys(): + user_other = user_mapping[user] + elif user in user_mapping.values(): + user_other = search_mapping(user_mapping, user) - user_id = None - for key in self.users.keys(): - if user.lower() == key.lower(): - user_id = self.users[key] - break - elif user_other and user_other.lower() == key.lower(): - user_id = self.users[key] - break + user_id = None + for key in self.users.keys(): + if user.lower() == key.lower(): + user_id = self.users[key] + break + elif user_other and user_other.lower() == key.lower(): + user_id = self.users[key] + break - if not user_id: - logger(f"{user} {user_other} not found in Jellyfin", 2) - continue + if not user_id: + logger(f"{user} {user_other} not found in Jellyfin", 2) + continue - jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] + jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] - for library, videos in libraries.items(): - library_other = None - if library_mapping: - if library in library_mapping.keys(): - library_other = library_mapping[library] - elif library in library_mapping.values(): - library_other = search_mapping(library_mapping, library) + for library, videos in libraries.items(): + library_other = None + if library_mapping: + if library in library_mapping.keys(): + library_other = library_mapping[library] + elif library in library_mapping.values(): + library_other = search_mapping(library_mapping, library) - if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]: - if library_other and library_other.lower() in [x["Name"].lower() for x in jellyfin_libraries]: - logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1) - library = library_other - else: - logger(f"Library {library} {library_other} not found in Plex library list", 2) - continue + if library.lower() not in [x["Name"].lower() for x in jellyfin_libraries]: + if library_other: + if library_other.lower() in [x["Name"].lower() for x in jellyfin_libraries]: + logger(f"Jellyfin: Library {library} not found, but {library_other} found, using {library_other}", 1) + library = library_other + else: + logger(f"Jellyfin: Library {library} or {library_other} not found in library list", 2) + continue + else: + logger(f"Jellyfin: Library {library} not found in library list", 2) + continue + library_id = None + for jellyfin_library in jellyfin_libraries: + if jellyfin_library["Name"] == library: + library_id = jellyfin_library["Id"] + continue - library_id = None - for jellyfin_library in jellyfin_libraries: - if jellyfin_library["Name"] == library: - library_id = jellyfin_library["Id"] - continue + if library_id: + args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) - if library_id: - args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) - - future_thread_executor(args) + future_thread_executor(args) + except Exception as e: + logger(f"Jellyfin: Error updating watched", 2) + raise Exception(e) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..6528b08 --- /dev/null +++ b/src/main.py @@ -0,0 +1,400 @@ +import copy, os, traceback, json +from dotenv import load_dotenv +from time import sleep + +from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict, future_thread_executor +from src.plex import Plex +from src.jellyfin import Jellyfin + + + +load_dotenv(override=True) + +def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_mapping=None): + modified_watched_list_1 = copy.deepcopy(watched_list_1) + + # remove entries from plex_watched that are in jellyfin_watched + for user_1 in watched_list_1: + user_other = None + if user_mapping: + user_other = search_mapping(user_mapping, user_1) + if user_1 in modified_watched_list_1: + if user_1 in watched_list_2: + user_2 = user_1 + elif user_other in watched_list_2: + user_2 = user_other + else: + logger(f"User {user_1} and {user_other} not found in watched list 2", 1) + continue + + for library_1 in watched_list_1[user_1]: + library_other = None + if library_mapping: + library_other = search_mapping(library_mapping, library_1) + if library_1 in modified_watched_list_1[user_1]: + if library_1 in watched_list_2[user_2]: + library_2 = library_1 + elif library_other in watched_list_2[user_2]: + library_2 = library_other + else: + logger(f"library {library_1} and {library_other} not found in watched list 2", 1) + continue + + # Movies + if isinstance(watched_list_1[user_1][library_1], list): + for item in watched_list_1[user_1][library_1]: + for watch_list_1_key, watch_list_1_value in item.items(): + for watch_list_2_item in watched_list_2[user_2][library_2]: + for watch_list_2_item_key, watch_list_2_item_value in watch_list_2_item.items(): + if watch_list_1_key == watch_list_2_item_key and watch_list_1_value == watch_list_2_item_value: + if item in modified_watched_list_1[user_1][library_1]: + logger(f"Removing {item} from {library_1}", 3) + modified_watched_list_1[user_1][library_1].remove(item) + + + # TV Shows + elif isinstance(watched_list_1[user_1][library_1], dict): + # Generate full list of provider ids for episodes in watch_list_2 to easily compare if they exist in watch_list_1 + _, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 1) + + for show_key_1 in watched_list_1[user_1][library_1].keys(): + show_key_dict = dict(show_key_1) + for season in watched_list_1[user_1][library_1][show_key_1]: + for episode in watched_list_1[user_1][library_1][show_key_1][season]: + for episode_key, episode_item in episode.items(): + # If episode_key and episode_item are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1 + if episode_key in episode_watched_list_2_keys_dict.keys(): + if episode_item in episode_watched_list_2_keys_dict[episode_key]: + if episode in modified_watched_list_1[user_1][library_1][show_key_1][season]: + logger(f"Removing {show_key_dict['title']} {episode} from {library_1}", 3) + modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode) + + # Remove empty seasons + if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0: + if season in modified_watched_list_1[user_1][library_1][show_key_1]: + logger(f"Removing {season} from {library_1} because it is empty", 3) + del modified_watched_list_1[user_1][library_1][show_key_1][season] + + # If the show is empty, remove the show + if len(modified_watched_list_1[user_1][library_1][show_key_1]) == 0: + if show_key_1 in modified_watched_list_1[user_1][library_1]: + logger(f"Removing {show_key_dict['title']} from {library_1} because it is empty", 1) + del modified_watched_list_1[user_1][library_1][show_key_1] + + for user_1 in watched_list_1: + for library_1 in watched_list_1[user_1]: + if library_1 in modified_watched_list_1[user_1]: + # If library is empty then remove it + if len(modified_watched_list_1[user_1][library_1]) == 0: + logger(f"Removing {library_1} from {user_1} because it is empty", 1) + del modified_watched_list_1[user_1][library_1] + + if user_1 in modified_watched_list_1: + # If user is empty delete user + if len(modified_watched_list_1[user_1]) == 0: + logger(f"Removing {user_1} from watched list 1 because it is empty", 1) + del modified_watched_list_1[user_1] + + return modified_watched_list_1 + +def setup_black_white_lists(library_mapping=None): + blacklist_library = os.getenv("BLACKLIST_LIBRARY") + if blacklist_library: + if len(blacklist_library) > 0: + blacklist_library = blacklist_library.split(",") + blacklist_library = [x.strip() for x in blacklist_library] + if library_mapping: + temp_library = [] + for library in blacklist_library: + library_other = search_mapping(library_mapping, library) + if library_other: + temp_library.append(library_other) + + blacklist_library = blacklist_library + temp_library + else: + blacklist_library = [] + + logger(f"Blacklist Library: {blacklist_library}", 1) + + whitelist_library = os.getenv("WHITELIST_LIBRARY") + if whitelist_library: + if len(whitelist_library) > 0: + whitelist_library = whitelist_library.split(",") + whitelist_library = [x.strip() for x in whitelist_library] + if library_mapping: + temp_library = [] + for library in whitelist_library: + library_other = search_mapping(library_mapping, library) + if library_other: + temp_library.append(library_other) + + whitelist_library = whitelist_library + temp_library + else: + whitelist_library = [] + logger(f"Whitelist Library: {whitelist_library}", 1) + + blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE") + if blacklist_library_type: + if len(blacklist_library_type) > 0: + blacklist_library_type = blacklist_library_type.split(",") + blacklist_library_type = [x.lower().strip() for x in blacklist_library_type] + else: + blacklist_library_type = [] + logger(f"Blacklist Library Type: {blacklist_library_type}", 1) + + whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE") + if whitelist_library_type: + if len(whitelist_library_type) > 0: + whitelist_library_type = whitelist_library_type.split(",") + whitelist_library_type = [x.lower().strip() for x in whitelist_library_type] + else: + whitelist_library_type = [] + logger(f"Whitelist Library Type: {whitelist_library_type}", 1) + + blacklist_users = os.getenv("BLACKLIST_USERS") + if blacklist_users: + if len(blacklist_users) > 0: + blacklist_users = blacklist_users.split(",") + blacklist_users = [x.lower().strip() for x in blacklist_users] + else: + blacklist_users = [] + logger(f"Blacklist Users: {blacklist_users}", 1) + + whitelist_users = os.getenv("WHITELIST_USERS") + if whitelist_users: + if len(whitelist_users) > 0: + whitelist_users = whitelist_users.split(",") + whitelist_users = [x.lower().strip() for x in whitelist_users] + else: + whitelist_users = [] + else: + whitelist_users = [] + logger(f"Whitelist Users: {whitelist_users}", 1) + + return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users + +def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping=None): + + # generate list of users from server 1 and server 2 + server_1_type = server_1[0] + server_1_connection = server_1[1] + server_2_type = server_2[0] + server_2_connection = server_2[1] + + server_1_users = [] + if server_1_type == "plex": + server_1_users = [ x.title.lower() for x in server_1_connection.users ] + elif server_1_type == "jellyfin": + server_1_users = [ key.lower() for key in server_1_connection.users.keys() ] + + server_2_users = [] + if server_2_type == "plex": + server_2_users = [ x.title.lower() for x in server_2_connection.users ] + elif server_2_type == "jellyfin": + server_2_users = [ key.lower() for key in server_2_connection.users.keys() ] + + + # combined list of overlapping users from plex and jellyfin + users = {} + + for server_1_user in server_1_users: + if user_mapping: + jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user) + if jellyfin_plex_mapped_user: + users[server_1_user] = jellyfin_plex_mapped_user + continue + + if server_1_user in server_2_users: + users[server_1_user] = server_1_user + + for server_2_user in server_2_users: + if user_mapping: + plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user) + if plex_jellyfin_mapped_user: + users[plex_jellyfin_mapped_user] = server_2_user + continue + + if server_2_user in server_1_users: + users[server_2_user] = server_2_user + + logger(f"User list that exist on both servers {users}", 1) + + users_filtered = {} + for user in users: + # whitelist_user is not empty and user lowercase is not in whitelist lowercase + if len(whitelist_users) > 0: + if user not in whitelist_users and users[user] not in whitelist_users: + logger(f"{user} or {users[user]} is not in whitelist", 1) + continue + + if user not in blacklist_users and users[user] not in blacklist_users: + users_filtered[user] = users[user] + + logger(f"Filtered user list {users_filtered}", 1) + + if server_1_type == "plex": + output_server_1_users = [] + for plex_user in server_1_connection.users: + if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): + output_server_1_users.append(plex_user) + elif server_1_type == "jellyfin": + output_server_1_users = {} + for jellyfin_user, jellyfin_id in server_1_connection.users.items(): + if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): + output_server_1_users[jellyfin_user] = jellyfin_id + + if server_2_type == "plex": + output_server_2_users = [] + for plex_user in server_2_connection.users: + if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): + output_server_2_users.append(plex_user) + elif server_2_type == "jellyfin": + output_server_2_users = {} + for jellyfin_user, jellyfin_id in server_2_connection.users.items(): + if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): + output_server_2_users[jellyfin_user] = jellyfin_id + + if len(output_server_1_users) == 0: + raise Exception(f"No users found for server 1, users found {users} filtered users {users_filtered}") + + if len(output_server_2_users) == 0: + raise Exception(f"No users found for server 2, users found {users} filtered users {users_filtered}") + + logger(f"Server 1 users: {output_server_1_users}", 1) + logger(f"Server 2 users: {output_server_2_users}", 1) + + return output_server_1_users, output_server_2_users + +def generate_server_connections(): + servers = [] + + plex_baseurl = os.getenv("PLEX_BASEURL", None) + plex_token = os.getenv("PLEX_TOKEN", None) + plex_username = os.getenv("PLEX_USERNAME", None) + plex_password = os.getenv("PLEX_PASSWORD", None) + plex_servername = os.getenv("PLEX_SERVERNAME", None) + + if plex_baseurl and plex_token: + plex_baseurl = plex_baseurl.split(",") + plex_token = plex_token.split(",") + + if len(plex_baseurl) != len(plex_token): + raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries") + + for i, url in enumerate(plex_baseurl): + servers.append(("plex", Plex(baseurl=url.strip(), token=plex_token[i].strip(), username=None, password=None, servername=None))) + + if plex_username and plex_password and plex_servername: + plex_username = plex_username.split(",") + plex_password = plex_password.split(",") + plex_servername = plex_servername.split(",") + + if len(plex_username) != len(plex_password) or len(plex_username) != len(plex_servername): + raise Exception("PLEX_USERNAME, PLEX_PASSWORD and PLEX_SERVERNAME must have the same number of entries") + + for i, username in enumerate(plex_username): + servers.append(("plex", Plex(baseurl=None, token=None, username=username.strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip()))) + + jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None) + jellyfin_token = os.getenv("JELLYFIN_TOKEN", None) + + if jellyfin_baseurl and jellyfin_token: + jellyfin_baseurl = jellyfin_baseurl.split(",") + jellyfin_token = jellyfin_token.split(",") + + if len(jellyfin_baseurl) != len(jellyfin_token): + raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries") + + for i, baseurl in enumerate(jellyfin_baseurl): + servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()))) + + return servers + +def main_loop(): + logfile = os.getenv("LOGFILE","log.log") + # Delete logfile if it exists + if os.path.exists(logfile): + os.remove(logfile) + + dryrun = str_to_bool(os.getenv("DRYRUN", "False")) + logger(f"Dryrun: {dryrun}", 1) + + user_mapping = os.getenv("USER_MAPPING") + if user_mapping: + user_mapping = json.loads(user_mapping.lower()) + logger(f"User Mapping: {user_mapping}", 1) + + library_mapping = os.getenv("LIBRARY_MAPPING") + if library_mapping: + library_mapping = json.loads(library_mapping) + logger(f"Library Mapping: {library_mapping}", 1) + + # Create (black/white)lists + blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(library_mapping) + + # Create server connections + servers = generate_server_connections() + + for server_1 in servers: + # If server is the final server in the list, then we are done with the loop + if server_1 == servers[-1]: + break + + # Start server_2 at the next server in the list + for server_2 in servers[servers.index(server_1) + 1:]: + + server_1_connection = server_1[1] + server_2_connection = server_2[1] + + # Create users list + server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) + + args = [[server_1_connection.get_watched, server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping] + , [server_2_connection.get_watched, server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping]] + + results = future_thread_executor(args) + server_1_watched = results[0] + server_2_watched = results[1] + logger(f"Server 1 watched: {server_1_watched}", 3) + logger(f"Server 2 watched: {server_2_watched}", 3) + + # clone watched so it isnt modified in the cleanup function so all duplicates are actually removed + server_1_watched_filtered = copy.deepcopy(server_1_watched) + server_2_watched_filtered = copy.deepcopy(server_2_watched) + + logger("Cleaning Server 1 Watched", 1) + server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping) + + logger("Cleaning Server 2 Watched", 1) + server_2_watched_filtered = cleanup_watched(server_2_watched, server_1_watched, user_mapping, library_mapping) + + logger(f"server 1 watched that needs to be synced to server 2:\n{server_1_watched_filtered}", 1) + logger(f"server 2 watched that needs to be synced to server 1:\n{server_2_watched_filtered}", 1) + + args= [[server_1_connection.update_watched, server_2_watched_filtered, user_mapping, library_mapping, dryrun] + , [server_2_connection.update_watched, server_1_watched_filtered, user_mapping, library_mapping, dryrun]] + + future_thread_executor(args) + +def main(): + sleep_duration = float(os.getenv("SLEEP_DURATION", "3600")) + + while(True): + try: + main_loop() + logger(f"Looping in {sleep_duration}") + sleep(sleep_duration) + except Exception as error: + if isinstance(error, list): + for message in error: + logger(message, log_type=2) + else: + logger(error, log_type=2) + + + logger(traceback.format_exc(), 2) + logger(f"Retrying in {sleep_duration}", log_type=0) + + except KeyboardInterrupt: + logger("Exiting", log_type=0) + os._exit(0) diff --git a/src/plex.py b/src/plex.py index b10f3d9..a1c6874 100644 --- a/src/plex.py +++ b/src/plex.py @@ -37,209 +37,235 @@ class Plex: logger(f"Plex: Failed to login, {msg}, Error: {e}", 2) else: logger(f"Plex: Failed to login, Error: {e}", 2) - return None + raise Exception(e) def get_users(self): - users = self.plex.myPlexAccount().users() + try: + users = self.plex.myPlexAccount().users() - # append self to users - users.append(self.plex.myPlexAccount()) + # append self to users + users.append(self.plex.myPlexAccount()) - return users + return users + except Exception as e: + logger(f"Plex: Failed to get users, Error: {e}", 2) + raise Exception(e) def get_user_watched(self, user, user_plex, library): - user_watched = {} - user_watched[user.title] = {} + try: + user_name = user.title.lower() + user_watched = {} + user_watched[user_name] = {} - logger(f"Plex: Generating watched for {user.title} in library {library.title}", 0) + logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0) - if library.type == "movie": - user_watched[user.title][library.title] = [] + if library.type == "movie": + user_watched[user_name][library.title] = [] - library_videos = user_plex.library.section(library.title) - for video in library_videos.search(unmatched=False, unwatched=False): - guids = {} - for guid in video.guids: - guid_source = re.search(r'(.*)://', guid.id).group(1).lower() - guid_id = re.search(r'://(.*)', guid.id).group(1) - guids[guid_source] = guid_id - user_watched[user.title][library.title].append(guids) + library_videos = user_plex.library.section(library.title) + for video in library_videos.search(unmatched=False, unwatched=False): + guids = {} + for guid in video.guids: + guid_source = re.search(r'(.*)://', guid.id).group(1).lower() + guid_id = re.search(r'://(.*)', guid.id).group(1) + guids[guid_source] = guid_id + user_watched[user_name][library.title].append(guids) - elif library.type == "show": - user_watched[user.title][library.title] = {} + elif library.type == "show": + user_watched[user_name][library.title] = {} - library_videos = user_plex.library.section(library.title) - for show in library_videos.search(unmatched=False, unwatched=False): - show_guids = {} - for show_guid in show.guids: - show_guids["title"] = show.title - # Extract after :// from guid.id - show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower() - show_guid_id = re.search(r'://(.*)', show_guid.id).group(1) - show_guids[show_guid_source] = show_guid_id - show_guids = frozenset(show_guids.items()) + library_videos = user_plex.library.section(library.title) + for show in library_videos.search(unmatched=False, unwatched=False): + show_guids = {} + for show_guid in show.guids: + show_guids["title"] = show.title + # Extract after :// from guid.id + show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower() + show_guid_id = re.search(r'://(.*)', show_guid.id).group(1) + show_guids[show_guid_source] = show_guid_id + show_guids = frozenset(show_guids.items()) - for season in show.seasons(): - episode_guids = [] - for episode in season.episodes(): - if episode.viewCount > 0: - episode_guids_temp = {} - for guid in episode.guids: - # Extract after :// from guid.id - guid_source = re.search(r'(.*)://', guid.id).group(1).lower() - guid_id = re.search(r'://(.*)', guid.id).group(1) - episode_guids_temp[guid_source] = guid_id + for season in show.seasons(): + episode_guids = [] + for episode in season.episodes(): + if episode.viewCount > 0: + episode_guids_temp = {} + for guid in episode.guids: + # Extract after :// from guid.id + guid_source = re.search(r'(.*)://', guid.id).group(1).lower() + guid_id = re.search(r'://(.*)', guid.id).group(1) + episode_guids_temp[guid_source] = guid_id - episode_guids.append(episode_guids_temp) + episode_guids.append(episode_guids_temp) - if episode_guids: - # append show, season, episode - if show_guids not in user_watched[user.title][library.title]: - user_watched[user.title][library.title][show_guids] = {} - if season.title not in user_watched[user.title][library.title][show_guids]: - user_watched[user.title][library.title][show_guids][season.title] = {} - user_watched[user.title][library.title][show_guids][season.title] = episode_guids + if episode_guids: + # append show, season, episode + if show_guids not in user_watched[user_name][library.title]: + user_watched[user_name][library.title][show_guids] = {} + if season.title not in user_watched[user_name][library.title][show_guids]: + user_watched[user_name][library.title][show_guids][season.title] = {} + user_watched[user_name][library.title][show_guids][season.title] = episode_guids - return user_watched + return user_watched + except Exception as e: + logger(f"Plex: Failed to get watched for {user_name} in library {library.title}, Error: {e}", 2) + raise Exception(e) + def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): - # Get all libraries - users_watched = {} - args = [] + try: + # Get all libraries + users_watched = {} + args = [] - for user in users: - if self.admin_user == user: - user_plex = self.plex - else: - user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) + for user in users: + if self.admin_user == user: + user_plex = self.plex + else: + user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) - libraries = user_plex.library.sections() + libraries = user_plex.library.sections() - for library in libraries: - library_title = library.title - library_type = library.type + for library in libraries: + library_title = library.title + library_type = library.type - skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) + skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) - if skip_reason: - logger(f"Plex: Skipping library {library_title} {skip_reason}", 1) - continue + if skip_reason: + logger(f"Plex: Skipping library {library_title} {skip_reason}", 1) + continue - args.append([self.get_user_watched, user, user_plex, library]) + args.append([self.get_user_watched, user, user_plex, library]) - for user_watched in future_thread_executor(args): - for user, user_watched_temp in user_watched.items(): - if user not in users_watched: - users_watched[user] = {} - users_watched[user].update(user_watched_temp) + for user_watched in future_thread_executor(args): + for user, user_watched_temp in user_watched.items(): + if user not in users_watched: + users_watched[user] = {} + users_watched[user].update(user_watched_temp) + + return users_watched + except Exception as e: + logger(f"Plex: Failed to get watched, Error: {e}", 2) + raise Exception(e) - return users_watched def update_user_watched (self, user, user_plex, library, videos, dryrun): - logger(f"Plex: Updating watched for {user.title} in library {library}", 1) - library_videos = user_plex.library.section(library) + try: + logger(f"Plex: Updating watched for {user.title} in library {library}", 1) + library_videos = user_plex.library.section(library) - if library_videos.type == "movie": - _, _, videos_movies_ids = generate_library_guids_dict(videos, 2) - for movies_search in library_videos.search(unmatched=False, unwatched=True): - for movie_guid in movies_search.guids: - movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower() - movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1) + if library_videos.type == "movie": + _, _, videos_movies_ids = generate_library_guids_dict(videos, 2) + for movies_search in library_videos.search(unmatched=False, unwatched=True): + for movie_guid in movies_search.guids: + movie_guid_source = re.search(r'(.*)://', movie_guid.id).group(1).lower() + movie_guid_id = re.search(r'://(.*)', movie_guid.id).group(1) - # If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list - if movie_guid_source in videos_movies_ids.keys(): - if movie_guid_id in videos_movies_ids[movie_guid_source]: - if movies_search.viewCount == 0: - msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex" - if not dryrun: - logger(f"Marked {msg}", 0) - movies_search.markWatched() - else: - logger(f"Dryrun {msg}", 0) + # If movie provider source and movie provider id are in videos_movie_ids exactly, then the movie is in the list + if movie_guid_source in videos_movies_ids.keys(): + if movie_guid_id in videos_movies_ids[movie_guid_source]: + if movies_search.viewCount == 0: + msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex" + if not dryrun: + logger(f"Marked {msg}", 0) + movies_search.markWatched() + else: + logger(f"Dryrun {msg}", 0) + break + + + elif library_videos.type == "show": + videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3) + + for show_search in library_videos.search(unmatched=False, unwatched=True): + show_found = False + for show_guid in show_search.guids: + show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower() + show_guid_id = re.search(r'://(.*)', show_guid.id).group(1) + + # If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list + if show_guid_source in videos_shows_ids.keys(): + if show_guid_id in videos_shows_ids[show_guid_source]: + show_found = True + for episode_search in show_search.episodes(): + for episode_guid in episode_search.guids: + episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower() + episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1) + + # If episode provider source and episode provider id are in videos_episode_ids exactly, then the episode is in the list + if episode_guid_source in videos_episode_ids.keys(): + if episode_guid_id in videos_episode_ids[episode_guid_source]: + if episode_search.viewCount == 0: + msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex" + if not dryrun: + logger(f"Marked {msg}", 0) + episode_search.markWatched() + else: + logger(f"Dryrun {msg}", 0) + break + + if show_found: break - - - elif library_videos.type == "show": - videos_shows_ids, videos_episode_ids, _ = generate_library_guids_dict(videos, 3) - - for show_search in library_videos.search(unmatched=False, unwatched=True): - show_found = False - for show_guid in show_search.guids: - show_guid_source = re.search(r'(.*)://', show_guid.id).group(1).lower() - show_guid_id = re.search(r'://(.*)', show_guid.id).group(1) - - # If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list - if show_guid_source in videos_shows_ids.keys(): - if show_guid_id in videos_shows_ids[show_guid_source]: - show_found = True - for episode_search in show_search.episodes(): - for episode_guid in episode_search.guids: - episode_guid_source = re.search(r'(.*)://', episode_guid.id).group(1).lower() - episode_guid_id = re.search(r'://(.*)', episode_guid.id).group(1) - - # If episode provider source and episode provider id are in videos_episode_ids exactly, then the episode is in the list - if episode_guid_source in videos_episode_ids.keys(): - if episode_guid_id in videos_episode_ids[episode_guid_source]: - if episode_search.viewCount == 0: - msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex" - if not dryrun: - logger(f"Marked {msg}", 0) - episode_search.markWatched() - else: - logger(f"Dryrun {msg}", 0) - break - - if show_found: - break - + except Exception as e: + logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2) + raise Exception(e) def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): - args = [] + try: + args = [] - for user, libraries in watched_list.items(): - user_other = None - # If type of user is dict - if user_mapping: - if user in user_mapping.keys(): - user_other = user_mapping[user] - elif user in user_mapping.values(): - user_other = search_mapping(user_mapping, user) + for user, libraries in watched_list.items(): + user_other = None + # If type of user is dict + if user_mapping: + if user in user_mapping.keys(): + user_other = user_mapping[user] + elif user in user_mapping.values(): + user_other = search_mapping(user_mapping, user) - for index, value in enumerate(self.users): - if user.lower() == value.title.lower(): - user = self.users[index] - break - elif user_other and user_other.lower() == value.title.lower(): - user = self.users[index] - break + for index, value in enumerate(self.users): + if user.lower() == value.title.lower(): + user = self.users[index] + break + elif user_other and user_other.lower() == value.title.lower(): + user = self.users[index] + break - if self.admin_user == user: - user_plex = self.plex - else: - user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) + if self.admin_user == user: + user_plex = self.plex + else: + user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) - for library, videos in libraries.items(): - library_other = None - if library_mapping: - if library in library_mapping.keys(): - library_other = library_mapping[library] - elif library in library_mapping.values(): - library_other = search_mapping(library_mapping, library) + for library, videos in libraries.items(): + library_other = None + if library_mapping: + if library in library_mapping.keys(): + library_other = library_mapping[library] + elif library in library_mapping.values(): + library_other = search_mapping(library_mapping, library) - # if library in plex library list - library_list = user_plex.library.sections() - if library.lower() not in [x.title.lower() for x in library_list]: - if library_other and library_other.lower() in [x.title.lower() for x in library_list]: - logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1) - library = library_other - else: - logger(f"Library {library} {library_other} not found in Plex library list", 2) - continue + # if library in plex library list + library_list = user_plex.library.sections() + if library.lower() not in [x.title.lower() for x in library_list]: + if library_other: + if library_other.lower() in [x.title.lower() for x in library_list]: + logger(f"Plex: Library {library} not found, but {library_other} found, using {library_other}", 1) + library = library_other + else: + logger(f"Plex: Library {library} or {library_other} not found in library list", 2) + continue + else: + logger(f"Plex: Library {library} not found in library list", 2) + continue - args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) + args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) - future_thread_executor(args) + future_thread_executor(args) + except Exception as e: + logger(f"Plex: Failed to update watched, Error: {e}", 2) + raise Exception(e)