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:
Harvey
2026-04-06 23:08:58 +01:00
committed by GitHub
parent a3e0b2f65a
commit 343bafd7f4
44 changed files with 2360 additions and 271 deletions
+26 -1
View File
@@ -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,
+58 -4
View File
@@ -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",
)
+6 -3
View File
@@ -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,
)
+17
View File
@@ -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
+8 -2
View File
@@ -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