diff --git a/sonarr/models.py b/sonarr/models.py index 30d5bb4d..5a7a3a47 100644 --- a/sonarr/models.py +++ b/sonarr/models.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from datetime import datetime -from typing import List, Optional +from typing import List from .exceptions import SonarrError @@ -33,11 +33,11 @@ class Season: number: int monitored: bool - downloaded: Optional[int] = 0 - episodes: Optional[int] = 0 - total_episodes: Optional[int] = 0 - progress: Optional[int] = 0 - diskspace: Optional[int] = 0 + downloaded: int = 0 + episodes: int = 0 + total_episodes: int = 0 + progress: int = 0 + diskspace: int = 0 @staticmethod def from_dict(data: dict): @@ -183,6 +183,58 @@ def from_dict(data: dict): return Info(app_name="Sonarr", version=data.get("version", "Unknown")) +@dataclass(frozen=True) +class CommandItem: + """Object holding command item information from Sonarr.""" + + command_id: int + name: int + state: str + queued: datetime + started: datetime + changed: datetime + priority: str = "unknown" + trigger: str = "unknown" + message: str = "Not Provided" + send_to_client: bool = False + + @staticmethod + def from_dict(data: dict): + """Return CommandItem object from Sonarr API response.""" + if "started" in data: + started = data.get("started", None) + else: + started = data.get("startedOn", None) + + if "queued" in data: + queued = data.get("queued", None) + else: + queued = started + + if started is not None: + started = datetime.strptime(started, "%Y-%m-%dT%H:%M:%S.%f%z") + + if queued is not None: + queued = datetime.strptime(queued, "%Y-%m-%dT%H:%M:%S.%f%z") + + changed = data.get("stateChangeTime", None) + if changed is not None: + changed = datetime.strptime(changed, "%Y-%m-%dT%H:%M:%S.%f%z") + + return CommandItem( + command_id=data.get("id", 0), + name=data.get("name", "Unknown"), + state=data.get("state", "unknown"), + priority=data.get("priority", "unknown"), + trigger=data.get("trigger", "unknown"), + message=data.get("message", "Not Provided"), + send_to_client=data.get("sendUpdatesToClient", False), + queued=queued, + started=started, + changed=changed, + ) + + @dataclass(frozen=True) class QueueItem: """Object holding queue item information from Sonarr.""" diff --git a/sonarr/sonarr.py b/sonarr/sonarr.py index b25aef4d..b42b0026 100644 --- a/sonarr/sonarr.py +++ b/sonarr/sonarr.py @@ -10,7 +10,14 @@ from .__version__ import __version__ from .exceptions import SonarrAccessRestricted, SonarrConnectionError, SonarrError -from .models import Application, Episode, QueueItem, SeriesItem, WantedResults +from .models import ( + Application, + CommandItem, + Episode, + QueueItem, + SeriesItem, + WantedResults, +) class Sonarr: @@ -155,6 +162,18 @@ async def calendar(self, start: int = None, end: int = None) -> List[Episode]: return [Episode.from_dict(result) for result in results] + async def commands(self) -> List[CommandItem]: + """Query the status of all currently started commands.""" + results = await self._request("command") + + return [CommandItem.from_dict(result) for result in results] + + async def command_status(self, command_id: int) -> CommandItem: + """Query the status of a previously started command.""" + result = await self._request(f"command/{command_id}") + + return CommandItem.from_dict(result) + async def queue(self) -> List[QueueItem]: """Get currently downloading info.""" results = await self._request("queue") diff --git a/tests/fixtures/command-id.json b/tests/fixtures/command-id.json new file mode 100644 index 00000000..1071513e --- /dev/null +++ b/tests/fixtures/command-id.json @@ -0,0 +1,27 @@ +{ + "name": "RefreshSeries", + "message": "Updating The Andy Griffith Show", + "body": { + "isNewSeries": false, + "sendUpdatesToClient": true, + "updateScheduledTask": true, + "completionMessage": "Completed", + "requiresDiskAccess": false, + "isExclusive": false, + "name": "RefreshSeries", + "trigger": "manual", + "suppressMessages": false + }, + "priority": "normal", + "status": "started", + "queued": "2020-04-06T16:57:51.406504Z", + "started": "2020-04-06T16:57:51.417931Z", + "trigger": "manual", + "state": "started", + "manual": true, + "startedOn": "2020-04-06T16:57:51.406504Z", + "stateChangeTime": "2020-04-06T16:57:51.417931Z", + "sendUpdatesToClient": true, + "updateScheduledTask": true, + "id": 368630 +} diff --git a/tests/fixtures/command.json b/tests/fixtures/command.json new file mode 100644 index 00000000..97acc2f9 --- /dev/null +++ b/tests/fixtures/command.json @@ -0,0 +1,36 @@ +[ + { + "name": "RefreshSeries", + "body": { + "isNewSeries": false, + "sendUpdatesToClient": true, + "updateScheduledTask": true, + "completionMessage": "Completed", + "requiresDiskAccess": false, + "isExclusive": false, + "name": "RefreshSeries", + "trigger": "manual", + "suppressMessages": false + }, + "priority": "normal", + "status": "started", + "queued": "2020-04-06T16:54:06.41945Z", + "started": "2020-04-06T16:54:06.421322Z", + "trigger": "manual", + "state": "started", + "manual": true, + "startedOn": "2020-04-06T16:54:06.41945Z", + "stateChangeTime": "2020-04-06T16:54:06.421322Z", + "sendUpdatesToClient": true, + "updateScheduledTask": true, + "id": 368621 + }, + { + "name": "RefreshSeries", + "state": "started", + "startedOn": "2020-04-06T16:57:51.406504Z", + "stateChangeTime": "2020-04-06T16:57:51.417931Z", + "sendUpdatesToClient": true, + "id": 368629 + } +] diff --git a/tests/test_interface.py b/tests/test_interface.py index ba6bbde0..2d45280b 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -50,7 +50,7 @@ async def test_app(aresponses): @pytest.mark.asyncio async def test_calendar(aresponses): - """Test calendar is handled correctly.""" + """Test calendar method is handled correctly.""" aresponses.add( MATCH_HOST, "/api/calendar?start=2014-01-26&end=2014-01-27", @@ -74,9 +74,56 @@ async def test_calendar(aresponses): assert isinstance(response[0], models.Episode) +@pytest.mark.asyncio +async def test_commands(aresponses): + """Test commands method is handled correctly.""" + aresponses.add( + MATCH_HOST, + "/api/command", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "application/json"}, + text=load_fixture("command.json"), + ), + ) + + async with ClientSession() as session: + client = Sonarr(HOST, API_KEY, session=session) + response = await client.commands() + + assert response + assert isinstance(response, List) + + assert response[0] + assert isinstance(response[0], models.CommandItem) + + +@pytest.mark.asyncio +async def test_command_status(aresponses): + """Test command_status method is handled correctly.""" + aresponses.add( + MATCH_HOST, + "/api/command/368630", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "application/json"}, + text=load_fixture("command-id.json"), + ), + ) + + async with ClientSession() as session: + client = Sonarr(HOST, API_KEY, session=session) + response = await client.command_status(368630) + + assert response + assert isinstance(response, models.CommandItem) + + @pytest.mark.asyncio async def test_queue(aresponses): - """Test queue is handled correctly.""" + """Test queue method is handled correctly.""" aresponses.add( MATCH_HOST, "/api/queue", @@ -103,7 +150,7 @@ async def test_queue(aresponses): @pytest.mark.asyncio async def test_series(aresponses): - """Test series is handled correctly.""" + """Test series method is handled correctly.""" aresponses.add( MATCH_HOST, "/api/series", @@ -136,7 +183,7 @@ async def test_series(aresponses): @pytest.mark.asyncio async def test_update(aresponses): - """Test update is handled correctly.""" + """Test update method is handled correctly.""" aresponses.add( MATCH_HOST, "/api/system/status", @@ -187,16 +234,17 @@ async def test_update(aresponses): @pytest.mark.asyncio async def test_wanted(aresponses): - """Test queue is handled correctly.""" + """Test queue method is handled correctly.""" aresponses.add( MATCH_HOST, - "/api/wanted/missing", + "/api/wanted/missing?sortKey=airDateUtc&page=1&pageSize=10&sortDir=desc", "GET", aresponses.Response( status=200, headers={"Content-Type": "application/json"}, text=load_fixture("wanted-missing.json"), ), + match_querystring=True, ) async with ClientSession() as session: diff --git a/tests/test_models.py b/tests/test_models.py index 7f507992..4682ca2c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -11,6 +11,7 @@ INFO = json.loads(load_fixture("system-status.json")) CALENDAR = json.loads(load_fixture("calendar.json")) +COMMAND = json.loads(load_fixture("command.json")) DISKSPACE = json.loads(load_fixture("diskspace.json")) QUEUE = json.loads(load_fixture("queue.json")) SERIES = json.loads(load_fixture("series.json")) @@ -48,6 +49,33 @@ def test_info() -> None: assert info.version == "2.0.0.1121" +def test_command_item() -> None: + """Test the CommandItem model.""" + item = models.CommandItem.from_dict(COMMAND[0]) + + assert item + assert item.name == "RefreshSeries" + assert item.message == "Not Provided" + assert item.state == "started" + assert item.priority == "normal" + assert item.trigger == "manual" + assert item.started == datetime(2020, 4, 6, 16, 54, 6, 421322, tzinfo=timezone.utc) + assert item.queued == datetime(2020, 4, 6, 16, 54, 6, 419450, tzinfo=timezone.utc) + assert item.changed == datetime(2020, 4, 6, 16, 54, 6, 421322, tzinfo=timezone.utc) + + item = models.CommandItem.from_dict(COMMAND[1]) + + assert item + assert item.name == "RefreshSeries" + assert item.message == "Not Provided" + assert item.state == "started" + assert item.priority == "unknown" + assert item.trigger == "unknown" + assert item.started == datetime(2020, 4, 6, 16, 57, 51, 406504, tzinfo=timezone.utc) + assert item.queued == datetime(2020, 4, 6, 16, 57, 51, 406504, tzinfo=timezone.utc) + assert item.changed == datetime(2020, 4, 6, 16, 57, 51, 417931, tzinfo=timezone.utc) + + def test_episode() -> None: """Test the Episode model.""" episode = models.Episode.from_dict(CALENDAR[0])