22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
.env
|
**.env
|
||||||
*.prof
|
*.prof
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|||||||
473
src/jellyfin.py
473
src/jellyfin.py
@@ -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)
|
||||||
|
|||||||
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 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):
|
||||||
|
|||||||
Reference in New Issue
Block a user