feat: Requests / Add to Library Rework - Unmonitored album default + … (#25)
* feat: Requests / Add to Library Rework - Unmonitored album default + Resilience * checking for source + refresh album logic * artist monitoring + auto downloading + various request fixes * synchronous album requests * format
This commit is contained in:
@@ -5,10 +5,11 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, R
|
||||
from core.exceptions import ClientDisconnectedError
|
||||
from api.v1.schemas.album import AlbumInfo, AlbumBasicInfo, AlbumTracksInfo, LastFmAlbumEnrichment
|
||||
from api.v1.schemas.discovery import SimilarAlbumsResponse, MoreByArtistResponse
|
||||
from core.dependencies import get_album_service, get_album_discovery_service, get_album_enrichment_service
|
||||
from core.dependencies import get_album_service, get_album_discovery_service, get_album_enrichment_service, get_navidrome_library_service
|
||||
from services.album_service import AlbumService
|
||||
from services.album_discovery_service import AlbumDiscoveryService
|
||||
from services.album_enrichment_service import AlbumEnrichmentService
|
||||
from services.navidrome_library_service import NavidromeLibraryService
|
||||
from infrastructure.validators import is_unknown_mbid
|
||||
from infrastructure.degradation import try_get_degradation_context
|
||||
from infrastructure.msgspec_fastapi import MsgSpecRoute
|
||||
@@ -44,6 +45,30 @@ async def get_album(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{album_id}/refresh", response_model=AlbumBasicInfo)
|
||||
async def refresh_album(
|
||||
album_id: str,
|
||||
album_service: AlbumService = Depends(get_album_service),
|
||||
navidrome_service: NavidromeLibraryService = Depends(get_navidrome_library_service),
|
||||
):
|
||||
"""Clear all caches for an album and return fresh data."""
|
||||
if is_unknown_mbid(album_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid or unknown album ID: {album_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
navidrome_service.invalidate_album_cache(album_id)
|
||||
await album_service.refresh_album(album_id)
|
||||
return await album_service.get_album_basic_info(album_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid album request"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{album_id}/basic", response_model=AlbumBasicInfo)
|
||||
async def get_album_basic(
|
||||
album_id: str,
|
||||
|
||||
@@ -2,15 +2,15 @@ import logging
|
||||
from typing import Literal, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from core.exceptions import ClientDisconnectedError
|
||||
from api.v1.schemas.artist import ArtistInfo, ArtistExtendedInfo, ArtistReleases, LastFmArtistEnrichment
|
||||
from core.exceptions import ClientDisconnectedError, ExternalServiceError
|
||||
from api.v1.schemas.artist import ArtistInfo, ArtistExtendedInfo, ArtistReleases, LastFmArtistEnrichment, ArtistMonitoringRequest, ArtistMonitoringResponse, ArtistMonitoringStatus
|
||||
from api.v1.schemas.discovery import SimilarArtistsResponse, TopSongsResponse, TopAlbumsResponse
|
||||
from core.dependencies import get_artist_service, get_artist_discovery_service, get_artist_enrichment_service
|
||||
from services.artist_service import ArtistService
|
||||
from services.artist_discovery_service import ArtistDiscoveryService
|
||||
from services.artist_enrichment_service import ArtistEnrichmentService
|
||||
from infrastructure.validators import is_unknown_mbid
|
||||
from infrastructure.msgspec_fastapi import MsgSpecRoute
|
||||
from infrastructure.validators import is_unknown_mbid, validate_mbid
|
||||
from infrastructure.msgspec_fastapi import MsgSpecBody, MsgSpecRoute
|
||||
from infrastructure.degradation import try_get_degradation_context
|
||||
|
||||
import msgspec.structs
|
||||
@@ -150,3 +150,57 @@ async def get_artist_lastfm_enrichment(
|
||||
if result is None:
|
||||
return LastFmArtistEnrichment()
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{artist_id}/monitoring", response_model=ArtistMonitoringStatus)
|
||||
async def get_artist_monitoring_status(
|
||||
artist_id: str,
|
||||
artist_service: ArtistService = Depends(get_artist_service),
|
||||
):
|
||||
try:
|
||||
validate_mbid(artist_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid artist ID",
|
||||
)
|
||||
try:
|
||||
return await artist_service.get_artist_monitoring_status(artist_id)
|
||||
except Exception:
|
||||
logger.debug("Failed to fetch monitoring status for %s", artist_id, exc_info=True)
|
||||
return ArtistMonitoringStatus(in_lidarr=False, monitored=False, auto_download=False)
|
||||
|
||||
|
||||
@router.put("/{artist_id}/monitoring", response_model=ArtistMonitoringResponse)
|
||||
async def update_artist_monitoring(
|
||||
artist_id: str,
|
||||
body: ArtistMonitoringRequest = MsgSpecBody(ArtistMonitoringRequest),
|
||||
artist_service: ArtistService = Depends(get_artist_service),
|
||||
):
|
||||
try:
|
||||
validate_mbid(artist_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid artist MBID format",
|
||||
)
|
||||
try:
|
||||
result = await artist_service.set_artist_monitoring(
|
||||
artist_id, monitored=body.monitored, auto_download=body.auto_download,
|
||||
)
|
||||
return ArtistMonitoringResponse(
|
||||
success=True,
|
||||
monitored=result.get("monitored", body.monitored),
|
||||
auto_download=result.get("auto_download", False),
|
||||
)
|
||||
except ExternalServiceError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="Could not update monitoring. The music server returned an error.",
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to update artist monitoring for %s", artist_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update monitoring status",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends
|
||||
from api.v1.schemas.request import AlbumRequest, RequestResponse, QueueStatusResponse
|
||||
from api.v1.schemas.request import AlbumRequest, RequestAcceptedResponse, QueueStatusResponse
|
||||
from core.dependencies import get_request_service
|
||||
from infrastructure.msgspec_fastapi import MsgSpecBody, MsgSpecRoute
|
||||
from services.request_service import RequestService
|
||||
@@ -10,16 +10,19 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(route_class=MsgSpecRoute, prefix="/requests", tags=["requests"])
|
||||
|
||||
|
||||
@router.post("/new", response_model=RequestResponse)
|
||||
@router.post("/new", response_model=RequestAcceptedResponse, status_code=202)
|
||||
async def request_album(
|
||||
album_request: AlbumRequest = MsgSpecBody(AlbumRequest),
|
||||
request_service: RequestService = Depends(get_request_service)
|
||||
request_service: RequestService = Depends(get_request_service),
|
||||
):
|
||||
return await request_service.request_album(
|
||||
album_request.musicbrainz_id,
|
||||
artist=album_request.artist,
|
||||
album=album_request.album,
|
||||
year=album_request.year,
|
||||
artist_mbid=album_request.artist_mbid,
|
||||
monitor_artist=album_request.monitor_artist,
|
||||
auto_download_artist=album_request.auto_download_artist,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ class AdvancedSettings(AppStruct):
|
||||
audiodb_prewarm_concurrency: int = 4
|
||||
audiodb_prewarm_delay: float = 0.3
|
||||
genre_section_ttl: int = 21600
|
||||
request_concurrency: int = 2
|
||||
request_history_retention_days: int = 180
|
||||
ignored_releases_retention_days: int = 365
|
||||
orphan_cover_demote_interval_hours: int = 24
|
||||
@@ -158,6 +159,7 @@ class AdvancedSettings(AppStruct):
|
||||
"sync_stall_timeout_minutes": (2, 30),
|
||||
"sync_max_timeout_hours": (1, 48),
|
||||
"audiodb_prewarm_concurrency": (1, 8),
|
||||
"request_concurrency": (1, 5),
|
||||
"audiodb_prewarm_delay": (0.0, 5.0),
|
||||
"discover_queue_size": (1, 20),
|
||||
"discover_queue_ttl": (3600, 604800),
|
||||
@@ -282,6 +284,7 @@ class AdvancedSettingsFrontend(AppStruct):
|
||||
sync_max_timeout_hours: int = 8
|
||||
audiodb_prewarm_concurrency: int = 4
|
||||
audiodb_prewarm_delay: float = 0.3
|
||||
request_concurrency: int = 2
|
||||
artist_discovery_precache_concurrency: int = 3
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -389,6 +392,7 @@ class AdvancedSettingsFrontend(AppStruct):
|
||||
"sync_stall_timeout_minutes": (2, 30),
|
||||
"sync_max_timeout_hours": (1, 48),
|
||||
"audiodb_prewarm_concurrency": (1, 8),
|
||||
"request_concurrency": (1, 5),
|
||||
"audiodb_prewarm_delay": (0.0, 5.0),
|
||||
"artist_discovery_precache_concurrency": (1, 8),
|
||||
}
|
||||
@@ -474,6 +478,7 @@ class AdvancedSettingsFrontend(AppStruct):
|
||||
sync_max_timeout_hours=settings.sync_max_timeout_hours,
|
||||
audiodb_prewarm_concurrency=settings.audiodb_prewarm_concurrency,
|
||||
audiodb_prewarm_delay=settings.audiodb_prewarm_delay,
|
||||
request_concurrency=settings.request_concurrency,
|
||||
artist_discovery_precache_concurrency=settings.artist_discovery_precache_concurrency,
|
||||
)
|
||||
|
||||
@@ -555,5 +560,6 @@ class AdvancedSettingsFrontend(AppStruct):
|
||||
sync_max_timeout_hours=self.sync_max_timeout_hours,
|
||||
audiodb_prewarm_concurrency=self.audiodb_prewarm_concurrency,
|
||||
audiodb_prewarm_delay=self.audiodb_prewarm_delay,
|
||||
request_concurrency=self.request_concurrency,
|
||||
artist_discovery_precache_concurrency=self.artist_discovery_precache_concurrency,
|
||||
)
|
||||
|
||||
@@ -34,3 +34,20 @@ class LastFmArtistEnrichment(AppStruct):
|
||||
playcount: int = 0
|
||||
similar_artists: list[LastFmSimilarArtistSchema] = []
|
||||
url: str | None = None
|
||||
|
||||
|
||||
class ArtistMonitoringRequest(AppStruct):
|
||||
monitored: bool
|
||||
auto_download: bool = False
|
||||
|
||||
|
||||
class ArtistMonitoringResponse(AppStruct):
|
||||
success: bool
|
||||
monitored: bool
|
||||
auto_download: bool
|
||||
|
||||
|
||||
class ArtistMonitoringStatus(AppStruct):
|
||||
in_lidarr: bool
|
||||
monitored: bool
|
||||
auto_download: bool
|
||||
|
||||
@@ -7,14 +7,20 @@ class AlbumRequest(AppStruct):
|
||||
artist: str | None = None
|
||||
album: str | None = None
|
||||
year: int | None = None
|
||||
artist_mbid: str | None = None
|
||||
monitor_artist: bool = False
|
||||
auto_download_artist: bool = False
|
||||
|
||||
|
||||
class RequestResponse(AppStruct):
|
||||
class RequestAcceptedResponse(AppStruct):
|
||||
success: bool
|
||||
message: str
|
||||
lidarr_response: dict | None = None
|
||||
musicbrainz_id: str
|
||||
status: str = "pending"
|
||||
|
||||
|
||||
class QueueStatusResponse(AppStruct):
|
||||
queue_size: int
|
||||
processing: bool
|
||||
active_workers: int = 0
|
||||
max_workers: int = 1
|
||||
|
||||
Reference in New Issue
Block a user