gukebox 1.0.0.dev7__tar.gz → 1.0.0.dev8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/PKG-INFO +25 -36
  2. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/README.md +24 -35
  3. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/api/current_tag_router.py +9 -6
  4. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/api/discs_router.py +5 -2
  5. gukebox-1.0.0.dev8/discstore/adapters/inbound/ui_controller.py +367 -0
  6. gukebox-1.0.0.dev8/discstore/adapters/inbound/ui_pages/library.py +370 -0
  7. gukebox-1.0.0.dev8/discstore/adapters/inbound/ui_pages/settings.py +519 -0
  8. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/app.py +0 -2
  9. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/commands.py +1 -6
  10. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/edit_disc.py +2 -2
  11. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/inbound/config.py +5 -20
  12. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/app.py +0 -5
  13. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/commands.py +1 -6
  14. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/di_container.py +2 -5
  15. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/app.py +1 -1
  16. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/entities.py +1 -6
  17. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/resolve.py +5 -15
  18. gukebox-1.0.0.dev8/jukebox/shared/__init__.py +0 -0
  19. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/shared/config_utils.py +0 -13
  20. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/sonos/selection.py +1 -6
  21. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/pyproject.toml +1 -1
  22. gukebox-1.0.0.dev7/discstore/adapters/inbound/ui_controller.py +0 -1084
  23. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/LICENSE +0 -0
  24. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/__init__.py +0 -0
  25. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/__init__.py +0 -0
  26. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/__init__.py +0 -0
  27. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/api/__init__.py +0 -0
  28. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/api/models.py +0 -0
  29. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/api/settings_router.py +0 -0
  30. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/api_controller.py +0 -0
  31. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/cli_controller.py +0 -0
  32. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/cli_display.py +0 -0
  33. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/config.py +0 -0
  34. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
  35. {gukebox-1.0.0.dev7/discstore/adapters/outbound → gukebox-1.0.0.dev8/discstore/adapters/inbound/ui_pages}/__init__.py +0 -0
  36. {gukebox-1.0.0.dev7/discstore/domain → gukebox-1.0.0.dev8/discstore/adapters/outbound}/__init__.py +0 -0
  37. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/outbound/json_library_adapter.py +0 -0
  38. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
  39. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/command_handlers.py +0 -0
  40. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/di_container.py +0 -0
  41. {gukebox-1.0.0.dev7/jukebox → gukebox-1.0.0.dev8/discstore/domain}/__init__.py +0 -0
  42. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/entities/__init__.py +0 -0
  43. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/entities/current_tag_status.py +0 -0
  44. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/repositories/__init__.py +0 -0
  45. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/__init__.py +0 -0
  46. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/add_disc.py +0 -0
  47. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
  48. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/get_disc.py +0 -0
  49. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/list_discs.py +0 -0
  50. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/remove_disc.py +0 -0
  51. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
  52. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/discstore/domain/use_cases/search_discs.py +0 -0
  53. {gukebox-1.0.0.dev7/jukebox/adapters → gukebox-1.0.0.dev8/jukebox}/__init__.py +0 -0
  54. {gukebox-1.0.0.dev7/jukebox/adapters/inbound → gukebox-1.0.0.dev8/jukebox/adapters}/__init__.py +0 -0
  55. {gukebox-1.0.0.dev7/jukebox/adapters/outbound → gukebox-1.0.0.dev8/jukebox/adapters/inbound}/__init__.py +0 -0
  56. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/inbound/cli_controller.py +0 -0
  57. {gukebox-1.0.0.dev7/jukebox/adapters/outbound/players → gukebox-1.0.0.dev8/jukebox/adapters/outbound}/__init__.py +0 -0
  58. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
  59. {gukebox-1.0.0.dev7/jukebox/adapters/outbound/readers → gukebox-1.0.0.dev8/jukebox/adapters/outbound/players}/__init__.py +0 -0
  60. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
  61. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/players/sonos_player_adapter.py +0 -0
  62. {gukebox-1.0.0.dev7/jukebox/domain → gukebox-1.0.0.dev8/jukebox/adapters/outbound/readers}/__init__.py +0 -0
  63. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
  64. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +0 -0
  65. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
  66. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
  67. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/__init__.py +0 -0
  68. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/cli_presentation.py +0 -0
  69. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/command_handlers.py +0 -0
  70. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/admin/services.py +0 -0
  71. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/di_container.py +0 -0
  72. {gukebox-1.0.0.dev7/jukebox/shared → gukebox-1.0.0.dev8/jukebox/domain}/__init__.py +0 -0
  73. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/__init__.py +0 -0
  74. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/current_tag_action.py +0 -0
  75. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/disc.py +0 -0
  76. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/library.py +0 -0
  77. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/playback_action.py +0 -0
  78. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/playback_session.py +0 -0
  79. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/entities/tag_event.py +0 -0
  80. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/ports/__init__.py +0 -0
  81. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/ports/player_port.py +0 -0
  82. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/ports/reader_port.py +0 -0
  83. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/repositories/__init__.py +0 -0
  84. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/repositories/current_tag_repository.py +0 -0
  85. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/repositories/library_repository.py +0 -0
  86. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/use_cases/__init__.py +0 -0
  87. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/use_cases/determine_action.py +0 -0
  88. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
  89. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/domain/use_cases/handle_tag_event.py +0 -0
  90. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/__init__.py +0 -0
  91. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/definitions.py +0 -0
  92. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/dict_utils.py +0 -0
  93. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/errors.py +0 -0
  94. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/file_settings_repository.py +0 -0
  95. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/migration.py +0 -0
  96. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/repositories.py +0 -0
  97. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/runtime_resolver.py +0 -0
  98. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/runtime_validation.py +0 -0
  99. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/selected_sonos_group_repository.py +0 -0
  100. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/service_protocols.py +0 -0
  101. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/timing_validation.py +0 -0
  102. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/types.py +0 -0
  103. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/validation_rules.py +0 -0
  104. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/settings/view_utils.py +0 -0
  105. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/shared/dependency_messages.py +0 -0
  106. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/shared/logger.py +0 -0
  107. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/shared/timing.py +0 -0
  108. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/sonos/__init__.py +0 -0
  109. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/sonos/discovery.py +0 -0
  110. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/jukebox/sonos/service.py +0 -0
  111. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/pn532/__init__.py +0 -0
  112. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/pn532/pn532.py +0 -0
  113. {gukebox-1.0.0.dev7 → gukebox-1.0.0.dev8}/pn532/spi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gukebox
3
- Version: 1.0.0.dev7
3
+ Version: 1.0.0.dev8
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
@@ -58,8 +58,9 @@ Description-Content-Type: text/markdown
58
58
 
59
59
  # Jukebox \[gukebox\]
60
60
 
61
- [![python versions](https://img.shields.io/pypi/pyversions/gukebox.svg)](https://pypi.python.org/pypi/gukebox)
62
- [![gukebox last version](https://img.shields.io/pypi/v/gukebox.svg)](https://pypi.python.org/pypi/gukebox)
61
+ ![rpi](https://img.shields.io/badge/-Zero%202W%20%7C%203%20%7C%205-C51A4A?logo=raspberry-pi&label=RPi&logoColor=C51A4A&labcolor=C51A4A)
62
+ [![python versions](https://img.shields.io/pypi/pyversions/gukebox.svg?logo=python)](https://pypi.python.org/pypi/gukebox)
63
+ [![gukebox last version](https://img.shields.io/pypi/v/gukebox.svg?logo=pypi)](https://pypi.python.org/pypi/gukebox)
63
64
  [![license](https://img.shields.io/pypi/l/gukebox.svg)](https://pypi.python.org/pypi/gukebox)
64
65
  [![actions status](https://github.com/gudsfile/jukebox/actions/workflows/python.yml/badge.svg)](https://github.com/gudsfile/jukebox/actions)
65
66
  [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
@@ -84,10 +85,9 @@ Description-Content-Type: text/markdown
84
85
 
85
86
  - [Install](#install)
86
87
  - [First steps](#first-steps)
87
- - [Discstore](#manage-the-library-with-the-discstore)
88
88
  - [Usage](#usage)
89
- - [Readers](#readers)
90
- - [Players](#players)
89
+ - [Readers](#readers)
90
+ - [Players](#players)
91
91
  - [The library file](#the-library-file)
92
92
  - [Developer setup](#developer-setup)
93
93
 
@@ -216,16 +216,18 @@ Take a look at `library.example.json` and the [The library file](#the-library-fi
216
216
  Start the jukebox with the `jukebox` command (show help message with `--help`)
217
217
 
218
218
  ```shell
219
- jukebox PLAYER_TO_USE READER_TO_USE
219
+ jukebox --player PLAYER --reader READER
220
220
  ```
221
221
 
222
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.
223
223
 
224
- Optional Parameters
224
+ **Optional Parameters**
225
225
 
226
226
  | Parameter | Description |
227
227
  | --- | --- |
228
228
  | `--help` | Show help message. |
229
+ | `--player PLAYER` | Player to use (`sonos`, `dryrun`). |
230
+ | `--reader READER` | Reader to use (`pn532`, `dryrun`). |
229
231
  | `--library` | Path to the library file, default: `~/.config/jukebox/library.json`. |
230
232
  | `--pause-delay SECONDS` | Grace period before pausing when the NFC tag is removed. Fractional values such as `0.5` or `0.2` are supported, with a minimum of `0.2` seconds to avoid pausing on brief missed reads. Default: 0.25 seconds. |
231
233
  | `--pause-duration SECONDS` | Maximum duration of a pause before resetting the queue. Default: 900 seconds (15 minutes). |
@@ -234,34 +236,23 @@ Optional Parameters
234
236
 
235
237
  ### Readers
236
238
 
237
- **Dry run** (`dryrun`)
238
- Read a text entry.
239
- Allows you to simulate reading an NFC tag by writting the tag id in the console.
240
- Expected syntax: `tag_id` or `tag_id duration_seconds`.
241
- - tag_id: the full identifier of the tag, in the format required by the system
242
- - duration_seconds: a non-negative number of seconds used to simulate how long the tag remains in place. Fractional values are allowed.
243
- Complete example: `your:tag:uid 2.5`
239
+ | Name | Description |
240
+ | --- | --- |
241
+ | Dry Run (`dryrun`) | Simulates NFC tag reading via stdin. Input format: `tag_id` or `tag_id duration_seconds`. |
242
+ | Pn532 NFC (`pn532`) | Reads physical NFC tags. Works with a **PN532** reader and **NTAG2xx** tags. Requires the `pn532` extra and SPI enabled on the Raspberry Pi. |
244
243
 
245
- **NFC Pn532** (`pn532`)
246
- Read an NFC tag and get its UID.
247
- This project works with an NFC reader like the **PN532** and NFC tags like the **NTAG2xx**.
248
- It is configured according to the [Waveshare PN532 wiki](https://www.waveshare.com/wiki/PN532_NFC_HAT).
249
- Don't forget to enable the SPI interface using the command `sudo raspi-config`, then go to: `Interface Options > SPI > Enable > Yes`.
244
+ > [!NOTE]
245
+ > See [docs/readers.md](docs/readers.md) for full setup, hardware requirements, and settings reference.
250
246
 
251
247
  ### Players
252
248
 
253
- **Dry run** (`dryrun`)
254
- Displays the events that a real speaker would have performed (`playing …`, `pause`, etc.).
255
-
256
- **Sonos** (`sonos`) [![SoCo](https://img.shields.io/badge/based%20on-SoCo-000)](https://github.com/SoCo/SoCo)
257
- Play music through a Sonos speaker.
258
- Three ways to select the speaker (mutually exclusive):
249
+ | Name | Description |
250
+ | --- | --- |
251
+ | Dry Run (`dryrun`) | Displays the events that a real speaker would have performed (`playing …`, `pause`, etc.). |
252
+ | Sonos (`sonos`) | [![SoCo](https://img.shields.io/badge/based%20on-SoCo-000)](https://github.com/SoCo/SoCo) Plays music through a Sonos speaker. Select by IP (`--sonos-host`), by name (`--sonos-name`), or let it auto-discover. |
259
253
 
260
- | Option | CLI flag | Environment variable | Behaviour |
261
- | --- | --- | --- | --- |
262
- | By IP | `--sonos-host 192.168.0.x` | `JUKEBOX_SONOS_HOST` | Connect directly, no discovery |
263
- | By name | `--sonos-name "Living Room"` | `JUKEBOX_SONOS_NAME` | Discover, then filter by name (case-sensitive) |
264
- | Auto | *(omit both)* | *(omit both)* | Discover, pick the first speaker alphabetically |
254
+ > [!NOTE]
255
+ > See [docs/players.md](docs/players.md) for the full configuration reference.
265
256
 
266
257
  ## The library file
267
258
 
@@ -341,9 +332,7 @@ uv sync
341
332
 
342
333
  Add `--all-extras` to install dependencies for all extras (`api` and `ui`).
343
334
 
344
- If needed, set `JUKEBOX_SONOS_HOST` (IP) or `JUKEBOX_SONOS_NAME` (speaker name) to select your Sonos speaker (see [Players](#players)).
345
- If neither is set, the jukebox will auto-discover a speaker on the network.
346
- To do this you can use a `.env` file and `uv run --env-file .env <command to run>`.
335
+ If needed, you can use a `.env` file and `uv run --env-file .env <command to run>`.
347
336
  A `.env.example` file is available, you can copy it and modify it to use it.
348
337
 
349
338
  Create a `library.json` file and complete it with the desired NFC tags and CDs.
@@ -354,7 +343,7 @@ Take a look at `library.example.json` and the [The library file](#the-library-fi
354
343
  Start the jukebox with `uv` and use `--help` to show help message
355
344
 
356
345
  ```shell
357
- uv run jukebox PLAYER_TO_USE READER_TO_USE
346
+ uv run jukebox --player PLAYER_TO_USE --reader READER_TO_USE
358
347
  ```
359
348
 
360
349
  Start the discstore `uv` and use `--help` to show help message
@@ -383,7 +372,7 @@ uv run --extra api discstore api
383
372
  uv run --extra ui discstore ui
384
373
  ```
385
374
 
386
- Other commands are available:
375
+ ### Development commands
387
376
 
388
377
  | Command | Description |
389
378
  | --- | --- |
@@ -1,7 +1,8 @@
1
1
  # Jukebox \[gukebox\]
2
2
 
3
- [![python versions](https://img.shields.io/pypi/pyversions/gukebox.svg)](https://pypi.python.org/pypi/gukebox)
4
- [![gukebox last version](https://img.shields.io/pypi/v/gukebox.svg)](https://pypi.python.org/pypi/gukebox)
3
+ ![rpi](https://img.shields.io/badge/-Zero%202W%20%7C%203%20%7C%205-C51A4A?logo=raspberry-pi&label=RPi&logoColor=C51A4A&labcolor=C51A4A)
4
+ [![python versions](https://img.shields.io/pypi/pyversions/gukebox.svg?logo=python)](https://pypi.python.org/pypi/gukebox)
5
+ [![gukebox last version](https://img.shields.io/pypi/v/gukebox.svg?logo=pypi)](https://pypi.python.org/pypi/gukebox)
5
6
  [![license](https://img.shields.io/pypi/l/gukebox.svg)](https://pypi.python.org/pypi/gukebox)
6
7
  [![actions status](https://github.com/gudsfile/jukebox/actions/workflows/python.yml/badge.svg)](https://github.com/gudsfile/jukebox/actions)
7
8
  [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
@@ -26,10 +27,9 @@
26
27
 
27
28
  - [Install](#install)
28
29
  - [First steps](#first-steps)
29
- - [Discstore](#manage-the-library-with-the-discstore)
30
30
  - [Usage](#usage)
31
- - [Readers](#readers)
32
- - [Players](#players)
31
+ - [Readers](#readers)
32
+ - [Players](#players)
33
33
  - [The library file](#the-library-file)
34
34
  - [Developer setup](#developer-setup)
35
35
 
@@ -158,16 +158,18 @@ Take a look at `library.example.json` and the [The library file](#the-library-fi
158
158
  Start the jukebox with the `jukebox` command (show help message with `--help`)
159
159
 
160
160
  ```shell
161
- jukebox PLAYER_TO_USE READER_TO_USE
161
+ jukebox --player PLAYER --reader READER
162
162
  ```
163
163
 
164
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
- Optional Parameters
166
+ **Optional Parameters**
167
167
 
168
168
  | Parameter | Description |
169
169
  | --- | --- |
170
170
  | `--help` | Show help message. |
171
+ | `--player PLAYER` | Player to use (`sonos`, `dryrun`). |
172
+ | `--reader READER` | Reader to use (`pn532`, `dryrun`). |
171
173
  | `--library` | Path to the library file, default: `~/.config/jukebox/library.json`. |
172
174
  | `--pause-delay SECONDS` | Grace period before pausing when the NFC tag is removed. Fractional values such as `0.5` or `0.2` are supported, with a minimum of `0.2` seconds to avoid pausing on brief missed reads. Default: 0.25 seconds. |
173
175
  | `--pause-duration SECONDS` | Maximum duration of a pause before resetting the queue. Default: 900 seconds (15 minutes). |
@@ -176,34 +178,23 @@ Optional Parameters
176
178
 
177
179
  ### Readers
178
180
 
179
- **Dry run** (`dryrun`)
180
- Read a text entry.
181
- Allows you to simulate reading an NFC tag by writting the tag id in the console.
182
- Expected syntax: `tag_id` or `tag_id duration_seconds`.
183
- - tag_id: the full identifier of the tag, in the format required by the system
184
- - duration_seconds: a non-negative number of seconds used to simulate how long the tag remains in place. Fractional values are allowed.
185
- Complete example: `your:tag:uid 2.5`
181
+ | Name | Description |
182
+ | --- | --- |
183
+ | Dry Run (`dryrun`) | Simulates NFC tag reading via stdin. Input format: `tag_id` or `tag_id duration_seconds`. |
184
+ | Pn532 NFC (`pn532`) | Reads physical NFC tags. Works with a **PN532** reader and **NTAG2xx** tags. Requires the `pn532` extra and SPI enabled on the Raspberry Pi. |
186
185
 
187
- **NFC Pn532** (`pn532`)
188
- Read an NFC tag and get its UID.
189
- This project works with an NFC reader like the **PN532** and NFC tags like the **NTAG2xx**.
190
- It is configured according to the [Waveshare PN532 wiki](https://www.waveshare.com/wiki/PN532_NFC_HAT).
191
- Don't forget to enable the SPI interface using the command `sudo raspi-config`, then go to: `Interface Options > SPI > Enable > Yes`.
186
+ > [!NOTE]
187
+ > See [docs/readers.md](docs/readers.md) for full setup, hardware requirements, and settings reference.
192
188
 
193
189
  ### Players
194
190
 
195
- **Dry run** (`dryrun`)
196
- Displays the events that a real speaker would have performed (`playing …`, `pause`, etc.).
197
-
198
- **Sonos** (`sonos`) [![SoCo](https://img.shields.io/badge/based%20on-SoCo-000)](https://github.com/SoCo/SoCo)
199
- Play music through a Sonos speaker.
200
- Three ways to select the speaker (mutually exclusive):
191
+ | Name | Description |
192
+ | --- | --- |
193
+ | Dry Run (`dryrun`) | Displays the events that a real speaker would have performed (`playing …`, `pause`, etc.). |
194
+ | Sonos (`sonos`) | [![SoCo](https://img.shields.io/badge/based%20on-SoCo-000)](https://github.com/SoCo/SoCo) Plays music through a Sonos speaker. Select by IP (`--sonos-host`), by name (`--sonos-name`), or let it auto-discover. |
201
195
 
202
- | Option | CLI flag | Environment variable | Behaviour |
203
- | --- | --- | --- | --- |
204
- | By IP | `--sonos-host 192.168.0.x` | `JUKEBOX_SONOS_HOST` | Connect directly, no discovery |
205
- | By name | `--sonos-name "Living Room"` | `JUKEBOX_SONOS_NAME` | Discover, then filter by name (case-sensitive) |
206
- | Auto | *(omit both)* | *(omit both)* | Discover, pick the first speaker alphabetically |
196
+ > [!NOTE]
197
+ > See [docs/players.md](docs/players.md) for the full configuration reference.
207
198
 
208
199
  ## The library file
209
200
 
@@ -283,9 +274,7 @@ uv sync
283
274
 
284
275
  Add `--all-extras` to install dependencies for all extras (`api` and `ui`).
285
276
 
286
- If needed, set `JUKEBOX_SONOS_HOST` (IP) or `JUKEBOX_SONOS_NAME` (speaker name) to select your Sonos speaker (see [Players](#players)).
287
- If neither is set, the jukebox will auto-discover a speaker on the network.
288
- To do this you can use a `.env` file and `uv run --env-file .env <command to run>`.
277
+ If needed, you can use a `.env` file and `uv run --env-file .env <command to run>`.
289
278
  A `.env.example` file is available, you can copy it and modify it to use it.
290
279
 
291
280
  Create a `library.json` file and complete it with the desired NFC tags and CDs.
@@ -296,7 +285,7 @@ Take a look at `library.example.json` and the [The library file](#the-library-fi
296
285
  Start the jukebox with `uv` and use `--help` to show help message
297
286
 
298
287
  ```shell
299
- uv run jukebox PLAYER_TO_USE READER_TO_USE
288
+ uv run jukebox --player PLAYER_TO_USE --reader READER_TO_USE
300
289
  ```
301
290
 
302
291
  Start the discstore `uv` and use `--help` to show help message
@@ -325,7 +314,7 @@ uv run --extra api discstore api
325
314
  uv run --extra ui discstore ui
326
315
  ```
327
316
 
328
- Other commands are available:
317
+ ### Development commands
329
318
 
330
319
  | Command | Description |
331
320
  | --- | --- |
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Optional
2
2
 
3
3
  from fastapi import APIRouter, HTTPException, Response, status
4
+ from pydantic import ValidationError
4
5
 
5
6
  from discstore.adapters.inbound.api.models import (
6
7
  CurrentTagDiscOutput,
@@ -92,8 +93,8 @@ def build_current_tag_router(
92
93
 
93
94
  try:
94
95
  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)
96
+ created_disc = add_disc.execute(current_tag_status.tag_id, new_disc)
97
+ return build_current_tag_disc_output(current_tag_status.tag_id, created_disc)
97
98
  except ValueError as value_err:
98
99
  raise HTTPException(status_code=409, detail=str(value_err))
99
100
  except Exception as err:
@@ -121,14 +122,16 @@ def build_current_tag_router(
121
122
  try:
122
123
  metadata = None
123
124
  if disc_patch.metadata is not None:
124
- metadata = DiscMetadata(**disc_patch.metadata.model_dump(exclude_unset=True, exclude_none=True))
125
+ metadata = DiscMetadata(**disc_patch.metadata.model_dump(exclude_unset=True))
125
126
 
126
127
  option = None
127
128
  if disc_patch.option is not None:
128
- option = DiscOption(**disc_patch.option.model_dump(exclude_unset=True, exclude_none=True))
129
+ option = DiscOption(**disc_patch.option.model_dump(exclude_unset=True))
129
130
 
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))
131
+ updated_disc = edit_disc.execute(current_tag_status.tag_id, disc_patch.uri, metadata, option)
132
+ return build_current_tag_disc_output(current_tag_status.tag_id, updated_disc)
133
+ except ValidationError as err:
134
+ raise HTTPException(status_code=422, detail=err.errors())
132
135
  except ValueError as value_err:
133
136
  raise HTTPException(status_code=404, detail=str(value_err))
134
137
  except Exception as err:
@@ -1,6 +1,7 @@
1
1
  from typing import Dict
2
2
 
3
3
  from fastapi import APIRouter, HTTPException, Response, status
4
+ from pydantic import ValidationError
4
5
 
5
6
  from discstore.adapters.inbound.api.models import DiscInput, DiscOutput, DiscPatchInput
6
7
  from discstore.domain.entities import Disc, DiscMetadata, DiscOption
@@ -48,13 +49,15 @@ def build_discs_router(
48
49
  try:
49
50
  metadata = None
50
51
  if disc_patch.metadata is not None:
51
- metadata = DiscMetadata(**disc_patch.metadata.model_dump(exclude_unset=True, exclude_none=True))
52
+ metadata = DiscMetadata(**disc_patch.metadata.model_dump(exclude_unset=True))
52
53
 
53
54
  option = None
54
55
  if disc_patch.option is not None:
55
- option = DiscOption(**disc_patch.option.model_dump(exclude_unset=True, exclude_none=True))
56
+ option = DiscOption(**disc_patch.option.model_dump(exclude_unset=True))
56
57
 
57
58
  return edit_disc.execute(tag_id, disc_patch.uri, metadata, option)
59
+ except ValidationError as err:
60
+ raise HTTPException(status_code=422, detail=err.errors())
58
61
  except ValueError as value_err:
59
62
  raise HTTPException(status_code=404, detail=str(value_err))
60
63
  except Exception as err: