lexicon-python 0.3.0__tar.gz → 0.5.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.5.0/PKG-INFO +588 -0
- lexicon_python-0.5.0/README.md +573 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/pyproject.toml +1 -1
- lexicon_python-0.5.0/src/lexicon/__init__.py +50 -0
- lexicon_python-0.5.0/src/lexicon/client.py +492 -0
- lexicon_python-0.5.0/src/lexicon/colors.py +29 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/__init__.py +2 -0
- lexicon_python-0.5.0/src/lexicon/resources/control_types.py +450 -0
- lexicon_python-0.5.0/src/lexicon/resources/player.py +100 -0
- lexicon_python-0.5.0/src/lexicon/resources/player_types.py +49 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/playlists.py +35 -2
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/playlists_types.py +2 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/tag_categories.py +41 -12
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/tags_types.py +2 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/tracks.py +443 -38
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/tracks_types.py +205 -39
- lexicon_python-0.5.0/src/lexicon/tools/tempo.py +74 -0
- lexicon_python-0.5.0/src/lexicon_python.egg-info/PKG-INFO +588 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon_python.egg-info/SOURCES.txt +9 -0
- lexicon_python-0.5.0/tests/test_client.py +459 -0
- lexicon_python-0.5.0/tests/test_colors.py +34 -0
- lexicon_python-0.5.0/tests/test_integration_lifecycle.py +366 -0
- lexicon_python-0.5.0/tests/test_player.py +174 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_playlists.py +23 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_tag_categories.py +45 -0
- lexicon_python-0.5.0/tests/test_tools_tempo.py +79 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_tracks.py +734 -11
- lexicon_python-0.3.0/PKG-INFO +0 -393
- lexicon_python-0.3.0/README.md +0 -378
- lexicon_python-0.3.0/src/lexicon/__init__.py +0 -23
- lexicon_python-0.3.0/src/lexicon/client.py +0 -162
- lexicon_python-0.3.0/src/lexicon_python.egg-info/PKG-INFO +0 -393
- lexicon_python-0.3.0/tests/test_client.py +0 -164
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/LICENSE +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/setup.cfg +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/_common_types.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/base.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/playlist_tracks.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/playlist_tracks_types.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/tag_categories_types.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/resources/tags.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/tools/__init__.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/tools/playlists.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/tools/tracks.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon/utils.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon_python.egg-info/dependency_links.txt +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon_python.egg-info/requires.txt +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/src/lexicon_python.egg-info/top_level.txt +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_base.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_common_types.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_playlist_tracks.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_playlists_types.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_tags.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_tools_playlists.py +0 -0
- {lexicon_python-0.3.0 → lexicon_python-0.5.0}/tests/test_tools_tracks.py +0 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lexicon-python
|
|
3
|
+
Version: 0.5.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.10
|
|
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 optional validation. It is designed
|
|
21
|
+
for scripting library automation, playlist management, and metadata edits while keeping a clean escape hatch to the raw
|
|
22
|
+
API.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- Resource grouped client (`lex.tracks`, `lex.playlists`, `lex.tags`, `lex.player`)
|
|
27
|
+
- Player and hotkey commands via `lex.player` and `lex.control()`
|
|
28
|
+
- Pagination handled for `tracks.list()` and `tracks.search()` (API returns 1000 per page)
|
|
29
|
+
- Lexicon v1.12 app approval and tokens, with v1.11 still supported
|
|
30
|
+
- Validation modes for inputs: warn (default), strict, or off
|
|
31
|
+
- Typed responses and payload hints (TypedDicts and Literals)
|
|
32
|
+
- Optional interactive playlist chooser via `InquirerPy`
|
|
33
|
+
- Raw request escape hatch via `lex.request(...)`
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Python 3.10+
|
|
38
|
+
- Lexicon DJ running with the Local API enabled (Lexicon settings, Local API). Lexicon v1.12 is the target; v1.11 works
|
|
39
|
+
for everything that existed in it (see [Lexicon versions](#lexicon-versions)).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install lexicon-python
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from lexicon import Lexicon
|
|
51
|
+
|
|
52
|
+
# Verifies the API is reachable; raises LexiconConnectionError otherwise. On Lexicon v1.12
|
|
53
|
+
# the first run shows the user a popup asking to allow the app; see Authentication.
|
|
54
|
+
lex = Lexicon(app_name="My Lexicon Tool")
|
|
55
|
+
playlist = lex.playlists.choose()
|
|
56
|
+
|
|
57
|
+
# list tracks (default fields: id, artist, title, bpm, key, ...)
|
|
58
|
+
tracks = lex.tracks.list(limit=10) or []
|
|
59
|
+
for t in tracks:
|
|
60
|
+
print(t.get("artist"), "-", t.get("title"))
|
|
61
|
+
|
|
62
|
+
# search — pass fields=[...] for a narrow projection, or fields="all" for the
|
|
63
|
+
# full payload (tempomarkers, cuepoints, tags, etc.)
|
|
64
|
+
results = lex.tracks.search({"artist": "Daft Punk"}) or []
|
|
65
|
+
print("matches:", len(results))
|
|
66
|
+
|
|
67
|
+
# get a single track by id — always returns the full record
|
|
68
|
+
full = lex.tracks.get(123)
|
|
69
|
+
|
|
70
|
+
# get a playlist by path
|
|
71
|
+
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="folder")
|
|
72
|
+
print(playlist)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
By default the client targets:
|
|
78
|
+
|
|
79
|
+
- Host: `localhost`
|
|
80
|
+
- Port: `48624`
|
|
81
|
+
|
|
82
|
+
You can override these via constructor args or environment variables.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
lex = Lexicon(host="127.0.0.1", port=48624, raise_on_error=False)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Other options:
|
|
89
|
+
|
|
90
|
+
- `default_timeout`: request timeout in seconds
|
|
91
|
+
- `session`: optional `requests.Session`
|
|
92
|
+
- `raise_on_error`: raise HTTP errors instead of returning None
|
|
93
|
+
- `verify_connection` (default `True`): probe the API on construction and raise `LexiconConnectionError` if Lexicon
|
|
94
|
+
isn't reachable, or `LexiconAuthError` if it refuses the app. Set to `False` to build a client without a running
|
|
95
|
+
Lexicon (e.g. for tests).
|
|
96
|
+
- `app_name` (default `"lexicon-python"`): the name the user sees in Lexicon's approval popup and settings.
|
|
97
|
+
- `token`: the app's API token for Lexicon v1.12 (see below).
|
|
98
|
+
|
|
99
|
+
Environment variables:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export LEXICON_HOST=localhost
|
|
103
|
+
export LEXICON_PORT=48624 # Lexicon v1.12 lets the user change it in the Local API settings
|
|
104
|
+
export LEXICON_APP_NAME="My Lexicon Tool"
|
|
105
|
+
export LEXICON_TOKEN=... # optional, see Authentication
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Authentication
|
|
109
|
+
|
|
110
|
+
Starting in Lexicon v1.12, the user has to allow each app before it can use the API. Every request names the app in the
|
|
111
|
+
`X-Lexicon-App` header (`app_name`, default `"lexicon-python"`). Lexicon asks once per name, so give each tool its own.
|
|
112
|
+
|
|
113
|
+
- **First run:** the first request from a new app name shows the user a popup, and Lexicon holds the request until they
|
|
114
|
+
answer. `Lexicon()` waits up to 70 seconds for that. When the user allows the app, Lexicon sends a token, which the
|
|
115
|
+
client keeps in `lex.token`.
|
|
116
|
+
- **On the computer running Lexicon:** an allowed app is let in by name alone, so a local script needs no token.
|
|
117
|
+
- **From another device:** every request needs the token. Store `lex.token` and pass it back as `token=` or
|
|
118
|
+
`LEXICON_TOKEN`. Lexicon only keeps a hash, so it cannot show the token again.
|
|
119
|
+
- **Pairing explicitly:** `lex.pair()` asks the user right away (waiting up to two minutes) and returns the token.
|
|
120
|
+
- **Personal scripts:** the user can instead copy the API token from the Lexicon settings under Local API. Lexicon
|
|
121
|
+
always lets that token in.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from lexicon import Lexicon, LexiconAuthError
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
lex = Lexicon(app_name="My Lexicon Tool", token=load_saved_token())
|
|
128
|
+
except LexiconAuthError as exc:
|
|
129
|
+
print(exc.error_code, exc) # 10: not answered yet, 6: blocked, 11: allowed but token missing
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`LexiconAuthError` (a `LexiconConnectionError`) carries Lexicon's error code:
|
|
133
|
+
|
|
134
|
+
| Code | Meaning |
|
|
135
|
+
| ---- | ---------------------------------------------------------------------------------------------- |
|
|
136
|
+
| `10` | The popup was not answered in time, or was closed. Lexicon waits a minute before asking again. |
|
|
137
|
+
| `6` | The user blocked the app. Unblock it in the Lexicon settings. |
|
|
138
|
+
| `11` | The name is already allowed, but the request did not send its token. |
|
|
139
|
+
| `9` | The token is wrong, or the request looked like it came from a web page. |
|
|
140
|
+
|
|
141
|
+
The user can block or remove an app at any time in the Lexicon settings, which stops its token from working. Lexicon
|
|
142
|
+
v1.11 and older have no authentication and ignore these headers.
|
|
143
|
+
|
|
144
|
+
## Validation Modes
|
|
145
|
+
|
|
146
|
+
Many methods accept a `validation` parameter with three modes:
|
|
147
|
+
|
|
148
|
+
- `"warn"` (default): invalid inputs are skipped with a warning. This avoids API failures, but intended changes may be
|
|
149
|
+
ignored.
|
|
150
|
+
- `"strict"`: invalid inputs raise `ValueError`.
|
|
151
|
+
- `"off"`: skips normalization and sends inputs as-is (inputs must match API-native shapes)
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
lex.tracks.search({"rating": "bad"}, validation="warn") # logs warning
|
|
157
|
+
lex.tracks.search({"rating": "bad"}, validation="strict") # raises
|
|
158
|
+
lex.tracks.search({"rating": "bad"}, validation="off") # sends as-is
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Convenience vs Raw API
|
|
162
|
+
|
|
163
|
+
This SDK adds several quality-of-life behaviors on top of the raw Lexicon API. In general, response shapes are unwrapped
|
|
164
|
+
(e.g., `"data": {...}` is removed and single-item lists are collapsed to a single dict).
|
|
165
|
+
|
|
166
|
+
### Additional Methods (Not in the Raw API)
|
|
167
|
+
|
|
168
|
+
- `tracks.get_many()` repeats `get()` and preserves input order.
|
|
169
|
+
- `playlists.get_many()` repeats `get()` and preserves input order.
|
|
170
|
+
- `playlists.tracks.get()` fetches track dicts in playlist order.
|
|
171
|
+
- `playlists.tracks.update()` replaces the full track list (remove + add).
|
|
172
|
+
- `playlists.choose()` provides an interactive chooser (wraps list + get).
|
|
173
|
+
- `playlists.get_path()` resolves a path from a playlist tree.
|
|
174
|
+
|
|
175
|
+
### Input Normalization (Broader Accepted Inputs)
|
|
176
|
+
|
|
177
|
+
- **ID normalization**: Many methods accept lists of IDs instead of a single ID for easier batch operations.
|
|
178
|
+
- **Field selection**: By default, `tracks.list()` and `tracks.search()` return a minimal set of fields rather than the
|
|
179
|
+
full payload:
|
|
180
|
+
- `id`, `artist`, `title`, `albumTitle`, `bpm`, `key`, `duration`, `year`
|
|
181
|
+
- Fields used as a search filter or sort item are also returned.
|
|
182
|
+
- **Search filter normalization**:
|
|
183
|
+
- Text fields accept `None` (becomes `"NONE"` in filter context).
|
|
184
|
+
- Numeric filters accept `None` (becomes `"0"`).
|
|
185
|
+
- Date filters accept `YYYY-MM-DD`, full datetime strings, or `datetime.date` / `datetime.datetime` inputs (time is
|
|
186
|
+
stripped).
|
|
187
|
+
- Comparisons (`>YYYY-MM-DD`) are warned/blocked because the API currently ignores them.
|
|
188
|
+
- **Sort normalization**:
|
|
189
|
+
- Accepts tuple shorthand: `[("title", "asc")]`.
|
|
190
|
+
- **Track update helpers**:
|
|
191
|
+
- Cuepoint and tempomarker entries are normalized (e.g., cuepoint type accepts name/number variants).
|
|
192
|
+
- Invalid entries can be skipped in `"warn"` mode without failing the update.
|
|
193
|
+
|
|
194
|
+
If no SDK normalization is desired, use `validation="off"` and pass API-native payloads. API-native shapes are also
|
|
195
|
+
accepted in `"warn"`/`"strict"`; those modes simply add normalization/validation on top. For fully raw access,
|
|
196
|
+
`lex.request(...)` can always be called directly.
|
|
197
|
+
|
|
198
|
+
## Tracks
|
|
199
|
+
|
|
200
|
+
Common operations:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
track = lex.tracks.get(123)
|
|
204
|
+
tracks = lex.tracks.get_many([1, 2, 3])
|
|
205
|
+
tracks = lex.tracks.list(limit=100)
|
|
206
|
+
tracks = lex.tracks.search({"artist": "Daft Punk"})
|
|
207
|
+
|
|
208
|
+
added = lex.tracks.add(["/path/to/file1.mp3", "/path/to/file2.mp3"])
|
|
209
|
+
updated = lex.tracks.update(123, {"title": "New Title"})
|
|
210
|
+
lex.tracks.delete([123, 456])
|
|
211
|
+
|
|
212
|
+
# Lexicon v1.12: the selection in Lexicon, and relocating a track to another file
|
|
213
|
+
ids = lex.tracks.selected() # [12, 7, 33]
|
|
214
|
+
track = lex.tracks.get("selected") # the topmost selected track
|
|
215
|
+
lex.tracks.update("selected", {"rating": 5}) # every selected track
|
|
216
|
+
moved = lex.tracks.update_location(123, "/Music/new/file.mp3")
|
|
217
|
+
|
|
218
|
+
# Replace tempomarkers while keeping cuepoints anchored to the same beats
|
|
219
|
+
lex.tracks.update_tempogrid(123, [{"startTime": 0.0, "bpm": 124.0}])
|
|
220
|
+
|
|
221
|
+
# Start Lexicon's analyzer on specific tracks
|
|
222
|
+
lex.tracks.analyze([123, 456], analyze_beatgrid=True, do_not_overwrite=False)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Notes:
|
|
226
|
+
|
|
227
|
+
- `tracks.search()` returns every match on Lexicon v1.12, which pages results 1000 at a time; pass `limit=` to cap it.
|
|
228
|
+
Lexicon v1.11 returns at most 1000 matches and logs a warning when there are more.
|
|
229
|
+
- `tracks.update()` returns the track as Lexicon stored it. On v1.12 that comes from the PATCH response; on older
|
|
230
|
+
versions the client fetches it again.
|
|
231
|
+
- A `cuepoints` or `tempomarkers` edit replaces the whole list, and Lexicon clears the `data` of any cue or marker sent
|
|
232
|
+
without it. That `data` holds what a DJ app import stored (Traktor, Rekordbox, ...), so to keep it, edit the list from
|
|
233
|
+
`tracks.get()` and send it back. `update_tempogrid()` does this for cuepoints.
|
|
234
|
+
- `tracks.update_location()` only changes the library; the file is never moved. Lexicon normalizes the path, so read
|
|
235
|
+
`location` back from the returned track.
|
|
236
|
+
- `tracks.get_many()` preserves input order and returns `None` for missing IDs.
|
|
237
|
+
- `tracks.get()` always returns the full record (no `fields` parameter).
|
|
238
|
+
- For `tracks.list()` / `tracks.search()`:
|
|
239
|
+
- `fields=None` returns a minimal default set.
|
|
240
|
+
- In `validation="off"` mode, `fields=None` returns the full API payload.
|
|
241
|
+
- Pass a list of field names for a narrow projection (`fields=["id", "location"]`).
|
|
242
|
+
- `fields="all"` or `fields="*"` requests the full payload.
|
|
243
|
+
- Comma-separated strings (e.g. `"id,location"`) are _not_ accepted; pass a list. With `validation="warn"` the call
|
|
244
|
+
falls back to the default field set and logs.
|
|
245
|
+
- `tracks.add()` returns track dicts, but analysis fields (tempo markers, key, etc.) may be populated later by Lexicon.
|
|
246
|
+
- `tracks.update_tempogrid()` replaces tempomarkers while preserving each cuepoint's beat position by rewriting
|
|
247
|
+
`startTime`/`endTime` against the new grid. Use `tracks.update(track_id, {"tempomarkers": [...]})` directly if you
|
|
248
|
+
want the raw field replacement (cuepoints' seconds stay put, beats shift).
|
|
249
|
+
- `tracks.analyze()` returns as soon as Lexicon accepts the request. Analysis runs in the background and the API has no
|
|
250
|
+
completion signal, so poll `tracks.get()` and watch `tempomarkers`, `bpm`, `key`, or `dateModified`.
|
|
251
|
+
- `tracks.analyze(tempo_range=...)` takes one of the eight ids Lexicon's analyze dropdown stores: `"0_300"` (Any),
|
|
252
|
+
`"58_115"`, `"68_135"`, `"78_155"`, `"88_175"`, `"98_195"`, `"128_255"`, `"180_300"` (180+). An equivalent
|
|
253
|
+
`(min, max)` tuple works too. The value is written to Lexicon's stored analysis setting and persists after the call;
|
|
254
|
+
the boolean options apply to that run only.
|
|
255
|
+
|
|
256
|
+
### Track Payload Shapes
|
|
257
|
+
|
|
258
|
+
Rich-payload fields like `cuepoints` and `tempomarkers` aren't in the default field set; request them explicitly
|
|
259
|
+
(`fields=["cuepoints", "tempomarkers"]`) or ask for everything (`fields="all"`). Their shapes are typed via
|
|
260
|
+
`CuePointResponse` and `TempoMarkerResponse`, both re-exported from `lexicon`:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from lexicon import TrackResponse, TempoMarkerResponse, CuePointResponse
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Color values (cuepoint/tag/etc. `color` fields) are Lexicon-specific name strings declared by the `Color` literal type.
|
|
267
|
+
To convert a name to RGB:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from lexicon import color_rgb
|
|
271
|
+
|
|
272
|
+
color_rgb("red_dark") # → (158, 15, 7)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Track Search, Filters, and Sort
|
|
276
|
+
|
|
277
|
+
`tracks.search(filter=...)` accepts a dict of field names and values. The SDK validates fields/values in
|
|
278
|
+
`"warn"`/`"strict"` modes and can send API-native values in `"off"` mode.
|
|
279
|
+
|
|
280
|
+
Examples:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
# text filters
|
|
284
|
+
lex.tracks.search({"artist": "Daft Punk"})
|
|
285
|
+
|
|
286
|
+
# numeric filters (strings)
|
|
287
|
+
lex.tracks.search({"bpm": "120"})
|
|
288
|
+
lex.tracks.search({"bpm": "120-128"})
|
|
289
|
+
lex.tracks.search({"bpm": ">=120"})
|
|
290
|
+
lex.tracks.search({"bpm": "120~128"}) # stops before 128
|
|
291
|
+
|
|
292
|
+
# date filters (YYYY-MM-DD)
|
|
293
|
+
# - ">" is that day and later, "<" is before that day
|
|
294
|
+
lex.tracks.search({"dateAdded": "2024-01-01"})
|
|
295
|
+
lex.tracks.search({"dateAdded": ">2024-01-01"})
|
|
296
|
+
|
|
297
|
+
# tag filters (comma-separated names)
|
|
298
|
+
# - default: OR across tags
|
|
299
|
+
# - prefix with "~" to require ALL tags (AND)
|
|
300
|
+
# - prefix with "!" to exclude a tag
|
|
301
|
+
lex.tracks.search({"tags": "Rock, Chill"}) # Rock OR Chill
|
|
302
|
+
lex.tracks.search({"tags": "~Rock, Chill"}) # Rock AND Chill
|
|
303
|
+
lex.tracks.search({"tags": "~Rock, !Chill"}) # Rock AND NOT Chill
|
|
304
|
+
lex.tracks.search({"tags": None}) # no tags at all (v1.12)
|
|
305
|
+
|
|
306
|
+
# Lexicon v1.12: look tracks up by id, or by file extension
|
|
307
|
+
lex.tracks.search({"id": [12, 48, 153]}, fields=["id", "title"])
|
|
308
|
+
lex.tracks.search({"fileType": "flac"})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Tag filter details:
|
|
312
|
+
|
|
313
|
+
- Input is a single string with comma-separated tag names.
|
|
314
|
+
- Whitespace is ignored around commas.
|
|
315
|
+
- `~` at the start switches from OR to AND for the list.
|
|
316
|
+
- `!` before a tag name negates that tag.
|
|
317
|
+
- `None` finds tracks without any tag on Lexicon v1.12 (unless a tag is labeled "None").
|
|
318
|
+
|
|
319
|
+
Lexicon v1.12 rejects a filter on a field it cannot filter with an error, where v1.11 silently ignored it. The `id`
|
|
320
|
+
filter is the dangerous case on v1.11, which ignores it and matches the whole library, so `search()` returns `None` when
|
|
321
|
+
more tracks come back than ids were asked for.
|
|
322
|
+
|
|
323
|
+
Sort can be expressed in two shapes:
|
|
324
|
+
|
|
325
|
+
- API-native: list of dicts: `[{"field": "title", "dir": "asc"}]`
|
|
326
|
+
- Alternative: list of tuples: `[("title", "asc")]`
|
|
327
|
+
|
|
328
|
+
API-native dicts work in all modes; `validation="off"` requires the dict shape.
|
|
329
|
+
|
|
330
|
+
## Playlists
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
playlist = lex.playlists.get(42)
|
|
334
|
+
playlist = lex.playlists.get_by_path(["Genres", "Drum & Bass"], playlist_type="playlist")
|
|
335
|
+
tree = lex.playlists.list()
|
|
336
|
+
|
|
337
|
+
new_id = lex.playlists.add("Demo Playlist", playlist_type="playlist", parent_id=1)
|
|
338
|
+
lex.playlists.update(new_id, name="Renamed Playlist")
|
|
339
|
+
lex.playlists.update(new_id, position=0) # v1.12: the others shift down, like dragging
|
|
340
|
+
lex.playlists.delete([new_id])
|
|
341
|
+
|
|
342
|
+
selected = lex.playlists.selected() # v1.12: IDs selected in the playlist tree
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Playlist type accepts:
|
|
346
|
+
|
|
347
|
+
- `"folder"`, `"playlist"`, `"smartlist"`
|
|
348
|
+
- `1`, `2`, `3` (or string numerals `"1"`, `"2"`, `"3"`)
|
|
349
|
+
|
|
350
|
+
### Playlist tracks helpers
|
|
351
|
+
|
|
352
|
+
For getting the tracks of a playlist or editing the tracklist.
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
track_ids = lex.playlists.tracks.list(42)
|
|
356
|
+
tracks = lex.playlists.tracks.get(42)
|
|
357
|
+
lex.playlists.tracks.add(42, [1, 2, 3])
|
|
358
|
+
lex.playlists.tracks.remove(42, [1, 2])
|
|
359
|
+
lex.playlists.tracks.update(42, [3, 2, 1])
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Tags
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
tags = lex.tags.list()
|
|
366
|
+
new_tag = lex.tags.add(category_id=1, label="Demo Tag")
|
|
367
|
+
lex.tags.update(new_tag["id"], label="Renamed Tag")
|
|
368
|
+
lex.tags.delete(new_tag["id"])
|
|
369
|
+
|
|
370
|
+
categories = lex.tags.categories.list()
|
|
371
|
+
new_cat = lex.tags.categories.add(label="Demo Category", color="red")
|
|
372
|
+
lex.tags.categories.update(new_cat["id"], label="Renamed Category")
|
|
373
|
+
lex.tags.categories.delete(new_cat["id"])
|
|
374
|
+
|
|
375
|
+
# Lexicon v1.12: reorder a category's tags (every tag it has, in the new order; tags from
|
|
376
|
+
# other categories move in), and move the category itself
|
|
377
|
+
lex.tags.categories.update(new_cat["id"], tags=[12, 10, 11])
|
|
378
|
+
lex.tags.categories.update(new_cat["id"], position=0)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Player
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
playing = lex.player.playing() # None when no track is loaded
|
|
385
|
+
if playing:
|
|
386
|
+
print(playing["track"]["title"], playing["currentTime"], playing["progress"])
|
|
387
|
+
|
|
388
|
+
queue = lex.player.queue() # the tracks after the loaded one
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`playing()` returns the loaded track whether it is playing or paused. For Beatport, Apple Music, and Spotify tracks,
|
|
392
|
+
`track` is not a library row: `id` is the service's own track ID, and `duration` is the length of the preview clip.
|
|
393
|
+
|
|
394
|
+
## Control
|
|
395
|
+
|
|
396
|
+
`lex.control()` runs the commands that can be bound to a hotkey. Parameters use their API names.
|
|
397
|
+
|
|
398
|
+
```python
|
|
399
|
+
lex.control("MusicPlayer_TogglePlay")
|
|
400
|
+
lex.control("MusicPlayer_Load", id=123)
|
|
401
|
+
lex.control("MusicPlayer_Seek", seconds=30)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
It returns `True` when Lexicon accepts the request. Lexicon does not wait for the action to finish and has no completion
|
|
405
|
+
signal, and it accepts an action whose target is missing, such as one for the selected tracks with nothing selected.
|
|
406
|
+
Names are checked against the `ControlAction` list; pass `validation="off"` for one that is not listed. The list covers
|
|
407
|
+
Lexicon v1.11 and v1.12, so it holds both names of the actions v1.12 renamed: v1.12 calls the `Direct2Cdj_*` player
|
|
408
|
+
actions `DeckConnect_*` (`DeckConnect_LoadDeck`, `DeckConnect_LoadDeck1` to `8`, `DeckConnect_LoadEmptyDeck`) and
|
|
409
|
+
rejects the old names.
|
|
410
|
+
|
|
411
|
+
## Tools
|
|
412
|
+
|
|
413
|
+
Interactive playlist chooser (requires InquirerPy):
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
choice = lex.playlists.choose()
|
|
417
|
+
print(choice)
|
|
418
|
+
|
|
419
|
+
# Or avoid an API call if you already have the tree
|
|
420
|
+
playlist_tree = lex.playlists.list()
|
|
421
|
+
choice = lex.tools.playlists.choose_playlist(playlist_tree) if playlist_tree else None
|
|
422
|
+
print(choice)
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Helper to resolve a playlist path from a tree:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
path = lex.playlists.get_path(42)
|
|
429
|
+
print(path)
|
|
430
|
+
|
|
431
|
+
# Or avoid an API call if you already have the tree
|
|
432
|
+
playlist_tree = lex.playlists.list()
|
|
433
|
+
path = lex.tools.playlists.get_path_from_tree(playlist_tree, playlist_id=42)
|
|
434
|
+
print(path)
|
|
435
|
+
|
|
436
|
+
# -> ["Genres", "Drum & Bass"]
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Raw Requests (Escape Hatch)
|
|
440
|
+
|
|
441
|
+
The API can always be accessed directly with `lex.request`:
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
payload = lex.request("GET", "/tracks", params={"fields": "all"})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Resource Overview
|
|
448
|
+
|
|
449
|
+
High level namespaces (full mapping in `docs/resource-map.md`):
|
|
450
|
+
|
|
451
|
+
- `lex.tracks`: get, get_many, list, search, selected, add, update, update_location, update_tempogrid, add_tags,
|
|
452
|
+
remove_tags, delete, analyze
|
|
453
|
+
- `lex.playlists`: get, get_many, list (tree root), selected, get_path, get_by_path, add, update, delete, choose
|
|
454
|
+
- `lex.playlists.tracks`: list (IDs), get (track dicts), add, remove, update
|
|
455
|
+
- `lex.tags`: list, add, update, delete
|
|
456
|
+
- `lex.tags.categories`: list, add, update, delete
|
|
457
|
+
- `lex.player`: playing, queue
|
|
458
|
+
- `lex.control()`: run a control action
|
|
459
|
+
- `lex.pair()`: ask the user to allow the app and get its token (v1.12)
|
|
460
|
+
|
|
461
|
+
## Lexicon versions
|
|
462
|
+
|
|
463
|
+
The client targets Lexicon v1.12 and keeps working with v1.11. Requests are the same, and the client falls back where
|
|
464
|
+
v1.11 answers differently:
|
|
465
|
+
|
|
466
|
+
| Feature | v1.12 | v1.11 |
|
|
467
|
+
| ---------------------------------------------------- | ----------------------- | -------------------------------------------------------- |
|
|
468
|
+
| App approval, `token`, `pair()` | required | no authentication; headers ignored |
|
|
469
|
+
| `selected()`, `get("selected")`, `update_location()` | yes | `None` (endpoint does not exist) |
|
|
470
|
+
| `search()` past 1000 matches | pages through all | first 1000, with a warning |
|
|
471
|
+
| `search()` filters `id`, `fileType`, `tags=None` | yes | `id`: `None`; `fileType`: error; `tags=None`: no matches |
|
|
472
|
+
| `update()` result | from the PATCH response | fetched again |
|
|
473
|
+
| `playlists.update(position=)` | inserts, others shift | stores the number, positions can overlap |
|
|
474
|
+
| `tags.categories.update(tags=, position=)` | yes | accepted but ignored |
|
|
475
|
+
| Cue `activeLoop` | sent as `true`/`false` | not tested with booleans |
|
|
476
|
+
| DeckConnect actions | `DeckConnect_*` | `Direct2Cdj_*` |
|
|
477
|
+
|
|
478
|
+
## Type Hints
|
|
479
|
+
|
|
480
|
+
The SDK includes TypedDict and Literal types for payloads and enums. These are intended to improve editor autocomplete
|
|
481
|
+
and static checks.
|
|
482
|
+
|
|
483
|
+
For full payload schemas and endpoint details, refer to the Lexicon API docs:
|
|
484
|
+
|
|
485
|
+
- https://www.lexicondj.com/docs/developers/api
|
|
486
|
+
- https://www.lexicondj.com/developer/api-docs.yaml
|
|
487
|
+
|
|
488
|
+
## Development
|
|
489
|
+
|
|
490
|
+
### Setup
|
|
491
|
+
|
|
492
|
+
#### Pip
|
|
493
|
+
|
|
494
|
+
Ensure that you have Python 3.9+ installed locally. To install all runtime and dev dependencies into a local virtual
|
|
495
|
+
environment using pip:
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
python -m venv .venv
|
|
499
|
+
source .venv/bin/activate
|
|
500
|
+
pip install -e ".[dev]"
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### uv
|
|
504
|
+
|
|
505
|
+
To use [uv](https://docs.astral.sh/uv/) to install all runtime and dev dependencies into a local virtual environment,
|
|
506
|
+
simply install `uv` and run:
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
uv sync --dev
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The lockfile (`uv.lock`) is checked in to ensure reproducible installs.
|
|
513
|
+
|
|
514
|
+
### Running Tests
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
# Unit tests
|
|
518
|
+
make run-tests
|
|
519
|
+
|
|
520
|
+
# Integration tests (requires Lexicon running)
|
|
521
|
+
# Note: Integration tests enforce an empty library state to avoid destructive edits on existing libraries.
|
|
522
|
+
# The fixture setup will back up the existing library, clear it for testing, and restore it afterward.
|
|
523
|
+
make run-integration-tests
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
The integration fixture quits Lexicon and renames `main.db` while it runs. If the Lexicon data folder is synced to
|
|
527
|
+
another machine (Syncthing, Dropbox, iCloud), pause the sync first. On Lexicon v1.12 the first run shows an approval
|
|
528
|
+
popup for "lexicon-python integration tests"; Lexicon remembers the answer outside the library, so later runs do not ask
|
|
529
|
+
again.
|
|
530
|
+
|
|
531
|
+
### Linting and Formatting
|
|
532
|
+
|
|
533
|
+
The project uses [ruff](https://docs.astral.sh/ruff/) for both linting and formatting.
|
|
534
|
+
|
|
535
|
+
`make test` runs the full suite: format, lint (with auto-fix), then tests. `make fix` runs all auto-fixers (lint +
|
|
536
|
+
format) without running tests.
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
make test # format-fix → lint-fix → format-check → lint-check → tests
|
|
540
|
+
make fix # lint-fix → format-fix
|
|
541
|
+
make clean # remove __pycache__, .pytest_cache, .ruff_cache, etc.
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
If you want to run the linters or formatters manually, you can use the following commands:
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
make lint-check # check for lint issues
|
|
548
|
+
make lint-fix # auto-fix lint issues
|
|
549
|
+
make format-check # check formatting
|
|
550
|
+
make format-fix # auto-fix formatting
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Pre-commit hook (recommended)
|
|
554
|
+
|
|
555
|
+
The repo ships a [pre-commit](https://pre-commit.com/) config (`.pre-commit-config.yaml`) that mirrors the CI lint job.
|
|
556
|
+
After cloning, install the hook once:
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
uv tool install pre-commit # or: pipx install pre-commit
|
|
560
|
+
pre-commit install
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
`git commit` now runs `ruff format` + `ruff check --fix` on staged files and blocks the commit on any failure. Run
|
|
564
|
+
against the whole repo any time with:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
pre-commit run --all-files
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
The ruff version is pinned in `.pre-commit-config.yaml`; bump it when CI bumps to keep them aligned.
|
|
571
|
+
|
|
572
|
+
### CI
|
|
573
|
+
|
|
574
|
+
GitHub Actions runs on every push to `main` and on pull requests targeting those branches. The pipeline includes:
|
|
575
|
+
|
|
576
|
+
- **Tests** across Python 3.9, 3.10, 3.11, and 3.12
|
|
577
|
+
- **Lint and format checks** via ruff on Python 3.12
|
|
578
|
+
|
|
579
|
+
Before opening a PR, make sure `make test` passes locally.
|
|
580
|
+
|
|
581
|
+
### Pull Requests
|
|
582
|
+
|
|
583
|
+
A PR template is provided at `.github/pull_request_template.md`. When opening a PR, fill in the description, check the
|
|
584
|
+
relevant change-type boxes, and confirm testing/checklist items.
|
|
585
|
+
|
|
586
|
+
## License
|
|
587
|
+
|
|
588
|
+
MIT (see `LICENSE`).
|