Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions resources/lib/traktapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ def _get_all_pages(
authorized: bool = False,
timeout: int = 90,
limit: int = 100,
params: Optional[Dict] = None,
) -> List:
if not self.client:
return []
Expand All @@ -412,7 +413,10 @@ def _get_all_pages(
page = 1
page_count = 1
while page <= page_count:
page_path = self.client.build_path(path, {"page": page, "limit": limit})
query = {"page": page, "limit": limit}
if params:
query.update(params)
page_path = self.client.build_path(path, query)
response = self._get(
page_path,
authorized=authorized,
Expand Down Expand Up @@ -595,8 +599,8 @@ def scrobbleMovie(self, movie: Dict, percent: float, status: str) -> Optional[Di
)

def getShowsCollected(self, shows: Dict) -> Dict:
for item in (
self._get("/sync/collection/shows", authorized=True, timeout=90) or []
for item in self._get_all_pages(
"/sync/collection/shows", authorized=True, timeout=90
):
self._merge_show(shows, item, ("collected_at",))
return shows
Expand All @@ -609,15 +613,23 @@ def getMoviesCollected(self, movies: Dict) -> Dict:
return movies

def getShowsWatched(self, shows: Dict) -> Dict:
for item in self._get("/sync/watched/shows", authorized=True, timeout=90) or []:
# extended=progress is required for the season/episode breakdown; without
# it Trakt returns show-level plays only and episode watched-state can't
# be synced. All sync endpoints are also paginated, so page through them.
for item in self._get_all_pages(
"/sync/watched/shows",
authorized=True,
timeout=90,
params={"extended": "progress"},
):
self._merge_show(
shows, item, ("plays", "last_watched_at", "last_updated_at", "reset_at")
)
return shows

def getMoviesWatched(self, movies: Dict) -> Dict:
for item in (
self._get("/sync/watched/movies", authorized=True, timeout=90) or []
for item in self._get_all_pages(
"/sync/watched/movies", authorized=True, timeout=90
):
self._merge_object(
movies, item, "movie", ("plays", "last_watched_at", "last_updated_at")
Expand Down
66 changes: 66 additions & 0 deletions tests/test_traktapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,23 @@ def test_get_all_pages_follows_pagination_headers():
)


def test_get_all_pages_merges_extra_params_into_query():
api = traktAPI.__new__(traktAPI)
api.client = TraktClient("id", "secret", "ua")
api._get = mock.Mock(return_value=([{"title": "One"}], {}))

api._get_all_pages(
"/sync/watched/shows", authorized=True, params={"extended": "progress"}
)

api._get.assert_called_once_with(
"/sync/watched/shows?page=1&limit=100&extended=progress",
authorized=True,
timeout=90,
include_headers=True,
)


def test_get_all_ratings_fetches_current_rating_bucket_endpoints():
api = traktAPI.__new__(traktAPI)
api._get_all_pages = mock.Mock(return_value=[])
Expand Down Expand Up @@ -269,6 +286,55 @@ def test_episode_playback_progress_uses_pagination():
assert episode["progress"] == 50


def test_get_shows_watched_requests_progress_and_paginates():
api = traktAPI.__new__(traktAPI)
api._get_all_pages = mock.Mock(
return_value=[
{
"plays": 3,
"last_watched_at": "2026-01-01T00:00:00.000Z",
"show": {"title": "Show", "ids": {"trakt": 1}},
"seasons": [{"number": 1, "episodes": [{"number": 1, "plays": 1}]}],
}
]
)

result = api.getShowsWatched({})

# extended=progress is what restores the season/episode breakdown (Trakt 2026
# API change); without it episode watched-state can't be synced.
api._get_all_pages.assert_called_once_with(
"/sync/watched/shows",
authorized=True,
timeout=90,
params={"extended": "progress"},
)
episode = result[1].to_dict()["seasons"][0]["episodes"][0]
assert episode["plays"] == 1


def test_get_shows_collected_paginates():
api = traktAPI.__new__(traktAPI)
api._get_all_pages = mock.Mock(return_value=[])

api.getShowsCollected({})

api._get_all_pages.assert_called_once_with(
"/sync/collection/shows", authorized=True, timeout=90
)


def test_get_movies_watched_paginates():
api = traktAPI.__new__(traktAPI)
api._get_all_pages = mock.Mock(return_value=[])

api.getMoviesWatched({})

api._get_all_pages.assert_called_once_with(
"/sync/watched/movies", authorized=True, timeout=90
)


def test_client_retries_once_after_rate_limit_retry_after():
client = TraktClient("id", "secret", "ua")
rate_limit = urllib.error.HTTPError(
Expand Down