diff --git a/src/jellyfin.py b/src/jellyfin.py index 9cc7b38..c938b11 100644 --- a/src/jellyfin.py +++ b/src/jellyfin.py @@ -1,4 +1,5 @@ import asyncio, aiohttp, traceback +from math import floor from src.functions import ( logger, @@ -13,6 +14,56 @@ from src.watched import ( ) +def get_movie_guids(movie): + if "ProviderIds" in movie: + logger( + f"Jellyfin: {movie['Name']} {movie['ProviderIds']} {movie['MediaSources']}", + 3, + ) + else: + logger( + f"Jellyfin: {movie['Name']} {movie['MediaSources']['Path']}", + 3, + ) + + # Create a dictionary for the movie with its title + movie_guids = {"title": movie["Name"]} + + # If the movie has provider IDs, add them to the dictionary + if "ProviderIds" in movie: + movie_guids.update({k.lower(): v for k, v in movie["ProviderIds"].items()}) + + # If the movie has media sources, add them to the dictionary + if "MediaSources" in movie: + movie_guids["locations"] = tuple( + [x["Path"].split("/")[-1] for x in movie["MediaSources"]] + ) + + movie_guids["status"] = { + "completed": movie["UserData"]["Played"], + # Convert ticks to milliseconds to match Plex + "time": floor(movie["UserData"]["PlaybackPositionTicks"] / 10000), + } + + return movie_guids + + +def get_episode_guids(episode): + # Create a dictionary for the episode with its provider IDs and media sources + episode_dict = {k.lower(): v for k, v in episode["ProviderIds"].items()} + episode_dict["title"] = episode["Name"] + episode_dict["locations"] = tuple( + [x["Path"].split("/")[-1] for x in episode["MediaSources"]] + ) + + episode_dict["status"] = { + "completed": episode["UserData"]["Played"], + "time": floor(episode["UserData"]["PlaybackPositionTicks"] / 10000), + } + + return episode_dict + + class Jellyfin: def __init__(self, baseurl, token): self.baseurl = baseurl @@ -114,48 +165,43 @@ class Jellyfin: session, ) + in_progress = await self.query( + f"/Users/{user_id}/Items" + + f"?ParentId={library_id}&Filters=IsResumable&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources", + "get", + session, + ) + for movie in watched["Items"]: - # Check if the movie has been played - if ( - movie["UserData"]["Played"] is True - and "MediaSources" in movie - and movie["MediaSources"] is not {} - ): + if "MediaSources" in movie and movie["MediaSources"] is not {}: logger( f"Jellyfin: Adding {movie['Name']} to {user_name} watched list", 3, ) - if "ProviderIds" in movie: - logger( - f"Jellyfin: {movie['Name']} {movie['ProviderIds']} {movie['MediaSources']}", - 3, - ) - else: - logger( - f"Jellyfin: {movie['Name']} {movie['MediaSources']['Path']}", - 3, - ) - # Create a dictionary for the movie with its title - movie_guids = {"title": movie["Name"]} + # Get the movie's GUIDs + movie_guids = get_movie_guids(movie) - # If the movie has provider IDs, add them to the dictionary - if "ProviderIds" in movie: - movie_guids.update( - { - k.lower(): v - for k, v in movie["ProviderIds"].items() - } - ) + # Append the movie dictionary to the list for the given user and library + user_watched[user_name][library_title].append(movie_guids) + logger( + f"Jellyfin: Added {movie_guids} to {user_name} watched list", + 3, + ) - # If the movie has media sources, add them to the dictionary - if "MediaSources" in movie: - movie_guids["locations"] = tuple( - [ - x["Path"].split("/")[-1] - for x in movie["MediaSources"] - ] - ) + # Get all partially watched movies greater than 1 minute + for movie in in_progress["Items"]: + if "MediaSources" in movie and movie["MediaSources"] is not {}: + if movie["UserData"]["PlaybackPositionTicks"] < 600000000: + continue + + logger( + f"Jellyfin: Adding {movie['Name']} to {user_name} watched list", + 3, + ) + + # Get the movie's GUIDs + movie_guids = get_movie_guids(movie) # Append the movie dictionary to the list for the given user and library user_watched[user_name][library_title].append(movie_guids) @@ -244,16 +290,26 @@ class Jellyfin: season_identifiers = dict(seasons["Identifiers"]) season_identifiers["season_id"] = season["Id"] season_identifiers["season_name"] = season["Name"] - episode_task = asyncio.ensure_future( + watched_task = asyncio.ensure_future( self.query( f"/Shows/{season_identifiers['show_id']}/Episodes" - + f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&isPlayed=true&Fields=ProviderIds,MediaSources", + + f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsPlayed&Fields=ProviderIds,MediaSources", "get", session, frozenset(season_identifiers.items()), ) ) - episodes_tasks.append(episode_task) + in_progress_task = asyncio.ensure_future( + self.query( + f"/Shows/{season_identifiers['show_id']}/Episodes" + + f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsResumable&Fields=ProviderIds,MediaSources", + "get", + session, + frozenset(season_identifiers.items()), + ) + ) + episodes_tasks.append(watched_task) + episodes_tasks.append(in_progress_task) # Retrieve the episodes for each watched season watched_episodes = await asyncio.gather(*episodes_tasks) @@ -268,24 +324,19 @@ class Jellyfin: season_dict["Episodes"] = [] for episode in episodes["Items"]: if ( - episode["UserData"]["Played"] is True - and "MediaSources" in episode + "MediaSources" in episode and episode["MediaSources"] is not {} ): - # Create a dictionary for the episode with its provider IDs and media sources - episode_dict = { - k.lower(): v - for k, v in episode["ProviderIds"].items() - } - episode_dict["title"] = episode["Name"] - episode_dict["locations"] = tuple( - [ - x["Path"].split("/")[-1] - for x in episode["MediaSources"] - ] - ) - # Add the episode dictionary to the season's list of episodes - season_dict["Episodes"].append(episode_dict) + # If watched or watched more than a minute + if ( + episode["UserData"]["Played"] == True + or episode["UserData"]["PlaybackPositionTicks"] + > 600000000 + ): + episode_dict = get_episode_guids(episode) + # Add the episode dictionary to the season's list of episodes + season_dict["Episodes"].append(episode_dict) + # Add the season dictionary to the show's list of seasons if ( season_dict["Identifiers"]["show_guids"] @@ -498,7 +549,7 @@ class Jellyfin: session, ) for jellyfin_video in jellyfin_search["Items"]: - movie_found = False + movie_status = None if "MediaSources" in jellyfin_video: for movie_location in jellyfin_video["MediaSources"]: @@ -506,10 +557,16 @@ class Jellyfin: movie_location["Path"].split("/")[-1] in videos_movies_ids["locations"] ): - movie_found = True + for video in videos: + if ( + movie_location["Path"].split("/")[-1] + in video["locations"] + ): + movie_status = video["status"] + break break - if not movie_found: + if not movie_status: for ( movie_provider_source, movie_provider_id, @@ -521,21 +578,38 @@ class Jellyfin: movie_provider_source.lower() ] ): - movie_found = True + for video in videos: + if ( + movie_provider_id.lower() + in video["ids"][ + movie_provider_source.lower() + ] + ): + movie_status = video["status"] + break 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, - ) + if movie_status: + if movie_status["completed"]: + 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) else: - logger(f"Dryrun {msg}", 0) + # TODO add support for partially watched movies + jellyfin_video_id = jellyfin_video["Id"] + msg = f"{jellyfin_video['Name']} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" + if not dryrun: + logger(f"Marking {msg}", 0) + else: + logger(f"Dryrun {msg}", 0) else: logger( f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", @@ -562,6 +636,16 @@ class Jellyfin: in videos_shows_ids["locations"] ): show_found = True + episode_videos = [] + for show, seasons in videos.items(): + show = {k: v for k, v in show} + if ( + jellyfin_show["Path"].split("/")[-1] + in show["locations"] + ): + for season in seasons.values(): + for episode in season: + episode_videos.append(episode) if not show_found: for show_provider_source, show_provider_id in jellyfin_show[ @@ -575,7 +659,18 @@ class Jellyfin: ] ): show_found = True - break + episode_videos = [] + for show, seasons in videos.items(): + show = {k: v for k, v in show} + if ( + show_provider_id.lower() + in show["ids"][ + show_provider_source.lower() + ] + ): + for season in seasons.values(): + for episode in season: + episode_videos.append(episode) if show_found: logger( @@ -591,7 +686,7 @@ class Jellyfin: ) for jellyfin_episode in jellyfin_episodes["Items"]: - episode_found = False + episode_status = None if "MediaSources" in jellyfin_episode: for episode_location in jellyfin_episode[ @@ -601,10 +696,18 @@ class Jellyfin: episode_location["Path"].split("/")[-1] in videos_episodes_ids["locations"] ): - episode_found = True + for episode in episode_videos: + if ( + episode_location["Path"].split("/")[ + -1 + ] + in episode["locations"] + ): + episode_status = episode["status"] + break break - if not episode_found: + if not episode_status: for ( episode_provider_source, episode_provider_id, @@ -619,24 +722,46 @@ class Jellyfin: episode_provider_source.lower() ] ): - episode_found = True + for episode in episode_videos: + if ( + episode_provider_id.lower() + in episode["ids"][ + episode_provider_source.lower() + ] + ): + episode_status = episode[ + "status" + ] + break break - if episode_found: - jellyfin_episode_id = jellyfin_episode["Id"] - msg = ( - f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['Name']}" - + f" 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, + if episode_status: + if episode_status["completed"]: + jellyfin_episode_id = jellyfin_episode["Id"] + msg = ( + f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']}" + + f" 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, + ) + else: + logger(f"Dryrun {msg}", 0) else: - logger(f"Dryrun {msg}", 0) + # TODO add support for partially watched episodes + jellyfin_episode_id = jellyfin_episode["Id"] + msg = ( + f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['IndexNumber']} {jellyfin_episode['Name']}" + + f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library} for Jellyfin" + ) + if not dryrun: + logger(f"Marked {msg}", 0) + 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}", @@ -663,6 +788,7 @@ class Jellyfin: f"Jellyfin: Error updating watched for {user_name} in library {library}, {e}", 2, ) + logger(traceback.format_exc(), 2) raise Exception(e) async def update_watched( diff --git a/src/library.py b/src/library.py index 65cb3e3..36d5060 100644 --- a/src/library.py +++ b/src/library.py @@ -163,13 +163,25 @@ def episode_title_dict(user_list: dict): for season in user_list[show]: for episode in user_list[show][season]: for episode_key, episode_value in episode.items(): - if episode_key.lower() not in episode_output_dict: - episode_output_dict[episode_key.lower()] = [] + if episode_key != "status": + if episode_key.lower() not in episode_output_dict: + episode_output_dict[episode_key.lower()] = [] + + if "completed" not in episode_output_dict: + episode_output_dict["completed"] = [] + if "time" not in episode_output_dict: + episode_output_dict["time"] = [] + if episode_key == "locations": for episode_location in episode_value: episode_output_dict[episode_key.lower()].append( episode_location ) + elif episode_key == "status": + episode_output_dict["completed"].append( + episode_value["completed"] + ) + episode_output_dict["time"].append(episode_value["time"]) else: episode_output_dict[episode_key.lower()].append( episode_value.lower() @@ -186,11 +198,21 @@ def movies_title_dict(user_list: dict): movies_output_dict = {} 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()] = [] + if movie_key != "status": + if movie_key.lower() not in movies_output_dict: + movies_output_dict[movie_key.lower()] = [] + + if "completed" not in movies_output_dict: + movies_output_dict["completed"] = [] + if "time" not in movies_output_dict: + movies_output_dict["time"] = [] + if movie_key == "locations": for movie_location in movie_value: movies_output_dict[movie_key.lower()].append(movie_location) + elif movie_key == "status": + movies_output_dict["completed"].append(movie_value["completed"]) + movies_output_dict["time"].append(movie_value["time"]) else: movies_output_dict[movie_key.lower()].append(movie_value.lower()) diff --git a/src/plex.py b/src/plex.py index f902742..0f1e8f0 100644 --- a/src/plex.py +++ b/src/plex.py @@ -1,5 +1,6 @@ import re, requests, os, traceback from urllib3.poolmanager import PoolManager +from math import floor from plexapi.server import PlexServer from plexapi.myplex import MyPlexAccount @@ -27,14 +28,70 @@ class HostNameIgnoringAdapter(requests.adapters.HTTPAdapter): ) +def get_movie_guids(video, completed=True): + logger(f"Plex: {video.title} {video.guids} {video.locations}", 3) + + movie_guids = {} + try: + for guid in video.guids: + # Extract source and id from guid.id + m = re.match(r"(.*)://(.*)", guid.id) + guid_source, guid_id = m.group(1).lower(), m.group(2) + movie_guids[guid_source] = guid_id + except Exception: + logger(f"Plex: Failed to get guids for {video.title}, Using location only", 1) + + movie_guids["title"] = video.title + movie_guids["locations"] = tuple([x.split("/")[-1] for x in video.locations]) + + movie_guids["status"] = { + "completed": completed, + "time": video.viewOffset, + } + + return movie_guids + + +def get_episode_guids(episode, show, completed=True): + episode_guids_temp = {} + try: + for guid in episode.guids: + # Extract after :// from guid.id + m = re.match(r"(.*)://(.*)", guid.id) + guid_source, guid_id = m.group(1).lower(), m.group(2) + episode_guids_temp[guid_source] = guid_id + except Exception: + logger( + f"Plex: Failed to get guids for {episode.title} in {show.title}, Using location only", + 1, + ) + + episode_guids_temp["title"] = episode.title + episode_guids_temp["locations"] = tuple( + [x.split("/")[-1] for x in episode.locations] + ) + + episode_guids_temp["status"] = { + "completed": completed, + "time": episode.viewOffset, + } + + return episode_guids_temp + + def get_user_library_watched_show(show): try: show_guids = {} - for show_guid in show.guids: - # Extract source and id from guid.id - m = re.match(r"(.*)://(.*)", show_guid.id) - show_guid_source, show_guid_id = m.group(1).lower(), m.group(2) - show_guids[show_guid_source] = show_guid_id + try: + for show_guid in show.guids: + # Extract source and id from guid.id + m = re.match(r"(.*)://(.*)", show_guid.id) + show_guid_source, show_guid_id = m.group(1).lower(), m.group(2) + show_guids[show_guid_source] = show_guid_id + except Exception: + logger( + f"Plex: Failed to get guids for {show.title}, Using location only", 1 + ) show_guids["title"] = show.title show_guids["locations"] = tuple([x.split("/")[-1] for x in show.locations]) @@ -42,30 +99,23 @@ def get_user_library_watched_show(show): # Get all watched episodes for show episode_guids = {} - watched_episodes = show.watched() - for episode in watched_episodes: - episode_guids_temp = {} - try: - if len(episode.guids) > 0: - for guid in episode.guids: - # Extract after :// from guid.id - m = re.match(r"(.*)://(.*)", guid.id) - guid_source, guid_id = m.group(1).lower(), m.group(2) - episode_guids_temp[guid_source] = guid_id - except Exception: - logger( - f"Plex: Failed to get guids for {episode.title} in {show.title}, Using location only", - 1, + watched = show.watched() + + for episode in show.episodes(): + if episode in watched: + if episode.parentTitle not in episode_guids: + episode_guids[episode.parentTitle] = [] + + episode_guids[episode.parentTitle].append( + get_episode_guids(episode, show, completed=True) ) + elif episode.viewOffset > 0: + if episode.parentTitle not in episode_guids: + episode_guids[episode.parentTitle] = [] - episode_guids_temp["locations"] = tuple( - [x.split("/")[-1] for x in episode.locations] - ) - - if episode.parentTitle not in episode_guids: - episode_guids[episode.parentTitle] = [] - - episode_guids[episode.parentTitle].append(episode_guids_temp) + episode_guids[episode.parentTitle].append( + get_episode_guids(episode, show, completed=False) + ) return show_guids, episode_guids @@ -89,32 +139,37 @@ def get_user_library_watched(user, user_plex, library): if library.type == "movie": user_watched[user_name][library.title] = [] + # Get all watched movies for video in library_videos.search(unwatched=False): logger(f"Plex: Adding {video.title} to {user_name} watched list", 3) - logger(f"Plex: {video.title} {video.guids} {video.locations}", 3) - movie_guids = {} - for guid in video.guids: - # Extract source and id from guid.id - m = re.match(r"(.*)://(.*)", guid.id) - guid_source, guid_id = m.group(1).lower(), m.group(2) - 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 = get_movie_guids(video, completed=True) + + user_watched[user_name][library.title].append(movie_guids) + + # Get all partially watched movies greater than 1 minute + for video in library_videos.search(inProgress=True): + if video.viewOffset < 60000: + continue + + logger(f"Plex: Adding {video.title} to {user_name} watched list", 3) + + movie_guids = get_movie_guids(video, completed=False) user_watched[user_name][library.title].append(movie_guids) - logger(f"Plex: Added {movie_guids} to {user_name} watched list", 3) elif library.type == "show": user_watched[user_name][library.title] = {} - shows = library_videos.search(unwatched=False) # Parallelize show processing args = [] - for show in shows: + + # Get all watched shows + for show in library_videos.search(unwatched=False): + args.append([get_user_library_watched_show, show]) + + # Get all partially watched shows + for show in library_videos.search(inProgress=True): args.append([get_user_library_watched_show, show]) for show_guids, episode_guids in future_thread_executor( @@ -144,11 +199,20 @@ def get_user_library_watched(user, user_plex, library): return {} -def find_video(plex_search, video_ids): +def find_video(plex_search, video_ids, videos=None): try: for location in plex_search.locations: if location.split("/")[-1] in video_ids["locations"]: - return True + episode_videos = [] + if videos: + for show, seasons in videos.items(): + show = {k: v for k, v in show} + if location.split("/")[-1] in show["locations"]: + for season in seasons.values(): + for episode in season: + episode_videos.append(episode) + + return True, episode_videos for guid in plex_search.guids: guid_source = re.search(r"(.*)://", guid.id).group(1).lower() @@ -157,11 +221,46 @@ def find_video(plex_search, video_ids): # If show provider source and show provider id are in videos_shows_ids exactly, then the show is in the list if guid_source in video_ids.keys(): if guid_id in video_ids[guid_source]: - return True + episode_videos = [] + if videos: + for show, seasons in videos.items(): + show = {k: v for k, v in show} + if guid_source in show["ids"].keys(): + if guid_id in show["ids"][guid_source]: + for season in seasons: + for episode in season: + episode_videos.append(episode) - return False + return True, episode_videos + + return False, [] except Exception: - return False + return False, [] + + +def get_video_status(plex_search, video_ids, videos): + try: + for location in plex_search.locations: + if location.split("/")[-1] in video_ids["locations"]: + for video in videos: + if location.split("/")[-1] in video["locations"]: + return video["status"] + + for guid in plex_search.guids: + guid_source = re.search(r"(.*)://", guid.id).group(1).lower() + guid_id = re.search(r"://(.*)", 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 guid_source in video_ids.keys(): + if guid_id in video_ids[guid_source]: + for video in videos: + if guid_source in video["ids"].keys(): + if guid_id in video["ids"][guid_source]: + return video["status"] + + return None + except Exception: + return None def update_user_watched(user, user_plex, library, videos, dryrun): @@ -180,13 +279,24 @@ def update_user_watched(user, user_plex, library, videos, dryrun): library_videos = user_plex.library.section(library) if videos_movies_ids: for movies_search in library_videos.search(unwatched=True): - if find_video(movies_search, videos_movies_ids): - 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) + video_status = get_video_status( + movies_search, videos_movies_ids, videos + ) + if video_status: + if video_status["completed"]: + 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 video_status["time"] > 60_000: + msg = f"{movies_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library} for Plex" + if not dryrun: + logger(f"Marked {msg}", 0) + movies_search.updateProgress(video_status["time"]) + 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}", @@ -195,15 +305,29 @@ def update_user_watched(user, user_plex, library, videos, dryrun): if videos_shows_ids and videos_episodes_ids: for show_search in library_videos.search(unwatched=True): - if find_video(show_search, videos_shows_ids): + show_found, episode_videos = find_video( + show_search, videos_shows_ids, videos + ) + if show_found: for episode_search in show_search.episodes(): - if find_video(episode_search, videos_episodes_ids): - 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() + video_status = get_video_status( + episode_search, videos_episodes_ids, episode_videos + ) + if video_status: + if video_status["completed"]: + 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) else: - logger(f"Dryrun {msg}", 0) + msg = f"{show_search.title} {episode_search.title} as partially watched for {floor(video_status['time'] / 60_000)} minutes for {user.title} in {library} for Plex" + if not dryrun: + logger(f"Marked {msg}", 0) + episode_search.updateProgress(video_status["time"]) + 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}", diff --git a/src/watched.py b/src/watched.py index 1cd3a03..28d2cc8 100644 --- a/src/watched.py +++ b/src/watched.py @@ -29,6 +29,48 @@ def combine_watched_dicts(dicts: list): return combined_dict +def check_remove_entry(video, library, video_index, library_watched_list_2): + if video_index is not None: + if ( + library_watched_list_2["completed"][video_index] + == video["status"]["completed"] + ) and (library_watched_list_2["time"][video_index] == video["status"]["time"]): + logger( + f"Removing {video['title']} from {library} due to exact match", + 3, + ) + return True + elif ( + library_watched_list_2["completed"][video_index] == True + and video["status"]["completed"] == False + ): + logger( + f"Removing {video['title']} from {library} due to being complete in one library and not the other", + 3, + ) + return True + elif ( + library_watched_list_2["completed"][video_index] == False + and video["status"]["completed"] == False + ) and (video["status"]["time"] < library_watched_list_2["time"][video_index]): + logger( + f"Removing {video['title']} from {library} due to more time watched in one library than the other", + 3, + ) + return True + elif ( + library_watched_list_2["completed"][video_index] == True + and video["status"]["completed"] == True + ): + logger( + f"Removing {video['title']} from {library} due to being complete in both libraries", + 3, + ) + return True + + return False + + def cleanup_watched( watched_list_1, watched_list_2, user_mapping=None, library_mapping=None ): @@ -60,9 +102,17 @@ def cleanup_watched( # Movies if isinstance(watched_list_1[user_1][library_1], list): for movie in watched_list_1[user_1][library_1]: - if is_movie_in_dict(movie, movies_watched_list_2_keys_dict): - logger(f"Removing {movie} from {library_1}", 3) - modified_watched_list_1[user_1][library_1].remove(movie) + movie_index = get_movie_index_in_dict( + movie, movies_watched_list_2_keys_dict + ) + if movie_index is not None: + if check_remove_entry( + movie, + library_1, + movie_index, + movies_watched_list_2_keys_dict, + ): + modified_watched_list_1[user_1][library_1].remove(movie) # TV Shows elif isinstance(watched_list_1[user_1][library_1], dict): @@ -72,19 +122,16 @@ def cleanup_watched( for episode in watched_list_1[user_1][library_1][show_key_1][ season ]: - if is_episode_in_dict( + episode_index = get_episode_index_in_dict( episode, episode_watched_list_2_keys_dict - ): - if ( - episode - in modified_watched_list_1[user_1][library_1][ - show_key_1 - ][season] + ) + if episode_index is not None: + if check_remove_entry( + episode, + library_1, + episode_index, + episode_watched_list_2_keys_dict, ): - logger( - f"Removing {episode} from {show_key_dict['title']}", - 3, - ) modified_watched_list_1[user_1][library_1][ show_key_1 ][season].remove(episode) @@ -148,7 +195,7 @@ def get_other(watched_list, object_1, object_2): return None -def is_movie_in_dict(movie, movies_watched_list_2_keys_dict): +def get_movie_index_in_dict(movie, movies_watched_list_2_keys_dict): # Iterate through the keys and values of the movie dictionary for movie_key, movie_value in movie.items(): # If the key is "locations", check if the "locations" key is present in the movies_watched_list_2_keys_dict dictionary @@ -156,21 +203,24 @@ def is_movie_in_dict(movie, movies_watched_list_2_keys_dict): if "locations" in movies_watched_list_2_keys_dict.keys(): # Iterate through the locations in the movie dictionary for location in movie_value: - # If the location is in the movies_watched_list_2_keys_dict dictionary, return True + # If the location is in the movies_watched_list_2_keys_dict dictionary, return index of the key if location in movies_watched_list_2_keys_dict["locations"]: - return True + return movies_watched_list_2_keys_dict["locations"].index( + location + ) + # If the key is not "locations", check if the movie_key is present in the movies_watched_list_2_keys_dict dictionary else: if movie_key in movies_watched_list_2_keys_dict.keys(): # If the movie_value is in the movies_watched_list_2_keys_dict dictionary, return True if movie_value in movies_watched_list_2_keys_dict[movie_key]: - return True + return movies_watched_list_2_keys_dict[movie_key].index(movie_value) # If the loop completes without finding a match, return False - return False + return None -def is_episode_in_dict(episode, episode_watched_list_2_keys_dict): +def get_episode_index_in_dict(episode, episode_watched_list_2_keys_dict): # Iterate through the keys and values of the episode dictionary for episode_key, episode_value in episode.items(): # If the key is "locations", check if the "locations" key is present in the episode_watched_list_2_keys_dict dictionary @@ -178,15 +228,19 @@ def is_episode_in_dict(episode, episode_watched_list_2_keys_dict): if "locations" in episode_watched_list_2_keys_dict.keys(): # Iterate through the locations in the episode dictionary for location in episode_value: - # If the location is in the episode_watched_list_2_keys_dict dictionary, return True + # If the location is in the episode_watched_list_2_keys_dict dictionary, return index of the key if location in episode_watched_list_2_keys_dict["locations"]: - return True + return episode_watched_list_2_keys_dict["locations"].index( + location + ) # If the key is not "locations", check if the episode_key is present in the episode_watched_list_2_keys_dict dictionary else: if episode_key in episode_watched_list_2_keys_dict.keys(): # If the episode_value is in the episode_watched_list_2_keys_dict dictionary, return True if episode_value in episode_watched_list_2_keys_dict[episode_key]: - return True + return episode_watched_list_2_keys_dict[episode_key].index( + episode_value + ) # If the loop completes without finding a match, return False - return False + return None diff --git a/test/test_library.py b/test/test_library.py index 2ffd1da..e9fd02e 100644 --- a/test/test_library.py +++ b/test/test_library.py @@ -51,6 +51,7 @@ show_list = { "locations": ( "The Last of Us - S01E01 - When You're Lost in the Darkness WEBDL-1080p.mkv", ), + "status": {"completed": True, "time": 0}, } ] } @@ -61,6 +62,7 @@ movie_list = [ "imdb": "tt2380307", "tmdb": "354912", "locations": ("Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv"), + "status": {"completed": True, "time": 0}, } ] @@ -77,12 +79,16 @@ episode_titles = { ], "tmdb": ["2181581"], "tvdb": ["8444132"], + "completed": [True], + "time": [0], } movie_titles = { "imdb": ["tt2380307"], "locations": ["Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv"], "title": ["coco"], "tmdb": ["354912"], + "completed": [True], + "time": [0], } diff --git a/test/test_watched.py b/test/test_watched.py index 8257457..105541a 100644 --- a/test/test_watched.py +++ b/test/test_watched.py @@ -30,42 +30,43 @@ tv_shows_watched_list_1 = { "imdb": "tt0550489", "tmdb": "282843", "tvdb": "176357", + "title": "Extreme Aggressor", "locations": ( "Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv", ), + "status": {"completed": True, "time": 0}, }, { "imdb": "tt0550487", "tmdb": "282861", "tvdb": "300385", + "title": "Compulsion", "locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",), + "status": {"completed": True, "time": 0}, }, ] }, frozenset({("title", "Test"), ("locations", ("Test",))}): { "Season 1": [ - {"locations": ("Test S01E01.mkv",)}, - {"locations": ("Test S01E02.mkv",)}, + { + "title": "S01E01", + "locations": ("Test S01E01.mkv",), + "status": {"completed": True, "time": 0}, + }, + { + "title": "S01E02", + "locations": ("Test S01E02.mkv",), + "status": {"completed": True, "time": 0}, + }, + { + "title": "S01E04", + "locations": ("Test S01E04.mkv",), + "status": {"completed": False, "time": 5}, + }, ] }, } -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( { @@ -81,32 +82,146 @@ tv_shows_watched_list_2 = { "imdb": "tt0550487", "tmdb": "282861", "tvdb": "300385", + "title": "Compulsion", "locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",), + "status": {"completed": True, "time": 0}, }, { "imdb": "tt0550498", "tmdb": "282865", "tvdb": "300474", + "title": "Won't Get Fooled Again", "locations": ( "Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv", ), + "status": {"completed": True, "time": 0}, }, ] }, frozenset({("title", "Test"), ("locations", ("Test",))}): { "Season 1": [ - {"locations": ("Test S01E02.mkv",)}, - {"locations": ("Test S01E03.mkv",)}, + { + "title": "S01E02", + "locations": ("Test S01E02.mkv",), + "status": {"completed": False, "time": 10}, + }, + { + "title": "S01E03", + "locations": ("Test S01E03.mkv",), + "status": {"completed": True, "time": 0}, + }, + { + "title": "S01E04", + "locations": ("Test S01E04.mkv",), + "status": {"completed": False, "time": 10}, + }, ] }, } +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", + "title": "Extreme Aggressor", + "locations": ( + "Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv", + ), + "status": {"completed": True, "time": 0}, + } + ] + }, + frozenset({("title", "Test"), ("locations", ("Test",))}): { + "Season 1": [ + { + "title": "S01E01", + "locations": ("Test S01E01.mkv",), + "status": {"completed": True, "time": 0}, + }, + { + "title": "S01E02", + "locations": ("Test S01E02.mkv",), + "status": {"completed": True, "time": 0}, + }, + ] + }, +} + +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", + "title": "Won't Get Fooled Again", + "locations": ( + "Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv", + ), + "status": {"completed": True, "time": 0}, + } + ] + }, + frozenset({("title", "Test"), ("locations", ("Test",))}): { + "Season 1": [ + { + "title": "S01E03", + "locations": ("Test S01E03.mkv",), + "status": {"completed": True, "time": 0}, + }, + { + "title": "S01E04", + "locations": ("Test S01E04.mkv",), + "status": {"completed": False, "time": 10}, + }, + ] + }, +} + +movies_watched_list_1 = [ + { + "imdb": "tt2380307", + "tmdb": "354912", + "title": "Coco", + "locations": ("Coco (2017) Remux-1080p.mkv",), + "status": {"completed": True, "time": 0}, + }, + { + "tmdbcollection": "448150", + "imdb": "tt1431045", + "tmdb": "293660", + "title": "Deadpool", + "locations": ("Deadpool (2016) Remux-1080p.mkv",), + "status": {"completed": True, "time": 0}, + }, +] + movies_watched_list_2 = [ { "imdb": "tt2380307", "tmdb": "354912", "title": "Coco", "locations": ("Coco (2017) Remux-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, { "imdb": "tt0384793", @@ -114,9 +229,33 @@ movies_watched_list_2 = [ "tvdb": "9103", "title": "Accepted", "locations": ("Accepted (2006) Remux-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, ] + +expected_movie_watched_list_1 = [ + { + "tmdbcollection": "448150", + "imdb": "tt1431045", + "tmdb": "293660", + "title": "Deadpool", + "locations": ("Deadpool (2016) Remux-1080p.mkv",), + "status": {"completed": True, "time": 0}, + } +] + +expected_movie_watched_list_2 = [ + { + "imdb": "tt0384793", + "tmdb": "9788", + "tvdb": "9103", + "title": "Accepted", + "locations": ("Accepted (2006) Remux-1080p.mkv",), + "status": {"completed": True, "time": 0}, + } +] + # Test to see if objects get deleted all the way up to the root. tv_shows_2_watched_list_1 = { frozenset( @@ -133,86 +272,16 @@ tv_shows_2_watched_list_1 = { "imdb": "tt0550489", "tmdb": "282843", "tvdb": "176357", + "title": "Extreme Aggressor", "locations": ( "Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv", ), + "status": {"completed": True, "time": 0}, }, ] } } -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", - ), - } - ] - }, - frozenset({("title", "Test"), ("locations", ("Test",))}): { - "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",), - } -] - -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", - ), - } - ] - }, - frozenset({("title", "Test"), ("locations", ("Test",))}): { - "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",), - } -] - def test_simple_cleanup_watched(): user_watched_list_1 = { @@ -311,18 +380,21 @@ def test_combine_watched_dicts(): "tmdb": "12429", "imdb": "tt0876563", "locations": ("Ponyo (2008) Bluray-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, { "title": "Spirited Away", "tmdb": "129", "imdb": "tt0245429", "locations": ("Spirited Away (2001) Bluray-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, { "title": "Castle in the Sky", "tmdb": "10515", "imdb": "tt0092067", "locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, ] } @@ -349,6 +421,7 @@ def test_combine_watched_dicts(): "locations": ( "11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv", ), + "status": {"completed": True, "time": 0}, } ] } @@ -365,18 +438,21 @@ def test_combine_watched_dicts(): "tmdb": "12429", "imdb": "tt0876563", "locations": ("Ponyo (2008) Bluray-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, { "title": "Spirited Away", "tmdb": "129", "imdb": "tt0245429", "locations": ("Spirited Away (2001) Bluray-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, { "title": "Castle in the Sky", "tmdb": "10515", "imdb": "tt0092067", "locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",), + "status": {"completed": True, "time": 0}, }, ], "Anime Shows": {}, @@ -399,6 +475,7 @@ def test_combine_watched_dicts(): "locations": ( "11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv", ), + "status": {"completed": True, "time": 0}, } ] }