196
src/jellyfin.py
196
src/jellyfin.py
@@ -1,6 +1,7 @@
|
||||
import asyncio, aiohttp, traceback, os
|
||||
import traceback, os
|
||||
from math import floor
|
||||
from dotenv import load_dotenv
|
||||
import requests
|
||||
|
||||
from src.functions import (
|
||||
logger,
|
||||
@@ -78,12 +79,7 @@ class Jellyfin:
|
||||
def __init__(self, baseurl, token):
|
||||
self.baseurl = baseurl
|
||||
self.token = token
|
||||
self.timeout = aiohttp.ClientTimeout(
|
||||
total=int(os.getenv("REQUEST_TIMEOUT", 300)),
|
||||
connect=None,
|
||||
sock_connect=None,
|
||||
sock_read=None,
|
||||
)
|
||||
self.timeout = int(os.getenv("REQUEST_TIMEOUT", 300))
|
||||
|
||||
if not self.baseurl:
|
||||
raise Exception("Jellyfin baseurl not set")
|
||||
@@ -91,14 +87,11 @@ class Jellyfin:
|
||||
if not self.token:
|
||||
raise Exception("Jellyfin token not set")
|
||||
|
||||
self.users = asyncio.run(self.get_users())
|
||||
self.session = requests.Session()
|
||||
self.users = self.get_users()
|
||||
|
||||
async def query(self, query, query_type, session=None, identifiers=None):
|
||||
def query(self, query, query_type, session=None, identifiers=None):
|
||||
try:
|
||||
if not session:
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
return await self.query(query, query_type, session, identifiers)
|
||||
|
||||
results = None
|
||||
|
||||
authorization = (
|
||||
@@ -115,24 +108,20 @@ class Jellyfin:
|
||||
}
|
||||
|
||||
if query_type == "get":
|
||||
async with session.get(
|
||||
self.baseurl + query, headers=headers
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
response = self.session.get(self.baseurl + query, headers=headers)
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Query failed with status {response.status} {response.reason}"
|
||||
)
|
||||
results = await response.json()
|
||||
results = response.json()
|
||||
|
||||
elif query_type == "post":
|
||||
async with session.post(
|
||||
self.baseurl + query, headers=headers
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
response = self.session.post(self.baseurl + query, headers=headers)
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Query failed with status {response.status} {response.reason}"
|
||||
)
|
||||
results = await response.json()
|
||||
results = response.json()
|
||||
|
||||
if not isinstance(results, list) and not isinstance(results, dict):
|
||||
raise Exception("Query result is not of type list or dict")
|
||||
@@ -151,7 +140,7 @@ class Jellyfin:
|
||||
try:
|
||||
query_string = "/System/Info/Public"
|
||||
|
||||
response = asyncio.run(self.query(query_string, "get"))
|
||||
response = self.query(query_string, "get")
|
||||
|
||||
if response:
|
||||
return f"{response['ServerName']}: {response['Version']}"
|
||||
@@ -162,13 +151,12 @@ class Jellyfin:
|
||||
logger(f"Jellyfin: Get server name failed {e}", 2)
|
||||
raise Exception(e)
|
||||
|
||||
async def get_users(self):
|
||||
def get_users(self):
|
||||
try:
|
||||
users = {}
|
||||
|
||||
query_string = "/Users"
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
response = await self.query(query_string, "get", session)
|
||||
response = self.query(query_string, "get")
|
||||
|
||||
# If response is not empty
|
||||
if response:
|
||||
@@ -180,7 +168,7 @@ class Jellyfin:
|
||||
logger(f"Jellyfin: Get users failed {e}", 2)
|
||||
raise Exception(e)
|
||||
|
||||
async def get_user_library_watched(
|
||||
def get_user_library_watched(
|
||||
self, user_name, user_id, library_type, library_id, library_title
|
||||
):
|
||||
try:
|
||||
@@ -193,22 +181,19 @@ class Jellyfin:
|
||||
0,
|
||||
)
|
||||
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
# Movies
|
||||
if library_type == "Movie":
|
||||
user_watched[user_name][library_title] = []
|
||||
watched = await self.query(
|
||||
watched = self.query(
|
||||
f"/Users/{user_id}/Items"
|
||||
+ f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
|
||||
"get",
|
||||
session,
|
||||
)
|
||||
|
||||
in_progress = await self.query(
|
||||
in_progress = 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"]:
|
||||
@@ -255,11 +240,10 @@ class Jellyfin:
|
||||
user_watched[user_name][library_title] = {}
|
||||
|
||||
# Retrieve a list of watched TV shows
|
||||
watched_shows = await self.query(
|
||||
watched_shows = self.query(
|
||||
f"/Users/{user_id}/Items"
|
||||
+ f"?ParentId={library_id}&isPlaceHolder=false&IncludeItemTypes=Series&Recursive=True&Fields=ProviderIds,Path,RecursiveItemCount",
|
||||
"get",
|
||||
session,
|
||||
)
|
||||
|
||||
# Filter the list of shows to only include those that have been partially or fully watched
|
||||
@@ -270,15 +254,13 @@ class Jellyfin:
|
||||
watched_shows_filtered.append(show)
|
||||
|
||||
# Create a list of tasks to retrieve the seasons of each watched show
|
||||
seasons_tasks = []
|
||||
seasons_watched = []
|
||||
for show in watched_shows_filtered:
|
||||
logger(
|
||||
f"Jellyfin: Adding {show.get('Name')} to {user_name} watched list",
|
||||
3,
|
||||
)
|
||||
show_guids = {
|
||||
k.lower(): v for k, v in show["ProviderIds"].items()
|
||||
}
|
||||
show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
|
||||
show_guids["title"] = show["Name"]
|
||||
show_guids["locations"] = (
|
||||
tuple([show["Path"].split("/")[-1]])
|
||||
@@ -291,19 +273,13 @@ class Jellyfin:
|
||||
"show_id": show["Id"],
|
||||
}
|
||||
|
||||
season_task = asyncio.ensure_future(
|
||||
self.query(
|
||||
season_task = self.query(
|
||||
f"/Shows/{show['Id']}/Seasons"
|
||||
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount",
|
||||
"get",
|
||||
session,
|
||||
frozenset(show_identifiers.items()),
|
||||
identifiers=frozenset(show_identifiers.items()),
|
||||
)
|
||||
)
|
||||
seasons_tasks.append(season_task)
|
||||
|
||||
# Retrieve the seasons for each watched show
|
||||
seasons_watched = await asyncio.gather(*seasons_tasks)
|
||||
seasons_watched.append(season_task)
|
||||
|
||||
# Filter the list of seasons to only include those that have been partially or fully watched
|
||||
seasons_watched_filtered = []
|
||||
@@ -316,47 +292,33 @@ class Jellyfin:
|
||||
for season in seasons["Items"]:
|
||||
if "PlayedPercentage" in season["UserData"]:
|
||||
if season["UserData"]["PlayedPercentage"] > 0:
|
||||
seasons_watched_filtered_dict["Items"].append(
|
||||
season
|
||||
)
|
||||
seasons_watched_filtered_dict["Items"].append(season)
|
||||
|
||||
if seasons_watched_filtered_dict["Items"]:
|
||||
seasons_watched_filtered.append(
|
||||
seasons_watched_filtered_dict
|
||||
)
|
||||
seasons_watched_filtered.append(seasons_watched_filtered_dict)
|
||||
|
||||
# Create a list of tasks to retrieve the episodes of each watched season
|
||||
episodes_tasks = []
|
||||
watched_episodes = []
|
||||
for seasons in seasons_watched_filtered:
|
||||
if len(seasons["Items"]) > 0:
|
||||
for season in seasons["Items"]:
|
||||
season_identifiers = dict(seasons["Identifiers"])
|
||||
season_identifiers["season_index"] = season[
|
||||
"IndexNumber"
|
||||
]
|
||||
watched_task = asyncio.ensure_future(
|
||||
self.query(
|
||||
season_identifiers["season_index"] = season["IndexNumber"]
|
||||
watched_task = self.query(
|
||||
f"/Shows/{season_identifiers['show_id']}/Episodes"
|
||||
+ f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsPlayed&Fields=ProviderIds,MediaSources",
|
||||
"get",
|
||||
session,
|
||||
frozenset(season_identifiers.items()),
|
||||
identifiers=frozenset(season_identifiers.items()),
|
||||
)
|
||||
)
|
||||
in_progress_task = asyncio.ensure_future(
|
||||
self.query(
|
||||
|
||||
in_progress_task = 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()),
|
||||
identifiers=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)
|
||||
watched_episodes.append(watched_task)
|
||||
watched_episodes.append(in_progress_task)
|
||||
|
||||
# Iterate through the watched episodes
|
||||
for episodes in watched_episodes:
|
||||
@@ -426,7 +388,7 @@ class Jellyfin:
|
||||
logger(traceback.format_exc(), 2)
|
||||
return {}
|
||||
|
||||
async def get_users_watched(
|
||||
def get_users_watched(
|
||||
self,
|
||||
user_name,
|
||||
user_id,
|
||||
@@ -441,30 +403,23 @@ class Jellyfin:
|
||||
user_name = user_name.lower()
|
||||
tasks_watched = []
|
||||
|
||||
tasks_libraries = []
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
libraries = await self.query(f"/Users/{user_id}/Views", "get", session)
|
||||
for library in libraries["Items"]:
|
||||
libraries = []
|
||||
|
||||
all_libraries = self.query(f"/Users/{user_id}/Views", "get")
|
||||
for library in all_libraries["Items"]:
|
||||
library_id = library["Id"]
|
||||
library_title = library["Name"]
|
||||
identifiers = {
|
||||
"library_id": library_id,
|
||||
"library_title": library_title,
|
||||
}
|
||||
task = asyncio.ensure_future(
|
||||
self.query(
|
||||
task = self.query(
|
||||
f"/Users/{user_id}/Items"
|
||||
+ f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100",
|
||||
"get",
|
||||
session,
|
||||
identifiers=identifiers,
|
||||
)
|
||||
)
|
||||
tasks_libraries.append(task)
|
||||
|
||||
libraries = await asyncio.gather(
|
||||
*tasks_libraries, return_exceptions=True
|
||||
)
|
||||
libraries.append(task)
|
||||
|
||||
for watched in libraries:
|
||||
if len(watched["Items"]) == 0:
|
||||
@@ -509,25 +464,23 @@ class Jellyfin:
|
||||
|
||||
for library_type in types:
|
||||
# Get watched for user
|
||||
task = asyncio.ensure_future(
|
||||
self.get_user_library_watched(
|
||||
task = self.get_user_library_watched(
|
||||
user_name,
|
||||
user_id,
|
||||
library_type,
|
||||
library_id,
|
||||
library_title,
|
||||
)
|
||||
)
|
||||
tasks_watched.append(task)
|
||||
|
||||
watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
|
||||
watched = tasks_watched
|
||||
|
||||
return watched
|
||||
except Exception as e:
|
||||
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
|
||||
raise Exception(e)
|
||||
|
||||
async def get_watched(
|
||||
def get_watched(
|
||||
self,
|
||||
users,
|
||||
blacklist_library,
|
||||
@@ -553,7 +506,6 @@ class Jellyfin:
|
||||
)
|
||||
)
|
||||
|
||||
watched = await asyncio.gather(*watched, return_exceptions=True)
|
||||
for user_watched in watched:
|
||||
user_watched_combine = combine_watched_dicts(user_watched)
|
||||
for user, user_watched_temp in user_watched_combine.items():
|
||||
@@ -566,7 +518,7 @@ class Jellyfin:
|
||||
logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
|
||||
raise Exception(e)
|
||||
|
||||
async def update_user_watched(
|
||||
def update_user_watched(
|
||||
self, user_name, user_id, library, library_id, videos, dryrun
|
||||
):
|
||||
try:
|
||||
@@ -583,14 +535,13 @@ class Jellyfin:
|
||||
f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
|
||||
1,
|
||||
)
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
|
||||
if videos_movies_ids:
|
||||
jellyfin_search = await self.query(
|
||||
jellyfin_search = self.query(
|
||||
f"/Users/{user_id}/Items"
|
||||
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
|
||||
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources&IncludeItemTypes=Movie",
|
||||
"get",
|
||||
session,
|
||||
)
|
||||
for jellyfin_video in jellyfin_search["Items"]:
|
||||
movie_status = None
|
||||
@@ -608,9 +559,7 @@ class Jellyfin:
|
||||
for video in videos:
|
||||
if (
|
||||
contains_nested(
|
||||
movie_location["Path"].split("/")[
|
||||
-1
|
||||
],
|
||||
movie_location["Path"].split("/")[-1],
|
||||
video["locations"],
|
||||
)
|
||||
is not None
|
||||
@@ -627,9 +576,7 @@ class Jellyfin:
|
||||
if movie_provider_source.lower() in videos_movies_ids:
|
||||
if (
|
||||
movie_provider_id.lower()
|
||||
in videos_movies_ids[
|
||||
movie_provider_source.lower()
|
||||
]
|
||||
in videos_movies_ids[movie_provider_source.lower()]
|
||||
):
|
||||
for video in videos:
|
||||
if movie_provider_id.lower() in video.get(
|
||||
@@ -645,10 +592,9 @@ class Jellyfin:
|
||||
msg = f"Jellyfin: {jellyfin_video.get('Name')} as watched for {user_name} in {library}"
|
||||
if not dryrun:
|
||||
logger(msg, 5)
|
||||
await self.query(
|
||||
self.query(
|
||||
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
|
||||
"post",
|
||||
session,
|
||||
)
|
||||
else:
|
||||
logger(msg, 6)
|
||||
@@ -683,12 +629,11 @@ class Jellyfin:
|
||||
|
||||
# TV Shows
|
||||
if videos_shows_ids and videos_episodes_ids:
|
||||
jellyfin_search = await self.query(
|
||||
jellyfin_search = self.query(
|
||||
f"/Users/{user_id}/Items"
|
||||
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
|
||||
+ "&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series",
|
||||
"get",
|
||||
session,
|
||||
)
|
||||
jellyfin_shows = [x for x in jellyfin_search["Items"]]
|
||||
|
||||
@@ -726,9 +671,7 @@ class Jellyfin:
|
||||
if show_provider_source.lower() in videos_shows_ids:
|
||||
if (
|
||||
show_provider_id.lower()
|
||||
in videos_shows_ids[
|
||||
show_provider_source.lower()
|
||||
]
|
||||
in videos_shows_ids[show_provider_source.lower()]
|
||||
):
|
||||
show_found = True
|
||||
episode_videos = []
|
||||
@@ -747,11 +690,10 @@ class Jellyfin:
|
||||
1,
|
||||
)
|
||||
jellyfin_show_id = jellyfin_show["Id"]
|
||||
jellyfin_episodes = await self.query(
|
||||
jellyfin_episodes = self.query(
|
||||
f"/Shows/{jellyfin_show_id}/Episodes"
|
||||
+ f"?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources",
|
||||
"get",
|
||||
session,
|
||||
)
|
||||
|
||||
for jellyfin_episode in jellyfin_episodes["Items"]:
|
||||
@@ -764,9 +706,7 @@ class Jellyfin:
|
||||
if "Path" in episode_location:
|
||||
if (
|
||||
contains_nested(
|
||||
episode_location["Path"].split("/")[
|
||||
-1
|
||||
],
|
||||
episode_location["Path"].split("/")[-1],
|
||||
videos_episodes_ids["locations"],
|
||||
)
|
||||
is not None
|
||||
@@ -774,16 +714,14 @@ class Jellyfin:
|
||||
for episode in episode_videos:
|
||||
if (
|
||||
contains_nested(
|
||||
episode_location[
|
||||
"Path"
|
||||
].split("/")[-1],
|
||||
episode_location["Path"].split(
|
||||
"/"
|
||||
)[-1],
|
||||
episode["locations"],
|
||||
)
|
||||
is not None
|
||||
):
|
||||
episode_status = episode[
|
||||
"status"
|
||||
]
|
||||
episode_status = episode["status"]
|
||||
break
|
||||
break
|
||||
|
||||
@@ -828,10 +766,9 @@ class Jellyfin:
|
||||
)
|
||||
if not dryrun:
|
||||
logger(msg, 5)
|
||||
await self.query(
|
||||
self.query(
|
||||
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}",
|
||||
"post",
|
||||
session,
|
||||
)
|
||||
else:
|
||||
logger(msg, 6)
|
||||
@@ -892,12 +829,12 @@ class Jellyfin:
|
||||
logger(traceback.format_exc(), 2)
|
||||
raise Exception(e)
|
||||
|
||||
async def update_watched(
|
||||
def update_watched(
|
||||
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
|
||||
):
|
||||
try:
|
||||
tasks = []
|
||||
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
||||
|
||||
for user, libraries in watched_list.items():
|
||||
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1)
|
||||
user_other = None
|
||||
@@ -923,8 +860,9 @@ class Jellyfin:
|
||||
logger(f"{user} {user_other} not found in Jellyfin", 2)
|
||||
continue
|
||||
|
||||
jellyfin_libraries = await self.query(
|
||||
f"/Users/{user_id}/Views", "get", session
|
||||
jellyfin_libraries = self.query(
|
||||
f"/Users/{user_id}/Views",
|
||||
"get",
|
||||
)
|
||||
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
|
||||
|
||||
@@ -968,12 +906,10 @@ class Jellyfin:
|
||||
continue
|
||||
|
||||
if library_id:
|
||||
task = self.update_user_watched(
|
||||
self.update_user_watched(
|
||||
user_name, user_id, library, library_id, videos, dryrun
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
except Exception as e:
|
||||
logger(f"Jellyfin: Error updating watched, {e}", 2)
|
||||
raise Exception(e)
|
||||
|
||||
10
src/main.py
10
src/main.py
@@ -1,4 +1,4 @@
|
||||
import os, traceback, json, asyncio
|
||||
import os, traceback, json
|
||||
from dotenv import load_dotenv
|
||||
from time import sleep, perf_counter
|
||||
|
||||
@@ -28,6 +28,8 @@ def setup_users(
|
||||
):
|
||||
server_1_users = generate_user_list(server_1)
|
||||
server_2_users = generate_user_list(server_2)
|
||||
logger(f"Server 1 users: {server_1_users}", 1)
|
||||
logger(f"Server 2 users: {server_2_users}", 1)
|
||||
|
||||
users = combine_user_lists(server_1_users, server_2_users, user_mapping)
|
||||
logger(f"User list that exist on both servers {users}", 1)
|
||||
@@ -180,8 +182,7 @@ def get_server_watched(
|
||||
library_mapping,
|
||||
)
|
||||
elif server_connection[0] == "jellyfin":
|
||||
return asyncio.run(
|
||||
server_connection[1].get_watched(
|
||||
return server_connection[1].get_watched(
|
||||
users,
|
||||
blacklist_library,
|
||||
whitelist_library,
|
||||
@@ -189,7 +190,6 @@ def get_server_watched(
|
||||
whitelist_library_type,
|
||||
library_mapping,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_server_watched(
|
||||
@@ -204,11 +204,9 @@ def update_server_watched(
|
||||
server_watched_filtered, user_mapping, library_mapping, dryrun
|
||||
)
|
||||
elif server_connection[0] == "jellyfin":
|
||||
asyncio.run(
|
||||
server_connection[1].update_watched(
|
||||
server_watched_filtered, user_mapping, library_mapping, dryrun
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def should_sync_server(server_1_type, server_2_type):
|
||||
|
||||
Reference in New Issue
Block a user