gukebox 1.0.0.dev11__tar.gz → 1.0.0.dev13__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.dev11 → gukebox-1.0.0.dev13}/PKG-INFO +1 -1
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/cli_controller.py +4 -4
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_controller.py +1 -1
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/settings.py +2 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/sonos.py +2 -2
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/json_library_adapter.py +6 -3
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +1 -1
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/players/sonos_player_adapter.py +33 -18
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +4 -3
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +1 -1
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/text_current_tag_adapter.py +1 -1
- gukebox-1.0.0.dev13/jukebox/domain/errors.py +2 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/handle_tag_event.py +39 -17
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/pyproject.toml +2 -1
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/LICENSE +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/README.md +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/current_tag_router.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/discs_router.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/models.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/settings_router.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api_controller.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/cli_display.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/library.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/command_handlers.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/commands.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/di_container.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/add_disc.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/edit_disc.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/get_disc.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/list_discs.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/remove_disc.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/search_discs.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/inbound/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/inbound/cli_controller.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/inbound/config.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/players/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/readers/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/app.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/cli_presentation.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/command_handlers.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/commands.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/di_container.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/pn532_command_handlers.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/pn532_commands.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/services.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/admin/sonos_households.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/app.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/di_container.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/current_tag_action.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/playback_action.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/playback_session.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/determine_action.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/pn532/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/pn532/profiles.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/definitions.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/entities.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/errors.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/file_settings_repository.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/migration.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/resolve.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/runtime_resolver.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/selected_sonos_group_repository.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/service_protocols.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/types.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/shared/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/shared/dependency_messages.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/shared/logger.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/shared/terminal_ui.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/shared/timing.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/sonos/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/sonos/discovery.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/sonos/selection.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/sonos/service.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/pn532/__init__.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/pn532/pn532.py +0 -0
- {gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/pn532/spi.py +0 -0
|
@@ -60,7 +60,7 @@ class CLIController:
|
|
|
60
60
|
elif isinstance(command, CliSearchCommand):
|
|
61
61
|
self.search_discs_flow(command)
|
|
62
62
|
else:
|
|
63
|
-
LOGGER.error(
|
|
63
|
+
LOGGER.error("Command not implemented yet: command='%s'", command)
|
|
64
64
|
|
|
65
65
|
def add_disc_flow(self, command: CliAddCommand) -> None:
|
|
66
66
|
tag = self.resolve_tag_id.execute(command.tag, command.use_current_tag)
|
|
@@ -79,7 +79,7 @@ class CLIController:
|
|
|
79
79
|
if command.mode == "line":
|
|
80
80
|
display_library_line(discs)
|
|
81
81
|
return
|
|
82
|
-
LOGGER.error(
|
|
82
|
+
LOGGER.error("Displaying mode not implemented yet: mode='%s'", command.mode)
|
|
83
83
|
|
|
84
84
|
def remove_disc_flow(self, command: CliRemoveCommand) -> None:
|
|
85
85
|
tag = self.resolve_tag_id.execute(command.tag, command.use_current_tag)
|
|
@@ -122,7 +122,7 @@ class CLIController:
|
|
|
122
122
|
def search_discs_flow(self, command: CliSearchCommand) -> None:
|
|
123
123
|
results = self.search_discs.execute(command.query)
|
|
124
124
|
if not results:
|
|
125
|
-
LOGGER.info(
|
|
125
|
+
LOGGER.info("No discs found matching '%s'", command.query)
|
|
126
126
|
return
|
|
127
|
-
LOGGER.info(
|
|
127
|
+
LOGGER.info("Found %d disc(s) matching '%s':", len(results), command.query)
|
|
128
128
|
display_library_table(results)
|
|
@@ -352,6 +352,8 @@ class SettingsUIPageBuilder:
|
|
|
352
352
|
|
|
353
353
|
def build_settings_badges(self, setting: EditableSettingDisplay) -> list[AnyComponent]:
|
|
354
354
|
badge_components: list[AnyComponent] = []
|
|
355
|
+
if setting.is_persisted and not setting.is_pinned_default:
|
|
356
|
+
badge_components.append(c.Paragraph(text="Configured", class_name="badge text-bg-success text-uppercase"))
|
|
355
357
|
if setting.is_pinned_default:
|
|
356
358
|
badge_components.append(c.Paragraph(text="Pinned default", class_name="badge text-bg-info text-uppercase"))
|
|
357
359
|
if setting.requires_restart:
|
|
@@ -80,7 +80,7 @@ class SonosUIPageBuilder:
|
|
|
80
80
|
selected_group_repository=SettingsSelectedSonosGroupRepository(self.settings_service),
|
|
81
81
|
sonos_service=self.sonos_service,
|
|
82
82
|
).execute()
|
|
83
|
-
speakers = self.sonos_service.
|
|
83
|
+
speakers = self.sonos_service.list_network_speakers()
|
|
84
84
|
except SonosDiscoveryError as err:
|
|
85
85
|
discovery_error = str(err)
|
|
86
86
|
|
|
@@ -154,7 +154,7 @@ class SonosUIPageBuilder:
|
|
|
154
154
|
]
|
|
155
155
|
|
|
156
156
|
try:
|
|
157
|
-
speakers = self.sonos_service.
|
|
157
|
+
speakers = self.sonos_service.list_network_speakers()
|
|
158
158
|
except SonosDiscoveryError as err:
|
|
159
159
|
components.append(
|
|
160
160
|
c.Error(
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/json_library_adapter.py
RENAMED
|
@@ -26,17 +26,20 @@ class JsonLibraryAdapter(LibraryRepository):
|
|
|
26
26
|
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
27
27
|
data = json.load(f)
|
|
28
28
|
return Library.model_validate(data)
|
|
29
|
-
except FileNotFoundError
|
|
30
|
-
LOGGER.warning(
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
LOGGER.warning("No library file found, starting with an empty library: %s", self.filepath)
|
|
31
31
|
return Library()
|
|
32
32
|
except (json.JSONDecodeError, ValidationError) as err:
|
|
33
33
|
LOGGER.warning(
|
|
34
|
-
|
|
34
|
+
"Error deserializing library, continuing with empty library: filepath: %s, error: %s",
|
|
35
|
+
self.filepath,
|
|
36
|
+
err,
|
|
35
37
|
)
|
|
36
38
|
return Library()
|
|
37
39
|
|
|
38
40
|
def _write_library(self, library: Library) -> None:
|
|
39
41
|
directory = os.path.dirname(self.filepath) or "."
|
|
42
|
+
os.makedirs(directory, exist_ok=True)
|
|
40
43
|
temp_fd, temp_path = tempfile.mkstemp(dir=directory, prefix=".library-", suffix=".json")
|
|
41
44
|
|
|
42
45
|
try:
|
|
@@ -9,7 +9,7 @@ class DryrunPlayerAdapter(PlayerPort):
|
|
|
9
9
|
"""Adapter for dryrun player implementing PlayerPort."""
|
|
10
10
|
|
|
11
11
|
def play(self, uri: str, shuffle: bool = False) -> None:
|
|
12
|
-
LOGGER.info(
|
|
12
|
+
LOGGER.info("Dryrun: Playing `%s` with shuffle=%s", uri, shuffle)
|
|
13
13
|
|
|
14
14
|
def pause(self) -> None:
|
|
15
15
|
LOGGER.info("Dryrun: Pausing")
|
|
@@ -8,6 +8,7 @@ from soco.exceptions import SoCoException, SoCoUPnPException
|
|
|
8
8
|
from soco.plugins.sharelink import ShareLinkPlugin
|
|
9
9
|
from urllib3.exceptions import HTTPError
|
|
10
10
|
|
|
11
|
+
from jukebox.domain.errors import PlaybackError
|
|
11
12
|
from jukebox.domain.ports import PlayerPort
|
|
12
13
|
from jukebox.settings.entities import ResolvedSonosGroupRuntime
|
|
13
14
|
from jukebox.settings.errors import InvalidSettingsError
|
|
@@ -21,14 +22,17 @@ def catch_soco_upnp_exception(func):
|
|
|
21
22
|
return func(*args, **kwargs)
|
|
22
23
|
except SoCoUPnPException as err:
|
|
23
24
|
if "UPnP Error 804" in str(err.message):
|
|
24
|
-
LOGGER.warning(
|
|
25
|
+
LOGGER.warning("%s with `%s` failed, probably a bad uri: %s", func.__name__, args, err.message)
|
|
25
26
|
elif "UPnP Error 701" in str(err.message):
|
|
26
27
|
LOGGER.warning(
|
|
27
|
-
|
|
28
|
+
"%s with `%s` failed, probably a not available transition: %s",
|
|
29
|
+
func.__name__,
|
|
30
|
+
args,
|
|
31
|
+
err.message,
|
|
28
32
|
)
|
|
29
33
|
else:
|
|
30
|
-
LOGGER.
|
|
31
|
-
|
|
34
|
+
LOGGER.exception("%s with `%s` failed: %s", func.__name__, args, str(err))
|
|
35
|
+
raise PlaybackError(str(err)) from err
|
|
32
36
|
|
|
33
37
|
return wrapper
|
|
34
38
|
|
|
@@ -59,7 +63,9 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
59
63
|
raise InvalidSettingsError(f"Failed to initialize Sonos player: {err}") from err
|
|
60
64
|
|
|
61
65
|
LOGGER.info(
|
|
62
|
-
|
|
66
|
+
"Found `%s` with software version: %s",
|
|
67
|
+
self.speaker.player_name,
|
|
68
|
+
speaker_info.get("software_version", None),
|
|
63
69
|
)
|
|
64
70
|
self.sharelink = ShareLinkPlugin(self.speaker)
|
|
65
71
|
|
|
@@ -69,13 +75,14 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
69
75
|
if not discovered:
|
|
70
76
|
raise RuntimeError("No Sonos speakers found on the network")
|
|
71
77
|
speakers = sorted(discovered, key=lambda s: s.player_name)
|
|
72
|
-
LOGGER.info(
|
|
78
|
+
LOGGER.info("Discovered %d Sonos speaker(s): %s", len(speakers), [s.player_name for s in speakers])
|
|
73
79
|
if name:
|
|
74
80
|
matching = [s for s in speakers if s.player_name == name]
|
|
75
81
|
if len(matching) > 1:
|
|
76
82
|
LOGGER.warning(
|
|
77
|
-
|
|
78
|
-
"Consider using host IP to disambiguate."
|
|
83
|
+
"Multiple Sonos speakers with name '%s' found. Using first match. "
|
|
84
|
+
"Consider using host IP to disambiguate.",
|
|
85
|
+
name,
|
|
79
86
|
)
|
|
80
87
|
if matching:
|
|
81
88
|
return matching[0]
|
|
@@ -89,7 +96,7 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
89
96
|
applied_operations = []
|
|
90
97
|
|
|
91
98
|
if group.is_partial:
|
|
92
|
-
LOGGER.warning(
|
|
99
|
+
LOGGER.warning("Applying Sonos group best-effort with missing saved members: %s", group.missing_member_uids)
|
|
93
100
|
|
|
94
101
|
try:
|
|
95
102
|
for member in group.members:
|
|
@@ -102,7 +109,9 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
102
109
|
|
|
103
110
|
rollback_coordinator = self._get_rollback_coordinator_for_join(speaker)
|
|
104
111
|
LOGGER.info(
|
|
105
|
-
|
|
112
|
+
"Joining Sonos speaker `%s` to `%s` before playback",
|
|
113
|
+
speaker.player_name,
|
|
114
|
+
coordinator.player_name,
|
|
106
115
|
)
|
|
107
116
|
speaker.join(coordinator)
|
|
108
117
|
applied_operations.append(("join", speaker, rollback_coordinator))
|
|
@@ -114,7 +123,8 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
114
123
|
continue
|
|
115
124
|
|
|
116
125
|
LOGGER.info(
|
|
117
|
-
|
|
126
|
+
"Removing Sonos speaker `%s` from coordinator group before playback",
|
|
127
|
+
current_member.player_name,
|
|
118
128
|
)
|
|
119
129
|
current_member.unjoin()
|
|
120
130
|
applied_operations.append(("unjoin", current_member, None))
|
|
@@ -127,7 +137,8 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
127
137
|
try:
|
|
128
138
|
if operation == "join":
|
|
129
139
|
LOGGER.warning(
|
|
130
|
-
|
|
140
|
+
"Rolling back Sonos join for `%s` after startup group enforcement failed",
|
|
141
|
+
speaker.player_name,
|
|
131
142
|
)
|
|
132
143
|
if rollback_target is None:
|
|
133
144
|
speaker.unjoin()
|
|
@@ -135,12 +146,16 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
135
146
|
speaker.join(rollback_target)
|
|
136
147
|
else:
|
|
137
148
|
LOGGER.warning(
|
|
138
|
-
|
|
149
|
+
"Rolling back Sonos removal for `%s` after startup group enforcement failed",
|
|
150
|
+
speaker.player_name,
|
|
139
151
|
)
|
|
140
152
|
speaker.join(coordinator)
|
|
141
153
|
except Exception as err:
|
|
142
154
|
LOGGER.warning(
|
|
143
|
-
|
|
155
|
+
"Failed to roll back Sonos group change `%s` for `%s`: %s",
|
|
156
|
+
operation,
|
|
157
|
+
speaker.player_name,
|
|
158
|
+
err,
|
|
144
159
|
)
|
|
145
160
|
|
|
146
161
|
@staticmethod
|
|
@@ -173,7 +188,7 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
173
188
|
|
|
174
189
|
@catch_soco_upnp_exception
|
|
175
190
|
def play(self, uri: str, shuffle: bool = False) -> None:
|
|
176
|
-
LOGGER.info(
|
|
191
|
+
LOGGER.info("Playing `%s` on the player `%s`", uri, self.speaker.player_name)
|
|
177
192
|
self.speaker.clear_queue()
|
|
178
193
|
_ = self.handle_uri(uri)
|
|
179
194
|
self.speaker.play_mode = "SHUFFLE_NOREPEAT" if shuffle else "NORMAL"
|
|
@@ -181,17 +196,17 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
181
196
|
|
|
182
197
|
@catch_soco_upnp_exception
|
|
183
198
|
def pause(self) -> None:
|
|
184
|
-
LOGGER.info(
|
|
199
|
+
LOGGER.info("Pausing player `%s`", self.speaker.player_name)
|
|
185
200
|
self.speaker.pause()
|
|
186
201
|
|
|
187
202
|
@catch_soco_upnp_exception
|
|
188
203
|
def resume(self) -> None:
|
|
189
|
-
LOGGER.info(
|
|
204
|
+
LOGGER.info("Resuming player `%s`", self.speaker.player_name)
|
|
190
205
|
self.speaker.play()
|
|
191
206
|
|
|
192
207
|
@catch_soco_upnp_exception
|
|
193
208
|
def stop(self) -> None:
|
|
194
|
-
LOGGER.info(
|
|
209
|
+
LOGGER.info("Stopping player `%s` and clearing its queue", self.speaker.player_name)
|
|
195
210
|
self.speaker.clear_queue()
|
|
196
211
|
|
|
197
212
|
def handle_uri(self, uri):
|
|
@@ -20,7 +20,7 @@ class DryrunReaderAdapter(ReaderPort):
|
|
|
20
20
|
|
|
21
21
|
def read(self) -> Union[str, None]:
|
|
22
22
|
if self.uid is not None and self.hold_until is not None and time.monotonic() < self.hold_until:
|
|
23
|
-
LOGGER.info(
|
|
23
|
+
LOGGER.info("Reading tag %s", self.uid)
|
|
24
24
|
return self.uid
|
|
25
25
|
|
|
26
26
|
self.uid = None
|
|
@@ -47,8 +47,9 @@ class DryrunReaderAdapter(ReaderPort):
|
|
|
47
47
|
self.hold_until = time.monotonic() + duration_seconds
|
|
48
48
|
except ValueError:
|
|
49
49
|
LOGGER.warning(
|
|
50
|
-
|
|
50
|
+
"Duration parameter should be a non-negative number of seconds, received: `%s`",
|
|
51
|
+
commands[1],
|
|
51
52
|
)
|
|
52
53
|
return self.uid
|
|
53
|
-
LOGGER.warning(
|
|
54
|
+
LOGGER.warning("Invalid input, should be `tag_uid duration_seconds`, received: %s", commands)
|
|
54
55
|
return None
|
|
@@ -46,7 +46,7 @@ class Pn532ReaderAdapter(ReaderPort):
|
|
|
46
46
|
self.pn532 = PN532_SPI(debug=False, reset=spi_reset, cs=spi_cs, irq=spi_irq)
|
|
47
47
|
self.read_timeout_seconds = read_timeout_seconds
|
|
48
48
|
ic, ver, rev, support = self.pn532.get_firmware_version()
|
|
49
|
-
LOGGER.info(
|
|
49
|
+
LOGGER.info("Found PN532 with firmware version: %s.%s", ver, rev)
|
|
50
50
|
self._firmware_version: tuple[int, int] = (ver, rev)
|
|
51
51
|
self.pn532.SAM_configuration()
|
|
52
52
|
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/text_current_tag_adapter.py
RENAMED
|
@@ -56,7 +56,7 @@ class TextCurrentTagAdapter(CurrentTagRepository):
|
|
|
56
56
|
except FileNotFoundError:
|
|
57
57
|
return None
|
|
58
58
|
except OSError as err:
|
|
59
|
-
LOGGER.warning(
|
|
59
|
+
LOGGER.warning("Error reading current tag state: filepath: %s, error: %s", self.filepath, err)
|
|
60
60
|
return None
|
|
61
61
|
|
|
62
62
|
if not tag_id:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from contextlib import contextmanager
|
|
2
3
|
|
|
3
4
|
from jukebox.domain.entities import CurrentTagAction, PlaybackAction, PlaybackSession, TagEvent
|
|
5
|
+
from jukebox.domain.errors import PlaybackError
|
|
4
6
|
from jukebox.domain.ports import PlayerPort
|
|
5
7
|
from jukebox.domain.repositories import CurrentTagRepository, LibraryRepository
|
|
6
8
|
from jukebox.domain.use_cases.determine_action import DetermineAction
|
|
@@ -31,8 +33,12 @@ class HandleTagEvent:
|
|
|
31
33
|
action = self.determine_action.execute(tag_event, session)
|
|
32
34
|
|
|
33
35
|
LOGGER.debug(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
"%s \t\t %s | %s | %s | %s",
|
|
37
|
+
action.value,
|
|
38
|
+
tag_event.tag_id,
|
|
39
|
+
session.playing_tag,
|
|
40
|
+
session.paused_at,
|
|
41
|
+
session.playing_tag_removed_at,
|
|
36
42
|
)
|
|
37
43
|
|
|
38
44
|
if action == PlaybackAction.CONTINUE:
|
|
@@ -40,36 +46,42 @@ class HandleTagEvent:
|
|
|
40
46
|
session.playing_tag_removed_at = None
|
|
41
47
|
|
|
42
48
|
elif action == PlaybackAction.RESUME:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
with suppress_playback_error("Playback operation `RESUME` failed; stopping session update"):
|
|
50
|
+
self.player.resume()
|
|
51
|
+
session.paused_at = None
|
|
52
|
+
session.playing_tag_removed_at = None
|
|
46
53
|
|
|
47
54
|
elif action == PlaybackAction.PLAY:
|
|
48
|
-
LOGGER.info(
|
|
55
|
+
LOGGER.info("Found card with UID: %s", tag_event.tag_id)
|
|
49
56
|
|
|
50
57
|
disc = self.library.get_disc(tag_event.tag_id) if tag_event.tag_id is not None else None
|
|
51
58
|
if disc is not None:
|
|
52
|
-
LOGGER.info(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
LOGGER.info("Found corresponding disc: %s", disc)
|
|
60
|
+
with suppress_playback_error(
|
|
61
|
+
f"Playback operation `PLAY` failed for tag_id='{tag_event.tag_id}'; stopping session update"
|
|
62
|
+
):
|
|
63
|
+
self.player.play(disc.uri, disc.option.shuffle)
|
|
64
|
+
session.playing_tag = tag_event.tag_id
|
|
65
|
+
session.paused_at = None
|
|
66
|
+
session.playing_tag_removed_at = None
|
|
57
67
|
else:
|
|
58
|
-
LOGGER.warning(
|
|
68
|
+
LOGGER.warning("No disc found for UID: %s", tag_event.tag_id)
|
|
59
69
|
|
|
60
70
|
elif action == PlaybackAction.WAITING:
|
|
61
71
|
# Grace period - tag removed but not pausing yet
|
|
62
72
|
if session.playing_tag_removed_at is None:
|
|
63
73
|
session.playing_tag_removed_at = tag_event.timestamp
|
|
64
74
|
grace_period_elapsed = tag_event.timestamp - session.playing_tag_removed_at
|
|
65
|
-
LOGGER.debug(
|
|
75
|
+
LOGGER.debug("Grace period: %.3fs / %gs", grace_period_elapsed, self.determine_action.pause_delay)
|
|
66
76
|
|
|
67
77
|
elif action == PlaybackAction.PAUSE:
|
|
68
|
-
|
|
78
|
+
with suppress_playback_error("Playback operation `PAUSE` failed; continuing session update"):
|
|
79
|
+
self.player.pause()
|
|
69
80
|
session.paused_at = tag_event.timestamp
|
|
70
81
|
|
|
71
82
|
elif action == PlaybackAction.STOP:
|
|
72
|
-
|
|
83
|
+
with suppress_playback_error("Playback operation `STOP` failed; continuing session update"):
|
|
84
|
+
self.player.stop()
|
|
73
85
|
session.playing_tag = None
|
|
74
86
|
session.paused_at = None
|
|
75
87
|
session.playing_tag_removed_at = None
|
|
@@ -78,7 +90,7 @@ class HandleTagEvent:
|
|
|
78
90
|
pass
|
|
79
91
|
|
|
80
92
|
else:
|
|
81
|
-
LOGGER.warning(
|
|
93
|
+
LOGGER.warning("`%s` action is not implemented yet", action.value)
|
|
82
94
|
|
|
83
95
|
session.last_event_timestamp = tag_event.timestamp
|
|
84
96
|
return session
|
|
@@ -89,7 +101,9 @@ class HandleTagEvent:
|
|
|
89
101
|
self._apply_current_tag_action(action, tag_event, session)
|
|
90
102
|
except Exception as err:
|
|
91
103
|
LOGGER.warning(
|
|
92
|
-
|
|
104
|
+
"Failed to sync current tag state; continuing tag handling: tag_id=%r, error=%s",
|
|
105
|
+
tag_event.tag_id,
|
|
106
|
+
err,
|
|
93
107
|
)
|
|
94
108
|
|
|
95
109
|
def _apply_current_tag_action(
|
|
@@ -119,3 +133,11 @@ class HandleTagEvent:
|
|
|
119
133
|
|
|
120
134
|
elif action == CurrentTagAction.KEEP:
|
|
121
135
|
pass # No state changed
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@contextmanager
|
|
139
|
+
def suppress_playback_error(msg: str):
|
|
140
|
+
try:
|
|
141
|
+
yield
|
|
142
|
+
except PlaybackError:
|
|
143
|
+
LOGGER.warning(msg)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "gukebox"
|
|
3
|
-
version = "1.0.0.
|
|
3
|
+
version = "1.0.0.dev13"
|
|
4
4
|
description = "A Jukebox to play music on speakers using 'CD' with NFC tag"
|
|
5
5
|
authors = [{ name = "Gudsfile" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -67,6 +67,7 @@ extend-select = [
|
|
|
67
67
|
"SIM",
|
|
68
68
|
"I",
|
|
69
69
|
"UP032",
|
|
70
|
+
"G",
|
|
70
71
|
]
|
|
71
72
|
|
|
72
73
|
[tool.ty.src]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/current_tag_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/settings_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/interactive_cli_controller.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/json_library_adapter.py
RENAMED
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/text_current_tag_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/get_current_tag_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/sonos_discovery_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/current_tag_repository.py
RENAMED
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/library_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/determine_current_tag_action.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gukebox-1.0.0.dev11 → gukebox-1.0.0.dev13}/jukebox/settings/selected_sonos_group_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|