mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 20:43:04 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user