gukebox 1.3.2.dev1__tar.gz → 1.4.0.dev1__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.3.2.dev1 → gukebox-1.4.0.dev1}/PKG-INFO +1 -1
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/current_tag_router.py +1 -5
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/discs_router.py +1 -5
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api_controller.py +1 -6
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/cli_controller.py +1 -7
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/interactive_cli_controller.py +1 -5
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/ui_controller.py +1 -6
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/ui_pages/library.py +1 -3
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/cli_controller.py +8 -4
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/players/sonos_player_adapter.py +5 -2
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/cli_presentation.py +38 -30
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/command_handlers.py +1 -1
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/di_container.py +10 -8
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/app.py +2 -1
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/di_container.py +15 -10
- gukebox-1.4.0.dev1/jukebox/domain/entities/__init__.py +49 -0
- gukebox-1.4.0.dev1/jukebox/domain/entities/current_tag_state.py +32 -0
- gukebox-1.4.0.dev1/jukebox/domain/entities/playback_state.py +69 -0
- gukebox-1.4.0.dev1/jukebox/domain/use_cases/__init__.py +23 -0
- gukebox-1.4.0.dev1/jukebox/domain/use_cases/handle_tag_event.py +140 -0
- gukebox-1.4.0.dev1/jukebox/domain/use_cases/sync_current_tag.py +35 -0
- gukebox-1.4.0.dev1/jukebox/domain/use_cases/transition_current_tag.py +54 -0
- gukebox-1.4.0.dev1/jukebox/domain/use_cases/transition_playback.py +74 -0
- gukebox-1.4.0.dev1/jukebox/settings/errors.py +34 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/file_settings_repository.py +15 -4
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/migration.py +3 -3
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/resolve.py +57 -17
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/runtime_resolver.py +5 -2
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/pyproject.toml +1 -1
- gukebox-1.3.2.dev1/jukebox/domain/entities/__init__.py +0 -20
- gukebox-1.3.2.dev1/jukebox/domain/entities/current_tag_action.py +0 -11
- gukebox-1.3.2.dev1/jukebox/domain/entities/playback_action.py +0 -13
- gukebox-1.3.2.dev1/jukebox/domain/entities/playback_session.py +0 -48
- gukebox-1.3.2.dev1/jukebox/domain/use_cases/__init__.py +0 -5
- gukebox-1.3.2.dev1/jukebox/domain/use_cases/determine_action.py +0 -38
- gukebox-1.3.2.dev1/jukebox/domain/use_cases/determine_current_tag_action.py +0 -35
- gukebox-1.3.2.dev1/jukebox/domain/use_cases/handle_tag_event.py +0 -253
- gukebox-1.3.2.dev1/jukebox/settings/errors.py +0 -14
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/LICENSE +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/README.md +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/models.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/settings_router.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/cli_display.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/ui_pages/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/ui_pages/settings.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/ui_pages/sonos.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/json_library_adapter.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/players/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/players/dryrun_player_adapter.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/readers/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/readers/dryrun_reader_adapter.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/readers/pn532_reader_adapter.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/sonos_discovery_adapter.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/text_current_tag_adapter.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/app.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/commands.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/library_command_handlers.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/library_commands.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/pn532_command_handlers.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/pn532_commands.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/services.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/admin/sonos_households.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/entities/current_tag_status.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/entities/disc.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/entities/library.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/entities/tag_event.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/errors.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/ports/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/ports/player_port.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/ports/reader_port.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/repositories/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/repositories/current_tag_repository.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/repositories/library_repository.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/add_disc.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/edit_disc.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/get_current_tag_status.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/get_disc.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/list_discs.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/remove_disc.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/resolve_tag_id.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/domain/use_cases/library/search_discs.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/pn532/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/pn532/profiles.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/definitions.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/dict_utils.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/entities.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/repositories.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/runtime_validation.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/selected_sonos_group_repository.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/service_protocols.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/timing_validation.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/types.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/validation_rules.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/settings/view_utils.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/shared/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/shared/config_utils.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/shared/errors.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/shared/logger.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/shared/terminal_ui.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/shared/timing.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/sonos/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/sonos/discovery.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/sonos/selection.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/sonos/service.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/pn532/__init__.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/pn532/pn532.py +0 -0
- {gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/pn532/spi.py +0 -0
{gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/current_tag_router.py
RENAMED
|
@@ -11,11 +11,7 @@ from jukebox.adapters.inbound.admin.api.models import (
|
|
|
11
11
|
DiscPatchInput,
|
|
12
12
|
)
|
|
13
13
|
from jukebox.domain.entities import CurrentTagStatus, Disc, DiscMetadata, DiscOption
|
|
14
|
-
from jukebox.domain.use_cases
|
|
15
|
-
from jukebox.domain.use_cases.library.edit_disc import EditDisc
|
|
16
|
-
from jukebox.domain.use_cases.library.get_current_tag_status import GetCurrentTagStatus
|
|
17
|
-
from jukebox.domain.use_cases.library.get_disc import GetDisc
|
|
18
|
-
from jukebox.domain.use_cases.library.remove_disc import RemoveDisc
|
|
14
|
+
from jukebox.domain.use_cases import AddDisc, EditDisc, GetCurrentTagStatus, GetDisc, RemoveDisc
|
|
19
15
|
|
|
20
16
|
|
|
21
17
|
def build_current_tag_router(
|
{gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/api/discs_router.py
RENAMED
|
@@ -3,11 +3,7 @@ from pydantic import ValidationError
|
|
|
3
3
|
|
|
4
4
|
from jukebox.adapters.inbound.admin.api.models import DiscInput, DiscOutput, DiscPatchInput
|
|
5
5
|
from jukebox.domain.entities import Disc, DiscMetadata, DiscOption
|
|
6
|
-
from jukebox.domain.use_cases
|
|
7
|
-
from jukebox.domain.use_cases.library.edit_disc import EditDisc
|
|
8
|
-
from jukebox.domain.use_cases.library.get_disc import GetDisc
|
|
9
|
-
from jukebox.domain.use_cases.library.list_discs import ListDiscs
|
|
10
|
-
from jukebox.domain.use_cases.library.remove_disc import RemoveDisc
|
|
6
|
+
from jukebox.domain.use_cases import AddDisc, EditDisc, GetDisc, ListDiscs, RemoveDisc
|
|
11
7
|
|
|
12
8
|
|
|
13
9
|
def build_discs_router(
|
|
@@ -21,12 +21,7 @@ except ModuleNotFoundError as e:
|
|
|
21
21
|
if e.name != "fastapi":
|
|
22
22
|
raise
|
|
23
23
|
raise MissingOptionalDependencyError("The `api_controller` module", "api", "jukebox-admin api") from e
|
|
24
|
-
from jukebox.domain.use_cases
|
|
25
|
-
from jukebox.domain.use_cases.library.edit_disc import EditDisc
|
|
26
|
-
from jukebox.domain.use_cases.library.get_current_tag_status import GetCurrentTagStatus
|
|
27
|
-
from jukebox.domain.use_cases.library.get_disc import GetDisc
|
|
28
|
-
from jukebox.domain.use_cases.library.list_discs import ListDiscs
|
|
29
|
-
from jukebox.domain.use_cases.library.remove_disc import RemoveDisc
|
|
24
|
+
from jukebox.domain.use_cases import AddDisc, EditDisc, GetCurrentTagStatus, GetDisc, ListDiscs, RemoveDisc
|
|
30
25
|
from jukebox.settings.entities import SelectedSonosGroupSettings
|
|
31
26
|
from jukebox.settings.selected_sonos_group_repository import SettingsSelectedSonosGroupRepository
|
|
32
27
|
from jukebox.settings.service_protocols import SettingsService
|
|
@@ -10,13 +10,7 @@ from jukebox.admin.library_commands import (
|
|
|
10
10
|
CliSearchCommand,
|
|
11
11
|
)
|
|
12
12
|
from jukebox.domain.entities import Disc, DiscMetadata, DiscOption
|
|
13
|
-
from jukebox.domain.use_cases
|
|
14
|
-
from jukebox.domain.use_cases.library.edit_disc import EditDisc
|
|
15
|
-
from jukebox.domain.use_cases.library.get_disc import GetDisc
|
|
16
|
-
from jukebox.domain.use_cases.library.list_discs import ListDiscs
|
|
17
|
-
from jukebox.domain.use_cases.library.remove_disc import RemoveDisc
|
|
18
|
-
from jukebox.domain.use_cases.library.resolve_tag_id import ResolveTagId
|
|
19
|
-
from jukebox.domain.use_cases.library.search_discs import SearchDiscs
|
|
13
|
+
from jukebox.domain.use_cases import AddDisc, EditDisc, GetDisc, ListDiscs, RemoveDisc, ResolveTagId, SearchDiscs
|
|
20
14
|
|
|
21
15
|
|
|
22
16
|
class CLIController:
|
|
@@ -2,11 +2,7 @@ import typer
|
|
|
2
2
|
|
|
3
3
|
from jukebox.adapters.inbound.admin.cli_display import display_library_line, display_library_table
|
|
4
4
|
from jukebox.domain.entities import CurrentTagStatus, Disc, DiscMetadata, DiscOption
|
|
5
|
-
from jukebox.domain.use_cases
|
|
6
|
-
from jukebox.domain.use_cases.library.edit_disc import EditDisc
|
|
7
|
-
from jukebox.domain.use_cases.library.get_current_tag_status import GetCurrentTagStatus
|
|
8
|
-
from jukebox.domain.use_cases.library.list_discs import ListDiscs
|
|
9
|
-
from jukebox.domain.use_cases.library.remove_disc import RemoveDisc
|
|
5
|
+
from jukebox.domain.use_cases import AddDisc, EditDisc, GetCurrentTagStatus, ListDiscs, RemoveDisc
|
|
10
6
|
|
|
11
7
|
|
|
12
8
|
class InteractiveCLIController:
|
|
@@ -19,12 +19,7 @@ from jukebox.adapters.inbound.admin.ui_pages.library import DiscForm, LibraryUIP
|
|
|
19
19
|
from jukebox.adapters.inbound.admin.ui_pages.settings import SettingsUIPageBuilder
|
|
20
20
|
from jukebox.adapters.inbound.admin.ui_pages.sonos import SonosSelectionForm, SonosUIPageBuilder
|
|
21
21
|
from jukebox.domain.entities import Disc, DiscMetadata, DiscOption
|
|
22
|
-
from jukebox.domain.use_cases
|
|
23
|
-
from jukebox.domain.use_cases.library.edit_disc import EditDisc
|
|
24
|
-
from jukebox.domain.use_cases.library.get_current_tag_status import GetCurrentTagStatus
|
|
25
|
-
from jukebox.domain.use_cases.library.get_disc import GetDisc
|
|
26
|
-
from jukebox.domain.use_cases.library.list_discs import ListDiscs
|
|
27
|
-
from jukebox.domain.use_cases.library.remove_disc import RemoveDisc
|
|
22
|
+
from jukebox.domain.use_cases import AddDisc, EditDisc, GetCurrentTagStatus, GetDisc, ListDiscs, RemoveDisc
|
|
28
23
|
from jukebox.settings.definitions import get_setting_definition
|
|
29
24
|
from jukebox.settings.errors import SettingsError
|
|
30
25
|
from jukebox.settings.selected_sonos_group_repository import SettingsSelectedSonosGroupRepository
|
{gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/inbound/admin/ui_pages/library.py
RENAMED
|
@@ -9,9 +9,7 @@ from fastui.events import BackEvent, GoToEvent, PageEvent
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
11
|
from jukebox.domain.entities import CurrentTagStatus, DiscMetadata, DiscOption
|
|
12
|
-
from jukebox.domain.use_cases
|
|
13
|
-
from jukebox.domain.use_cases.library.get_disc import GetDisc
|
|
14
|
-
from jukebox.domain.use_cases.library.list_discs import ListDiscs
|
|
12
|
+
from jukebox.domain.use_cases import GetCurrentTagStatus, GetDisc, ListDiscs
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class DiscTable(DiscMetadata, DiscOption):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from time import sleep
|
|
3
3
|
|
|
4
|
-
from jukebox.domain.entities import
|
|
4
|
+
from jukebox.domain.entities import CurrentTagState, Idle, NoTag, PlaybackState, TagEvent
|
|
5
5
|
from jukebox.domain.ports import ReaderPort
|
|
6
|
-
from jukebox.domain.use_cases
|
|
6
|
+
from jukebox.domain.use_cases import HandleTagEvent, SyncCurrentTag
|
|
7
7
|
from jukebox.shared.timing import DEFAULT_LOOP_INTERVAL_SECONDS
|
|
8
8
|
|
|
9
9
|
|
|
@@ -14,21 +14,25 @@ class CLIController:
|
|
|
14
14
|
self,
|
|
15
15
|
reader: ReaderPort,
|
|
16
16
|
handle_tag_event: HandleTagEvent,
|
|
17
|
+
sync_current_tag: SyncCurrentTag,
|
|
17
18
|
loop_interval_seconds: float = DEFAULT_LOOP_INTERVAL_SECONDS,
|
|
18
19
|
):
|
|
19
20
|
self.reader = reader
|
|
20
21
|
self.handle_tag_event = handle_tag_event
|
|
22
|
+
self.sync_current_tag = sync_current_tag
|
|
21
23
|
self.loop_interval_seconds = loop_interval_seconds
|
|
22
24
|
|
|
23
25
|
def run(self):
|
|
24
26
|
"""Run the main event loop."""
|
|
25
|
-
|
|
27
|
+
state: PlaybackState = Idle()
|
|
28
|
+
current_tag_state: CurrentTagState = NoTag()
|
|
26
29
|
|
|
27
30
|
while True:
|
|
28
31
|
loop_started = time.monotonic()
|
|
29
32
|
tag_id = self.reader.read()
|
|
30
33
|
tag_event = TagEvent(tag_id=tag_id, timestamp=time.monotonic())
|
|
31
|
-
|
|
34
|
+
current_tag_state = self.sync_current_tag.execute(tag_event, current_tag_state)
|
|
35
|
+
state = self.handle_tag_event.execute(tag_event, state)
|
|
32
36
|
remaining_sleep = self.loop_interval_seconds - (time.monotonic() - loop_started)
|
|
33
37
|
if remaining_sleep > 0:
|
|
34
38
|
sleep(remaining_sleep)
|
{gukebox-1.3.2.dev1 → gukebox-1.4.0.dev1}/jukebox/adapters/outbound/players/sonos_player_adapter.py
RENAMED
|
@@ -11,7 +11,7 @@ from urllib3.exceptions import HTTPError
|
|
|
11
11
|
from jukebox.domain.errors import PlaybackError
|
|
12
12
|
from jukebox.domain.ports import PlayerPort
|
|
13
13
|
from jukebox.settings.entities import ResolvedSonosGroupRuntime
|
|
14
|
-
from jukebox.settings.errors import InvalidSettingsError
|
|
14
|
+
from jukebox.settings.errors import ErrorCode, InvalidSettingsError
|
|
15
15
|
from jukebox.sonos.service import (
|
|
16
16
|
SonosPlaybackTarget,
|
|
17
17
|
SonosPlaybackTargetResolver,
|
|
@@ -66,7 +66,10 @@ class SonosPlayerAdapter(PlayerPort):
|
|
|
66
66
|
|
|
67
67
|
speaker_info = self._refresh_speaker_metadata()
|
|
68
68
|
except (HTTPError, OSError, RequestException, RuntimeError, SoCoException, SoCoUPnPException) as err:
|
|
69
|
-
raise InvalidSettingsError(
|
|
69
|
+
raise InvalidSettingsError(
|
|
70
|
+
f"Failed to initialize Sonos player: {err}",
|
|
71
|
+
code=ErrorCode.INVALID_EFFECTIVE,
|
|
72
|
+
) from err
|
|
70
73
|
|
|
71
74
|
LOGGER.info(
|
|
72
75
|
"Found `%s` with software version: %s",
|
|
@@ -3,8 +3,11 @@ import re
|
|
|
3
3
|
from collections.abc import Iterable, Mapping
|
|
4
4
|
from typing import cast
|
|
5
5
|
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
6
8
|
from jukebox.settings.definitions import SETTINGS, get_setting_definition, is_editable_setting_path
|
|
7
9
|
from jukebox.settings.errors import (
|
|
10
|
+
ErrorCode,
|
|
8
11
|
InvalidSettingsError,
|
|
9
12
|
MalformedSettingsFileError,
|
|
10
13
|
SettingsError,
|
|
@@ -385,40 +388,45 @@ def _render_cli_error_message(err: BaseException) -> str:
|
|
|
385
388
|
|
|
386
389
|
|
|
387
390
|
def _render_invalid_settings_error(err: InvalidSettingsError) -> str:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return f"
|
|
391
|
+
match err.code:
|
|
392
|
+
case ErrorCode.UNSUPPORTED_PATH:
|
|
393
|
+
if err.path is not None:
|
|
394
|
+
return (
|
|
395
|
+
f"Unsupported settings path: '{err.path}'. Use `jukebox-admin settings show --effective --json` "
|
|
396
|
+
"to inspect supported editable paths."
|
|
397
|
+
)
|
|
398
|
+
return "Unsupported settings path."
|
|
399
|
+
case ErrorCode.INVALID_JSON_VALUE:
|
|
400
|
+
return f"Invalid value for '{err.path or 'setting'}'. Pass a JSON object or `null`."
|
|
401
|
+
case ErrorCode.INVALID_JSON_TYPE:
|
|
402
|
+
return f"Invalid value for '{err.path or 'setting'}'. Expected a JSON object or `null`."
|
|
403
|
+
case ErrorCode.INVALID_UPDATE:
|
|
404
|
+
return f"Settings update rejected: {_extract_error_detail(err)}"
|
|
405
|
+
case ErrorCode.INVALID_FILE:
|
|
406
|
+
detail = _extract_error_detail(err)
|
|
407
|
+
if err.path is not None:
|
|
408
|
+
return f"Persisted settings are invalid at '{err.path}': {detail}"
|
|
409
|
+
return f"Persisted settings are invalid: {detail}"
|
|
410
|
+
case ErrorCode.INVALID_EFFECTIVE:
|
|
411
|
+
return f"Effective settings are invalid: {_extract_error_detail(err)}"
|
|
412
|
+
case ErrorCode.UNKNOWN_PATH:
|
|
413
|
+
return str(err)
|
|
414
|
+
case _:
|
|
415
|
+
return str(err)
|
|
407
416
|
|
|
408
|
-
if message.startswith("Invalid settings update:"):
|
|
409
|
-
return f"Settings update rejected: {_extract_compact_detail(message)}"
|
|
410
417
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return f"Persisted settings are invalid at '{filepath}': {detail}"
|
|
416
|
-
return f"Persisted settings are invalid: {detail}"
|
|
418
|
+
def _extract_error_detail(err: InvalidSettingsError) -> str:
|
|
419
|
+
if isinstance(err.__cause__, ValidationError):
|
|
420
|
+
return _format_validation_errors(err.__cause__)
|
|
421
|
+
return _extract_compact_detail(str(err))
|
|
417
422
|
|
|
418
|
-
if message.startswith("Invalid effective settings"):
|
|
419
|
-
return f"Effective settings are invalid: {_extract_compact_detail(message)}"
|
|
420
423
|
|
|
421
|
-
|
|
424
|
+
def _format_validation_errors(cause: ValidationError) -> str:
|
|
425
|
+
parts = []
|
|
426
|
+
for error in cause.errors(include_url=False):
|
|
427
|
+
location = ".".join(str(loc) for loc in error["loc"])
|
|
428
|
+
parts.append(f"{location}: {error['msg']}" if location else error["msg"])
|
|
429
|
+
return "; ".join(parts)
|
|
422
430
|
|
|
423
431
|
|
|
424
432
|
def _extract_compact_detail(message: str) -> str:
|
|
@@ -58,7 +58,7 @@ def _build_server_app(
|
|
|
58
58
|
try:
|
|
59
59
|
return build_app(library_path, services)
|
|
60
60
|
except ModuleNotFoundError as err:
|
|
61
|
-
if err.name in {"fastapi", "fastui"}
|
|
61
|
+
if err.name in {"fastapi", "fastui"}:
|
|
62
62
|
_raise_optional_extra_error(command_name, extra_name, source_command, err)
|
|
63
63
|
raise
|
|
64
64
|
|
|
@@ -3,14 +3,16 @@ from typing import cast
|
|
|
3
3
|
from jukebox.adapters.outbound.json_library_adapter import JsonLibraryAdapter
|
|
4
4
|
from jukebox.adapters.outbound.sonos_discovery_adapter import SoCoSonosDiscoveryAdapter
|
|
5
5
|
from jukebox.adapters.outbound.text_current_tag_adapter import TextCurrentTagAdapter
|
|
6
|
-
from jukebox.domain.use_cases
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
from jukebox.domain.use_cases import (
|
|
7
|
+
AddDisc,
|
|
8
|
+
EditDisc,
|
|
9
|
+
GetCurrentTagStatus,
|
|
10
|
+
GetDisc,
|
|
11
|
+
ListDiscs,
|
|
12
|
+
RemoveDisc,
|
|
13
|
+
ResolveTagId,
|
|
14
|
+
SearchDiscs,
|
|
15
|
+
)
|
|
14
16
|
from jukebox.settings.file_settings_repository import FileSettingsRepository
|
|
15
17
|
from jukebox.settings.resolve import SettingsService as SettingsServiceImpl
|
|
16
18
|
from jukebox.settings.resolve import build_environment_settings_overrides
|
|
@@ -115,13 +115,14 @@ def _run(state: JukeboxCliState) -> None:
|
|
|
115
115
|
settings_service = build_settings_service(**{k: v for k, v in asdict(state).items() if k != "verbose"})
|
|
116
116
|
runtime_resolver = build_runtime_resolver(settings_service)
|
|
117
117
|
runtime_config = runtime_resolver.resolve(verbose=state.verbose)
|
|
118
|
-
reader, handle_tag_event = build_jukebox(runtime_config)
|
|
118
|
+
reader, handle_tag_event, sync_current_tag = build_jukebox(runtime_config)
|
|
119
119
|
except SettingsError as err:
|
|
120
120
|
_exit_error(str(err))
|
|
121
121
|
|
|
122
122
|
controller = CLIController(
|
|
123
123
|
reader=reader,
|
|
124
124
|
handle_tag_event=handle_tag_event,
|
|
125
|
+
sync_current_tag=sync_current_tag,
|
|
125
126
|
loop_interval_seconds=runtime_config.loop_interval_seconds,
|
|
126
127
|
)
|
|
127
128
|
controller.run()
|
|
@@ -6,9 +6,13 @@ from jukebox.adapters.outbound.players.sonos_player_adapter import SonosPlayerAd
|
|
|
6
6
|
from jukebox.adapters.outbound.readers.dryrun_reader_adapter import DryrunReaderAdapter
|
|
7
7
|
from jukebox.adapters.outbound.sonos_discovery_adapter import SoCoSonosDiscoveryAdapter
|
|
8
8
|
from jukebox.adapters.outbound.text_current_tag_adapter import TextCurrentTagAdapter
|
|
9
|
-
from jukebox.domain.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
from jukebox.domain.entities import (
|
|
10
|
+
CURRENT_TAG_ABSENCE_GRACE_SECONDS,
|
|
11
|
+
PLAYBACK_RETRY_DELAYS_SECONDS,
|
|
12
|
+
CurrentTagContext,
|
|
13
|
+
TransitionContext,
|
|
14
|
+
)
|
|
15
|
+
from jukebox.domain.use_cases import HandleTagEvent, SyncCurrentTag
|
|
12
16
|
from jukebox.settings.entities import ResolvedJukeboxRuntimeConfig
|
|
13
17
|
from jukebox.settings.file_settings_repository import FileSettingsRepository
|
|
14
18
|
from jukebox.settings.resolve import SettingsService as SettingsServiceImpl
|
|
@@ -134,21 +138,22 @@ def build_jukebox(
|
|
|
134
138
|
case _:
|
|
135
139
|
raise ValueError(f"Unknown reader type: {config.reader_type}")
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
ctx = TransitionContext(
|
|
138
142
|
pause_delay=config.pause_delay_seconds,
|
|
139
143
|
max_pause_duration=config.pause_duration_seconds,
|
|
144
|
+
retry_delays=PLAYBACK_RETRY_DELAYS_SECONDS,
|
|
145
|
+
)
|
|
146
|
+
sync_current_tag = SyncCurrentTag(
|
|
147
|
+
repository=current_tag_repository,
|
|
148
|
+
ctx=CurrentTagContext(grace_seconds=CURRENT_TAG_ABSENCE_GRACE_SECONDS),
|
|
140
149
|
)
|
|
141
|
-
determine_current_tag_action = DetermineCurrentTagAction()
|
|
142
|
-
|
|
143
150
|
handle_tag_event = HandleTagEvent(
|
|
144
151
|
player=player,
|
|
145
152
|
library=library,
|
|
146
|
-
|
|
147
|
-
determine_action=determine_action,
|
|
148
|
-
determine_current_tag_action=determine_current_tag_action,
|
|
153
|
+
ctx=ctx,
|
|
149
154
|
)
|
|
150
155
|
|
|
151
|
-
return reader, handle_tag_event
|
|
156
|
+
return reader, handle_tag_event, sync_current_tag
|
|
152
157
|
|
|
153
158
|
|
|
154
159
|
def build_sonos_playback_target_resolver() -> SonosPlaybackTargetResolver:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from .current_tag_state import (
|
|
2
|
+
CURRENT_TAG_ABSENCE_GRACE_SECONDS,
|
|
3
|
+
CurrentTagCommand,
|
|
4
|
+
CurrentTagContext,
|
|
5
|
+
CurrentTagState,
|
|
6
|
+
NoTag,
|
|
7
|
+
TagPresent,
|
|
8
|
+
TagRemoved,
|
|
9
|
+
)
|
|
10
|
+
from .current_tag_status import CurrentTagStatus
|
|
11
|
+
from .disc import Disc, DiscMetadata, DiscOption
|
|
12
|
+
from .library import Library
|
|
13
|
+
from .playback_state import (
|
|
14
|
+
PLAYBACK_RETRY_DELAYS_SECONDS,
|
|
15
|
+
Idle,
|
|
16
|
+
Paused,
|
|
17
|
+
PlaybackCommand,
|
|
18
|
+
PlaybackState,
|
|
19
|
+
Playing,
|
|
20
|
+
RetryState,
|
|
21
|
+
TransitionContext,
|
|
22
|
+
Waiting,
|
|
23
|
+
)
|
|
24
|
+
from .tag_event import TagEvent
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CURRENT_TAG_ABSENCE_GRACE_SECONDS",
|
|
28
|
+
"CurrentTagCommand",
|
|
29
|
+
"CurrentTagContext",
|
|
30
|
+
"CurrentTagState",
|
|
31
|
+
"CurrentTagStatus",
|
|
32
|
+
"Idle",
|
|
33
|
+
"NoTag",
|
|
34
|
+
"TagPresent",
|
|
35
|
+
"TagRemoved",
|
|
36
|
+
"Playing",
|
|
37
|
+
"Waiting",
|
|
38
|
+
"Paused",
|
|
39
|
+
"PLAYBACK_RETRY_DELAYS_SECONDS",
|
|
40
|
+
"PlaybackCommand",
|
|
41
|
+
"PlaybackState",
|
|
42
|
+
"RetryState",
|
|
43
|
+
"TransitionContext",
|
|
44
|
+
"TagEvent",
|
|
45
|
+
"Library",
|
|
46
|
+
"Disc",
|
|
47
|
+
"DiscMetadata",
|
|
48
|
+
"DiscOption",
|
|
49
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
CurrentTagCommand = Literal["set", "clear"]
|
|
5
|
+
|
|
6
|
+
CURRENT_TAG_ABSENCE_GRACE_SECONDS: float = 1.0
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class NoTag:
|
|
11
|
+
last_event_timestamp: float | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TagPresent:
|
|
16
|
+
tag: str
|
|
17
|
+
last_event_timestamp: float | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class TagRemoved:
|
|
22
|
+
tag: str
|
|
23
|
+
removed_at: float
|
|
24
|
+
last_event_timestamp: float | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CurrentTagState = NoTag | TagPresent | TagRemoved
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class CurrentTagContext:
|
|
32
|
+
grace_seconds: float
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
PlaybackCommand = Literal["play", "pause", "resume", "stop"]
|
|
5
|
+
|
|
6
|
+
PLAYBACK_RETRY_DELAYS_SECONDS: tuple[float, ...] = (0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class RetryState:
|
|
11
|
+
"""Tracks retry timing for a failed playback command."""
|
|
12
|
+
|
|
13
|
+
action: PlaybackCommand
|
|
14
|
+
tag_id: str | None
|
|
15
|
+
first_failed_at: float
|
|
16
|
+
last_failed_at: float
|
|
17
|
+
attempt_count: int
|
|
18
|
+
next_retry_at: float | None
|
|
19
|
+
exhausted: bool = False
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if self.action == "play" and self.tag_id is None:
|
|
23
|
+
raise ValueError("RetryState action='play' requires tag_id")
|
|
24
|
+
if self.action != "play" and self.tag_id is not None:
|
|
25
|
+
raise ValueError(f"RetryState action='{self.action}' must not have tag_id")
|
|
26
|
+
|
|
27
|
+
def matches(self, *, action: PlaybackCommand, tag_id: str | None = None) -> bool:
|
|
28
|
+
return self.action == action and self.tag_id == tag_id
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Idle:
|
|
33
|
+
retry: RetryState | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Playing:
|
|
38
|
+
tag: str
|
|
39
|
+
retry: RetryState | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Waiting:
|
|
44
|
+
tag: str
|
|
45
|
+
removed_at: float
|
|
46
|
+
retry: RetryState | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Paused:
|
|
51
|
+
tag: str
|
|
52
|
+
paused_at: float
|
|
53
|
+
retry: RetryState | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
PlaybackState = Idle | Playing | Waiting | Paused
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class TransitionContext:
|
|
61
|
+
"""Configuration bundle for the playback state transition function."""
|
|
62
|
+
|
|
63
|
+
pause_delay: float
|
|
64
|
+
max_pause_duration: float
|
|
65
|
+
retry_delays: tuple[float, ...]
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
if not self.retry_delays:
|
|
69
|
+
raise ValueError("retry_delays must not be empty")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .handle_tag_event import HandleTagEvent
|
|
2
|
+
from .library.add_disc import AddDisc
|
|
3
|
+
from .library.edit_disc import EditDisc
|
|
4
|
+
from .library.get_current_tag_status import GetCurrentTagStatus
|
|
5
|
+
from .library.get_disc import GetDisc
|
|
6
|
+
from .library.list_discs import ListDiscs
|
|
7
|
+
from .library.remove_disc import RemoveDisc
|
|
8
|
+
from .library.resolve_tag_id import ResolveTagId
|
|
9
|
+
from .library.search_discs import SearchDiscs
|
|
10
|
+
from .sync_current_tag import SyncCurrentTag
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"HandleTagEvent",
|
|
14
|
+
"AddDisc",
|
|
15
|
+
"EditDisc",
|
|
16
|
+
"GetCurrentTagStatus",
|
|
17
|
+
"GetDisc",
|
|
18
|
+
"ListDiscs",
|
|
19
|
+
"RemoveDisc",
|
|
20
|
+
"ResolveTagId",
|
|
21
|
+
"SearchDiscs",
|
|
22
|
+
"SyncCurrentTag",
|
|
23
|
+
]
|