diff --git a/sonarr/models.py b/sonarr/models.py index 714da085..30d5bb4d 100644 --- a/sonarr/models.py +++ b/sonarr/models.py @@ -252,6 +252,32 @@ def from_dict(data: dict): ) +@dataclass(frozen=True) +class WantedResults: + """Object holding wanted episode results from Sonarr.""" + + page: int + per_page: int + total: int + sort_key: str + sort_dir: str + episodes: List[Episode] + + @staticmethod + def from_dict(data: dict): + """Return WantedResults object from Sonarr API response.""" + episodes = [Episode.from_dict(episode) for episode in data.get("records", [])] + + return WantedResults( + page=data.get("page", 0), + per_page=data.get("pageSize", 0), + total=data.get("totalRecords", 0), + sort_key=data.get("sortKey", ""), + sort_dir=data.get("sortDirection", ""), + episodes=episodes, + ) + + class Application: """Object holding all information of the Sonarr Application.""" diff --git a/sonarr/sonarr.py b/sonarr/sonarr.py index 63cad1cb..b25aef4d 100644 --- a/sonarr/sonarr.py +++ b/sonarr/sonarr.py @@ -10,7 +10,7 @@ from .__version__ import __version__ from .exceptions import SonarrAccessRestricted, SonarrConnectionError, SonarrError -from .models import Application, Episode, QueueItem, SeriesItem +from .models import Application, Episode, QueueItem, SeriesItem, WantedResults class Sonarr: @@ -167,6 +167,25 @@ async def series(self) -> List[SeriesItem]: return [SeriesItem.from_dict(result) for result in results] + async def wanted( + self, + sort_key: str = "airDateUtc", + page: int = 1, + page_size: int = 10, + sort_dir: str = "desc", + ) -> WantedResults: + """Get wanted missing episodes.""" + params = { + "sortKey": sort_key, + "page": str(page), + "pageSize": str(page_size), + "sortDir": sort_dir, + } + + results = await self._request("wanted/missing", params=params) + + return WantedResults.from_dict(results) + async def close(self) -> None: """Close open client session.""" if self._session and self._close_session: diff --git a/tests/fixtures/wanted-missing.json b/tests/fixtures/wanted-missing.json new file mode 100644 index 00000000..5db7c52f --- /dev/null +++ b/tests/fixtures/wanted-missing.json @@ -0,0 +1,253 @@ +{ + "page": 1, + "pageSize": 10, + "sortKey": "airDateUtc", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "seriesId": 3, + "episodeFileId": 0, + "seasonNumber": 4, + "episodeNumber": 11, + "title": "Easy Com-mercial, Easy Go-mercial", + "airDate": "2014-01-26", + "airDateUtc": "2014-01-27T01:30:00Z", + "overview": "To compete with fellow \"restaurateur,\" Jimmy Pesto, and his blowout Super Bowl event, Bob is determined to create a Bob's Burgers commercial to air during the \"big game.\" In an effort to outshine Pesto, the Belchers recruit Randy, a documentarian, to assist with the filmmaking and hire on former pro football star Connie Frye to be the celebrity endorser.", + "hasFile": false, + "monitored": true, + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "tvdbId": 194031, + "tvRageId": 24607, + "imdbId": "tt1561755", + "title": "Bob's Burgers", + "sortTitle": "bob burgers", + "cleanTitle": "bobsburgers", + "seasonCount": 4, + "status": "continuing", + "overview": "Bob's Burgers follows a third-generation restaurateur, Bob, as he runs Bob's Burgers with the help of his wife and their three kids. Bob and his quirky family have big ideas about burgers, but fall short on service and sophistication. Despite the greasy counters, lousy location and a dearth of customers, Bob and his family are determined to make Bob's Burgers \"grand re-re-re-opening\" a success.", + "airTime": "17:30", + "monitored": true, + "qualityProfileId": 1, + "seasonFolder": true, + "lastInfoSync": "2014-01-26T19:25:55.455594Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us/images/banners/1387.6.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.us/images/posters/1387.6-300.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/images/fanart/1387.6.jpg" + } + ], + "seriesType": "standard", + "network": "FOX", + "useSceneNumbering": false, + "titleSlug": "bobs-burgers", + "certification": "TV-14", + "path": "T:\\Bob's Burgers", + "year": 2011, + "firstAired": "2011-01-10T01:30:00Z", + "genres": [ + "Animation", + "Comedy" + ], + "tags": [], + "added": "2011-01-26T19:25:55.455594Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": true + }, + "seasons": [ + { + "seasonNumber": 4, + "monitored": true + }, + { + "seasonNumber": 3, + "monitored": true + }, + { + "seasonNumber": 2, + "monitored": true + }, + { + "seasonNumber": 1, + "monitored": true + }, + { + "seasonNumber": 0, + "monitored": false + } + ], + "id": 66 + }, + "downloading": false, + "id": 14402 + }, + { + "seriesId": 17, + "episodeFileId": 0, + "seasonNumber": 1, + "episodeNumber": 1, + "title": "The New Housekeeper", + "airDate": "1960-10-03", + "airDateUtc": "1960-10-03T01:00:00Z", + "overview": "Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.", + "hasFile": false, + "monitored": true, + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "imdbId": "", + "tvdbId": 77754, + "tvRageId": 5574, + "tvMazeId": 3853, + "title": "The Andy Griffith Show", + "sortTitle": "andy griffith show", + "cleanTitle": "theandygriffithshow", + "seasonCount": 8, + "status": "ended", + "overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", + "airTime": "21:30", + "monitored": true, + "qualityProfileId": 1, + "seasonFolder": true, + "lastInfoSync": "2016-02-05T16:40:11.614176Z", + "runtime": 25, + "images": [ + { + "coverType": "fanart", + "url": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg" + }, + { + "coverType": "banner", + "url": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg" + }, + { + "coverType": "poster", + "url": "https://artworks.thetvdb.com/banners/posters/77754-4.jpg" + } + ], + "seriesType": "standard", + "network": "CBS", + "useSceneNumbering": false, + "titleSlug": "the-andy-griffith-show", + "certification": "TV-G", + "path": "F:\\The Andy Griffith Show", + "year": 1960, + "firstAired": "1960-02-15T06:00:00Z", + "genres": [ + "Comedy" + ], + "tags": [], + "added": "2008-02-04T13:44:24.204583Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": true + }, + "seasons": [ + { + "seasonNumber": 0, + "monitored": false + }, + { + "seasonNumber": 1, + "monitored": false + }, + { + "seasonNumber": 2, + "monitored": true + }, + { + "seasonNumber": 3, + "monitored": false + }, + { + "seasonNumber": 4, + "monitored": false + }, + { + "seasonNumber": 5, + "monitored": true + }, + { + "seasonNumber": 6, + "monitored": true + }, + { + "seasonNumber": 7, + "monitored": true + }, + { + "seasonNumber": 8, + "monitored": true + } + ], + "id": 17 + }, + "downloading": false, + "id": 889 + } + ] +} diff --git a/tests/test_interface.py b/tests/test_interface.py index 5f3b80ce..ba6bbde0 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -51,28 +51,6 @@ async def test_app(aresponses): @pytest.mark.asyncio async def test_calendar(aresponses): """Test calendar is handled correctly.""" - aresponses.add( - MATCH_HOST, - "/api/system/status", - "GET", - aresponses.Response( - status=200, - headers={"Content-Type": "application/json"}, - text=load_fixture("system-status.json"), - ), - ) - - aresponses.add( - MATCH_HOST, - "/api/diskspace", - "GET", - aresponses.Response( - status=200, - headers={"Content-Type": "application/json"}, - text=load_fixture("diskspace.json"), - ), - ) - aresponses.add( MATCH_HOST, "/api/calendar?start=2014-01-26&end=2014-01-27", @@ -99,28 +77,6 @@ async def test_calendar(aresponses): @pytest.mark.asyncio async def test_queue(aresponses): """Test queue is handled correctly.""" - aresponses.add( - MATCH_HOST, - "/api/system/status", - "GET", - aresponses.Response( - status=200, - headers={"Content-Type": "application/json"}, - text=load_fixture("system-status.json"), - ), - ) - - aresponses.add( - MATCH_HOST, - "/api/diskspace", - "GET", - aresponses.Response( - status=200, - headers={"Content-Type": "application/json"}, - text=load_fixture("diskspace.json"), - ), - ) - aresponses.add( MATCH_HOST, "/api/queue", @@ -148,28 +104,6 @@ async def test_queue(aresponses): @pytest.mark.asyncio async def test_series(aresponses): """Test series is handled correctly.""" - aresponses.add( - MATCH_HOST, - "/api/system/status", - "GET", - aresponses.Response( - status=200, - headers={"Content-Type": "application/json"}, - text=load_fixture("system-status.json"), - ), - ) - - aresponses.add( - MATCH_HOST, - "/api/diskspace", - "GET", - aresponses.Response( - status=200, - headers={"Content-Type": "application/json"}, - text=load_fixture("diskspace.json"), - ), - ) - aresponses.add( MATCH_HOST, "/api/series", @@ -249,3 +183,38 @@ async def test_update(aresponses): assert response assert isinstance(response.info, models.Info) assert isinstance(response.disks, List) + + +@pytest.mark.asyncio +async def test_wanted(aresponses): + """Test queue is handled correctly.""" + aresponses.add( + MATCH_HOST, + "/api/wanted/missing", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "application/json"}, + text=load_fixture("wanted-missing.json"), + ), + ) + + async with ClientSession() as session: + client = Sonarr(HOST, API_KEY, session=session) + response = await client.wanted() + + assert response + assert isinstance(response, models.WantedResults) + + assert response.page == 1 + assert response.per_page == 10 + assert response.total == 2 + assert response.sort_key == "airDateUtc" + assert response.sort_dir == "descending" + + assert response.episodes + assert isinstance(response.episodes, List) + assert len(response.episodes) == 2 + + assert response.episodes[0] + assert isinstance(response.episodes[0], models.Episode) diff --git a/tests/test_models.py b/tests/test_models.py index c107412a..7f507992 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -14,6 +14,7 @@ DISKSPACE = json.loads(load_fixture("diskspace.json")) QUEUE = json.loads(load_fixture("queue.json")) SERIES = json.loads(load_fixture("series.json")) +WANTED = json.loads(load_fixture("wanted-missing.json")) APPLICATION = {"info": INFO, "diskspace": DISKSPACE} @@ -215,3 +216,22 @@ def test_series_item() -> None: assert item.seasons[3].total_episodes == 32 assert item.seasons[3].progress == 100 assert item.seasons[3].diskspace == 8000000000 + + +def test_wanted_results() -> None: + """Test the WantedResults model.""" + results = models.WantedResults.from_dict(WANTED) + + assert results + assert results.page == 1 + assert results.per_page == 10 + assert results.total == 2 + assert results.sort_key == "airDateUtc" + assert results.sort_dir == "descending" + + assert results.episodes + assert isinstance(results.episodes, List) + assert len(results.episodes) == 2 + + assert results.episodes[0] + assert isinstance(results.episodes[0], models.Episode)