From 7695994ec2a2dc41c7b9262f54f759841f612315 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Jun 2022 22:30:41 -0600 Subject: [PATCH 01/12] Support x many servers of any combination --- .env.sample | 6 +- README.md | 6 +- main.py | 185 +++++++++++++++++++++++++++++++++++------------ src/functions.py | 6 +- src/jellyfin.py | 43 +++++------ src/plex.py | 58 ++++++--------- 6 files changed, 191 insertions(+), 113 deletions(-) diff --git a/.env.sample b/.env.sample index 6464b80..cce23ea 100644 --- a/.env.sample +++ b/.env.sample @@ -2,8 +2,8 @@ DRYRUN = "True" ## Additional logging information DEBUG = "True" -## Debugging level, INFO is default, DEBUG is more verbose -DEBUG_LEVEL = "INFO" +## Debugging level, "info" is default, "debug" is more verbose +DEBUG_LEVEL = "info" ## How often to run the script in seconds SLEEP_DURATION = "3600" ## Log file where all output will be written to @@ -16,6 +16,7 @@ LOGFILE = "log.log" ## Recommended to use token as it is faster to connect as it is direct to the server instead of going through the plex servers ## URL of the plex server, use hostname or IP address if the hostname is not resolving correctly +## Comma seperated list for multiple servers PLEX_BASEURL = "http://localhost:32400" ## Plex token https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/ PLEX_TOKEN = "SuperSecretToken" @@ -26,6 +27,7 @@ PLEX_TOKEN = "SuperSecretToken" ## Jellyfin server URL, use hostname or IP address if the hostname is not resolving correctly +## Comma seperated list for multiple servers JELLYFIN_BASEURL = "http://localhost:8096" ## Jellyfin api token, created manually by logging in to the jellyfin server admin dashboard and creating an api key JELLYFIN_TOKEN = "SuperSecretToken" diff --git a/README.md b/README.md index 36556d5..0da25f4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ Sync watched between jellyfin and plex ## Description -Keep in sync all your users watched history between jellyfin and plex locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases. +Keep in sync all your users watched history between jellyfin and plex servers locally. This uses the imdb ids and any other matching id to find the correct episode/movie between the two. This is not perfect but it works for most cases. You can use this for as many servers as you want by enterying multiple options in the .env plex/jellyfin section seperated by commas. + +## Configuration + + ## Installation diff --git a/main.py b/main.py index 4b02093..2f28d36 100644 --- a/main.py +++ b/main.py @@ -168,34 +168,49 @@ def setup_black_white_lists(library_mapping=None): return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users -def setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping=None): +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() ] - # generate list of users from plex.users - plex_users = [ x.title.lower() for x in plex.users ] - jellyfin_users = [ key.lower() for key in jellyfin.users.keys() ] # combined list of overlapping users from plex and jellyfin users = {} - for plex_user in plex_users: + for server_1_user in server_1_users: if user_mapping: - jellyfin_plex_mapped_user = search_mapping(user_mapping, plex_user) + jellyfin_plex_mapped_user = search_mapping(user_mapping, server_1_user) if jellyfin_plex_mapped_user: - users[plex_user] = jellyfin_plex_mapped_user + users[server_1_user] = jellyfin_plex_mapped_user continue - if plex_user in jellyfin_users: - users[plex_user] = plex_user + if server_1_user in server_2_users: + users[server_1_user] = server_1_user - for jellyfin_user in jellyfin_users: + for server_2_user in server_2_users: if user_mapping: - plex_jellyfin_mapped_user = search_mapping(user_mapping, jellyfin_user) + plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user) if plex_jellyfin_mapped_user: - users[plex_jellyfin_mapped_user] = jellyfin_user + users[plex_jellyfin_mapped_user] = server_2_user continue - if jellyfin_user in plex_users: - users[jellyfin_user] = jellyfin_user + 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) @@ -212,26 +227,84 @@ def setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_mapping=N logger(f"Filtered user list {users_filtered}", 1) - plex_users = [] - for plex_user in plex.users: - if plex_user.title.lower() in users_filtered.keys() or plex_user.title.lower() in users_filtered.values(): - plex_users.append(plex_user) + 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(",") - jellyfin_users = {} - for jellyfin_user, jellyfin_id in jellyfin.users.items(): - if jellyfin_user.lower() in users_filtered.keys() or jellyfin_user.lower() in users_filtered.values(): - jellyfin_users[jellyfin_user] = jellyfin_id + if len(plex_baseurl) != len(plex_token): + raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries") - if len(plex_users) == 0: - raise Exception(f"No plex users found, users found {users} filtered users {users_filtered}") + for i in range(len(plex_baseurl)): + servers.append(("plex", Plex(baseurl=plex_baseurl[i].strip(), token=plex_token[i].strip(), username=None, password=None, servername=None))) - if len(jellyfin_users) == 0: - raise Exception(f"No jellyfin users found, users found {users} filtered users {users_filtered}") + if plex_username and plex_password and plex_servername: + plex_username = plex_username.split(",") + plex_password = plex_password.split(",") + plex_servername = plex_servername.split(",") - logger(f"plex_users: {plex_users}", 1) - logger(f"jellyfin_users: {jellyfin_users}", 1) + 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") - return plex_users, jellyfin_users + for i in range(len(plex_username)): + servers.append(("plex", Plex(baseurl=None, token=None, username=plex_username[i].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 in range(len(jellyfin_baseurl)): + servers.append(("jellyfin", Jellyfin(baseurl=jellyfin_baseurl[i].strip(), token=jellyfin_token[i].strip()))) + + print(f"Servers: {servers}") + return servers def main(): logfile = os.getenv("LOGFILE","log.log") @@ -252,34 +325,50 @@ def main(): library_mapping = json.loads(library_mapping) logger(f"Library Mapping: {library_mapping}", 1) - plex = Plex() - jellyfin = Jellyfin() - # 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 users list - plex_users, jellyfin_users = setup_users(plex, jellyfin, blacklist_users, whitelist_users, user_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 + servers_2_ = servers[servers.index(server_1) + 1:] + for server_2 in servers[servers.index(server_1) + 1:]: + print(f"server_1: {server_1}, server_2: {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] + + # Create users list + server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) - plex_watched = plex.get_plex_watched(plex_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) - jellyfin_watched = jellyfin.get_jellyfin_watched(jellyfin_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) + server_1_watched = server_1_connection.get_watched(server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) + server_2_watched = server_2_connection.get_watched(server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) - # clone watched so it isnt modified in the cleanup function so all duplicates are actually removed - plex_watched_filtered = copy.deepcopy(plex_watched) - jellyfin_watched_filtered = copy.deepcopy(jellyfin_watched) + # 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 Plex Watched", 1) - plex_watched = cleanup_watched(plex_watched_filtered, jellyfin_watched_filtered, user_mapping, library_mapping) + logger("Cleaning Server 1 Watched", 1) + server_1_watched_filtered = cleanup_watched(server_1_watched, server_2_watched, user_mapping, library_mapping) - logger("Cleaning Jellyfin Watched", 1) - jellyfin_watched = cleanup_watched(jellyfin_watched_filtered, plex_watched_filtered, 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"plex_watched that needs to be synced to jellyfin:\n{plex_watched}", 1) - logger(f"jellyfin_watched that needs to be synced to plex:\n{jellyfin_watched}", 1) + 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) - # Update watched status - plex.update_watched(jellyfin_watched, user_mapping, library_mapping, dryrun) - jellyfin.update_watched(plex_watched, user_mapping, library_mapping, dryrun) + # Update watched status + 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) if __name__ == "__main__": diff --git a/src/functions.py b/src/functions.py index e246ead..0ef75f4 100644 --- a/src/functions.py +++ b/src/functions.py @@ -6,16 +6,16 @@ def logger(message, log_type=0): debug = str_to_bool(os.getenv("DEBUG", "True")) - debug_level = os.getenv("DEBUG_LEVEL", "INFO") + debug_level = os.getenv("DEBUG_LEVEL", "info").lower() output = str(message) if log_type == 0: pass - elif log_type == 1 and (debug or debug_level == "INFO"): + elif log_type == 1 and (debug or debug_level == "info"): output = f"[INFO]: {output}" elif log_type == 2: output = f"[ERROR]: {output}" - elif log_type == 3 and (debug and debug_level == "DEBUG"): + elif log_type == 3 and (debug and debug_level == "debug"): output = f"[DEBUG]: {output}" else: output = None diff --git a/src/jellyfin.py b/src/jellyfin.py index 44332c6..1052714 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -1,16 +1,10 @@ -import requests, os -from dotenv import load_dotenv +import requests from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict -load_dotenv(override=True) - -jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL") -jellyfin_token = os.getenv("JELLYFIN_TOKEN") - class Jellyfin(): - def __init__(self): - self.baseurl = jellyfin_baseurl - self.token = jellyfin_token + def __init__(self, baseurl, token): + self.baseurl = baseurl + self.token = token if not self.baseurl: raise Exception("Jellyfin baseurl not set") @@ -56,7 +50,7 @@ def get_users(self): return users - def get_jellyfin_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): + def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): users_watched = {} for user_name, user_id in users.items(): @@ -131,46 +125,45 @@ def get_jellyfin_watched(self, users, blacklist_library, whitelist_library, blac def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): for user, libraries in watched_list.items(): + user_other = None if user_mapping: - user_other = None - if user in user_mapping.keys(): user_other = user_mapping[user] elif user in user_mapping.values(): user_other = search_mapping(user_mapping, user) - if user_other: - logger(f"Swapping user {user} with {user_other}", 1) - user = user_other - 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} not found in Jellyfin", 2) + logger(f"{user} {user_other} not found in Jellyfin", 2) break jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] for library, videos in libraries.items(): + library_other = None if library_mapping: - library_other = None - 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_other: - logger(f"Swapping library {library} with {library_other}", 1) + + 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 not in [x["Name"] for x in jellyfin_libraries]: - logger(f"{library} not found in Jellyfin", 2) - continue library_id = None for jellyfin_library in jellyfin_libraries: diff --git a/src/plex.py b/src/plex.py index 5207214..0c43e49 100644 --- a/src/plex.py +++ b/src/plex.py @@ -1,31 +1,22 @@ -import re, os -from dotenv import load_dotenv +import re from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict from plexapi.server import PlexServer from plexapi.myplex import MyPlexAccount -load_dotenv(override=True) - -plex_baseurl = os.getenv("PLEX_BASEURL") -plex_token = os.getenv("PLEX_TOKEN") -username = os.getenv("PLEX_USERNAME") -password = os.getenv("PLEX_PASSWORD") -servername = os.getenv("PLEX_SERVERNAME") - # class plex accept base url and token and username and password but default with none class Plex: - def __init__(self): - self.baseurl = plex_baseurl - self.token = plex_token + def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None): + self.baseurl = baseurl + self.token = token self.username = username self.password = password self.servername = servername - self.plex = self.plex_login() + self.plex = self.login() self.admin_user = self.plex.myPlexAccount() - self.users = self.get_plex_users() + self.users = self.get_users() - def plex_login(self): + def login(self): try: if self.baseurl and self.token: # Login via token @@ -47,7 +38,7 @@ def plex_login(self): return None - def get_plex_users(self): + def get_users(self): users = self.plex.myPlexAccount().users() # append self to users @@ -55,7 +46,7 @@ def get_plex_users(self): return users - def get_plex_user_watched(self, user, library): + def get_user_watched(self, user, library): if self.admin_user == user: user_plex = self.plex else: @@ -110,7 +101,7 @@ def get_plex_user_watched(self, user, library): return watched - def get_plex_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): + def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): # Get all libraries libraries = self.plex.library.sections() users_watched = {} @@ -129,7 +120,7 @@ def get_plex_watched(self, users, blacklist_library, whitelist_library, blacklis for user in users: logger(f"Plex: Generating watched for {user.title} in library {library_title}", 0) user_name = user.title.lower() - watched = self.get_plex_user_watched(user, library) + watched = self.get_user_watched(user, library) if watched: if user_name not in users_watched: users_watched[user_name] = {} @@ -141,22 +132,21 @@ def get_plex_watched(self, users, blacklist_library, whitelist_library, blacklis def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): for user, libraries in watched_list.items(): + user_other = None + # If type of user is dict if user_mapping: - user_other = None - if user in user_mapping.keys(): user_other = user_mapping[user] elif user in user_mapping.values(): user_other = search_mapping(user_mapping, user) - if user_other: - logger(f"Swapping user {user} with {user_other}", 1) - user = user_other - 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 @@ -164,23 +154,22 @@ def update_watched(self, watched_list, user_mapping=None, library_mapping=None, user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier)) for library, videos in libraries.items(): + library_other = None if library_mapping: - library_other = None - 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_other: - logger(f"Swapping library {library} with {library_other}", 1) - library = library_other - # 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]: - logger(f"Library {library} not found in Plex library list", 2) - continue + 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 logger(f"Plex: Updating watched for {user.title} in library {library}", 1) library_videos = user_plex.library.section(library) @@ -191,6 +180,7 @@ def update_watched(self, watched_list, user_mapping=None, library_mapping=None, 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]: From beb4e667ae2b66cc9730b04a02745e54a82f571f Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 13 Jun 2022 22:43:56 -0600 Subject: [PATCH 02/12] Cleanup --- main.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index 2f28d36..aa32ca2 100644 --- a/main.py +++ b/main.py @@ -276,8 +276,8 @@ def generate_server_connections(): if len(plex_baseurl) != len(plex_token): raise Exception("PLEX_BASEURL and PLEX_TOKEN must have the same number of entries") - for i in range(len(plex_baseurl)): - servers.append(("plex", Plex(baseurl=plex_baseurl[i].strip(), token=plex_token[i].strip(), username=None, password=None, servername=None))) + 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(",") @@ -287,8 +287,8 @@ def generate_server_connections(): 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 in range(len(plex_username)): - servers.append(("plex", Plex(baseurl=None, token=None, username=plex_username[i].strip(), password=plex_password[i].strip(), servername=plex_servername[i].strip()))) + 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) @@ -300,8 +300,8 @@ def generate_server_connections(): if len(jellyfin_baseurl) != len(jellyfin_token): raise Exception("JELLYFIN_BASEURL and JELLYFIN_TOKEN must have the same number of entries") - for i in range(len(jellyfin_baseurl)): - servers.append(("jellyfin", Jellyfin(baseurl=jellyfin_baseurl[i].strip(), token=jellyfin_token[i].strip()))) + for i, baseurl in enumerate(jellyfin_baseurl): + servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()))) print(f"Servers: {servers}") return servers @@ -337,14 +337,10 @@ def main(): break # Start server_2 at the next server in the list - servers_2_ = servers[servers.index(server_1) + 1:] for server_2 in servers[servers.index(server_1) + 1:]: print(f"server_1: {server_1}, server_2: {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] # Create users list From 0584a85f90be27aa2573dcfc90002cf02ac45403 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Tue, 14 Jun 2022 22:36:44 -0600 Subject: [PATCH 03/12] Add parallel threading --- .gitignore | 1 + main.py | 17 ++-- src/functions.py | 21 ++++- src/jellyfin.py | 236 ++++++++++++++++++++++++++--------------------- src/plex.py | 136 ++++++++++++++------------- 5 files changed, 237 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index 5ac8951..06e876a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +*.prof # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/main.py b/main.py index aa32ca2..ea9f4c2 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,8 @@ from dotenv import load_dotenv from time import sleep -from src.functions import logger, str_to_bool, search_mapping, generate_library_guids_dict + +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 @@ -346,8 +347,12 @@ def main(): # Create users list server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) - server_1_watched = server_1_connection.get_watched(server_1_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) - server_2_watched = server_2_connection.get_watched(server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_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) @@ -362,10 +367,10 @@ def main(): 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) - # Update watched status - 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) + 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_timer = float(os.getenv("SLEEP_TIMER", "3600")) diff --git a/src/functions.py b/src/functions.py index 0ef75f4..8e3d81c 100644 --- a/src/functions.py +++ b/src/functions.py @@ -1,10 +1,12 @@ import os +from concurrent.futures import ThreadPoolExecutor from dotenv import load_dotenv + load_dotenv(override=True) logfile = os.getenv("LOGFILE","log.log") -def logger(message, log_type=0): +def logger(message: str, log_type=0): debug = str_to_bool(os.getenv("DEBUG", "True")) debug_level = os.getenv("DEBUG_LEVEL", "info").lower() @@ -114,3 +116,20 @@ 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): + futures_list = [] + results = [] + + with ThreadPoolExecutor() as executor: + for arg in args: + # * arg unpacks the list into actual arguments + futures_list.append(executor.submit(*arg)) + + for future in futures_list: + try: + result = future.result() + results.append(result) + except Exception as e: + raise Exception(e) + + return results diff --git a/src/jellyfin.py b/src/jellyfin.py index 1052714..9df880d 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -1,10 +1,11 @@ import requests -from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict +from src.functions import logger, search_mapping, str_to_bool, check_skip_logic, generate_library_guids_dict, future_thread_executor class Jellyfin(): def __init__(self, baseurl, token): self.baseurl = baseurl self.token = token + self.session = requests.Session() if not self.baseurl: raise Exception("Jellyfin baseurl not set") @@ -19,8 +20,12 @@ def query(self, query, query_type): try: response = None + headers = { + "Accept": "application/json", + "X-Emby-Token": self.token + } if query_type == "get": - response = requests.get(self.baseurl + query, headers={"accept":"application/json", "X-Emby-Token": self.token}) + response = self.session.get(self.baseurl + query, headers=headers) elif query_type == "post": authorization = ( @@ -30,7 +35,8 @@ def query(self, query, query_type): 'DeviceId="script", ' 'Version="0.0.0"' ) - response = requests.post(self.baseurl + query, headers={"accept":"application/json", "X-Emby-Authorization": authorization, "X-Emby-Token": self.token}) + headers["X-Emby-Authorization"] = authorization + response = self.session.post(self.baseurl + query, headers=headers) return response.json() except Exception as e: @@ -50,9 +56,61 @@ def get_users(self): return users + def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): + user_watched = {} + + logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0) + # Movies + if library_type == "Movie": + 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"]: + if user_name not in user_watched: + user_watched[user_name] = {} + if library_title not in user_watched[user_name]: + user_watched[user_name][library_title] = [] + # 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": + 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 user_name not in user_watched: + user_watched[user_name] = {} + if library_title not in user_watched[user_name]: + user_watched[user_name][library_title] = {} + 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"]) + + return user_watched + + def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): users_watched = {} - + args = [] + for user_name, user_id in users.items(): # Get all libraries user_name = user_name.lower() @@ -76,54 +134,75 @@ def get_watched(self, users, blacklist_library, whitelist_library, blacklist_lib logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) continue - logger(f"Jellyfin: Generating watched for {user_name} in library {library_title}", 0) - # Movies - if library_type == "Movie": - 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"]: - if user_name not in users_watched: - users_watched[user_name] = {} - if library_title not in users_watched[user_name]: - users_watched[user_name][library_title] = [] - # Lowercase movie["ProviderIds"] keys - movie["ProviderIds"] = {k.lower(): v for k, v in movie["ProviderIds"].items()} - users_watched[user_name][library_title].append(movie["ProviderIds"]) - - # TV Shows - if library_type == "Episode": - 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 user_name not in users_watched: - users_watched[user_name] = {} - if library_title not in users_watched[user_name]: - users_watched[user_name][library_title] = {} - if show_guids not in users_watched[user_name][library_title]: - users_watched[user_name][library_title][show_guids] = {} - if season["Name"] not in users_watched[user_name][library_title][show_guids]: - users_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()} - users_watched[user_name][library_title][show_guids][season["Name"]].append(episode["ProviderIds"]) + args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) + + for user_watched in future_thread_executor(args): + users_watched.update(user_watched) return users_watched + + 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"] + + # 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"] + + 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) + + 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 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 + + 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: @@ -143,7 +222,7 @@ def update_watched(self, watched_list, user_mapping=None, library_mapping=None, if not user_id: logger(f"{user} {user_other} not found in Jellyfin", 2) - break + continue jellyfin_libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] @@ -172,59 +251,6 @@ def update_watched(self, watched_list, user_mapping=None, library_mapping=None, continue if library_id: - 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) - - 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 - - # 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"]] - - 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 - - if show_found: - break + args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) + + future_thread_executor(args) diff --git a/src/plex.py b/src/plex.py index 0c43e49..e398c19 100644 --- a/src/plex.py +++ b/src/plex.py @@ -1,9 +1,11 @@ import re -from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict from plexapi.server import PlexServer from plexapi.myplex import MyPlexAccount +from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, future_thread_executor + + # class plex accept base url and token and username and password but default with none class Plex: def __init__(self, baseurl=None, token=None, username=None, password=None, servername=None): @@ -117,20 +119,81 @@ def get_watched(self, users, blacklist_library, whitelist_library, blacklist_lib logger(f"Plex: Skipping library {library_title} {skip_reason}", 1) continue + args = [] for user in users: logger(f"Plex: Generating watched for {user.title} in library {library_title}", 0) user_name = user.title.lower() - watched = self.get_user_watched(user, library) - if watched: + watched = args.append([self.get_user_watched, user, library]) + + for user_watched in future_thread_executor(args): + if user_watched: if user_name not in users_watched: users_watched[user_name] = {} - if library_title not in users_watched[user_name]: - users_watched[user_name][library_title] = [] - users_watched[user_name][library_title] = watched - + users_watched[user_name][library_title] = user_watched 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) + + 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) + 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 + + + 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 type of user is dict @@ -171,58 +234,7 @@ def update_watched(self, watched_list, user_mapping=None, library_mapping=None, logger(f"Library {library} {library_other} not found in Plex library list", 2) continue - 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 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 + + args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) + + future_thread_executor(args) From 70ef31ff47aeefd052d9d24845be19468b4d75d7 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 15 Jun 2022 12:51:09 -0600 Subject: [PATCH 04/12] Fix threading --- main.py | 13 +++++---- src/jellyfin.py | 18 ++++++------ src/plex.py | 75 +++++++++++++++++++++++++++---------------------- 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/main.py b/main.py index ea9f4c2..b78b42e 100644 --- a/main.py +++ b/main.py @@ -80,16 +80,19 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m 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: - if library_1 in modified_watched_list_1[user_1]: logger(f"Removing {library_1} from {user_1} because it is empty", 1) del modified_watched_list_1[user_1][library_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] + 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 diff --git a/src/jellyfin.py b/src/jellyfin.py index 9df880d..a98ea8b 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -58,24 +58,23 @@ def get_users(self): def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): 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"]: - if user_name not in user_watched: - user_watched[user_name] = {} - if library_title not in user_watched[user_name]: - user_watched[user_name][library_title] = [] # 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"] @@ -91,10 +90,6 @@ def get_user_watched(self, user_name, user_id, library_type, library_id, library for episode in episodes["Items"]: if episode["UserData"]["Played"] == True: if episode["ProviderIds"]: - if user_name not in user_watched: - user_watched[user_name] = {} - if library_title not in user_watched[user_name]: - user_watched[user_name][library_title] = {} 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]: @@ -110,7 +105,7 @@ def get_user_watched(self, user_name, user_id, library_type, library_id, library def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): users_watched = {} args = [] - + for user_name, user_id in users.items(): # Get all libraries user_name = user_name.lower() @@ -137,7 +132,10 @@ def get_watched(self, users, blacklist_library, whitelist_library, blacklist_lib args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) for user_watched in future_thread_executor(args): - users_watched.update(user_watched) + 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 diff --git a/src/plex.py b/src/plex.py index e398c19..cf71fc1 100644 --- a/src/plex.py +++ b/src/plex.py @@ -1,4 +1,5 @@ import re +from collections import ChainMap from plexapi.server import PlexServer from plexapi.myplex import MyPlexAccount @@ -48,16 +49,15 @@ def get_users(self): return users - def get_user_watched(self, user, library): - if self.admin_user == user: - user_plex = self.plex - else: - user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier)) + def get_user_watched(self, user, user_plex, library): + user_watched = {} + user_watched[user.title] = {} - watched = None + logger(f"Plex: Generating watched for {user.title} in library {library.title}", 0) if library.type == "movie": - watched = [] + user_watched[user.title][library.title] = [] + library_videos = user_plex.library.section(library.title) for video in library_videos.search(unmatched=False, unwatched=False): guids = {} @@ -65,10 +65,11 @@ def get_user_watched(self, user, library): guid_source = re.search(r'(.*)://', guid.id).group(1).lower() guid_id = re.search(r'://(.*)', guid.id).group(1) guids[guid_source] = guid_id - watched.append(guids) + user_watched[user.title][library.title].append(guids) elif library.type == "show": - watched = {} + user_watched[user.title][library.title] = {} + library_videos = user_plex.library.section(library.title) for show in library_videos.search(unmatched=False, unwatched=False): show_guids = {} @@ -95,41 +96,47 @@ def get_user_watched(self, user, library): if episode_guids: # append show, season, episode - if show_guids not in watched: - watched[show_guids] = {} - if season.title not in watched[show_guids]: - watched[show_guids][season.title] = {} - watched[show_guids][season.title] = episode_guids + 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 + - return watched + return user_watched def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): # Get all libraries - libraries = self.plex.library.sections() + users_watched = {} + args = [] + + for user in users: + if self.admin_user == user: + user_plex = self.plex + else: + user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier)) + + libraries = user_plex.library.sections() + + for library in libraries: + library_title = library.title + library_type = library.type - # for not in blacklist - 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 = [] - for user in users: - logger(f"Plex: Generating watched for {user.title} in library {library_title}", 0) - user_name = user.title.lower() - watched = args.append([self.get_user_watched, user, 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): - if user_watched: - if user_name not in users_watched: - users_watched[user_name] = {} - users_watched[user_name][library_title] = user_watched return users_watched def update_user_watched (self, user, user_plex, library, videos, dryrun): From aeb86f6b85a70a805bac0716a9c43de2be23d21e Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 15 Jun 2022 13:21:03 -0600 Subject: [PATCH 05/12] Fix user when using plex login. Fix sleep duration --- main.py | 10 ++++------ src/functions.py | 2 +- src/plex.py | 5 ++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index b78b42e..83fb0bf 100644 --- a/main.py +++ b/main.py @@ -307,7 +307,6 @@ def generate_server_connections(): for i, baseurl in enumerate(jellyfin_baseurl): servers.append(("jellyfin", Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()))) - print(f"Servers: {servers}") return servers def main(): @@ -342,7 +341,6 @@ def main(): # Start server_2 at the next server in the list for server_2 in servers[servers.index(server_1) + 1:]: - print(f"server_1: {server_1}, server_2: {server_2}") server_1_connection = server_1[1] server_2_connection = server_2[1] @@ -376,12 +374,12 @@ def main(): future_thread_executor(args) if __name__ == "__main__": - sleep_timer = float(os.getenv("SLEEP_TIMER", "3600")) + sleep_duration = float(os.getenv("SLEEP_DURATION", "3600")) while(True): try: main() - logger(f"Looping in {sleep_timer}") + logger(f"Looping in {sleep_duration}") except Exception as error: if isinstance(error, list): for message in error: @@ -391,10 +389,10 @@ def main(): logger(traceback.format_exc(), 2) - logger(f"Retrying in {sleep_timer}", log_type=0) + logger(f"Retrying in {sleep_duration}", log_type=0) except KeyboardInterrupt: logger("Exiting", log_type=0) os._exit(0) - sleep(sleep_timer) + sleep(sleep_duration) diff --git a/src/functions.py b/src/functions.py index 8e3d81c..d6511eb 100644 --- a/src/functions.py +++ b/src/functions.py @@ -131,5 +131,5 @@ def future_thread_executor(args: list): results.append(result) except Exception as e: raise Exception(e) - + return results diff --git a/src/plex.py b/src/plex.py index cf71fc1..a26a0c4 100644 --- a/src/plex.py +++ b/src/plex.py @@ -107,7 +107,6 @@ def get_user_watched(self, user, user_plex, library): def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): # Get all libraries - users_watched = {} args = [] @@ -115,7 +114,7 @@ def get_watched(self, users, blacklist_library, whitelist_library, blacklist_lib if self.admin_user == user: user_plex = self.plex else: - user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier)) + user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) libraries = user_plex.library.sections() @@ -221,7 +220,7 @@ def update_watched(self, watched_list, user_mapping=None, library_mapping=None, if self.admin_user == user: user_plex = self.plex else: - user_plex = PlexServer(self.baseurl, user.get_token(self.plex.machineIdentifier)) + user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) for library, videos in libraries.items(): library_other = None From 21fe4875eb4c4c2ea98d1e9d0d63ff91f4de7f34 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Wed, 15 Jun 2022 13:38:28 -0600 Subject: [PATCH 06/12] Add ENVs to dockerfile --- Dockerfile | 27 ++++++++++++++++++++++++++- main.py | 3 +-- src/plex.py | 1 - 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index e267be0..ab72099 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,30 @@ FROM python:3-slim +ENV DRYRUN 'True' +ENV DEBUG 'True' +ENV DEBUG_LEVEL 'INFO' +ENV SLEEP_DURATION '3600' +ENV LOGFILE 'log.log' + +ENV USER_MAPPING '{ "User Test": "User Test2" }' +ENV LIBRARY_MAPPING '{ "Shows Test": "TV Shows Test" }' + +ENV PLEX_BASEURL 'http://localhost:32400' +ENV PLEX_TOKEN '' +ENV PLEX_USERNAME '' +ENV PLEX_PASSWORD '' +ENV PLEX_SERVERNAME '' + +ENV JELLYFIN_BASEURL 'http://localhost:8096' +ENV JELLYFIN_TOKEN '' + +ENV BLACKLIST_LIBRARY '' +ENV WHITELIST_LIBRARY '' +ENV BLACKLIST_LIBRARY_TYPE '' +ENV WHITELIST_LIBRARY_TYPE '' +ENV BLACKLIST_USERS '' +ENV WHITELIST_USERS '' + WORKDIR /app COPY ./requirements.txt ./ @@ -7,4 +32,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["python", "-u", "main.py"] \ No newline at end of file +CMD ["python", "-u", "main.py"] diff --git a/main.py b/main.py index 83fb0bf..5276f08 100644 --- a/main.py +++ b/main.py @@ -380,6 +380,7 @@ def main(): try: main() logger(f"Looping in {sleep_duration}") + sleep(sleep_duration) except Exception as error: if isinstance(error, list): for message in error: @@ -394,5 +395,3 @@ def main(): except KeyboardInterrupt: logger("Exiting", log_type=0) os._exit(0) - - sleep(sleep_duration) diff --git a/src/plex.py b/src/plex.py index a26a0c4..b10f3d9 100644 --- a/src/plex.py +++ b/src/plex.py @@ -1,5 +1,4 @@ import re -from collections import ChainMap from plexapi.server import PlexServer from plexapi.myplex import MyPlexAccount From 74b5ea7b5e6a2f33b4000dfde0ca8758432a10de Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sun, 19 Jun 2022 02:56:50 -0600 Subject: [PATCH 07/12] Fix username differences in watch list. Add python version check. More error handling. --- main.py | 403 +---------------------------------------------- src/functions.py | 11 +- src/jellyfin.py | 397 ++++++++++++++++++++++++---------------------- src/main.py | 400 ++++++++++++++++++++++++++++++++++++++++++++++ src/plex.py | 390 ++++++++++++++++++++++++--------------------- 5 files changed, 834 insertions(+), 767 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 @@ def query(self, query, query_type): 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] = {} - - 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"] - - 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"]) - - return user_watched + 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"]) + + # 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"]] = [] + + # 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 + 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 = [] - - for user_name, user_id in users.items(): - # Get all libraries - user_name = user_name.lower() + try: + users_watched = {} + args = [] - libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] + for user_name, user_id in users.items(): + # Get all libraries + user_name = user_name.lower() - 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") + libraries = self.query(f"/Users/{user_id}/Views", "get")["Items"] - 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"] + 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") - skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) + 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 skip_reason: - logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) - continue + skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) - args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) + if skip_reason: + logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) + continue - 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) + args.append([self.get_user_watched, user_name, user_id, library_type, library_id, library_title]) - 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"] - - # 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"] - - 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) - - 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 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 + 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) + + 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 + + # 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"]] + + 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 + + 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) - - 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 - - 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) - - - 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 - - - library_id = None - for jellyfin_library in jellyfin_libraries: - if jellyfin_library["Name"] == library: - library_id = jellyfin_library["Id"] - continue + 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 - if library_id: - args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) + if not user_id: + logger(f"{user} {user_other} not found in Jellyfin", 2) + continue - future_thread_executor(args) + 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) + + + 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 + + if library_id: + args.append([self.update_user_watched, user, user_id, library, library_id, videos, dryrun]) + + 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 @@ def login(self): 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] = {} - - logger(f"Plex: Generating watched for {user.title} in library {library.title}", 0) - - if library.type == "movie": - user_watched[user.title][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) - - elif library.type == "show": - user_watched[user.title][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()) - - 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) - - 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 - - - return user_watched + try: + user_name = user.title.lower() + user_watched = {} + user_watched[user_name] = {} + + logger(f"Plex: Generating watched for {user_name} in library {library.title}", 0) + + 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_name][library.title].append(guids) + + 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()) + + 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) + + 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 + 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) - - 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) + 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 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 = [] - - 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 - - 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) - - # 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 - - - args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) + 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 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 - future_thread_executor(args) + 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) + + # 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]) + + future_thread_executor(args) + except Exception as e: + logger(f"Plex: Failed to update watched, Error: {e}", 2) + raise Exception(e) From 7cd492dc98e04fc27b3f80a89acf22e1c69ebc73 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Sun, 19 Jun 2022 03:03:17 -0600 Subject: [PATCH 08/12] Remove worker=1 --- src/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions.py b/src/functions.py index 0ffafe9..4edc73f 100644 --- a/src/functions.py +++ b/src/functions.py @@ -119,7 +119,7 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): 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) From 2ad6b3afdf7f2a8ccff640151d1c7c58a463e09e Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 20 Jun 2022 15:48:07 -0600 Subject: [PATCH 09/12] Add pytest --- src/main.py | 41 +++++--- test/requirements.txt | 1 + test/test_main_.py | 47 +++++++++ test/test_main_cleanup_watched.py | 154 ++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 test/requirements.txt create mode 100644 test/test_main_.py create mode 100644 test/test_main_cleanup_watched.py diff --git a/src/main.py b/src/main.py index 6528b08..177ba5e 100644 --- a/src/main.py +++ b/src/main.py @@ -6,8 +6,6 @@ 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): @@ -97,8 +95,7 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m return modified_watched_list_1 -def setup_black_white_lists(library_mapping=None): - blacklist_library = os.getenv("BLACKLIST_LIBRARY") +def setup_black_white_lists(blacklist_library: str, whitelist_library: str, blacklist_library_type: str, whitelist_library_type: str, blacklist_users: str, whitelist_users: str, library_mapping=None, user_mapping=None): if blacklist_library: if len(blacklist_library) > 0: blacklist_library = blacklist_library.split(",") @@ -113,10 +110,8 @@ def setup_black_white_lists(library_mapping=None): 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(",") @@ -133,7 +128,6 @@ def setup_black_white_lists(library_mapping=None): 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(",") @@ -142,7 +136,6 @@ def setup_black_white_lists(library_mapping=None): 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(",") @@ -151,20 +144,34 @@ def setup_black_white_lists(library_mapping=None): 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] + if user_mapping: + temp_users = [] + for user in blacklist_users: + user_other = search_mapping(user_mapping, user) + if user_other: + temp_users.append(user_other) + + blacklist_users = blacklist_users + temp_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] + if user_mapping: + temp_users = [] + for user in whitelist_users: + user_other = search_mapping(user_mapping, user) + if user_other: + temp_users.append(user_other) + + whitelist_users = whitelist_users + temp_users else: whitelist_users = [] else: @@ -330,9 +337,18 @@ def main_loop(): 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) + logger("Creating (black/white)lists", 1) + blacklist_library = os.getenv("BLACKLIST_LIBRARY", None) + whitelist_library = os.getenv("WHITELIST_LIBRARY", None) + blacklist_library_type = os.getenv("BLACKLIST_LIBRARY_TYPE", None) + whitelist_library_type = os.getenv("WHITELIST_LIBRARY_TYPE", None) + blacklist_users = os.getenv("BLACKLIST_USERS", None) + whitelist_users = os.getenv("WHITELIST_USERS", None) + + blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users, library_mapping, user_mapping) # Create server connections + logger("Creating server connections", 1) servers = generate_server_connections() for server_1 in servers: @@ -347,8 +363,10 @@ def main_loop(): server_2_connection = server_2[1] # Create users list + logger("Creating users list", 1) server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) + logger("Creating watched lists", 1) 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]] @@ -394,6 +412,7 @@ def main(): logger(traceback.format_exc(), 2) logger(f"Retrying in {sleep_duration}", log_type=0) + sleep(sleep_duration) except KeyboardInterrupt: logger("Exiting", log_type=0) diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1 @@ +pytest diff --git a/test/test_main_.py b/test/test_main_.py new file mode 100644 index 0000000..b1c350e --- /dev/null +++ b/test/test_main_.py @@ -0,0 +1,47 @@ +import sys +import os + +# getting the name of the directory +# where the this file is present. +current = os.path.dirname(os.path.realpath(__file__)) + +# Getting the parent directory name +# where the current directory is present. +parent = os.path.dirname(current) + +# adding the parent directory to +# the sys.path. +sys.path.append(parent) + +from src.main import setup_black_white_lists + +def test_setup_black_white_lists(): + # Simple + blacklist_library = 'library1, library2' + whitelist_library = 'library1, library2' + blacklist_library_type = 'library_type1, library_type2' + whitelist_library_type = 'library_type1, library_type2' + blacklist_users = 'user1, user2' + whitelist_users = 'user1, user2' + + results_blacklist_library, return_whitelist_library, return_blacklist_library_type, return_whitelist_library_type, return_blacklist_users, return_whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users) + + assert results_blacklist_library == ['library1', 'library2'] + assert return_whitelist_library == ['library1', 'library2'] + assert return_blacklist_library_type == ['library_type1', 'library_type2'] + assert return_whitelist_library_type == ['library_type1', 'library_type2'] + assert return_blacklist_users == ['user1', 'user2'] + assert return_whitelist_users == ['user1', 'user2'] + + # Library Mapping and user mapping + library_mapping = { "library1": "library3" } + user_mapping = { "user1": "user3" } + + results_blacklist_library, return_whitelist_library, return_blacklist_library_type, return_whitelist_library_type, return_blacklist_users, return_whitelist_users = setup_black_white_lists(blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users, library_mapping, user_mapping) + + assert results_blacklist_library == ['library1', 'library2', 'library3'] + assert return_whitelist_library == ['library1', 'library2', 'library3'] + assert return_blacklist_library_type == ['library_type1', 'library_type2'] + assert return_whitelist_library_type == ['library_type1', 'library_type2'] + assert return_blacklist_users == ['user1', 'user2', 'user3'] + assert return_whitelist_users == ['user1', 'user2', 'user3'] diff --git a/test/test_main_cleanup_watched.py b/test/test_main_cleanup_watched.py new file mode 100644 index 0000000..b010542 --- /dev/null +++ b/test/test_main_cleanup_watched.py @@ -0,0 +1,154 @@ +import sys +import os + +# getting the name of the directory +# where the this file is present. +current = os.path.dirname(os.path.realpath(__file__)) + +# Getting the parent directory name +# where the current directory is present. +parent = os.path.dirname(current) + +# adding the parent directory to +# the sys.path. +sys.path.append(parent) + +from src.main import cleanup_watched + +tv_shows_watched_list_1 = { + frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { + "Season 1": [ + {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, + {'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)} + ] + } +} + +movies_watched_list_1 = [ + {"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)}, + {"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)}, +] + +tv_shows_watched_list_2 = { + frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { + "Season 1": [ + {'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)}, + {'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} + ] + } +} + +movies_watched_list_2 = [ + {"imdb":"tt2380307", "tmdb":"354912", 'title': 'Coco', 'locations': ('Coco (2017) Remux-1080p.mkv',)}, + {'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} +] + +# Test to see if objects get deleted all the way up to the root. +tv_shows_2_watched_list_1 = { + frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { + "Season 1": [ + {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, + ] + } +} + +expected_tv_show_watched_list_1 = { + frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { + "Season 1": [ + {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)} + ] + } +} + +expected_movie_watched_list_1 = [ + {"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)} +] + +expected_tv_show_watched_list_2 = { + frozenset({("tvdb", "75710"), ("title", "Criminal Minds"), ("imdb", "tt0452046"), ("locations", ("Criminal Minds",)), ("tmdb", "4057")}): { + "Season 1": [ + {'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} + ] + } +} + +expected_movie_watched_list_2 = [ + {'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} +] + + +def test_simple_cleanup_watched(): + user_watched_list_1 = { + "user1": { + "TV Shows": tv_shows_watched_list_1, + "Movies": movies_watched_list_1, + "Other Shows": tv_shows_2_watched_list_1 + }, + } + user_watched_list_2 = { + "user1": { + "TV Shows": tv_shows_watched_list_2, + "Movies": movies_watched_list_2, + "Other Shows": tv_shows_2_watched_list_1 + } + } + + expected_watched_list_1 = { + "user1": { + "TV Shows": expected_tv_show_watched_list_1 + , "Movies": expected_movie_watched_list_1 + } + } + + expected_watched_list_2 = { + "user1": { + "TV Shows": expected_tv_show_watched_list_2 + , "Movies": expected_movie_watched_list_2 + } + } + + return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2) + return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1) + + assert return_watched_list_1 == expected_watched_list_1 + assert return_watched_list_2 == expected_watched_list_2 + + +def test_mapping_cleanup_watched(): + user_watched_list_1 = { + "user1": { + "TV Shows": tv_shows_watched_list_1, + "Movies": movies_watched_list_1, + "Other Shows": tv_shows_2_watched_list_1 + }, + } + user_watched_list_2 = { + "user2": { + "Shows": tv_shows_watched_list_2, + "Movies": movies_watched_list_2, + "Other Shows": tv_shows_2_watched_list_1 + } + } + + expected_watched_list_1 = { + "user1": { + "TV Shows": expected_tv_show_watched_list_1 + , "Movies": expected_movie_watched_list_1 + } + } + + expected_watched_list_2 = { + "user2": { + "Shows": expected_tv_show_watched_list_2 + , "Movies": expected_movie_watched_list_2 + } + } + + user_mapping = { "user1": "user2" } + library_mapping = { "TV Shows": "Shows" } + + return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2, user_mapping=user_mapping, library_mapping=library_mapping) + return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1, user_mapping=user_mapping, library_mapping=library_mapping) + + assert return_watched_list_1 == expected_watched_list_1 + assert return_watched_list_2 == expected_watched_list_2 From 34d97f8dde82cff09399d1aa277bd34dfa70c7b2 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 20 Jun 2022 16:05:05 -0600 Subject: [PATCH 10/12] Add pytest action --- .github/workflows/ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e606928..e56dbcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,20 @@ on: - "*.md" jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: "Install dependencies" + run: pip install -r requirements.txt && pip install -r test/requirements.txt + + - name: "Run tests" + run: pytest -vvv + docker: runs-on: ubuntu-latest + needs: pytest steps: - name: Checkout uses: actions/checkout@v2 From afb71d8e00c8a5b3eb7c6e3d1e0692ae9e5a61a8 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 20 Jun 2022 16:07:52 -0600 Subject: [PATCH 11/12] Handle locations in generate_library_guids_dict --- src/functions.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/functions.py b/src/functions.py index 4edc73f..64050aa 100644 --- a/src/functions.py +++ b/src/functions.py @@ -90,13 +90,16 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): show_output_keys = user_list.keys() show_output_keys = ([ dict(x) for x in list(show_output_keys) ]) for show_key in show_output_keys: - for provider_key, prvider_value in show_key.items(): + for provider_key, provider_value in show_key.items(): # Skip title if provider_key.lower() == "title": continue if provider_key.lower() not in show_output_dict: show_output_dict[provider_key.lower()] = [] - show_output_dict[provider_key.lower()].append(prvider_value.lower()) + if provider_key.lower() == "locations": + show_output_dict[provider_key.lower()].append(provider_value) + else: + show_output_dict[provider_key.lower()].append(provider_value.lower()) if generate_output in (1, 3): for show in user_list: @@ -105,14 +108,20 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): for episode_key, episode_value in episode.items(): if episode_key.lower() not in episode_output_dict: episode_output_dict[episode_key.lower()] = [] - episode_output_dict[episode_key.lower()].append(episode_value.lower()) + if episode_key == "locations": + episode_output_dict[episode_key.lower()].append(episode_value) + else: + episode_output_dict[episode_key.lower()].append(episode_value.lower()) if generate_output == 2: for movie in user_list: for movie_key, movie_value in movie.items(): if movie_key.lower() not in movies_output_dict: movies_output_dict[movie_key.lower()] = [] - movies_output_dict[movie_key.lower()].append(movie_value.lower()) + if movie_key == "locations": + movies_output_dict[movie_key.lower()].append(movie_value) + else: + movies_output_dict[movie_key.lower()].append(movie_value.lower()) return show_output_dict, episode_output_dict, movies_output_dict From c104973f95e0d7f51dcd00ff3ebb4f0b8e29b833 Mon Sep 17 00:00:00 2001 From: Luigi311 Date: Mon, 20 Jun 2022 21:12:02 -0600 Subject: [PATCH 12/12] Add location based matching --- .vscode/launch.json | 30 +++---- src/functions.py | 9 +- src/jellyfin.py | 134 +++++++++++++++++++----------- src/main.py | 56 +++++++++---- src/plex.py | 132 ++++++++++++++++++----------- test/test_main_.py | 8 +- test/test_main_cleanup_watched.py | 34 ++++++-- 7 files changed, 262 insertions(+), 141 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ae8f15f..aa73483 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,16 +1,16 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python", - "type": "python", - "request": "launch", - "program": "main.py", - "console": "integratedTerminal", - "justMyCode": true - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Main", + "type": "python", + "request": "launch", + "program": "main.py", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} diff --git a/src/functions.py b/src/functions.py index 64050aa..8b404ec 100644 --- a/src/functions.py +++ b/src/functions.py @@ -97,7 +97,8 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): if provider_key.lower() not in show_output_dict: show_output_dict[provider_key.lower()] = [] if provider_key.lower() == "locations": - show_output_dict[provider_key.lower()].append(provider_value) + for show_location in provider_value: + show_output_dict[provider_key.lower()].append(show_location) else: show_output_dict[provider_key.lower()].append(provider_value.lower()) @@ -109,7 +110,8 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): if episode_key.lower() not in episode_output_dict: episode_output_dict[episode_key.lower()] = [] if episode_key == "locations": - episode_output_dict[episode_key.lower()].append(episode_value) + for episode_location in episode_value: + episode_output_dict[episode_key.lower()].append(episode_location) else: episode_output_dict[episode_key.lower()].append(episode_value.lower()) @@ -119,7 +121,8 @@ def generate_library_guids_dict(user_list: dict, generate_output: int): if movie_key.lower() not in movies_output_dict: movies_output_dict[movie_key.lower()] = [] if movie_key == "locations": - movies_output_dict[movie_key.lower()].append(movie_value) + for movie_location in movie_value: + movies_output_dict[movie_key.lower()].append(movie_location) else: movies_output_dict[movie_key.lower()].append(movie_value.lower()) diff --git a/src/jellyfin.py b/src/jellyfin.py index b0a753d..f16ac3c 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -39,7 +39,7 @@ def query(self, query, query_type): response = self.session.post(self.baseurl + query, headers=headers) return response.json() - + except Exception as e: logger(f"Jellyfin: Query failed {e}", 2) raise Exception(e) @@ -71,40 +71,49 @@ def get_user_watched(self, user_name, user_id, library_type, library_id, library # 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") + watched = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", "get") for movie in watched["Items"]: if movie["UserData"]["Played"] == True: + movie_guids = {} + movie_guids["title"] = movie["Name"] 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"]) + movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()} + if movie["MediaSources"]: + movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in movie["MediaSources"]]) + user_watched[user_name][library_title].append(movie_guids) # 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 = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&ParentId={library_id}&Fields=ItemCounts,ProviderIds,Path", "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["locations"] = tuple([show["Path"].split("/")[-1]]) 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") + episodes = self.query(f"/Shows/{show['Id']}/Episodes?seasonId={season['Id']}&userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get") if len(episodes["Items"]) > 0: for episode in episodes["Items"]: if episode["UserData"]["Played"] == True: - if episode["ProviderIds"]: + if episode["ProviderIds"] or episode["MediaSources"]: 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"]) + episode_guids = {} + if episode["ProviderIds"]: + episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()} + if episode["MediaSources"]: + episode_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in episode["MediaSources"]]) + user_watched[user_name][library_title][show_guids][season["Name"]].append(episode_guids) return user_watched except Exception as e: @@ -163,55 +172,86 @@ def update_user_watched(self, user, user_id, library, library_id, videos, dryrun 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") + jellyfin_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources", "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) + movie_found = False + + if "MediaSources" in jellyfin_video: + for movie_location in jellyfin_video["MediaSources"]: + if movie_location["Path"].split("/")[-1] in videos_movies_ids["locations"]: + movie_found = True break + if not movie_found: + 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()]: + movie_found = True + break + + if movie_found: + jellyfin_video_id = jellyfin_video["Id"] + 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) + + # 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_search = self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,Path", "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 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 jellyfin_show["Name"] == "The 13 Ghosts of Scooby-Doo": + print(jellyfin_show) + + if "Path" in jellyfin_show: + if jellyfin_show["Path"].split("/")[-1] in videos_shows_ids["locations"]: + show_found = True + + if not show_found: + 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 + break + + if show_found: + jellyfin_show_id = jellyfin_show["Id"] + jellyfin_episodes = self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get") + + for jellyfin_episode in jellyfin_episodes["Items"]: + episode_found = False + + if "MediaSources" in jellyfin_episode: + for episode_location in jellyfin_episode["MediaSources"]: + if episode_location["Path"].split("/")[-1] in videos_episode_ids["locations"]: + episode_found = True + break + + if not episode_found: + 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()]: + episode_found = True + break + + if episode_found: + jellyfin_episode_id = jellyfin_episode["Id"] + 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) + except Exception as e: logger(f"Jellyfin: Error updating watched for {user} in library {library}", 2) raise Exception(e) diff --git a/src/main.py b/src/main.py index 177ba5e..a4cffdb 100644 --- a/src/main.py +++ b/src/main.py @@ -40,32 +40,54 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m # 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) + _, _, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2], 2) + for movie in watched_list_1[user_1][library_1]: + movie_found = False + for movie_key, movie_value in movie.items(): + if movie_key == "locations": + for location in movie_value: + if location in movies_watched_list_2_keys_dict["locations"]: + movie_found = True + break + else: + if movie_key in movies_watched_list_2_keys_dict.keys(): + if movie_value in movies_watched_list_2_keys_dict[movie_key]: + movie_found = True + + if movie_found: + logger(f"Removing {movie} from {library_1}", 3) + modified_watched_list_1[user_1][library_1].remove(movie) + break # 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) + show_watched_list_2_keys_dict, episode_watched_list_2_keys_dict, _ = generate_library_guids_dict(watched_list_2[user_2][library_2], 3) 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) + episode_found = False + for episode_key, episode_value in episode.items(): + # If episode_key and episode_value are in episode_watched_list_2_keys_dict exactly, then remove from watch_list_1 + if episode_key == "locations": + for location in episode_value: + if location in episode_watched_list_2_keys_dict["locations"]: + episode_found = True + break + + else: + if episode_key in episode_watched_list_2_keys_dict.keys(): + if episode_value in episode_watched_list_2_keys_dict[episode_key]: + episode_found = True + + if episode_found: + 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) + break # Remove empty seasons if len(modified_watched_list_1[user_1][library_1][show_key_1][season]) == 0: @@ -154,7 +176,7 @@ def setup_black_white_lists(blacklist_library: str, whitelist_library: str, blac user_other = search_mapping(user_mapping, user) if user_other: temp_users.append(user_other) - + blacklist_users = blacklist_users + temp_users else: blacklist_users = [] diff --git a/src/plex.py b/src/plex.py index a1c6874..825c3a0 100644 --- a/src/plex.py +++ b/src/plex.py @@ -64,26 +64,32 @@ def get_user_watched(self, user, user_plex, library): 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 video in library_videos.search(unwatched=False): + movie_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) + movie_guids[guid_source] = guid_id + + movie_guids["title"] = video.title + movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations]) + + user_watched[user_name][library.title].append(movie_guids) 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): + for show in library_videos.search(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["title"] = show.title + show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations]) show_guids = frozenset(show_guids.items()) for season in show.seasons(): @@ -97,6 +103,7 @@ def get_user_watched(self, user, user_plex, library): guid_id = re.search(r'://(.*)', guid.id).group(1) episode_guids_temp[guid_source] = guid_id + episode_guids_temp["locations"] = tuple([x.split("/")[-1] for x in episode.locations]) episode_guids.append(episode_guids_temp) if episode_guids: @@ -159,56 +166,83 @@ def update_user_watched (self, user, user_plex, library, videos, dryrun): 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) - break + for movies_search in library_videos.search(unwatched=True): + movie_found = False + for movie_location in movies_search.locations: + if movie_location.split("/")[-1] in videos_movies_ids["locations"]: + movie_found = True + break + + if not movie_found: + 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]: + movie_found = True + break + + if movie_found: + 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) 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): + for show_search in library_videos.search(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: + for show_location in show_search.locations: + if show_location.split("/")[-1] in videos_shows_ids["locations"]: + show_found = True break + + if not show_found: + 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 + break + + if show_found: + for episode_search in show_search.episodes(): + episode_found = False + + for episode_location in episode_search.locations: + if episode_location.split("/")[-1] in videos_episode_ids["locations"]: + episode_found = True + break + + if not episode_found: + 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]: + episode_found = True + break + + if episode_found: + 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) except Exception as e: logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2) raise Exception(e) diff --git a/test/test_main_.py b/test/test_main_.py index b1c350e..2b15392 100644 --- a/test/test_main_.py +++ b/test/test_main_.py @@ -1,15 +1,15 @@ import sys import os - + # getting the name of the directory # where the this file is present. current = os.path.dirname(os.path.realpath(__file__)) - + # Getting the parent directory name # where the current directory is present. parent = os.path.dirname(current) - -# adding the parent directory to + +# adding the parent directory to # the sys.path. sys.path.append(parent) diff --git a/test/test_main_cleanup_watched.py b/test/test_main_cleanup_watched.py index b010542..4d2bbcd 100644 --- a/test/test_main_cleanup_watched.py +++ b/test/test_main_cleanup_watched.py @@ -1,15 +1,15 @@ import sys import os - + # getting the name of the directory # where the this file is present. current = os.path.dirname(os.path.realpath(__file__)) - + # Getting the parent directory name # where the current directory is present. parent = os.path.dirname(current) - -# adding the parent directory to + +# adding the parent directory to # the sys.path. sys.path.append(parent) @@ -21,6 +21,12 @@ {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)}, {'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)} ] + }, + frozenset({("title", "Test"), ("locations", ("Test",))}): { + "Season 1": [ + {'locations': ('Test S01E01.mkv',)}, + {'locations': ('Test S01E02.mkv',)} + ] } } @@ -35,6 +41,12 @@ {'imdb': 'tt0550487', 'tmdb': '282861', 'tvdb': '300385', 'locations': ('Criminal Minds S01E02 Compulsion WEBDL-720p.mkv',)}, {'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} ] + }, + frozenset({("title", "Test"), ("locations", ("Test",))}): { + "Season 1": [ + {'locations': ('Test S01E02.mkv',)}, + {'locations': ('Test S01E03.mkv',)} + ] } } @@ -57,6 +69,11 @@ "Season 1": [ {'imdb': 'tt0550489', 'tmdb': '282843', 'tvdb': '176357', 'locations': ('Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv',)} ] + }, + frozenset({("title", "Test"), ("locations", ("Test",))}): { + "Season 1": [ + {'locations': ('Test S01E01.mkv',)} + ] } } @@ -69,6 +86,11 @@ "Season 1": [ {'imdb': 'tt0550498', 'tmdb': '282865', 'tvdb': '300474', 'locations': ("Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",)} ] + }, + frozenset({("title", "Test"), ("locations", ("Test",))}): { + "Season 1": [ + {'locations': ('Test S01E03.mkv',)} + ] } } @@ -106,7 +128,7 @@ def test_simple_cleanup_watched(): , "Movies": expected_movie_watched_list_2 } } - + return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2) return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1) @@ -149,6 +171,6 @@ def test_mapping_cleanup_watched(): return_watched_list_1 = cleanup_watched(user_watched_list_1, user_watched_list_2, user_mapping=user_mapping, library_mapping=library_mapping) return_watched_list_2 = cleanup_watched(user_watched_list_2, user_watched_list_1, user_mapping=user_mapping, library_mapping=library_mapping) - + assert return_watched_list_1 == expected_watched_list_1 assert return_watched_list_2 == expected_watched_list_2