Files
musicseerr/backend/api/v1/schemas/settings.py
T
2026-04-03 15:53:00 +01:00

206 lines
5.2 KiB
Python

from typing import Literal
import msgspec
from infrastructure.msgspec_fastapi import AppStruct
LASTFM_SECRET_MASK = "••••••••"
def _mask_secret(value: str) -> str:
if not value:
return ""
if len(value) <= 4:
return LASTFM_SECRET_MASK
return LASTFM_SECRET_MASK + value[-4:]
class LastFmConnectionSettings(AppStruct):
api_key: str = ""
shared_secret: str = ""
session_key: str = ""
username: str = ""
enabled: bool = False
class LastFmConnectionSettingsResponse(AppStruct):
api_key: str = ""
shared_secret: str = ""
session_key: str = ""
username: str = ""
enabled: bool = False
@classmethod
def from_settings(cls, settings: LastFmConnectionSettings) -> "LastFmConnectionSettingsResponse":
return cls(
api_key=settings.api_key,
shared_secret=_mask_secret(settings.shared_secret),
session_key=_mask_secret(settings.session_key),
username=settings.username,
enabled=settings.enabled,
)
class LastFmVerifyResponse(AppStruct):
valid: bool
message: str
class LastFmAuthTokenResponse(AppStruct):
token: str
auth_url: str
class LastFmAuthSessionRequest(AppStruct):
token: str
class LastFmAuthSessionResponse(AppStruct):
success: bool
message: str
username: str = ""
class UserPreferences(AppStruct):
primary_types: list[str] = msgspec.field(default_factory=lambda: ["album", "ep", "single"])
secondary_types: list[str] = msgspec.field(default_factory=lambda: ["studio"])
release_statuses: list[str] = msgspec.field(default_factory=lambda: ["official"])
class LidarrConnectionSettings(AppStruct):
lidarr_url: str = "http://lidarr:8686"
lidarr_api_key: str = ""
quality_profile_id: int = 1
metadata_profile_id: int = 1
root_folder_path: str = "/music"
def __post_init__(self) -> None:
self.lidarr_url = self.lidarr_url.rstrip("/")
if self.quality_profile_id < 1:
raise msgspec.ValidationError("quality_profile_id must be >= 1")
if self.metadata_profile_id < 1:
raise msgspec.ValidationError("metadata_profile_id must be >= 1")
class JellyfinConnectionSettings(AppStruct):
jellyfin_url: str = "http://jellyfin:8096"
api_key: str = ""
user_id: str = ""
enabled: bool = False
def __post_init__(self) -> None:
self.jellyfin_url = self.jellyfin_url.rstrip("/")
NAVIDROME_PASSWORD_MASK = "********"
class NavidromeConnectionSettings(AppStruct):
navidrome_url: str = ""
username: str = ""
password: str = ""
enabled: bool = False
def __post_init__(self) -> None:
self.navidrome_url = self.navidrome_url.rstrip("/") if self.navidrome_url else ""
class JellyfinUserInfo(AppStruct):
id: str
name: str
class JellyfinVerifyResponse(AppStruct):
success: bool
message: str
users: list[JellyfinUserInfo] = []
class ListenBrainzConnectionSettings(AppStruct):
username: str = ""
user_token: str = ""
enabled: bool = False
class YouTubeConnectionSettings(AppStruct):
api_key: str = ""
enabled: bool = False
api_enabled: bool = False
daily_quota_limit: int = 80
def __post_init__(self) -> None:
if self.daily_quota_limit < 1 or self.daily_quota_limit > 10000:
raise msgspec.ValidationError("daily_quota_limit must be between 1 and 10000")
def has_valid_api_key(self) -> bool:
return bool(self.api_key and self.api_key.strip())
class HomeSettings(AppStruct):
cache_ttl_trending: int = 3600
cache_ttl_personal: int = 300
def __post_init__(self) -> None:
if self.cache_ttl_trending < 300 or self.cache_ttl_trending > 86400:
raise msgspec.ValidationError("cache_ttl_trending must be between 300 and 86400")
if self.cache_ttl_personal < 60 or self.cache_ttl_personal > 3600:
raise msgspec.ValidationError("cache_ttl_personal must be between 60 and 3600")
class LocalFilesConnectionSettings(AppStruct):
enabled: bool = False
music_path: str = "/music"
lidarr_root_path: str = "/music"
class LocalFilesVerifyResponse(AppStruct):
success: bool
message: str
track_count: int = 0
class LidarrSettings(AppStruct):
sync_frequency: Literal["manual", "5min", "10min", "30min", "1hr"] = "10min"
last_sync: int | None = None
last_sync_success: bool = True
class LidarrProfileSummary(AppStruct):
id: int
name: str
class LidarrRootFolderSummary(AppStruct):
id: str
path: str
class LidarrVerifyResponse(AppStruct):
success: bool
message: str
quality_profiles: list[LidarrProfileSummary] = []
metadata_profiles: list[LidarrProfileSummary] = []
root_folders: list[LidarrRootFolderSummary] = []
class LidarrMetadataProfileSummary(AppStruct):
id: int
name: str
class ScrobbleSettings(AppStruct):
scrobble_to_lastfm: bool = False
scrobble_to_listenbrainz: bool = False
class PrimaryMusicSourceSettings(AppStruct):
source: Literal["listenbrainz", "lastfm"] = "listenbrainz"
class LidarrMetadataProfilePreferences(AppStruct):
profile_id: int
profile_name: str
primary_types: list[str] = []
secondary_types: list[str] = []
release_statuses: list[str] = []