gukebox 0.9.0__tar.gz → 1.0.0.dev2__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.
Files changed (108) hide show
  1. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/PKG-INFO +56 -16
  2. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/README.md +53 -15
  3. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/inbound/api_controller.py +50 -3
  4. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/inbound/cli_controller.py +1 -1
  5. gukebox-1.0.0.dev2/discstore/adapters/inbound/config.py +250 -0
  6. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/inbound/ui_controller.py +72 -6
  7. gukebox-1.0.0.dev2/discstore/app.py +92 -0
  8. gukebox-1.0.0.dev2/discstore/command_handlers.py +31 -0
  9. gukebox-1.0.0.dev2/discstore/commands.py +90 -0
  10. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/di_container.py +0 -31
  11. gukebox-1.0.0.dev2/jukebox/.DS_Store +0 -0
  12. gukebox-1.0.0.dev2/jukebox/adapters/.DS_Store +0 -0
  13. gukebox-1.0.0.dev2/jukebox/adapters/inbound/config.py +100 -0
  14. gukebox-1.0.0.dev2/jukebox/adapters/outbound/.DS_Store +0 -0
  15. gukebox-1.0.0.dev2/jukebox/adapters/outbound/players/sonos_player_adapter.py +200 -0
  16. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +3 -1
  17. gukebox-1.0.0.dev2/jukebox/admin/__init__.py +1 -0
  18. gukebox-1.0.0.dev2/jukebox/admin/app.py +355 -0
  19. gukebox-1.0.0.dev2/jukebox/admin/cli_presentation.py +484 -0
  20. gukebox-1.0.0.dev2/jukebox/admin/command_handlers.py +115 -0
  21. gukebox-1.0.0.dev2/jukebox/admin/commands.py +70 -0
  22. gukebox-1.0.0.dev2/jukebox/admin/di_container.py +73 -0
  23. gukebox-1.0.0.dev2/jukebox/app.py +75 -0
  24. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/di_container.py +18 -32
  25. gukebox-1.0.0.dev2/jukebox/domain/.DS_Store +0 -0
  26. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/entities/__init__.py +2 -0
  27. gukebox-1.0.0.dev2/jukebox/domain/entities/current_tag_action.py +11 -0
  28. gukebox-1.0.0.dev2/jukebox/domain/entities/playback_session.py +19 -0
  29. gukebox-1.0.0.dev2/jukebox/domain/use_cases/__init__.py +5 -0
  30. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/use_cases/determine_action.py +10 -11
  31. gukebox-1.0.0.dev2/jukebox/domain/use_cases/determine_current_tag_action.py +35 -0
  32. gukebox-1.0.0.dev2/jukebox/domain/use_cases/handle_tag_event.py +121 -0
  33. gukebox-1.0.0.dev2/jukebox/settings/__init__.py +14 -0
  34. gukebox-1.0.0.dev2/jukebox/settings/definitions.py +159 -0
  35. gukebox-1.0.0.dev2/jukebox/settings/dict_utils.py +17 -0
  36. gukebox-1.0.0.dev2/jukebox/settings/entities.py +267 -0
  37. gukebox-1.0.0.dev2/jukebox/settings/errors.py +14 -0
  38. gukebox-1.0.0.dev2/jukebox/settings/file_settings_repository.py +83 -0
  39. gukebox-1.0.0.dev2/jukebox/settings/migration.py +40 -0
  40. gukebox-1.0.0.dev2/jukebox/settings/repositories.py +12 -0
  41. gukebox-1.0.0.dev2/jukebox/settings/resolve.py +459 -0
  42. gukebox-1.0.0.dev2/jukebox/settings/runtime_validation.py +26 -0
  43. gukebox-1.0.0.dev2/jukebox/settings/service_protocols.py +20 -0
  44. gukebox-1.0.0.dev2/jukebox/settings/sonos_runtime.py +175 -0
  45. gukebox-1.0.0.dev2/jukebox/settings/timing_validation.py +8 -0
  46. gukebox-1.0.0.dev2/jukebox/settings/types.py +4 -0
  47. gukebox-1.0.0.dev2/jukebox/settings/validation_rules.py +81 -0
  48. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/shared/config_utils.py +0 -18
  49. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/shared/dependency_messages.py +1 -3
  50. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/pyproject.toml +7 -4
  51. gukebox-0.9.0/discstore/adapters/inbound/config.py +0 -187
  52. gukebox-0.9.0/discstore/app.py +0 -50
  53. gukebox-0.9.0/jukebox/adapters/inbound/config.py +0 -138
  54. gukebox-0.9.0/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -68
  55. gukebox-0.9.0/jukebox/app.py +0 -18
  56. gukebox-0.9.0/jukebox/domain/entities/playback_session.py +0 -16
  57. gukebox-0.9.0/jukebox/domain/use_cases/__init__.py +0 -4
  58. gukebox-0.9.0/jukebox/domain/use_cases/handle_tag_event.py +0 -139
  59. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/LICENSE +0 -0
  60. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/__init__.py +0 -0
  61. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/__init__.py +0 -0
  62. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/inbound/__init__.py +0 -0
  63. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/inbound/cli_display.py +0 -0
  64. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
  65. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/outbound/__init__.py +0 -0
  66. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/outbound/json_library_adapter.py +0 -0
  67. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
  68. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/__init__.py +0 -0
  69. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/entities/__init__.py +0 -0
  70. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/entities/current_tag_status.py +0 -0
  71. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/repositories/__init__.py +0 -0
  72. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/__init__.py +0 -0
  73. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/add_disc.py +0 -0
  74. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/edit_disc.py +0 -0
  75. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
  76. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/get_disc.py +0 -0
  77. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/list_discs.py +0 -0
  78. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/remove_disc.py +0 -0
  79. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
  80. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/discstore/domain/use_cases/search_discs.py +0 -0
  81. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/__init__.py +0 -0
  82. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/__init__.py +0 -0
  83. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/inbound/__init__.py +0 -0
  84. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/inbound/cli_controller.py +0 -0
  85. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/__init__.py +0 -0
  86. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
  87. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/players/__init__.py +0 -0
  88. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
  89. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/readers/__init__.py +0 -0
  90. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/readers/nfc_reader_adapter.py +0 -0
  91. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
  92. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/__init__.py +0 -0
  93. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/entities/disc.py +0 -0
  94. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/entities/library.py +0 -0
  95. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/entities/playback_action.py +0 -0
  96. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/entities/tag_event.py +0 -0
  97. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/ports/__init__.py +0 -0
  98. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/ports/player_port.py +0 -0
  99. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/ports/reader_port.py +0 -0
  100. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/repositories/__init__.py +0 -0
  101. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/repositories/current_tag_repository.py +0 -0
  102. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/domain/repositories/library_repository.py +0 -0
  103. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/shared/__init__.py +0 -0
  104. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/shared/logger.py +0 -0
  105. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/jukebox/shared/timing.py +0 -0
  106. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/pn532/__init__.py +0 -0
  107. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/pn532/pn532.py +0 -0
  108. {gukebox-0.9.0 → gukebox-1.0.0.dev2}/pn532/spi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gukebox
3
- Version: 0.9.0
3
+ Version: 1.0.0.dev2
4
4
  Summary: A Jukebox to play music on speakers using 'CD' with NFC tag
5
5
  Keywords: jukebox,discstore,music,nfc
6
6
  Author: Gudsfile
@@ -36,6 +36,8 @@ Classifier: Programming Language :: Python :: 3.12
36
36
  Classifier: Programming Language :: Python :: 3.13
37
37
  Requires-Dist: pydantic==2.12.5
38
38
  Requires-Dist: soco==0.30.14
39
+ Requires-Dist: typer==0.23.2 ; python_full_version < '3.10'
40
+ Requires-Dist: typer==0.24.1 ; python_full_version >= '3.10'
39
41
  Requires-Dist: fastapi==0.128.7 ; python_full_version < '3.10' and extra == 'api'
40
42
  Requires-Dist: fastapi==0.135.1 ; python_full_version >= '3.10' and extra == 'api'
41
43
  Requires-Dist: uvicorn==0.39.0 ; python_full_version < '3.10' and extra == 'api'
@@ -146,7 +148,7 @@ pip install "gukebox[nfc]"
146
148
  - For non-system Python 3.13+, you can still install via pip/uv/poetry/etc. but you must build the `lgpio` package from source and it may require other system packages.
147
149
  - All releases can be downloaded and installed from the [GitHub releases page](https://github.com/Gudsfile/jukebox/releases).
148
150
 
149
- ### Developer setup
151
+ ### Installation for development
150
152
 
151
153
  For development read the [Developer setup](#developer-setup) section.
152
154
 
@@ -158,9 +160,7 @@ uv sync
158
160
 
159
161
  ## First steps
160
162
 
161
- Set the `JUKEBOX_SONOS_HOST` environment variable with the IP address of your Sonos Zone Player (see [Available players and readers](#players)).
162
-
163
- Initialize the library file with `discstore` or manually create it at `~/.jukebox/library.json`.
163
+ Initialize the library file with `discstore` or manually create it at `~/.config/jukebox/library.json`.
164
164
 
165
165
  ### Manage the library with the discstore
166
166
 
@@ -176,6 +176,16 @@ discstore add --from-current --uri /path/to/media.mp3
176
176
 
177
177
  Other commands are available, use `--help` to see them.
178
178
 
179
+ ### Admin CLI
180
+
181
+ Use `jukebox-admin` for admin workflows such as settings inspection and the
182
+ admin API/UI servers.
183
+
184
+ ```shell
185
+ jukebox-admin settings show
186
+ jukebox-admin settings show --effective
187
+ ```
188
+
179
189
  To use the `api` and `ui` commands, additional packages are required. You can install the `package[extra]` syntax regardless of the package manager you use, for example:
180
190
 
181
191
  ```shell
@@ -189,13 +199,15 @@ uv tool install gukebox[ui]
189
199
  When running from this repository with `uv`, include the extra on the command as well:
190
200
 
191
201
  ```shell
192
- uv run --extra api discstore api
193
- uv run --extra ui discstore ui
202
+ uv run --extra api jukebox-admin api
203
+ uv run --extra ui jukebox-admin ui
194
204
  ```
195
205
 
206
+ `discstore settings ...`, `discstore api`, and `discstore ui` remain available as compatibility commands, but `jukebox-admin` is the preferred CLI for admin flows.
207
+
196
208
  ### Manage the library manually
197
209
 
198
- Complete your `~/.jukebox/library.json` file with each tag id and the expected media URI.
210
+ Complete your `~/.config/jukebox/library.json` file with each tag id and the expected media URI.
199
211
  Take a look at `library.example.json` and the [The library file](#the-library-file) section for more information.
200
212
 
201
213
  ## Usage
@@ -213,7 +225,7 @@ Optional Parameters
213
225
  | Parameter | Description |
214
226
  | --- | --- |
215
227
  | `--help` | Show help message. |
216
- | `--library` | Path to the library file, default: `~/.jukebox/library.json`. |
228
+ | `--library` | Path to the library file, default: `~/.config/jukebox/library.json`. |
217
229
  | `--pause-delay SECONDS` | Grace period before pausing when the NFC tag is removed. Fractional values such as `0.5` or `0.2` are supported, with a minimum of `0.2` seconds to avoid pausing on brief missed reads. Default: 0.25 seconds. |
218
230
  | `--pause-duration SECONDS` | Maximum duration of a pause before resetting the queue. Default: 900 seconds (15 minutes). |
219
231
  | `--verbose` | Enable verbose logging. |
@@ -242,9 +254,13 @@ Displays the events that a real speaker would have performed (`playing …`, `pa
242
254
 
243
255
  **Sonos** (`sonos`) [![SoCo](https://img.shields.io/badge/based%20on-SoCo-000)](https://github.com/SoCo/SoCo)
244
256
  Play music through a Sonos speaker.
245
- `JUKEBOX_SONOS_HOST` environment variable must be set with the IP address of your Sonos Zone Player.
246
- You could set the environment varible with `export JUKEBOX_SONOS_HOST=192.168.0.???` to use this speaker through the `jukebox` command.
247
- Or set it in a `.env` file to use the `uv run --env-file .env <command to run>` version.
257
+ Three ways to select the speaker (mutually exclusive):
258
+
259
+ | Option | CLI flag | Environment variable | Behaviour |
260
+ | --- | --- | --- | --- |
261
+ | By IP | `--sonos-host 192.168.0.x` | `JUKEBOX_SONOS_HOST` | Connect directly, no discovery |
262
+ | By name | `--sonos-name "Living Room"` | `JUKEBOX_SONOS_NAME` | Discover, then filter by name (case-sensitive) |
263
+ | Auto | *(omit both)* | *(omit both)* | Discover, pick the first speaker alphabetically |
248
264
 
249
265
  ## The library file
250
266
 
@@ -252,7 +268,7 @@ The `library.json` file is a JSON file that contains the artists, albums and tag
252
268
  It is used by the `jukebox` command to find the corresponding metadata for each tag.
253
269
  And the `discstore` command help you to managed this file with a CLI, an interactive CLI, an API or an UI (see `discstore --help`).
254
270
 
255
- By default, this file should be placed at `~/.jukebox/library.json`. But you can use another path by creating a `JUKEBOX_LIBRARY_PATH` environment variable or with the `--library` argument.
271
+ By default, this file should be placed at `~/.config/jukebox/library.json`. But you can use another path by creating a `JUKEBOX_LIBRARY_PATH` environment variable or with the `--library` argument.
256
272
 
257
273
  ```json
258
274
  {
@@ -291,7 +307,7 @@ It is also possible to use the `shuffle` key to play the album in shuffle mode:
291
307
  }
292
308
  ```
293
309
 
294
- To summarize, for example, if you have the following `~/.jukebox/library.json` file:
310
+ To summarize, for example, if you have the following `~/.config/jukebox/library.json` file:
295
311
 
296
312
  ```json
297
313
  {
@@ -324,7 +340,8 @@ uv sync
324
340
 
325
341
  Add `--all-extras` to install dependencies for all extras (`api` and `ui`).
326
342
 
327
- Set the `JUKEBOX_SONOS_HOST` environment variable with the IP address of your Sonos Zone Player (see [Available players and readers](#players)).
343
+ If needed, set `JUKEBOX_SONOS_HOST` (IP) or `JUKEBOX_SONOS_NAME` (speaker name) to select your Sonos speaker (see [Players](#players)).
344
+ If neither is set, the jukebox will auto-discover a speaker on the network.
328
345
  To do this you can use a `.env` file and `uv run --env-file .env <command to run>`.
329
346
  A `.env.example` file is available, you can copy it and modify it to use it.
330
347
 
@@ -344,9 +361,23 @@ Start the discstore `uv` and use `--help` to show help message
344
361
  uv run discstore --help
345
362
  ```
346
363
 
347
- For the server-backed commands, include the matching extra:
364
+ Use `jukebox-admin` for admin commands:
348
365
 
349
366
  ```shell
367
+ uv run jukebox-admin settings show
368
+ ```
369
+
370
+ For the server-backed admin commands, include the matching extra:
371
+
372
+ ```shell
373
+ uv run --extra api jukebox-admin api
374
+ uv run --extra ui jukebox-admin ui
375
+ ```
376
+
377
+ Legacy compatibility commands remain available during the transition:
378
+
379
+ ```shell
380
+ uv run discstore settings show
350
381
  uv run --extra api discstore api
351
382
  uv run --extra ui discstore ui
352
383
  ```
@@ -360,6 +391,15 @@ Other commands are available:
360
391
  | `uv run ruff check --fix` | Fix the code. |
361
392
  | `uv run pytest` | Run the tests. |
362
393
 
394
+ ### Pre-commit
395
+
396
+ [prek](https://github.com/j178/prek) is configured; you can [install it](https://github.com/j178/prek?tab=readme-ov-file#installation) to automatically run validations on each commit.
397
+
398
+ ```shell
399
+ uv tool install prek
400
+ prek install
401
+ ```
402
+
363
403
  ## Contributing
364
404
 
365
405
  Contributions are welcome! Feel free to open an issue or a pull request.
@@ -91,7 +91,7 @@ pip install "gukebox[nfc]"
91
91
  - For non-system Python 3.13+, you can still install via pip/uv/poetry/etc. but you must build the `lgpio` package from source and it may require other system packages.
92
92
  - All releases can be downloaded and installed from the [GitHub releases page](https://github.com/Gudsfile/jukebox/releases).
93
93
 
94
- ### Developer setup
94
+ ### Installation for development
95
95
 
96
96
  For development read the [Developer setup](#developer-setup) section.
97
97
 
@@ -103,9 +103,7 @@ uv sync
103
103
 
104
104
  ## First steps
105
105
 
106
- Set the `JUKEBOX_SONOS_HOST` environment variable with the IP address of your Sonos Zone Player (see [Available players and readers](#players)).
107
-
108
- Initialize the library file with `discstore` or manually create it at `~/.jukebox/library.json`.
106
+ Initialize the library file with `discstore` or manually create it at `~/.config/jukebox/library.json`.
109
107
 
110
108
  ### Manage the library with the discstore
111
109
 
@@ -121,6 +119,16 @@ discstore add --from-current --uri /path/to/media.mp3
121
119
 
122
120
  Other commands are available, use `--help` to see them.
123
121
 
122
+ ### Admin CLI
123
+
124
+ Use `jukebox-admin` for admin workflows such as settings inspection and the
125
+ admin API/UI servers.
126
+
127
+ ```shell
128
+ jukebox-admin settings show
129
+ jukebox-admin settings show --effective
130
+ ```
131
+
124
132
  To use the `api` and `ui` commands, additional packages are required. You can install the `package[extra]` syntax regardless of the package manager you use, for example:
125
133
 
126
134
  ```shell
@@ -134,13 +142,15 @@ uv tool install gukebox[ui]
134
142
  When running from this repository with `uv`, include the extra on the command as well:
135
143
 
136
144
  ```shell
137
- uv run --extra api discstore api
138
- uv run --extra ui discstore ui
145
+ uv run --extra api jukebox-admin api
146
+ uv run --extra ui jukebox-admin ui
139
147
  ```
140
148
 
149
+ `discstore settings ...`, `discstore api`, and `discstore ui` remain available as compatibility commands, but `jukebox-admin` is the preferred CLI for admin flows.
150
+
141
151
  ### Manage the library manually
142
152
 
143
- Complete your `~/.jukebox/library.json` file with each tag id and the expected media URI.
153
+ Complete your `~/.config/jukebox/library.json` file with each tag id and the expected media URI.
144
154
  Take a look at `library.example.json` and the [The library file](#the-library-file) section for more information.
145
155
 
146
156
  ## Usage
@@ -158,7 +168,7 @@ Optional Parameters
158
168
  | Parameter | Description |
159
169
  | --- | --- |
160
170
  | `--help` | Show help message. |
161
- | `--library` | Path to the library file, default: `~/.jukebox/library.json`. |
171
+ | `--library` | Path to the library file, default: `~/.config/jukebox/library.json`. |
162
172
  | `--pause-delay SECONDS` | Grace period before pausing when the NFC tag is removed. Fractional values such as `0.5` or `0.2` are supported, with a minimum of `0.2` seconds to avoid pausing on brief missed reads. Default: 0.25 seconds. |
163
173
  | `--pause-duration SECONDS` | Maximum duration of a pause before resetting the queue. Default: 900 seconds (15 minutes). |
164
174
  | `--verbose` | Enable verbose logging. |
@@ -187,9 +197,13 @@ Displays the events that a real speaker would have performed (`playing …`, `pa
187
197
 
188
198
  **Sonos** (`sonos`) [![SoCo](https://img.shields.io/badge/based%20on-SoCo-000)](https://github.com/SoCo/SoCo)
189
199
  Play music through a Sonos speaker.
190
- `JUKEBOX_SONOS_HOST` environment variable must be set with the IP address of your Sonos Zone Player.
191
- You could set the environment varible with `export JUKEBOX_SONOS_HOST=192.168.0.???` to use this speaker through the `jukebox` command.
192
- Or set it in a `.env` file to use the `uv run --env-file .env <command to run>` version.
200
+ Three ways to select the speaker (mutually exclusive):
201
+
202
+ | Option | CLI flag | Environment variable | Behaviour |
203
+ | --- | --- | --- | --- |
204
+ | By IP | `--sonos-host 192.168.0.x` | `JUKEBOX_SONOS_HOST` | Connect directly, no discovery |
205
+ | By name | `--sonos-name "Living Room"` | `JUKEBOX_SONOS_NAME` | Discover, then filter by name (case-sensitive) |
206
+ | Auto | *(omit both)* | *(omit both)* | Discover, pick the first speaker alphabetically |
193
207
 
194
208
  ## The library file
195
209
 
@@ -197,7 +211,7 @@ The `library.json` file is a JSON file that contains the artists, albums and tag
197
211
  It is used by the `jukebox` command to find the corresponding metadata for each tag.
198
212
  And the `discstore` command help you to managed this file with a CLI, an interactive CLI, an API or an UI (see `discstore --help`).
199
213
 
200
- By default, this file should be placed at `~/.jukebox/library.json`. But you can use another path by creating a `JUKEBOX_LIBRARY_PATH` environment variable or with the `--library` argument.
214
+ By default, this file should be placed at `~/.config/jukebox/library.json`. But you can use another path by creating a `JUKEBOX_LIBRARY_PATH` environment variable or with the `--library` argument.
201
215
 
202
216
  ```json
203
217
  {
@@ -236,7 +250,7 @@ It is also possible to use the `shuffle` key to play the album in shuffle mode:
236
250
  }
237
251
  ```
238
252
 
239
- To summarize, for example, if you have the following `~/.jukebox/library.json` file:
253
+ To summarize, for example, if you have the following `~/.config/jukebox/library.json` file:
240
254
 
241
255
  ```json
242
256
  {
@@ -269,7 +283,8 @@ uv sync
269
283
 
270
284
  Add `--all-extras` to install dependencies for all extras (`api` and `ui`).
271
285
 
272
- Set the `JUKEBOX_SONOS_HOST` environment variable with the IP address of your Sonos Zone Player (see [Available players and readers](#players)).
286
+ If needed, set `JUKEBOX_SONOS_HOST` (IP) or `JUKEBOX_SONOS_NAME` (speaker name) to select your Sonos speaker (see [Players](#players)).
287
+ If neither is set, the jukebox will auto-discover a speaker on the network.
273
288
  To do this you can use a `.env` file and `uv run --env-file .env <command to run>`.
274
289
  A `.env.example` file is available, you can copy it and modify it to use it.
275
290
 
@@ -289,9 +304,23 @@ Start the discstore `uv` and use `--help` to show help message
289
304
  uv run discstore --help
290
305
  ```
291
306
 
292
- For the server-backed commands, include the matching extra:
307
+ Use `jukebox-admin` for admin commands:
293
308
 
294
309
  ```shell
310
+ uv run jukebox-admin settings show
311
+ ```
312
+
313
+ For the server-backed admin commands, include the matching extra:
314
+
315
+ ```shell
316
+ uv run --extra api jukebox-admin api
317
+ uv run --extra ui jukebox-admin ui
318
+ ```
319
+
320
+ Legacy compatibility commands remain available during the transition:
321
+
322
+ ```shell
323
+ uv run discstore settings show
295
324
  uv run --extra api discstore api
296
325
  uv run --extra ui discstore ui
297
326
  ```
@@ -305,6 +334,15 @@ Other commands are available:
305
334
  | `uv run ruff check --fix` | Fix the code. |
306
335
  | `uv run pytest` | Run the tests. |
307
336
 
337
+ ### Pre-commit
338
+
339
+ [prek](https://github.com/j178/prek) is configured; you can [install it](https://github.com/j178/prek?tab=readme-ov-file#installation) to automatically run validations on each commit.
340
+
341
+ ```shell
342
+ uv tool install prek
343
+ prek install
344
+ ```
345
+
308
346
  ## Contributing
309
347
 
310
348
  Contributions are welcome! Feel free to open an issue or a pull request.
@@ -1,4 +1,6 @@
1
- from typing import Dict
1
+ from typing import Any, Dict, cast
2
+
3
+ from pydantic import BaseModel, RootModel
2
4
 
3
5
  from jukebox.shared.dependency_messages import optional_extra_dependency_message
4
6
 
@@ -15,6 +17,9 @@ from discstore.domain.use_cases.edit_disc import EditDisc
15
17
  from discstore.domain.use_cases.get_current_tag_status import GetCurrentTagStatus
16
18
  from discstore.domain.use_cases.list_discs import ListDiscs
17
19
  from discstore.domain.use_cases.remove_disc import RemoveDisc
20
+ from jukebox.settings.errors import SettingsError
21
+ from jukebox.settings.service_protocols import SettingsService
22
+ from jukebox.settings.types import JsonObject
18
23
 
19
24
 
20
25
  class DiscInput(Disc):
@@ -29,6 +34,14 @@ class CurrentTagStatusOutput(CurrentTagStatus):
29
34
  pass
30
35
 
31
36
 
37
+ class SettingsResetInput(BaseModel):
38
+ path: str
39
+
40
+
41
+ class SettingsPatchInput(RootModel[Dict[str, Any]]):
42
+ pass
43
+
44
+
32
45
  class APIController:
33
46
  def __init__(
34
47
  self,
@@ -37,12 +50,14 @@ class APIController:
37
50
  remove_disc: RemoveDisc,
38
51
  edit_disc: EditDisc,
39
52
  get_current_tag_status: GetCurrentTagStatus,
53
+ settings_service: SettingsService,
40
54
  ):
41
55
  self.add_disc = add_disc
42
56
  self.list_discs = list_discs
43
57
  self.remove_disc = remove_disc
44
58
  self.edit_disc = edit_disc
45
59
  self.get_current_tag_status = get_current_tag_status
60
+ self.settings_service = settings_service
46
61
  self.app = FastAPI(
47
62
  title="DiscStore API",
48
63
  description="API for managing Jukebox disc library",
@@ -68,6 +83,38 @@ class APIController:
68
83
 
69
84
  return CurrentTagStatusOutput(**current_tag_status.model_dump())
70
85
 
86
+ @self.app.get("/api/v1/settings")
87
+ def get_settings():
88
+ try:
89
+ return self.settings_service.get_persisted_settings_view()
90
+ except Exception as err:
91
+ raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
92
+
93
+ @self.app.get("/api/v1/settings/effective")
94
+ def get_effective_settings():
95
+ try:
96
+ return self.settings_service.get_effective_settings_view()
97
+ except Exception as err:
98
+ raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
99
+
100
+ @self.app.patch("/api/v1/settings")
101
+ def patch_settings(patch: SettingsPatchInput):
102
+ try:
103
+ return self.settings_service.patch_persisted_settings(cast(JsonObject, patch.root))
104
+ except SettingsError as err:
105
+ raise HTTPException(status_code=400, detail=str(err))
106
+ except Exception as err:
107
+ raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
108
+
109
+ @self.app.post("/api/v1/settings/reset")
110
+ def reset_settings(payload: SettingsResetInput):
111
+ try:
112
+ return self.settings_service.reset_persisted_value(payload.path)
113
+ except SettingsError as err:
114
+ raise HTTPException(status_code=400, detail=str(err))
115
+ except Exception as err:
116
+ raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
117
+
71
118
  @self.app.post("/api/v1/disc", status_code=201)
72
119
  def add_or_edit_disc(tag_id: str, disc: DiscInput):
73
120
  try:
@@ -85,7 +132,7 @@ class APIController:
85
132
  try:
86
133
  self.remove_disc.execute(tag_id)
87
134
  return {"message": "Disc removed"}
88
- except ValueError as valueErr:
89
- raise HTTPException(status_code=404, detail=str(valueErr))
135
+ except ValueError as value_err:
136
+ raise HTTPException(status_code=404, detail=str(value_err))
90
137
  except Exception as err:
91
138
  raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
@@ -2,7 +2,7 @@ import logging
2
2
  from typing import Union
3
3
 
4
4
  from discstore.adapters.inbound.cli_display import display_library_line, display_library_table
5
- from discstore.adapters.inbound.config import (
5
+ from discstore.commands import (
6
6
  CliAddCommand,
7
7
  CliEditCommand,
8
8
  CliGetCommand,
@@ -0,0 +1,250 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from typing import Optional, Union
5
+
6
+ from pydantic import BaseModel, ValidationError
7
+
8
+ from discstore.commands import (
9
+ CliAddCommand,
10
+ CliEditCommand,
11
+ CliGetCommand,
12
+ CliListCommand,
13
+ CliListCommandModes,
14
+ CliRemoveCommand,
15
+ CliSearchCommand,
16
+ InteractiveCliCommand,
17
+ )
18
+ from jukebox.admin.commands import ApiCommand, SettingsResetCommand, SettingsSetCommand, SettingsShowCommand, UiCommand
19
+ from jukebox.shared.config_utils import add_verbose_arg, add_version_arg
20
+
21
+ LOGGER = logging.getLogger("discstore")
22
+
23
+ __all__ = [
24
+ "ApiCommand",
25
+ "CliAddCommand",
26
+ "CliEditCommand",
27
+ "CliGetCommand",
28
+ "CliListCommand",
29
+ "CliListCommandModes",
30
+ "CliRemoveCommand",
31
+ "CliSearchCommand",
32
+ "DiscStoreConfig",
33
+ "InteractiveCliCommand",
34
+ "SettingsResetCommand",
35
+ "SettingsSetCommand",
36
+ "SettingsShowCommand",
37
+ "UiCommand",
38
+ "add_from_current_arg",
39
+ "parse_config",
40
+ ]
41
+
42
+
43
+ class DiscStoreConfig(BaseModel):
44
+ library: Optional[str] = None
45
+ verbose: bool = False
46
+
47
+ command: Union[
48
+ ApiCommand,
49
+ InteractiveCliCommand,
50
+ CliAddCommand,
51
+ CliListCommand,
52
+ CliRemoveCommand,
53
+ CliEditCommand,
54
+ CliGetCommand,
55
+ CliSearchCommand,
56
+ SettingsResetCommand,
57
+ SettingsSetCommand,
58
+ SettingsShowCommand,
59
+ UiCommand,
60
+ ]
61
+
62
+
63
+ def add_from_current_arg(parser: argparse.ArgumentParser) -> None:
64
+ parser.add_argument(
65
+ "--from-current",
66
+ dest="use_current_tag",
67
+ action="store_true",
68
+ help="Resolve the tag ID from shared current-tag.txt state",
69
+ )
70
+
71
+
72
+ def _build_library_command(command_name: str, args: argparse.Namespace):
73
+ if command_name == "add":
74
+ return CliAddCommand(
75
+ type="add",
76
+ tag=args.tag,
77
+ use_current_tag=args.use_current_tag,
78
+ uri=args.uri,
79
+ track=args.track,
80
+ artist=args.artist,
81
+ album=args.album,
82
+ )
83
+ if command_name == "list":
84
+ if args.positional_mode is not None:
85
+ print(
86
+ "warning: positional mode argument is deprecated; use --mode instead",
87
+ file=sys.stderr,
88
+ )
89
+ # if args.positional_mode is not None and args.mode is not None:
90
+ # raise ValueError("You must provide exactly one of: mode OR --mode argument")
91
+ return CliListCommand(type="list", mode=args.positional_mode if args.positional_mode else args.mode)
92
+ if command_name == "remove":
93
+ return CliRemoveCommand(type="remove", tag=args.tag, use_current_tag=args.use_current_tag)
94
+ if command_name == "edit":
95
+ return CliEditCommand(
96
+ type="edit",
97
+ tag=args.tag,
98
+ use_current_tag=args.use_current_tag,
99
+ uri=args.uri,
100
+ track=args.track,
101
+ artist=args.artist,
102
+ album=args.album,
103
+ )
104
+ if command_name == "get":
105
+ return CliGetCommand(type="get", tag=args.tag, use_current_tag=args.use_current_tag)
106
+ if command_name == "search":
107
+ return CliSearchCommand(type="search", query=args.query)
108
+ if command_name == "interactive":
109
+ return InteractiveCliCommand(type="interactive")
110
+ raise ValueError(f"Unsupported command: {command_name}")
111
+
112
+
113
+ def _build_admin_command(args: argparse.Namespace):
114
+ if args.command == "api":
115
+ return ApiCommand(type="api", port=args.port)
116
+ if args.command == "ui":
117
+ return UiCommand(type="ui", port=args.port)
118
+ if args.command != "settings":
119
+ raise ValueError(f"Unsupported admin command: {args.command}")
120
+
121
+ if args.settings_command == "show":
122
+ return SettingsShowCommand(type="settings_show", effective=args.effective, json_output=args.json_output)
123
+ if args.settings_command == "set":
124
+ return SettingsSetCommand(
125
+ type="settings_set",
126
+ dotted_path=args.dotted_path,
127
+ value=args.value,
128
+ json_output=args.json_output,
129
+ )
130
+ if args.settings_command == "reset":
131
+ return SettingsResetCommand(
132
+ type="settings_reset",
133
+ dotted_path=args.dotted_path,
134
+ json_output=args.json_output,
135
+ )
136
+ raise ValueError(f"Unsupported settings command: {args.settings_command}")
137
+
138
+
139
+ def parse_config() -> DiscStoreConfig:
140
+ parser = argparse.ArgumentParser(
141
+ prog="discstore",
142
+ description="Manage your disc collection for jukebox",
143
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
144
+ )
145
+
146
+ parser.add_argument(
147
+ "-l",
148
+ "--library",
149
+ default=None,
150
+ help="override the library JSON path for this process",
151
+ )
152
+ add_verbose_arg(parser)
153
+ add_version_arg(parser)
154
+
155
+ subparsers = parser.add_subparsers(dest="command", required=True)
156
+
157
+ # CLI commands
158
+ add_parser = subparsers.add_parser("add", help="Add a disc")
159
+ add_from_current_arg(add_parser)
160
+ add_parser.add_argument("tag", nargs="?", help="Tag to be associated with the disc")
161
+ add_parser.add_argument("--uri", required=True, help="Path or URI of the media file")
162
+ add_parser.add_argument("--track", required=False, help="Name of the track")
163
+ add_parser.add_argument("--artist", required=False, help="Name of the artist or band")
164
+ add_parser.add_argument("--album", required=False, help="Name of the album")
165
+ add_parser.add_argument("--opts", required=False, help="Playback options for the discs")
166
+
167
+ list_parser = subparsers.add_parser("list", help="List all discs")
168
+ list_parser.add_argument("positional_mode", nargs="?", choices=["line", "table"], help=argparse.SUPPRESS)
169
+ list_parser.add_argument(
170
+ "--mode", choices=["line", "table"], default="table", help="Displaying mode (default: table)"
171
+ )
172
+
173
+ remove_parser = subparsers.add_parser("remove", help="Remove a disc")
174
+ add_from_current_arg(remove_parser)
175
+ remove_parser.add_argument("tag", nargs="?", help="Tag to remove")
176
+
177
+ edit_parser = subparsers.add_parser("edit", help="Edit a disc (partial updates supported)")
178
+ add_from_current_arg(edit_parser)
179
+ edit_parser.add_argument("tag", nargs="?", help="Tag to be edited")
180
+ edit_parser.add_argument("--uri", required=False, help="Path or URI of the media file")
181
+ edit_parser.add_argument("--track", required=False, help="Name of the track")
182
+ edit_parser.add_argument("--artist", required=False, help="Name of the artist or band")
183
+ edit_parser.add_argument("--album", required=False, help="Name of the album")
184
+ edit_parser.add_argument("--opts", required=False, help="Playback options for the discs")
185
+
186
+ get_parser = subparsers.add_parser("get", help="Get a disc by tag ID")
187
+ add_from_current_arg(get_parser)
188
+ get_parser.add_argument("tag", nargs="?", help="Tag to retrieve")
189
+
190
+ search_parser = subparsers.add_parser("search", help="Search discs by query")
191
+ search_parser.add_argument("query", help="Search query (matches artist, album, track, playlist, or tag)")
192
+
193
+ # API commands
194
+ api_parser = subparsers.add_parser("api", help="Start an API server")
195
+ api_parser.add_argument("--port", type=int, default=None, help="override the configured API port")
196
+
197
+ # UI commands
198
+ ui_parser = subparsers.add_parser("ui", help="Start an UI server")
199
+ ui_parser.add_argument("--port", type=int, default=None, help="override the configured UI port")
200
+
201
+ # Interactive commands
202
+ _ = subparsers.add_parser("interactive", help="Run interactive CLI")
203
+
204
+ settings_parser = subparsers.add_parser("settings", help="Inspect application settings")
205
+ settings_subparsers = settings_parser.add_subparsers(dest="settings_command", required=True)
206
+ settings_show_parser = settings_subparsers.add_parser("show", help="Show persisted settings")
207
+ settings_show_parser.add_argument(
208
+ "--effective",
209
+ action="store_true",
210
+ help="show merged effective settings with provenance",
211
+ )
212
+ settings_show_parser.add_argument(
213
+ "--json",
214
+ dest="json_output",
215
+ action="store_true",
216
+ help="print the raw machine-readable payload",
217
+ )
218
+ settings_set_parser = settings_subparsers.add_parser("set", help="Set a persisted setting override")
219
+ settings_set_parser.add_argument("dotted_path", help="canonical dotted path to update")
220
+ settings_set_parser.add_argument("value", help="value to persist for the given path")
221
+ settings_set_parser.add_argument(
222
+ "--json",
223
+ dest="json_output",
224
+ action="store_true",
225
+ help="print the raw machine-readable payload",
226
+ )
227
+ settings_reset_parser = settings_subparsers.add_parser("reset", help="Remove a persisted setting override")
228
+ settings_reset_parser.add_argument("dotted_path", help="canonical dotted path to reset")
229
+ settings_reset_parser.add_argument(
230
+ "--json",
231
+ dest="json_output",
232
+ action="store_true",
233
+ help="print the raw machine-readable payload",
234
+ )
235
+
236
+ args = parser.parse_args()
237
+
238
+ # Build command config
239
+ try:
240
+ command = (
241
+ _build_admin_command(args)
242
+ if args.command in {"api", "ui", "settings"}
243
+ else _build_library_command(args.command, args)
244
+ )
245
+ config = DiscStoreConfig(library=args.library, verbose=args.verbose, command=command)
246
+ except (ValidationError, ValueError) as err:
247
+ LOGGER.error("Config error: %s", err)
248
+ exit(1)
249
+
250
+ return config