ibroadcastaio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.1
2
+ Name: ibroadcastaio
3
+ Version: 0.1.0
4
+ Summary: Client for async communication with the iBroadcast api.
5
+ License: MIT
6
+ Author: Rob Sonke
7
+ Author-email: rob@tigrou.nl
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
16
+ Requires-Dist: pre-commit (>=4.0.1,<5.0.0)
17
+ Requires-Dist: pylint (>=3.3.1,<4.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # iBroadcastAIO
21
+
22
+ A Python library inspired by [ibroadcast-python](https://github.com/ctrueden/ibroadcast-python) for interacting with the iBroadcast API in an async manner.
23
+ This library is initially built for usage in an iBroadcast music provider in [Music Assistant](https://music-assistant.io/).
24
+
25
+ ## Shortcomings of the Current API
26
+
27
+ While the iBroadcast API provides a robust set of features for interacting with the service, it has some shortcomings. One of the main issues is the lack of fine-grained control over data retrieval and manipulation, which can lead to higher memory usage. This library aims to address these issues by providing more efficient data handling in future updates, allowing for better memory management and performance.
28
+
29
+ For more info, see their [documentation](https://devguide.ibroadcast.com/).
30
+
31
+
32
+ ## Installation
33
+
34
+ To see if you already have a virtual env (but `poetry install` should create this for you):
35
+
36
+ ```bash
37
+ poetry env info --path
38
+ source venv/bin/activate
39
+ ```
40
+
41
+ This project uses Poetry for dependency management. To install the dependencies and run the example script:
42
+
43
+ ```bash
44
+ poetry install
45
+ poetry run example
46
+ ```
47
+
48
+ To build and publish the package, use:
49
+
50
+ ```bash
51
+ poetry build
52
+ poetry publish
53
+ ```
54
+
55
+ To run the unit tests:
56
+
57
+ ```bash
58
+ poetry run python -m unittest discover -s tests
59
+ ```
60
+
61
+
62
+ ## Data Structures
63
+
64
+ For a very short and simplified example of the complete library JSON that the API provides, see [example.json](./example.json). Below you will find the fields of each main topic.
65
+
66
+ ### Tracks
67
+
68
+ ```json
69
+ {
70
+ "trashed": 10,
71
+ "track": 0,
72
+ "artists_additional_map": {
73
+ "type": 2,
74
+ "phrase": 1,
75
+ "artist_id": 0
76
+ },
77
+ "type": 17,
78
+ "genre": 3,
79
+ "year": 1,
80
+ "enid": 8,
81
+ "uploaded_time": 19,
82
+ "length": 4,
83
+ "size": 11,
84
+ "uid": 13,
85
+ "path": 12,
86
+ "artwork_id": 6,
87
+ "artists_additional": 20,
88
+ "album_id": 5,
89
+ "uploaded_on": 9,
90
+ "rating": 14,
91
+ "title": 2,
92
+ "artist_id": 7,
93
+ "icatid": 22,
94
+ "genres_additional": 21,
95
+ "plays": 15,
96
+ "file": 16,
97
+ "replay_gain": 18
98
+ }
99
+ ```
100
+
101
+ ### Playlist
102
+
103
+ ```json
104
+ {
105
+ "public_id": 4,
106
+ "uid": 2,
107
+ "sort": 8,
108
+ "system_created": 3,
109
+ "artwork_id": 7,
110
+ "type": 5,
111
+ "tracks": 1,
112
+ "name": 0,
113
+ "description": 6
114
+ }
115
+ ```
116
+
117
+ ### Artist
118
+
119
+ ```json
120
+ {
121
+ "tracks": 1,
122
+ "trashed": 2,
123
+ "artwork_id": 4,
124
+ "rating": 3,
125
+ "name": 0,
126
+ "icatid": 5
127
+ }
128
+ ```
129
+
130
+ ### Album
131
+
132
+ ```json
133
+ {
134
+ "year": 6,
135
+ "name": 0,
136
+ "artists_additional": 7,
137
+ "artists_additional_map": {
138
+ "type": 2,
139
+ "artist_id": 0,
140
+ "phrase": 1
141
+ },
142
+ "tracks": 1,
143
+ "rating": 4,
144
+ "icatid": 8,
145
+ "disc": 5,
146
+ "trashed": 3,
147
+ "artist_id": 2
148
+ }
149
+ ```
150
+
@@ -0,0 +1,130 @@
1
+ # iBroadcastAIO
2
+
3
+ A Python library inspired by [ibroadcast-python](https://github.com/ctrueden/ibroadcast-python) for interacting with the iBroadcast API in an async manner.
4
+ This library is initially built for usage in an iBroadcast music provider in [Music Assistant](https://music-assistant.io/).
5
+
6
+ ## Shortcomings of the Current API
7
+
8
+ While the iBroadcast API provides a robust set of features for interacting with the service, it has some shortcomings. One of the main issues is the lack of fine-grained control over data retrieval and manipulation, which can lead to higher memory usage. This library aims to address these issues by providing more efficient data handling in future updates, allowing for better memory management and performance.
9
+
10
+ For more info, see their [documentation](https://devguide.ibroadcast.com/).
11
+
12
+
13
+ ## Installation
14
+
15
+ To see if you already have a virtual env (but `poetry install` should create this for you):
16
+
17
+ ```bash
18
+ poetry env info --path
19
+ source venv/bin/activate
20
+ ```
21
+
22
+ This project uses Poetry for dependency management. To install the dependencies and run the example script:
23
+
24
+ ```bash
25
+ poetry install
26
+ poetry run example
27
+ ```
28
+
29
+ To build and publish the package, use:
30
+
31
+ ```bash
32
+ poetry build
33
+ poetry publish
34
+ ```
35
+
36
+ To run the unit tests:
37
+
38
+ ```bash
39
+ poetry run python -m unittest discover -s tests
40
+ ```
41
+
42
+
43
+ ## Data Structures
44
+
45
+ For a very short and simplified example of the complete library JSON that the API provides, see [example.json](./example.json). Below you will find the fields of each main topic.
46
+
47
+ ### Tracks
48
+
49
+ ```json
50
+ {
51
+ "trashed": 10,
52
+ "track": 0,
53
+ "artists_additional_map": {
54
+ "type": 2,
55
+ "phrase": 1,
56
+ "artist_id": 0
57
+ },
58
+ "type": 17,
59
+ "genre": 3,
60
+ "year": 1,
61
+ "enid": 8,
62
+ "uploaded_time": 19,
63
+ "length": 4,
64
+ "size": 11,
65
+ "uid": 13,
66
+ "path": 12,
67
+ "artwork_id": 6,
68
+ "artists_additional": 20,
69
+ "album_id": 5,
70
+ "uploaded_on": 9,
71
+ "rating": 14,
72
+ "title": 2,
73
+ "artist_id": 7,
74
+ "icatid": 22,
75
+ "genres_additional": 21,
76
+ "plays": 15,
77
+ "file": 16,
78
+ "replay_gain": 18
79
+ }
80
+ ```
81
+
82
+ ### Playlist
83
+
84
+ ```json
85
+ {
86
+ "public_id": 4,
87
+ "uid": 2,
88
+ "sort": 8,
89
+ "system_created": 3,
90
+ "artwork_id": 7,
91
+ "type": 5,
92
+ "tracks": 1,
93
+ "name": 0,
94
+ "description": 6
95
+ }
96
+ ```
97
+
98
+ ### Artist
99
+
100
+ ```json
101
+ {
102
+ "tracks": 1,
103
+ "trashed": 2,
104
+ "artwork_id": 4,
105
+ "rating": 3,
106
+ "name": 0,
107
+ "icatid": 5
108
+ }
109
+ ```
110
+
111
+ ### Album
112
+
113
+ ```json
114
+ {
115
+ "year": 6,
116
+ "name": 0,
117
+ "artists_additional": 7,
118
+ "artists_additional_map": {
119
+ "type": 2,
120
+ "artist_id": 0,
121
+ "phrase": 1
122
+ },
123
+ "tracks": 1,
124
+ "rating": 4,
125
+ "icatid": 8,
126
+ "disc": 5,
127
+ "trashed": 3,
128
+ "artist_id": 2
129
+ }
130
+ ```
@@ -0,0 +1,7 @@
1
+ """Provide a package for ibroadcastaio."""
2
+
3
+ from .client import IBroadcastClient
4
+
5
+ __all__ = [
6
+ "IBroadcastClient",
7
+ ]
@@ -0,0 +1,196 @@
1
+ import json
2
+ from typing import Any, AsyncGenerator
3
+
4
+ from aiohttp import ClientSession
5
+
6
+ from ibroadcastaio.const import (
7
+ BASE_API_URL,
8
+ BASE_LIBRARY_URL,
9
+ REFERER,
10
+ STATUS_API,
11
+ VERSION,
12
+ )
13
+
14
+
15
+ class IBroadcastClient:
16
+ """iBroadcast API Client to use the API in an async manner"""
17
+
18
+ _user_id = None
19
+ _token = None
20
+
21
+ _albums = None
22
+ _artists = None
23
+ _playlists = None
24
+ _tags = None
25
+ _tracks = None
26
+ _settings = None
27
+
28
+ def __init__(self, http_session: ClientSession) -> None:
29
+ """Main constructor"""
30
+ self.http_session = http_session
31
+
32
+ async def login(self, username: str, password: str) -> dict[str, Any]:
33
+ data = {
34
+ "mode": "status",
35
+ "email_address": username,
36
+ "password": password,
37
+ "version": VERSION,
38
+ "client": REFERER,
39
+ "supported_types": False,
40
+ }
41
+
42
+ status = await self.__post(
43
+ f"{BASE_API_URL}{STATUS_API}", {"content_type": "application/json"}, data
44
+ )
45
+ if "user" not in status:
46
+ raise ValueError("Invalid credentials")
47
+
48
+ # Store the token and user id
49
+ self._token = status["user"]["token"]
50
+ self._user_id = status["user"]["id"]
51
+
52
+ return status
53
+
54
+ async def refresh_library(self):
55
+ """Fetch the library to cache it locally"""
56
+ data = {
57
+ "_token": self._token,
58
+ "_userid": self._user_id,
59
+ "client": REFERER,
60
+ "version": VERSION,
61
+ "mode": "library",
62
+ "supported_types": False,
63
+ }
64
+
65
+ """
66
+ In a future version of this library, mainly once ibroadcast has a more fine grained API, we should not keep the library in memory.
67
+ For now we fetch the complete librady and split it into in memory class members.
68
+ Later, we remove this step and rewrite methods such as _get_albums(album_id) to directly fetch it from the API.
69
+ """
70
+ library = await self.__post(
71
+ f"{BASE_LIBRARY_URL}", {"content_type": "application/json"}, data
72
+ )
73
+
74
+ self._albums = {
75
+ album["album_id"]: album
76
+ async for album in self.__jsonToDict(
77
+ library["library"]["albums"], "album_id"
78
+ )
79
+ }
80
+ self._artists = {
81
+ artist["artist_id"]: artist
82
+ async for artist in self.__jsonToDict(
83
+ library["library"]["artists"], "artist_id"
84
+ )
85
+ }
86
+ self._playlists = {
87
+ playlist["playlist_id"]: playlist
88
+ async for playlist in self.__jsonToDict(
89
+ library["library"]["playlists"], "playlist_id"
90
+ )
91
+ }
92
+ self._tags = {
93
+ tag["tag_id"]: tag
94
+ async for tag in self.__jsonToDict(library["library"]["tags"], "tag_id")
95
+ }
96
+ self._tracks = {
97
+ track["track_id"]: track
98
+ async for track in self.__jsonToDict(
99
+ library["library"]["tracks"], "track_id"
100
+ )
101
+ }
102
+
103
+ self._settings = library["settings"]
104
+
105
+ async def get_settings(self):
106
+ self._check_library_loaded()
107
+ return self._settings
108
+
109
+ async def get_album(self, album_id: int):
110
+ self._check_library_loaded()
111
+ return self._albums.get(album_id)
112
+
113
+ async def __post(
114
+ self,
115
+ url: str,
116
+ headers: dict[str, Any] | None = None,
117
+ data: dict[str, Any] | None = None,
118
+ ):
119
+ async with self.http_session.post(
120
+ url=url, data=json.dumps(data), headers=headers
121
+ ) as response:
122
+ return await response.json()
123
+
124
+ async def __jsonToDict(
125
+ self, data: list[dict[str, Any]], main_key: str
126
+ ) -> AsyncGenerator[dict[str, Any], None]:
127
+ """
128
+ Convert the library json into python dicts. See the readme for all fields.
129
+
130
+ Example Album:
131
+
132
+ data = {
133
+ "12345" : [
134
+ "My album",
135
+ [
136
+ 123,
137
+ 124,
138
+ 125
139
+ ],
140
+ "123",
141
+ false,
142
+ null,
143
+ null,
144
+ null,
145
+ 456,
146
+ 1
147
+ ],
148
+ "map" : {
149
+ "artwork_id" : 7,
150
+ "description" : 6,
151
+ "name" : 0,
152
+ "public_id" : 4,
153
+ "sort" : 8,
154
+ "system_created" : 3,
155
+ "tracks" : 1,
156
+ "type" : 5,
157
+ "uid" : 2
158
+ }
159
+ }
160
+
161
+ will be turned into a dict as:
162
+
163
+ data = {
164
+ "12345" : {
165
+ "album_id" : 12345, ==> this is an extra field, to make life easier
166
+ "name": "My album",
167
+ "tracks": [
168
+ 123,
169
+ 124,
170
+ 125
171
+ ],
172
+ "uid": "123",
173
+ "system_created": false,
174
+ "public_id": null,
175
+ "type": null,
176
+ "description": null,
177
+ "artwork_id": 456,
178
+ "sort": 1
179
+ }
180
+ }
181
+ """
182
+ if "map" not in data or type(data["map"]) is not dict:
183
+ return
184
+
185
+ keymap = {v: k for (k, v) in data["map"].items() if not isinstance(v, dict)}
186
+
187
+ for key, value in data.items():
188
+ if type(value) is list:
189
+ result = {keymap[i]: value[i] for i in range(len(value))}
190
+ result[main_key] = int(key)
191
+ yield result
192
+
193
+ def _check_library_loaded(self):
194
+ """Check if the library is loaded"""
195
+ if self._settings is None:
196
+ raise ValueError("Library not loaded. Please call refresh_library first.")
@@ -0,0 +1,9 @@
1
+ """Constants for the iBroadcast client."""
2
+
3
+ BASE_API_URL = "https://api.ibroadcast.com"
4
+ BASE_LIBRARY_URL = "https://library.ibroadcast.com"
5
+
6
+ STATUS_API = "/s/JSON/status"
7
+
8
+ REFERER = "ibroadcastaio-client"
9
+ VERSION = "0.1.0"
@@ -0,0 +1,27 @@
1
+ [tool.poetry]
2
+ name = "ibroadcastaio"
3
+ version = "0.1.0"
4
+ description = "Client for async communication with the iBroadcast api."
5
+ authors = ["Rob Sonke <rob@tigrou.nl>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ aiohttp = "^3.10.10"
12
+ pre-commit = "^4.0.1"
13
+ pylint = "^3.3.1"
14
+
15
+ [tool.poetry.dev-dependencies]
16
+ pytest = "^6.2.4"
17
+ black = "^24.10.0"
18
+ isort = "^5.13.2"
19
+ flake8 = "^7.1.1"
20
+
21
+ [tool.poetry.scripts]
22
+ example = "example:main"
23
+ test = "unittest:main"
24
+
25
+ [build-system]
26
+ requires = ["poetry-core"]
27
+ build-backend = "poetry.core.masonry.api"