diff --git a/src/jellyfin_emby.py b/src/jellyfin_emby.py index 5d65380..3334b12 100644 --- a/src/jellyfin_emby.py +++ b/src/jellyfin_emby.py @@ -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( diff --git a/src/plex.py b/src/plex.py index f873c44..3d9f983 100644 --- a/src/plex.py +++ b/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 + ), ) diff --git a/src/watched.py b/src/watched.py index 89722e8..c2b883a 100644 --- a/src/watched.py +++ b/src/watched.py @@ -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): diff --git a/test/test_watched.py b/test/test_watched.py index 4035872..e4245ab 100644 --- a/test/test_watched.py +++ b/test/test_watched.py @@ -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), ) ], )