Jellyfin/Emby: Sync across the view times
Signed-off-by: Luis Garcia <git@luigi311.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
# Functions for Jellyfin and Emby
|
||||
|
||||
from datetime import datetime
|
||||
import requests
|
||||
import traceback
|
||||
import os
|
||||
from math import floor
|
||||
from typing import Any, Literal
|
||||
from packaging.version import parse, Version
|
||||
from loguru import logger
|
||||
from urllib.parse import quote
|
||||
|
||||
from src.functions import (
|
||||
search_mapping,
|
||||
@@ -76,15 +77,21 @@ def get_mediaitem(
|
||||
generate_guids: bool,
|
||||
generate_locations: bool,
|
||||
) -> MediaItem:
|
||||
user_data = item.get("UserData", {})
|
||||
last_played_date = user_data.get("LastPlayedDate")
|
||||
|
||||
viewed_date = datetime.today()
|
||||
if last_played_date:
|
||||
viewed_date = datetime.fromisoformat(last_played_date.replace("Z", "+00:00"))
|
||||
|
||||
return MediaItem(
|
||||
identifiers=extract_identifiers_from_item(
|
||||
server_type, item, generate_guids, generate_locations
|
||||
),
|
||||
status=WatchedStatus(
|
||||
completed=item.get("UserData", {}).get("Played"),
|
||||
time=floor(
|
||||
item.get("UserData", {}).get("PlaybackPositionTicks", 0) / 10000
|
||||
),
|
||||
completed=user_data.get("Played"),
|
||||
time=floor(user_data.get("PlaybackPositionTicks", 0) / 10000),
|
||||
viewed_date=viewed_date,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -311,7 +318,7 @@ class JellyfinEmby:
|
||||
movie_items = []
|
||||
watched_items = self.query(
|
||||
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,UserDataLastPlayedDate",
|
||||
"get",
|
||||
)
|
||||
|
||||
@@ -320,7 +327,7 @@ class JellyfinEmby:
|
||||
|
||||
in_progress_items = self.query(
|
||||
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,UserDataLastPlayedDate",
|
||||
"get",
|
||||
)
|
||||
|
||||
@@ -388,7 +395,7 @@ class JellyfinEmby:
|
||||
|
||||
show_episodes = self.query(
|
||||
f"/Shows/{show.get('Id')}/Episodes"
|
||||
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,MediaSources",
|
||||
+ f"?userId={user_id}&isPlaceHolder=false&Fields=ProviderIds,MediaSources,UserDataLastPlayedDate",
|
||||
"get",
|
||||
)
|
||||
|
||||
@@ -558,11 +565,18 @@ class JellyfinEmby:
|
||||
jelly_identifiers, stored_movie.identifiers
|
||||
):
|
||||
jellyfin_video_id = jellyfin_video.get("Id")
|
||||
|
||||
viewed_date: str = (
|
||||
stored_movie.status.viewed_date.isoformat(
|
||||
timespec="milliseconds"
|
||||
).replace("+00:00", "Z")
|
||||
)
|
||||
|
||||
if stored_movie.status.completed:
|
||||
msg = f"{self.server_type}: {jellyfin_video.get('Name')} as watched for {user_name} in {library_name}"
|
||||
if not dryrun:
|
||||
self.query(
|
||||
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}",
|
||||
f"/Users/{user_id}/PlayedItems/{jellyfin_video_id}?DatePlayed={quote(viewed_date)}",
|
||||
"post",
|
||||
)
|
||||
|
||||
@@ -581,14 +595,15 @@ class JellyfinEmby:
|
||||
msg = f"{self.server_type}: {jellyfin_video.get('Name')} as partially watched for {floor(stored_movie.status.time / 60_000)} minutes for {user_name} in {library_name}"
|
||||
|
||||
if not dryrun:
|
||||
playback_position_payload: dict[str, float] = {
|
||||
user_data_payload: dict[str, float] = {
|
||||
"PlaybackPositionTicks": stored_movie.status.time
|
||||
* 10_000,
|
||||
"LastPlayedDate": viewed_date,
|
||||
}
|
||||
self.query(
|
||||
f"/Users/{user_id}/Items/{jellyfin_video_id}/UserData",
|
||||
"post",
|
||||
json=playback_position_payload,
|
||||
json=user_data_payload,
|
||||
)
|
||||
|
||||
logger.success(f"{'[DRYRUN] ' if dryrun else ''}{msg}")
|
||||
@@ -671,6 +686,13 @@ class JellyfinEmby:
|
||||
stored_ep.identifiers,
|
||||
):
|
||||
jellyfin_episode_id = jellyfin_episode.get("Id")
|
||||
|
||||
viewed_date: str = (
|
||||
stored_ep.status.viewed_date.isoformat(
|
||||
timespec="milliseconds"
|
||||
).replace("+00:00", "Z")
|
||||
)
|
||||
|
||||
if stored_ep.status.completed:
|
||||
msg = (
|
||||
f"{self.server_type}: {jellyfin_episode.get('SeriesName')} {jellyfin_episode.get('SeasonName')} Episode {jellyfin_episode.get('IndexNumber')} {jellyfin_episode.get('Name')}"
|
||||
@@ -678,7 +700,7 @@ class JellyfinEmby:
|
||||
)
|
||||
if not dryrun:
|
||||
self.query(
|
||||
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}",
|
||||
f"/Users/{user_id}/PlayedItems/{jellyfin_episode_id}?DatePlayed={quote(viewed_date)}",
|
||||
"post",
|
||||
)
|
||||
|
||||
@@ -703,14 +725,15 @@ class JellyfinEmby:
|
||||
)
|
||||
|
||||
if not dryrun:
|
||||
playback_position_payload = {
|
||||
user_data_payload = {
|
||||
"PlaybackPositionTicks": stored_ep.status.time
|
||||
* 10_000,
|
||||
"LastPlayedDate": viewed_date,
|
||||
}
|
||||
self.query(
|
||||
f"/Users/{user_id}/Items/{jellyfin_episode_id}/UserData",
|
||||
"post",
|
||||
json=playback_position_payload,
|
||||
json=user_data_payload,
|
||||
)
|
||||
|
||||
logger.success(
|
||||
|
||||
11
src/plex.py
11
src/plex.py
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
@@ -83,11 +84,19 @@ def get_mediaitem(
|
||||
generate_guids: bool = True,
|
||||
generate_locations: bool = True,
|
||||
) -> MediaItem:
|
||||
last_viewed_at = item.lastViewedAt
|
||||
viewed_date = datetime.today()
|
||||
|
||||
if last_viewed_at:
|
||||
viewed_date = last_viewed_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
return MediaItem(
|
||||
identifiers=extract_identifiers_from_item(
|
||||
item, generate_guids, generate_locations
|
||||
),
|
||||
status=WatchedStatus(completed=completed, time=item.viewOffset),
|
||||
status=WatchedStatus(
|
||||
completed=completed, time=item.viewOffset, viewed_date=viewed_date
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
from loguru import logger
|
||||
from typing import Any
|
||||
@@ -21,6 +22,7 @@ class MediaIdentifiers(BaseModel):
|
||||
class WatchedStatus(BaseModel):
|
||||
completed: bool
|
||||
time: int
|
||||
viewed_date: datetime
|
||||
|
||||
|
||||
class MediaItem(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user