-
Notifications
You must be signed in to change notification settings - Fork 5
/
kodi.py
438 lines (334 loc) · 13.9 KB
/
kodi.py
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
"""Implementation of a Kodi inteface."""
import aiohttp
import urllib
import asyncio
import jsonrpc_base
import jsonrpc_async
import jsonrpc_websocket
def get_kodi_connection(
host, port, ws_port, username, password, ssl=False, timeout=5, session=None
):
"""Returns a Kodi connection."""
if ws_port is None:
return KodiHTTPConnection(host, port, username, password, ssl, timeout, session)
else:
return KodiWSConnection(
host, port, ws_port, username, password, ssl, timeout, session
)
class KodiConnection:
"""A connection to Kodi interface."""
def __init__(self, host, port, username, password, ssl, timeout, session):
"""Initialize the object."""
self._session = session
self._created_session = False
if self._session is None:
self._session = aiohttp.ClientSession()
self._created_session = True
self._kwargs = {"timeout": timeout, "session": self._session}
if username is not None:
self._kwargs["auth"] = aiohttp.BasicAuth(username, password)
image_auth_string = f"{username}:{password}@"
else:
image_auth_string = ""
http_protocol = "https" if ssl else "http"
self._image_url = f"{http_protocol}://{image_auth_string}{host}:{port}/image"
async def connect(self):
"""Connect to kodi."""
pass
async def close(self):
"""Close the connection."""
if self._created_session and self._session is not None:
await self._session.close()
self._session = None
self._created_session = False
@property
def server(self):
raise NotImplementedError
@property
def connected(self):
"""Is the server connected."""
raise NotImplementedError
@property
def can_subscribe(self):
return False
def thumbnail_url(self, thumbnail):
"""Get the URL for a thumbnail."""
if thumbnail is None:
return None
url_components = urllib.parse.urlparse(thumbnail)
if url_components.scheme == "image":
return f"{self._image_url}/{urllib.parse.quote_plus(thumbnail)}"
class KodiHTTPConnection(KodiConnection):
"""An HTTP connection to Kodi."""
def __init__(self, host, port, username, password, ssl, timeout, session):
"""Initialize the object."""
super().__init__(host, port, username, password, ssl, timeout, session)
http_protocol = "https" if ssl else "http"
http_url = f"{http_protocol}://{host}:{port}/jsonrpc"
self._http_server = jsonrpc_async.Server(http_url, **self._kwargs)
@property
def connected(self):
"""Is the server connected."""
return True
async def close(self):
"""Close the connection."""
self._http_server = None
await super().close()
@property
def server(self):
"""Active server for json-rpc requests."""
return self._http_server
class KodiWSConnection(KodiConnection):
"""A WS connection to Kodi."""
def __init__(self, host, port, ws_port, username, password, ssl, timeout, session):
"""Initialize the object."""
super().__init__(host, port, username, password, ssl, timeout, session)
ws_protocol = "wss" if ssl else "ws"
ws_url = f"{ws_protocol}://{host}:{ws_port}/jsonrpc"
self._ws_server = jsonrpc_websocket.Server(ws_url, **self._kwargs)
@property
def connected(self):
"""Return whether websocket is connected."""
return self._ws_server.connected
@property
def can_subscribe(self):
return True
async def connect(self):
"""Connect to kodi over websocket."""
if self.connected:
return
try:
await self._ws_server.ws_connect()
except (
jsonrpc_base.jsonrpc.TransportError,
asyncio.exceptions.CancelledError,
) as error:
raise CannotConnectError from error
async def close(self):
"""Close the connection."""
await self._ws_server.close()
await super().close()
@property
def server(self):
"""Active server for json-rpc requests."""
return self._ws_server
class Kodi:
"""A high level Kodi interface."""
def __init__(self, connection):
"""Initialize the object."""
self._conn = connection
self._server = connection.server
async def ping(self):
"""Ping the server."""
try:
response = await self._server.JSONRPC.Ping()
return response == "pong"
except jsonrpc_base.jsonrpc.TransportError as error:
if "401" in str(error):
raise InvalidAuthError from error
else:
raise CannotConnectError from error
async def get_application_properties(self, properties):
"""Get value of given properties."""
return await self._server.Application.GetProperties(properties)
async def get_player_properties(self, player, properties):
"""Get value of given properties."""
return await self._server.Player.GetProperties(player["playerid"], properties)
async def get_playing_item_properties(self, player, properties):
"""Get value of given properties."""
return (await self._server.Player.GetItem(player["playerid"], properties))[
"item"
]
async def volume_up(self):
"""Send volume up command."""
await self._server.Input.ExecuteAction("volumeup")
async def volume_down(self):
"""Send volume down command."""
await self._server.Input.ExecuteAction("volumedown")
async def set_volume_level(self, volume):
"""Set volume level, range 0-100."""
await self._server.Application.SetVolume(volume)
async def mute(self, mute):
"""Send (un)mute command."""
await self._server.Application.SetMute(mute)
async def _set_play_state(self, state):
players = await self.get_players()
if players:
await self._server.Player.PlayPause(players[0]["playerid"], state)
async def play_pause(self):
"""Send toggle command command."""
await self._set_play_state("toggle")
async def play(self):
"""Send play command."""
await self._set_play_state(True)
async def pause(self):
"""Send pause command."""
await self._set_play_state(False)
async def stop(self):
"""Send stop command."""
players = await self.get_players()
if players:
await self._server.Player.Stop(players[0]["playerid"])
async def _goto(self, direction):
players = await self.get_players()
if players:
if direction == "previous":
# First seek to position 0. Kodi goes to the beginning of the
# current track if the current track is not at the beginning.
await self._server.Player.Seek(
players[0]["playerid"], {"percentage": 0}
)
await self._server.Player.GoTo(players[0]["playerid"], direction)
async def next_track(self):
"""Send next track command."""
await self._goto("next")
async def previous_track(self):
"""Send previous track command."""
await self._goto("previous")
async def media_seek(self, position):
"""Send seek command."""
players = await self.get_players()
time = {"milliseconds": int((position % 1) * 1000)}
position = int(position)
time["seconds"] = int(position % 60)
position /= 60
time["minutes"] = int(position % 60)
position /= 60
time["hours"] = int(position)
if players:
await self._server.Player.Seek(players[0]["playerid"], {"time": time})
async def play_item(self, item):
await self._server.Player.Open(**{"item": item})
async def play_channel(self, channel_id):
"""Play the given channel."""
await self.play_item({"channelid": channel_id})
async def play_playlist(self, playlist_id):
"""Play the given playlist."""
await self.play_item({"playlistid": playlist_id})
async def play_directory(self, directory):
"""Play the given directory."""
await self.play_item({"directory": directory})
async def play_file(self, file):
"""Play the given file."""
await self.play_item({"file": file})
async def set_shuffle(self, shuffle):
"""Set shuffle mode, for the first player."""
players = await self.get_players()
if players:
await self._server.Player.SetShuffle(
**{"playerid": players[0]["playerid"], "shuffle": shuffle}
)
async def call_method(self, method, **kwargs):
"""Run Kodi JSONRPC API method with params."""
if "." not in method or len(method.split(".")) != 2:
raise ValueError(f"Invalid method: {method}")
return await getattr(self._server, method)(**kwargs)
async def _add_item_to_playlist(self, item):
await self._server.Playlist.Add(**{"playlistid": 0, "item": item})
async def add_song_to_playlist(self, song_id):
"""Add song to default playlist (i.e. playlistid=0)."""
await self._add_item_to_playlist({"songid": song_id})
async def add_album_to_playlist(self, album_id):
"""Add album to default playlist (i.e. playlistid=0)."""
await self._add_item_to_playlist({"albumid": album_id})
async def add_artist_to_playlist(self, artist_id):
"""Add album to default playlist (i.e. playlistid=0)."""
await self._add_item_to_playlist({"artistid": artist_id})
async def clear_playlist(self):
"""Clear default playlist (i.e. playlistid=0)."""
await self._server.Playlist.Clear(**{"playlistid": 0})
async def get_artists(self, properties=None):
"""Get artists list."""
return await self._server.AudioLibrary.GetArtists(
**_build_query(properties=properties)
)
async def get_artist_details(self, artist_id=None, properties=None):
"""Get artist details."""
return await self._server.AudioLibrary.GetArtistDetails(
**_build_query(artistid=artist_id, properties=properties)
)
async def get_albums(self, artist_id=None, album_id=None, properties=None):
"""Get albums list."""
filter = {}
if artist_id:
filter["artistid"] = artist_id
if album_id:
filter["albumid"] = album_id
return await self._server.AudioLibrary.GetAlbums(
**_build_query(filter=filter, properties=properties)
)
async def get_album_details(self, album_id, properties=None):
"""Get album details."""
return await self._server.AudioLibrary.GetAlbumDetails(
**_build_query(albumid=album_id, properties=properties)
)
async def get_songs(self, artist_id=None, album_id=None, properties=None):
"""Get songs list."""
filter = {}
if artist_id:
filter["artistid"] = artist_id
if album_id:
filter["albumid"] = album_id
return await self._server.AudioLibrary.GetSongs(
**_build_query(filter=filter, properties=properties)
)
async def get_movies(self, properties=None):
"""Get movies list."""
return await self._server.VideoLibrary.GetMovies(
**_build_query(properties=properties)
)
async def get_movie_details(self, movie_id, properties=None):
"""Get movie details."""
return await self._server.VideoLibrary.GetMovieDetails(
**_build_query(movieid=movie_id, properties=properties)
)
async def get_seasons(self, tv_show_id, properties=None):
"""Get seasons list."""
return await self._server.VideoLibrary.GetSeasons(
**_build_query(tvshowid=tv_show_id, properties=properties)
)
async def get_season_details(self, season_id, properties=None):
"""Get songs list."""
return await self._server.VideoLibrary.GetSeasonDetails(
**_build_query(seasonid=season_id, properties=properties)
)
async def get_episodes(self, tv_show_id, season_id, properties=None):
"""Get episodes list."""
return await self._server.VideoLibrary.GetEpisodes(
**_build_query(tvshowid=tv_show_id, season=season_id, properties=properties)
)
async def get_tv_shows(self, properties=None):
"""Get tv shows list."""
return await self._server.VideoLibrary.GetTVShows(
**_build_query(properties=properties)
)
async def get_tv_show_details(self, tv_show_id=None, properties=None):
"""Get songs list."""
return await self._server.VideoLibrary.GetTVShowDetails(
**_build_query(tvshowid=tv_show_id, properties=properties)
)
async def get_channels(self, channel_group_id, properties=None):
"""Get channels list."""
return await self._server.PVR.GetChannels(
**_build_query(channelgroupid=channel_group_id, properties=properties)
)
async def get_players(self):
"""Return the active player objects."""
return await self._server.Player.GetActivePlayers()
async def send_notification(self, title, message, icon="info", displaytime=10000):
"""Display on-screen message."""
await self._server.GUI.ShowNotification(title, message, icon, displaytime)
def thumbnail_url(self, thumbnail):
"""Get the URL for a thumbnail."""
return self._conn.thumbnail_url(thumbnail)
def _build_query(**kwargs):
"""Build query."""
query = {}
for key, val in kwargs.items():
if val:
query.update({key: val})
return query
class CannotConnectError(Exception):
"""Exception to indicate an error in connection."""
class InvalidAuthError(Exception):
"""Exception to indicate an error in authentication."""