Gather partially watched movie/episodes with todo for processing.

This commit is contained in:
Luigi311
2023-03-22 19:48:19 -06:00
parent 962b1149ad
commit a5540b94d5
3 changed files with 418 additions and 163 deletions

View File

@@ -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(

View File

@@ -163,17 +163,18 @@ 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 == "locations":
for episode_location in episode_value:
if episode_key != "status":
if episode_key.lower() not in episode_output_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
)
else:
episode_output_dict[episode_key.lower()].append(
episode_location
episode_value.lower()
)
else:
episode_output_dict[episode_key.lower()].append(
episode_value.lower()
)
return episode_output_dict
except Exception:
@@ -186,13 +187,16 @@ 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 == "locations":
for movie_location in movie_value:
movies_output_dict[movie_key.lower()].append(movie_location)
else:
movies_output_dict[movie_key.lower()].append(movie_value.lower())
if movie_key != "status":
if movie_key.lower() not in movies_output_dict:
movies_output_dict[movie_key.lower()] = []
if movie_key == "locations":
for movie_location in movie_value:
movies_output_dict[movie_key.lower()].append(movie_location)
else:
movies_output_dict[movie_key.lower()].append(
movie_value.lower()
)
return movies_output_dict
except Exception:

View File

@@ -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,69 @@ 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["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 +98,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 +138,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 +198,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 +220,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 +278,26 @@ 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:
# Only mark partially watched if watched for more than 1 minute
# TODO add support for partially watched movies
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)
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 +306,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)
# TODO add support for partially watched episodes
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)
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}",