gukebox 1.0.0.dev8__tar.gz → 1.0.0.dev9__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.
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/PKG-INFO +8 -23
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/README.md +6 -21
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/api_controller.py +3 -3
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/ui_controller.py +146 -2
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/ui_pages/library.py +6 -1
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/ui_pages/settings.py +7 -3
- gukebox-1.0.0.dev9/discstore/adapters/inbound/ui_pages/sonos.py +502 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/commands.py +1 -27
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/app.py +0 -1
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/cli_presentation.py +0 -40
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/command_handlers.py +0 -11
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/resolve.py +1 -1
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/pyproject.toml +2 -3
- gukebox-1.0.0.dev8/discstore/adapters/inbound/config.py +0 -250
- gukebox-1.0.0.dev8/discstore/app.py +0 -103
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/LICENSE +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/api/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/api/current_tag_router.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/api/discs_router.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/api/models.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/api/settings_router.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/cli_display.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/inbound/ui_pages/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/command_handlers.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/di_container.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/add_disc.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/edit_disc.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/get_disc.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/list_discs.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/remove_disc.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/discstore/domain/use_cases/search_discs.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/inbound/config.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/players/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/readers/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/commands.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/di_container.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/admin/services.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/app.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/di_container.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/current_tag_action.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/playback_action.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/playback_session.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/use_cases/determine_action.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/domain/use_cases/handle_tag_event.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/definitions.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/entities.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/errors.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/file_settings_repository.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/migration.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/runtime_resolver.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/selected_sonos_group_repository.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/service_protocols.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/types.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/shared/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/shared/dependency_messages.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/shared/logger.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/shared/timing.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/sonos/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/sonos/discovery.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/sonos/selection.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/jukebox/sonos/service.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/pn532/__init__.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/pn532/pn532.py +0 -0
- {gukebox-1.0.0.dev8 → gukebox-1.0.0.dev9}/pn532/spi.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: gukebox
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev9
|
|
4
4
|
Summary: A Jukebox to play music on speakers using 'CD' with NFC tag
|
|
5
|
-
Keywords: jukebox,
|
|
5
|
+
Keywords: jukebox,music,nfc
|
|
6
6
|
Author: Gudsfile
|
|
7
7
|
License: MIT License
|
|
8
8
|
|
|
@@ -71,7 +71,7 @@ Description-Content-Type: text/markdown
|
|
|
71
71
|
|
|
72
72
|
🚧 At the moment:
|
|
73
73
|
|
|
74
|
-
- NFC tags - CDs must be pre-populated in a JSON file (`
|
|
74
|
+
- NFC tags - CDs must be pre-populated in a JSON file (`jukebox-admin` included with `jukebox` may be of help to you)
|
|
75
75
|
- supports many music providers (Spotify, Apple Music, etc.), just add the URIs to the JSON file
|
|
76
76
|
- only works with Sonos speakers (there is a "dryrun" player for development), but code is designed to **add new ones**
|
|
77
77
|
- **as soon as** the NFC tag is removed, the music pauses, then resumes when the NFC tag is replaced
|
|
@@ -161,18 +161,18 @@ uv sync
|
|
|
161
161
|
|
|
162
162
|
## First steps
|
|
163
163
|
|
|
164
|
-
Initialize the library file with `
|
|
164
|
+
Initialize the library file with `jukebox-admin` or manually create it at `~/.config/jukebox/library.json`.
|
|
165
165
|
|
|
166
|
-
### Manage the library with the
|
|
166
|
+
### Manage the library with the Admin CLI
|
|
167
167
|
|
|
168
168
|
To associate an URI with an NFC tag:
|
|
169
169
|
|
|
170
170
|
```shell
|
|
171
|
-
|
|
171
|
+
jukebox-admin library add tag_id --uri /path/to/media.mp3
|
|
172
172
|
```
|
|
173
173
|
or to pull the `tag_id` currently on the reader:
|
|
174
174
|
```shell
|
|
175
|
-
|
|
175
|
+
jukebox-admin library add --from-current --uri /path/to/media.mp3
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
Other commands are available, use `--help` to see them.
|
|
@@ -204,8 +204,6 @@ uv run --extra api jukebox-admin api
|
|
|
204
204
|
uv run --extra ui jukebox-admin ui
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
-
`discstore settings ...`, `discstore api`, and `discstore ui` remain available as compatibility commands, but `jukebox-admin` is the preferred CLI for admin flows.
|
|
208
|
-
|
|
209
207
|
### Manage the library manually
|
|
210
208
|
|
|
211
209
|
Complete your `~/.config/jukebox/library.json` file with each tag id and the expected media URI.
|
|
@@ -258,7 +256,7 @@ jukebox --player PLAYER --reader READER
|
|
|
258
256
|
|
|
259
257
|
The `library.json` file is a JSON file that contains the artists, albums and tags.
|
|
260
258
|
It is used by the `jukebox` command to find the corresponding metadata for each tag.
|
|
261
|
-
And the `
|
|
259
|
+
And the `jukebox-admin library` command help you to managed this file with a CLI, an interactive CLI, an API or an UI (see `jukebox-admin --help`).
|
|
262
260
|
|
|
263
261
|
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.
|
|
264
262
|
|
|
@@ -346,11 +344,6 @@ Start the jukebox with `uv` and use `--help` to show help message
|
|
|
346
344
|
uv run jukebox --player PLAYER_TO_USE --reader READER_TO_USE
|
|
347
345
|
```
|
|
348
346
|
|
|
349
|
-
Start the discstore `uv` and use `--help` to show help message
|
|
350
|
-
```shell
|
|
351
|
-
uv run discstore --help
|
|
352
|
-
```
|
|
353
|
-
|
|
354
347
|
Use `jukebox-admin` for admin commands:
|
|
355
348
|
|
|
356
349
|
```shell
|
|
@@ -364,14 +357,6 @@ uv run --extra api jukebox-admin api
|
|
|
364
357
|
uv run --extra ui jukebox-admin ui
|
|
365
358
|
```
|
|
366
359
|
|
|
367
|
-
Legacy compatibility commands remain available during the transition:
|
|
368
|
-
|
|
369
|
-
```shell
|
|
370
|
-
uv run discstore settings show
|
|
371
|
-
uv run --extra api discstore api
|
|
372
|
-
uv run --extra ui discstore ui
|
|
373
|
-
```
|
|
374
|
-
|
|
375
360
|
### Development commands
|
|
376
361
|
|
|
377
362
|
| Command | Description |
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
🚧 At the moment:
|
|
15
15
|
|
|
16
|
-
- NFC tags - CDs must be pre-populated in a JSON file (`
|
|
16
|
+
- NFC tags - CDs must be pre-populated in a JSON file (`jukebox-admin` included with `jukebox` may be of help to you)
|
|
17
17
|
- supports many music providers (Spotify, Apple Music, etc.), just add the URIs to the JSON file
|
|
18
18
|
- only works with Sonos speakers (there is a "dryrun" player for development), but code is designed to **add new ones**
|
|
19
19
|
- **as soon as** the NFC tag is removed, the music pauses, then resumes when the NFC tag is replaced
|
|
@@ -103,18 +103,18 @@ uv sync
|
|
|
103
103
|
|
|
104
104
|
## First steps
|
|
105
105
|
|
|
106
|
-
Initialize the library file with `
|
|
106
|
+
Initialize the library file with `jukebox-admin` or manually create it at `~/.config/jukebox/library.json`.
|
|
107
107
|
|
|
108
|
-
### Manage the library with the
|
|
108
|
+
### Manage the library with the Admin CLI
|
|
109
109
|
|
|
110
110
|
To associate an URI with an NFC tag:
|
|
111
111
|
|
|
112
112
|
```shell
|
|
113
|
-
|
|
113
|
+
jukebox-admin library add tag_id --uri /path/to/media.mp3
|
|
114
114
|
```
|
|
115
115
|
or to pull the `tag_id` currently on the reader:
|
|
116
116
|
```shell
|
|
117
|
-
|
|
117
|
+
jukebox-admin library add --from-current --uri /path/to/media.mp3
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
Other commands are available, use `--help` to see them.
|
|
@@ -146,8 +146,6 @@ uv run --extra api jukebox-admin api
|
|
|
146
146
|
uv run --extra ui jukebox-admin ui
|
|
147
147
|
```
|
|
148
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
|
-
|
|
151
149
|
### Manage the library manually
|
|
152
150
|
|
|
153
151
|
Complete your `~/.config/jukebox/library.json` file with each tag id and the expected media URI.
|
|
@@ -200,7 +198,7 @@ jukebox --player PLAYER --reader READER
|
|
|
200
198
|
|
|
201
199
|
The `library.json` file is a JSON file that contains the artists, albums and tags.
|
|
202
200
|
It is used by the `jukebox` command to find the corresponding metadata for each tag.
|
|
203
|
-
And the `
|
|
201
|
+
And the `jukebox-admin library` command help you to managed this file with a CLI, an interactive CLI, an API or an UI (see `jukebox-admin --help`).
|
|
204
202
|
|
|
205
203
|
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.
|
|
206
204
|
|
|
@@ -288,11 +286,6 @@ Start the jukebox with `uv` and use `--help` to show help message
|
|
|
288
286
|
uv run jukebox --player PLAYER_TO_USE --reader READER_TO_USE
|
|
289
287
|
```
|
|
290
288
|
|
|
291
|
-
Start the discstore `uv` and use `--help` to show help message
|
|
292
|
-
```shell
|
|
293
|
-
uv run discstore --help
|
|
294
|
-
```
|
|
295
|
-
|
|
296
289
|
Use `jukebox-admin` for admin commands:
|
|
297
290
|
|
|
298
291
|
```shell
|
|
@@ -306,14 +299,6 @@ uv run --extra api jukebox-admin api
|
|
|
306
299
|
uv run --extra ui jukebox-admin ui
|
|
307
300
|
```
|
|
308
301
|
|
|
309
|
-
Legacy compatibility commands remain available during the transition:
|
|
310
|
-
|
|
311
|
-
```shell
|
|
312
|
-
uv run discstore settings show
|
|
313
|
-
uv run --extra api discstore api
|
|
314
|
-
uv run --extra ui discstore ui
|
|
315
|
-
```
|
|
316
|
-
|
|
317
302
|
### Development commands
|
|
318
303
|
|
|
319
304
|
| Command | Description |
|
|
@@ -23,7 +23,7 @@ except ModuleNotFoundError as e:
|
|
|
23
23
|
if e.name != "fastapi":
|
|
24
24
|
raise
|
|
25
25
|
raise ModuleNotFoundError(
|
|
26
|
-
optional_extra_dependency_message("The `api_controller` module", "api", "
|
|
26
|
+
optional_extra_dependency_message("The `api_controller` module", "api", "jukebox-admin api")
|
|
27
27
|
) from e
|
|
28
28
|
from discstore.domain.use_cases.add_disc import AddDisc
|
|
29
29
|
from discstore.domain.use_cases.edit_disc import EditDisc
|
|
@@ -108,8 +108,8 @@ class APIController:
|
|
|
108
108
|
self.settings_service = settings_service
|
|
109
109
|
self.sonos_service = sonos_service
|
|
110
110
|
self.app = FastAPI(
|
|
111
|
-
title="
|
|
112
|
-
description="API for managing Jukebox disc library",
|
|
111
|
+
title="Jukebox Admin API",
|
|
112
|
+
description="API for managing Jukebox disc library and settings",
|
|
113
113
|
docs_url="/docs",
|
|
114
114
|
redoc_url="/redoc",
|
|
115
115
|
)
|
|
@@ -16,7 +16,7 @@ try:
|
|
|
16
16
|
from fastui.forms import fastui_form
|
|
17
17
|
except ModuleNotFoundError as e:
|
|
18
18
|
raise ModuleNotFoundError(
|
|
19
|
-
optional_extra_dependency_message("The `ui_controller` module", "ui", "
|
|
19
|
+
optional_extra_dependency_message("The `ui_controller` module", "ui", "jukebox-admin ui")
|
|
20
20
|
) from e
|
|
21
21
|
|
|
22
22
|
from pydantic import BaseModel, Field
|
|
@@ -24,6 +24,7 @@ from pydantic import BaseModel, Field
|
|
|
24
24
|
from discstore.adapters.inbound.api_controller import APIController
|
|
25
25
|
from discstore.adapters.inbound.ui_pages.library import DiscForm, DiscTable, LibraryUIPageBuilder
|
|
26
26
|
from discstore.adapters.inbound.ui_pages.settings import SettingsUIPageBuilder
|
|
27
|
+
from discstore.adapters.inbound.ui_pages.sonos import SonosSelectionForm, SonosUIPageBuilder
|
|
27
28
|
from discstore.domain.entities import CurrentTagStatus, Disc, DiscMetadata, DiscOption
|
|
28
29
|
from discstore.domain.use_cases.add_disc import AddDisc
|
|
29
30
|
from discstore.domain.use_cases.edit_disc import EditDisc
|
|
@@ -36,8 +37,11 @@ from jukebox.settings.definitions import (
|
|
|
36
37
|
get_setting_definition,
|
|
37
38
|
)
|
|
38
39
|
from jukebox.settings.errors import SettingsError
|
|
40
|
+
from jukebox.settings.selected_sonos_group_repository import SettingsSelectedSonosGroupRepository
|
|
39
41
|
from jukebox.settings.service_protocols import SettingsService
|
|
40
42
|
from jukebox.settings.types import JsonObject
|
|
43
|
+
from jukebox.sonos.discovery import SonosDiscoveryError
|
|
44
|
+
from jukebox.sonos.selection import SaveSonosSelection
|
|
41
45
|
from jukebox.sonos.service import SonosService
|
|
42
46
|
|
|
43
47
|
|
|
@@ -63,6 +67,7 @@ class UIController(APIController):
|
|
|
63
67
|
get_disc=get_disc,
|
|
64
68
|
get_current_tag_status=get_current_tag_status,
|
|
65
69
|
)
|
|
70
|
+
self.sonos_pages = SonosUIPageBuilder(settings_service=settings_service, sonos_service=sonos_service)
|
|
66
71
|
self.settings_pages = SettingsUIPageBuilder(settings_service=settings_service)
|
|
67
72
|
super().__init__(
|
|
68
73
|
add_disc,
|
|
@@ -222,10 +227,69 @@ class UIController(APIController):
|
|
|
222
227
|
async def reset_setting(setting_path: str) -> list[AnyComponent]:
|
|
223
228
|
return self._reset_setting(setting_path)
|
|
224
229
|
|
|
230
|
+
@self.app.get("/api/ui/sonos", response_model=FastUI, response_model_exclude_none=True)
|
|
231
|
+
def sonos_page(toast: Optional[str] = None, toast_message: Optional[str] = None) -> List[AnyComponent]:
|
|
232
|
+
return self._build_sonos_page_components(toast=toast, toast_message=toast_message)
|
|
233
|
+
|
|
234
|
+
@self.app.get("/api/ui/sonos/edit", response_model=FastUI, response_model_exclude_none=True)
|
|
235
|
+
def edit_sonos_form(
|
|
236
|
+
error_message: Optional[str] = None,
|
|
237
|
+
uids: Optional[List[str]] = None,
|
|
238
|
+
coordinator_uid: Optional[str] = None,
|
|
239
|
+
) -> List[AnyComponent]:
|
|
240
|
+
field_errors = None
|
|
241
|
+
if error_message:
|
|
242
|
+
field_errors = {self._sonos_field_name_for_error(error_message): error_message}
|
|
243
|
+
return self._build_sonos_edit_page_components(
|
|
244
|
+
error_message=error_message,
|
|
245
|
+
field_errors=field_errors,
|
|
246
|
+
submitted_uids=uids,
|
|
247
|
+
submitted_coordinator_uid=coordinator_uid,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
@self.app.post("/api/ui/sonos/edit", response_model=FastUI, response_model_exclude_none=True)
|
|
251
|
+
async def update_sonos_selection(
|
|
252
|
+
form: Annotated[SonosSelectionForm, fastui_form(SonosSelectionForm)],
|
|
253
|
+
) -> list[AnyComponent]:
|
|
254
|
+
try:
|
|
255
|
+
result = SaveSonosSelection(
|
|
256
|
+
selected_group_repository=SettingsSelectedSonosGroupRepository(self.settings_service),
|
|
257
|
+
sonos_service=self.sonos_service,
|
|
258
|
+
).execute(form.uids, coordinator_uid=form.coordinator_uid)
|
|
259
|
+
except SonosDiscoveryError as err:
|
|
260
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
261
|
+
except SettingsError as err:
|
|
262
|
+
display_message = self._build_sonos_error_message(str(err), form.coordinator_uid)
|
|
263
|
+
if self._persisted_sonos_selection_matches(form.uids, form.coordinator_uid):
|
|
264
|
+
return self.sonos_pages.build_sonos_success_response(
|
|
265
|
+
"Sonos selection saved, but effective settings are still unavailable."
|
|
266
|
+
)
|
|
267
|
+
return self.sonos_pages.build_sonos_edit_error_response(
|
|
268
|
+
display_message,
|
|
269
|
+
form.uids,
|
|
270
|
+
form.coordinator_uid,
|
|
271
|
+
)
|
|
272
|
+
except ValueError as err:
|
|
273
|
+
return self.sonos_pages.build_sonos_edit_error_response(
|
|
274
|
+
self._build_sonos_error_message(str(err), form.coordinator_uid),
|
|
275
|
+
form.uids,
|
|
276
|
+
form.coordinator_uid,
|
|
277
|
+
)
|
|
278
|
+
except HTTPException:
|
|
279
|
+
raise
|
|
280
|
+
except Exception as err:
|
|
281
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
282
|
+
|
|
283
|
+
return self.sonos_pages.build_sonos_success_response(str(result.settings_message))
|
|
284
|
+
|
|
285
|
+
@self.app.post("/api/ui/sonos/reset", response_model=FastUI, response_model_exclude_none=True)
|
|
286
|
+
async def reset_sonos_selection() -> list[AnyComponent]:
|
|
287
|
+
return self._reset_sonos_selection()
|
|
288
|
+
|
|
225
289
|
@self.app.get("/{path:path}")
|
|
226
290
|
def html_landing(path: str) -> HTMLResponse:
|
|
227
291
|
del path
|
|
228
|
-
return HTMLResponse(prebuilt_html(title="
|
|
292
|
+
return HTMLResponse(prebuilt_html(title="Jukebox Admin", api_root_url="/api/ui"))
|
|
229
293
|
|
|
230
294
|
def _build_success_response(self, toast_event_name: str) -> list[AnyComponent]:
|
|
231
295
|
return [
|
|
@@ -238,6 +302,80 @@ class UIController(APIController):
|
|
|
238
302
|
def _reset_setting(self, setting_path: str) -> list[AnyComponent]:
|
|
239
303
|
return self.settings_pages.reset_setting(setting_path)
|
|
240
304
|
|
|
305
|
+
def _build_sonos_page_components(
|
|
306
|
+
self,
|
|
307
|
+
toast: Optional[str] = None,
|
|
308
|
+
toast_message: Optional[str] = None,
|
|
309
|
+
error_message: Optional[str] = None,
|
|
310
|
+
) -> List[AnyComponent]:
|
|
311
|
+
return self.sonos_pages.build_sonos_page_components(
|
|
312
|
+
toast=toast,
|
|
313
|
+
toast_message=toast_message,
|
|
314
|
+
error_message=error_message,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _build_sonos_edit_page_components(
|
|
318
|
+
self,
|
|
319
|
+
error_message: Optional[str] = None,
|
|
320
|
+
field_errors: Optional[dict[str, str]] = None,
|
|
321
|
+
submitted_uids: Optional[List[str]] = None,
|
|
322
|
+
submitted_coordinator_uid: Optional[str] = None,
|
|
323
|
+
) -> List[AnyComponent]:
|
|
324
|
+
return self.sonos_pages.build_sonos_edit_page_components(
|
|
325
|
+
error_message=error_message,
|
|
326
|
+
field_errors=field_errors,
|
|
327
|
+
submitted_uids=submitted_uids,
|
|
328
|
+
submitted_coordinator_uid=submitted_coordinator_uid,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _reset_sonos_selection(self) -> list[AnyComponent]:
|
|
332
|
+
selected_group_repository = SettingsSelectedSonosGroupRepository(self.settings_service)
|
|
333
|
+
try:
|
|
334
|
+
result = self.settings_service.reset_persisted_value("jukebox.player.sonos.selected_group")
|
|
335
|
+
except SettingsError as err:
|
|
336
|
+
if selected_group_repository.get_selected_group() is None:
|
|
337
|
+
return self.sonos_pages.build_sonos_success_response(
|
|
338
|
+
"Sonos selection cleared, but effective settings are still unavailable."
|
|
339
|
+
)
|
|
340
|
+
return self._build_sonos_page_components(error_message=str(err))
|
|
341
|
+
except HTTPException:
|
|
342
|
+
raise
|
|
343
|
+
except Exception as err:
|
|
344
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
345
|
+
|
|
346
|
+
return self.sonos_pages.build_sonos_success_response(str(result.get("message", "Settings saved.")))
|
|
347
|
+
|
|
348
|
+
def _persisted_sonos_selection_matches(self, uids: List[str], coordinator_uid: Optional[str]) -> bool:
|
|
349
|
+
try:
|
|
350
|
+
selected_group = SettingsSelectedSonosGroupRepository(self.settings_service).get_selected_group()
|
|
351
|
+
except Exception:
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
if selected_group is None:
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
expected_coordinator_uid = coordinator_uid or (uids[0] if uids else None)
|
|
358
|
+
return (
|
|
359
|
+
selected_group.coordinator_uid == expected_coordinator_uid
|
|
360
|
+
and [member.uid for member in selected_group.members] == uids
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _build_sonos_error_message(self, message: str, coordinator_uid: Optional[str]) -> str:
|
|
364
|
+
prefix = "Selected Sonos coordinator must be one of the selected speakers: "
|
|
365
|
+
if coordinator_uid is None or not message.startswith(prefix):
|
|
366
|
+
return message
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
speakers = self.sonos_service.list_available_speakers()
|
|
370
|
+
except Exception:
|
|
371
|
+
return message
|
|
372
|
+
|
|
373
|
+
speaker = next((speaker for speaker in speakers if speaker.uid == coordinator_uid), None)
|
|
374
|
+
if speaker is None:
|
|
375
|
+
return message
|
|
376
|
+
|
|
377
|
+
return f"{prefix}{speaker.name} [{speaker.uid}]"
|
|
378
|
+
|
|
241
379
|
def _build_index_page_components(self, toast: Optional[str] = None) -> List[AnyComponent]:
|
|
242
380
|
return self.library_pages.build_index_page_components(toast=toast)
|
|
243
381
|
|
|
@@ -350,6 +488,12 @@ class UIController(APIController):
|
|
|
350
488
|
def _serialize_current_tag_components(self, components: List[AnyComponent]) -> str:
|
|
351
489
|
return self.library_pages.serialize_current_tag_components(components)
|
|
352
490
|
|
|
491
|
+
@staticmethod
|
|
492
|
+
def _sonos_field_name_for_error(message: str) -> str:
|
|
493
|
+
if "coordinator" in message:
|
|
494
|
+
return "coordinator_uid"
|
|
495
|
+
return "uids"
|
|
496
|
+
|
|
353
497
|
def _field_validation_error(self, field_name: str, message: str) -> HTTPException:
|
|
354
498
|
return HTTPException(
|
|
355
499
|
status_code=422,
|
|
@@ -47,7 +47,7 @@ class LibraryUIPageBuilder:
|
|
|
47
47
|
]
|
|
48
48
|
|
|
49
49
|
components: list[AnyComponent] = [
|
|
50
|
-
c.Heading(text="
|
|
50
|
+
c.Heading(text="Jukebox Admin", level=1),
|
|
51
51
|
c.Paragraph(text=f"📀 {len(discs)} disc(s) in library"),
|
|
52
52
|
c.ServerLoad(
|
|
53
53
|
path="/current-tag-banner/events",
|
|
@@ -58,6 +58,11 @@ class LibraryUIPageBuilder:
|
|
|
58
58
|
class_name="d-flex flex-wrap gap-2",
|
|
59
59
|
components=[
|
|
60
60
|
c.Button(text="➕ Add a new disc", on_click=GoToEvent(url="/discs/new")),
|
|
61
|
+
c.Button(
|
|
62
|
+
text="🔊 Sonos Speakers",
|
|
63
|
+
on_click=GoToEvent(url="/sonos"),
|
|
64
|
+
class_name="btn btn-secondary",
|
|
65
|
+
),
|
|
61
66
|
c.Button(text="⚙️ Settings", on_click=GoToEvent(url="/settings"), class_name="btn btn-secondary"),
|
|
62
67
|
],
|
|
63
68
|
),
|
|
@@ -142,9 +142,13 @@ class SettingsUIPageBuilder:
|
|
|
142
142
|
|
|
143
143
|
action_components: list[AnyComponent] = [
|
|
144
144
|
c.Button(
|
|
145
|
-
text="Edit ✏️",
|
|
146
|
-
on_click=
|
|
147
|
-
|
|
145
|
+
text="Manage Speakers 🔊" if setting.path == "jukebox.player.sonos.selected_group" else "Edit ✏️",
|
|
146
|
+
on_click=(
|
|
147
|
+
GoToEvent(url="/sonos")
|
|
148
|
+
if setting.path == "jukebox.player.sonos.selected_group"
|
|
149
|
+
else GoToEvent(url=f"/settings/{setting.path}/edit")
|
|
150
|
+
),
|
|
151
|
+
class_name="btn btn-secondary text-nowrap",
|
|
148
152
|
)
|
|
149
153
|
]
|
|
150
154
|
row_class_name = "px-3 py-3"
|