@@ -4903,7 +4903,11 @@ async def _apply_arvio_watched_movie(
49034903 except (TypeError , ValueError ):
49044904 return False
49054905
4906- watched_at = _parse_arvio_timestamp (item .get ("watchedAt" ) or item .get ("timestamp" ) or item .get ("updatedAtMs" ))
4906+ # updatedAt (in addition to updatedAtMs) matters here now too: a completed
4907+ # continue-watching movie routed in via _apply_arvio_playback_progress's
4908+ # high-completion branch may only carry that field, same as the episode
4909+ # version of this fallback chain below.
4910+ watched_at = _parse_arvio_timestamp (item .get ("watchedAt" ) or item .get ("timestamp" ) or item .get ("updatedAtMs" ) or item .get ("updatedAt" ))
49074911
49084912 result = await db .execute (
49094913 select (Media ).where (
@@ -4942,34 +4946,92 @@ async def _apply_arvio_watched_movie(
49424946 return False
49434947
49444948
4949+ def _parse_arvio_episode_info (item : dict [str , Any ] | str | int ) -> tuple [int , int , int ] | None :
4950+ """Extract (show_tmdb_id, season, episode) from various ARVIO item representations."""
4951+ import re
4952+ if isinstance (item , (int , str )):
4953+ item_str = str (item ).strip ()
4954+ match = re .search (r"(?:tv:|series:|tmdb:)?(\d+)[:_\-\s]+(?:s|season)?(\d+)[:_\-\s]+(?:e|ep|episode)?(\d+)" , item_str , re .IGNORECASE )
4955+ if match :
4956+ try :
4957+ return int (match .group (1 )), int (match .group (2 )), int (match .group (3 ))
4958+ except ValueError :
4959+ pass
4960+ try :
4961+ parsed = json .loads (item_str )
4962+ if isinstance (parsed , dict ):
4963+ item = parsed
4964+ except Exception :
4965+ return None
4966+
4967+ if isinstance (item , dict ):
4968+ for field in ("id" , "mediaId" , "episodeId" , "item_id" , "itemId" ):
4969+ val = item .get (field )
4970+ if isinstance (val , str ):
4971+ match = re .search (r"(?:tv:|series:|tmdb:)?(\d+)[:_\-\s]+(?:s|season)?(\d+)[:_\-\s]+(?:e|ep|episode)?(\d+)" , val , re .IGNORECASE )
4972+ if match :
4973+ try :
4974+ return int (match .group (1 )), int (match .group (2 )), int (match .group (3 ))
4975+ except ValueError :
4976+ pass
4977+
4978+ show_tmdb_id_raw = (
4979+ item .get ("showTmdbId" )
4980+ or item .get ("show_tmdb_id" )
4981+ or item .get ("showId" )
4982+ or item .get ("seriesTmdbId" )
4983+ or item .get ("series_tmdb_id" )
4984+ or item .get ("seriesId" )
4985+ or item .get ("series_id" )
4986+ or item .get ("tmdbId" )
4987+ or item .get ("tmdb_id" )
4988+ )
4989+ season_raw = (
4990+ item .get ("season" )
4991+ or item .get ("seasonNumber" )
4992+ or item .get ("season_number" )
4993+ or item .get ("seasonIndex" )
4994+ or item .get ("s" )
4995+ )
4996+ episode_raw = (
4997+ item .get ("episode" )
4998+ or item .get ("episodeNumber" )
4999+ or item .get ("episode_number" )
5000+ or item .get ("episodeIndex" )
5001+ or item .get ("e" )
5002+ )
5003+
5004+ if show_tmdb_id_raw is not None and season_raw is not None and episode_raw is not None :
5005+ try :
5006+ return int (show_tmdb_id_raw ), int (season_raw ), int (episode_raw )
5007+ except (TypeError , ValueError ):
5008+ pass
5009+
5010+ return None
5011+
5012+
49455013async def _apply_arvio_watched_episode (
49465014 db : AsyncSession ,
49475015 user_id : int ,
49485016 item : dict [str , Any ] | int | str ,
49495017 tmdb_api_key : str | None ,
49505018) -> bool :
4951- if not isinstance (item , dict ):
5019+ info = _parse_arvio_episode_info (item )
5020+ if not info :
49525021 return False
49535022
4954- show_tmdb_id_raw = item .get ("showTmdbId" ) or item .get ("show_tmdb_id" ) or item .get ("showId" ) or item .get ("id" )
4955- season_raw = item .get ("season" ) or item .get ("seasonNumber" )
4956- episode_raw = item .get ("episode" ) or item .get ("episodeNumber" )
4957-
4958- if not show_tmdb_id_raw or season_raw is None or episode_raw is None :
4959- return False
4960- try :
4961- show_tmdb_id = int (show_tmdb_id_raw )
4962- season = int (season_raw )
4963- episode = int (episode_raw )
4964- except (TypeError , ValueError ):
4965- return False
5023+ show_tmdb_id , season , episode = info
49665024
4967- watched_at = _parse_arvio_timestamp (item .get ("watchedAt" ) or item .get ("timestamp" ) or item .get ("updatedAtMs" ))
5025+ watched_at = None
5026+ if isinstance (item , dict ):
5027+ watched_at = _parse_arvio_timestamp (item .get ("watchedAt" ) or item .get ("timestamp" ) or item .get ("updatedAtMs" ) or item .get ("updatedAt" ))
49685028
49695029 show_res = await db .execute (select (Show ).where (Show .tmdb_id == show_tmdb_id ))
49705030 show = show_res .scalars ().first ()
49715031 if not show :
4972- show_title = str (item .get ("title" ) or item .get ("showTitle" ) or f"Show { show_tmdb_id } " )
5032+ show_title = f"Show { show_tmdb_id } "
5033+ if isinstance (item , dict ):
5034+ show_title = str (item .get ("title" ) or item .get ("showTitle" ) or item .get ("seriesTitle" ) or show_title )
49735035 show = Show (tmdb_id = show_tmdb_id , title = show_title )
49745036 db .add (show )
49755037 await db .flush ()
@@ -4984,7 +5046,9 @@ async def _apply_arvio_watched_episode(
49845046 )
49855047 media = ep_res .scalars ().first ()
49865048 if not media :
4987- ep_title = str (item .get ("episodeTitle" ) or f"S{ season :02d} E{ episode :02d} " )
5049+ ep_title = f"S{ season :02d} E{ episode :02d} "
5050+ if isinstance (item , dict ):
5051+ ep_title = str (item .get ("episodeTitle" ) or item .get ("title" ) or ep_title )
49885052 media = Media (
49895053 show_id = show .id ,
49905054 season_number = season ,
@@ -5025,6 +5089,11 @@ async def _apply_arvio_playback_progress(
50255089 item : dict [str , Any ] | int | str ,
50265090 tmdb_api_key : str | None ,
50275091) -> bool :
5092+ if isinstance (item , str ):
5093+ try :
5094+ item = json .loads (item )
5095+ except Exception :
5096+ pass
50285097 if not isinstance (item , dict ):
50295098 return False
50305099
@@ -5037,7 +5106,16 @@ async def _apply_arvio_playback_progress(
50375106 except (TypeError , ValueError ):
50385107 progress_pct = 0.0
50395108
5040- if progress_pct < 1.0 or progress_pct >= 90.0 :
5109+ is_completed = item .get ("completed" ) is True or progress_pct >= 85.0
5110+
5111+ if is_completed :
5112+ ep_info = _parse_arvio_episode_info (item )
5113+ if ep_info :
5114+ return await _apply_arvio_watched_episode (db , user_id , item , tmdb_api_key )
5115+ else :
5116+ return await _apply_arvio_watched_movie (db , user_id , item , tmdb_api_key )
5117+
5118+ if progress_pct < 1.0 :
50415119 return False
50425120
50435121 pos_sec = item .get ("resumePositionSeconds" ) or item .get ("positionSeconds" ) or item .get ("position" )
@@ -5063,19 +5141,15 @@ async def _apply_arvio_playback_progress(
50635141 is_episode = (
50645142 media_type_str in ("TV" , "EPISODE" , "SERIES" )
50655143 or (season_raw is not None and episode_raw is not None )
5144+ or _parse_arvio_episode_info (item ) is not None
50665145 )
50675146
50685147 media : Media | None = None
50695148 if is_episode :
5070- show_tmdb_raw = item .get ("showTmdbId" ) or item .get ("show_tmdb_id" ) or item .get ("showId" ) or item .get ("id" )
5071- if not show_tmdb_raw or season_raw is None or episode_raw is None :
5072- return False
5073- try :
5074- show_tmdb_id = int (show_tmdb_raw )
5075- season = int (season_raw )
5076- episode = int (episode_raw )
5077- except (TypeError , ValueError ):
5149+ ep_info = _parse_arvio_episode_info (item )
5150+ if not ep_info :
50785151 return False
5152+ show_tmdb_id , season , episode = ep_info
50795153
50805154 show_res = await db .execute (select (Show ).where (Show .tmdb_id == show_tmdb_id ))
50815155 show = show_res .scalars ().first ()
0 commit comments