mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 12:33:03 +00:00
186 lines
5.5 KiB
Python
186 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from typing import Any
|
|
|
|
from swingmusic.config import UserConfig
|
|
from swingmusic.db.production import InviteTokenTable, UserRootDirOwnershipTable
|
|
from swingmusic.db.userdata import PluginTable, UserTable
|
|
from swingmusic.services.library_projection import get_owner_user, sync_owner_projection
|
|
from swingmusic.store.homepage import HomepageStore
|
|
from swingmusic.utils.auth import hash_password
|
|
|
|
|
|
def get_bootstrap_status() -> dict[str, Any]:
|
|
users = list(UserTable.get_all())
|
|
owner = next((u for u in users if "owner" in u.roles), None)
|
|
|
|
return {
|
|
"required": len(users) == 0,
|
|
"has_users": len(users) > 0,
|
|
"user_count": len(users),
|
|
"owner_exists": owner is not None,
|
|
"owner_username": owner.username if owner else None,
|
|
}
|
|
|
|
|
|
def _normalize_root_dirs(root_dirs: list[str] | None) -> list[str] | None:
|
|
if root_dirs is None:
|
|
return None
|
|
|
|
cleaned = [item.strip() for item in root_dirs if item and item.strip()]
|
|
return list(dict.fromkeys(cleaned))
|
|
|
|
|
|
def default_user_root_dir(username: str) -> str:
|
|
config = UserConfig()
|
|
if config.rootDirs:
|
|
base = config.rootDirs[0]
|
|
if base == "$home":
|
|
root = os.path.join(os.path.expanduser("~"), "Music")
|
|
else:
|
|
root = os.path.expanduser(base)
|
|
else:
|
|
root = os.path.join(os.path.expanduser("~"), "Music")
|
|
|
|
safe_username = re.sub(r"[^\w\-. ]", "", username).strip() or "user"
|
|
return os.path.join(root, "SwingMusic Users", safe_username)
|
|
|
|
|
|
def bootstrap_owner_user(
|
|
*,
|
|
username: str,
|
|
password: str,
|
|
root_dirs: list[str] | None = None,
|
|
) -> Any:
|
|
status = get_bootstrap_status()
|
|
if not status["required"]:
|
|
raise ValueError("Bootstrap is only available when no users exist")
|
|
|
|
if not username.strip() or not password:
|
|
raise ValueError("Username and password are required")
|
|
|
|
if UserTable.get_by_username(username):
|
|
raise ValueError("Username already exists")
|
|
|
|
UserTable.insert_one(
|
|
{
|
|
"username": username.strip(),
|
|
"password": hash_password(password),
|
|
"roles": ["owner", "admin", "user"],
|
|
}
|
|
)
|
|
|
|
owner = UserTable.get_by_username(username.strip())
|
|
if not owner:
|
|
raise ValueError("Failed to create owner")
|
|
|
|
if root_dirs is not None:
|
|
config = UserConfig()
|
|
config.rootDirs = _normalize_root_dirs(root_dirs) or []
|
|
|
|
# Ensure in-memory homepage structures include the new user.
|
|
HomepageStore.entries["recently_played"].add_new_user(owner.id)
|
|
sync_owner_projection(owner.id)
|
|
|
|
return owner
|
|
|
|
|
|
def create_invite_token(
|
|
*,
|
|
created_by: int,
|
|
roles: list[str] | None = None,
|
|
expires_in_seconds: int = 7 * 24 * 3600,
|
|
) -> Any:
|
|
return InviteTokenTable.create_token(
|
|
created_by=created_by,
|
|
roles=roles or ["user"],
|
|
expires_in_seconds=expires_in_seconds,
|
|
extra={"purpose": "user_onboarding"},
|
|
)
|
|
|
|
|
|
def accept_invite_token(
|
|
*,
|
|
token: str,
|
|
username: str,
|
|
password: str,
|
|
) -> Any:
|
|
if not username.strip() or not password:
|
|
raise ValueError("Username and password are required")
|
|
|
|
invite = InviteTokenTable.get_valid_token(token)
|
|
if not invite:
|
|
raise ValueError("Invite token is invalid or expired")
|
|
|
|
if UserTable.get_by_username(username.strip()):
|
|
raise ValueError("Username already exists")
|
|
|
|
UserTable.insert_one(
|
|
{
|
|
"username": username.strip(),
|
|
"password": hash_password(password),
|
|
"roles": invite.roles or ["user"],
|
|
}
|
|
)
|
|
|
|
user = UserTable.get_by_username(username.strip())
|
|
if not user:
|
|
raise ValueError("Failed to create user from invite")
|
|
|
|
InviteTokenTable.consume_token(token, used_by=user.id)
|
|
user_root = default_user_root_dir(user.username)
|
|
os.makedirs(user_root, exist_ok=True)
|
|
UserRootDirOwnershipTable.assign_paths(user.id, [user_root])
|
|
HomepageStore.entries["recently_played"].add_new_user(user.id)
|
|
return user
|
|
|
|
|
|
def ensure_owner_and_projection() -> None:
|
|
"""
|
|
Startup safety net for existing installs:
|
|
- Guarantees there is exactly one logical owner role holder.
|
|
- Projects existing indexed tracks to owner ownership without data loss.
|
|
"""
|
|
status = get_bootstrap_status()
|
|
if status["required"]:
|
|
return
|
|
|
|
owner = get_owner_user()
|
|
if not owner:
|
|
return
|
|
|
|
# Keep per-user homepage recents maps initialized.
|
|
for user in UserTable.get_all():
|
|
HomepageStore.entries["recently_played"].items.setdefault(user.id, [])
|
|
if UserRootDirOwnershipTable.get_paths(user.id):
|
|
continue
|
|
|
|
# Existing owner/admin users can continue to use configured roots.
|
|
if "owner" in user.roles or "admin" in user.roles:
|
|
UserRootDirOwnershipTable.assign_paths(user.id, UserConfig().rootDirs or [])
|
|
continue
|
|
|
|
user_root = default_user_root_dir(user.username)
|
|
os.makedirs(user_root, exist_ok=True)
|
|
UserRootDirOwnershipTable.assign_paths(user.id, [user_root])
|
|
|
|
sync_owner_projection(owner.id)
|
|
|
|
|
|
def ensure_lyrics_defaults() -> None:
|
|
"""
|
|
Force lyrics auto retrieval defaults to enabled in production mode.
|
|
"""
|
|
plugin = PluginTable.get_by_name("lyrics_finder")
|
|
if not plugin:
|
|
return
|
|
|
|
settings = plugin.settings or {}
|
|
settings["auto_download"] = True
|
|
settings["overide_unsynced"] = True
|
|
|
|
PluginTable.activate("lyrics_finder", True)
|
|
PluginTable.update_settings("lyrics_finder", settings)
|