-
Notifications
You must be signed in to change notification settings - Fork 132
Sync watched status with Plex Discover #2435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ConwayJ18
wants to merge
3
commits into
Taxel:main
Choose a base branch
from
ConwayJ18:plex-discover-watched-sync
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from plextraktsync.decorators.flatten import flatten_dict | ||
|
|
||
| if TYPE_CHECKING: | ||
| from plextraktsync.trakt.TraktApi import TraktApi | ||
|
|
||
|
|
||
| class TraktWatchedCollection(dict): | ||
| """ | ||
| Lazy-loaded mapping: | ||
| ["movies", "episodes"] => {trakt_id: TraktMedia} | ||
| (values are the raw pytrakt Movie/TVEpisode objects returned by the API). | ||
| """ | ||
|
Comment on lines
+12
to
+16
|
||
|
|
||
| def __init__(self, trakt: TraktApi): | ||
| super().__init__() | ||
| self.trakt = trakt | ||
|
|
||
| def __missing__(self, media_type: str): | ||
| self[media_type] = items = self.watched_items(media_type) | ||
|
|
||
| return items | ||
|
|
||
| @flatten_dict | ||
| def watched_items(self, media_type: str): | ||
| if media_type == "movies": | ||
| for movie in self.trakt.me.watched_movies: | ||
| yield movie.trakt, movie | ||
| elif media_type == "episodes": | ||
| for episode in self.trakt.me.watched_episodes: | ||
| yield episode.trakt, episode | ||
| else: | ||
| raise ValueError(f"Unsupported media type: {media_type}") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| #!/usr/bin/env python3 -m pytest | ||
| """Unit tests for TraktWatchedCollection with mocked Trakt API responses. | ||
|
|
||
| These tests use MagicMock to simulate Trakt API responses without making | ||
| real API calls. They run in CI and provide fast, reliable test coverage. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from unittest.mock import MagicMock | ||
|
|
||
| import pytest | ||
|
|
||
| from plextraktsync.trakt.TraktWatchedCollection import TraktWatchedCollection | ||
|
|
||
|
|
||
| def test_trakt_watched_collection_movies_with_sample_data(): | ||
| """Test movies are correctly indexed by trakt ID using sample Trakt API data.""" | ||
| # Mock Trakt API with real-world sample watched movies | ||
| # Sample structure: Trakt API returns watched movies with trakt ID and metadata | ||
| mock_trakt = MagicMock() | ||
|
|
||
| # Create mock movies using real-world-like structure | ||
| mock_movie1 = MagicMock() | ||
| mock_movie1.trakt = 278 # The Shawshank Redemption | ||
| mock_movie1.title = "The Shawshank Redemption" | ||
| mock_movie1.year = 1994 | ||
|
|
||
| mock_movie2 = MagicMock() | ||
| mock_movie2.trakt = 278945 # Inception | ||
| mock_movie2.title = "Inception" | ||
| mock_movie2.year = 2010 | ||
|
|
||
| # Configure mock API to return these movies | ||
| mock_trakt.me.watched_movies = [mock_movie1, mock_movie2] | ||
|
|
||
| collection = TraktWatchedCollection(mock_trakt) | ||
| movies = collection["movies"] | ||
|
|
||
| # Verify structure and content match expected behavior | ||
| assert isinstance(movies, dict) | ||
| assert len(movies) == 2 | ||
| assert 278 in movies | ||
| assert 278945 in movies | ||
| assert movies[278] is mock_movie1 | ||
| assert movies[278945] is mock_movie2 | ||
|
|
||
|
|
||
| def test_trakt_watched_collection_episodes_with_sample_data(): | ||
| """Test episodes are correctly indexed by trakt ID using sample Trakt API data.""" | ||
| # Mock Trakt API with real-world sample watched episodes | ||
| mock_trakt = MagicMock() | ||
|
|
||
| # Create mock episodes using real-world-like structure | ||
| # Episodes typically have: trakt ID, show info, season, number, etc. | ||
| mock_episode1 = MagicMock() | ||
| mock_episode1.trakt = 73641 # Game of Thrones S01E01 | ||
| mock_episode1.show = MagicMock() | ||
| mock_episode1.show.title = "Game of Thrones" | ||
| mock_episode1.season = 1 | ||
| mock_episode1.number = 1 | ||
|
|
||
| mock_episode2 = MagicMock() | ||
| mock_episode2.trakt = 73642 # Game of Thrones S01E02 | ||
| mock_episode2.show = MagicMock() | ||
| mock_episode2.show.title = "Game of Thrones" | ||
| mock_episode2.season = 1 | ||
| mock_episode2.number = 2 | ||
|
|
||
| # Configure mock API to return these episodes | ||
| mock_trakt.me.watched_episodes = [mock_episode1, mock_episode2] | ||
|
|
||
| collection = TraktWatchedCollection(mock_trakt) | ||
| episodes = collection["episodes"] | ||
|
|
||
| # Verify structure and content match expected behavior | ||
| assert isinstance(episodes, dict) | ||
| assert len(episodes) == 2 | ||
| assert 73641 in episodes | ||
| assert 73642 in episodes | ||
| assert episodes[73641] is mock_episode1 | ||
| assert episodes[73642] is mock_episode2 | ||
|
|
||
|
|
||
| def test_trakt_watched_collection_empty(): | ||
| """Test handling of empty watched collections.""" | ||
| # Mock Trakt API with no watched items | ||
| mock_trakt = MagicMock() | ||
| mock_trakt.me.watched_movies = [] | ||
| mock_trakt.me.watched_episodes = [] | ||
|
|
||
| collection = TraktWatchedCollection(mock_trakt) | ||
| movies = collection["movies"] | ||
| episodes = collection["episodes"] | ||
|
|
||
| # Verify empty dicts are returned correctly | ||
| assert isinstance(movies, dict) | ||
| assert len(movies) == 0 | ||
| assert isinstance(episodes, dict) | ||
| assert len(episodes) == 0 | ||
|
|
||
|
|
||
| def test_trakt_watched_collection_invalid_media_type(): | ||
| """Test that unsupported media types raise appropriate errors.""" | ||
| mock_trakt = MagicMock() | ||
| collection = TraktWatchedCollection(mock_trakt) | ||
|
|
||
| # Verify only "movies" and "episodes" are supported | ||
| with pytest.raises(ValueError, match="Unsupported media type: invalid"): | ||
| _ = collection["invalid"] | ||
|
|
||
| # Verify other unsupported types also fail | ||
| with pytest.raises(ValueError, match="Unsupported media type: shows"): | ||
| _ = collection["shows"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| #!/usr/bin/env python3 -m pytest | ||
| """Integration tests for TraktWatchedCollection using real Trakt API. | ||
|
|
||
| These tests validate TraktWatchedCollection against real Trakt API responses. | ||
| They are skipped in CI to avoid dependency on external API availability. | ||
| Run locally with: pytest tests/test_trakt_watched_collection_integration.py -v | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
|
|
||
| import pytest | ||
|
|
||
| from plextraktsync.trakt.TraktWatchedCollection import TraktWatchedCollection | ||
|
|
||
|
|
||
| @pytest.mark.skipif(os.environ.get("CI"), reason="Requires real Trakt API") | ||
| def test_trakt_watched_collection_movies(trakt_api): | ||
| """Validate that movies are correctly loaded from real Trakt API.""" | ||
| collection = TraktWatchedCollection(trakt_api) | ||
| movies = collection["movies"] | ||
| assert isinstance(movies, dict) | ||
|
|
||
|
|
||
| @pytest.mark.skipif(os.environ.get("CI"), reason="Requires real Trakt API") | ||
| def test_trakt_watched_collection_episodes(trakt_api): | ||
| """Validate that episodes are correctly loaded from real Trakt API.""" | ||
| collection = TraktWatchedCollection(trakt_api) | ||
| episodes = collection["episodes"] | ||
| assert isinstance(episodes, dict) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not feel particularly confident implementing ratings. My chosen fix here was to update the PR title.