loudly-py-sdk 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.
- loudly_py_sdk-0.1.0/PKG-INFO +9 -0
- loudly_py_sdk-0.1.0/README.md +0 -0
- loudly_py_sdk-0.1.0/pyproject.toml +14 -0
- loudly_py_sdk-0.1.0/setup.cfg +4 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk/__init__.py +2 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk/client.py +401 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk/exceptions.py +7 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk.egg-info/PKG-INFO +9 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk.egg-info/SOURCES.txt +11 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk.egg-info/dependency_links.txt +1 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk.egg-info/requires.txt +2 -0
- loudly_py_sdk-0.1.0/src/loudly_py_sdk.egg-info/top_level.txt +1 -0
- loudly_py_sdk-0.1.0/tests/test_client.py +200 -0
File without changes
|
@@ -0,0 +1,14 @@
|
|
1
|
+
[project]
|
2
|
+
name = "loudly-py-sdk"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "Python SDK for Loudly Music API"
|
5
|
+
requires-python = ">=3.8"
|
6
|
+
dependencies = [
|
7
|
+
"requests>=2.25.0",
|
8
|
+
"python-dotenv>=1.0.0"
|
9
|
+
]
|
10
|
+
readme = "README.md"
|
11
|
+
license = { text = "MIT" }
|
12
|
+
|
13
|
+
[tool.uv]
|
14
|
+
python = ">=3.8"
|
@@ -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,11 @@
|
|
1
|
+
README.md
|
2
|
+
pyproject.toml
|
3
|
+
src/loudly_py_sdk/__init__.py
|
4
|
+
src/loudly_py_sdk/client.py
|
5
|
+
src/loudly_py_sdk/exceptions.py
|
6
|
+
src/loudly_py_sdk.egg-info/PKG-INFO
|
7
|
+
src/loudly_py_sdk.egg-info/SOURCES.txt
|
8
|
+
src/loudly_py_sdk.egg-info/dependency_links.txt
|
9
|
+
src/loudly_py_sdk.egg-info/requires.txt
|
10
|
+
src/loudly_py_sdk.egg-info/top_level.txt
|
11
|
+
tests/test_client.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
loudly_py_sdk
|
@@ -0,0 +1,200 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Loudly API SDK Test Script
|
4
|
+
Tests all major functionality of the Loudly API client with separate functions for each API call.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from loudly import LoudlyClient, LoudlyAPIError
|
8
|
+
|
9
|
+
|
10
|
+
def test_genres(sdk: LoudlyClient) -> None:
|
11
|
+
"""Test listing available genres."""
|
12
|
+
print("\n=== GENRES ===")
|
13
|
+
try:
|
14
|
+
genres = sdk.list_genres()
|
15
|
+
print(f"Found {len(genres)} genres:")
|
16
|
+
for genre in genres:
|
17
|
+
print(f" {genre['id']}: {genre['name']} ({genre['description']})")
|
18
|
+
except LoudlyAPIError as e:
|
19
|
+
print(f"Genres API Error [{e.status_code}]: {e.message}")
|
20
|
+
except Exception as e:
|
21
|
+
print(f"Genres Error: {e}")
|
22
|
+
|
23
|
+
|
24
|
+
def test_structures(sdk: LoudlyClient) -> None:
|
25
|
+
"""Test listing available song structures."""
|
26
|
+
print("\n=== STRUCTURES ===")
|
27
|
+
try:
|
28
|
+
structures = sdk.list_structures()
|
29
|
+
print(f" Found {len(structures)} structures:")
|
30
|
+
for s in structures:
|
31
|
+
print(f" {s['id']}: {s['name']} ({s['description']})")
|
32
|
+
except LoudlyAPIError as e:
|
33
|
+
print(f"Structures API Error [{e.status_code}]: {e.message}")
|
34
|
+
except Exception as e:
|
35
|
+
print(f"Structures Error: {e}")
|
36
|
+
|
37
|
+
|
38
|
+
def test_random_prompt(sdk: LoudlyClient) -> None:
|
39
|
+
"""Test getting a random prompt."""
|
40
|
+
print("\n=== RANDOM PROMPT ===")
|
41
|
+
try:
|
42
|
+
prompt_data = sdk.get_random_prompt()
|
43
|
+
print(f"Random prompt: {prompt_data['prompt']}")
|
44
|
+
except LoudlyAPIError as e:
|
45
|
+
print(f"Random Prompt API Error [{e.status_code}]: {e.message}")
|
46
|
+
except Exception as e:
|
47
|
+
print(f"Random Prompt Error: {e}")
|
48
|
+
|
49
|
+
|
50
|
+
def test_song_tags(sdk: LoudlyClient) -> None:
|
51
|
+
"""Test getting song tags with filters."""
|
52
|
+
print("\n=== SONG TAGS ===")
|
53
|
+
try:
|
54
|
+
tags = sdk.get_song_tags(
|
55
|
+
mood=["Dreamy", "Laid Back", "Dark"],
|
56
|
+
genre=["Funk", "Beats", "Indian"],
|
57
|
+
key=["E Major", "Ab/G# Major"]
|
58
|
+
)
|
59
|
+
print("Song tags retrieved:")
|
60
|
+
print(f"Filters used: mood=['Dreamy', 'Laid Back', 'Dark'], genre=['Funk', 'Beats', 'Indian'], key=['E Major', 'Ab/G# Major']")
|
61
|
+
print(f"Result: {tags}")
|
62
|
+
except LoudlyAPIError as e:
|
63
|
+
print(f"Song Tags API Error [{e.status_code}]: {e.message}")
|
64
|
+
except Exception as e:
|
65
|
+
print(f"Song Tags Error: {e}")
|
66
|
+
|
67
|
+
|
68
|
+
def test_list_songs(sdk: LoudlyClient) -> None:
|
69
|
+
"""Test listing songs with pagination."""
|
70
|
+
print("\n=== SONGS LIST ===")
|
71
|
+
try:
|
72
|
+
songs_data = sdk.list_songs(page=1, per_page=10)
|
73
|
+
print(f"Found {len(songs_data['items'])} songs on page 1:")
|
74
|
+
for song in songs_data["items"]:
|
75
|
+
print(f" {song['id']}: {song['title']} ({song['duration']} sec)")
|
76
|
+
except LoudlyAPIError as e:
|
77
|
+
print(f"List Songs API Error [{e.status_code}]: {e.message}")
|
78
|
+
except Exception as e:
|
79
|
+
print(f"List Songs Error: {e}")
|
80
|
+
|
81
|
+
|
82
|
+
def test_generate_ai_song(sdk: LoudlyClient) -> None:
|
83
|
+
"""Test AI song generation with parameters."""
|
84
|
+
print("\n=== AI SONG GENERATION ===")
|
85
|
+
try:
|
86
|
+
song = sdk.generate_ai_song(
|
87
|
+
genre="House",
|
88
|
+
duration=30,
|
89
|
+
energy="high",
|
90
|
+
bpm=115,
|
91
|
+
key_root="D",
|
92
|
+
key_quality="minor",
|
93
|
+
instruments="Synth,Drums",
|
94
|
+
test=True
|
95
|
+
)
|
96
|
+
|
97
|
+
print("AI Song generated successfully:")
|
98
|
+
print(f"Title: {song['title']}")
|
99
|
+
print(f"Music File: {song['music_file_path']}")
|
100
|
+
print(f"Duration: {song.get('duration', 'N/A')} ms")
|
101
|
+
print(f"BPM: {song.get('bpm', 'N/A')}")
|
102
|
+
print(f"Key: {song.get('key', {}).get('name', 'N/A')}")
|
103
|
+
|
104
|
+
except LoudlyAPIError as e:
|
105
|
+
print(f"AI Song Generation API Error [{e.status_code}]: {e.message}")
|
106
|
+
except Exception as e:
|
107
|
+
print(f"AI Song Generation Error: {e}")
|
108
|
+
|
109
|
+
|
110
|
+
def test_generate_song_from_prompt(sdk: LoudlyClient) -> None:
|
111
|
+
"""Test song generation from text prompt."""
|
112
|
+
print("\n=== SONG FROM PROMPT ===")
|
113
|
+
try:
|
114
|
+
song_from_prompt = sdk.generate_song_from_prompt(
|
115
|
+
prompt="A 90-second energetic house track with tropical vibes and a melodic flute line",
|
116
|
+
duration=30,
|
117
|
+
test=True
|
118
|
+
)
|
119
|
+
|
120
|
+
print("Song from prompt generated successfully:")
|
121
|
+
print(f"Prompt: 'A 90-second energetic house track with tropical vibes and a melodic flute line'")
|
122
|
+
print(f"Title: {song_from_prompt['title']}")
|
123
|
+
print(f"Music File: {song_from_prompt['music_file_path']}")
|
124
|
+
print(f"Duration: {song_from_prompt['duration']} ms")
|
125
|
+
print(f"BPM: {song_from_prompt['bpm']}")
|
126
|
+
print(f"Key: {song_from_prompt['key']['name']}")
|
127
|
+
|
128
|
+
except LoudlyAPIError as e:
|
129
|
+
print(f"Song from Prompt API Error [{e.status_code}]: {e.message}")
|
130
|
+
except Exception as e:
|
131
|
+
print(f"Song from Prompt Error: {e}")
|
132
|
+
|
133
|
+
|
134
|
+
def test_account_limits(sdk: LoudlyClient) -> None:
|
135
|
+
"""Test getting account limits information."""
|
136
|
+
print("\n=== ACCOUNT LIMITS ===")
|
137
|
+
try:
|
138
|
+
# Test without date filters
|
139
|
+
print("Getting current limits...")
|
140
|
+
limits = sdk.get_limits()
|
141
|
+
print("Current limits retrieved:")
|
142
|
+
for limit in limits.get('limits', []):
|
143
|
+
print(f"{limit['request_type']}: {limit['used']}/{limit['limit']} ({limit['left']} remaining)")
|
144
|
+
|
145
|
+
top_up = limits.get('top_up', {})
|
146
|
+
if top_up.get('total', 0) > 0:
|
147
|
+
print(f"Top-up available: {top_up['available']}/{top_up['total']}")
|
148
|
+
|
149
|
+
# Test with date range
|
150
|
+
print("\n Getting limits for specific date range...")
|
151
|
+
limits_filtered = sdk.get_limits(date_from="2025-02-25", date_to="2025-03-27")
|
152
|
+
print("Filtered limits retrieved:")
|
153
|
+
print(f"Date range: 2025-02-25 to 2025-03-27")
|
154
|
+
for limit in limits_filtered.get('limits', []):
|
155
|
+
print(f"{limit['request_type']}: {limit['used']}/{limit['limit']} ({limit['left']} remaining)")
|
156
|
+
|
157
|
+
except LoudlyAPIError as e:
|
158
|
+
print(f"Account Limits API Error [{e.status_code}]: {e.message}")
|
159
|
+
except Exception as e:
|
160
|
+
print(f"Account Limits Error: {e}")
|
161
|
+
|
162
|
+
|
163
|
+
def main():
|
164
|
+
"""Main function to run all tests."""
|
165
|
+
print("Loudly API SDK Test Suite")
|
166
|
+
print("=" * 60)
|
167
|
+
|
168
|
+
# Initialize SDK
|
169
|
+
sdk = LoudlyClient(
|
170
|
+
api_key="gkatOxIWWGY26B4Czb9H8UF01JGwHa2Kiigf8nTiHnI",
|
171
|
+
base_url="https://soundtracks.loudly.com"
|
172
|
+
)
|
173
|
+
|
174
|
+
# Run all tests
|
175
|
+
test_functions = [
|
176
|
+
test_genres,
|
177
|
+
test_structures,
|
178
|
+
test_random_prompt,
|
179
|
+
test_song_tags,
|
180
|
+
test_list_songs,
|
181
|
+
test_generate_ai_song,
|
182
|
+
test_generate_song_from_prompt,
|
183
|
+
test_account_limits
|
184
|
+
]
|
185
|
+
|
186
|
+
for test_func in test_functions:
|
187
|
+
try:
|
188
|
+
test_func(sdk)
|
189
|
+
except KeyboardInterrupt:
|
190
|
+
print("\n Test interrupted by user")
|
191
|
+
break
|
192
|
+
except Exception as e:
|
193
|
+
print(f"Unexpected error in {test_func.__name__}: {e}")
|
194
|
+
|
195
|
+
print("\n" + "=" * 60)
|
196
|
+
print("Test suite completed")
|
197
|
+
|
198
|
+
|
199
|
+
if __name__ == "__main__":
|
200
|
+
main()
|