forked from ctalkington/python-sonarr
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsonarr.py
More file actions
228 lines (182 loc) · 6.98 KB
/
Copy pathsonarr.py
File metadata and controls
228 lines (182 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
"""Asynchronous Python client for Sonarr."""
import asyncio
import json
from socket import gaierror as SocketGIAError
from typing import Any, List, Mapping, Optional
import aiohttp
import async_timeout
from yarl import URL
from .__version__ import __version__
from .exceptions import SonarrAccessRestricted, SonarrConnectionError, SonarrError
from .models import (
Application,
CommandItem,
Episode,
QueueItem,
SeriesItem,
WantedResults,
)
class Sonarr:
"""Main class for handling connections with Sonarr API."""
_application: Optional[Application] = None
def __init__(
self,
host: str,
api_key: str,
base_path: str = "/api/",
port: int = 8989,
request_timeout: int = 8,
session: aiohttp.client.ClientSession = None,
tls: bool = False,
verify_ssl: bool = True,
user_agent: str = None,
) -> None:
"""Initialize connection with receiver."""
self._session = session
self._close_session = False
self.api_key = api_key
self.base_path = base_path
self.host = host
self.port = port
self.request_timeout = request_timeout
self.tls = tls
self.verify_ssl = verify_ssl
self.user_agent = user_agent
if user_agent is None:
self.user_agent = f"PythonSonarr/{__version__}"
if self.base_path[-1] != "/":
self.base_path += "/"
async def _request(
self,
uri: str = "",
method: str = "GET",
data: Optional[Any] = None,
params: Optional[Mapping[str, str]] = None,
) -> Any:
"""Handle a request to API."""
scheme = "https" if self.tls else "http"
url = URL.build(
scheme=scheme, host=self.host, port=self.port, path=self.base_path
).join(URL(uri))
headers = {
"User-Agent": self.user_agent,
"Accept": "application/json, text/plain, */*",
"X-Api-Key": self.api_key,
}
if self._session is None:
self._session = aiohttp.ClientSession()
self._close_session = True
try:
with async_timeout.timeout(self.request_timeout):
response = await self._session.request(
method,
url,
data=data,
params=params,
headers=headers,
ssl=self.verify_ssl,
)
except asyncio.TimeoutError as exception:
raise SonarrConnectionError(
"Timeout occurred while connecting to API"
) from exception
except (aiohttp.ClientError, SocketGIAError) as exception:
raise SonarrConnectionError(
"Error occurred while communicating with API"
) from exception
if response.status == 403:
raise SonarrAccessRestricted(
"Access restricted. Please ensure valid API Key is provided", {}
)
content_type = response.headers.get("Content-Type", "")
if (response.status // 100) in [4, 5]:
content = await response.read()
response.close()
if content_type == "application/json":
raise SonarrError(
f"HTTP {response.status}", json.loads(content.decode("utf8"))
)
raise SonarrError(
f"HTTP {response.status}",
{
"content-type": content_type,
"message": content.decode("utf8"),
"status-code": response.status,
},
)
if "application/json" in content_type:
data = await response.json()
return data
return await response.text()
@property
def app(self) -> Optional[Application]:
"""Return the cached Application object."""
return self._application
async def update(self, full_update: bool = False) -> Application:
"""Get all information about the application in a single call."""
if self._application is None or full_update:
status = await self._request("system/status")
if status is None:
raise SonarrError("Sonarr returned an empty API status response")
diskspace = await self._request("diskspace")
if not diskspace or diskspace is None:
raise SonarrError("Sonarr returned an empty API diskspace response")
self._application = Application({"info": status, "diskspace": diskspace})
return self._application
diskspace = await self._request("diskspace")
self._application.update_from_dict({"diskspace": diskspace})
return self._application
async def calendar(self, start: str = None, end: str = 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 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")
return [QueueItem.from_dict(result) for result in results]
async def series(self) -> List[SeriesItem]:
"""Return all series."""
results = await self._request("series")
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:
await self._session.close()
async def __aenter__(self) -> "Sonarr":
"""Async enter."""
return self
async def __aexit__(self, *exc_info) -> None:
"""Async exit."""
await self.close()