Jellyfin/Emby: Sync across the view times

Signed-off-by: Luis Garcia <git@luigi311.com>
pull/292/head
Luis Garcia 2025-07-11 23:23:46 +00:00
parent 24f56769f9
commit 75f7f576ac
4 changed files with 105 additions and 52 deletions

View File

@ -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(

View File

@ -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
),
)

View File

@ -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):

View File

@ -1,3 +1,4 @@
from datetime import datetime
import sys
import os
@ -23,6 +24,8 @@ from src.watched import (
cleanup_watched,
)
viewed_date = datetime.today()
tv_shows_watched_list_1: list[Series] = [
Series(
identifiers=MediaIdentifiers(
@ -41,7 +44,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="968589",
tvdb_id="295296",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -51,7 +54,9 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="968590",
tvdb_id="295297",
),
status=WatchedStatus(completed=False, time=240000),
status=WatchedStatus(
completed=False, time=240000, viewed_date=viewed_date
),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -61,7 +66,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="968592",
tvdb_id="295298",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
],
),
@ -82,7 +87,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="4661246",
tvdb_id="10009418",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -92,7 +97,9 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="4712059",
tvdb_id="10009419",
),
status=WatchedStatus(completed=False, time=240000),
status=WatchedStatus(
completed=False, time=240000, viewed_date=viewed_date
),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -102,7 +109,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="4712061",
tvdb_id="10009420",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
],
),
@ -123,7 +130,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="3070048",
tvdb_id="8438181",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -133,7 +140,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="4568681",
tvdb_id="9829910",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -143,7 +150,7 @@ tv_shows_watched_list_1: list[Series] = [
tmdb_id="4497012",
tvdb_id="9870382",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
],
),
@ -170,7 +177,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="295294",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -180,7 +187,9 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="295295",
tmdb_id=None,
),
status=WatchedStatus(completed=False, time=300670),
status=WatchedStatus(
completed=False, time=300670, viewed_date=viewed_date
),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -190,7 +199,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="295298",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
],
),
@ -211,7 +220,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="9959300",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -221,7 +230,9 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="10009417",
tmdb_id=None,
),
status=WatchedStatus(completed=False, time=300741),
status=WatchedStatus(
completed=False, time=300741, viewed_date=viewed_date
),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -231,7 +242,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="10009420",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
],
),
@ -252,7 +263,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="8438181",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -262,7 +273,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="9829910",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -272,7 +283,7 @@ tv_shows_watched_list_2: list[Series] = [
tvdb_id="9870382",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
],
),
@ -299,7 +310,7 @@ expected_tv_show_watched_list_1: list[Series] = [
tmdb_id="968589",
tvdb_id="295296",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -309,7 +320,9 @@ expected_tv_show_watched_list_1: list[Series] = [
tmdb_id="968590",
tvdb_id="295297",
),
status=WatchedStatus(completed=False, time=240000),
status=WatchedStatus(
completed=False, time=240000, viewed_date=viewed_date
),
),
],
),
@ -330,7 +343,7 @@ expected_tv_show_watched_list_1: list[Series] = [
tmdb_id="4661246",
tvdb_id="10009418",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -340,7 +353,9 @@ expected_tv_show_watched_list_1: list[Series] = [
tmdb_id="4712059",
tvdb_id="10009419",
),
status=WatchedStatus(completed=False, time=240000),
status=WatchedStatus(
completed=False, time=240000, viewed_date=viewed_date
),
),
],
),
@ -367,7 +382,7 @@ expected_tv_show_watched_list_2: list[Series] = [
tvdb_id="295294",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -377,7 +392,9 @@ expected_tv_show_watched_list_2: list[Series] = [
tvdb_id="295295",
tmdb_id=None,
),
status=WatchedStatus(completed=False, time=300670),
status=WatchedStatus(
completed=False, time=300670, viewed_date=viewed_date
),
),
],
),
@ -398,7 +415,7 @@ expected_tv_show_watched_list_2: list[Series] = [
tvdb_id="9959300",
tmdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -408,7 +425,9 @@ expected_tv_show_watched_list_2: list[Series] = [
tvdb_id="10009417",
tmdb_id=None,
),
status=WatchedStatus(completed=False, time=300741),
status=WatchedStatus(
completed=False, time=300741, viewed_date=viewed_date
),
),
],
),
@ -426,7 +445,7 @@ movies_watched_list_1: list[MediaItem] = [
tmdb_id="10378",
tvdb_id="12352",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -436,7 +455,7 @@ movies_watched_list_1: list[MediaItem] = [
tmdb_id="1029575",
tvdb_id="351194",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -446,7 +465,7 @@ movies_watched_list_1: list[MediaItem] = [
tmdb_id="466420",
tvdb_id="135852",
),
status=WatchedStatus(completed=False, time=240000),
status=WatchedStatus(completed=False, time=240000, viewed_date=viewed_date),
),
]
@ -462,7 +481,7 @@ movies_watched_list_2: list[MediaItem] = [
tmdb_id="1029575",
tvdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -472,7 +491,7 @@ movies_watched_list_2: list[MediaItem] = [
tmdb_id="507089",
tvdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -482,7 +501,7 @@ movies_watched_list_2: list[MediaItem] = [
tmdb_id="695721",
tvdb_id=None,
),
status=WatchedStatus(completed=False, time=301215),
status=WatchedStatus(completed=False, time=301215, viewed_date=viewed_date),
),
]
@ -498,7 +517,7 @@ expected_movie_watched_list_1: list[MediaItem] = [
tmdb_id="10378",
tvdb_id="12352",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -508,7 +527,7 @@ expected_movie_watched_list_1: list[MediaItem] = [
tmdb_id="466420",
tvdb_id="135852",
),
status=WatchedStatus(completed=False, time=240000),
status=WatchedStatus(completed=False, time=240000, viewed_date=viewed_date),
),
]
@ -524,7 +543,7 @@ expected_movie_watched_list_2: list[MediaItem] = [
tmdb_id="507089",
tvdb_id=None,
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
),
MediaItem(
identifiers=MediaIdentifiers(
@ -534,7 +553,7 @@ expected_movie_watched_list_2: list[MediaItem] = [
tmdb_id="695721",
tvdb_id=None,
),
status=WatchedStatus(completed=False, time=301215),
status=WatchedStatus(completed=False, time=301215, viewed_date=viewed_date),
),
]
@ -562,7 +581,7 @@ tv_shows_2_watched_list_1: list[Series] = [
tmdb_id="282843",
tvdb_id="176357",
),
status=WatchedStatus(completed=True, time=0),
status=WatchedStatus(completed=True, time=0, viewed_date=viewed_date),
)
],
)