diff --git a/sonarr/models.py b/sonarr/models.py index 8904b618..8b30b960 100644 --- a/sonarr/models.py +++ b/sonarr/models.py @@ -1,6 +1,7 @@ """Models for DirecTV.""" from dataclasses import dataclass +from datetime import datetime from typing import List from .exceptions import SonarrError @@ -26,17 +27,93 @@ def from_dict(data: dict): ) +@dataclass(frozen=True) +class Series: + """Object holding series information from Sonarr.""" + + tvdb_id: int + series_id: int + series_type: str + slug: str + status: str + title: str + overview: str + network: str + runtime: int + timeslot: str + premieres: datetime + path: str + monitored: bool + + @staticmethod + def from_dict(data: dict): + """Return Series object from Sonarr API response.""" + premieres = data.get("firstAired", None) + if premieres is not None: + premieres = datetime.strptime(premieres, "%Y-%m-%dT%H:%M:%S%z") + + return Series( + tvdb_id=data.get("tvdbId", 0), + series_id=data.get("id", 0), + series_type=data.get("seriesType", "unknown"), + slug=data.get("titleSlug", ""), + status=data.get("status", "unknown"), + title=data.get("title", ""), + overview=data.get("overview", ""), + network=data.get("network", "Unknown"), + runtime=data.get("runtime", 0), + timeslot=data.get("airTime", ""), + premieres=premieres, + path=data.get("path", ""), + monitored=data.get("monitored", False), + ) + + +@dataclass(frozen=True) +class Episode: + """Object holding episode information from Sonarr.""" + + tvdb_id: int + episode_id: int + episode_number: int + season_number: int + title: str + overview: str + airs: datetime + downloading: bool + series: Series + + @staticmethod + def from_dict(data: dict): + """Return Episode object from Sonarr API response.""" + airs = data.get("airDateUtc", None) + if airs is not None: + airs = datetime.strptime(airs, "%Y-%m-%dT%H:%M:%S%z") + + return Episode( + tvdb_id=data.get("tvDbEpisodeId", 0), + episode_id=data.get("id", 0), + episode_number=data.get("episodeNumber", 0), + season_number=data.get("seasonNumber", 0), + title=data.get("title", ""), + overview=data.get("overview", ""), + airs=airs, + downloading=data.get("downloading", False), + series=Series.from_dict(data.get("series", {})), + ) + + @dataclass(frozen=True) class Info: """Object holding information from Sonarr.""" - brand: str + app_name: str version: str @staticmethod def from_dict(data: dict): """Return Info object from Sonarr API response.""" - return Info(brand="Sonarr", version=data.get("version", "Unknown")) + return Info(app_name="Sonarr", version=data.get("version", "Unknown")) class Application: diff --git a/sonarr/sonarr.py b/sonarr/sonarr.py index 104b2b07..bbbae2eb 100644 --- a/sonarr/sonarr.py +++ b/sonarr/sonarr.py @@ -2,7 +2,7 @@ import asyncio import json from socket import gaierror as SocketGIAError -from typing import Any, Mapping, Optional +from typing import Any, List, Mapping, Optional import aiohttp import async_timeout @@ -10,7 +10,7 @@ from .__version__ import __version__ from .exceptions import SonarrAccessRestricted, SonarrConnectionError, SonarrError -from .models import Application +from .models import Application, Episode class Sonarr: @@ -137,6 +137,24 @@ async def update(self, full_update: bool = False) -> Application: self._application.update_from_dict({"diskspace": diskspace}) return self._application + async def calendar(self, start: int = None, end: int = None) -> List[Episode]: + """Get upcoming episodes. + + If start/end are not supplied, episodes airing + today and tomorrow will be returned. + """ + params = {} + + if start is not None: + params["start"] = str(start) + + if end is not None: + params["end"] = str(end) + + results = await self._request("calendar", params=params) + + return [Episode.from_dict(result) for result in results] + async def close(self) -> None: """Close open client session.""" if self._session and self._close_session: diff --git a/tests/fixtures/calendar.json b/tests/fixtures/calendar.json new file mode 100644 index 00000000..14b24a68 --- /dev/null +++ b/tests/fixtures/calendar.json @@ -0,0 +1,107 @@ +[ + { + "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", + "cleanTitle": "bobsburgers", + "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": "5:30pm", + "monitored": true, + "qualityProfileId": 1, + "seasonFolder": true, + "lastInfoSync": "2014-01-26T19:25:55.4555946Z", + "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", + "path": "T:\\Bob's Burgers", + "year": 0, + "firstAired": "2011-01-10T01:30:00Z", + "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 + } +] diff --git a/tests/test_models.py b/tests/test_models.py index d41613eb..f23f1485 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,6 @@ """Tests for Sonarr Models.""" import json +from datetime import datetime, timezone import pytest import sonarr.models as models @@ -8,6 +9,7 @@ from . import load_fixture INFO = json.loads(load_fixture("system-status.json")) +CALENDAR = json.loads(load_fixture("calendar.json")) DISKSPACE = json.loads(load_fixture("diskspace.json")) APPLICATION = {"info": INFO, "diskspace": DISKSPACE} @@ -37,10 +39,32 @@ def test_info() -> None: info = models.Info.from_dict(INFO) assert info - assert info.brand == "Sonarr" + assert info.app_name == "Sonarr" assert info.version == "2.0.0.1121" +def test_episode() -> None: + """Test the Episode model.""" + episode = models.Episode.from_dict(CALENDAR[0]) + + 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.""" + + assert episode + assert episode.tvdb_id == 0 + assert episode.episode_id == 14402 + assert episode.episode_number == 11 + assert episode.season_number == 4 + assert isinstance(episode.series, models.Series) + assert episode.title == "Easy Com-mercial, Easy Go-mercial" + assert episode.overview == overview.replace("\n", " ") + assert episode.airs == datetime(2014, 1, 27, 1, 30, tzinfo=timezone.utc) + + def test_disk() -> None: """Test the Disk model.""" disk = models.Disk.from_dict(DISKSPACE[0]) @@ -50,3 +74,30 @@ def test_disk() -> None: assert disk.label == "" assert disk.free == 282500067328 assert disk.total == 499738734592 + + +def test_series() -> None: + """Test the Series model.""" + series = models.Series.from_dict(CALENDAR[0]["series"]) + + 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.""" + + assert series + assert series.monitored + assert series.tvdb_id == 194031 + assert series.series_id == 66 + assert series.series_type == "standard" + assert series.status == "continuing" + assert series.slug == "bobs-burgers" + assert series.title == "Bob's Burgers" + assert series.overview == overview.replace("\n", " ") + assert series.network == "FOX" + assert series.runtime == 30 + assert series.timeslot == "5:30pm" + assert series.premieres == datetime(2011, 1, 10, 1, 30, tzinfo=timezone.utc) + assert series.path == "T:\\Bob's Burgers"