loudly-py-sdk 0.1.0__py3-none-any.whl
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.
loudly_py_sdk/client.py
ADDED
@@ -0,0 +1,401 @@
|
|
1
|
+
import os
|
2
|
+
import requests
|
3
|
+
from typing import Optional, Dict, Any, List
|
4
|
+
from dotenv import load_dotenv
|
5
|
+
from .exceptions import LoudlyAPIError
|
6
|
+
|
7
|
+
# Load .env variables automatically
|
8
|
+
load_dotenv()
|
9
|
+
|
10
|
+
|
11
|
+
class LoudlyClient:
|
12
|
+
DEFAULT_BASE_URL = "https://api.loudly.com/v1"
|
13
|
+
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
api_key: Optional[str] = None,
|
17
|
+
config: Optional[Dict[str, Any]] = None,
|
18
|
+
timeout: float = 10.0,
|
19
|
+
base_url: Optional[str] = None,
|
20
|
+
):
|
21
|
+
if config:
|
22
|
+
api_key = config.get("apiKey", api_key)
|
23
|
+
|
24
|
+
self.api_key = api_key or os.getenv("LOUDLY_API_KEY")
|
25
|
+
self.timeout = timeout
|
26
|
+
self.base_url = base_url or self.DEFAULT_BASE_URL
|
27
|
+
|
28
|
+
self.session = requests.Session()
|
29
|
+
if self.api_key:
|
30
|
+
self._set_auth_header(self.api_key)
|
31
|
+
|
32
|
+
# -------------------
|
33
|
+
# Fluent setters
|
34
|
+
# -------------------
|
35
|
+
def with_api_key(self, api_key: str) -> "LoudlyClient":
|
36
|
+
self._set_auth_header(api_key)
|
37
|
+
return self
|
38
|
+
|
39
|
+
def with_timeout(self, timeout: float) -> "LoudlyClient":
|
40
|
+
self.timeout = timeout
|
41
|
+
return self
|
42
|
+
|
43
|
+
def with_base_url(self, base_url: str) -> "LoudlyClient":
|
44
|
+
self.base_url = base_url
|
45
|
+
return self
|
46
|
+
|
47
|
+
# -------------------
|
48
|
+
# Internal helpers
|
49
|
+
# -------------------
|
50
|
+
def _set_auth_header(self, api_key: str):
|
51
|
+
self.api_key = api_key
|
52
|
+
self.session.headers.update({
|
53
|
+
"Authorization": f"Bearer {self.api_key}",
|
54
|
+
"Content-Type": "application/json",
|
55
|
+
"Accept": "application/json",
|
56
|
+
})
|
57
|
+
|
58
|
+
def _ensure_api_key(self):
|
59
|
+
if not self.api_key:
|
60
|
+
raise ValueError(
|
61
|
+
"No API key set. Use api_key=..., config={'apiKey': ...}, "
|
62
|
+
"LOUDLY_API_KEY in a .env file, or .with_api_key()."
|
63
|
+
)
|
64
|
+
|
65
|
+
def _request(self, method: str, path: str, params=None, json=None, headers=None):
|
66
|
+
self._ensure_api_key()
|
67
|
+
url = f"{self.base_url}{path}"
|
68
|
+
try:
|
69
|
+
resp = self.session.request(
|
70
|
+
method=method,
|
71
|
+
url=url,
|
72
|
+
params=params,
|
73
|
+
json=json,
|
74
|
+
headers=headers,
|
75
|
+
timeout=self.timeout
|
76
|
+
)
|
77
|
+
except requests.RequestException as e:
|
78
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
79
|
+
|
80
|
+
if not resp.ok:
|
81
|
+
try:
|
82
|
+
err = resp.json()
|
83
|
+
message = err.get("error", err.get("message", resp.text))
|
84
|
+
except ValueError:
|
85
|
+
message = resp.text
|
86
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
87
|
+
|
88
|
+
try:
|
89
|
+
return resp.json()
|
90
|
+
except ValueError:
|
91
|
+
return {"raw": resp.text}
|
92
|
+
|
93
|
+
# -------------------
|
94
|
+
# API Methods
|
95
|
+
# -------------------
|
96
|
+
|
97
|
+
# 1. Genres
|
98
|
+
def list_genres(self) -> List[Dict[str, Any]]:
|
99
|
+
self._ensure_api_key()
|
100
|
+
headers = {
|
101
|
+
"API-KEY": self.api_key,
|
102
|
+
"Accept": "application/json"
|
103
|
+
}
|
104
|
+
url = "https://soundtracks.loudly.com/api/ai/genres"
|
105
|
+
|
106
|
+
try:
|
107
|
+
resp = requests.get(url, headers=headers, timeout=self.timeout)
|
108
|
+
except requests.RequestException as e:
|
109
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
110
|
+
|
111
|
+
if not resp.ok:
|
112
|
+
try:
|
113
|
+
err = resp.json()
|
114
|
+
message = err.get("error", err.get("message", resp.text))
|
115
|
+
except ValueError:
|
116
|
+
message = resp.text
|
117
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
118
|
+
|
119
|
+
try:
|
120
|
+
return resp.json()
|
121
|
+
except ValueError:
|
122
|
+
return {"raw": resp.text}
|
123
|
+
|
124
|
+
# 2. Structures
|
125
|
+
def list_structures(self) -> List[Dict[str, Any]]:
|
126
|
+
self._ensure_api_key()
|
127
|
+
headers = {
|
128
|
+
"API-KEY": self.api_key,
|
129
|
+
"Accept": "application/json"
|
130
|
+
}
|
131
|
+
url = "https://soundtracks.loudly.com/api/ai/structures"
|
132
|
+
|
133
|
+
try:
|
134
|
+
resp = requests.get(url, headers=headers, timeout=self.timeout)
|
135
|
+
except requests.RequestException as e:
|
136
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
137
|
+
|
138
|
+
if not resp.ok:
|
139
|
+
try:
|
140
|
+
err = resp.json()
|
141
|
+
message = err.get("error", err.get("message", resp.text))
|
142
|
+
except ValueError:
|
143
|
+
message = resp.text
|
144
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
145
|
+
|
146
|
+
try:
|
147
|
+
return resp.json()
|
148
|
+
except ValueError:
|
149
|
+
return {"raw": resp.text}
|
150
|
+
|
151
|
+
# 3. Random prompt
|
152
|
+
def get_random_prompt(self) -> Dict[str, Any]:
|
153
|
+
self._ensure_api_key()
|
154
|
+
headers = {
|
155
|
+
"API-KEY": self.api_key,
|
156
|
+
"Accept": "application/json"
|
157
|
+
}
|
158
|
+
url = "https://soundtracks.loudly.com/api/ai/prompt/random"
|
159
|
+
|
160
|
+
try:
|
161
|
+
resp = requests.get(url, headers=headers, timeout=self.timeout)
|
162
|
+
except requests.RequestException as e:
|
163
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
164
|
+
|
165
|
+
if not resp.ok:
|
166
|
+
try:
|
167
|
+
err = resp.json()
|
168
|
+
message = err.get("error", err.get("message", resp.text))
|
169
|
+
except ValueError:
|
170
|
+
message = resp.text
|
171
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
172
|
+
|
173
|
+
try:
|
174
|
+
return resp.json()
|
175
|
+
except ValueError:
|
176
|
+
return {"raw": resp.text}
|
177
|
+
|
178
|
+
# 4. Song tags
|
179
|
+
def get_song_tags(
|
180
|
+
self,
|
181
|
+
mood: Optional[List[str]] = None,
|
182
|
+
genre: Optional[List[str]] = None,
|
183
|
+
key: Optional[List[str]] = None
|
184
|
+
) -> Dict[str, Any]:
|
185
|
+
self._ensure_api_key()
|
186
|
+
headers = {
|
187
|
+
"API-KEY": self.api_key,
|
188
|
+
"Accept": "application/json"
|
189
|
+
}
|
190
|
+
url = "https://soundtracks.loudly.com/api/songs/tags"
|
191
|
+
payload = {}
|
192
|
+
if mood: payload["mood"] = mood
|
193
|
+
if genre: payload["genre"] = genre
|
194
|
+
if key: payload["key"] = key
|
195
|
+
|
196
|
+
try:
|
197
|
+
resp = requests.get(url, headers=headers, json=payload, timeout=self.timeout)
|
198
|
+
except requests.RequestException as e:
|
199
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
200
|
+
|
201
|
+
if not resp.ok:
|
202
|
+
try:
|
203
|
+
err = resp.json()
|
204
|
+
message = err.get("error", err.get("message", resp.text))
|
205
|
+
except ValueError:
|
206
|
+
message = resp.text
|
207
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
208
|
+
|
209
|
+
try:
|
210
|
+
return resp.json()
|
211
|
+
except ValueError:
|
212
|
+
return {"raw": resp.text}
|
213
|
+
|
214
|
+
# 5. List songs
|
215
|
+
def list_songs(
|
216
|
+
self,
|
217
|
+
page: int = 1,
|
218
|
+
per_page: int = 20
|
219
|
+
) -> Dict[str, Any]:
|
220
|
+
self._ensure_api_key()
|
221
|
+
headers = {
|
222
|
+
"API-KEY": self.api_key,
|
223
|
+
"Accept": "application/json"
|
224
|
+
}
|
225
|
+
params = {"page": page, "per_page": per_page}
|
226
|
+
url = "https://soundtracks.loudly.com/api/songs"
|
227
|
+
|
228
|
+
try:
|
229
|
+
resp = requests.get(url, headers=headers, params=params, timeout=self.timeout)
|
230
|
+
except requests.RequestException as e:
|
231
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
232
|
+
|
233
|
+
if not resp.ok:
|
234
|
+
try:
|
235
|
+
err = resp.json()
|
236
|
+
message = err.get("error", err.get("message", resp.text))
|
237
|
+
except ValueError:
|
238
|
+
message = resp.text
|
239
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
240
|
+
|
241
|
+
try:
|
242
|
+
return resp.json()
|
243
|
+
except ValueError:
|
244
|
+
return {"raw": resp.text}
|
245
|
+
|
246
|
+
# 6. Song Generation
|
247
|
+
def generate_ai_song(
|
248
|
+
self,
|
249
|
+
genre: str,
|
250
|
+
genre_blend: str = "",
|
251
|
+
duration: Optional[int] = None,
|
252
|
+
energy: Optional[str] = "",
|
253
|
+
bpm: Optional[int] = None,
|
254
|
+
key_root: Optional[str] = "",
|
255
|
+
key_quality: Optional[str] = "",
|
256
|
+
instruments: Optional[str] = "",
|
257
|
+
structure_id: Optional[int] = None,
|
258
|
+
test: Optional[bool] = False
|
259
|
+
) -> Dict[str, Any]:
|
260
|
+
"""Generate AI song using form data"""
|
261
|
+
|
262
|
+
if not genre:
|
263
|
+
raise ValueError("genre is required")
|
264
|
+
|
265
|
+
self._ensure_api_key()
|
266
|
+
|
267
|
+
url = "https://soundtracks.loudly.com/api/ai/songs"
|
268
|
+
|
269
|
+
# Prepare form data - only include non-empty values
|
270
|
+
data = {"genre": genre}
|
271
|
+
|
272
|
+
if genre_blend:
|
273
|
+
data["genre_blend"] = genre_blend
|
274
|
+
if duration is not None:
|
275
|
+
data["duration"] = str(duration)
|
276
|
+
if energy:
|
277
|
+
data["energy"] = energy
|
278
|
+
if bpm is not None:
|
279
|
+
data["bpm"] = str(bpm)
|
280
|
+
if key_root:
|
281
|
+
data["key_root"] = key_root
|
282
|
+
if key_quality:
|
283
|
+
data["key_quality"] = key_quality
|
284
|
+
if instruments:
|
285
|
+
data["instruments"] = instruments
|
286
|
+
if structure_id is not None:
|
287
|
+
data["structure_id"] = str(structure_id)
|
288
|
+
if test is not None:
|
289
|
+
data["test"] = str(test).lower()
|
290
|
+
|
291
|
+
headers = {
|
292
|
+
"API-KEY": self.api_key,
|
293
|
+
"Accept": "application/json"
|
294
|
+
# Don't set Content-Type - requests will set it automatically for form data
|
295
|
+
}
|
296
|
+
|
297
|
+
try:
|
298
|
+
resp = requests.post(url, headers=headers, data=data, timeout=self.timeout)
|
299
|
+
except requests.RequestException as e:
|
300
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
301
|
+
|
302
|
+
if not resp.ok:
|
303
|
+
try:
|
304
|
+
err = resp.json()
|
305
|
+
message = err.get("error", err.get("message", resp.text))
|
306
|
+
except ValueError:
|
307
|
+
message = resp.text
|
308
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
309
|
+
|
310
|
+
try:
|
311
|
+
return resp.json()
|
312
|
+
except ValueError:
|
313
|
+
return {"raw": resp.text}
|
314
|
+
|
315
|
+
# 7. Song generation from prompt
|
316
|
+
def generate_song_from_prompt(
|
317
|
+
self,
|
318
|
+
prompt: str,
|
319
|
+
duration: Optional[int] = None,
|
320
|
+
test: Optional[bool] = False,
|
321
|
+
structure_id: Optional[int] = None
|
322
|
+
) -> Dict[str, Any]:
|
323
|
+
|
324
|
+
if not prompt:
|
325
|
+
raise ValueError("Prompt is required")
|
326
|
+
|
327
|
+
self._ensure_api_key()
|
328
|
+
|
329
|
+
url = "https://soundtracks.loudly.com/api/ai/prompt/songs"
|
330
|
+
|
331
|
+
data = {"prompt": prompt}
|
332
|
+
|
333
|
+
if duration is not None:
|
334
|
+
data["duration"] = str(duration)
|
335
|
+
|
336
|
+
if test is not None:
|
337
|
+
data["test"] = str(test).lower()
|
338
|
+
|
339
|
+
if structure_id is not None:
|
340
|
+
data["structure_id"] = str(structure_id)
|
341
|
+
|
342
|
+
headers = {
|
343
|
+
"API-KEY": self.api_key,
|
344
|
+
"Accept": "application/json"
|
345
|
+
}
|
346
|
+
|
347
|
+
try:
|
348
|
+
resp = requests.post(url, headers=headers, data=data, timeout=self.timeout)
|
349
|
+
except requests.RequestException as e:
|
350
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
351
|
+
|
352
|
+
if not resp.ok:
|
353
|
+
try:
|
354
|
+
err = resp.json()
|
355
|
+
message = err.get("error", err.get("message", resp.text))
|
356
|
+
except ValueError:
|
357
|
+
message = resp.text
|
358
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
359
|
+
|
360
|
+
try:
|
361
|
+
return resp.json()
|
362
|
+
except ValueError:
|
363
|
+
return {"raw": resp.text}
|
364
|
+
|
365
|
+
# 8. Limits Account
|
366
|
+
def get_limits(
|
367
|
+
self,
|
368
|
+
date_from: Optional[str] = None,
|
369
|
+
date_to: Optional[str] = None
|
370
|
+
) -> List[Dict[str, Any]]:
|
371
|
+
self._ensure_api_key()
|
372
|
+
headers = {
|
373
|
+
"API-KEY": self.api_key,
|
374
|
+
"Accept": "application/json"
|
375
|
+
}
|
376
|
+
# Use the soundtracks.loudly.com domain for limits as well
|
377
|
+
url = "https://soundtracks.loudly.com/api/account/limits"
|
378
|
+
|
379
|
+
params: Dict[str, Any] = {}
|
380
|
+
if date_from:
|
381
|
+
params["date_from"] = date_from
|
382
|
+
if date_to:
|
383
|
+
params["date_to"] = date_to
|
384
|
+
|
385
|
+
try:
|
386
|
+
resp = requests.get(url, headers=headers, params=params, timeout=self.timeout)
|
387
|
+
except requests.RequestException as e:
|
388
|
+
raise LoudlyAPIError(-1, f"Network error: {e}")
|
389
|
+
|
390
|
+
if not resp.ok:
|
391
|
+
try:
|
392
|
+
err = resp.json()
|
393
|
+
message = err.get("error", err.get("message", resp.text))
|
394
|
+
except ValueError:
|
395
|
+
message = resp.text
|
396
|
+
raise LoudlyAPIError(resp.status_code, message, response=resp)
|
397
|
+
|
398
|
+
try:
|
399
|
+
return resp.json()
|
400
|
+
except ValueError:
|
401
|
+
return {"raw": resp.text}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
loudly_py_sdk/__init__.py,sha256=QhxhC1MDaPN25pqKU170TltaBmR3NyagbV7Rn9ZXwUU,72
|
2
|
+
loudly_py_sdk/client.py,sha256=xcyLGktA2Qf9ATqTrA8IFa6I7o2LWYJPNfLfBLkDLtE,12636
|
3
|
+
loudly_py_sdk/exceptions.py,sha256=NDZQzVDikQPAUyWOaw7_FZNRTxyVVNWVrNzxlMrc7Uk,286
|
4
|
+
loudly_py_sdk-0.1.0.dist-info/METADATA,sha256=N0GSbEZCI6pOgZbPtxYa-8w0IJTqkp9D-WG1mBbkEus,242
|
5
|
+
loudly_py_sdk-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
loudly_py_sdk-0.1.0.dist-info/top_level.txt,sha256=7uCtoDw4K0bxopM0acT-c7sNLOx_-1FiMYvthMx0Rk8,14
|
7
|
+
loudly_py_sdk-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
loudly_py_sdk
|