Jellyfin/Emby: Check partial sync support
- add `is_partial_update_supported` method to each class to validate given version against earliest known supported version - add `get_server_version` to get server version number - add `update_partial` parameter to user update function, deciding whether or not to allow partial updatespull/185/head
parent
679d3535b1
commit
b1639eab0f
|
|
@ -1,4 +1,5 @@
|
||||||
from src.jellyfin_emby import JellyfinEmby
|
from src.jellyfin_emby import JellyfinEmby
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
|
|
||||||
class Emby(JellyfinEmby):
|
class Emby(JellyfinEmby):
|
||||||
|
|
@ -8,7 +9,7 @@ class Emby(JellyfinEmby):
|
||||||
'Client="JellyPlex-Watched", '
|
'Client="JellyPlex-Watched", '
|
||||||
'Device="script", '
|
'Device="script", '
|
||||||
'DeviceId="script", '
|
'DeviceId="script", '
|
||||||
'Version="0.0.0"'
|
'Version="6.0.2"'
|
||||||
)
|
)
|
||||||
headers = {
|
headers = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
|
|
@ -19,3 +20,6 @@ class Emby(JellyfinEmby):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
server_type="Emby", baseurl=baseurl, token=token, headers=headers
|
server_type="Emby", baseurl=baseurl, token=token, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_partial_update_supported(self, server_version):
|
||||||
|
return server_version > version.parse("4.4")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from src.jellyfin_emby import JellyfinEmby
|
from src.jellyfin_emby import JellyfinEmby
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
class Jellyfin(JellyfinEmby):
|
class Jellyfin(JellyfinEmby):
|
||||||
def __init__(self, baseurl, token):
|
def __init__(self, baseurl, token):
|
||||||
|
|
@ -8,7 +8,7 @@ class Jellyfin(JellyfinEmby):
|
||||||
'Client="JellyPlex-Watched", '
|
'Client="JellyPlex-Watched", '
|
||||||
'Device="script", '
|
'Device="script", '
|
||||||
'DeviceId="script", '
|
'DeviceId="script", '
|
||||||
'Version="5.2.0", '
|
'Version="6.0.2", '
|
||||||
f'Token="{token}"'
|
f'Token="{token}"'
|
||||||
)
|
)
|
||||||
headers = {
|
headers = {
|
||||||
|
|
@ -19,3 +19,6 @@ class Jellyfin(JellyfinEmby):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
server_type="Jellyfin", baseurl=baseurl, token=token, headers=headers
|
server_type="Jellyfin", baseurl=baseurl, token=token, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def is_partial_update_supported(self, server_version):
|
||||||
|
return server_version >= version.parse("10.9.0")
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,6 @@ class JellyfinEmby:
|
||||||
raise Exception(f"{self.server_type} token not set")
|
raise Exception(f"{self.server_type} token not set")
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.version = version.parse(self.info(version=True))
|
|
||||||
self.users = self.get_users()
|
self.users = self.get_users()
|
||||||
|
|
||||||
def query(self, query, query_type, identifiers=None, json=None):
|
def query(self, query, query_type, identifiers=None, json=None):
|
||||||
|
|
@ -178,17 +177,13 @@ class JellyfinEmby:
|
||||||
)
|
)
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
def info(self, version=False) -> str:
|
def info(self) -> str:
|
||||||
try:
|
try:
|
||||||
query_string = "/System/Info/Public"
|
query_string = "/System/Info/Public"
|
||||||
|
|
||||||
response = self.query(query_string, "get")
|
response = self.query(query_string, "get")
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
# Return version only if requested
|
|
||||||
if version:
|
|
||||||
return response['Version']
|
|
||||||
|
|
||||||
return f"{self.server_type} {response['ServerName']}: {response['Version']}"
|
return f"{self.server_type} {response['ServerName']}: {response['Version']}"
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
@ -197,6 +192,19 @@ class JellyfinEmby:
|
||||||
logger(f"{self.server_type}: Get server name failed {e}", 2)
|
logger(f"{self.server_type}: Get server name failed {e}", 2)
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
|
def get_server_version(self):
|
||||||
|
try:
|
||||||
|
response = self.query('/System/Info/Public', 'get')
|
||||||
|
|
||||||
|
if response:
|
||||||
|
return version.parse(response['Version'])
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger(f"{self.server_type}: Get server version failed: {e}", 2)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
def get_users(self):
|
def get_users(self):
|
||||||
try:
|
try:
|
||||||
users = {}
|
users = {}
|
||||||
|
|
@ -505,7 +513,7 @@ class JellyfinEmby:
|
||||||
raise Exception(e)
|
raise Exception(e)
|
||||||
|
|
||||||
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, update_partial, dryrun
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
logger(
|
logger(
|
||||||
|
|
@ -565,33 +573,29 @@ class JellyfinEmby:
|
||||||
library,
|
library,
|
||||||
jellyfin_video.get("Name"),
|
jellyfin_video.get("Name"),
|
||||||
)
|
)
|
||||||
else:
|
elif update_partial:
|
||||||
# Handle partially watched movies not supported in jellyfin < 10.9.0
|
msg = f"{self.server_type}: {jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library}"
|
||||||
if self.server_type == "Jellyfin" and self.version < version.parse("10.9.0"):
|
|
||||||
logger(f"{self.server_type}: Skipping movie {jellyfin_video.get('Name')} as partially watched not supported in Jellyfin < 10.9.0", 4)
|
|
||||||
else:
|
|
||||||
msg = f"{self.server_type}: {jellyfin_video.get('Name')} as partially watched for {floor(movie_status['time'] / 60_000)} minutes for {user_name} in {library}"
|
|
||||||
|
|
||||||
if not dryrun:
|
if not dryrun:
|
||||||
logger(msg, 5)
|
logger(msg, 5)
|
||||||
playback_position_payload = {
|
playback_position_payload = {
|
||||||
"PlaybackPositionTicks": movie_status["time"]
|
"PlaybackPositionTicks": movie_status["time"]
|
||||||
* 10_000,
|
* 10_000,
|
||||||
}
|
}
|
||||||
self.query(
|
self.query(
|
||||||
f"/Users/{user_id}/Items/{jellyfin_video_id}/UserData",
|
f"/Users/{user_id}/Items/{jellyfin_video_id}/UserData",
|
||||||
"post",
|
"post",
|
||||||
json=playback_position_payload,
|
json=playback_position_payload,
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger(msg, 6)
|
|
||||||
|
|
||||||
log_marked(
|
|
||||||
user_name,
|
|
||||||
library,
|
|
||||||
jellyfin_video.get("Name"),
|
|
||||||
duration=floor(movie_status["time"] / 60_000),
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger(msg, 6)
|
||||||
|
|
||||||
|
log_marked(
|
||||||
|
user_name,
|
||||||
|
library,
|
||||||
|
jellyfin_video.get("Name"),
|
||||||
|
duration=floor(movie_status["time"] / 60_000),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger(
|
logger(
|
||||||
f"{self.server_type}: Skipping movie {jellyfin_video.get('Name')} as it is not in mark list for {user_name}",
|
f"{self.server_type}: Skipping movie {jellyfin_video.get('Name')} as it is not in mark list for {user_name}",
|
||||||
|
|
@ -698,39 +702,35 @@ class JellyfinEmby:
|
||||||
jellyfin_episode.get("SeriesName"),
|
jellyfin_episode.get("SeriesName"),
|
||||||
jellyfin_episode.get("Name"),
|
jellyfin_episode.get("Name"),
|
||||||
)
|
)
|
||||||
else:
|
elif update_partial:
|
||||||
# Handle partially watched episodes not supported in jellyfin < 10.9.0
|
msg = (
|
||||||
if self.server_type == "Jellyfin" and self.version < version.parse("10.9.0"):
|
f"{self.server_type}: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
|
||||||
logger(f"{self.server_type}: Skipping episode {jellyfin_episode.get('Name')} as partially watched not supported in Jellyfin < 10.9.0", 4)
|
+ f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dryrun:
|
||||||
|
logger(msg, 5)
|
||||||
|
playback_position_payload = {
|
||||||
|
"PlaybackPositionTicks": episode_status[
|
||||||
|
"time"
|
||||||
|
]
|
||||||
|
* 10_000,
|
||||||
|
}
|
||||||
|
self.query(
|
||||||
|
f"/Users/{user_id}/Items/{jellyfin_episode_id}/UserData",
|
||||||
|
"post",
|
||||||
|
json=playback_position_payload,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
msg = (
|
logger(msg, 6)
|
||||||
f"{self.server_type}: {jellyfin_episode['SeriesName']} {jellyfin_episode['SeasonName']} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
|
|
||||||
+ f" as partially watched for {floor(episode_status['time'] / 60_000)} minutes for {user_name} in {library}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not dryrun:
|
log_marked(
|
||||||
logger(msg, 5)
|
user_name,
|
||||||
playback_position_payload = {
|
library,
|
||||||
"PlaybackPositionTicks": episode_status[
|
jellyfin_episode.get("SeriesName"),
|
||||||
"time"
|
jellyfin_episode.get("Name"),
|
||||||
]
|
duration=floor(episode_status["time"] / 60_000),
|
||||||
* 10_000,
|
)
|
||||||
}
|
|
||||||
self.query(
|
|
||||||
f"/Users/{user_id}/Items/{jellyfin_episode_id}/UserData",
|
|
||||||
"post",
|
|
||||||
json=playback_position_payload,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger(msg, 6)
|
|
||||||
|
|
||||||
log_marked(
|
|
||||||
user_name,
|
|
||||||
library,
|
|
||||||
jellyfin_episode.get("SeriesName"),
|
|
||||||
jellyfin_episode.get("Name"),
|
|
||||||
duration=floor(episode_status["time"] / 60_000),
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger(
|
logger(
|
||||||
f"{self.server_type}: Skipping episode {jellyfin_episode.get('Name')} as it is not in mark list for {user_name}",
|
f"{self.server_type}: Skipping episode {jellyfin_episode.get('Name')} as it is not in mark list for {user_name}",
|
||||||
|
|
@ -754,6 +754,15 @@ class JellyfinEmby:
|
||||||
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
|
self, watched_list, user_mapping=None, library_mapping=None, dryrun=False
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
|
server_version = self.get_server_version()
|
||||||
|
update_partial = self.is_partial_update_supported(server_version)
|
||||||
|
|
||||||
|
if not update_partial:
|
||||||
|
logger(
|
||||||
|
f"{self.server_type}: Server version {server_version} does not support updating playback position.",
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
for user, libraries in watched_list.items():
|
for user, libraries in watched_list.items():
|
||||||
logger(f"{self.server_type}: Updating for entry {user}, {libraries}", 1)
|
logger(f"{self.server_type}: Updating for entry {user}, {libraries}", 1)
|
||||||
user_other = None
|
user_other = None
|
||||||
|
|
@ -826,7 +835,13 @@ class JellyfinEmby:
|
||||||
|
|
||||||
if library_id:
|
if library_id:
|
||||||
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,
|
||||||
|
update_partial,
|
||||||
|
dryrun
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue