File: __init__.py

package info (click to toggle)
pyforked-daapd 0.1.14-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 92 kB
  • sloc: python: 346; makefile: 7
file content (388 lines) | stat: -rw-r--r-- 15,138 bytes parent folder | download
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
"""This library wraps the forked-daapd API for use with Home Assistant."""
__version__ = "0.1.11"
import asyncio
import concurrent
import logging
from urllib.parse import urljoin

import aiohttp

_LOGGER = logging.getLogger(__name__)


class ForkedDaapdAPI:
    """Class for interfacing with forked-daapd API."""

    def __init__(self, websession, ip_address, api_port, api_password):
        """Initialize the ForkedDaapdAPI object."""
        self._ip_address = ip_address
        self._api_port = api_port
        self._websession = websession
        self._auth = (
            aiohttp.BasicAuth(login="admin", password=api_password)
            if api_password
            else None
        )
        self._api_password = api_password

    @staticmethod
    async def test_connection(websession, host, port, password):
        """Validate the user input."""

        try:
            url = f"http://{host}:{port}/api/config"
            auth = (
                aiohttp.BasicAuth(login="admin", password=password)
                if password
                else None
            )
            # _LOGGER.debug("Trying to connect to %s with auth %s", url, auth)
            async with websession.get(
                url=url, auth=auth, timeout=aiohttp.ClientTimeout(total=5)
            ) as resp:
                json = await resp.json()
                # _LOGGER.debug("JSON %s", json)
                if json["websocket_port"] == 0:
                    return ["websocket_not_enabled"]
                return ["ok", json["library_name"]]
        except (
            aiohttp.ClientConnectionError,
            asyncio.TimeoutError,
            # pylint: disable=protected-access
            concurrent.futures._base.TimeoutError,
            # maybe related to https://github.com/aio-libs/aiohttp/issues/1207
            aiohttp.InvalidURL,
        ):
            return ["wrong_host_or_port"]
        except (aiohttp.ClientResponseError, KeyError):
            if resp.status == 401:
                return ["wrong_password"]
            if resp.status == 403:
                return ["forbidden"]
            return ["wrong_server_type"]
        finally:
            pass
        return ["unknown_error"]

    async def get_request(self, endpoint, params=None) -> dict:
        """Get request from endpoint."""
        url = f"http://{self._ip_address}:{self._api_port}/api/{endpoint}"
        # get params not working so add params ourselves
        if params:
            url += "?" + "&".join(f"{k}={v}" for k, v in params.items())
        try:
            async with self._websession.get(url=url, auth=self._auth) as resp:
                json = await resp.json()
        except (asyncio.TimeoutError, aiohttp.ClientError):
            _LOGGER.error("Can not get %s with params %s", url, params)
            return None
        return json

    async def put_request(self, endpoint, params=None, json=None) -> int:
        """Put request to endpoint."""
        url = f"http://{self._ip_address}:{self._api_port}/api/{endpoint}"
        _LOGGER.debug(
            "PUT request to %s with params %s, json payload %s.", url, params, json
        )
        if params:  # convert bool to text
            params = {
                key: str(value).lower() if isinstance(value, bool) else value
                for key, value in params.items()
            }
        response = await self._websession.put(
            url=url, params=params, json=json, auth=self._auth
        )
        return response.status

    async def post_request(self, endpoint, params=None, json=None) -> int:
        """Post request to endpoint."""
        url = f"http://{self._ip_address}:{self._api_port}/api/{endpoint}"
        _LOGGER.debug(
            "POST request to %s with params %s, data payload %s.", url, params, json
        )
        if params:  # convert bool to text
            params = {
                key: str(value).lower() if isinstance(value, bool) else value
                for key, value in params.items()
            }
        response = await self._websession.post(
            url=url, params=params, json=json, auth=self._auth
        )
        return response.status

    async def start_websocket_handler(
        self,
        ws_port,
        event_types,
        update_callback,
        websocket_reconnect_time,
        disconnected_callback=None,
    ) -> None:
        """Websocket handler daemon."""
        _LOGGER.debug("Starting websocket handler")
        if ws_port == 0:
            _LOGGER.error(
                "This library requires a forked-daapd instance with websocket enabled."
            )
            raise Exception("forked-daapd websocket not enabled.")
        url = f"http://{self._ip_address}:{ws_port}/"
        while True:
            try:
                async with self._websession.ws_connect(
                    url, protocols=("notify",), heartbeat=websocket_reconnect_time
                ) as websocket:
                    await update_callback(
                        event_types
                    )  # send all requested updates once
                    await websocket.send_json(data={"notify": event_types})
                    _LOGGER.debug("Sent notify to %s", url)
                    async for msg in websocket:
                        updates = msg.json()["notify"]
                        _LOGGER.debug("Message JSON: %s", msg.json())
                        await update_callback(updates)
                        _LOGGER.debug("Done with callbacks %s", updates)
            except (asyncio.TimeoutError, aiohttp.ClientError) as exception:
                _LOGGER.warning(
                    "Can not connect to WebSocket at %s, will retry in %s seconds.",
                    url,
                    websocket_reconnect_time,
                )
                _LOGGER.warning("Error %s", repr(exception))
                if disconnected_callback:
                    disconnected_callback()
                await asyncio.sleep(websocket_reconnect_time)
                continue

    async def start_playback(self) -> int:
        """Start playback."""
        status = await self.put_request(endpoint="player/play")
        if status != 204:
            _LOGGER.debug("Unable to start playback.")
        return status

    async def pause_playback(self) -> int:
        """Pause playback."""
        status = await self.put_request(endpoint="player/pause")
        if status != 204:
            _LOGGER.debug("Unable to pause playback.")
        return status

    async def stop_playback(self) -> int:
        """Stop playback."""
        status = await self.put_request(endpoint="player/stop")
        if status != 204:
            _LOGGER.debug("Unable to stop playback.")
        return status

    async def previous_track(self) -> int:
        """Previous track."""
        status = await self.put_request(endpoint="player/previous")
        if status != 204:
            _LOGGER.debug("Unable to skip to previous track.")
        return status

    async def next_track(self) -> int:
        """Next track."""
        status = await self.put_request(endpoint="player/next")
        if status != 204:
            _LOGGER.debug("Unable to skip to next track.")
        return status

    async def seek(self, **kwargs) -> int:
        """Seek."""
        if "position_ms" in kwargs:
            params = {"position_ms": int(kwargs["position_ms"])}
        elif "seek_ms" in kwargs:
            params = {"seek_ms": int(kwargs["seek_ms"])}
        else:
            _LOGGER.error("seek needs either position_ms or seek_ms")
            return -1
        status = await self.put_request(endpoint="player/seek", params=params)
        if status != 204:
            _LOGGER.debug(
                "Unable to seek to %s of %s.",
                next(iter(params.keys())),
                next(iter(params.values())),
            )
        return status

    async def shuffle(self, shuffle) -> int:
        """Shuffle."""
        status = await self.put_request(
            endpoint="player/shuffle", params={"state": shuffle},
        )
        if status != 204:
            _LOGGER.debug("Unable to set shuffle to %s.", shuffle)
        return status

    async def set_enabled_outputs(self, output_ids) -> int:
        """Set enabled outputs."""
        status = await self.put_request(
            endpoint="outputs/set", json={"outputs": output_ids}
        )
        if status != 204:
            _LOGGER.debug("Unable to set enabled outputs for %s.", output_ids)
        return status

    async def set_volume(self, **kwargs) -> int:
        """Set volume."""
        if "volume" in kwargs:
            params = {"volume": int(kwargs["volume"])}
        elif "step" in kwargs:
            params = {"step": int(kwargs["step"])}
        else:
            _LOGGER.error("set_volume needs either volume or step")
            return
        if "output_id" in kwargs:
            params = {**params, **{"output_id": kwargs["output_id"]}}
        status = await self.put_request(endpoint="player/volume", params=params)
        if status != 204:
            _LOGGER.debug("Unable to set volume.")
        return status

    async def get_track_info(self, track_id) -> dict:
        """Get track info."""
        return await self.get_request(endpoint=f"library/tracks/{track_id}")

    async def change_output(self, output_id, selected=None, volume=None) -> int:
        """Change output."""
        json = {} if selected is None else {"selected": selected}
        json = json if volume is None else {**json, **{"volume": int(volume)}}
        status = await self.put_request(endpoint=f"outputs/{output_id}", json=json)
        if status != 204:
            _LOGGER.debug(
                "%s: Unable to change output %s to %s.", status, output_id, json
            )
        return status

    async def add_to_queue(self, uris=None, expression=None, **kwargs) -> int:
        """Add item to queue."""
        if not (uris or expression):
            _LOGGER.error("Either uris or expression must be set.")
            return
        if uris:
            params = {"uris": uris}
        else:
            params = {"expression": expression}
        for field in [
            "playback",
            "playback_from_position",
            "clear",
            "shuffle",
        ]:
            if field in kwargs:
                params[field] = kwargs[field]
        if "position" in kwargs:
            params["position"] = int(kwargs["position"])
        status = await self.post_request(endpoint="queue/items/add", params=params)
        if status != 200:
            _LOGGER.debug("%s: Unable to add items to queue.", status)
        return status

    async def clear_queue(self) -> int:
        """Clear queue."""
        status = await self.put_request(endpoint="queue/clear")
        if status != 204:
            _LOGGER.debug("%s: Unable to clear queue.", status)
        return status

    def full_url(self, url):
        """Get full url (including basic auth) of urls such as artwork_url."""
        creds = f"admin:{self._api_password}@" if self._api_password else ""
        return urljoin(f"http://{creds}{self._ip_address}:{self._api_port}", url)

    async def get_pipes(self, **kwargs) -> []:
        """Get list of pipes."""
        pipes = await self.get_request(
            "search",
            params={"type": "tracks", "expression": "data_kind+is+pipe", **kwargs},
        )
        if pipes:
            return pipes["tracks"]["items"]
        return None

    async def get_playlists(self, **kwargs) -> []:
        """Get list of playlists."""
        playlists = await self.get_request("library/playlists", params=kwargs)
        return playlists.get("items")

    async def get_artists(self, **kwargs) -> []:
        """Get a list of artists."""
        artists = await self.get_request("library/artists", params=kwargs)
        return artists.get("items")

    async def get_albums(self, artist_id=None, **kwargs) -> []:
        """Get a list of albums."""
        if artist_id:
            albums = await self.get_request(
                f"library/artists/{artist_id}/albums", params=kwargs
            )
        else:
            albums = await self.get_request("library/albums", params=kwargs)
        return albums.get("items")

    async def get_genres(self, **kwargs) -> []:
        """Get a list of genres in library."""
        genres = await self.get_request("library/genres", params=kwargs)
        return genres.get("items")

    async def get_genre(self, genre, media_type=None, **kwargs) -> []:
        """Get artists, albums, or tracks in a given genre."""
        params = {
            "expression": f'genre+is+"{genre}"',
            "type": media_type or "artist,album,track",
            **kwargs,
        }
        result = await self.get_request("search", params=params)
        return [
            item
            for sublist in [items_by_type["items"] for items_by_type in result.values()]
            for item in sublist
        ]

    async def get_directory(self, **kwargs) -> []:
        """Get directory contents."""
        return await self.get_request("library/files", params=kwargs)

    async def get_tracks(self, album_id=None, playlist_id=None, **kwargs) -> []:
        """Get a list of tracks from an album or playlist or by genre."""
        item_id = album_id or playlist_id
        if item_id is None:
            return []
        tracks = await self.get_request(
            f"library/{'albums' if album_id else 'playlists'}/{item_id}/tracks",
            params=kwargs,
        )
        return tracks.get("items")

    async def get_track(self, track_id) -> {}:
        """Get track."""
        track = await self.get_request(f"library/tracks/{track_id}")
        return track

    # not used by HA

    async def consume(self, consume) -> int:
        """Consume."""
        status = await self.put_request(
            endpoint="player/consume", params={"state": consume},
        )
        if status != 204:
            _LOGGER.debug("Unable to set consume to %s.", consume)
        return status

    async def repeat(self, repeat) -> int:
        """Repeat. Takes string argument of 'off','all', or 'single'."""
        status = await self.put_request(
            endpoint="player/repeat", params={"state": repeat}
        )
        if status != 204:
            _LOGGER.debug("Unable to set repeat to %s.", repeat)
        return status

    async def toggle_playback(self) -> int:
        """Toggle playback."""
        status = await self.put_request(endpoint="player/toggle")
        if status != 204:
            _LOGGER.debug("Unable to toggle playback.")
        return status