diff --git a/main.py b/main.py index 1e5ee99..f4779d9 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,11 @@ import sys -if __name__ == '__main__': +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.main import main + main() diff --git a/src/functions.py b/src/functions.py index 4370076..cc2274c 100644 --- a/src/functions.py +++ b/src/functions.py @@ -4,7 +4,8 @@ from dotenv import load_dotenv load_dotenv(override=True) -logfile = os.getenv("LOGFILE","log.log") +logfile = os.getenv("LOGFILE", "log.log") + def logger(message: str, log_type=0): debug = str_to_bool(os.getenv("DEBUG", "True")) @@ -29,6 +30,7 @@ def logger(message: str, log_type=0): file = open(logfile, "a", encoding="utf-8") file.write(output + "\n") + # Reimplementation of distutils.util.strtobool due to it being deprecated # Source: https://github.com/PostHog/posthog/blob/01e184c29d2c10c43166f1d40a334abbc3f99d8a/posthog/utils.py#L668 def str_to_bool(value: any) -> bool: @@ -36,6 +38,7 @@ def str_to_bool(value: any) -> bool: return False return str(value).lower() in ("y", "yes", "t", "true", "on", "1") + # Get mapped value def search_mapping(dictionary: dict, key_value: str): if key_value in dictionary.keys(): @@ -45,12 +48,22 @@ def search_mapping(dictionary: dict, key_value: str): 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.lower())] + return list(dictionary.keys())[ + list(dictionary.values()).index(key_value.lower()) + ] else: return None -def check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): +def check_skip_logic( + library_title, + library_type, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping, +): skip_reason = None if library_type.lower() in blacklist_library_type: @@ -89,7 +102,7 @@ def generate_library_guids_dict(user_list: dict): try: show_output_keys = user_list.keys() - show_output_keys = ([ dict(x) for x in list(show_output_keys) ]) + show_output_keys = [dict(x) for x in list(show_output_keys)] for show_key in show_output_keys: for provider_key, provider_value in show_key.items(): # Skip title @@ -101,7 +114,9 @@ def generate_library_guids_dict(user_list: dict): 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()) + show_output_dict[provider_key.lower()].append( + provider_value.lower() + ) except: logger("Generating show_output_dict failed, skipping", 1) @@ -114,9 +129,13 @@ def generate_library_guids_dict(user_list: dict): episode_output_dict[episode_key.lower()] = [] if episode_key == "locations": for episode_location in episode_value: - episode_output_dict[episode_key.lower()].append(episode_location) + episode_output_dict[episode_key.lower()].append( + episode_location + ) else: - episode_output_dict[episode_key.lower()].append(episode_value.lower()) + episode_output_dict[episode_key.lower()].append( + episode_value.lower() + ) except: logger("Generating episode_output_dict failed, skipping", 1) @@ -135,6 +154,7 @@ def generate_library_guids_dict(user_list: dict): return show_output_dict, episode_output_dict, movies_output_dict + def combine_watched_dicts(dicts: list): combined_dict = {} for single_dict in dicts: @@ -146,12 +166,13 @@ def combine_watched_dicts(dicts: list): return combined_dict + def future_thread_executor(args: list, workers: int = -1): futures_list = [] results = [] if workers == -1: - workers = min(32, os.cpu_count()*1.25) + workers = min(32, os.cpu_count() * 1.25) with ThreadPoolExecutor(max_workers=workers) as executor: for arg in args: diff --git a/src/jellyfin.py b/src/jellyfin.py index 85435b4..51f9df2 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -1,7 +1,14 @@ import asyncio, aiohttp -from src.functions import logger, search_mapping, check_skip_logic, generate_library_guids_dict, combine_watched_dicts +from src.functions import ( + logger, + search_mapping, + check_skip_logic, + generate_library_guids_dict, + combine_watched_dicts, +) -class Jellyfin(): + +class Jellyfin: def __init__(self, baseurl, token): self.baseurl = baseurl self.token = token @@ -14,16 +21,12 @@ class Jellyfin(): self.users = asyncio.run(self.get_users()) - async def query(self, query, query_type, session, identifiers=None): try: results = None - headers = { - "Accept": "application/json", - "X-Emby-Token": self.token - } + headers = {"Accept": "application/json", "X-Emby-Token": self.token} authorization = ( - 'MediaBrowser , ' + "MediaBrowser , " 'Client="other", ' 'Device="script", ' 'DeviceId="script", ' @@ -32,11 +35,15 @@ class Jellyfin(): headers["X-Emby-Authorization"] = authorization if query_type == "get": - async with session.get(self.baseurl + query, headers=headers) as response: + async with session.get( + self.baseurl + query, headers=headers + ) as response: results = await response.json() elif query_type == "post": - async with session.post(self.baseurl + query, headers=headers) as response: + async with session.post( + self.baseurl + query, headers=headers + ) as response: results = await response.json() # append identifiers to results @@ -45,9 +52,8 @@ class Jellyfin(): return results except Exception as e: - logger(f"Jellyfin: Query failed {e}", 2) - raise Exception(e) - + logger(f"Jellyfin: Query failed {e}", 2) + raise Exception(e) async def get_users(self): try: @@ -67,47 +73,76 @@ class Jellyfin(): logger(f"Jellyfin: Get users failed {e}", 2) raise Exception(e) - - async def get_user_watched(self, user_name, user_id, library_type, library_id, library_title): + async def get_user_watched( + self, user_name, user_id, library_type, library_id, library_title + ): 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) + logger( + f"Jellyfin: Generating watched for {user_name} in library {library_title}", + 0, + ) # Movies async with aiohttp.ClientSession() as session: if library_type == "Movie": user_watched[user_name][library_title] = [] - watched = await self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", "get", session) + watched = await self.query( + f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&Fields=ItemCounts,ProviderIds,MediaSources", + "get", + session, + ) for movie in watched["Items"]: if movie["UserData"]["Played"] == True: movie_guids = {} movie_guids["title"] = movie["Name"] if "ProviderIds" in movie: # Lowercase movie["ProviderIds"] keys - movie_guids = {k.lower(): v for k, v in movie["ProviderIds"].items()} + movie_guids = { + k.lower(): v + for k, v in movie["ProviderIds"].items() + } if "MediaSources" in movie: - movie_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in 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 == "Series": user_watched[user_name][library_title] = {} - watched_shows = await self.query(f"/Users/{user_id}/Items?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount", "get", session) + watched_shows = await self.query( + f"/Users/{user_id}/Items?ParentId={library_id}&isPlaceHolder=false&Fields=ProviderIds,Path,RecursiveItemCount", + "get", + session, + ) watched_shows_filtered = [] for show in watched_shows["Items"]: - if "PlayedPercentage" in show["UserData"]: + if "PlayedPercentage" in show["UserData"]: if show["UserData"]["PlayedPercentage"] > 0: watched_shows_filtered.append(show) seasons_tasks = [] for show in watched_shows_filtered: - show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()} + 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()) identifiers = {"show_guids": show_guids, "show_id": show["Id"]} - task = asyncio.ensure_future(self.query(f"/Shows/{show['Id']}/Seasons?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount", "get", session, frozenset(identifiers.items()))) + task = asyncio.ensure_future( + self.query( + f"/Shows/{show['Id']}/Seasons?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount", + "get", + session, + frozenset(identifiers.items()), + ) + ) seasons_tasks.append(task) seasons_watched = await asyncio.gather(*seasons_tasks) @@ -115,15 +150,21 @@ class Jellyfin(): for seasons in seasons_watched: seasons_watched_filtered_dict = {} - seasons_watched_filtered_dict["Identifiers"] = seasons["Identifiers"] + seasons_watched_filtered_dict["Identifiers"] = seasons[ + "Identifiers" + ] seasons_watched_filtered_dict["Items"] = [] for season in seasons["Items"]: if "PlayedPercentage" in season["UserData"]: if season["UserData"]["PlayedPercentage"] > 0: - seasons_watched_filtered_dict["Items"].append(season) + seasons_watched_filtered_dict["Items"].append( + season + ) if seasons_watched_filtered_dict["Items"]: - seasons_watched_filtered.append(seasons_watched_filtered_dict) + seasons_watched_filtered.append( + seasons_watched_filtered_dict + ) episodes_tasks = [] for seasons in seasons_watched_filtered: @@ -132,7 +173,14 @@ class Jellyfin(): season_identifiers = dict(seasons["Identifiers"]) season_identifiers["season_id"] = season["Id"] season_identifiers["season_name"] = season["Name"] - task = asyncio.ensure_future(self.query(f"/Shows/{season_identifiers['show_id']}/Episodes?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&isPlayed=true&Fields=ProviderIds,MediaSources", "get", session, frozenset(season_identifiers.items()))) + task = asyncio.ensure_future( + self.query( + f"/Shows/{season_identifiers['show_id']}/Episodes?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&isPlayed=true&Fields=ProviderIds,MediaSources", + "get", + session, + frozenset(season_identifiers.items()), + ) + ) episodes_tasks.append(task) watched_episodes = await asyncio.gather(*episodes_tasks) @@ -140,28 +188,72 @@ class Jellyfin(): if len(episodes["Items"]) > 0: for episode in episodes["Items"]: if episode["UserData"]["Played"] == True: - if "ProviderIds" in episode or "MediaSources" in episode: - episode_identifiers = dict(episodes["Identifiers"]) + if ( + "ProviderIds" in episode + or "MediaSources" in episode + ): + episode_identifiers = dict( + episodes["Identifiers"] + ) show_guids = episode_identifiers["show_guids"] - if show_guids not in user_watched[user_name][library_title]: - user_watched[user_name][library_title][show_guids] = {} - if episode_identifiers["season_name"] not in user_watched[user_name][library_title][show_guids]: - user_watched[user_name][library_title][show_guids][episode_identifiers["season_name"]] = [] + if ( + show_guids + not in user_watched[user_name][ + library_title + ] + ): + user_watched[user_name][library_title][ + show_guids + ] = {} + if ( + episode_identifiers["season_name"] + not in user_watched[user_name][ + library_title + ][show_guids] + ): + user_watched[user_name][library_title][ + show_guids + ][episode_identifiers["season_name"]] = [] episode_guids = {} if "ProviderIds" in episode: - episode_guids = {k.lower(): v for k, v in episode["ProviderIds"].items()} + episode_guids = { + k.lower(): v + for k, v in episode[ + "ProviderIds" + ].items() + } if "MediaSources" in episode: - episode_guids["locations"] = tuple([x["Path"].split("/")[-1] for x in episode["MediaSources"]]) - user_watched[user_name][library_title][show_guids][episode_identifiers["season_name"]].append(episode_guids) + episode_guids["locations"] = tuple( + [ + x["Path"].split("/")[-1] + for x in episode["MediaSources"] + ] + ) + user_watched[user_name][library_title][ + show_guids + ][episode_identifiers["season_name"]].append( + episode_guids + ) return user_watched except Exception as e: - logger(f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", 2) + logger( + f"Jellyfin: Failed to get watched for {user_name} in library {library_title}, Error: {e}", + 2, + ) raise Exception(e) - - async def get_users_watched(self, user_name, user_id, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping): + async def get_users_watched( + self, + user_name, + user_id, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping, + ): try: # Get all libraries user_name = user_name.lower() @@ -173,11 +265,23 @@ class Jellyfin(): for library in libraries["Items"]: library_id = library["Id"] library_title = library["Name"] - identifiers = {"library_id": library_id, "library_title": library_title} - task = asyncio.ensure_future(self.query(f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&limit=1", "get", session, identifiers=identifiers)) + identifiers = { + "library_id": library_id, + "library_title": library_title, + } + task = asyncio.ensure_future( + self.query( + f"/Users/{user_id}/Items?ParentId={library_id}&Filters=IsPlayed&limit=1", + "get", + session, + identifiers=identifiers, + ) + ) tasks_libraries.append(task) - libraries = await asyncio.gather(*tasks_libraries, return_exceptions=True) + libraries = await asyncio.gather( + *tasks_libraries, return_exceptions=True + ) for watched in libraries: if len(watched["Items"]) == 0: @@ -187,14 +291,29 @@ class Jellyfin(): library_title = watched["Identifiers"]["library_title"] library_type = watched["Items"][0]["Type"] - skip_reason = check_skip_logic(library_title, library_type, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping) + skip_reason = check_skip_logic( + library_title, + library_type, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping, + ) if skip_reason: - logger(f"Jellyfin: Skipping library {library_title} {skip_reason}", 1) + logger( + f"Jellyfin: Skipping library {library_title} {skip_reason}", + 1, + ) continue # Get watched for user - task = asyncio.ensure_future(self.get_user_watched(user_name, user_id, library_type, library_id, library_title)) + task = asyncio.ensure_future( + self.get_user_watched( + user_name, user_id, library_type, library_id, library_title + ) + ) tasks_watched.append(task) watched = await asyncio.gather(*tasks_watched, return_exceptions=True) @@ -203,14 +322,31 @@ class Jellyfin(): logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2) raise Exception(e) - - async def get_watched(self, users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping=None): + async def get_watched( + self, + users, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping=None, + ): try: users_watched = {} watched = [] for user_name, user_id in users.items(): - watched.append(await self.get_users_watched(user_name, user_id, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)) + watched.append( + await self.get_users_watched( + user_name, + user_id, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping, + ) + ) for user_watched in watched: user_watched_temp = combine_watched_dicts(user_watched) @@ -224,82 +360,149 @@ class Jellyfin(): logger(f"Jellyfin: Failed to get watched, Error: {e}", 2) raise Exception(e) - - async def update_user_watched(self, user_name, user_id, library, library_id, videos, dryrun): + async def update_user_watched( + self, user_name, user_id, library, library_id, videos, dryrun + ): try: - logger(f"Jellyfin: Updating watched for {user_name} in library {library}", 1) - videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos) + logger( + f"Jellyfin: Updating watched for {user_name} in library {library}", 1 + ) + ( + videos_shows_ids, + videos_episodes_ids, + videos_movies_ids, + ) = generate_library_guids_dict(videos) - logger(f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1) + logger( + f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", + 1, + ) async with aiohttp.ClientSession() as session: if videos_movies_ids: - jellyfin_search = await self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources", "get", session) + jellyfin_search = await self.query( + f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources", + "get", + session, + ) for jellyfin_video in jellyfin_search["Items"]: - movie_found = False + 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"]: + 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 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_name} in {library} for Jellyfin" - if not dryrun: - logger(f"Marking {msg}", 0) - await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", "post", session) - else: - logger(f"Dryrun {msg}", 0) + if movie_found: + jellyfin_video_id = jellyfin_video["Id"] + msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin" + if not dryrun: + logger(f"Marking {msg}", 0) + await self.query( + f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", + "post", + session, + ) else: - logger(f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", 1) - - + logger(f"Dryrun {msg}", 0) + else: + logger( + f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", + 1, + ) # TV Shows if videos_shows_ids and videos_episodes_ids: - jellyfin_search = await self.query(f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,Path", "get", session) + jellyfin_search = await self.query( + f"/Users/{user_id}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=false&ParentId={library_id}&isPlayed=false&Fields=ItemCounts,ProviderIds,Path", + "get", + session, + ) jellyfin_shows = [x for x in jellyfin_search["Items"]] for jellyfin_show in jellyfin_shows: show_found = False if "Path" in jellyfin_show: - if jellyfin_show["Path"].split("/")[-1] in videos_shows_ids["locations"]: + 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(): + 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()]: + if ( + show_provider_id.lower() + in videos_shows_ids[ + show_provider_source.lower() + ] + ): show_found = True break if show_found: - logger(f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}", 1) + logger( + f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show['Name']}", + 1, + ) jellyfin_show_id = jellyfin_show["Id"] - jellyfin_episodes = await self.query(f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", "get", session) + jellyfin_episodes = await self.query( + f"/Shows/{jellyfin_show_id}/Episodes?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", + "get", + session, + ) 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_episodes_ids["locations"]: + for episode_location in jellyfin_episode[ + "MediaSources" + ]: + if ( + episode_location["Path"].split("/")[-1] + in videos_episodes_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_episodes_ids: - if episode_provider_id.lower() in videos_episodes_ids[episode_provider_source.lower()]: + for ( + episode_provider_source, + episode_provider_id, + ) in jellyfin_episode["ProviderIds"].items(): + if ( + episode_provider_source.lower() + in videos_episodes_ids + ): + if ( + episode_provider_id.lower() + in videos_episodes_ids[ + episode_provider_source.lower() + ] + ): episode_found = True break @@ -308,23 +511,44 @@ class Jellyfin(): msg = f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['Name']} as watched for {user_name} in {library} for Jellyfin" if not dryrun: logger(f"Marked {msg}", 0) - await self.query(f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", "post", session) + await self.query( + f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", + "post", + session, + ) else: logger(f"Dryrun {msg}", 0) else: - logger(f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", 1) + logger( + f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", + 1, + ) else: - logger(f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}", 1) + logger( + f"Jellyfin: Skipping show {jellyfin_show['Name']} as it is not in mark list for {user_name}", + 1, + ) - if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids: - logger(f"Jellyfin: No videos to mark as watched for {user_name} in library {library}", 1) + if ( + not videos_movies_ids + and not videos_shows_ids + and not videos_episodes_ids + ): + logger( + f"Jellyfin: No videos to mark as watched for {user_name} in library {library}", + 1, + ) except Exception as e: - logger(f"Jellyfin: Error updating watched for {user_name} in library {library}", 2) + logger( + f"Jellyfin: Error updating watched for {user_name} in library {library}", + 2, + ) raise Exception(e) - - async def update_watched(self, watched_list, user_mapping=None, library_mapping=None, dryrun=False): + async def update_watched( + self, watched_list, user_mapping=None, library_mapping=None, dryrun=False + ): try: tasks = [] async with aiohttp.ClientSession() as session: @@ -353,7 +577,9 @@ class Jellyfin(): logger(f"{user} {user_other} not found in Jellyfin", 2) continue - jellyfin_libraries = await self.query(f"/Users/{user_id}/Views", "get", session) + jellyfin_libraries = await self.query( + f"/Users/{user_id}/Views", "get", session + ) jellyfin_libraries = [x for x in jellyfin_libraries["Items"]] for library, videos in libraries.items(): @@ -364,17 +590,29 @@ class Jellyfin(): 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.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) + 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) + 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) + logger( + f"Jellyfin: Library {library} not found in library list", + 2, + ) continue library_id = None @@ -384,7 +622,9 @@ class Jellyfin(): continue if library_id: - task = self.update_user_watched(user_name, user_id, library, library_id, videos, dryrun) + task = self.update_user_watched( + user_name, user_id, library, library_id, videos, dryrun + ) tasks.append(task) await asyncio.gather(*tasks, return_exceptions=True) diff --git a/src/main.py b/src/main.py index 55bca93..bdf3c8b 100644 --- a/src/main.py +++ b/src/main.py @@ -2,13 +2,21 @@ import copy, os, traceback, json, asyncio from dotenv import load_dotenv from time import sleep, perf_counter -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, +) 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): + +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 @@ -35,10 +43,17 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m 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) + logger( + f"library {library_1} and {library_other} not found in watched list 2", + 1, + ) continue - _, episode_watched_list_2_keys_dict, movies_watched_list_2_keys_dict = generate_library_guids_dict(watched_list_2[user_2][library_2]) + ( + _, + episode_watched_list_2_keys_dict, + movies_watched_list_2_keys_dict, + ) = generate_library_guids_dict(watched_list_2[user_2][library_2]) # Movies if isinstance(watched_list_1[user_1][library_1], list): @@ -46,22 +61,39 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m movie_found = False for movie_key, movie_value in movie.items(): if movie_key == "locations": - if "locations" in movies_watched_list_2_keys_dict.keys(): + if ( + "locations" + in movies_watched_list_2_keys_dict.keys() + ): for location in movie_value: - if location in movies_watched_list_2_keys_dict["locations"]: + 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]: + 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) + 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 @@ -69,47 +101,107 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m 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 in watched_list_1[user_1][library_1][ + show_key_1 + ][season]: 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": - if "locations" in episode_watched_list_2_keys_dict.keys(): + if ( + "locations" + in episode_watched_list_2_keys_dict.keys() + ): for location in episode_value: - if location in episode_watched_list_2_keys_dict["locations"]: + 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]: + 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 {episode} from {show_key_dict['title']}", 3) - modified_watched_list_1[user_1][library_1][show_key_1][season].remove(episode) + if ( + episode + in modified_watched_list_1[user_1][ + library_1 + ][show_key_1][season] + ): + logger( + f"Removing {episode} from {show_key_dict['title']}", + 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: - if season in modified_watched_list_1[user_1][library_1][show_key_1]: - logger(f"Removing {season} from {show_key_dict['title']} because it is empty", 3) - del modified_watched_list_1[user_1][library_1][show_key_1][season] + 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 {show_key_dict['title']} 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] + 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] + 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 @@ -119,7 +211,17 @@ def cleanup_watched(watched_list_1, watched_list_2, user_mapping=None, library_m return modified_watched_list_1 -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): + +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(",") @@ -202,9 +304,19 @@ def setup_black_white_lists(blacklist_library: str, whitelist_library: str, blac whitelist_users = [] logger(f"Whitelist Users: {whitelist_users}", 1) - return blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, blacklist_users, whitelist_users + 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): + +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] @@ -216,23 +328,22 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi server_1_users = [] if server_1_type == "plex": - server_1_users = [ x.title.lower() for x in server_1_connection.users ] + 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_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 ] + 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() ] - + 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) + 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 @@ -242,7 +353,7 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi for server_2_user in server_2_users: if user_mapping: - plex_jellyfin_mapped_user = search_mapping(user_mapping, server_2_user) + 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 @@ -268,36 +379,53 @@ def setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mappi 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(): + 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(): + 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(): + 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(): + 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}") + 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}") + 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 = [] @@ -313,21 +441,51 @@ def generate_server_connections(): 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") + 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, ssl_bypass=ssl_bypass))) + servers.append( + ( + "plex", + Plex( + baseurl=url.strip(), + token=plex_token[i].strip(), + username=None, + password=None, + servername=None, + ssl_bypass=ssl_bypass, + ), + ) + ) 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") + 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(), ssl_bypass=ssl_bypass))) + servers.append( + ( + "plex", + Plex( + baseurl=None, + token=None, + username=username.strip(), + password=plex_password[i].strip(), + servername=plex_servername[i].strip(), + ssl_bypass=ssl_bypass, + ), + ) + ) jellyfin_baseurl = os.getenv("JELLYFIN_BASEURL", None) jellyfin_token = os.getenv("JELLYFIN_TOKEN", None) @@ -337,16 +495,23 @@ def generate_server_connections(): 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") + 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()))) + servers.append( + ( + "jellyfin", + Jellyfin(baseurl=baseurl.strip(), token=jellyfin_token[i].strip()), + ) + ) return servers def main_loop(): - logfile = os.getenv("LOGFILE","log.log") + logfile = os.getenv("LOGFILE", "log.log") # Delete logfile if it exists if os.path.exists(logfile): os.remove(logfile) @@ -373,7 +538,23 @@ def main_loop(): 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) + ( + 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) @@ -385,19 +566,37 @@ def main_loop(): break # Start server_2 at the next server in the list - for server_2 in servers[servers.index(server_1) + 1:]: + 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 logger("Creating users list", 1) - server_1_users, server_2_users = setup_users(server_1, server_2, blacklist_users, whitelist_users, user_mapping) + server_1_users, server_2_users = setup_users( + server_1, server_2, blacklist_users, whitelist_users, user_mapping + ) logger("Creating watched lists", 1) - server_1_watched = server_1_connection.get_watched(server_1_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, + ) logger("Finished creating watched list server 1", 1) - server_2_watched = asyncio.run(server_2_connection.get_watched(server_2_users, blacklist_library, whitelist_library, blacklist_library_type, whitelist_library_type, library_mapping)) + server_2_watched = asyncio.run( + server_2_connection.get_watched( + server_2_users, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping, + ) + ) logger("Finished creating watched list server 2", 1) logger(f"Server 1 watched: {server_1_watched}", 3) logger(f"Server 2 watched: {server_2_watched}", 3) @@ -407,21 +606,38 @@ def main_loop(): 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) + 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) + 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) + 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, + ) + + server_1_connection.update_watched( + server_2_watched_filtered, user_mapping, library_mapping, dryrun + ) + asyncio.run( + server_2_connection.update_watched( + server_1_watched_filtered, user_mapping, library_mapping, dryrun + ) + ) - server_1_connection.update_watched(server_2_watched_filtered, user_mapping, library_mapping, dryrun) - asyncio.run(server_2_connection.update_watched(server_1_watched_filtered, user_mapping, library_mapping, dryrun)) def main(): sleep_duration = float(os.getenv("SLEEP_DURATION", "3600")) times = [] - while(True): + while True: try: start = perf_counter() main_loop() @@ -438,7 +654,6 @@ def main(): else: logger(error, log_type=2) - logger(traceback.format_exc(), 2) logger(f"Retrying in {sleep_duration}", log_type=0) sleep(sleep_duration) diff --git a/src/plex.py b/src/plex.py index 6804fd9..4babaff 100644 --- a/src/plex.py +++ b/src/plex.py @@ -3,12 +3,26 @@ import re, requests 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 +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, ssl_bypass=False): + def __init__( + self, + baseurl=None, + token=None, + username=None, + password=None, + servername=None, + ssl_bypass=False, + ): self.baseurl = baseurl self.token = token self.username = username @@ -21,13 +35,13 @@ class Plex: def login(self, ssl_bypass=False): try: if self.baseurl and self.token: - # Login via token - if ssl_bypass: - session = requests.Session() - session.verify = False - plex = PlexServer(self.baseurl, self.token, session=session) - else: - plex = PlexServer(self.baseurl, self.token) + # Login via token + if ssl_bypass: + session = requests.Session() + session.verify = False + plex = PlexServer(self.baseurl, self.token, session=session) + else: + plex = PlexServer(self.baseurl, self.token) elif self.username and self.password and self.servername: # Login via plex account account = MyPlexAccount(self.username, self.password) @@ -44,7 +58,6 @@ class Plex: logger(f"Plex: Failed to login, Error: {e}", 2) raise Exception(e) - def get_users(self): try: users = self.plex.myPlexAccount().users() @@ -57,14 +70,16 @@ class Plex: logger(f"Plex: Failed to get users, Error: {e}", 2) raise Exception(e) - def get_user_watched(self, user, user_plex, library): 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) + logger( + f"Plex: Generating watched for {user_name} in library {library.title}", + 0, + ) if library.type == "movie": user_watched[user_name][library.title] = [] @@ -73,12 +88,14 @@ class Plex: 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) + guid_source = re.search(r"(.*)://", guid.id).group(1).lower() + guid_id = re.search(r"://(.*)", guid.id).group(1) movie_guids[guid_source] = guid_id movie_guids["title"] = video.title - movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations]) + movie_guids["locations"] = tuple( + [x.split("/")[-1] for x in video.locations] + ) user_watched[user_name][library.title].append(movie_guids) @@ -90,12 +107,16 @@ class Plex: show_guids = {} for show_guid in show.guids: # 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_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["locations"] = tuple( + [x.split("/")[-1] for x in show.locations] + ) show_guids = frozenset(show_guids.items()) for season in show.seasons(): @@ -105,29 +126,51 @@ class Plex: 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) + 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_temp["locations"] = tuple([x.split("/")[-1] for x in episode.locations]) + episode_guids_temp["locations"] = tuple( + [x.split("/")[-1] for x in episode.locations] + ) 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 - + 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) + 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): + def get_watched( + self, + users, + blacklist_library, + whitelist_library, + blacklist_library_type, + whitelist_library_type, + library_mapping, + ): try: # Get all libraries users_watched = {} @@ -137,7 +180,9 @@ class Plex: if self.admin_user == user: user_plex = self.plex else: - user_plex = PlexServer(self.plex._baseurl, user.get_token(self.plex.machineIdentifier)) + user_plex = PlexServer( + self.plex._baseurl, user.get_token(self.plex.machineIdentifier) + ) libraries = user_plex.library.sections() @@ -145,10 +190,20 @@ class Plex: 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) + logger( + f"Plex: Skipping library {library_title} {skip_reason}", 1 + ) continue args.append([self.get_user_watched, user, user_plex, library]) @@ -164,30 +219,46 @@ class Plex: logger(f"Plex: Failed to get watched, Error: {e}", 2) raise Exception(e) - def update_user_watched(self, user, user_plex, library, videos, dryrun): try: logger(f"Plex: Updating watched for {user.title} in library {library}", 1) - videos_shows_ids, videos_episodes_ids, videos_movies_ids = generate_library_guids_dict(videos) - logger(f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", 1) + ( + videos_shows_ids, + videos_episodes_ids, + videos_movies_ids, + ) = generate_library_guids_dict(videos) + logger( + f"Plex: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", + 1, + ) library_videos = user_plex.library.section(library) if videos_movies_ids: 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"]: + 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) + 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 ( + movie_guid_id + in videos_movies_ids[movie_guid_source] + ): movie_found = True break @@ -199,21 +270,28 @@ class Plex: else: logger(f"Dryrun {msg}", 0) else: - logger(f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", 1) - + logger( + f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", + 1, + ) if videos_shows_ids and videos_episodes_ids: for show_search in library_videos.search(unwatched=True): show_found = False for show_location in show_search.locations: - if show_location.split("/")[-1] in videos_shows_ids["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) + 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(): @@ -226,18 +304,33 @@ class Plex: episode_found = False for episode_location in episode_search.locations: - if episode_location.split("/")[-1] in videos_episodes_ids["locations"]: + if ( + episode_location.split("/")[-1] + in videos_episodes_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) + 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_episodes_ids exactly, then the episode is in the list - if episode_guid_source in videos_episodes_ids.keys(): - if episode_guid_id in videos_episodes_ids[episode_guid_source]: + if ( + episode_guid_source + in videos_episodes_ids.keys() + ): + if ( + episode_guid_id + in videos_episodes_ids[episode_guid_source] + ): episode_found = True break @@ -249,19 +342,36 @@ class Plex: else: logger(f"Dryrun {msg}", 0) else: - logger(f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", 1) + logger( + f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", + 1, + ) else: - logger(f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", 1) + logger( + f"Plex: Skipping show {show_search.title} as it is not in mark list for {user.title}", + 1, + ) - if not videos_movies_ids and not videos_shows_ids and not videos_episodes_ids: - logger(f"Jellyfin: No videos to mark as watched for {user.title} in library {library}", 1) + if ( + not videos_movies_ids + and not videos_shows_ids + and not videos_episodes_ids + ): + logger( + f"Jellyfin: No videos to mark as watched for {user.title} in library {library}", + 1, + ) except Exception as e: - logger(f"Plex: Failed to update watched for {user.title} in library {library}, Error: {e}", 2) + 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): + def update_watched( + self, watched_list, user_mapping=None, library_mapping=None, dryrun=False + ): try: args = [] @@ -285,7 +395,9 @@ class Plex: if self.admin_user == user: user_plex = self.plex else: - user_plex = PlexServer(self.plex._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 @@ -299,18 +411,36 @@ class Plex: 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) + 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) + 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) + logger( + f"Plex: Library {library} not found in library list", 2 + ) continue - - args.append([self.update_user_watched, user, user_plex, library, videos, dryrun]) + args.append( + [ + self.update_user_watched, + user, + user_plex, + library, + videos, + dryrun, + ] + ) future_thread_executor(args) except Exception as e: diff --git a/test/test_main_.py b/test/test_main_.py index 30d8ab0..fbe990b 100644 --- a/test/test_main_.py +++ b/test/test_main_.py @@ -15,33 +15,64 @@ 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' + 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) + ( + 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'] + 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" } + 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) + ( + 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'] + 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 index 4e1bd7d..2625d49 100644 --- a/test/test_main_cleanup_watched.py +++ b/test/test_main_cleanup_watched.py @@ -16,86 +16,201 @@ 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")}): { + 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',)} + { + "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',)} + {"locations": ("Test S01E01.mkv",)}, + {"locations": ("Test S01E02.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',)}, + { + "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")}): { + 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",)} + { + "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',)} + {"locations": ("Test S01E02.mkv",)}, + {"locations": ("Test S01E03.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',)} + { + "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")}): { + 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": "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")}): { + 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": "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',)} - ] - } + "Season 1": [{"locations": ("Test S01E01.mkv",)}] + }, } expected_movie_watched_list_1 = [ - {"tmdbcollection":"448150", "imdb":"tt1431045", "tmdb":"293660", 'title': 'Deadpool', 'locations': ('Deadpool (2016) Remux-1080p.mkv',)} + { + "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")}): { + 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",)} + { + "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',)} - ] - } + "Season 1": [{"locations": ("Test S01E03.mkv",)}] + }, } expected_movie_watched_list_2 = [ - {'imdb': 'tt0384793', 'tmdb': '9788', 'tvdb': '9103', 'title': 'Accepted', 'locations': ('Accepted (2006) Remux-1080p.mkv',)} + { + "imdb": "tt0384793", + "tmdb": "9788", + "tvdb": "9103", + "title": "Accepted", + "locations": ("Accepted (2006) Remux-1080p.mkv",), + } ] @@ -104,28 +219,28 @@ def test_simple_cleanup_watched(): "user1": { "TV Shows": tv_shows_watched_list_1, "Movies": movies_watched_list_1, - "Other Shows": tv_shows_2_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 + "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 + "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 + "TV Shows": expected_tv_show_watched_list_2, + "Movies": expected_movie_watched_list_2, } } @@ -141,36 +256,46 @@ def test_mapping_cleanup_watched(): "user1": { "TV Shows": tv_shows_watched_list_1, "Movies": movies_watched_list_1, - "Other Shows": tv_shows_2_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 + "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 + "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 + "Shows": expected_tv_show_watched_list_2, + "Movies": expected_movie_watched_list_2, } } - user_mapping = { "user1": "user2" } - library_mapping = { "TV Shows": "Shows" } + 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) + 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