Merge pull request #57 from luigi311/partial_watch

Partially implement in progress syncing
This commit is contained in:
Luigi311
2023-03-31 12:14:53 -06:00
committed by GitHub
6 changed files with 676 additions and 267 deletions

View File

@@ -1,4 +1,5 @@
import asyncio, aiohttp, traceback import asyncio, aiohttp, traceback
from math import floor
from src.functions import ( from src.functions import (
logger, 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: class Jellyfin:
def __init__(self, baseurl, token): def __init__(self, baseurl, token):
self.baseurl = baseurl self.baseurl = baseurl
@@ -114,48 +165,43 @@ class Jellyfin:
session, 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"]: for movie in watched["Items"]:
# Check if the movie has been played if "MediaSources" in movie and movie["MediaSources"] is not {}:
if (
movie["UserData"]["Played"] is True
and "MediaSources" in movie
and movie["MediaSources"] is not {}
):
logger( logger(
f"Jellyfin: Adding {movie['Name']} to {user_name} watched list", f"Jellyfin: Adding {movie['Name']} to {user_name} watched list",
3, 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 # Get the movie's GUIDs
movie_guids = {"title": movie["Name"]} movie_guids = get_movie_guids(movie)
# If the movie has provider IDs, add them to the dictionary # Append the movie dictionary to the list for the given user and library
if "ProviderIds" in movie: user_watched[user_name][library_title].append(movie_guids)
movie_guids.update( logger(
{ f"Jellyfin: Added {movie_guids} to {user_name} watched list",
k.lower(): v 3,
for k, v in movie["ProviderIds"].items() )
}
)
# If the movie has media sources, add them to the dictionary # Get all partially watched movies greater than 1 minute
if "MediaSources" in movie: for movie in in_progress["Items"]:
movie_guids["locations"] = tuple( if "MediaSources" in movie and movie["MediaSources"] is not {}:
[ if movie["UserData"]["PlaybackPositionTicks"] < 600000000:
x["Path"].split("/")[-1] continue
for x in movie["MediaSources"]
] 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 # Append the movie dictionary to the list for the given user and library
user_watched[user_name][library_title].append(movie_guids) user_watched[user_name][library_title].append(movie_guids)
@@ -244,16 +290,26 @@ class Jellyfin:
season_identifiers = dict(seasons["Identifiers"]) season_identifiers = dict(seasons["Identifiers"])
season_identifiers["season_id"] = season["Id"] season_identifiers["season_id"] = season["Id"]
season_identifiers["season_name"] = season["Name"] season_identifiers["season_name"] = season["Name"]
episode_task = asyncio.ensure_future( watched_task = asyncio.ensure_future(
self.query( self.query(
f"/Shows/{season_identifiers['show_id']}/Episodes" 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", "get",
session, session,
frozenset(season_identifiers.items()), 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 # Retrieve the episodes for each watched season
watched_episodes = await asyncio.gather(*episodes_tasks) watched_episodes = await asyncio.gather(*episodes_tasks)
@@ -268,24 +324,19 @@ class Jellyfin:
season_dict["Episodes"] = [] season_dict["Episodes"] = []
for episode in episodes["Items"]: for episode in episodes["Items"]:
if ( if (
episode["UserData"]["Played"] is True "MediaSources" in episode
and "MediaSources" in episode
and episode["MediaSources"] is not {} and episode["MediaSources"] is not {}
): ):
# Create a dictionary for the episode with its provider IDs and media sources # If watched or watched more than a minute
episode_dict = { if (
k.lower(): v episode["UserData"]["Played"] == True
for k, v in episode["ProviderIds"].items() or episode["UserData"]["PlaybackPositionTicks"]
} > 600000000
episode_dict["title"] = episode["Name"] ):
episode_dict["locations"] = tuple( episode_dict = get_episode_guids(episode)
[ # Add the episode dictionary to the season's list of episodes
x["Path"].split("/")[-1] season_dict["Episodes"].append(episode_dict)
for x in episode["MediaSources"]
]
)
# 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 # Add the season dictionary to the show's list of seasons
if ( if (
season_dict["Identifiers"]["show_guids"] season_dict["Identifiers"]["show_guids"]
@@ -498,7 +549,7 @@ class Jellyfin:
session, session,
) )
for jellyfin_video in jellyfin_search["Items"]: for jellyfin_video in jellyfin_search["Items"]:
movie_found = False movie_status = None
if "MediaSources" in jellyfin_video: if "MediaSources" in jellyfin_video:
for movie_location in jellyfin_video["MediaSources"]: for movie_location in jellyfin_video["MediaSources"]:
@@ -506,10 +557,16 @@ class Jellyfin:
movie_location["Path"].split("/")[-1] movie_location["Path"].split("/")[-1]
in videos_movies_ids["locations"] 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 break
if not movie_found: if not movie_status:
for ( for (
movie_provider_source, movie_provider_source,
movie_provider_id, movie_provider_id,
@@ -521,21 +578,38 @@ class Jellyfin:
movie_provider_source.lower() 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 break
if movie_found: if movie_status:
jellyfin_video_id = jellyfin_video["Id"] if movie_status["completed"]:
msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin" jellyfin_video_id = jellyfin_video["Id"]
if not dryrun: msg = f"{jellyfin_video['Name']} as watched for {user_name} in {library} for Jellyfin"
logger(f"Marking {msg}", 0) if not dryrun:
await self.query( logger(f"Marking {msg}", 0)
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", await self.query(
"post", f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
session, "post",
) session,
)
else:
logger(f"Dryrun {msg}", 0)
else: 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: else:
logger( logger(
f"Jellyfin: Skipping movie {jellyfin_video['Name']} as it is not in mark list for {user_name}", 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"] in videos_shows_ids["locations"]
): ):
show_found = True 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: if not show_found:
for show_provider_source, show_provider_id in jellyfin_show[ for show_provider_source, show_provider_id in jellyfin_show[
@@ -575,7 +659,18 @@ class Jellyfin:
] ]
): ):
show_found = True 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: if show_found:
logger( logger(
@@ -591,7 +686,7 @@ class Jellyfin:
) )
for jellyfin_episode in jellyfin_episodes["Items"]: for jellyfin_episode in jellyfin_episodes["Items"]:
episode_found = False episode_status = None
if "MediaSources" in jellyfin_episode: if "MediaSources" in jellyfin_episode:
for episode_location in jellyfin_episode[ for episode_location in jellyfin_episode[
@@ -601,10 +696,18 @@ class Jellyfin:
episode_location["Path"].split("/")[-1] episode_location["Path"].split("/")[-1]
in videos_episodes_ids["locations"] 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 break
if not episode_found: if not episode_status:
for ( for (
episode_provider_source, episode_provider_source,
episode_provider_id, episode_provider_id,
@@ -619,24 +722,46 @@ class Jellyfin:
episode_provider_source.lower() 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 break
if episode_found: if episode_status:
jellyfin_episode_id = jellyfin_episode["Id"] if episode_status["completed"]:
msg = ( jellyfin_episode_id = jellyfin_episode["Id"]
f"{jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode['Name']}" msg = (
+ f" as watched for {user_name} in {library} for Jellyfin" 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,
) )
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: 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: else:
logger( logger(
f"Jellyfin: Skipping episode {jellyfin_episode['Name']} as it is not in mark list for {user_name}", 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}", f"Jellyfin: Error updating watched for {user_name} in library {library}, {e}",
2, 2,
) )
logger(traceback.format_exc(), 2)
raise Exception(e) raise Exception(e)
async def update_watched( async def update_watched(

View File

@@ -163,13 +163,25 @@ def episode_title_dict(user_list: dict):
for season in user_list[show]: for season in user_list[show]:
for episode in user_list[show][season]: for episode in user_list[show][season]:
for episode_key, episode_value in episode.items(): for episode_key, episode_value in episode.items():
if episode_key.lower() not in episode_output_dict: if episode_key != "status":
episode_output_dict[episode_key.lower()] = [] 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": if episode_key == "locations":
for episode_location in episode_value: for episode_location in episode_value:
episode_output_dict[episode_key.lower()].append( episode_output_dict[episode_key.lower()].append(
episode_location episode_location
) )
elif episode_key == "status":
episode_output_dict["completed"].append(
episode_value["completed"]
)
episode_output_dict["time"].append(episode_value["time"])
else: else:
episode_output_dict[episode_key.lower()].append( episode_output_dict[episode_key.lower()].append(
episode_value.lower() episode_value.lower()
@@ -186,11 +198,21 @@ def movies_title_dict(user_list: dict):
movies_output_dict = {} movies_output_dict = {}
for movie in user_list: for movie in user_list:
for movie_key, movie_value in movie.items(): for movie_key, movie_value in movie.items():
if movie_key.lower() not in movies_output_dict: if movie_key != "status":
movies_output_dict[movie_key.lower()] = [] 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": if movie_key == "locations":
for movie_location in movie_value: for movie_location in movie_value:
movies_output_dict[movie_key.lower()].append(movie_location) 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: else:
movies_output_dict[movie_key.lower()].append(movie_value.lower()) movies_output_dict[movie_key.lower()].append(movie_value.lower())

View File

@@ -1,5 +1,6 @@
import re, requests, os, traceback import re, requests, os, traceback
from urllib3.poolmanager import PoolManager from urllib3.poolmanager import PoolManager
from math import floor
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.myplex import MyPlexAccount 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): def get_user_library_watched_show(show):
try: try:
show_guids = {} show_guids = {}
for show_guid in show.guids: try:
# Extract source and id from guid.id for show_guid in show.guids:
m = re.match(r"(.*)://(.*)", show_guid.id) # Extract source and id from guid.id
show_guid_source, show_guid_id = m.group(1).lower(), m.group(2) m = re.match(r"(.*)://(.*)", show_guid.id)
show_guids[show_guid_source] = 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["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])
@@ -42,30 +99,23 @@ def get_user_library_watched_show(show):
# Get all watched episodes for show # Get all watched episodes for show
episode_guids = {} episode_guids = {}
watched_episodes = show.watched() watched = show.watched()
for episode in watched_episodes:
episode_guids_temp = {} for episode in show.episodes():
try: if episode in watched:
if len(episode.guids) > 0: if episode.parentTitle not in episode_guids:
for guid in episode.guids: episode_guids[episode.parentTitle] = []
# Extract after :// from guid.id
m = re.match(r"(.*)://(.*)", guid.id) episode_guids[episode.parentTitle].append(
guid_source, guid_id = m.group(1).lower(), m.group(2) get_episode_guids(episode, show, completed=True)
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,
) )
elif episode.viewOffset > 0:
if episode.parentTitle not in episode_guids:
episode_guids[episode.parentTitle] = []
episode_guids_temp["locations"] = tuple( episode_guids[episode.parentTitle].append(
[x.split("/")[-1] for x in episode.locations] get_episode_guids(episode, show, completed=False)
) )
if episode.parentTitle not in episode_guids:
episode_guids[episode.parentTitle] = []
episode_guids[episode.parentTitle].append(episode_guids_temp)
return show_guids, episode_guids return show_guids, episode_guids
@@ -89,32 +139,37 @@ def get_user_library_watched(user, user_plex, library):
if library.type == "movie": if library.type == "movie":
user_watched[user_name][library.title] = [] user_watched[user_name][library.title] = []
# Get all watched movies
for video in library_videos.search(unwatched=False): for video in library_videos.search(unwatched=False):
logger(f"Plex: Adding {video.title} to {user_name} watched list", 3) logger(f"Plex: Adding {video.title} to {user_name} watched list", 3)
logger(f"Plex: {video.title} {video.guids} {video.locations}", 3)
movie_guids = {} movie_guids = get_movie_guids(video, completed=True)
for guid in video.guids:
# Extract source and id from guid.id user_watched[user_name][library.title].append(movie_guids)
m = re.match(r"(.*)://(.*)", guid.id)
guid_source, guid_id = m.group(1).lower(), m.group(2) # Get all partially watched movies greater than 1 minute
movie_guids[guid_source] = guid_id for video in library_videos.search(inProgress=True):
if video.viewOffset < 60000:
movie_guids["title"] = video.title continue
movie_guids["locations"] = tuple(
[x.split("/")[-1] for x in video.locations] 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) 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": elif library.type == "show":
user_watched[user_name][library.title] = {} user_watched[user_name][library.title] = {}
shows = library_videos.search(unwatched=False)
# Parallelize show processing # Parallelize show processing
args = [] 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]) args.append([get_user_library_watched_show, show])
for show_guids, episode_guids in future_thread_executor( for show_guids, episode_guids in future_thread_executor(
@@ -144,11 +199,20 @@ def get_user_library_watched(user, user_plex, library):
return {} return {}
def find_video(plex_search, video_ids): def find_video(plex_search, video_ids, videos=None):
try: try:
for location in plex_search.locations: for location in plex_search.locations:
if location.split("/")[-1] in video_ids["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: for guid in plex_search.guids:
guid_source = re.search(r"(.*)://", guid.id).group(1).lower() 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 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_source in video_ids.keys():
if guid_id in video_ids[guid_source]: 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: 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): 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) library_videos = user_plex.library.section(library)
if videos_movies_ids: if videos_movies_ids:
for movies_search in library_videos.search(unwatched=True): for movies_search in library_videos.search(unwatched=True):
if find_video(movies_search, videos_movies_ids): video_status = get_video_status(
msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex" movies_search, videos_movies_ids, videos
if not dryrun: )
logger(f"Marked {msg}", 0) if video_status:
movies_search.markWatched() if video_status["completed"]:
else: msg = f"{movies_search.title} as watched for {user.title} in {library} for Plex"
logger(f"Dryrun {msg}", 0) 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: else:
logger( logger(
f"Plex: Skipping movie {movies_search.title} as it is not in mark list for {user.title}", 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: if videos_shows_ids and videos_episodes_ids:
for show_search in library_videos.search(unwatched=True): 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(): for episode_search in show_search.episodes():
if find_video(episode_search, videos_episodes_ids): video_status = get_video_status(
msg = f"{show_search.title} {episode_search.title} as watched for {user.title} in {library} for Plex" episode_search, videos_episodes_ids, episode_videos
if not dryrun: )
logger(f"Marked {msg}", 0) if video_status:
episode_search.markWatched() 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: 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: else:
logger( logger(
f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}", f"Plex: Skipping episode {episode_search.title} as it is not in mark list for {user.title}",

View File

@@ -29,6 +29,48 @@ def combine_watched_dicts(dicts: list):
return combined_dict 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( def cleanup_watched(
watched_list_1, watched_list_2, user_mapping=None, library_mapping=None watched_list_1, watched_list_2, user_mapping=None, library_mapping=None
): ):
@@ -60,9 +102,17 @@ def cleanup_watched(
# Movies # Movies
if isinstance(watched_list_1[user_1][library_1], list): if isinstance(watched_list_1[user_1][library_1], list):
for movie in watched_list_1[user_1][library_1]: for movie in watched_list_1[user_1][library_1]:
if is_movie_in_dict(movie, movies_watched_list_2_keys_dict): movie_index = get_movie_index_in_dict(
logger(f"Removing {movie} from {library_1}", 3) movie, movies_watched_list_2_keys_dict
modified_watched_list_1[user_1][library_1].remove(movie) )
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 # TV Shows
elif isinstance(watched_list_1[user_1][library_1], dict): 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][ for episode in watched_list_1[user_1][library_1][show_key_1][
season season
]: ]:
if is_episode_in_dict( episode_index = get_episode_index_in_dict(
episode, episode_watched_list_2_keys_dict episode, episode_watched_list_2_keys_dict
): )
if ( if episode_index is not None:
episode if check_remove_entry(
in modified_watched_list_1[user_1][library_1][ episode,
show_key_1 library_1,
][season] 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][ modified_watched_list_1[user_1][library_1][
show_key_1 show_key_1
][season].remove(episode) ][season].remove(episode)
@@ -148,7 +195,7 @@ def get_other(watched_list, object_1, object_2):
return None 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 # Iterate through the keys and values of the movie dictionary
for movie_key, movie_value in movie.items(): 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 # 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(): if "locations" in movies_watched_list_2_keys_dict.keys():
# Iterate through the locations in the movie dictionary # Iterate through the locations in the movie dictionary
for location in movie_value: 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"]: 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 # If the key is not "locations", check if the movie_key is present in the movies_watched_list_2_keys_dict dictionary
else: else:
if movie_key in movies_watched_list_2_keys_dict.keys(): 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 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]: 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 # 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 # Iterate through the keys and values of the episode dictionary
for episode_key, episode_value in episode.items(): 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 # 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(): if "locations" in episode_watched_list_2_keys_dict.keys():
# Iterate through the locations in the episode dictionary # Iterate through the locations in the episode dictionary
for location in episode_value: 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"]: 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 # If the key is not "locations", check if the episode_key is present in the episode_watched_list_2_keys_dict dictionary
else: else:
if episode_key in episode_watched_list_2_keys_dict.keys(): 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 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]: 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 # If the loop completes without finding a match, return False
return False return None

View File

@@ -51,6 +51,7 @@ show_list = {
"locations": ( "locations": (
"The Last of Us - S01E01 - When You're Lost in the Darkness WEBDL-1080p.mkv", "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", "imdb": "tt2380307",
"tmdb": "354912", "tmdb": "354912",
"locations": ("Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv"), "locations": ("Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv"),
"status": {"completed": True, "time": 0},
} }
] ]
@@ -77,12 +79,16 @@ episode_titles = {
], ],
"tmdb": ["2181581"], "tmdb": ["2181581"],
"tvdb": ["8444132"], "tvdb": ["8444132"],
"completed": [True],
"time": [0],
} }
movie_titles = { movie_titles = {
"imdb": ["tt2380307"], "imdb": ["tt2380307"],
"locations": ["Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv"], "locations": ["Coco (2017) Remux-2160p.mkv", "Coco (2017) Remux-1080p.mkv"],
"title": ["coco"], "title": ["coco"],
"tmdb": ["354912"], "tmdb": ["354912"],
"completed": [True],
"time": [0],
} }

View File

@@ -30,42 +30,43 @@ tv_shows_watched_list_1 = {
"imdb": "tt0550489", "imdb": "tt0550489",
"tmdb": "282843", "tmdb": "282843",
"tvdb": "176357", "tvdb": "176357",
"title": "Extreme Aggressor",
"locations": ( "locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv", "Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv",
), ),
"status": {"completed": True, "time": 0},
}, },
{ {
"imdb": "tt0550487", "imdb": "tt0550487",
"tmdb": "282861", "tmdb": "282861",
"tvdb": "300385", "tvdb": "300385",
"title": "Compulsion",
"locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",), "locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",),
"status": {"completed": True, "time": 0},
}, },
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "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 = { tv_shows_watched_list_2 = {
frozenset( frozenset(
{ {
@@ -81,32 +82,146 @@ tv_shows_watched_list_2 = {
"imdb": "tt0550487", "imdb": "tt0550487",
"tmdb": "282861", "tmdb": "282861",
"tvdb": "300385", "tvdb": "300385",
"title": "Compulsion",
"locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",), "locations": ("Criminal Minds S01E02 Compulsion WEBDL-720p.mkv",),
"status": {"completed": True, "time": 0},
}, },
{ {
"imdb": "tt0550498", "imdb": "tt0550498",
"tmdb": "282865", "tmdb": "282865",
"tvdb": "300474", "tvdb": "300474",
"title": "Won't Get Fooled Again",
"locations": ( "locations": (
"Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv", "Criminal Minds S01E03 Won't Get Fooled Again WEBDL-720p.mkv",
), ),
"status": {"completed": True, "time": 0},
}, },
] ]
}, },
frozenset({("title", "Test"), ("locations", ("Test",))}): { frozenset({("title", "Test"), ("locations", ("Test",))}): {
"Season 1": [ "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 = [ movies_watched_list_2 = [
{ {
"imdb": "tt2380307", "imdb": "tt2380307",
"tmdb": "354912", "tmdb": "354912",
"title": "Coco", "title": "Coco",
"locations": ("Coco (2017) Remux-1080p.mkv",), "locations": ("Coco (2017) Remux-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
{ {
"imdb": "tt0384793", "imdb": "tt0384793",
@@ -114,9 +229,33 @@ movies_watched_list_2 = [
"tvdb": "9103", "tvdb": "9103",
"title": "Accepted", "title": "Accepted",
"locations": ("Accepted (2006) Remux-1080p.mkv",), "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. # Test to see if objects get deleted all the way up to the root.
tv_shows_2_watched_list_1 = { tv_shows_2_watched_list_1 = {
frozenset( frozenset(
@@ -133,86 +272,16 @@ tv_shows_2_watched_list_1 = {
"imdb": "tt0550489", "imdb": "tt0550489",
"tmdb": "282843", "tmdb": "282843",
"tvdb": "176357", "tvdb": "176357",
"title": "Extreme Aggressor",
"locations": ( "locations": (
"Criminal Minds S01E01 Extreme Aggressor WEBDL-720p.mkv", "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(): def test_simple_cleanup_watched():
user_watched_list_1 = { user_watched_list_1 = {
@@ -311,18 +380,21 @@ def test_combine_watched_dicts():
"tmdb": "12429", "tmdb": "12429",
"imdb": "tt0876563", "imdb": "tt0876563",
"locations": ("Ponyo (2008) Bluray-1080p.mkv",), "locations": ("Ponyo (2008) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
{ {
"title": "Spirited Away", "title": "Spirited Away",
"tmdb": "129", "tmdb": "129",
"imdb": "tt0245429", "imdb": "tt0245429",
"locations": ("Spirited Away (2001) Bluray-1080p.mkv",), "locations": ("Spirited Away (2001) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
{ {
"title": "Castle in the Sky", "title": "Castle in the Sky",
"tmdb": "10515", "tmdb": "10515",
"imdb": "tt0092067", "imdb": "tt0092067",
"locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",), "locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
] ]
} }
@@ -349,6 +421,7 @@ def test_combine_watched_dicts():
"locations": ( "locations": (
"11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv", "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", "tmdb": "12429",
"imdb": "tt0876563", "imdb": "tt0876563",
"locations": ("Ponyo (2008) Bluray-1080p.mkv",), "locations": ("Ponyo (2008) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
{ {
"title": "Spirited Away", "title": "Spirited Away",
"tmdb": "129", "tmdb": "129",
"imdb": "tt0245429", "imdb": "tt0245429",
"locations": ("Spirited Away (2001) Bluray-1080p.mkv",), "locations": ("Spirited Away (2001) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
{ {
"title": "Castle in the Sky", "title": "Castle in the Sky",
"tmdb": "10515", "tmdb": "10515",
"imdb": "tt0092067", "imdb": "tt0092067",
"locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",), "locations": ("Castle in the Sky (1986) Bluray-1080p.mkv",),
"status": {"completed": True, "time": 0},
}, },
], ],
"Anime Shows": {}, "Anime Shows": {},
@@ -399,6 +475,7 @@ def test_combine_watched_dicts():
"locations": ( "locations": (
"11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv", "11.22.63 S01E01 The Rabbit Hole Bluray-1080p.mkv",
), ),
"status": {"completed": True, "time": 0},
} }
] ]
} }