Merge pull request #140 from luigi311/fixes

Fixes
This commit is contained in:
Luigi311
2024-01-17 15:01:59 -07:00
committed by GitHub
4 changed files with 528 additions and 667 deletions

View File

@@ -14,7 +14,7 @@ jobs:
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: "Install dependencies" - name: "Install dependencies"
run: pip install -r requirements.txt && pip install -r test/requirements.txt run: pip install -r requirements.txt && pip install -r test/requirements.txt
@@ -25,7 +25,7 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: "Install dependencies" - name: "Install dependencies"
run: | run: |
@@ -33,7 +33,7 @@ jobs:
sudo apt update && sudo apt install -y docker-compose sudo apt update && sudo apt install -y docker-compose
- name: "Checkout JellyPlex-Watched-CI" - name: "Checkout JellyPlex-Watched-CI"
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
repository: luigi311/JellyPlex-Watched-CI repository: luigi311/JellyPlex-Watched-CI
path: JellyPlex-Watched-CI path: JellyPlex-Watched-CI
@@ -95,11 +95,11 @@ jobs:
variant: slim variant: slim
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Docker meta - name: Docker meta
id: docker_meta id: docker_meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: | images: |
${{ secrets.DOCKER_USERNAME }}/jellyplex-watched,enable=${{ secrets.DOCKER_USERNAME != '' }} ${{ secrets.DOCKER_USERNAME }}/jellyplex-watched,enable=${{ secrets.DOCKER_USERNAME != '' }}
@@ -121,23 +121,23 @@ jobs:
type=sha,enable=${{ matrix.variant == env.DEFAULT_VARIANT }} type=sha,enable=${{ matrix.variant == env.DEFAULT_VARIANT }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
env: env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
if: "${{ env.DOCKER_USERNAME != '' }}" if: "${{ env.DOCKER_USERNAME != '' }}"
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: "${{ steps.docker_meta.outcome == 'success' }}" if: "${{ steps.docker_meta.outcome == 'success' }}"
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -146,7 +146,7 @@ jobs:
- name: Build - name: Build
id: build id: build
if: "${{ steps.docker_meta.outputs.tags == '' }}" if: "${{ steps.docker_meta.outputs.tags == '' }}"
uses: docker/build-push-action@v3 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}
@@ -157,7 +157,7 @@ jobs:
- name: Build Push - name: Build Push
id: build_push id: build_push
if: "${{ steps.docker_meta.outputs.tags != '' }}" if: "${{ steps.docker_meta.outputs.tags != '' }}"
uses: docker/build-push-action@v3 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
.env **.env
*.prof *.prof
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files

View File

@@ -1,12 +1,14 @@
import asyncio, aiohttp, traceback, os import traceback, os
from math import floor from math import floor
from dotenv import load_dotenv from dotenv import load_dotenv
import requests
from src.functions import ( from src.functions import (
logger, logger,
search_mapping, search_mapping,
contains_nested, contains_nested,
log_marked, log_marked,
str_to_bool,
) )
from src.library import ( from src.library import (
check_skip_logic, check_skip_logic,
@@ -18,72 +20,85 @@ from src.watched import (
load_dotenv(override=True) load_dotenv(override=True)
generate_guids = str_to_bool(os.getenv("GENERATE_GUIDS", "True"))
generate_locations = str_to_bool(os.getenv("GENERATE_LOCATIONS", "True"))
def get_movie_guids(movie):
if "ProviderIds" in movie: def get_guids(item):
logger( guids = {"title": item["Name"]}
f"Jellyfin: {movie.get('Name')} {movie['ProviderIds']} {movie['MediaSources']}",
3, if "ProviderIds" in item:
guids.update({k.lower(): v for k, v in item["ProviderIds"].items()})
if "MediaSources" in item:
guids["locations"] = tuple(
[x["Path"].split("/")[-1] for x in item["MediaSources"] if "Path" in x]
) )
else: else:
logger( guids["locations"] = tuple()
f"Jellyfin: {movie.get('Name')} {movie['MediaSources']['Path']}",
3,
)
# Create a dictionary for the movie with its title guids["status"] = {
movie_guids = {"title": movie["Name"]} "completed": item["UserData"]["Played"],
# 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"]]
)
else:
movie_guids["locations"] = tuple()
movie_guids["status"] = {
"completed": movie["UserData"]["Played"],
# Convert ticks to milliseconds to match Plex # Convert ticks to milliseconds to match Plex
"time": floor(movie["UserData"]["PlaybackPositionTicks"] / 10000), "time": floor(item["UserData"]["PlaybackPositionTicks"] / 10000),
} }
return movie_guids return guids
def get_episode_guids(episode): def get_video_status(jellyfin_video, videos_ids, videos):
# Create a dictionary for the episode with its provider IDs and media sources video_status = None
episode_dict = {k.lower(): v for k, v in episode["ProviderIds"].items()}
episode_dict["title"] = episode["Name"]
episode_dict["locations"] = tuple() if generate_locations:
if "MediaSources" in episode: if "MediaSources" in jellyfin_video:
for x in episode["MediaSources"]: for video_location in jellyfin_video["MediaSources"]:
if "Path" in x: if "Path" in video_location:
episode_dict["locations"] += (x["Path"].split("/")[-1],) if (
contains_nested(
video_location["Path"].split("/")[-1],
videos_ids["locations"],
)
is not None
):
for video in videos:
if (
contains_nested(
video_location["Path"].split("/")[-1],
video["locations"],
)
is not None
):
video_status = video["status"]
break
break
episode_dict["status"] = { if generate_guids:
"completed": episode["UserData"]["Played"], if not video_status:
"time": floor(episode["UserData"]["PlaybackPositionTicks"] / 10000), for (
} video_provider_source,
video_provider_id,
) in jellyfin_video["ProviderIds"].items():
if video_provider_source.lower() in videos_ids:
if (
video_provider_id.lower()
in videos_ids[video_provider_source.lower()]
):
for video in videos:
if video_provider_id.lower() in video.get(
video_provider_source.lower(), []
):
video_status = video["status"]
break
break
return episode_dict return video_status
class Jellyfin: class Jellyfin:
def __init__(self, baseurl, token): def __init__(self, baseurl, token):
self.baseurl = baseurl self.baseurl = baseurl
self.token = token self.token = token
self.timeout = aiohttp.ClientTimeout( self.timeout = int(os.getenv("REQUEST_TIMEOUT", 300))
total=int(os.getenv("REQUEST_TIMEOUT", 300)),
connect=None,
sock_connect=None,
sock_read=None,
)
if not self.baseurl: if not self.baseurl:
raise Exception("Jellyfin baseurl not set") raise Exception("Jellyfin baseurl not set")
@@ -91,14 +106,11 @@ class Jellyfin:
if not self.token: if not self.token:
raise Exception("Jellyfin token not set") 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: try:
if not session:
async with aiohttp.ClientSession(timeout=self.timeout) as session:
return await self.query(query, query_type, session, identifiers)
results = None results = None
authorization = ( authorization = (
@@ -115,24 +127,24 @@ class Jellyfin:
} }
if query_type == "get": if query_type == "get":
async with session.get( response = self.session.get(
self.baseurl + query, headers=headers self.baseurl + query, headers=headers, timeout=self.timeout
) as response: )
if response.status != 200: if response.status_code != 200:
raise Exception( raise Exception(
f"Query failed with status {response.status} {response.reason}" f"Query failed with status {response.status} {response.reason}"
) )
results = await response.json() results = response.json()
elif query_type == "post": elif query_type == "post":
async with session.post( response = self.session.post(
self.baseurl + query, headers=headers self.baseurl + query, headers=headers, timeout=self.timeout
) as response: )
if response.status != 200: if response.status_code != 200:
raise Exception( raise Exception(
f"Query failed with status {response.status} {response.reason}" 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): if not isinstance(results, list) and not isinstance(results, dict):
raise Exception("Query result is not of type list or dict") raise Exception("Query result is not of type list or dict")
@@ -151,7 +163,7 @@ class Jellyfin:
try: try:
query_string = "/System/Info/Public" query_string = "/System/Info/Public"
response = asyncio.run(self.query(query_string, "get")) response = self.query(query_string, "get")
if response: if response:
return f"{response['ServerName']}: {response['Version']}" return f"{response['ServerName']}: {response['Version']}"
@@ -162,13 +174,12 @@ class Jellyfin:
logger(f"Jellyfin: Get server name failed {e}", 2) logger(f"Jellyfin: Get server name failed {e}", 2)
raise Exception(e) raise Exception(e)
async def get_users(self): def get_users(self):
try: try:
users = {} users = {}
query_string = "/Users" query_string = "/Users"
async with aiohttp.ClientSession(timeout=self.timeout) as session: response = self.query(query_string, "get")
response = await self.query(query_string, "get", session)
# If response is not empty # If response is not empty
if response: if response:
@@ -180,7 +191,7 @@ class Jellyfin:
logger(f"Jellyfin: Get users failed {e}", 2) logger(f"Jellyfin: Get users failed {e}", 2)
raise Exception(e) 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 self, user_name, user_id, library_type, library_id, library_title
): ):
try: try:
@@ -193,54 +204,35 @@ class Jellyfin:
0, 0,
) )
async with aiohttp.ClientSession(timeout=self.timeout) as session:
# Movies # Movies
if library_type == "Movie": if library_type == "Movie":
user_watched[user_name][library_title] = [] user_watched[user_name][library_title] = []
watched = await self.query( watched = self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources", + f"?ParentId={library_id}&Filters=IsPlayed&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
"get", "get",
session,
) )
in_progress = await self.query( in_progress = self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsResumable&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources", + f"?ParentId={library_id}&Filters=IsResumable&IncludeItemTypes=Movie&Recursive=True&Fields=ItemCounts,ProviderIds,MediaSources",
"get", "get",
session,
) )
for movie in watched["Items"]: for movie in watched["Items"] + in_progress["Items"]:
if "MediaSources" in movie and movie["MediaSources"] != {}: if "MediaSources" in movie and movie["MediaSources"] != {}:
# Skip if not watched or watched less than a minute
if (
movie["UserData"]["Played"] == True
or movie["UserData"]["PlaybackPositionTicks"] > 600000000
):
logger( logger(
f"Jellyfin: Adding {movie.get('Name')} to {user_name} watched list", f"Jellyfin: Adding {movie.get('Name')} to {user_name} watched list",
3, 3,
) )
# Get the movie's GUIDs # Get the movie's GUIDs
movie_guids = get_movie_guids(movie) movie_guids = get_guids(movie)
# 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,
)
# Get all partially watched movies greater than 1 minute
for movie in in_progress["Items"]:
if "MediaSources" in movie and movie["MediaSources"] != {}:
if movie["UserData"]["PlaybackPositionTicks"] < 600000000:
continue
logger(
f"Jellyfin: Adding {movie.get('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)
@@ -255,11 +247,10 @@ class Jellyfin:
user_watched[user_name][library_title] = {} user_watched[user_name][library_title] = {}
# Retrieve a list of watched TV shows # Retrieve a list of watched TV shows
watched_shows = await self.query( watched_shows = self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&isPlaceHolder=false&IncludeItemTypes=Series&Recursive=True&Fields=ProviderIds,Path,RecursiveItemCount", + f"?ParentId={library_id}&isPlaceHolder=false&IncludeItemTypes=Series&Recursive=True&Fields=ProviderIds,Path,RecursiveItemCount",
"get", "get",
session,
) )
# Filter the list of shows to only include those that have been partially or fully watched # Filter the list of shows to only include those that have been partially or fully watched
@@ -269,16 +260,14 @@ class Jellyfin:
if show["UserData"]["PlayedPercentage"] > 0: if show["UserData"]["PlayedPercentage"] > 0:
watched_shows_filtered.append(show) watched_shows_filtered.append(show)
# Create a list of tasks to retrieve the seasons of each watched show # Retrieve the seasons of each watched show
seasons_tasks = [] seasons_watched = []
for show in watched_shows_filtered: for show in watched_shows_filtered:
logger( logger(
f"Jellyfin: Adding {show.get('Name')} to {user_name} watched list", f"Jellyfin: Adding {show.get('Name')} to {user_name} watched list",
3, 3,
) )
show_guids = { show_guids = {k.lower(): v for k, v in show["ProviderIds"].items()}
k.lower(): v for k, v in show["ProviderIds"].items()
}
show_guids["title"] = show["Name"] show_guids["title"] = show["Name"]
show_guids["locations"] = ( show_guids["locations"] = (
tuple([show["Path"].split("/")[-1]]) tuple([show["Path"].split("/")[-1]])
@@ -291,19 +280,14 @@ class Jellyfin:
"show_id": show["Id"], "show_id": show["Id"],
} }
season_task = asyncio.ensure_future( seasons_watched.append(
self.query( self.query(
f"/Shows/{show['Id']}/Seasons" f"/Shows/{show['Id']}/Seasons"
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount", + f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,RecursiveItemCount",
"get", "get",
session, identifiers=frozenset(show_identifiers.items()),
frozenset(show_identifiers.items()),
) )
) )
seasons_tasks.append(season_task)
# Retrieve the seasons for each watched show
seasons_watched = await asyncio.gather(*seasons_tasks)
# Filter the list of seasons to only include those that have been partially or fully watched # Filter the list of seasons to only include those that have been partially or fully watched
seasons_watched_filtered = [] seasons_watched_filtered = []
@@ -316,47 +300,40 @@ class Jellyfin:
for season in seasons["Items"]: for season in seasons["Items"]:
if "PlayedPercentage" in season["UserData"]: if "PlayedPercentage" in season["UserData"]:
if season["UserData"]["PlayedPercentage"] > 0: if season["UserData"]["PlayedPercentage"] > 0:
seasons_watched_filtered_dict["Items"].append( seasons_watched_filtered_dict["Items"].append(season)
season
)
if seasons_watched_filtered_dict["Items"]: if seasons_watched_filtered_dict["Items"]:
seasons_watched_filtered.append( seasons_watched_filtered.append(seasons_watched_filtered_dict)
seasons_watched_filtered_dict
)
# Create a list of tasks to retrieve the episodes of each watched season # Create a list of tasks to retrieve the episodes of each watched season
episodes_tasks = [] watched_episodes = []
for seasons in seasons_watched_filtered: for seasons in seasons_watched_filtered:
if len(seasons["Items"]) > 0: if len(seasons["Items"]) > 0:
for season in seasons["Items"]: for season in seasons["Items"]:
if "IndexNumber" not in season:
logger(
f"Jellyfin: Skipping show {season.get('SeriesName')} season {season.get('Name')} as it has no index number",
3,
)
continue
season_identifiers = dict(seasons["Identifiers"]) season_identifiers = dict(seasons["Identifiers"])
season_identifiers["season_index"] = season[ season_identifiers["season_index"] = season["IndexNumber"]
"IndexNumber" watched_task = self.query(
]
watched_task = asyncio.ensure_future(
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&Filters=IsPlayed&Fields=ProviderIds,MediaSources", + f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsPlayed&Fields=ProviderIds,MediaSources",
"get", "get",
session, identifiers=frozenset(season_identifiers.items()),
frozenset(season_identifiers.items()),
) )
)
in_progress_task = asyncio.ensure_future( in_progress_task = 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&Filters=IsResumable&Fields=ProviderIds,MediaSources", + f"?seasonId={season['Id']}&userId={user_id}&isPlaceHolder=false&Filters=IsResumable&Fields=ProviderIds,MediaSources",
"get", "get",
session, identifiers=frozenset(season_identifiers.items()),
frozenset(season_identifiers.items()),
) )
) watched_episodes.append(watched_task)
episodes_tasks.append(watched_task) watched_episodes.append(in_progress_task)
episodes_tasks.append(in_progress_task)
# Retrieve the episodes for each watched season
watched_episodes = await asyncio.gather(*episodes_tasks)
# Iterate through the watched episodes # Iterate through the watched episodes
for episodes in watched_episodes: for episodes in watched_episodes:
@@ -377,7 +354,7 @@ class Jellyfin:
or episode["UserData"]["PlaybackPositionTicks"] or episode["UserData"]["PlaybackPositionTicks"]
> 600000000 > 600000000
): ):
episode_dict = get_episode_guids(episode) episode_dict = get_guids(episode)
# Add the episode dictionary to the season's list of episodes # Add the episode dictionary to the season's list of episodes
season_dict["Episodes"].append(episode_dict) season_dict["Episodes"].append(episode_dict)
@@ -426,7 +403,7 @@ class Jellyfin:
logger(traceback.format_exc(), 2) logger(traceback.format_exc(), 2)
return {} return {}
async def get_users_watched( def get_users_watched(
self, self,
user_name, user_name,
user_id, user_id,
@@ -439,44 +416,38 @@ class Jellyfin:
try: try:
# Get all libraries # Get all libraries
user_name = user_name.lower() user_name = user_name.lower()
tasks_watched = [] watched = []
tasks_libraries = [] libraries = []
async with aiohttp.ClientSession(timeout=self.timeout) as session:
libraries = await self.query(f"/Users/{user_id}/Views", "get", session) all_libraries = self.query(f"/Users/{user_id}/Views", "get")
for library in libraries["Items"]: for library in all_libraries["Items"]:
library_id = library["Id"] library_id = library["Id"]
library_title = library["Name"] library_title = library["Name"]
identifiers = { identifiers = {
"library_id": library_id, "library_id": library_id,
"library_title": library_title, "library_title": library_title,
} }
task = asyncio.ensure_future( libraries.append(
self.query( self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100", + f"?ParentId={library_id}&Filters=IsPlayed&Recursive=True&excludeItemTypes=Folder&limit=100",
"get", "get",
session,
identifiers=identifiers, identifiers=identifiers,
) )
) )
tasks_libraries.append(task)
libraries = await asyncio.gather( for library in libraries:
*tasks_libraries, return_exceptions=True if len(library["Items"]) == 0:
)
for watched in libraries:
if len(watched["Items"]) == 0:
continue continue
library_id = watched["Identifiers"]["library_id"] library_id = library["Identifiers"]["library_id"]
library_title = watched["Identifiers"]["library_title"] library_title = library["Identifiers"]["library_title"]
# Get all library types excluding "Folder" # Get all library types excluding "Folder"
types = set( types = set(
[ [
x["Type"] x["Type"]
for x in watched["Items"] for x in library["Items"]
if x["Type"] in ["Movie", "Series", "Episode"] if x["Type"] in ["Movie", "Series", "Episode"]
] ]
) )
@@ -500,7 +471,7 @@ class Jellyfin:
# If there are multiple types in library raise error # If there are multiple types in library raise error
if types is None or len(types) < 1: if types is None or len(types) < 1:
all_types = set([x["Type"] for x in watched["Items"]]) all_types = set([x["Type"] for x in library["Items"]])
logger( logger(
f"Jellyfin: Skipping Library {library_title} found types: {types}, all types: {all_types}", f"Jellyfin: Skipping Library {library_title} found types: {types}, all types: {all_types}",
1, 1,
@@ -509,7 +480,7 @@ class Jellyfin:
for library_type in types: for library_type in types:
# Get watched for user # Get watched for user
task = asyncio.ensure_future( watched.append(
self.get_user_library_watched( self.get_user_library_watched(
user_name, user_name,
user_id, user_id,
@@ -518,16 +489,13 @@ class Jellyfin:
library_title, library_title,
) )
) )
tasks_watched.append(task)
watched = await asyncio.gather(*tasks_watched, return_exceptions=True)
return watched return watched
except Exception as e: except Exception as e:
logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2) logger(f"Jellyfin: Failed to get users watched, Error: {e}", 2)
raise Exception(e) raise Exception(e)
async def get_watched( def get_watched(
self, self,
users, users,
blacklist_library, blacklist_library,
@@ -553,7 +521,6 @@ class Jellyfin:
) )
) )
watched = await asyncio.gather(*watched, return_exceptions=True)
for user_watched in watched: for user_watched in watched:
user_watched_combine = combine_watched_dicts(user_watched) user_watched_combine = combine_watched_dicts(user_watched)
for user, user_watched_temp in user_watched_combine.items(): for user, user_watched_temp in user_watched_combine.items():
@@ -566,7 +533,7 @@ class Jellyfin:
logger(f"Jellyfin: Failed to get watched, Error: {e}", 2) logger(f"Jellyfin: Failed to get watched, Error: {e}", 2)
raise Exception(e) raise Exception(e)
async def update_user_watched( def update_user_watched(
self, user_name, user_id, library, library_id, videos, dryrun self, user_name, user_id, library, library_id, videos, dryrun
): ):
try: try:
@@ -579,65 +546,34 @@ class Jellyfin:
videos_movies_ids, videos_movies_ids,
) = generate_library_guids_dict(videos) ) = generate_library_guids_dict(videos)
if (
not videos_movies_ids
and not videos_shows_ids
and not videos_episodes_ids
):
logger(
f"Jellyfin: No videos to mark as watched for {user_name} in library {library}",
1,
)
return
logger( logger(
f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}", f"Jellyfin: mark list\nShows: {videos_shows_ids}\nEpisodes: {videos_episodes_ids}\nMovies: {videos_movies_ids}",
1, 1,
) )
async with aiohttp.ClientSession(timeout=self.timeout) as session:
if videos_movies_ids: if videos_movies_ids:
jellyfin_search = await self.query( jellyfin_search = self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}" + f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
+ "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources&IncludeItemTypes=Movie", + "&isPlayed=false&Fields=ItemCounts,ProviderIds,MediaSources&IncludeItemTypes=Movie",
"get", "get",
session,
) )
for jellyfin_video in jellyfin_search["Items"]: for jellyfin_video in jellyfin_search["Items"]:
movie_status = None movie_status = get_video_status(
jellyfin_video, videos_movies_ids, videos
if "MediaSources" in jellyfin_video:
for movie_location in jellyfin_video["MediaSources"]:
if "Path" in movie_location:
if (
contains_nested(
movie_location["Path"].split("/")[-1],
videos_movies_ids["locations"],
) )
is not None
):
for video in videos:
if (
contains_nested(
movie_location["Path"].split("/")[
-1
],
video["locations"],
)
is not None
):
movie_status = video["status"]
break
break
if not movie_status:
for (
movie_provider_source,
movie_provider_id,
) in jellyfin_video["ProviderIds"].items():
if movie_provider_source.lower() in videos_movies_ids:
if (
movie_provider_id.lower()
in videos_movies_ids[
movie_provider_source.lower()
]
):
for video in videos:
if movie_provider_id.lower() in video.get(
movie_provider_source.lower(), []
):
movie_status = video["status"]
break
break
if movie_status: if movie_status:
jellyfin_video_id = jellyfin_video["Id"] jellyfin_video_id = jellyfin_video["Id"]
@@ -645,10 +581,9 @@ class Jellyfin:
msg = f"Jellyfin: {jellyfin_video.get('Name')} as watched for {user_name} in {library}" msg = f"Jellyfin: {jellyfin_video.get('Name')} as watched for {user_name} in {library}"
if not dryrun: if not dryrun:
logger(msg, 5) logger(msg, 5)
await self.query( self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}", f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
"post", "post",
session,
) )
else: else:
logger(msg, 6) logger(msg, 6)
@@ -683,18 +618,19 @@ class Jellyfin:
# TV Shows # TV Shows
if videos_shows_ids and videos_episodes_ids: if videos_shows_ids and videos_episodes_ids:
jellyfin_search = await self.query( jellyfin_search = self.query(
f"/Users/{user_id}/Items" f"/Users/{user_id}/Items"
+ f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}" + f"?SortBy=SortName&SortOrder=Ascending&Recursive=True&ParentId={library_id}"
+ "&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series", + "&Fields=ItemCounts,ProviderIds,Path&IncludeItemTypes=Series",
"get", "get",
session,
) )
jellyfin_shows = [x for x in jellyfin_search["Items"]] jellyfin_shows = [x for x in jellyfin_search["Items"]]
for jellyfin_show in jellyfin_shows: for jellyfin_show in jellyfin_shows:
show_found = False show_found = False
episode_videos = []
if generate_locations:
if "Path" in jellyfin_show: if "Path" in jellyfin_show:
if ( if (
contains_nested( contains_nested(
@@ -704,10 +640,8 @@ class Jellyfin:
is not None is not None
): ):
show_found = True show_found = True
episode_videos = [] for shows, seasons in videos.items():
show = {k: v for k, v in shows}
for show, seasons in videos.items():
show = {k: v for k, v in show}
if ( if (
contains_nested( contains_nested(
jellyfin_show["Path"].split("/")[-1], jellyfin_show["Path"].split("/")[-1],
@@ -719,6 +653,9 @@ class Jellyfin:
for episode in season: for episode in season:
episode_videos.append(episode) episode_videos.append(episode)
break
if generate_guids:
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[
"ProviderIds" "ProviderIds"
@@ -731,7 +668,6 @@ class Jellyfin:
] ]
): ):
show_found = True show_found = True
episode_videos = []
for show, seasons in videos.items(): for show, seasons in videos.items():
show = {k: v for k, v in show} show = {k: v for k, v in show}
if show_provider_id.lower() in show.get( if show_provider_id.lower() in show.get(
@@ -741,83 +677,24 @@ class Jellyfin:
for episode in season: for episode in season:
episode_videos.append(episode) episode_videos.append(episode)
break
if show_found: if show_found:
logger( logger(
f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show.get('Name')}", f"Jellyfin: Updating watched for {user_name} in library {library} for show {jellyfin_show.get('Name')}",
1, 1,
) )
jellyfin_show_id = jellyfin_show["Id"] jellyfin_show_id = jellyfin_show["Id"]
jellyfin_episodes = await self.query( jellyfin_episodes = self.query(
f"/Shows/{jellyfin_show_id}/Episodes" f"/Shows/{jellyfin_show_id}/Episodes"
+ f"?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources", + f"?userId={user_id}&Fields=ItemCounts,ProviderIds,MediaSources",
"get", "get",
session,
) )
for jellyfin_episode in jellyfin_episodes["Items"]: for jellyfin_episode in jellyfin_episodes["Items"]:
episode_status = None episode_status = get_video_status(
jellyfin_episode, videos_episodes_ids, episode_videos
if "MediaSources" in jellyfin_episode:
for episode_location in jellyfin_episode[
"MediaSources"
]:
if "Path" in episode_location:
if (
contains_nested(
episode_location["Path"].split("/")[
-1
],
videos_episodes_ids["locations"],
) )
is not None
):
for episode in episode_videos:
if (
contains_nested(
episode_location[
"Path"
].split("/")[-1],
episode["locations"],
)
is not None
):
episode_status = episode[
"status"
]
break
break
if not episode_status:
for (
episode_provider_source,
episode_provider_id,
) in jellyfin_episode["ProviderIds"].items():
if (
episode_provider_source.lower()
in videos_episodes_ids
):
if (
episode_provider_id.lower()
in videos_episodes_ids[
episode_provider_source.lower()
]
):
for episode in episode_videos:
if (
episode_provider_source.lower()
in episode
):
if (
episode_provider_id.lower()
in episode[
episode_provider_source.lower()
]
):
episode_status = episode[
"status"
]
break
break
if episode_status: if episode_status:
jellyfin_episode_id = jellyfin_episode["Id"] jellyfin_episode_id = jellyfin_episode["Id"]
@@ -828,10 +705,9 @@ class Jellyfin:
) )
if not dryrun: if not dryrun:
logger(msg, 5) logger(msg, 5)
await self.query( self.query(
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}", f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}",
"post", "post",
session,
) )
else: else:
logger(msg, 6) logger(msg, 6)
@@ -874,16 +750,6 @@ class Jellyfin:
3, 3,
) )
if (
not videos_movies_ids
and not videos_shows_ids
and not videos_episodes_ids
):
logger(
f"Jellyfin: No videos to mark as watched for {user_name} in library {library}",
1,
)
except Exception as e: except Exception as e:
logger( logger(
f"Jellyfin: Error updating watched for {user_name} in library {library}, {e}", f"Jellyfin: Error updating watched for {user_name} in library {library}, {e}",
@@ -892,12 +758,10 @@ class Jellyfin:
logger(traceback.format_exc(), 2) logger(traceback.format_exc(), 2)
raise Exception(e) raise Exception(e)
async def update_watched( def update_watched(
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
): ):
try: try:
tasks = []
async with aiohttp.ClientSession(timeout=self.timeout) as session:
for user, libraries in watched_list.items(): for user, libraries in watched_list.items():
logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1) logger(f"Jellyfin: Updating for entry {user}, {libraries}", 1)
user_other = None user_other = None
@@ -909,7 +773,7 @@ class Jellyfin:
user_other = search_mapping(user_mapping, user) user_other = search_mapping(user_mapping, user)
user_id = None user_id = None
for key in self.users.keys(): for key in self.users:
if user.lower() == key.lower(): if user.lower() == key.lower():
user_id = self.users[key] user_id = self.users[key]
user_name = key user_name = key
@@ -923,8 +787,9 @@ class Jellyfin:
logger(f"{user} {user_other} not found in Jellyfin", 2) logger(f"{user} {user_other} not found in Jellyfin", 2)
continue continue
jellyfin_libraries = await self.query( jellyfin_libraries = self.query(
f"/Users/{user_id}/Views", "get", session f"/Users/{user_id}/Views",
"get",
) )
jellyfin_libraries = [x for x in jellyfin_libraries["Items"]] jellyfin_libraries = [x for x in jellyfin_libraries["Items"]]
@@ -968,12 +833,10 @@ class Jellyfin:
continue continue
if library_id: if library_id:
task = self.update_user_watched( self.update_user_watched(
user_name, user_id, library, library_id, videos, dryrun user_name, user_id, library, library_id, videos, dryrun
) )
tasks.append(task)
await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e: except Exception as e:
logger(f"Jellyfin: Error updating watched, {e}", 2) logger(f"Jellyfin: Error updating watched, {e}", 2)
raise Exception(e) raise Exception(e)

View File

@@ -1,4 +1,4 @@
import os, traceback, json, asyncio import os, traceback, json
from dotenv import load_dotenv from dotenv import load_dotenv
from time import sleep, perf_counter from time import sleep, perf_counter
@@ -28,6 +28,8 @@ def setup_users(
): ):
server_1_users = generate_user_list(server_1) server_1_users = generate_user_list(server_1)
server_2_users = generate_user_list(server_2) 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) users = combine_user_lists(server_1_users, server_2_users, user_mapping)
logger(f"User list that exist on both servers {users}", 1) logger(f"User list that exist on both servers {users}", 1)
@@ -180,8 +182,7 @@ def get_server_watched(
library_mapping, library_mapping,
) )
elif server_connection[0] == "jellyfin": elif server_connection[0] == "jellyfin":
return asyncio.run( return server_connection[1].get_watched(
server_connection[1].get_watched(
users, users,
blacklist_library, blacklist_library,
whitelist_library, whitelist_library,
@@ -189,7 +190,6 @@ def get_server_watched(
whitelist_library_type, whitelist_library_type,
library_mapping, library_mapping,
) )
)
def update_server_watched( def update_server_watched(
@@ -204,11 +204,9 @@ def update_server_watched(
server_watched_filtered, user_mapping, library_mapping, dryrun server_watched_filtered, user_mapping, library_mapping, dryrun
) )
elif server_connection[0] == "jellyfin": elif server_connection[0] == "jellyfin":
asyncio.run(
server_connection[1].update_watched( server_connection[1].update_watched(
server_watched_filtered, user_mapping, library_mapping, dryrun server_watched_filtered, user_mapping, library_mapping, dryrun
) )
)
def should_sync_server(server_1_type, server_2_type): def should_sync_server(server_1_type, server_2_type):