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,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,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"
|