gukebox 1.0.0.dev12__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.
Files changed (118) hide show
  1. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/PKG-INFO +1 -1
  2. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/cli_controller.py +4 -4
  3. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_controller.py +1 -1
  4. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/settings.py +2 -0
  5. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/sonos.py +2 -2
  6. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/json_library_adapter.py +4 -2
  7. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +1 -1
  8. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/players/sonos_player_adapter.py +33 -18
  9. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +4 -3
  10. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +1 -1
  11. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/text_current_tag_adapter.py +1 -1
  12. gukebox-1.0.0.dev13/jukebox/domain/errors.py +2 -0
  13. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/handle_tag_event.py +39 -17
  14. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/pyproject.toml +2 -1
  15. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/LICENSE +0 -0
  16. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/README.md +0 -0
  17. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/__init__.py +0 -0
  18. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/__init__.py +0 -0
  19. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/__init__.py +0 -0
  20. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/__init__.py +0 -0
  21. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/current_tag_router.py +0 -0
  22. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/discs_router.py +0 -0
  23. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/models.py +0 -0
  24. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api/settings_router.py +0 -0
  25. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/api_controller.py +0 -0
  26. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/cli_display.py +0 -0
  27. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/interactive_cli_controller.py +0 -0
  28. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/__init__.py +0 -0
  29. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/inbound/ui_pages/library.py +0 -0
  30. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/__init__.py +0 -0
  31. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/json_library_adapter.py +0 -0
  32. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/adapters/outbound/text_current_tag_adapter.py +0 -0
  33. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/command_handlers.py +0 -0
  34. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/commands.py +0 -0
  35. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/di_container.py +0 -0
  36. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/__init__.py +0 -0
  37. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/entities/__init__.py +0 -0
  38. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/entities/current_tag_status.py +0 -0
  39. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/repositories/__init__.py +0 -0
  40. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/__init__.py +0 -0
  41. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/add_disc.py +0 -0
  42. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/edit_disc.py +0 -0
  43. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/get_current_tag_status.py +0 -0
  44. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/get_disc.py +0 -0
  45. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/list_discs.py +0 -0
  46. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/remove_disc.py +0 -0
  47. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/resolve_tag_id.py +0 -0
  48. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/discstore/domain/use_cases/search_discs.py +0 -0
  49. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/__init__.py +0 -0
  50. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/__init__.py +0 -0
  51. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/inbound/__init__.py +0 -0
  52. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/inbound/cli_controller.py +0 -0
  53. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/inbound/config.py +0 -0
  54. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/__init__.py +0 -0
  55. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/players/__init__.py +0 -0
  56. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/readers/__init__.py +0 -0
  57. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
  58. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/__init__.py +0 -0
  59. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/app.py +0 -0
  60. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/cli_presentation.py +0 -0
  61. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/command_handlers.py +0 -0
  62. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/commands.py +0 -0
  63. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/di_container.py +0 -0
  64. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/pn532_command_handlers.py +0 -0
  65. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/pn532_commands.py +0 -0
  66. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/services.py +0 -0
  67. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/admin/sonos_households.py +0 -0
  68. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/app.py +0 -0
  69. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/di_container.py +0 -0
  70. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/__init__.py +0 -0
  71. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/__init__.py +0 -0
  72. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/current_tag_action.py +0 -0
  73. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/disc.py +0 -0
  74. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/library.py +0 -0
  75. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/playback_action.py +0 -0
  76. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/playback_session.py +0 -0
  77. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/entities/tag_event.py +0 -0
  78. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/ports/__init__.py +0 -0
  79. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/ports/player_port.py +0 -0
  80. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/ports/reader_port.py +0 -0
  81. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/__init__.py +0 -0
  82. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/current_tag_repository.py +0 -0
  83. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/repositories/library_repository.py +0 -0
  84. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/__init__.py +0 -0
  85. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/determine_action.py +0 -0
  86. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/domain/use_cases/determine_current_tag_action.py +0 -0
  87. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/pn532/__init__.py +0 -0
  88. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/pn532/profiles.py +0 -0
  89. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/__init__.py +0 -0
  90. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/definitions.py +0 -0
  91. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/dict_utils.py +0 -0
  92. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/entities.py +0 -0
  93. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/errors.py +0 -0
  94. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/file_settings_repository.py +0 -0
  95. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/migration.py +0 -0
  96. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/repositories.py +0 -0
  97. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/resolve.py +0 -0
  98. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/runtime_resolver.py +0 -0
  99. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/runtime_validation.py +0 -0
  100. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/selected_sonos_group_repository.py +0 -0
  101. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/service_protocols.py +0 -0
  102. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/timing_validation.py +0 -0
  103. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/types.py +0 -0
  104. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/validation_rules.py +0 -0
  105. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/settings/view_utils.py +0 -0
  106. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/shared/__init__.py +0 -0
  107. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/shared/config_utils.py +0 -0
  108. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/shared/dependency_messages.py +0 -0
  109. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/shared/logger.py +0 -0
  110. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/shared/terminal_ui.py +0 -0
  111. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/shared/timing.py +0 -0
  112. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/sonos/__init__.py +0 -0
  113. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/sonos/discovery.py +0 -0
  114. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/sonos/selection.py +0 -0
  115. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/jukebox/sonos/service.py +0 -0
  116. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/pn532/__init__.py +0 -0
  117. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/pn532/pn532.py +0 -0
  118. {gukebox-1.0.0.dev12 → gukebox-1.0.0.dev13}/pn532/spi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gukebox
3
- Version: 1.0.0.dev12
3
+ Version: 1.0.0.dev13
4
4
  Summary: A Jukebox to play music on speakers using 'CD' with NFC tag
5
5
  Keywords: jukebox,music,nfc
6
6
  Author: Gudsfile
@@ -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(f"Command not implemented yet: command='{command}'")
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(f"Displaying mode not implemented yet: mode='{command.mode}'")
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(f"No discs found matching '{command.query}'")
125
+ LOGGER.info("No discs found matching '%s'", command.query)
126
126
  return
127
- LOGGER.info(f"Found {len(results)} disc(s) matching '{command.query}':")
127
+ LOGGER.info("Found %d disc(s) matching '%s':", len(results), command.query)
128
128
  display_library_table(results)
@@ -366,7 +366,7 @@ class UIController(APIController):
366
366
  return message
367
367
 
368
368
  try:
369
- speakers = self.sonos_service.list_available_speakers()
369
+ speakers = self.sonos_service.list_network_speakers()
370
370
  except Exception:
371
371
  return message
372
372
 
@@ -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.list_available_speakers()
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.list_available_speakers()
157
+ speakers = self.sonos_service.list_network_speakers()
158
158
  except SonosDiscoveryError as err:
159
159
  components.append(
160
160
  c.Error(
@@ -27,11 +27,13 @@ class JsonLibraryAdapter(LibraryRepository):
27
27
  data = json.load(f)
28
28
  return Library.model_validate(data)
29
29
  except FileNotFoundError:
30
- LOGGER.warning(f"No library file found, starting with an empty library: {self.filepath}")
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
- f"Error deserializing library, continuing with empty library: filepath: {self.filepath}, error: {err}"
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
 
@@ -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(f"Dryrun: Playing `{uri}` with shuffle={shuffle}")
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(f"{func.__name__} with `{args}` failed, probably a bad uri: {str(err.message)}")
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
- f"{func.__name__} with `{args}` failed, probably a not available transition: {str(err.message)}"
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.error(f"{func.__name__} with `{args}` failed", err)
31
- return
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
- f"Found `{self.speaker.player_name}` with software version: {speaker_info.get('software_version', None)}"
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(f"Discovered {len(speakers)} Sonos speaker(s): {[s.player_name for s in speakers]}")
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
- f"Multiple Sonos speakers with name '{name}' found. Using first match. "
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(f"Applying Sonos group best-effort with missing saved members: {group.missing_member_uids}")
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
- f"Joining Sonos speaker `{speaker.player_name}` to `{coordinator.player_name}` before playback"
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
- f"Removing Sonos speaker `{current_member.player_name}` from coordinator group before playback"
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
- f"Rolling back Sonos join for `{speaker.player_name}` after startup group enforcement failed"
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
- f"Rolling back Sonos removal for `{speaker.player_name}` after startup group enforcement failed"
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
- f"Failed to roll back Sonos group change `{operation}` for `{speaker.player_name}`: {err}"
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(f"Playing `{uri}` on the player `{self.speaker.player_name}`")
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(f"Pausing player `{self.speaker.player_name}`")
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(f"Resuming player `{self.speaker.player_name}`")
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(f"Stopping player `{self.speaker.player_name}` and clearing its queue")
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(f"Reading tag {self.uid}")
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
- f"Duration parameter should be a non-negative number of seconds, received: `{commands[1]}`"
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(f"Invalid input, should be `tag_uid duration_seconds`, received: {commands}")
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(f"Found PN532 with firmware version: {ver}.{rev}")
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
 
@@ -56,7 +56,7 @@ class TextCurrentTagAdapter(CurrentTagRepository):
56
56
  except FileNotFoundError:
57
57
  return None
58
58
  except OSError as err:
59
- LOGGER.warning(f"Error reading current tag state: filepath: {self.filepath}, error: {err}")
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:
@@ -0,0 +1,2 @@
1
+ class PlaybackError(Exception):
2
+ """Raised when a player operation fails."""
@@ -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
- f"{action.value} \t\t {tag_event.tag_id} | {session.playing_tag} | "
35
- f"{session.paused_at} | {session.playing_tag_removed_at}"
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
- self.player.resume()
44
- session.paused_at = None
45
- session.playing_tag_removed_at = None
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(f"Found card with UID: {tag_event.tag_id}")
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(f"Found corresponding disc: {disc}")
53
- session.playing_tag = tag_event.tag_id
54
- self.player.play(disc.uri, disc.option.shuffle)
55
- session.paused_at = None
56
- session.playing_tag_removed_at = None
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(f"No disc found for UID: {tag_event.tag_id}")
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(f"Grace period: {grace_period_elapsed:.3f}s / {self.determine_action.pause_delay:g}s")
75
+ LOGGER.debug("Grace period: %.3fs / %gs", grace_period_elapsed, self.determine_action.pause_delay)
66
76
 
67
77
  elif action == PlaybackAction.PAUSE:
68
- self.player.pause()
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
- self.player.stop()
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(f"`{action.value}` action is not implemented yet")
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
- f"Failed to sync current tag state; continuing tag handling: tag_id={tag_event.tag_id!r}, error={err}"
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.dev12"
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