gukebox 1.0.0.dev5__tar.gz → 1.0.0.dev6__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.dev5 → gukebox-1.0.0.dev6}/PKG-INFO +10 -9
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/README.md +4 -4
- gukebox-1.0.0.dev6/discstore/adapters/inbound/api/current_tag_router.py +25 -0
- gukebox-1.0.0.dev6/discstore/adapters/inbound/api/discs_router.py +48 -0
- gukebox-1.0.0.dev6/discstore/adapters/inbound/api/models.py +25 -0
- gukebox-1.0.0.dev6/discstore/adapters/inbound/api/settings_router.py +46 -0
- gukebox-1.0.0.dev6/discstore/adapters/inbound/api_controller.py +166 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/inbound/config.py +3 -3
- gukebox-1.0.0.dev5/jukebox/adapters/outbound/readers/nfc_reader_adapter.py → gukebox-1.0.0.dev6/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +4 -4
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/app.py +45 -3
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/cli_presentation.py +34 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/command_handlers.py +49 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/commands.py +14 -1
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/di_container.py +3 -3
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/definitions.py +5 -5
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/entities.py +8 -8
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/runtime_resolver.py +1 -1
- gukebox-1.0.0.dev6/jukebox/settings/selected_sonos_group_repository.py +60 -0
- gukebox-1.0.0.dev6/jukebox/shared/__init__.py +0 -0
- gukebox-1.0.0.dev6/jukebox/sonos/__init__.py +32 -0
- gukebox-1.0.0.dev6/jukebox/sonos/selection.py +144 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/pn532/pn532.py +2 -2
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/pn532/spi.py +1 -1
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/pyproject.toml +3 -2
- gukebox-1.0.0.dev5/discstore/adapters/inbound/api_controller.py +0 -155
- gukebox-1.0.0.dev5/jukebox/sonos/__init__.py +0 -16
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/LICENSE +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev5/discstore/adapters/outbound → gukebox-1.0.0.dev6/discstore/adapters/inbound/api}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/cli_display.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/config.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/ui_controller.py +0 -0
- {gukebox-1.0.0.dev5/discstore/domain → gukebox-1.0.0.dev6/discstore/adapters/outbound}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/app.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/command_handlers.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/commands.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/di_container.py +0 -0
- {gukebox-1.0.0.dev5/jukebox → gukebox-1.0.0.dev6/discstore/domain}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/add_disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/edit_disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/get_disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/list_discs.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/remove_disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/search_discs.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/adapters → gukebox-1.0.0.dev6/jukebox}/__init__.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/adapters/inbound → gukebox-1.0.0.dev6/jukebox/adapters}/__init__.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/adapters/outbound → gukebox-1.0.0.dev6/jukebox/adapters/inbound}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/adapters/outbound/players → gukebox-1.0.0.dev6/jukebox/adapters/outbound}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/adapters/outbound/readers → gukebox-1.0.0.dev6/jukebox/adapters/outbound/players}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/domain → gukebox-1.0.0.dev6/jukebox/adapters/outbound/readers}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/di_container.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/admin/services.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/app.py +0 -0
- {gukebox-1.0.0.dev5/jukebox/shared → gukebox-1.0.0.dev6/jukebox/domain}/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/current_tag_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/playback_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/playback_session.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/determine_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/handle_tag_event.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/errors.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/file_settings_repository.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/migration.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/resolve.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/service_protocols.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/types.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/shared/dependency_messages.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/shared/logger.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/shared/timing.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/sonos/discovery.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/jukebox/sonos/service.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev6}/pn532/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: gukebox
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev6
|
|
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
|
|
@@ -35,6 +35,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
35
35
|
Classifier: Programming Language :: Python :: 3.12
|
|
36
36
|
Classifier: Programming Language :: Python :: 3.13
|
|
37
37
|
Requires-Dist: pydantic==2.12.5
|
|
38
|
+
Requires-Dist: questionary==2.1.1
|
|
38
39
|
Requires-Dist: soco==0.30.14
|
|
39
40
|
Requires-Dist: typer==0.23.2 ; python_full_version < '3.10'
|
|
40
41
|
Requires-Dist: typer==0.24.1 ; python_full_version >= '3.10'
|
|
@@ -42,16 +43,16 @@ Requires-Dist: fastapi==0.128.7 ; python_full_version < '3.10' and extra == 'api
|
|
|
42
43
|
Requires-Dist: fastapi==0.135.1 ; python_full_version >= '3.10' and extra == 'api'
|
|
43
44
|
Requires-Dist: uvicorn==0.39.0 ; python_full_version < '3.10' and extra == 'api'
|
|
44
45
|
Requires-Dist: uvicorn==0.41.0 ; python_full_version >= '3.10' and extra == 'api'
|
|
45
|
-
Requires-Dist: pyserial==3.5 ; extra == '
|
|
46
|
-
Requires-Dist: spidev==3.8 ; extra == '
|
|
47
|
-
Requires-Dist: lgpio==0.2.2.0 ; python_full_version < '3.13' and extra == '
|
|
46
|
+
Requires-Dist: pyserial==3.5 ; extra == 'pn532'
|
|
47
|
+
Requires-Dist: spidev==3.8 ; extra == 'pn532'
|
|
48
|
+
Requires-Dist: lgpio==0.2.2.0 ; python_full_version < '3.13' and extra == 'pn532'
|
|
48
49
|
Requires-Dist: gukebox[api] ; extra == 'ui'
|
|
49
50
|
Requires-Dist: fastui==0.9.0 ; python_full_version >= '3.10' and extra == 'ui'
|
|
50
51
|
Requires-Dist: python-multipart==0.0.22 ; python_full_version >= '3.10' and extra == 'ui'
|
|
51
52
|
Requires-Python: >=3.9, <3.14
|
|
52
53
|
Project-URL: Repository, https://github.com/Gudsfile/jukebox
|
|
53
54
|
Provides-Extra: api
|
|
54
|
-
Provides-Extra:
|
|
55
|
+
Provides-Extra: pn532
|
|
55
56
|
Provides-Extra: ui
|
|
56
57
|
Description-Content-Type: text/markdown
|
|
57
58
|
|
|
@@ -106,7 +107,7 @@ Install the package from [PyPI](https://pypi.org/project/gukebox/).
|
|
|
106
107
|
> The package name is `gukebox` with `g` instead of a `j` (due to a name already taken).
|
|
107
108
|
|
|
108
109
|
> [!NOTE]
|
|
109
|
-
> The `
|
|
110
|
+
> The `pn532` extra is optional but required for NFC reading, [check compatibility](#readers).
|
|
110
111
|
|
|
111
112
|
### Recommended installation
|
|
112
113
|
|
|
@@ -131,7 +132,7 @@ source jukebox/bin/activate
|
|
|
131
132
|
|
|
132
133
|
3. Install `gukebox` into the virtual environment:
|
|
133
134
|
```shell
|
|
134
|
-
pip install "gukebox[
|
|
135
|
+
pip install "gukebox[pn532]"
|
|
135
136
|
```
|
|
136
137
|
|
|
137
138
|
> [!IMPORTANT]
|
|
@@ -218,7 +219,7 @@ Start the jukebox with the `jukebox` command (show help message with `--help`)
|
|
|
218
219
|
jukebox PLAYER_TO_USE READER_TO_USE
|
|
219
220
|
```
|
|
220
221
|
|
|
221
|
-
🎉 With choosing the `sonos` player and `
|
|
222
|
+
🎉 With choosing the `sonos` player and `pn532` reader, by approaching a NFC tag stored in the `library.json` file, you should hear the associated music begins.
|
|
222
223
|
|
|
223
224
|
Optional Parameters
|
|
224
225
|
|
|
@@ -241,7 +242,7 @@ Expected syntax: `tag_id` or `tag_id duration_seconds`.
|
|
|
241
242
|
- duration_seconds: a non-negative number of seconds used to simulate how long the tag remains in place. Fractional values are allowed.
|
|
242
243
|
Complete example: `your:tag:uid 2.5`
|
|
243
244
|
|
|
244
|
-
**NFC** (`
|
|
245
|
+
**NFC Pn532** (`pn532`)
|
|
245
246
|
Read an NFC tag and get its UID.
|
|
246
247
|
This project works with an NFC reader like the **PN532** and NFC tags like the **NTAG2xx**.
|
|
247
248
|
It is configured according to the [Waveshare PN532 wiki](https://www.waveshare.com/wiki/PN532_NFC_HAT).
|
|
@@ -49,7 +49,7 @@ Install the package from [PyPI](https://pypi.org/project/gukebox/).
|
|
|
49
49
|
> The package name is `gukebox` with `g` instead of a `j` (due to a name already taken).
|
|
50
50
|
|
|
51
51
|
> [!NOTE]
|
|
52
|
-
> The `
|
|
52
|
+
> The `pn532` extra is optional but required for NFC reading, [check compatibility](#readers).
|
|
53
53
|
|
|
54
54
|
### Recommended installation
|
|
55
55
|
|
|
@@ -74,7 +74,7 @@ source jukebox/bin/activate
|
|
|
74
74
|
|
|
75
75
|
3. Install `gukebox` into the virtual environment:
|
|
76
76
|
```shell
|
|
77
|
-
pip install "gukebox[
|
|
77
|
+
pip install "gukebox[pn532]"
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
> [!IMPORTANT]
|
|
@@ -161,7 +161,7 @@ Start the jukebox with the `jukebox` command (show help message with `--help`)
|
|
|
161
161
|
jukebox PLAYER_TO_USE READER_TO_USE
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
🎉 With choosing the `sonos` player and `
|
|
164
|
+
🎉 With choosing the `sonos` player and `pn532` reader, by approaching a NFC tag stored in the `library.json` file, you should hear the associated music begins.
|
|
165
165
|
|
|
166
166
|
Optional Parameters
|
|
167
167
|
|
|
@@ -184,7 +184,7 @@ Expected syntax: `tag_id` or `tag_id duration_seconds`.
|
|
|
184
184
|
- duration_seconds: a non-negative number of seconds used to simulate how long the tag remains in place. Fractional values are allowed.
|
|
185
185
|
Complete example: `your:tag:uid 2.5`
|
|
186
186
|
|
|
187
|
-
**NFC** (`
|
|
187
|
+
**NFC Pn532** (`pn532`)
|
|
188
188
|
Read an NFC tag and get its UID.
|
|
189
189
|
This project works with an NFC reader like the **PN532** and NFC tags like the **NTAG2xx**.
|
|
190
190
|
It is configured according to the [Waveshare PN532 wiki](https://www.waveshare.com/wiki/PN532_NFC_HAT).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Response
|
|
4
|
+
|
|
5
|
+
from discstore.adapters.inbound.api.models import CurrentTagStatusOutput
|
|
6
|
+
from discstore.domain.use_cases.get_current_tag_status import GetCurrentTagStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_current_tag_router(get_current_tag_status: GetCurrentTagStatus) -> APIRouter:
|
|
10
|
+
router = APIRouter(prefix="/api/v1", tags=["current-tag"])
|
|
11
|
+
|
|
12
|
+
@router.get(
|
|
13
|
+
"/current-tag",
|
|
14
|
+
response_model=CurrentTagStatusOutput,
|
|
15
|
+
responses={204: {"description": "No current tag"}},
|
|
16
|
+
summary="Get the current NFC tag status",
|
|
17
|
+
)
|
|
18
|
+
def get_current_tag() -> Any:
|
|
19
|
+
current_tag_status = get_current_tag_status.execute()
|
|
20
|
+
if current_tag_status is None:
|
|
21
|
+
return Response(status_code=204)
|
|
22
|
+
|
|
23
|
+
return CurrentTagStatusOutput(**current_tag_status.model_dump())
|
|
24
|
+
|
|
25
|
+
return router
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
|
|
5
|
+
from discstore.adapters.inbound.api.models import DiscInput, DiscOutput
|
|
6
|
+
from discstore.domain.entities import Disc
|
|
7
|
+
from discstore.domain.use_cases.add_disc import AddDisc
|
|
8
|
+
from discstore.domain.use_cases.edit_disc import EditDisc
|
|
9
|
+
from discstore.domain.use_cases.list_discs import ListDiscs
|
|
10
|
+
from discstore.domain.use_cases.remove_disc import RemoveDisc
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_discs_router(
|
|
14
|
+
add_disc: AddDisc,
|
|
15
|
+
list_discs: ListDiscs,
|
|
16
|
+
remove_disc: RemoveDisc,
|
|
17
|
+
edit_disc: EditDisc,
|
|
18
|
+
) -> APIRouter:
|
|
19
|
+
router = APIRouter(prefix="/api/v1", tags=["discs"])
|
|
20
|
+
|
|
21
|
+
@router.get("/discs", response_model=Dict[str, DiscOutput], summary="List discs")
|
|
22
|
+
def list_discs_route() -> Dict[str, Disc]:
|
|
23
|
+
return list_discs.execute()
|
|
24
|
+
|
|
25
|
+
@router.post("/disc", status_code=201, summary="Create or update a disc")
|
|
26
|
+
def add_or_edit_disc(tag_id: str, disc: DiscInput) -> Dict[str, str]:
|
|
27
|
+
try:
|
|
28
|
+
self_disc = Disc(**disc.model_dump())
|
|
29
|
+
add_disc.execute(tag_id, self_disc)
|
|
30
|
+
return {"message": "Disc added"}
|
|
31
|
+
except ValueError:
|
|
32
|
+
new_disc = Disc(**disc.model_dump())
|
|
33
|
+
edit_disc.execute(tag_id, new_disc.uri, new_disc.metadata, new_disc.option)
|
|
34
|
+
return {"message": "Disc edited"}
|
|
35
|
+
except Exception as err:
|
|
36
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
37
|
+
|
|
38
|
+
@router.delete("/disc", status_code=200, summary="Delete a disc")
|
|
39
|
+
def remove_disc_route(tag_id: str) -> Dict[str, str]:
|
|
40
|
+
try:
|
|
41
|
+
remove_disc.execute(tag_id)
|
|
42
|
+
return {"message": "Disc removed"}
|
|
43
|
+
except ValueError as value_err:
|
|
44
|
+
raise HTTPException(status_code=404, detail=str(value_err))
|
|
45
|
+
except Exception as err:
|
|
46
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
47
|
+
|
|
48
|
+
return router
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, RootModel
|
|
4
|
+
|
|
5
|
+
from discstore.domain.entities import CurrentTagStatus, Disc
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DiscInput(Disc):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiscOutput(Disc):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CurrentTagStatusOutput(CurrentTagStatus):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SettingsResetInput(BaseModel):
|
|
21
|
+
path: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SettingsPatchInput(RootModel[Dict[str, Any]]):
|
|
25
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any, Dict, cast
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
|
|
5
|
+
from discstore.adapters.inbound.api.models import SettingsPatchInput, SettingsResetInput
|
|
6
|
+
from jukebox.settings.errors import SettingsError
|
|
7
|
+
from jukebox.settings.service_protocols import SettingsService
|
|
8
|
+
from jukebox.settings.types import JsonObject
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_settings_router(settings_service: SettingsService) -> APIRouter:
|
|
12
|
+
router = APIRouter(prefix="/api/v1", tags=["settings"])
|
|
13
|
+
|
|
14
|
+
@router.get("/settings", response_model=Dict[str, Any], summary="Get persisted settings")
|
|
15
|
+
def get_settings() -> JsonObject:
|
|
16
|
+
try:
|
|
17
|
+
return settings_service.get_persisted_settings_view()
|
|
18
|
+
except Exception as err:
|
|
19
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
20
|
+
|
|
21
|
+
@router.get("/settings/effective", response_model=Dict[str, Any], summary="Get effective settings")
|
|
22
|
+
def get_effective_settings() -> JsonObject:
|
|
23
|
+
try:
|
|
24
|
+
return settings_service.get_effective_settings_view()
|
|
25
|
+
except Exception as err:
|
|
26
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
27
|
+
|
|
28
|
+
@router.patch("/settings", response_model=Dict[str, Any], summary="Patch persisted settings")
|
|
29
|
+
def patch_settings(patch: SettingsPatchInput) -> JsonObject:
|
|
30
|
+
try:
|
|
31
|
+
return settings_service.patch_persisted_settings(cast(JsonObject, patch.root))
|
|
32
|
+
except SettingsError as err:
|
|
33
|
+
raise HTTPException(status_code=400, detail=str(err))
|
|
34
|
+
except Exception as err:
|
|
35
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
36
|
+
|
|
37
|
+
@router.post("/settings/reset", response_model=Dict[str, Any], summary="Reset a persisted setting")
|
|
38
|
+
def reset_settings(payload: SettingsResetInput) -> JsonObject:
|
|
39
|
+
try:
|
|
40
|
+
return settings_service.reset_persisted_value(payload.path)
|
|
41
|
+
except SettingsError as err:
|
|
42
|
+
raise HTTPException(status_code=400, detail=str(err))
|
|
43
|
+
except Exception as err:
|
|
44
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
45
|
+
|
|
46
|
+
return router
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from jukebox.shared.dependency_messages import optional_extra_dependency_message
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from fastapi import FastAPI, HTTPException
|
|
9
|
+
|
|
10
|
+
from discstore.adapters.inbound.api.current_tag_router import build_current_tag_router
|
|
11
|
+
from discstore.adapters.inbound.api.discs_router import build_discs_router
|
|
12
|
+
from discstore.adapters.inbound.api.models import (
|
|
13
|
+
CurrentTagStatusOutput,
|
|
14
|
+
DiscInput,
|
|
15
|
+
DiscOutput,
|
|
16
|
+
SettingsPatchInput,
|
|
17
|
+
SettingsResetInput,
|
|
18
|
+
)
|
|
19
|
+
from discstore.adapters.inbound.api.settings_router import build_settings_router
|
|
20
|
+
except ModuleNotFoundError as e:
|
|
21
|
+
if e.name != "fastapi":
|
|
22
|
+
raise
|
|
23
|
+
raise ModuleNotFoundError(
|
|
24
|
+
optional_extra_dependency_message("The `api_controller` module", "api", "discstore api")
|
|
25
|
+
) from e
|
|
26
|
+
from discstore.domain.use_cases.add_disc import AddDisc
|
|
27
|
+
from discstore.domain.use_cases.edit_disc import EditDisc
|
|
28
|
+
from discstore.domain.use_cases.get_current_tag_status import GetCurrentTagStatus
|
|
29
|
+
from discstore.domain.use_cases.list_discs import ListDiscs
|
|
30
|
+
from discstore.domain.use_cases.remove_disc import RemoveDisc
|
|
31
|
+
from jukebox.settings.entities import SelectedSonosGroupSettings
|
|
32
|
+
from jukebox.settings.selected_sonos_group_repository import SettingsSelectedSonosGroupRepository
|
|
33
|
+
from jukebox.settings.service_protocols import SettingsService
|
|
34
|
+
from jukebox.sonos.discovery import DiscoveredSonosSpeaker, SonosDiscoveryError
|
|
35
|
+
from jukebox.sonos.selection import GetSonosSelectionStatus, PlanSonosSelection, SaveSonosSelection
|
|
36
|
+
from jukebox.sonos.service import SonosService
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"APIController",
|
|
40
|
+
"CurrentTagStatusOutput",
|
|
41
|
+
"DiscInput",
|
|
42
|
+
"DiscOutput",
|
|
43
|
+
"SettingsPatchInput",
|
|
44
|
+
"SettingsResetInput",
|
|
45
|
+
"SonosSelectionInput",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SonosSpeakerOutput(DiscoveredSonosSpeaker):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SelectedSonosGroupOutput(SelectedSonosGroupSettings):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SonosSelectionAvailabilityOutput(BaseModel):
|
|
58
|
+
status: str
|
|
59
|
+
speaker: Optional[SonosSpeakerOutput] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SonosSelectionOutput(BaseModel):
|
|
63
|
+
selected_group: Optional[SelectedSonosGroupOutput] = None
|
|
64
|
+
availability: SonosSelectionAvailabilityOutput
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SonosSelectionInput(BaseModel):
|
|
68
|
+
uids: list[str]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SonosSelectionUpdateOutput(BaseModel):
|
|
72
|
+
selected_group: SelectedSonosGroupOutput
|
|
73
|
+
availability: SonosSelectionAvailabilityOutput
|
|
74
|
+
message: str
|
|
75
|
+
restart_required: bool
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class APIController:
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
add_disc: AddDisc,
|
|
82
|
+
list_discs: ListDiscs,
|
|
83
|
+
remove_disc: RemoveDisc,
|
|
84
|
+
edit_disc: EditDisc,
|
|
85
|
+
get_current_tag_status: GetCurrentTagStatus,
|
|
86
|
+
settings_service: SettingsService,
|
|
87
|
+
sonos_service: SonosService,
|
|
88
|
+
):
|
|
89
|
+
self.add_disc = add_disc
|
|
90
|
+
self.list_discs = list_discs
|
|
91
|
+
self.remove_disc = remove_disc
|
|
92
|
+
self.edit_disc = edit_disc
|
|
93
|
+
self.get_current_tag_status = get_current_tag_status
|
|
94
|
+
self.settings_service = settings_service
|
|
95
|
+
self.sonos_service = sonos_service
|
|
96
|
+
self.app = FastAPI(
|
|
97
|
+
title="DiscStore API",
|
|
98
|
+
description="API for managing Jukebox disc library",
|
|
99
|
+
docs_url="/docs",
|
|
100
|
+
redoc_url="/redoc",
|
|
101
|
+
)
|
|
102
|
+
self.register_routes()
|
|
103
|
+
|
|
104
|
+
def register_routes(self):
|
|
105
|
+
self.app.include_router(
|
|
106
|
+
build_discs_router(
|
|
107
|
+
add_disc=self.add_disc,
|
|
108
|
+
list_discs=self.list_discs,
|
|
109
|
+
remove_disc=self.remove_disc,
|
|
110
|
+
edit_disc=self.edit_disc,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
self.app.include_router(build_current_tag_router(self.get_current_tag_status))
|
|
114
|
+
self.app.include_router(build_settings_router(self.settings_service))
|
|
115
|
+
|
|
116
|
+
@self.app.get("/api/v1/sonos/speakers", response_model=list[SonosSpeakerOutput])
|
|
117
|
+
def get_sonos_speakers():
|
|
118
|
+
try:
|
|
119
|
+
return self.sonos_service.list_available_speakers()
|
|
120
|
+
except SonosDiscoveryError as err:
|
|
121
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
122
|
+
except Exception as err:
|
|
123
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
124
|
+
|
|
125
|
+
@self.app.get("/api/v1/sonos/selection", response_model=SonosSelectionOutput)
|
|
126
|
+
def get_sonos_selection():
|
|
127
|
+
try:
|
|
128
|
+
return GetSonosSelectionStatus(
|
|
129
|
+
SettingsSelectedSonosGroupRepository(self.settings_service),
|
|
130
|
+
self.sonos_service,
|
|
131
|
+
).execute()
|
|
132
|
+
except SonosDiscoveryError as err:
|
|
133
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
134
|
+
except Exception as err:
|
|
135
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
136
|
+
|
|
137
|
+
@self.app.put("/api/v1/sonos/selection", response_model=SonosSelectionUpdateOutput)
|
|
138
|
+
def put_sonos_selection(payload: SonosSelectionInput):
|
|
139
|
+
try:
|
|
140
|
+
plan = PlanSonosSelection(self.sonos_service).execute(requested_uids=payload.uids)
|
|
141
|
+
if plan.status in {"invalid_request", "none_available"}:
|
|
142
|
+
raise HTTPException(status_code=400, detail=str(plan.error_message))
|
|
143
|
+
if plan.status == "needs_choice" or plan.selected_uid is None:
|
|
144
|
+
raise HTTPException(status_code=400, detail="No Sonos speaker selection was made.")
|
|
145
|
+
|
|
146
|
+
result = SaveSonosSelection(
|
|
147
|
+
SettingsSelectedSonosGroupRepository(self.settings_service),
|
|
148
|
+
self.sonos_service,
|
|
149
|
+
).execute(plan.selected_uid)
|
|
150
|
+
return SonosSelectionUpdateOutput(
|
|
151
|
+
selected_group=SelectedSonosGroupOutput(**result.selected_group.model_dump()),
|
|
152
|
+
availability=SonosSelectionAvailabilityOutput(
|
|
153
|
+
status="available",
|
|
154
|
+
speaker=SonosSpeakerOutput(**result.speaker.model_dump()),
|
|
155
|
+
),
|
|
156
|
+
message=result.settings_message,
|
|
157
|
+
restart_required=result.restart_required,
|
|
158
|
+
)
|
|
159
|
+
except SonosDiscoveryError as err:
|
|
160
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
161
|
+
except ValueError as err:
|
|
162
|
+
raise HTTPException(status_code=400, detail=str(err))
|
|
163
|
+
except HTTPException:
|
|
164
|
+
raise
|
|
165
|
+
except Exception as err:
|
|
166
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
@@ -16,7 +16,7 @@ class JukeboxCliConfig(BaseModel):
|
|
|
16
16
|
library: Optional[str] = None
|
|
17
17
|
verbose: bool = False
|
|
18
18
|
player: Optional[Literal["dryrun", "sonos"]] = None
|
|
19
|
-
reader: Optional[Literal["dryrun", "
|
|
19
|
+
reader: Optional[Literal["dryrun", "pn532"]] = None
|
|
20
20
|
sonos_host: Optional[str] = None
|
|
21
21
|
sonos_name: Optional[str] = None
|
|
22
22
|
pause_duration_seconds: Optional[int] = None
|
|
@@ -40,7 +40,7 @@ def parse_config() -> JukeboxCliConfig:
|
|
|
40
40
|
add_version_arg(parser)
|
|
41
41
|
|
|
42
42
|
parser.add_argument("positional_player", nargs="?", choices=["dryrun", "sonos"], help="override the player type")
|
|
43
|
-
parser.add_argument("positional_reader", nargs="?", choices=["dryrun", "
|
|
43
|
+
parser.add_argument("positional_reader", nargs="?", choices=["dryrun", "pn532"], help="override the reader type")
|
|
44
44
|
|
|
45
45
|
parser.add_argument(
|
|
46
46
|
"--player",
|
|
@@ -50,7 +50,7 @@ def parse_config() -> JukeboxCliConfig:
|
|
|
50
50
|
)
|
|
51
51
|
parser.add_argument(
|
|
52
52
|
"--reader",
|
|
53
|
-
choices=["dryrun", "
|
|
53
|
+
choices=["dryrun", "pn532"],
|
|
54
54
|
default=None,
|
|
55
55
|
help="override the reader type without providing both positional type arguments",
|
|
56
56
|
)
|
|
@@ -9,7 +9,7 @@ from jukebox.shared.dependency_messages import optional_extra_dependency_message
|
|
|
9
9
|
try:
|
|
10
10
|
from pn532 import PN532_SPI
|
|
11
11
|
except ModuleNotFoundError as err:
|
|
12
|
-
raise ModuleNotFoundError(optional_extra_dependency_message("The `
|
|
12
|
+
raise ModuleNotFoundError(optional_extra_dependency_message("The `pn532` reader", "pn532", "jukebox ...")) from err
|
|
13
13
|
|
|
14
14
|
from jukebox.domain.ports import ReaderPort
|
|
15
15
|
from jukebox.shared.timing import DEFAULT_NFC_READ_TIMEOUT_SECONDS
|
|
@@ -25,13 +25,13 @@ def spi_active():
|
|
|
25
25
|
return any(dev.startswith("spidev") for dev in os.listdir("/dev"))
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class
|
|
29
|
-
"""Adapter for NFC reader implementing ReaderPort."""
|
|
28
|
+
class Pn532ReaderAdapter(ReaderPort):
|
|
29
|
+
"""Adapter for Pn532 NFC reader implementing ReaderPort."""
|
|
30
30
|
|
|
31
31
|
def __init__(self, read_timeout_seconds: float = DEFAULT_NFC_READ_TIMEOUT_SECONDS):
|
|
32
32
|
if not spi_active():
|
|
33
33
|
error_message = (
|
|
34
|
-
"The SPI interface is not enabled. Please enable it to use the NFC reader."
|
|
34
|
+
"The SPI interface is not enabled. Please enable it to use the PN532 NFC reader."
|
|
35
35
|
"You can enable SPI using `sudo raspi-config` then navigate to: Interface Options > SPI > Enable > Yes."
|
|
36
36
|
)
|
|
37
37
|
LOGGER.error(error_message)
|
|
@@ -20,8 +20,9 @@ from discstore.di_container import build_cli_controller, build_interactive_cli_c
|
|
|
20
20
|
from jukebox.settings.errors import SettingsError
|
|
21
21
|
from jukebox.shared.config_utils import get_package_version
|
|
22
22
|
from jukebox.shared.logger import set_logger
|
|
23
|
+
from jukebox.sonos.discovery import DiscoveredSonosSpeaker
|
|
23
24
|
|
|
24
|
-
from .cli_presentation import render_cli_error
|
|
25
|
+
from .cli_presentation import build_sonos_speaker_choice_label, render_cli_error
|
|
25
26
|
from .command_handlers import execute_server_command, execute_settings_command, execute_sonos_command
|
|
26
27
|
from .commands import (
|
|
27
28
|
ApiCommand,
|
|
@@ -29,6 +30,8 @@ from .commands import (
|
|
|
29
30
|
SettingsSetCommand,
|
|
30
31
|
SettingsShowCommand,
|
|
31
32
|
SonosListCommand,
|
|
33
|
+
SonosSelectCommand,
|
|
34
|
+
SonosShowCommand,
|
|
32
35
|
UiCommand,
|
|
33
36
|
is_settings_command,
|
|
34
37
|
is_sonos_command,
|
|
@@ -74,7 +77,12 @@ def _run_command(ctx: typer.Context, command: object) -> None:
|
|
|
74
77
|
source_command="jukebox-admin",
|
|
75
78
|
)
|
|
76
79
|
elif is_sonos_command(command):
|
|
77
|
-
execute_sonos_command(
|
|
80
|
+
execute_sonos_command(
|
|
81
|
+
command=command,
|
|
82
|
+
sonos_service=services.sonos,
|
|
83
|
+
settings_service=services.settings,
|
|
84
|
+
speaker_prompt_fn=_prompt_for_sonos_speaker_selection,
|
|
85
|
+
)
|
|
78
86
|
else:
|
|
79
87
|
execute_server_command(
|
|
80
88
|
verbose=state.verbose,
|
|
@@ -146,10 +154,28 @@ def _exit_on_command_validation_error(err: ValidationError) -> None:
|
|
|
146
154
|
raise SystemExit(str(err)) from err
|
|
147
155
|
|
|
148
156
|
|
|
157
|
+
def _prompt_for_sonos_speaker_selection(speakers: list[DiscoveredSonosSpeaker]) -> Optional[str]:
|
|
158
|
+
import questionary
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
return questionary.select(
|
|
162
|
+
"Select a Sonos speaker",
|
|
163
|
+
choices=[
|
|
164
|
+
questionary.Choice(
|
|
165
|
+
title=build_sonos_speaker_choice_label(speaker),
|
|
166
|
+
value=speaker.uid,
|
|
167
|
+
)
|
|
168
|
+
for speaker in speakers
|
|
169
|
+
],
|
|
170
|
+
).ask()
|
|
171
|
+
except KeyboardInterrupt:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
149
175
|
app = typer.Typer(help="Admin CLI for jukebox")
|
|
150
176
|
settings_app = typer.Typer(help="Inspect and manage application settings")
|
|
151
177
|
library_app = typer.Typer(help="Manage the library")
|
|
152
|
-
sonos_app = typer.Typer(help="Inspect Sonos speakers
|
|
178
|
+
sonos_app = typer.Typer(help="Inspect Sonos speakers and manage the saved Sonos selection")
|
|
153
179
|
app.add_typer(settings_app, name="settings")
|
|
154
180
|
app.add_typer(library_app, name="library")
|
|
155
181
|
app.add_typer(sonos_app, name="sonos")
|
|
@@ -263,6 +289,22 @@ def sonos_list(ctx: typer.Context) -> None:
|
|
|
263
289
|
_run_command(ctx, SonosListCommand(type="sonos_list"))
|
|
264
290
|
|
|
265
291
|
|
|
292
|
+
@sonos_app.command("select")
|
|
293
|
+
def sonos_select(
|
|
294
|
+
ctx: typer.Context,
|
|
295
|
+
uids: Annotated[
|
|
296
|
+
Optional[list[str]],
|
|
297
|
+
typer.Option("--uids", help="discover and persist exactly one Sonos speaker UID"),
|
|
298
|
+
] = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
_run_command(ctx, SonosSelectCommand(type="sonos_select", uids=uids))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@sonos_app.command("show")
|
|
304
|
+
def sonos_show(ctx: typer.Context) -> None:
|
|
305
|
+
_run_command(ctx, SonosShowCommand(type="sonos_show"))
|
|
306
|
+
|
|
307
|
+
|
|
266
308
|
@library_app.command("add")
|
|
267
309
|
def library_add(
|
|
268
310
|
ctx: typer.Context,
|
|
@@ -13,6 +13,7 @@ from jukebox.settings.errors import (
|
|
|
13
13
|
from jukebox.settings.types import JsonObject, JsonValue
|
|
14
14
|
from jukebox.settings.view_utils import MISSING, lookup_object, lookup_optional_dotted_path, lookup_provenance_label
|
|
15
15
|
from jukebox.sonos.discovery import DiscoveredSonosSpeaker
|
|
16
|
+
from jukebox.sonos.selection import SonosSelectionResult, SonosSelectionStatus
|
|
16
17
|
|
|
17
18
|
from .commands import SettingsResetCommand, SettingsSetCommand, SettingsShowCommand
|
|
18
19
|
|
|
@@ -73,6 +74,39 @@ def render_sonos_speakers_output(speakers: list[DiscoveredSonosSpeaker]) -> str:
|
|
|
73
74
|
)
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
def build_sonos_speaker_choice_label(speaker: DiscoveredSonosSpeaker) -> str:
|
|
78
|
+
return "{} ({})".format(speaker.name, speaker.host)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_sonos_selection_saved_output(result: SonosSelectionResult) -> str:
|
|
82
|
+
return "\n".join(
|
|
83
|
+
[
|
|
84
|
+
"Selected Sonos speaker: {}".format(result.speaker.name),
|
|
85
|
+
"UID: {}".format(result.speaker.uid),
|
|
86
|
+
result.settings_message,
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def render_sonos_selection_status_output(status: SonosSelectionStatus) -> str:
|
|
92
|
+
lines = ["Selected Sonos Speaker", ""]
|
|
93
|
+
|
|
94
|
+
if status.selected_group is None:
|
|
95
|
+
lines.append("- Status: not selected")
|
|
96
|
+
return "\n".join(lines)
|
|
97
|
+
|
|
98
|
+
lines.append("- UID: {}".format(status.selected_group.coordinator_uid))
|
|
99
|
+
lines.append("- Status: {}".format(status.availability.status))
|
|
100
|
+
|
|
101
|
+
speaker = status.availability.speaker
|
|
102
|
+
if speaker is not None:
|
|
103
|
+
lines.append("- Name: {}".format(speaker.name))
|
|
104
|
+
lines.append("- Host: {}".format(speaker.host))
|
|
105
|
+
lines.append("- Household: {}".format(speaker.household_id))
|
|
106
|
+
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
76
110
|
def _render_persisted_settings(payload: JsonObject) -> str:
|
|
77
111
|
lines = ["Persisted Settings", "Schema Version: {}".format(payload.get("schema_version", "unknown"))]
|
|
78
112
|
entries = list(_collect_persisted_entries(payload))
|