Gather partially watched movie/episodes with todo for processing.
This commit is contained in:
298
src/jellyfin.py
298
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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
247
src/plex.py
247
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,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}",
|
||||
|
||||
Reference in New Issue
Block a user