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
+175 -10
View File
@@ -2,11 +2,15 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import time
import shutil
from core.state import BoxState, RuntimeState
from player.base_player import BasePlayer
from core.media_sync import MediaSyncClient
import hashlib
@@ -17,11 +21,17 @@ class BoxController:
player: BasePlayer,
config: dict[str, Any],
config_path: str,
media_sync: MediaSyncClient | None = None,
) -> None:
self._state = state
self._player = player
self._config = config
self._config_path = Path(config_path)
self._media_sync = media_sync
@property
def config(self) -> dict[str, Any]:
return self._config
@property
def state(self) -> RuntimeState:
@@ -40,10 +50,24 @@ class BoxController:
print("error: server not reachable")
self._trigger_error("server_unreachable")
return None
blocked = self._config.get("blocked_tags")
if isinstance(blocked, list) and uid in blocked:
self._state.last_nfc_uid = uid
self._state.last_nfc_known = True
self._state.last_nfc_at = int(time.time())
self._trigger_error("tag_blocked")
return None
tag = self._resolve_tag(uid)
resolve_error = None
if tag is None:
tag, resolve_error = self._resolve_tag_from_server(uid)
self._state.last_nfc_uid = uid
self._state.last_nfc_known = tag is not None or resolve_error == "tag_blocked"
self._state.last_nfc_at = int(time.time())
if resolve_error == "tag_blocked":
self._trigger_error("tag_blocked")
return None
if tag is None:
print(f"error: uid '{uid}' not found in config tags")
self._trigger_error("uid_unmapped")
return None
self._state.last_error = None
@@ -51,11 +75,22 @@ class BoxController:
if not playlist:
path_value = tag.get("path", "-")
resolved = self._resolve_path(path_value)
print(
f"error: no playable files found for uid '{uid}' at path '{resolved}'"
)
self._trigger_error("no_playable_files")
return None
if self._request_media_sync(uid):
playlist = self._build_playlist(tag)
if playlist:
self._state.last_error = None
else:
print(
f"error: no playable files found for uid '{uid}' at path '{resolved}'"
)
self._trigger_error("no_playable_files")
return None
else:
print(
f"error: no playable files found for uid '{uid}' at path '{resolved}'"
)
self._trigger_error("no_playable_files")
return None
if uid in self._state._resume:
file_index, position = self._state._resume[uid]
@@ -100,11 +135,81 @@ class BoxController:
def handle_nfc_off(self, uid: str) -> None:
if self._state.state == BoxState.ERROR:
return None
if self._state.last_nfc_uid == uid:
self._state.last_nfc_uid = None
self._state.last_nfc_known = None
self._state.last_nfc_at = None
if self._state.active_uid != uid:
return None
self._store_progress(uid)
self.stop()
def set_tag(self, uid: str, media_path: str) -> None:
tags = self._config.get("tags")
if not isinstance(tags, dict):
tags = {}
self._config["tags"] = tags
tags[uid] = {"type": "folder", "path": media_path}
if uid in self._state._resume:
self._state._resume.pop(uid, None)
self._state._resume[uid] = (0, 0)
if self._state.last_nfc_uid == uid:
self._state.last_nfc_known = True
self._save_config()
if not self._build_playlist(tags[uid]):
self._request_media_sync(uid)
def _remove_tag_data(self, uid: str) -> None:
tags = self._config.get("tags")
if not isinstance(tags, dict):
return None
removed = None
if uid in tags:
removed = tags.pop(uid)
self._state._resume.pop(uid, None)
if self._state.active_uid == uid:
self.stop()
if self._state.last_nfc_uid == uid:
self._state.last_nfc_uid = None
self._state.last_nfc_known = None
self._state.last_nfc_at = None
if isinstance(removed, dict):
path_value = removed.get("path")
if isinstance(path_value, str):
media_root = (self._config_path.parent.parent / "media").resolve()
resolved = self._resolve_path(path_value)
if media_root in resolved.parents:
shutil.rmtree(resolved, ignore_errors=True)
def remove_tag(self, uid: str) -> None:
self._remove_tag_data(uid)
blocked = self._config.get("blocked_tags")
if isinstance(blocked, list) and uid in blocked:
blocked = [value for value in blocked if value != uid]
self._config["blocked_tags"] = blocked
self._save_config()
def block_tag(self, uid: str) -> None:
blocked = self._config.get("blocked_tags")
if not isinstance(blocked, list):
blocked = []
if uid not in blocked:
blocked.append(uid)
self._config["blocked_tags"] = blocked
if self._state.active_uid == uid or self._state.last_nfc_uid == uid:
self._trigger_error("tag_blocked")
self._remove_tag_data(uid)
self._save_config()
def unblock_tag(self, uid: str) -> None:
blocked = self._config.get("blocked_tags")
if not isinstance(blocked, list):
return None
if uid in blocked:
blocked = [value for value in blocked if value != uid]
self._config["blocked_tags"] = blocked
self._save_config()
def _resolve_tag(self, uid: str) -> dict[str, Any] | None:
tags = self._config.get("tags", {})
if not isinstance(tags, dict):
@@ -116,6 +221,56 @@ class BoxController:
return None
return tag
def _save_config(self) -> None:
data = dict(self._config)
self._config_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
def _request_media_sync(self, uid: str) -> bool:
if not self._media_sync:
return False
ok = self._media_sync.request_tag_media(uid)
if ok:
print(f"info: media sync requested for uid '{uid}'")
else:
print(f"error: media sync request failed for uid '{uid}'")
return ok
def _resolve_tag_from_server(self, uid: str) -> tuple[dict[str, Any] | None, str | None]:
if not self._media_sync:
return None, None
media_path, error = self._media_sync.resolve_tag(uid)
if error:
return None, error
if not media_path:
return None, None
self.set_tag(uid, f"media/{uid}")
return self._resolve_tag(uid), None
def export_tag_media(self, uid: str, target_folder: str) -> bool:
if not self._media_sync:
return False
tag = self._resolve_tag(uid)
if not tag:
return False
path_value = tag.get("path")
if not isinstance(path_value, str):
return False
root = self._resolve_path(path_value)
if not root.exists() or not root.is_dir():
return False
for file_path in root.rglob("*"):
if not file_path.is_file():
continue
rel_path = file_path.relative_to(root).as_posix()
try:
data = file_path.read_bytes()
except OSError:
return False
if not self._media_sync.upload_media_file(target_folder, rel_path, data):
return False
return True
def tick(self, seconds: int) -> None:
if self._state.state == BoxState.ERROR:
return None
@@ -186,7 +341,10 @@ class BoxController:
def _resolve_path(self, path_value: str) -> Path:
path = Path(path_value)
if not path.is_absolute():
return (self._config_path.parent / path).resolve()
media_root = self._config_path.parent.parent / "media"
if path_value.startswith("media/"):
return (media_root / path_value.replace("media/", "", 1)).resolve()
return (media_root / path).resolve()
return path
def _store_progress(self, uid: str) -> None:
@@ -199,8 +357,6 @@ class BoxController:
durations = {"01.mp3": 180, "02.mp3": 200, "03.mp3": 220}
if name in durations:
return durations[name]
if not name.lower().endswith(".mp3"):
return 0
digest = hashlib.md5(name.encode("utf-8")).hexdigest()
seed = int(digest[:8], 16)
return 120 + (seed % 241)
@@ -288,3 +444,12 @@ class BoxController:
def trigger_error(self, code: str) -> None:
self._trigger_error(code)
def factory_reset(self) -> None:
self.stop()
self._state._resume.clear()
self._state.last_error = None
self._state.last_nfc_uid = None
self._state.last_nfc_known = None
self._state.last_nfc_at = None
self._config.clear()