gukebox 1.0.0.dev5__tar.gz → 1.0.0.dev7__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.dev7}/PKG-INFO +10 -9
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/README.md +4 -4
- gukebox-1.0.0.dev7/discstore/adapters/inbound/api/__init__.py +25 -0
- gukebox-1.0.0.dev7/discstore/adapters/inbound/api/current_tag_router.py +161 -0
- gukebox-1.0.0.dev7/discstore/adapters/inbound/api/discs_router.py +73 -0
- gukebox-1.0.0.dev7/discstore/adapters/inbound/api/models.py +48 -0
- gukebox-1.0.0.dev7/discstore/adapters/inbound/api/settings_router.py +46 -0
- gukebox-1.0.0.dev7/discstore/adapters/inbound/api_controller.py +190 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/inbound/ui_controller.py +1 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/add_disc.py +2 -1
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/edit_disc.py +2 -1
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/inbound/config.py +3 -3
- gukebox-1.0.0.dev5/jukebox/adapters/outbound/readers/nfc_reader_adapter.py → gukebox-1.0.0.dev7/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +4 -4
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/sonos_discovery_adapter.py +1 -4
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/app.py +82 -3
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/cli_presentation.py +106 -50
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/command_handlers.py +61 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/commands.py +22 -2
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/di_container.py +1 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/di_container.py +3 -3
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/definitions.py +5 -5
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/entities.py +12 -9
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/runtime_resolver.py +1 -1
- gukebox-1.0.0.dev7/jukebox/settings/selected_sonos_group_repository.py +60 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/sonos/__init__.py +12 -0
- gukebox-1.0.0.dev7/jukebox/sonos/selection.py +168 -0
- gukebox-1.0.0.dev7/jukebox/sonos/service.py +125 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/pn532/pn532.py +2 -2
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/pn532/spi.py +1 -1
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/pyproject.toml +4 -2
- gukebox-1.0.0.dev5/discstore/adapters/inbound/api_controller.py +0 -155
- gukebox-1.0.0.dev5/jukebox/sonos/service.py +0 -73
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/LICENSE +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/inbound/cli_display.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/inbound/config.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/app.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/command_handlers.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/commands.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/di_container.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/get_disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/list_discs.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/remove_disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/discstore/domain/use_cases/search_discs.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/players/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/readers/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/admin/services.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/app.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/current_tag_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/playback_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/playback_session.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/use_cases/determine_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/domain/use_cases/handle_tag_event.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/errors.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/file_settings_repository.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/migration.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/resolve.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/service_protocols.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/types.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/shared/__init__.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/shared/dependency_messages.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/shared/logger.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/shared/timing.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/jukebox/sonos/discovery.py +0 -0
- {gukebox-1.0.0.dev5 → gukebox-1.0.0.dev7}/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.dev7
|
|
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 discstore.adapters.inbound.api.current_tag_router import build_current_tag_router
|
|
2
|
+
from discstore.adapters.inbound.api.discs_router import build_discs_router
|
|
3
|
+
from discstore.adapters.inbound.api.models import (
|
|
4
|
+
CurrentTagDiscOutput,
|
|
5
|
+
CurrentTagStatusOutput,
|
|
6
|
+
DiscInput,
|
|
7
|
+
DiscOutput,
|
|
8
|
+
DiscPatchInput,
|
|
9
|
+
SettingsPatchInput,
|
|
10
|
+
SettingsResetInput,
|
|
11
|
+
)
|
|
12
|
+
from discstore.adapters.inbound.api.settings_router import build_settings_router
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CurrentTagDiscOutput",
|
|
16
|
+
"CurrentTagStatusOutput",
|
|
17
|
+
"DiscInput",
|
|
18
|
+
"DiscOutput",
|
|
19
|
+
"DiscPatchInput",
|
|
20
|
+
"SettingsPatchInput",
|
|
21
|
+
"SettingsResetInput",
|
|
22
|
+
"build_current_tag_router",
|
|
23
|
+
"build_discs_router",
|
|
24
|
+
"build_settings_router",
|
|
25
|
+
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Response, status
|
|
4
|
+
|
|
5
|
+
from discstore.adapters.inbound.api.models import (
|
|
6
|
+
CurrentTagDiscOutput,
|
|
7
|
+
CurrentTagStatusOutput,
|
|
8
|
+
DiscInput,
|
|
9
|
+
DiscOutput,
|
|
10
|
+
DiscPatchInput,
|
|
11
|
+
)
|
|
12
|
+
from discstore.domain.entities import CurrentTagStatus, Disc, DiscMetadata, DiscOption
|
|
13
|
+
from discstore.domain.use_cases.add_disc import AddDisc
|
|
14
|
+
from discstore.domain.use_cases.edit_disc import EditDisc
|
|
15
|
+
from discstore.domain.use_cases.get_current_tag_status import GetCurrentTagStatus
|
|
16
|
+
from discstore.domain.use_cases.get_disc import GetDisc
|
|
17
|
+
from discstore.domain.use_cases.remove_disc import RemoveDisc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_current_tag_router(
|
|
21
|
+
get_current_tag_status: GetCurrentTagStatus,
|
|
22
|
+
add_disc: AddDisc,
|
|
23
|
+
edit_disc: EditDisc,
|
|
24
|
+
get_disc: GetDisc,
|
|
25
|
+
remove_disc: RemoveDisc,
|
|
26
|
+
) -> APIRouter:
|
|
27
|
+
router = APIRouter(prefix="/api/v1", tags=["current-tag"])
|
|
28
|
+
|
|
29
|
+
def read_current_tag_status() -> Optional[CurrentTagStatus]:
|
|
30
|
+
return get_current_tag_status.execute()
|
|
31
|
+
|
|
32
|
+
def ensure_expected_tag_id_matches(
|
|
33
|
+
expected_tag_id: Optional[str], current_tag_status: Optional[CurrentTagStatus]
|
|
34
|
+
) -> None:
|
|
35
|
+
if expected_tag_id is None:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
actual_tag_id = None if current_tag_status is None else current_tag_status.tag_id
|
|
39
|
+
if actual_tag_id != expected_tag_id:
|
|
40
|
+
raise HTTPException(
|
|
41
|
+
status_code=409,
|
|
42
|
+
detail=f"Current tag changed: expected_tag_id='{expected_tag_id}', actual_tag_id={repr(actual_tag_id)}",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def build_current_tag_disc_output(tag_id: str, disc: Disc) -> CurrentTagDiscOutput:
|
|
46
|
+
return CurrentTagDiscOutput(tag_id=tag_id, disc=DiscOutput(**disc.model_dump()))
|
|
47
|
+
|
|
48
|
+
@router.get(
|
|
49
|
+
"/current-tag",
|
|
50
|
+
response_model=CurrentTagStatusOutput,
|
|
51
|
+
responses={204: {"description": "No current tag"}},
|
|
52
|
+
summary="Get the current NFC tag status",
|
|
53
|
+
)
|
|
54
|
+
def get_current_tag() -> Any:
|
|
55
|
+
current_tag_status = read_current_tag_status()
|
|
56
|
+
if current_tag_status is None:
|
|
57
|
+
return Response(status_code=204)
|
|
58
|
+
|
|
59
|
+
return CurrentTagStatusOutput(**current_tag_status.model_dump())
|
|
60
|
+
|
|
61
|
+
@router.get(
|
|
62
|
+
"/current-tag/disc",
|
|
63
|
+
response_model=CurrentTagDiscOutput,
|
|
64
|
+
responses={204: {"description": "No current tag"}, 404: {"description": "Current tag disc not found"}},
|
|
65
|
+
summary="Get the current tag disc",
|
|
66
|
+
)
|
|
67
|
+
def get_current_tag_disc() -> Any:
|
|
68
|
+
current_tag_status = read_current_tag_status()
|
|
69
|
+
if current_tag_status is None:
|
|
70
|
+
return Response(status_code=204)
|
|
71
|
+
|
|
72
|
+
if not current_tag_status.known_in_library:
|
|
73
|
+
raise HTTPException(status_code=404, detail=f"Tag does not exist: tag_id='{current_tag_status.tag_id}'")
|
|
74
|
+
|
|
75
|
+
return build_current_tag_disc_output(current_tag_status.tag_id, get_disc.execute(current_tag_status.tag_id))
|
|
76
|
+
|
|
77
|
+
@router.post(
|
|
78
|
+
"/current-tag/disc",
|
|
79
|
+
response_model=CurrentTagDiscOutput,
|
|
80
|
+
status_code=201,
|
|
81
|
+
responses={204: {"description": "No current tag"}, 409: {"description": "Current tag changed or disc exists"}},
|
|
82
|
+
summary="Create a disc for the current tag",
|
|
83
|
+
)
|
|
84
|
+
def create_current_tag_disc(
|
|
85
|
+
disc: DiscInput,
|
|
86
|
+
expected_tag_id: Optional[str] = None,
|
|
87
|
+
) -> Any:
|
|
88
|
+
current_tag_status = read_current_tag_status()
|
|
89
|
+
ensure_expected_tag_id_matches(expected_tag_id, current_tag_status)
|
|
90
|
+
if current_tag_status is None:
|
|
91
|
+
return Response(status_code=204)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
new_disc = Disc(**disc.model_dump())
|
|
95
|
+
add_disc.execute(current_tag_status.tag_id, new_disc)
|
|
96
|
+
return build_current_tag_disc_output(current_tag_status.tag_id, new_disc)
|
|
97
|
+
except ValueError as value_err:
|
|
98
|
+
raise HTTPException(status_code=409, detail=str(value_err))
|
|
99
|
+
except Exception as err:
|
|
100
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
101
|
+
|
|
102
|
+
@router.patch(
|
|
103
|
+
"/current-tag/disc",
|
|
104
|
+
response_model=CurrentTagDiscOutput,
|
|
105
|
+
responses={
|
|
106
|
+
204: {"description": "No current tag"},
|
|
107
|
+
404: {"description": "Current tag disc not found"},
|
|
108
|
+
409: {"description": "Current tag changed"},
|
|
109
|
+
},
|
|
110
|
+
summary="Update the current tag disc",
|
|
111
|
+
)
|
|
112
|
+
def update_current_tag_disc(
|
|
113
|
+
disc_patch: DiscPatchInput,
|
|
114
|
+
expected_tag_id: Optional[str] = None,
|
|
115
|
+
) -> Any:
|
|
116
|
+
current_tag_status = read_current_tag_status()
|
|
117
|
+
ensure_expected_tag_id_matches(expected_tag_id, current_tag_status)
|
|
118
|
+
if current_tag_status is None:
|
|
119
|
+
return Response(status_code=204)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
metadata = None
|
|
123
|
+
if disc_patch.metadata is not None:
|
|
124
|
+
metadata = DiscMetadata(**disc_patch.metadata.model_dump(exclude_unset=True, exclude_none=True))
|
|
125
|
+
|
|
126
|
+
option = None
|
|
127
|
+
if disc_patch.option is not None:
|
|
128
|
+
option = DiscOption(**disc_patch.option.model_dump(exclude_unset=True, exclude_none=True))
|
|
129
|
+
|
|
130
|
+
edit_disc.execute(current_tag_status.tag_id, disc_patch.uri, metadata, option)
|
|
131
|
+
return build_current_tag_disc_output(current_tag_status.tag_id, get_disc.execute(current_tag_status.tag_id))
|
|
132
|
+
except ValueError as value_err:
|
|
133
|
+
raise HTTPException(status_code=404, detail=str(value_err))
|
|
134
|
+
except Exception as err:
|
|
135
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
136
|
+
|
|
137
|
+
@router.delete(
|
|
138
|
+
"/current-tag/disc",
|
|
139
|
+
status_code=204,
|
|
140
|
+
responses={
|
|
141
|
+
204: {"description": "No current tag or disc deleted"},
|
|
142
|
+
404: {"description": "Current tag disc not found"},
|
|
143
|
+
409: {"description": "Current tag changed"},
|
|
144
|
+
},
|
|
145
|
+
summary="Delete the current tag disc",
|
|
146
|
+
)
|
|
147
|
+
def delete_current_tag_disc(expected_tag_id: Optional[str] = None) -> Response:
|
|
148
|
+
current_tag_status = read_current_tag_status()
|
|
149
|
+
ensure_expected_tag_id_matches(expected_tag_id, current_tag_status)
|
|
150
|
+
if current_tag_status is None:
|
|
151
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
remove_disc.execute(current_tag_status.tag_id)
|
|
155
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
156
|
+
except ValueError as value_err:
|
|
157
|
+
raise HTTPException(status_code=404, detail=str(value_err))
|
|
158
|
+
except Exception as err:
|
|
159
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
160
|
+
|
|
161
|
+
return router
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException, Response, status
|
|
4
|
+
|
|
5
|
+
from discstore.adapters.inbound.api.models import DiscInput, DiscOutput, DiscPatchInput
|
|
6
|
+
from discstore.domain.entities import Disc, DiscMetadata, DiscOption
|
|
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.get_disc import GetDisc
|
|
10
|
+
from discstore.domain.use_cases.list_discs import ListDiscs
|
|
11
|
+
from discstore.domain.use_cases.remove_disc import RemoveDisc
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_discs_router(
|
|
15
|
+
add_disc: AddDisc,
|
|
16
|
+
list_discs: ListDiscs,
|
|
17
|
+
remove_disc: RemoveDisc,
|
|
18
|
+
edit_disc: EditDisc,
|
|
19
|
+
get_disc: GetDisc,
|
|
20
|
+
) -> APIRouter:
|
|
21
|
+
router = APIRouter(prefix="/api/v1", tags=["discs"])
|
|
22
|
+
|
|
23
|
+
@router.get("/discs", response_model=Dict[str, DiscOutput], summary="List discs")
|
|
24
|
+
def list_discs_route() -> Dict[str, Disc]:
|
|
25
|
+
return list_discs.execute()
|
|
26
|
+
|
|
27
|
+
@router.get("/discs/{tag_id}", response_model=DiscOutput, summary="Get a disc")
|
|
28
|
+
def get_disc_route(tag_id: str) -> Disc:
|
|
29
|
+
try:
|
|
30
|
+
return get_disc.execute(tag_id)
|
|
31
|
+
except ValueError as value_err:
|
|
32
|
+
raise HTTPException(status_code=404, detail=str(value_err))
|
|
33
|
+
except Exception as err:
|
|
34
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
35
|
+
|
|
36
|
+
@router.post("/discs/{tag_id}", response_model=DiscOutput, status_code=201, summary="Create a disc")
|
|
37
|
+
def create_disc_route(tag_id: str, disc: DiscInput) -> Disc:
|
|
38
|
+
try:
|
|
39
|
+
new_disc = Disc(**disc.model_dump())
|
|
40
|
+
return add_disc.execute(tag_id, new_disc)
|
|
41
|
+
except ValueError as value_err:
|
|
42
|
+
raise HTTPException(status_code=409, detail=str(value_err))
|
|
43
|
+
except Exception as err:
|
|
44
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
45
|
+
|
|
46
|
+
@router.patch("/discs/{tag_id}", response_model=DiscOutput, summary="Update a disc")
|
|
47
|
+
def update_disc_route(tag_id: str, disc_patch: DiscPatchInput) -> Disc:
|
|
48
|
+
try:
|
|
49
|
+
metadata = None
|
|
50
|
+
if disc_patch.metadata is not None:
|
|
51
|
+
metadata = DiscMetadata(**disc_patch.metadata.model_dump(exclude_unset=True, exclude_none=True))
|
|
52
|
+
|
|
53
|
+
option = None
|
|
54
|
+
if disc_patch.option is not None:
|
|
55
|
+
option = DiscOption(**disc_patch.option.model_dump(exclude_unset=True, exclude_none=True))
|
|
56
|
+
|
|
57
|
+
return edit_disc.execute(tag_id, disc_patch.uri, metadata, option)
|
|
58
|
+
except ValueError as value_err:
|
|
59
|
+
raise HTTPException(status_code=404, detail=str(value_err))
|
|
60
|
+
except Exception as err:
|
|
61
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
62
|
+
|
|
63
|
+
@router.delete("/discs/{tag_id}", status_code=204, summary="Delete a disc")
|
|
64
|
+
def remove_disc_route(tag_id: str) -> Response:
|
|
65
|
+
try:
|
|
66
|
+
remove_disc.execute(tag_id)
|
|
67
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
68
|
+
except ValueError as value_err:
|
|
69
|
+
raise HTTPException(status_code=404, detail=str(value_err))
|
|
70
|
+
except Exception as err:
|
|
71
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
72
|
+
|
|
73
|
+
return router
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
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 DiscPatchMetadataInput(BaseModel):
|
|
17
|
+
artist: Optional[str] = None
|
|
18
|
+
album: Optional[str] = None
|
|
19
|
+
track: Optional[str] = None
|
|
20
|
+
playlist: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DiscPatchOptionInput(BaseModel):
|
|
24
|
+
shuffle: Optional[bool] = None
|
|
25
|
+
is_test: Optional[bool] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DiscPatchInput(BaseModel):
|
|
29
|
+
uri: Optional[str] = None
|
|
30
|
+
metadata: Optional[DiscPatchMetadataInput] = None
|
|
31
|
+
option: Optional[DiscPatchOptionInput] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CurrentTagStatusOutput(CurrentTagStatus):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CurrentTagDiscOutput(BaseModel):
|
|
39
|
+
tag_id: str
|
|
40
|
+
disc: DiscOutput
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SettingsResetInput(BaseModel):
|
|
44
|
+
path: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SettingsPatchInput(RootModel[Dict[str, Any]]):
|
|
48
|
+
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,190 @@
|
|
|
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
|
+
CurrentTagDiscOutput,
|
|
14
|
+
CurrentTagStatusOutput,
|
|
15
|
+
DiscInput,
|
|
16
|
+
DiscOutput,
|
|
17
|
+
DiscPatchInput,
|
|
18
|
+
SettingsPatchInput,
|
|
19
|
+
SettingsResetInput,
|
|
20
|
+
)
|
|
21
|
+
from discstore.adapters.inbound.api.settings_router import build_settings_router
|
|
22
|
+
except ModuleNotFoundError as e:
|
|
23
|
+
if e.name != "fastapi":
|
|
24
|
+
raise
|
|
25
|
+
raise ModuleNotFoundError(
|
|
26
|
+
optional_extra_dependency_message("The `api_controller` module", "api", "discstore api")
|
|
27
|
+
) from e
|
|
28
|
+
from discstore.domain.use_cases.add_disc import AddDisc
|
|
29
|
+
from discstore.domain.use_cases.edit_disc import EditDisc
|
|
30
|
+
from discstore.domain.use_cases.get_current_tag_status import GetCurrentTagStatus
|
|
31
|
+
from discstore.domain.use_cases.get_disc import GetDisc
|
|
32
|
+
from discstore.domain.use_cases.list_discs import ListDiscs
|
|
33
|
+
from discstore.domain.use_cases.remove_disc import RemoveDisc
|
|
34
|
+
from jukebox.settings.entities import SelectedSonosGroupSettings
|
|
35
|
+
from jukebox.settings.selected_sonos_group_repository import SettingsSelectedSonosGroupRepository
|
|
36
|
+
from jukebox.settings.service_protocols import SettingsService
|
|
37
|
+
from jukebox.sonos.discovery import DiscoveredSonosSpeaker, SonosDiscoveryError
|
|
38
|
+
from jukebox.sonos.selection import GetSonosSelectionStatus, SaveSonosSelection
|
|
39
|
+
from jukebox.sonos.service import SonosService
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"APIController",
|
|
43
|
+
"CurrentTagDiscOutput",
|
|
44
|
+
"CurrentTagStatusOutput",
|
|
45
|
+
"DiscInput",
|
|
46
|
+
"DiscOutput",
|
|
47
|
+
"DiscPatchInput",
|
|
48
|
+
"SettingsPatchInput",
|
|
49
|
+
"SettingsResetInput",
|
|
50
|
+
"SonosSelectionInput",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SonosSpeakerOutput(DiscoveredSonosSpeaker):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SelectedSonosGroupOutput(SelectedSonosGroupSettings):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SonosSelectionMemberAvailabilityOutput(BaseModel):
|
|
63
|
+
uid: str
|
|
64
|
+
status: str
|
|
65
|
+
speaker: Optional[SonosSpeakerOutput] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SonosSelectionAvailabilityOutput(BaseModel):
|
|
69
|
+
status: str
|
|
70
|
+
members: list[SonosSelectionMemberAvailabilityOutput]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SonosSelectionOutput(BaseModel):
|
|
74
|
+
selected_group: Optional[SelectedSonosGroupOutput] = None
|
|
75
|
+
availability: SonosSelectionAvailabilityOutput
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SonosSelectionInput(BaseModel):
|
|
79
|
+
uids: list[str]
|
|
80
|
+
coordinator_uid: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SonosSelectionUpdateOutput(BaseModel):
|
|
84
|
+
selected_group: SelectedSonosGroupOutput
|
|
85
|
+
availability: SonosSelectionAvailabilityOutput
|
|
86
|
+
message: str
|
|
87
|
+
restart_required: bool
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class APIController:
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
add_disc: AddDisc,
|
|
94
|
+
list_discs: ListDiscs,
|
|
95
|
+
remove_disc: RemoveDisc,
|
|
96
|
+
edit_disc: EditDisc,
|
|
97
|
+
get_disc: GetDisc,
|
|
98
|
+
get_current_tag_status: GetCurrentTagStatus,
|
|
99
|
+
settings_service: SettingsService,
|
|
100
|
+
sonos_service: SonosService,
|
|
101
|
+
):
|
|
102
|
+
self.add_disc = add_disc
|
|
103
|
+
self.list_discs = list_discs
|
|
104
|
+
self.remove_disc = remove_disc
|
|
105
|
+
self.edit_disc = edit_disc
|
|
106
|
+
self.get_disc = get_disc
|
|
107
|
+
self.get_current_tag_status = get_current_tag_status
|
|
108
|
+
self.settings_service = settings_service
|
|
109
|
+
self.sonos_service = sonos_service
|
|
110
|
+
self.app = FastAPI(
|
|
111
|
+
title="DiscStore API",
|
|
112
|
+
description="API for managing Jukebox disc library",
|
|
113
|
+
docs_url="/docs",
|
|
114
|
+
redoc_url="/redoc",
|
|
115
|
+
)
|
|
116
|
+
self.register_routes()
|
|
117
|
+
|
|
118
|
+
def register_routes(self):
|
|
119
|
+
self.app.include_router(
|
|
120
|
+
build_discs_router(
|
|
121
|
+
add_disc=self.add_disc,
|
|
122
|
+
list_discs=self.list_discs,
|
|
123
|
+
remove_disc=self.remove_disc,
|
|
124
|
+
edit_disc=self.edit_disc,
|
|
125
|
+
get_disc=self.get_disc,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
self.app.include_router(
|
|
129
|
+
build_current_tag_router(
|
|
130
|
+
get_current_tag_status=self.get_current_tag_status,
|
|
131
|
+
add_disc=self.add_disc,
|
|
132
|
+
edit_disc=self.edit_disc,
|
|
133
|
+
get_disc=self.get_disc,
|
|
134
|
+
remove_disc=self.remove_disc,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
self.app.include_router(build_settings_router(self.settings_service))
|
|
138
|
+
|
|
139
|
+
@self.app.get("/api/v1/sonos/speakers", response_model=list[SonosSpeakerOutput])
|
|
140
|
+
def get_sonos_speakers():
|
|
141
|
+
try:
|
|
142
|
+
return self.sonos_service.list_available_speakers()
|
|
143
|
+
except SonosDiscoveryError as err:
|
|
144
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
145
|
+
except Exception as err:
|
|
146
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
147
|
+
|
|
148
|
+
@self.app.get("/api/v1/sonos/selection", response_model=SonosSelectionOutput)
|
|
149
|
+
def get_sonos_selection():
|
|
150
|
+
try:
|
|
151
|
+
return GetSonosSelectionStatus(
|
|
152
|
+
SettingsSelectedSonosGroupRepository(self.settings_service),
|
|
153
|
+
self.sonos_service,
|
|
154
|
+
).execute()
|
|
155
|
+
except SonosDiscoveryError as err:
|
|
156
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
157
|
+
except Exception as err:
|
|
158
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|
|
159
|
+
|
|
160
|
+
@self.app.put("/api/v1/sonos/selection", response_model=SonosSelectionUpdateOutput)
|
|
161
|
+
def put_sonos_selection(payload: SonosSelectionInput):
|
|
162
|
+
try:
|
|
163
|
+
result = SaveSonosSelection(
|
|
164
|
+
SettingsSelectedSonosGroupRepository(self.settings_service),
|
|
165
|
+
self.sonos_service,
|
|
166
|
+
).execute(payload.uids, coordinator_uid=payload.coordinator_uid)
|
|
167
|
+
return SonosSelectionUpdateOutput(
|
|
168
|
+
selected_group=SelectedSonosGroupOutput(**result.selected_group.model_dump()),
|
|
169
|
+
availability=SonosSelectionAvailabilityOutput(
|
|
170
|
+
status="available",
|
|
171
|
+
members=[
|
|
172
|
+
SonosSelectionMemberAvailabilityOutput(
|
|
173
|
+
uid=member.uid,
|
|
174
|
+
status="available",
|
|
175
|
+
speaker=SonosSpeakerOutput(**member.model_dump()),
|
|
176
|
+
)
|
|
177
|
+
for member in result.members
|
|
178
|
+
],
|
|
179
|
+
),
|
|
180
|
+
message=result.settings_message,
|
|
181
|
+
restart_required=result.restart_required,
|
|
182
|
+
)
|
|
183
|
+
except SonosDiscoveryError as err:
|
|
184
|
+
raise HTTPException(status_code=502, detail=str(err))
|
|
185
|
+
except ValueError as err:
|
|
186
|
+
raise HTTPException(status_code=400, detail=str(err))
|
|
187
|
+
except HTTPException:
|
|
188
|
+
raise
|
|
189
|
+
except Exception as err:
|
|
190
|
+
raise HTTPException(status_code=500, detail=f"Server error: {str(err)}")
|