Files
Tomas Dvorak 6e8fedf534 first commit
2026-04-13 17:46:58 +02:00

280 lines
8.1 KiB
Python

from __future__ import annotations
import threading
from typing import Any
from swingmusic.config import UserConfig
from swingmusic.db.production import SetupStateTable, UserRootDirOwnershipTable
from swingmusic.db.userdata import UserTable
from swingmusic.lib.index import run_index_pipeline
from swingmusic.services.production_readiness import bootstrap_owner_user
_index_lock = threading.RLock()
_index_thread: threading.Thread | None = None
def _normalize_root_dirs(root_dirs: list[str] | None) -> list[str]:
if not root_dirs:
return []
cleaned = [item.strip() for item in root_dirs if item and item.strip()]
return list(dict.fromkeys(cleaned))
def _owner_user():
users = list(UserTable.get_all())
owners = [user for user in users if "owner" in user.roles]
if owners:
return owners[0]
admins = [user for user in users if "admin" in user.roles]
if admins:
return admins[0]
return users[0] if users else None
def _primary_music_dir() -> str | None:
root_dirs = UserConfig().rootDirs or []
if not root_dirs:
return None
return root_dirs[0]
def _reconcile_legacy_ready_state() -> Any:
row = SetupStateTable.ensure_singleton()
owner = _owner_user()
primary_dir = _primary_music_dir()
owner_created = owner is not None
directory_configured = bool(primary_dir)
legacy_ready = owner_created and directory_configured and row.index_state == "idle"
if legacy_ready and not row.setup_completed:
row = SetupStateTable.update_state(
{
"setup_completed": True,
"owner_userid": owner.id if owner else None,
"primary_music_dir": primary_dir,
"index_state": "completed",
"index_progress": 100.0,
"index_message": "Setup inferred from existing installation",
}
)
return row
def get_setup_status() -> dict[str, Any]:
row = _reconcile_legacy_ready_state()
users = list(UserTable.get_all())
owner = _owner_user()
primary_dir = row.primary_music_dir or _primary_music_dir()
owner_created = owner is not None
directory_configured = bool(primary_dir)
initial_index_completed = row.index_state == "completed"
setup_completed = bool(
row.setup_completed
and owner_created
and directory_configured
and initial_index_completed
)
required = not setup_completed
if not owner_created:
stage = "owner"
elif not directory_configured:
stage = "directory"
elif not initial_index_completed:
stage = "indexing"
else:
stage = "completed"
return {
"required": required,
"setup_completed": setup_completed,
"stage": stage,
"needs_owner": stage == "owner",
"needs_directory": stage == "directory",
"needs_index": stage == "indexing",
"owner_created": owner_created,
"owner_username": owner.username if owner else None,
"owner_userid": owner.id if owner else None,
"directory_configured": directory_configured,
"primary_music_dir": primary_dir,
"index_state": row.index_state,
"index_progress": float(row.index_progress or 0.0),
"index_message": row.index_message,
"initial_index_completed": initial_index_completed,
"has_users": len(users) > 0,
"user_count": len(users),
}
def _set_index_state(
state: str, progress: float, message: str, extra: dict[str, Any] | None = None
):
SetupStateTable.mark_index_progress(
state=state,
progress=progress,
message=message,
extra=extra,
)
def _run_initial_index():
try:
_set_index_state("running", 1.0, "Starting initial index")
def _progress(state: str, progress: float, message: str):
_set_index_state(state, progress, message)
run_index_pipeline(progress_callback=_progress)
status = get_setup_status()
SetupStateTable.update_state(
{
"setup_completed": bool(
status["owner_created"] and status["directory_configured"]
),
"index_state": "completed",
"index_progress": 100.0,
"index_message": "Initial index completed",
"owner_userid": status.get("owner_userid"),
"primary_music_dir": status.get("primary_music_dir"),
}
)
except Exception as error:
SetupStateTable.update_state(
{
"setup_completed": False,
"index_state": "failed",
"index_message": str(error),
}
)
finally:
global _index_thread
with _index_lock:
_index_thread = None
def trigger_initial_index(force: bool = False) -> bool:
global _index_thread
with _index_lock:
if _index_thread and _index_thread.is_alive():
return False
row = SetupStateTable.ensure_singleton()
if not force and row.index_state == "running":
return False
SetupStateTable.update_state(
{
"index_state": "queued",
"index_progress": 0.0,
"index_message": "Queued initial index",
}
)
_index_thread = threading.Thread(
target=_run_initial_index,
daemon=True,
name="swingmusic-setup-index",
)
_index_thread.start()
return True
def bootstrap_setup(
*,
username: str,
password: str,
root_dirs: list[str] | None = None,
):
existing_users = list(UserTable.get_all())
if existing_users:
raise ValueError(
"Setup bootstrap is only available before any user account exists"
)
normalized_root_dirs = _normalize_root_dirs(root_dirs)
if not normalized_root_dirs:
raise ValueError("At least one primary music directory is required")
owner = bootstrap_owner_user(
username=username,
password=password,
root_dirs=normalized_root_dirs,
)
primary_dir = (
normalized_root_dirs[0] if normalized_root_dirs else _primary_music_dir()
)
SetupStateTable.update_state(
{
"owner_userid": owner.id,
"primary_music_dir": primary_dir,
"setup_completed": False,
"index_state": "queued",
"index_progress": 0.0,
"index_message": "Bootstrap complete. Initial index queued.",
"extra": {
"onboarding_version": 1,
},
}
)
trigger_initial_index(force=True)
return owner
def configure_primary_directory(
*,
root_dirs: list[str],
) -> bool:
"""
Configure primary music directories when setup is incomplete and owner already exists.
"""
normalized_root_dirs = _normalize_root_dirs(root_dirs)
if not normalized_root_dirs:
raise ValueError("At least one primary music directory is required")
owner = _owner_user()
if not owner:
raise ValueError(
"Owner account must exist before configuring music directories"
)
config = UserConfig()
config.rootDirs = normalized_root_dirs
UserRootDirOwnershipTable.assign_paths(owner.id, normalized_root_dirs)
SetupStateTable.update_state(
{
"owner_userid": owner.id,
"primary_music_dir": normalized_root_dirs[0],
"setup_completed": False,
"index_state": "queued",
"index_progress": 0.0,
"index_message": "Primary directory configured. Initial index queued.",
"extra": {
"onboarding_version": 1,
"directory_configured_at": "setup_api",
},
}
)
return trigger_initial_index(force=True)
def is_setup_complete() -> bool:
status = get_setup_status()
return bool(status["setup_completed"])
def resume_pending_index_if_needed() -> bool:
status = get_setup_status()
if status["index_state"] in {"queued", "running"} and not status["setup_completed"]:
return trigger_initial_index(force=True)
return False