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

485 lines
21 KiB
Python

import logging
import msgspec
from fastapi import APIRouter, Depends, HTTPException
from api.v1.schemas.settings import (
UserPreferences,
LidarrSettings,
LidarrConnectionSettings,
JellyfinConnectionSettings,
JellyfinVerifyResponse,
JellyfinUserInfo,
NavidromeConnectionSettings,
ListenBrainzConnectionSettings,
YouTubeConnectionSettings,
HomeSettings,
LidarrVerifyResponse,
LocalFilesConnectionSettings,
LocalFilesVerifyResponse,
LidarrMetadataProfilePreferences,
LidarrMetadataProfileSummary,
LastFmConnectionSettings,
LastFmConnectionSettingsResponse,
LastFmVerifyResponse,
ScrobbleSettings,
PrimaryMusicSourceSettings,
)
from api.v1.schemas.common import VerifyConnectionResponse
from api.v1.schemas.advanced_settings import AdvancedSettingsFrontend, FrontendCacheTTLs, _is_masked_api_key
from core.dependencies import (
get_preferences_service,
get_settings_service,
get_local_files_service,
)
from core.exceptions import ConfigurationError, ExternalServiceError
from infrastructure.msgspec_fastapi import MsgSpecBody, MsgSpecRoute
from services.local_files_service import LocalFilesService
from services.preferences_service import PreferencesService
from services.settings_service import SettingsService
logger = logging.getLogger(__name__)
router = APIRouter(route_class=MsgSpecRoute, prefix="/settings", tags=["settings"])
@router.get("/preferences", response_model=UserPreferences)
async def get_preferences(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_preferences()
@router.put("/preferences", response_model=UserPreferences)
async def update_preferences(
preferences: UserPreferences = MsgSpecBody(UserPreferences),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_preferences(preferences)
total_cleared = await settings_service.clear_caches_for_preference_change()
logger.info(f"Updated user preferences. Cleared {total_cleared} cache entries.")
return preferences
except ConfigurationError as e:
logger.warning(f"Configuration error updating preferences: {e}")
raise HTTPException(status_code=400, detail="Couldn't save these settings")
@router.get("/lidarr", response_model=LidarrSettings)
async def get_lidarr_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_lidarr_settings()
@router.put("/lidarr", response_model=LidarrSettings)
async def update_lidarr_settings(
lidarr_settings: LidarrSettings = MsgSpecBody(LidarrSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
):
try:
preferences_service.save_lidarr_settings(lidarr_settings)
logger.info(f"Updated Lidarr settings: sync_frequency={lidarr_settings.sync_frequency}")
return lidarr_settings
except ConfigurationError as e:
logger.warning(f"Configuration error updating Lidarr settings: {e}")
raise HTTPException(status_code=400, detail="Lidarr settings are incomplete or invalid")
@router.get("/cache-ttls", response_model=FrontendCacheTTLs)
async def get_frontend_cache_ttls(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
backend_settings = preferences_service.get_advanced_settings()
return FrontendCacheTTLs(
home=backend_settings.frontend_ttl_home,
discover=backend_settings.frontend_ttl_discover,
library=backend_settings.frontend_ttl_library,
recently_added=backend_settings.frontend_ttl_recently_added,
discover_queue=backend_settings.frontend_ttl_discover_queue,
search=backend_settings.frontend_ttl_search,
local_files_sidebar=backend_settings.frontend_ttl_local_files_sidebar,
jellyfin_sidebar=backend_settings.frontend_ttl_jellyfin_sidebar,
playlist_sources=backend_settings.frontend_ttl_playlist_sources,
discover_queue_polling_interval=backend_settings.discover_queue_polling_interval,
discover_queue_auto_generate=backend_settings.discover_queue_auto_generate,
)
@router.get("/advanced", response_model=AdvancedSettingsFrontend)
async def get_advanced_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
backend_settings = preferences_service.get_advanced_settings()
return AdvancedSettingsFrontend.from_backend(backend_settings)
@router.put("/advanced", response_model=AdvancedSettingsFrontend)
async def update_advanced_settings(
settings: AdvancedSettingsFrontend = MsgSpecBody(AdvancedSettingsFrontend),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
backend_settings = settings.to_backend()
if _is_masked_api_key(backend_settings.audiodb_api_key):
current = preferences_service.get_advanced_settings()
backend_settings = msgspec.structs.replace(
backend_settings, audiodb_api_key=current.audiodb_api_key
)
preferences_service.save_advanced_settings(backend_settings)
await settings_service.on_coverart_settings_changed()
logger.info("Updated advanced settings")
saved = preferences_service.get_advanced_settings()
return AdvancedSettingsFrontend.from_backend(saved)
except ConfigurationError as e:
logger.warning(f"Configuration error updating advanced settings: {e}")
raise HTTPException(status_code=400, detail="Couldn't save these settings")
except ValueError as e:
logger.warning(f"Validation error updating advanced settings: {e}")
raise HTTPException(status_code=400, detail="That settings value isn't valid")
@router.get("/lidarr/connection", response_model=LidarrConnectionSettings)
async def get_lidarr_connection(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_lidarr_connection()
@router.put("/lidarr/connection", response_model=LidarrConnectionSettings)
async def update_lidarr_connection(
settings: LidarrConnectionSettings = MsgSpecBody(LidarrConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
from repositories.lidarr.base import reset_lidarr_circuit_breaker
preferences_service.save_lidarr_connection(settings)
reset_lidarr_circuit_breaker()
await settings_service.on_lidarr_settings_changed()
logger.info("Updated Lidarr connection settings")
return settings
except ConfigurationError as e:
logger.warning(f"Configuration error updating Lidarr connection: {e}")
raise HTTPException(status_code=400, detail="Lidarr connection settings are incomplete or invalid")
@router.post("/lidarr/verify", response_model=LidarrVerifyResponse)
async def verify_lidarr_connection(
settings: LidarrConnectionSettings = MsgSpecBody(LidarrConnectionSettings),
settings_service: SettingsService = Depends(get_settings_service),
):
return await settings_service.verify_lidarr(settings)
@router.get(
"/lidarr/metadata-profiles",
response_model=list[LidarrMetadataProfileSummary],
)
async def list_lidarr_metadata_profiles(
settings_service: SettingsService = Depends(get_settings_service),
):
try:
return await settings_service.list_lidarr_metadata_profiles()
except ExternalServiceError as e:
logger.warning(f"Lidarr metadata profiles list failed: {e}")
raise HTTPException(status_code=502, detail="Couldn't load Lidarr metadata profiles")
@router.get(
"/lidarr/metadata-profile/preferences",
response_model=LidarrMetadataProfilePreferences,
)
async def get_lidarr_metadata_profile_preferences(
profile_id: int | None = None,
settings_service: SettingsService = Depends(get_settings_service),
):
try:
return await settings_service.get_lidarr_metadata_profile_preferences(
profile_id=profile_id
)
except ExternalServiceError as e:
logger.warning(f"Lidarr metadata profile fetch failed: {e}")
raise HTTPException(status_code=502, detail="Couldn't load the Lidarr metadata profile")
@router.put(
"/lidarr/metadata-profile/preferences",
response_model=LidarrMetadataProfilePreferences,
)
async def update_lidarr_metadata_profile_preferences(
preferences: UserPreferences = MsgSpecBody(UserPreferences),
profile_id: int | None = None,
settings_service: SettingsService = Depends(get_settings_service),
):
try:
return await settings_service.update_lidarr_metadata_profile(
preferences, profile_id=profile_id
)
except ExternalServiceError as e:
logger.warning(f"Lidarr metadata profile update failed: {e}")
raise HTTPException(status_code=502, detail="Couldn't update the Lidarr metadata profile")
@router.get("/jellyfin", response_model=JellyfinConnectionSettings)
async def get_jellyfin_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_jellyfin_connection()
@router.put("/jellyfin", response_model=JellyfinConnectionSettings)
async def update_jellyfin_settings(
settings: JellyfinConnectionSettings = MsgSpecBody(JellyfinConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_jellyfin_connection(settings)
await settings_service.on_jellyfin_settings_changed()
logger.info("Updated Jellyfin connection settings")
return settings
except ConfigurationError as e:
logger.warning(f"Configuration error updating Jellyfin settings: {e}")
raise HTTPException(status_code=400, detail="Jellyfin settings are incomplete or invalid")
@router.post("/jellyfin/verify", response_model=JellyfinVerifyResponse)
async def verify_jellyfin_connection(
settings: JellyfinConnectionSettings = MsgSpecBody(JellyfinConnectionSettings),
settings_service: SettingsService = Depends(get_settings_service),
):
result = await settings_service.verify_jellyfin(settings)
users = [JellyfinUserInfo(id=user.id, name=user.name) for user in (result.users or [])] if result.success else []
return JellyfinVerifyResponse(success=result.success, message=result.message, users=users)
@router.get("/navidrome", response_model=NavidromeConnectionSettings)
async def get_navidrome_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_navidrome_connection()
@router.put("/navidrome", response_model=NavidromeConnectionSettings)
async def update_navidrome_settings(
settings: NavidromeConnectionSettings = MsgSpecBody(NavidromeConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_navidrome_connection(settings)
await settings_service.on_navidrome_settings_changed(enabled=settings.enabled)
logger.info("Updated Navidrome connection settings")
return preferences_service.get_navidrome_connection()
except ConfigurationError as e:
logger.warning("Configuration error updating Navidrome settings: %s", e)
raise HTTPException(status_code=400, detail="Navidrome settings are incomplete or invalid")
@router.post("/navidrome/verify", response_model=VerifyConnectionResponse)
async def verify_navidrome_connection(
settings: NavidromeConnectionSettings = MsgSpecBody(NavidromeConnectionSettings),
settings_service: SettingsService = Depends(get_settings_service),
):
result = await settings_service.verify_navidrome(settings)
return VerifyConnectionResponse(valid=result.valid, message=result.message)
@router.get("/listenbrainz", response_model=ListenBrainzConnectionSettings)
async def get_listenbrainz_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_listenbrainz_connection()
@router.put("/listenbrainz", response_model=ListenBrainzConnectionSettings)
async def update_listenbrainz_settings(
settings: ListenBrainzConnectionSettings = MsgSpecBody(ListenBrainzConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_listenbrainz_connection(settings)
await settings_service.on_listenbrainz_settings_changed()
logger.info("Updated ListenBrainz connection settings")
return settings
except ConfigurationError as e:
logger.warning(f"Configuration error updating ListenBrainz settings: {e}")
raise HTTPException(status_code=400, detail="ListenBrainz settings are incomplete or invalid")
@router.post("/listenbrainz/verify", response_model=VerifyConnectionResponse)
async def verify_listenbrainz_connection(
settings: ListenBrainzConnectionSettings = MsgSpecBody(ListenBrainzConnectionSettings),
settings_service: SettingsService = Depends(get_settings_service),
):
result = await settings_service.verify_listenbrainz(settings)
return VerifyConnectionResponse(valid=result.valid, message=result.message)
@router.get("/youtube", response_model=YouTubeConnectionSettings)
async def get_youtube_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_youtube_connection()
@router.put("/youtube", response_model=YouTubeConnectionSettings)
async def update_youtube_settings(
settings: YouTubeConnectionSettings = MsgSpecBody(YouTubeConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_youtube_connection(settings)
await settings_service.on_youtube_settings_changed()
logger.info("Updated YouTube connection settings")
return settings
except ConfigurationError as e:
logger.warning(f"Configuration error updating YouTube settings: {e}")
raise HTTPException(status_code=400, detail="YouTube settings are incomplete or invalid")
@router.post("/youtube/verify", response_model=VerifyConnectionResponse)
async def verify_youtube_connection(
settings: YouTubeConnectionSettings = MsgSpecBody(YouTubeConnectionSettings),
settings_service: SettingsService = Depends(get_settings_service),
):
result = await settings_service.verify_youtube(settings)
return VerifyConnectionResponse(valid=result.valid, message=result.message)
@router.get("/home", response_model=HomeSettings)
async def get_home_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_home_settings()
@router.put("/home", response_model=HomeSettings)
async def update_home_settings(
settings: HomeSettings = MsgSpecBody(HomeSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_home_settings(settings)
await settings_service.clear_home_cache()
logger.info("Updated home settings")
return settings
except ConfigurationError as e:
logger.warning(f"Configuration error updating home settings: {e}")
raise HTTPException(status_code=400, detail="Home settings are incomplete or invalid")
@router.get("/local-files", response_model=LocalFilesConnectionSettings)
async def get_local_files_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_local_files_connection()
@router.put("/local-files", response_model=LocalFilesConnectionSettings)
async def update_local_files_settings(
settings: LocalFilesConnectionSettings = MsgSpecBody(LocalFilesConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_local_files_connection(settings)
await settings_service.on_local_files_settings_changed()
logger.info("Updated local files settings")
return settings
except ConfigurationError as e:
logger.warning("Configuration error updating local files settings: %s", e)
raise HTTPException(status_code=400, detail="Local files settings are incomplete or invalid")
@router.post("/local-files/verify", response_model=LocalFilesVerifyResponse)
async def verify_local_files_connection(
settings: LocalFilesConnectionSettings = MsgSpecBody(LocalFilesConnectionSettings),
local_service: LocalFilesService = Depends(get_local_files_service),
) -> LocalFilesVerifyResponse:
return await local_service.verify_path(settings.music_path)
@router.get("/lastfm", response_model=LastFmConnectionSettingsResponse)
async def get_lastfm_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
settings = preferences_service.get_lastfm_connection()
return LastFmConnectionSettingsResponse.from_settings(settings)
@router.put("/lastfm", response_model=LastFmConnectionSettingsResponse)
async def update_lastfm_settings(
settings: LastFmConnectionSettings = MsgSpecBody(LastFmConnectionSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_lastfm_connection(settings)
await settings_service.on_lastfm_settings_changed()
logger.info("Updated Last.fm connection settings")
saved = preferences_service.get_lastfm_connection()
return LastFmConnectionSettingsResponse.from_settings(saved)
except ConfigurationError as e:
logger.warning("Configuration error updating Last.fm settings: %s", e)
raise HTTPException(status_code=400, detail="Last.fm settings are incomplete or invalid")
@router.post("/lastfm/verify", response_model=LastFmVerifyResponse)
async def verify_lastfm_connection(
settings: LastFmConnectionSettings = MsgSpecBody(LastFmConnectionSettings),
settings_service: SettingsService = Depends(get_settings_service),
):
result = await settings_service.verify_lastfm(settings)
return LastFmVerifyResponse(valid=result.valid, message=result.message)
@router.get("/scrobble", response_model=ScrobbleSettings)
async def get_scrobble_settings(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_scrobble_settings()
@router.put("/scrobble", response_model=ScrobbleSettings)
async def update_scrobble_settings(
settings: ScrobbleSettings = MsgSpecBody(ScrobbleSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
):
try:
preferences_service.save_scrobble_settings(settings)
logger.info("Updated scrobble settings")
return preferences_service.get_scrobble_settings()
except ConfigurationError as e:
logger.warning("Configuration error updating scrobble settings: %s", e)
raise HTTPException(status_code=400, detail="Scrobbling settings are incomplete or invalid")
@router.get("/primary-source", response_model=PrimaryMusicSourceSettings)
async def get_primary_music_source(
preferences_service: PreferencesService = Depends(get_preferences_service),
):
return preferences_service.get_primary_music_source()
@router.put("/primary-source", response_model=PrimaryMusicSourceSettings)
async def update_primary_music_source(
settings: PrimaryMusicSourceSettings = MsgSpecBody(PrimaryMusicSourceSettings),
preferences_service: PreferencesService = Depends(get_preferences_service),
settings_service: SettingsService = Depends(get_settings_service),
):
try:
preferences_service.save_primary_music_source(settings)
await settings_service.clear_home_cache()
await settings_service.clear_source_resolution_cache()
logger.info("Updated primary music source to %s", settings.source)
return preferences_service.get_primary_music_source()
except ConfigurationError as e:
logger.warning("Configuration error updating primary music source: %s", e)
raise HTTPException(status_code=400, detail="Invalid primary music source")