Nice Work

This commit is contained in:
2026-01-13 19:44:02 +00:00
parent b503b372b4
commit 0718377b27
144 changed files with 15587 additions and 2132 deletions
+97 -33
View File
@@ -7,6 +7,7 @@ import asyncio
import contextlib
import os
import sys
import shutil
from pathlib import Path
from typing import Any
@@ -17,6 +18,7 @@ from core.announce import AnnounceConfig, announce_loop, pairing_poll_loop
from core.box_controller import BoxController
from core.identity import generate_box_id
from core.lifecycle import NetworkLifecycle
from core.media_sync import MediaSyncClient
from core.pairing import PairingManager
from core.state import BoxState, RuntimeState
from player.mock_player import MockPlayer
@@ -25,7 +27,6 @@ from spotify.mock import MockSpotifyBackend
from storage.box_store import BoxStore
from storage.state_store import StateStore
from storage.secret_store import SecretStore
from storage.json_storage import JsonStorage
from wifi.mock import MockWifiBackend
@@ -34,11 +35,6 @@ def build_parser() -> argparse.ArgumentParser:
prog="klangkiste-box",
description="Klangkiste Box CLI (Phase 1a, local simulation)",
)
parser.add_argument(
"--config",
default="config/box_config.json",
help="Path to local box config JSON (legacy seed)",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("status", help="Show current runtime status")
@@ -53,6 +49,7 @@ def build_parser() -> argparse.ArgumentParser:
tick_parser.add_argument("seconds", type=int, help="Seconds to advance")
subparsers.add_parser("run", help="Start interactive session + HTTP API")
subparsers.add_parser("factory-reset", help="Factory reset (delete all local data)")
return parser
@@ -81,17 +78,16 @@ def _state_from_data(
return state
def _normalize_tag_paths(tags: dict[str, Any]) -> dict[str, Any]:
normalized = {}
for uid, tag in tags.items():
if not isinstance(tag, dict):
continue
path = tag.get("path")
if isinstance(path, str) and path.startswith("../media/"):
tag = dict(tag)
tag["path"] = path.replace("../media/", "media/", 1)
normalized[uid] = tag
return normalized
def _prune_resume(state: RuntimeState, tags: dict[str, Any]) -> bool:
if not isinstance(tags, dict):
return False
valid_uids = {uid for uid, tag in tags.items() if isinstance(tag, dict)}
stale_uids = [uid for uid in state._resume.keys() if uid not in valid_uids]
if not stale_uids:
return False
for uid in stale_uids:
state._resume.pop(uid, None)
return True
def _print_status(state: RuntimeState) -> None:
@@ -119,12 +115,10 @@ def main() -> int:
parser = build_parser()
args = parser.parse_args()
config_path = Path(args.config)
if not config_path.is_absolute():
config_path = (Path(__file__).resolve().parent / config_path).resolve()
base_dir = Path(__file__).resolve().parent / "data"
base_dir.mkdir(parents=True, exist_ok=True)
(base_dir / "media").mkdir(parents=True, exist_ok=True)
media_root = Path(__file__).resolve().parent / "media"
media_root.mkdir(parents=True, exist_ok=True)
box_store = BoxStore(base_dir)
box_meta = box_store.load()
@@ -133,14 +127,12 @@ def main() -> int:
if "settings" not in box_meta:
box_meta["settings"] = {}
if "tags" not in box_meta:
legacy_storage = JsonStorage(str(config_path))
legacy_config = legacy_storage.load_config()
if isinstance(legacy_config.get("tags"), dict):
box_meta["tags"] = _normalize_tag_paths(legacy_config.get("tags", {}))
if isinstance(box_meta.get("tags"), dict):
box_meta["tags"] = _normalize_tag_paths(box_meta.get("tags", {}))
if "server_url" not in box_meta:
box_meta["server_url"] = os.getenv("KLANGKISTE_SERVER_URL", "http://127.0.0.1:7000")
box_meta["tags"] = {}
env_server_url = os.getenv("KLANGKISTE_SERVER_URL")
if env_server_url:
box_meta["server_url"] = env_server_url
elif "server_url" not in box_meta:
box_meta["server_url"] = "http://127.0.0.1:5001"
if "firmware_version" not in box_meta:
box_meta["firmware_version"] = "0.1.0"
if "capabilities" not in box_meta:
@@ -151,18 +143,27 @@ def main() -> int:
state_data = state_store.load()
state = _state_from_data(box_meta, state_data)
resume_pruned = _prune_resume(state, box_meta.get("tags", {}))
secrets = SecretStore(base_dir, state.box_id)
secrets.ensure_secret_seed()
wifi_backend = MockWifiBackend(state_store, secrets)
spotify_backend = MockSpotifyBackend(secrets)
lifecycle = NetworkLifecycle(wifi_backend)
pairing_manager = PairingManager(state_store, secrets)
media_sync = MediaSyncClient(
server_url=str(box_meta.get("server_url")),
box_id=state.box_id,
token_provider=pairing_manager.get_api_token,
)
controller = BoxController(
state=state,
player=MockPlayer(),
config=box_meta,
config_path=str(box_store.path),
media_sync=media_sync,
)
if resume_pruned:
_persist_resume(controller, state_store)
if args.command == "status":
_print_status(controller.state)
@@ -186,7 +187,16 @@ def main() -> int:
_persist_resume(controller, state_store)
return 0
if args.command == "factory-reset":
_factory_reset(controller, box_store, state_store, secrets, media_root)
print("Factory reset completed. Restart required.")
return 0
if args.command == "run":
announce_interval = _parse_int_env("KLANGKISTE_ANNOUNCE_INTERVAL", 30)
poll_interval = _parse_int_env("KLANGKISTE_POLL_INTERVAL", 10)
api_port = _parse_int_env("KLANGKISTE_API_PORT", 8000)
setup_port = _parse_int_env("KLANGKISTE_SETUP_PORT", 9000)
try:
asyncio.run(
_run_with_api(
@@ -201,6 +211,11 @@ def main() -> int:
server_url=str(box_meta.get("server_url")),
firmware_version=str(box_meta.get("firmware_version")),
capabilities=dict(box_meta.get("capabilities", {})),
api_port=api_port,
setup_port=setup_port,
media_root=str(media_root.resolve()),
announce_interval_seconds=announce_interval,
polling_interval_seconds=poll_interval,
),
)
)
@@ -218,6 +233,19 @@ def _resume_to_json(state: RuntimeState) -> dict[str, dict[str, int]]:
}
def _parse_int_env(name: str, default_value: int) -> int:
raw = os.getenv(name)
if not raw:
return default_value
try:
value = int(raw)
except ValueError:
return default_value
if value <= 0:
return default_value
return value
def _stdin_is_foreground() -> bool:
try:
if not sys.stdin.isatty():
@@ -237,6 +265,9 @@ async def _run_with_api(
secrets: SecretStore,
announce_config: AnnounceConfig,
) -> None:
base_dir = Path(__file__).resolve().parent / "data"
media_root = Path(__file__).resolve().parent / "media"
box_store = BoxStore(base_dir)
app = create_app(
controller,
lifecycle,
@@ -244,12 +275,13 @@ async def _run_with_api(
spotify_backend,
pairing_manager,
lambda: _persist_resume(controller, storage),
lambda: _factory_reset(controller, box_store, storage, secrets, media_root),
)
api_server = uvicorn.Server(
uvicorn.Config(
app,
host="0.0.0.0",
port=8000,
port=announce_config.api_port,
log_level="warning",
access_log=False,
)
@@ -259,7 +291,7 @@ async def _run_with_api(
uvicorn.Config(
setup_app,
host="0.0.0.0",
port=9000,
port=announce_config.setup_port,
log_level="warning",
access_log=False,
)
@@ -285,7 +317,17 @@ async def _run_with_api(
try:
if _stdin_is_foreground():
await _run_session(controller, storage)
await _run_session(
controller,
storage,
lambda: _factory_reset(
controller,
box_store,
storage,
secrets,
media_root,
),
)
api_server.should_exit = True
setup_server.should_exit = True
await api_task
@@ -309,7 +351,7 @@ async def _run_with_api(
async def _run_session(
controller: BoxController, storage: StateStore
controller: BoxController, storage: StateStore, factory_reset: callable
) -> None:
print("Klangkiste Box - Interactive Mode")
print("Type 'help' for commands.")
@@ -339,6 +381,10 @@ async def _run_session(
if command == "status":
_print_status(controller.state)
continue
if command == "factory-reset":
factory_reset()
print("Factory reset completed. Restart required.")
break
if command == "nfc":
if len(args) != 1:
print("error: usage: nfc <UID>")
@@ -414,6 +460,23 @@ def _persist_resume(
storage.save(data)
def _factory_reset(
controller: BoxController,
box_store: BoxStore,
state_store: StateStore,
secrets: SecretStore,
media_root: Path,
) -> None:
controller.factory_reset()
if box_store.path.exists():
box_store.path.unlink()
if state_store.path.exists():
state_store.path.unlink()
secrets.clear_all()
shutil.rmtree(media_root, ignore_errors=True)
media_root.mkdir(parents=True, exist_ok=True)
def _print_help() -> None:
print("Commands:")
print(" nfc <UID> simulate NFC tag detection")
@@ -425,6 +488,7 @@ def _print_help() -> None:
print(" vol-up | lauter increase volume")
print(" vol-down | leiser decrease volume")
print(" status show current status")
print(" factory-reset delete all local data (restart required)")
print(" help show this help")
print(" exit | quit leave interactive mode")