lexicon-python 0.1.1__tar.gz → 0.2.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.
- lexicon_python-0.2.0/PKG-INFO +329 -0
- lexicon_python-0.2.0/README.md +314 -0
- {lexicon_python-0.1.1 → lexicon_python-0.2.0}/pyproject.toml +17 -2
- lexicon_python-0.2.0/src/lexicon/__init__.py +8 -0
- lexicon_python-0.2.0/src/lexicon/client.py +147 -0
- lexicon_python-0.2.0/src/lexicon/resources/__init__.py +15 -0
- lexicon_python-0.2.0/src/lexicon/resources/_common_types.py +179 -0
- lexicon_python-0.2.0/src/lexicon/resources/base.py +67 -0
- lexicon_python-0.2.0/src/lexicon/resources/playlist_tracks.py +298 -0
- lexicon_python-0.2.0/src/lexicon/resources/playlist_tracks_types.py +5 -0
- lexicon_python-0.2.0/src/lexicon/resources/playlists.py +511 -0
- lexicon_python-0.2.0/src/lexicon/resources/playlists_types.py +109 -0
- lexicon_python-0.2.0/src/lexicon/resources/tag_categories.py +246 -0
- lexicon_python-0.2.0/src/lexicon/resources/tag_categories_types.py +18 -0
- lexicon_python-0.2.0/src/lexicon/resources/tags.py +240 -0
- lexicon_python-0.2.0/src/lexicon/resources/tags_types.py +20 -0
- lexicon_python-0.2.0/src/lexicon/resources/tracks.py +610 -0
- lexicon_python-0.2.0/src/lexicon/resources/tracks_types.py +642 -0
- lexicon_python-0.2.0/src/lexicon/tools/__init__.py +3 -0
- lexicon_python-0.2.0/src/lexicon/tools/playlists.py +159 -0
- lexicon_python-0.2.0/src/lexicon/utils.py +16 -0
- lexicon_python-0.2.0/src/lexicon_python.egg-info/PKG-INFO +329 -0
- lexicon_python-0.2.0/src/lexicon_python.egg-info/SOURCES.txt +36 -0
- lexicon_python-0.2.0/src/lexicon_python.egg-info/requires.txt +3 -0
- lexicon_python-0.2.0/tests/test_base.py +52 -0
- lexicon_python-0.2.0/tests/test_client.py +163 -0
- lexicon_python-0.2.0/tests/test_common_types.py +76 -0
- lexicon_python-0.2.0/tests/test_playlist_tracks.py +202 -0
- lexicon_python-0.2.0/tests/test_playlists.py +371 -0
- lexicon_python-0.2.0/tests/test_playlists_types.py +50 -0
- lexicon_python-0.2.0/tests/test_tag_categories.py +175 -0
- lexicon_python-0.2.0/tests/test_tags.py +163 -0
- lexicon_python-0.2.0/tests/test_tools_playlists.py +218 -0
- lexicon_python-0.2.0/tests/test_tracks.py +1055 -0
- lexicon_python-0.1.1/PKG-INFO +0 -95
- lexicon_python-0.1.1/README.md +0 -81
- lexicon_python-0.1.1/src/lexicon/__init__.py +0 -5
- lexicon_python-0.1.1/src/lexicon/lexicon.py +0 -697
- lexicon_python-0.1.1/src/lexicon_python.egg-info/PKG-INFO +0 -95
- lexicon_python-0.1.1/src/lexicon_python.egg-info/SOURCES.txt +0 -11
- lexicon_python-0.1.1/src/lexicon_python.egg-info/requires.txt +0 -2
- lexicon_python-0.1.1/tests/test_lexicon.py +0 -509
- {lexicon_python-0.1.1 → lexicon_python-0.2.0}/LICENSE +0 -0
- {lexicon_python-0.1.1 → lexicon_python-0.2.0}/setup.cfg +0 -0
- {lexicon_python-0.1.1 → lexicon_python-0.2.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
- {lexicon_python-0.1.1 → lexicon_python-0.2.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lexicon-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for the Lexicon DJ API
|
|
5
|
+
Author-email: Garrison Burger <burgerga123@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/photonicvelocity/lexicon-python
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: InquirerPy>=0.3
|
|
12
|
+
Requires-Dist: requests>=2.31
|
|
13
|
+
Requires-Dist: typing_extensions>=4.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Lexicon Python
|
|
17
|
+
|
|
18
|
+
Python client for the Lexicon DJ Local API.
|
|
19
|
+
|
|
20
|
+
This SDK wraps the Lexicon Local API with resource groups, sensible defaults, and
|
|
21
|
+
optional validation. It is designed for scripting library automation, playlist
|
|
22
|
+
management, and metadata edits while keeping a clean escape hatch to the raw API.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Resource grouped client (`lex.tracks`, `lex.playlists`, `lex.tags`)
|
|
27
|
+
- Pagination handled for `tracks.list()` (API returns 1000 per page)
|
|
28
|
+
- Validation modes for inputs: warn (default), strict, or off
|
|
29
|
+
- Typed responses and payload hints (TypedDicts and Literals)
|
|
30
|
+
- Optional interactive playlist chooser via `InquirerPy`
|
|
31
|
+
- Raw request escape hatch via `lex.request(...)`
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python 3.9+
|
|
36
|
+
- Lexicon DJ running with Local API enabled
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install lexicon-python
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Optional (for interactive playlist chooser):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install InquirerPy
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from lexicon import Lexicon
|
|
54
|
+
|
|
55
|
+
lex = Lexicon()
|
|
56
|
+
|
|
57
|
+
# list tracks (default fields)
|
|
58
|
+
tracks = lex.tracks.list(limit=10) or []
|
|
59
|
+
for t in tracks:
|
|
60
|
+
print(t.get("artist"), "-", t.get("title"))
|
|
61
|
+
|
|
62
|
+
# search
|
|
63
|
+
results = lex.tracks.search({"artist": "Daft Punk"}) or []
|
|
64
|
+
print("matches:", len(results))
|
|
65
|
+
|
|
66
|
+
# get a playlist by path
|
|
67
|
+
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
|
|
68
|
+
print(playlist)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
By default the client targets:
|
|
74
|
+
|
|
75
|
+
- Host: `localhost`
|
|
76
|
+
- Port: `48624`
|
|
77
|
+
|
|
78
|
+
You can override these via constructor args or environment variables.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
lex = Lexicon(host="127.0.0.1", port=48624, raise_on_error=False)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Other options:
|
|
85
|
+
|
|
86
|
+
- `default_timeout`: request timeout in seconds
|
|
87
|
+
- `session`: optional `requests.Session`
|
|
88
|
+
- `raise_on_error`: raise HTTP errors instead of returning None
|
|
89
|
+
|
|
90
|
+
Environment variables:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export LEXICON_HOST=localhost
|
|
94
|
+
export LEXICON_PORT=48624
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Validation Modes
|
|
98
|
+
|
|
99
|
+
Many methods accept a `validation` parameter with three modes:
|
|
100
|
+
|
|
101
|
+
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
|
|
102
|
+
failures, but intended changes may be ignored.
|
|
103
|
+
- `"strict"`: invalid inputs raise `ValueError`.
|
|
104
|
+
- `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
lex.tracks.search({"rating": "bad"}, validation="warn") # logs warning
|
|
110
|
+
lex.tracks.search({"rating": "bad"}, validation="strict") # raises
|
|
111
|
+
lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Convenience vs Raw API
|
|
115
|
+
|
|
116
|
+
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
|
|
117
|
+
In general, response shapes are unwrapped (e.g., `"data": {...}` is removed and
|
|
118
|
+
single-item lists are collapsed to a single dict).
|
|
119
|
+
|
|
120
|
+
### Additional Methods (Not in the Raw API)
|
|
121
|
+
|
|
122
|
+
- `tracks.get_many()` repeats `get()` and preserves input order.
|
|
123
|
+
- `playlists.get_many()` repeats `get()` and preserves input order.
|
|
124
|
+
- `playlists.tracks.get()` fetches track dicts in playlist order.
|
|
125
|
+
- `playlists.tracks.update()` replaces the full track list (remove + add).
|
|
126
|
+
- `playlists.choose()` provides an interactive chooser (wraps list + get).
|
|
127
|
+
- `playlists.get_path()` resolves a path from a playlist tree.
|
|
128
|
+
|
|
129
|
+
### Input Normalization (Broader Accepted Inputs)
|
|
130
|
+
|
|
131
|
+
- **ID normalization**: Many methods accept lists of IDs instead of a single ID
|
|
132
|
+
for easier batch operations.
|
|
133
|
+
- **Field selection**: By default, `tracks.list()` and `tracks.search()` return a
|
|
134
|
+
minimal set of fields rather than the full payload:
|
|
135
|
+
- `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
|
|
136
|
+
- Fields used as a search filter or sort item are also returned.
|
|
137
|
+
- **Search filter normalization**:
|
|
138
|
+
- Text fields accept `None` (becomes `"NONE"` in filter context).
|
|
139
|
+
- Numeric filters accept `None` (becomes `"0"`).
|
|
140
|
+
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
|
|
141
|
+
`datetime.datetime` inputs (time is stripped).
|
|
142
|
+
- Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
|
|
143
|
+
- **Sort normalization**:
|
|
144
|
+
- Accepts tuple shorthand: `[("title", "asc")]`.
|
|
145
|
+
- **Track update helpers**:
|
|
146
|
+
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
|
|
147
|
+
name/number variants).
|
|
148
|
+
- Invalid entries can be skipped in `"warn"` mode without failing the update.
|
|
149
|
+
|
|
150
|
+
If no SDK normalization is desired, use `validation="off"` and pass
|
|
151
|
+
API-native payloads. API-native shapes are also accepted in `"warn"`/`"strict"`;
|
|
152
|
+
those modes simply add normalization/validation on top. For fully raw access,
|
|
153
|
+
`lex.request(...)` can always be called directly.
|
|
154
|
+
|
|
155
|
+
## Tracks
|
|
156
|
+
|
|
157
|
+
Common operations:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
track = lex.tracks.get(123)
|
|
161
|
+
tracks = lex.tracks.get_many([1, 2, 3])
|
|
162
|
+
tracks = lex.tracks.list(limit=100)
|
|
163
|
+
tracks = lex.tracks.search({"artist": "Daft Punk"})
|
|
164
|
+
|
|
165
|
+
added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
|
|
166
|
+
updated = lex.tracks.update(123, {"title": "New Title"})
|
|
167
|
+
lex.tracks.delete([123, 456])
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Notes:
|
|
171
|
+
|
|
172
|
+
- `tracks.search()` results are capped at 1000 by the API.
|
|
173
|
+
- `tracks.get_many()` preserves input order and returns `None` for missing IDs.
|
|
174
|
+
- `fields=None` returns a minimal default set of fields.
|
|
175
|
+
- In `validation="off"` mode, `fields=None` returns full payloads (API-default)
|
|
176
|
+
- `fields="all"` or `fields="*"` requests full payloads.
|
|
177
|
+
- `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.)
|
|
178
|
+
may be populated later by Lexicon.
|
|
179
|
+
|
|
180
|
+
### Track Search, Filters, and Sort
|
|
181
|
+
|
|
182
|
+
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK
|
|
183
|
+
validates fields/values in `"warn"`/`"strict"` modes and can send API-native values
|
|
184
|
+
in `"off"` mode.
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# text filters
|
|
190
|
+
lex.tracks.search({"artist": "Daft Punk"})
|
|
191
|
+
|
|
192
|
+
# numeric filters (strings)
|
|
193
|
+
lex.tracks.search({"bpm": "120"})
|
|
194
|
+
lex.tracks.search({"bpm": "120-128"})
|
|
195
|
+
lex.tracks.search({"bpm": ">=120"})
|
|
196
|
+
|
|
197
|
+
# date filters (YYYY-MM-DD)
|
|
198
|
+
lex.tracks.search({"dateAdded": "2024-01-01"})
|
|
199
|
+
|
|
200
|
+
# tag filters (comma-separated names)
|
|
201
|
+
# - default: OR across tags
|
|
202
|
+
# - prefix with "~" to require ALL tags (AND)
|
|
203
|
+
# - prefix with "!" to exclude a tag
|
|
204
|
+
lex.tracks.search({"tags": "Rock, Chill"}) # Rock OR Chill
|
|
205
|
+
lex.tracks.search({"tags": "~Rock, Chill"}) # Rock AND Chill
|
|
206
|
+
lex.tracks.search({"tags": "~Rock, !Chill"}) # Rock AND NOT Chill
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Tag filter details:
|
|
210
|
+
|
|
211
|
+
- Input is a single string with comma-separated tag names.
|
|
212
|
+
- Whitespace is ignored around commas.
|
|
213
|
+
- `~` at the start switches from OR to AND for the list.
|
|
214
|
+
- `!` before a tag name negates that tag.
|
|
215
|
+
- There is no supported way to search for “no tags”; `NONE` is not accepted.
|
|
216
|
+
|
|
217
|
+
Sort can be expressed in two shapes:
|
|
218
|
+
|
|
219
|
+
- API-native: list of dicts: `[{"field": "title", "dir": "asc"}]`
|
|
220
|
+
- Alternative: list of tuples: `[("title", "asc")]`
|
|
221
|
+
|
|
222
|
+
API-native dicts work in all modes; `validation="off"` requires the dict shape.
|
|
223
|
+
|
|
224
|
+
## Playlists
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
playlist = lex.playlists.get(42)
|
|
228
|
+
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="playlist")
|
|
229
|
+
tree = lex.playlists.list()
|
|
230
|
+
|
|
231
|
+
new_id = lex.playlists.add("Demo Playlist", playlist_type="playlist", parent_id=1)
|
|
232
|
+
lex.playlists.update(new_id, name="Renamed Playlist")
|
|
233
|
+
lex.playlists.delete([new_id])
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Playlist type accepts:
|
|
237
|
+
|
|
238
|
+
- `"folder"`, `"playlist"`, `"smartlist"`
|
|
239
|
+
- `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
|
|
240
|
+
|
|
241
|
+
### Playlist tracks helpers
|
|
242
|
+
For getting the tracks of a playlist or editing the tracklist.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
track_ids = lex.playlists.tracks.list(42)
|
|
246
|
+
tracks = lex.playlists.tracks.get(42)
|
|
247
|
+
lex.playlists.tracks.add(42, [1, 2, 3])
|
|
248
|
+
lex.playlists.tracks.remove(42, [1, 2])
|
|
249
|
+
lex.playlists.tracks.update(42, [3, 2, 1])
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Tags
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
tags = lex.tags.list()
|
|
256
|
+
new_tag = lex.tags.add(category_id=1, label="Demo Tag")
|
|
257
|
+
lex.tags.update(new_tag["id"], label="Renamed Tag")
|
|
258
|
+
lex.tags.delete(new_tag["id"])
|
|
259
|
+
|
|
260
|
+
categories = lex.tags.categories.list()
|
|
261
|
+
new_cat = lex.tags.categories.add(label="Demo Category", color="red")
|
|
262
|
+
lex.tags.categories.update(new_cat["id"], label="Renamed Category")
|
|
263
|
+
lex.tags.categories.delete(new_cat["id"])
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Tools
|
|
267
|
+
|
|
268
|
+
Interactive playlist chooser (requires InquirerPy):
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
choice = lex.playlists.choose()
|
|
272
|
+
print(choice)
|
|
273
|
+
|
|
274
|
+
# Or avoid an API call if you already have the tree
|
|
275
|
+
playlist_tree = lex.playlists.list()
|
|
276
|
+
choice = lex.tools.playlists.choose_playlist(playlist_tree) if playlist_tree else None
|
|
277
|
+
print(choice)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Helper to resolve a playlist path from a tree:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
path = lex.playlists.get_path(42)
|
|
284
|
+
print(path)
|
|
285
|
+
|
|
286
|
+
# Or avoid an API call if you already have the tree
|
|
287
|
+
playlist_tree = lex.playlists.list()
|
|
288
|
+
path = lex.tools.playlists.get_path_from_tree(playlist_tree, playlist_id=42)
|
|
289
|
+
print(path)
|
|
290
|
+
|
|
291
|
+
# -> ["Genres", "Drum & Bass"]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Raw Requests (Escape Hatch)
|
|
295
|
+
The API can always be accessed directly with `lex.request`:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
payload = lex.request("GET", "/tracks", params={"fields": "all"})
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Resource Overview
|
|
302
|
+
|
|
303
|
+
High level namespaces (full mapping in `docs/resource-map.md`):
|
|
304
|
+
|
|
305
|
+
- `lex.tracks`: get, get_many, list, search, add, update, delete
|
|
306
|
+
- `lex.playlists`: get, get_many, list (tree root), get_path, get_by_path, add, update, delete, choose
|
|
307
|
+
- `lex.playlists.tracks`: list (IDs), get (track dicts), add, remove, update
|
|
308
|
+
- `lex.tags`: list, add, update, delete
|
|
309
|
+
- `lex.tags.categories`: list, add, update, delete
|
|
310
|
+
|
|
311
|
+
## Type Hints
|
|
312
|
+
|
|
313
|
+
The SDK includes TypedDict and Literal types for payloads and enums. These are
|
|
314
|
+
intended to improve editor autocomplete and static checks.
|
|
315
|
+
|
|
316
|
+
For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
317
|
+
|
|
318
|
+
- https://www.lexicondj.com/docs/developers/api
|
|
319
|
+
- https://www.lexicondj.com/developer/api-docs.yaml
|
|
320
|
+
|
|
321
|
+
## Development
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
./.venv/bin/python -m pytest --cov --cov-branch --cov-report=term-missing
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT (see `LICENSE`).
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Lexicon Python
|
|
2
|
+
|
|
3
|
+
Python client for the Lexicon DJ Local API.
|
|
4
|
+
|
|
5
|
+
This SDK wraps the Lexicon Local API with resource groups, sensible defaults, and
|
|
6
|
+
optional validation. It is designed for scripting library automation, playlist
|
|
7
|
+
management, and metadata edits while keeping a clean escape hatch to the raw API.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Resource grouped client (`lex.tracks`, `lex.playlists`, `lex.tags`)
|
|
12
|
+
- Pagination handled for `tracks.list()` (API returns 1000 per page)
|
|
13
|
+
- Validation modes for inputs: warn (default), strict, or off
|
|
14
|
+
- Typed responses and payload hints (TypedDicts and Literals)
|
|
15
|
+
- Optional interactive playlist chooser via `InquirerPy`
|
|
16
|
+
- Raw request escape hatch via `lex.request(...)`
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.9+
|
|
21
|
+
- Lexicon DJ running with Local API enabled
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install lexicon-python
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Optional (for interactive playlist chooser):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install InquirerPy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from lexicon import Lexicon
|
|
39
|
+
|
|
40
|
+
lex = Lexicon()
|
|
41
|
+
|
|
42
|
+
# list tracks (default fields)
|
|
43
|
+
tracks = lex.tracks.list(limit=10) or []
|
|
44
|
+
for t in tracks:
|
|
45
|
+
print(t.get("artist"), "-", t.get("title"))
|
|
46
|
+
|
|
47
|
+
# search
|
|
48
|
+
results = lex.tracks.search({"artist": "Daft Punk"}) or []
|
|
49
|
+
print("matches:", len(results))
|
|
50
|
+
|
|
51
|
+
# get a playlist by path
|
|
52
|
+
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
|
|
53
|
+
print(playlist)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
By default the client targets:
|
|
59
|
+
|
|
60
|
+
- Host: `localhost`
|
|
61
|
+
- Port: `48624`
|
|
62
|
+
|
|
63
|
+
You can override these via constructor args or environment variables.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
lex = Lexicon(host="127.0.0.1", port=48624, raise_on_error=False)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Other options:
|
|
70
|
+
|
|
71
|
+
- `default_timeout`: request timeout in seconds
|
|
72
|
+
- `session`: optional `requests.Session`
|
|
73
|
+
- `raise_on_error`: raise HTTP errors instead of returning None
|
|
74
|
+
|
|
75
|
+
Environment variables:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export LEXICON_HOST=localhost
|
|
79
|
+
export LEXICON_PORT=48624
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Validation Modes
|
|
83
|
+
|
|
84
|
+
Many methods accept a `validation` parameter with three modes:
|
|
85
|
+
|
|
86
|
+
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API
|
|
87
|
+
failures, but intended changes may be ignored.
|
|
88
|
+
- `"strict"`: invalid inputs raise `ValueError`.
|
|
89
|
+
- `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
lex.tracks.search({"rating": "bad"}, validation="warn") # logs warning
|
|
95
|
+
lex.tracks.search({"rating": "bad"}, validation="strict") # raises
|
|
96
|
+
lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Convenience vs Raw API
|
|
100
|
+
|
|
101
|
+
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API.
|
|
102
|
+
In general, response shapes are unwrapped (e.g., `"data": {...}` is removed and
|
|
103
|
+
single-item lists are collapsed to a single dict).
|
|
104
|
+
|
|
105
|
+
### Additional Methods (Not in the Raw API)
|
|
106
|
+
|
|
107
|
+
- `tracks.get_many()` repeats `get()` and preserves input order.
|
|
108
|
+
- `playlists.get_many()` repeats `get()` and preserves input order.
|
|
109
|
+
- `playlists.tracks.get()` fetches track dicts in playlist order.
|
|
110
|
+
- `playlists.tracks.update()` replaces the full track list (remove + add).
|
|
111
|
+
- `playlists.choose()` provides an interactive chooser (wraps list + get).
|
|
112
|
+
- `playlists.get_path()` resolves a path from a playlist tree.
|
|
113
|
+
|
|
114
|
+
### Input Normalization (Broader Accepted Inputs)
|
|
115
|
+
|
|
116
|
+
- **ID normalization**: Many methods accept lists of IDs instead of a single ID
|
|
117
|
+
for easier batch operations.
|
|
118
|
+
- **Field selection**: By default, `tracks.list()` and `tracks.search()` return a
|
|
119
|
+
minimal set of fields rather than the full payload:
|
|
120
|
+
- `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
|
|
121
|
+
- Fields used as a search filter or sort item are also returned.
|
|
122
|
+
- **Search filter normalization**:
|
|
123
|
+
- Text fields accept `None` (becomes `"NONE"` in filter context).
|
|
124
|
+
- Numeric filters accept `None` (becomes `"0"`).
|
|
125
|
+
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` /
|
|
126
|
+
`datetime.datetime` inputs (time is stripped).
|
|
127
|
+
- Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
|
|
128
|
+
- **Sort normalization**:
|
|
129
|
+
- Accepts tuple shorthand: `[("title", "asc")]`.
|
|
130
|
+
- **Track update helpers**:
|
|
131
|
+
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts
|
|
132
|
+
name/number variants).
|
|
133
|
+
- Invalid entries can be skipped in `"warn"` mode without failing the update.
|
|
134
|
+
|
|
135
|
+
If no SDK normalization is desired, use `validation="off"` and pass
|
|
136
|
+
API-native payloads. API-native shapes are also accepted in `"warn"`/`"strict"`;
|
|
137
|
+
those modes simply add normalization/validation on top. For fully raw access,
|
|
138
|
+
`lex.request(...)` can always be called directly.
|
|
139
|
+
|
|
140
|
+
## Tracks
|
|
141
|
+
|
|
142
|
+
Common operations:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
track = lex.tracks.get(123)
|
|
146
|
+
tracks = lex.tracks.get_many([1, 2, 3])
|
|
147
|
+
tracks = lex.tracks.list(limit=100)
|
|
148
|
+
tracks = lex.tracks.search({"artist": "Daft Punk"})
|
|
149
|
+
|
|
150
|
+
added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
|
|
151
|
+
updated = lex.tracks.update(123, {"title": "New Title"})
|
|
152
|
+
lex.tracks.delete([123, 456])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Notes:
|
|
156
|
+
|
|
157
|
+
- `tracks.search()` results are capped at 1000 by the API.
|
|
158
|
+
- `tracks.get_many()` preserves input order and returns `None` for missing IDs.
|
|
159
|
+
- `fields=None` returns a minimal default set of fields.
|
|
160
|
+
- In `validation="off"` mode, `fields=None` returns full payloads (API-default)
|
|
161
|
+
- `fields="all"` or `fields="*"` requests full payloads.
|
|
162
|
+
- `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.)
|
|
163
|
+
may be populated later by Lexicon.
|
|
164
|
+
|
|
165
|
+
### Track Search, Filters, and Sort
|
|
166
|
+
|
|
167
|
+
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK
|
|
168
|
+
validates fields/values in `"warn"`/`"strict"` modes and can send API-native values
|
|
169
|
+
in `"off"` mode.
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# text filters
|
|
175
|
+
lex.tracks.search({"artist": "Daft Punk"})
|
|
176
|
+
|
|
177
|
+
# numeric filters (strings)
|
|
178
|
+
lex.tracks.search({"bpm": "120"})
|
|
179
|
+
lex.tracks.search({"bpm": "120-128"})
|
|
180
|
+
lex.tracks.search({"bpm": ">=120"})
|
|
181
|
+
|
|
182
|
+
# date filters (YYYY-MM-DD)
|
|
183
|
+
lex.tracks.search({"dateAdded": "2024-01-01"})
|
|
184
|
+
|
|
185
|
+
# tag filters (comma-separated names)
|
|
186
|
+
# - default: OR across tags
|
|
187
|
+
# - prefix with "~" to require ALL tags (AND)
|
|
188
|
+
# - prefix with "!" to exclude a tag
|
|
189
|
+
lex.tracks.search({"tags": "Rock, Chill"}) # Rock OR Chill
|
|
190
|
+
lex.tracks.search({"tags": "~Rock, Chill"}) # Rock AND Chill
|
|
191
|
+
lex.tracks.search({"tags": "~Rock, !Chill"}) # Rock AND NOT Chill
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Tag filter details:
|
|
195
|
+
|
|
196
|
+
- Input is a single string with comma-separated tag names.
|
|
197
|
+
- Whitespace is ignored around commas.
|
|
198
|
+
- `~` at the start switches from OR to AND for the list.
|
|
199
|
+
- `!` before a tag name negates that tag.
|
|
200
|
+
- There is no supported way to search for “no tags”; `NONE` is not accepted.
|
|
201
|
+
|
|
202
|
+
Sort can be expressed in two shapes:
|
|
203
|
+
|
|
204
|
+
- API-native: list of dicts: `[{"field": "title", "dir": "asc"}]`
|
|
205
|
+
- Alternative: list of tuples: `[("title", "asc")]`
|
|
206
|
+
|
|
207
|
+
API-native dicts work in all modes; `validation="off"` requires the dict shape.
|
|
208
|
+
|
|
209
|
+
## Playlists
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
playlist = lex.playlists.get(42)
|
|
213
|
+
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="playlist")
|
|
214
|
+
tree = lex.playlists.list()
|
|
215
|
+
|
|
216
|
+
new_id = lex.playlists.add("Demo Playlist", playlist_type="playlist", parent_id=1)
|
|
217
|
+
lex.playlists.update(new_id, name="Renamed Playlist")
|
|
218
|
+
lex.playlists.delete([new_id])
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Playlist type accepts:
|
|
222
|
+
|
|
223
|
+
- `"folder"`, `"playlist"`, `"smartlist"`
|
|
224
|
+
- `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
|
|
225
|
+
|
|
226
|
+
### Playlist tracks helpers
|
|
227
|
+
For getting the tracks of a playlist or editing the tracklist.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
track_ids = lex.playlists.tracks.list(42)
|
|
231
|
+
tracks = lex.playlists.tracks.get(42)
|
|
232
|
+
lex.playlists.tracks.add(42, [1, 2, 3])
|
|
233
|
+
lex.playlists.tracks.remove(42, [1, 2])
|
|
234
|
+
lex.playlists.tracks.update(42, [3, 2, 1])
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Tags
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
tags = lex.tags.list()
|
|
241
|
+
new_tag = lex.tags.add(category_id=1, label="Demo Tag")
|
|
242
|
+
lex.tags.update(new_tag["id"], label="Renamed Tag")
|
|
243
|
+
lex.tags.delete(new_tag["id"])
|
|
244
|
+
|
|
245
|
+
categories = lex.tags.categories.list()
|
|
246
|
+
new_cat = lex.tags.categories.add(label="Demo Category", color="red")
|
|
247
|
+
lex.tags.categories.update(new_cat["id"], label="Renamed Category")
|
|
248
|
+
lex.tags.categories.delete(new_cat["id"])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Tools
|
|
252
|
+
|
|
253
|
+
Interactive playlist chooser (requires InquirerPy):
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
choice = lex.playlists.choose()
|
|
257
|
+
print(choice)
|
|
258
|
+
|
|
259
|
+
# Or avoid an API call if you already have the tree
|
|
260
|
+
playlist_tree = lex.playlists.list()
|
|
261
|
+
choice = lex.tools.playlists.choose_playlist(playlist_tree) if playlist_tree else None
|
|
262
|
+
print(choice)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Helper to resolve a playlist path from a tree:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
path = lex.playlists.get_path(42)
|
|
269
|
+
print(path)
|
|
270
|
+
|
|
271
|
+
# Or avoid an API call if you already have the tree
|
|
272
|
+
playlist_tree = lex.playlists.list()
|
|
273
|
+
path = lex.tools.playlists.get_path_from_tree(playlist_tree, playlist_id=42)
|
|
274
|
+
print(path)
|
|
275
|
+
|
|
276
|
+
# -> ["Genres", "Drum & Bass"]
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Raw Requests (Escape Hatch)
|
|
280
|
+
The API can always be accessed directly with `lex.request`:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
payload = lex.request("GET", "/tracks", params={"fields": "all"})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Resource Overview
|
|
287
|
+
|
|
288
|
+
High level namespaces (full mapping in `docs/resource-map.md`):
|
|
289
|
+
|
|
290
|
+
- `lex.tracks`: get, get_many, list, search, add, update, delete
|
|
291
|
+
- `lex.playlists`: get, get_many, list (tree root), get_path, get_by_path, add, update, delete, choose
|
|
292
|
+
- `lex.playlists.tracks`: list (IDs), get (track dicts), add, remove, update
|
|
293
|
+
- `lex.tags`: list, add, update, delete
|
|
294
|
+
- `lex.tags.categories`: list, add, update, delete
|
|
295
|
+
|
|
296
|
+
## Type Hints
|
|
297
|
+
|
|
298
|
+
The SDK includes TypedDict and Literal types for payloads and enums. These are
|
|
299
|
+
intended to improve editor autocomplete and static checks.
|
|
300
|
+
|
|
301
|
+
For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
302
|
+
|
|
303
|
+
- https://www.lexicondj.com/docs/developers/api
|
|
304
|
+
- https://www.lexicondj.com/developer/api-docs.yaml
|
|
305
|
+
|
|
306
|
+
## Development
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
./.venv/bin/python -m pytest --cov --cov-branch --cov-report=term-missing
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT (see `LICENSE`).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lexicon-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Python client for the Lexicon DJ API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -14,8 +14,9 @@ authors = [
|
|
|
14
14
|
{name = "Garrison Burger", email = "burgerga123@gmail.com"}
|
|
15
15
|
]
|
|
16
16
|
dependencies = [
|
|
17
|
+
"InquirerPy>=0.3",
|
|
17
18
|
"requests>=2.31",
|
|
18
|
-
"
|
|
19
|
+
"typing_extensions>=4.0"
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
[project.urls]
|
|
@@ -26,3 +27,17 @@ where = ["src"]
|
|
|
26
27
|
|
|
27
28
|
[tool.setuptools]
|
|
28
29
|
package-dir = {"" = "src"}
|
|
30
|
+
|
|
31
|
+
[tool.pydocstyle]
|
|
32
|
+
convention = "numpy"
|
|
33
|
+
|
|
34
|
+
[tool.coverage.run]
|
|
35
|
+
omit = [
|
|
36
|
+
"tests/*",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.coverage.report]
|
|
40
|
+
exclude_lines = [
|
|
41
|
+
"pragma: no cover",
|
|
42
|
+
"if __name__ == .__main__.:",
|
|
43
|
+
]
|