gukebox 1.0.0.dev4__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.dev4 → gukebox-1.0.0.dev6}/PKG-INFO +10 -9
- {gukebox-1.0.0.dev4 → 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.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/ui_controller.py +11 -1
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/app.py +25 -12
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/inbound/config.py +3 -3
- gukebox-1.0.0.dev4/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.dev6/jukebox/adapters/outbound/sonos_discovery_adapter.py +199 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/admin/app.py +81 -14
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/admin/cli_presentation.py +60 -5
- gukebox-1.0.0.dev6/jukebox/admin/command_handlers.py +197 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/admin/commands.py +23 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/admin/di_container.py +27 -4
- gukebox-1.0.0.dev6/jukebox/admin/services.py +10 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/app.py +9 -1
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/di_container.py +3 -3
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/__init__.py +0 -3
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/definitions.py +5 -5
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/entities.py +8 -8
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/resolve.py +8 -61
- gukebox-1.0.0.dev6/jukebox/settings/runtime_resolver.py +59 -0
- gukebox-1.0.0.dev6/jukebox/settings/selected_sonos_group_repository.py +60 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/service_protocols.py +8 -2
- 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/discovery.py +25 -0
- gukebox-1.0.0.dev6/jukebox/sonos/selection.py +144 -0
- gukebox-1.0.0.dev6/jukebox/sonos/service.py +73 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/pn532/pn532.py +2 -2
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/pn532/spi.py +1 -1
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/pyproject.toml +3 -2
- gukebox-1.0.0.dev4/discstore/adapters/inbound/api_controller.py +0 -138
- gukebox-1.0.0.dev4/jukebox/admin/command_handlers.py +0 -115
- gukebox-1.0.0.dev4/jukebox/settings/sonos_runtime.py +0 -175
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/LICENSE +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev4/discstore/adapters/outbound → gukebox-1.0.0.dev6/discstore/adapters/inbound/api}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/cli_display.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/config.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
- {gukebox-1.0.0.dev4/discstore/domain → gukebox-1.0.0.dev6/discstore/adapters/outbound}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/command_handlers.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/commands.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/di_container.py +0 -0
- {gukebox-1.0.0.dev4/jukebox → gukebox-1.0.0.dev6/discstore/domain}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/add_disc.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/edit_disc.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/get_disc.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/list_discs.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/remove_disc.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/discstore/domain/use_cases/search_discs.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/adapters → gukebox-1.0.0.dev6/jukebox}/__init__.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/adapters/inbound → gukebox-1.0.0.dev6/jukebox/adapters}/__init__.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/adapters/outbound → gukebox-1.0.0.dev6/jukebox/adapters/inbound}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/adapters/outbound/players → gukebox-1.0.0.dev6/jukebox/adapters/outbound}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/adapters/outbound/readers → gukebox-1.0.0.dev6/jukebox/adapters/outbound/players}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/domain → gukebox-1.0.0.dev6/jukebox/adapters/outbound/readers}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.0.0.dev4/jukebox/shared → gukebox-1.0.0.dev6/jukebox/domain}/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/current_tag_action.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/playback_action.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/playback_session.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/determine_action.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/domain/use_cases/handle_tag_event.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/errors.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/file_settings_repository.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/migration.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/types.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/shared/dependency_messages.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/shared/logger.py +0 -0
- {gukebox-1.0.0.dev4 → gukebox-1.0.0.dev6}/jukebox/shared/timing.py +0 -0
- {gukebox-1.0.0.dev4 → 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)}")
|
|
@@ -42,6 +42,7 @@ from jukebox.settings.definitions import (
|
|
|
42
42
|
from jukebox.settings.errors import SettingsError
|
|
43
43
|
from jukebox.settings.service_protocols import SettingsService
|
|
44
44
|
from jukebox.settings.types import JsonObject
|
|
45
|
+
from jukebox.sonos.service import SonosService
|
|
45
46
|
|
|
46
47
|
_MISSING = object()
|
|
47
48
|
|
|
@@ -74,9 +75,18 @@ class UIController(APIController):
|
|
|
74
75
|
get_disc: GetDisc,
|
|
75
76
|
get_current_tag_status: GetCurrentTagStatus,
|
|
76
77
|
settings_service: SettingsService,
|
|
78
|
+
sonos_service: SonosService,
|
|
77
79
|
):
|
|
78
80
|
self.get_disc = get_disc
|
|
79
|
-
super().__init__(
|
|
81
|
+
super().__init__(
|
|
82
|
+
add_disc,
|
|
83
|
+
list_discs,
|
|
84
|
+
remove_disc,
|
|
85
|
+
edit_disc,
|
|
86
|
+
get_current_tag_status,
|
|
87
|
+
settings_service,
|
|
88
|
+
sonos_service,
|
|
89
|
+
)
|
|
80
90
|
|
|
81
91
|
def register_routes(self):
|
|
82
92
|
super().register_routes()
|
|
@@ -10,10 +10,11 @@ from discstore.command_handlers import execute_library_command
|
|
|
10
10
|
from discstore.commands import is_library_command
|
|
11
11
|
from discstore.di_container import build_cli_controller, build_interactive_cli_controller
|
|
12
12
|
from jukebox.admin.cli_presentation import render_cli_error
|
|
13
|
-
from jukebox.admin.command_handlers import
|
|
14
|
-
from jukebox.admin.commands import is_admin_command
|
|
13
|
+
from jukebox.admin.command_handlers import execute_server_command, execute_settings_command
|
|
14
|
+
from jukebox.admin.commands import is_admin_command, is_settings_command
|
|
15
15
|
from jukebox.admin.di_container import (
|
|
16
16
|
build_admin_api_app,
|
|
17
|
+
build_admin_services,
|
|
17
18
|
build_admin_ui_app,
|
|
18
19
|
)
|
|
19
20
|
from jukebox.admin.di_container import (
|
|
@@ -37,24 +38,36 @@ def main():
|
|
|
37
38
|
config = parse_config()
|
|
38
39
|
set_logger("discstore", config.verbose)
|
|
39
40
|
try:
|
|
40
|
-
settings_service = _build_settings_service(config)
|
|
41
41
|
if is_admin_command(config.command):
|
|
42
|
+
services = build_admin_services(
|
|
43
|
+
library=config.library,
|
|
44
|
+
command=config.command,
|
|
45
|
+
logger_warning=LOGGER.warning,
|
|
46
|
+
)
|
|
42
47
|
try:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
if is_settings_command(config.command):
|
|
49
|
+
execute_settings_command(
|
|
50
|
+
command=config.command,
|
|
51
|
+
settings_service=services.settings,
|
|
52
|
+
source_command="discstore",
|
|
53
|
+
library=config.library,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
execute_server_command(
|
|
57
|
+
verbose=config.verbose,
|
|
58
|
+
command=config.command,
|
|
59
|
+
services=services,
|
|
60
|
+
build_api_app=build_admin_api_app,
|
|
61
|
+
build_ui_app=build_admin_ui_app,
|
|
62
|
+
source_command="discstore",
|
|
63
|
+
)
|
|
52
64
|
except RuntimeError as err:
|
|
53
65
|
print(str(err), file=sys.stderr)
|
|
54
66
|
raise SystemExit(1) from err
|
|
55
67
|
return
|
|
56
68
|
|
|
57
69
|
if is_library_command(config.command):
|
|
70
|
+
settings_service = _build_settings_service(config)
|
|
58
71
|
try:
|
|
59
72
|
execute_library_command(
|
|
60
73
|
verbose=config.verbose,
|
|
@@ -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)
|