Initial public release
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import logging
|
||||
from typing import Literal
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
from api.v1.schemas.discover import (
|
||||
DiscoverResponse,
|
||||
DiscoverQueueResponse,
|
||||
DiscoverQueueEnrichment,
|
||||
DiscoverIgnoredRelease,
|
||||
DiscoverQueueIgnoreRequest,
|
||||
DiscoverQueueValidateRequest,
|
||||
DiscoverQueueValidateResponse,
|
||||
DiscoverQueueStatusResponse,
|
||||
QueueGenerateRequest,
|
||||
QueueGenerateResponse,
|
||||
YouTubeSearchResponse,
|
||||
YouTubeQuotaResponse,
|
||||
TrackCacheCheckRequest,
|
||||
TrackCacheCheckResponse,
|
||||
TrackCacheCheckResponseItem,
|
||||
)
|
||||
from api.v1.schemas.common import StatusMessageResponse
|
||||
from core.dependencies import get_discover_service, get_discover_queue_manager, get_youtube_repo
|
||||
from infrastructure.degradation import try_get_degradation_context
|
||||
from infrastructure.msgspec_fastapi import MsgSpecBody, MsgSpecRoute
|
||||
|
||||
import msgspec.structs
|
||||
from repositories.youtube import YouTubeRepository
|
||||
from services.discover_service import DiscoverService
|
||||
from services.discover_queue_manager import DiscoverQueueManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(route_class=MsgSpecRoute, prefix="/discover", tags=["discover"])
|
||||
|
||||
|
||||
@router.get("", response_model=DiscoverResponse)
|
||||
async def get_discover_data(
|
||||
source: Literal["listenbrainz", "lastfm"] | None = Query(default=None, description="Data source: listenbrainz or lastfm"),
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
):
|
||||
result = await discover_service.get_discover_data(source=source)
|
||||
ctx = try_get_degradation_context()
|
||||
if ctx is not None and ctx.has_degradation():
|
||||
result = msgspec.structs.replace(result, service_status=ctx.degraded_summary())
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=StatusMessageResponse)
|
||||
async def refresh_discover_data(
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
):
|
||||
await discover_service.refresh_discover_data()
|
||||
return StatusMessageResponse(status="ok", message="Discover refresh triggered")
|
||||
|
||||
|
||||
@router.get("/queue", response_model=DiscoverQueueResponse)
|
||||
async def get_discover_queue(
|
||||
count: int | None = Query(default=None, description="Number of items (default from settings, max 20)"),
|
||||
source: Literal["listenbrainz", "lastfm"] | None = Query(default=None, description="Data source: listenbrainz or lastfm"),
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
queue_manager: DiscoverQueueManager = Depends(get_discover_queue_manager),
|
||||
):
|
||||
resolved = source or discover_service.resolve_source(None)
|
||||
cached = await queue_manager.consume_queue(resolved)
|
||||
if cached:
|
||||
logger.info("Serving pre-built discover queue (source=%s, items=%d)", resolved, len(cached.items))
|
||||
return cached
|
||||
effective_count = min(count, 20) if count is not None else None
|
||||
return await queue_manager.build_hydrated_queue(resolved, effective_count)
|
||||
|
||||
|
||||
@router.get("/queue/status", response_model=DiscoverQueueStatusResponse)
|
||||
async def get_queue_status(
|
||||
source: Literal["listenbrainz", "lastfm"] | None = Query(default=None, description="Data source"),
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
queue_manager: DiscoverQueueManager = Depends(get_discover_queue_manager),
|
||||
):
|
||||
resolved = source or discover_service.resolve_source(None)
|
||||
return queue_manager.get_status(resolved)
|
||||
|
||||
|
||||
@router.post("/queue/generate", response_model=QueueGenerateResponse)
|
||||
async def generate_queue(
|
||||
body: QueueGenerateRequest = MsgSpecBody(QueueGenerateRequest),
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
queue_manager: DiscoverQueueManager = Depends(get_discover_queue_manager),
|
||||
):
|
||||
resolved = body.source or discover_service.resolve_source(None)
|
||||
return await queue_manager.start_build(resolved, force=body.force)
|
||||
|
||||
|
||||
@router.get("/queue/enrich/{release_group_mbid}", response_model=DiscoverQueueEnrichment)
|
||||
async def enrich_queue_item(
|
||||
release_group_mbid: str,
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
):
|
||||
return await discover_service.enrich_queue_item(release_group_mbid)
|
||||
|
||||
|
||||
@router.post("/queue/ignore", status_code=204)
|
||||
async def ignore_queue_item(
|
||||
body: DiscoverQueueIgnoreRequest = MsgSpecBody(DiscoverQueueIgnoreRequest),
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
):
|
||||
await discover_service.ignore_release(
|
||||
body.release_group_mbid, body.artist_mbid, body.release_name, body.artist_name
|
||||
)
|
||||
|
||||
|
||||
@router.get("/queue/ignored", response_model=list[DiscoverIgnoredRelease])
|
||||
async def get_ignored_items(
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
):
|
||||
return await discover_service.get_ignored_releases()
|
||||
|
||||
|
||||
@router.post("/queue/validate", response_model=DiscoverQueueValidateResponse)
|
||||
async def validate_queue(
|
||||
body: DiscoverQueueValidateRequest = MsgSpecBody(DiscoverQueueValidateRequest),
|
||||
discover_service: DiscoverService = Depends(get_discover_service),
|
||||
):
|
||||
in_library = await discover_service.validate_queue_mbids(body.release_group_mbids)
|
||||
return DiscoverQueueValidateResponse(in_library=in_library)
|
||||
|
||||
|
||||
@router.get("/queue/youtube-search", response_model=YouTubeSearchResponse)
|
||||
async def youtube_search(
|
||||
artist: str = Query(..., description="Artist name"),
|
||||
album: str = Query(..., description="Album name"),
|
||||
yt_repo: YouTubeRepository = Depends(get_youtube_repo),
|
||||
):
|
||||
if not yt_repo or not yt_repo.is_configured:
|
||||
return YouTubeSearchResponse(error="not_configured")
|
||||
|
||||
if yt_repo.quota_remaining <= 0 and not yt_repo.is_cached(artist, album):
|
||||
return YouTubeSearchResponse(error="quota_exceeded")
|
||||
|
||||
was_cached = yt_repo.is_cached(artist, album)
|
||||
video_id = await yt_repo.search_video(artist, album)
|
||||
if video_id:
|
||||
return YouTubeSearchResponse(
|
||||
video_id=video_id,
|
||||
embed_url=f"https://www.youtube.com/embed/{video_id}",
|
||||
cached=was_cached,
|
||||
)
|
||||
return YouTubeSearchResponse(error="not_found")
|
||||
|
||||
|
||||
@router.get("/queue/youtube-track-search", response_model=YouTubeSearchResponse)
|
||||
async def youtube_track_search(
|
||||
artist: str = Query(..., description="Artist name"),
|
||||
track: str = Query(..., description="Track name"),
|
||||
yt_repo: YouTubeRepository = Depends(get_youtube_repo),
|
||||
):
|
||||
if not yt_repo or not yt_repo.is_configured:
|
||||
return YouTubeSearchResponse(error="not_configured")
|
||||
|
||||
if yt_repo.quota_remaining <= 0 and not yt_repo.is_cached(artist, track):
|
||||
return YouTubeSearchResponse(error="quota_exceeded")
|
||||
|
||||
was_cached = yt_repo.is_cached(artist, track)
|
||||
video_id = await yt_repo.search_track(artist, track)
|
||||
if video_id:
|
||||
return YouTubeSearchResponse(
|
||||
video_id=video_id,
|
||||
embed_url=f"https://www.youtube.com/embed/{video_id}",
|
||||
cached=was_cached,
|
||||
)
|
||||
return YouTubeSearchResponse(error="not_found")
|
||||
|
||||
|
||||
@router.get("/queue/youtube-quota", response_model=YouTubeQuotaResponse)
|
||||
async def youtube_quota(
|
||||
yt_repo: YouTubeRepository = Depends(get_youtube_repo),
|
||||
):
|
||||
if not yt_repo or not yt_repo.is_configured:
|
||||
raise HTTPException(status_code=404, detail="YouTube not configured")
|
||||
return yt_repo.get_quota_status()
|
||||
|
||||
|
||||
CACHE_CHECK_MAX_ITEMS = 100
|
||||
CACHE_CHECK_MAX_STR_LEN = 200
|
||||
|
||||
|
||||
@router.post("/queue/youtube-cache-check", response_model=TrackCacheCheckResponse)
|
||||
async def youtube_cache_check(
|
||||
body: TrackCacheCheckRequest = MsgSpecBody(TrackCacheCheckRequest),
|
||||
yt_repo: YouTubeRepository = Depends(get_youtube_repo),
|
||||
):
|
||||
if not yt_repo or not yt_repo.is_configured:
|
||||
return TrackCacheCheckResponse()
|
||||
|
||||
seen: set[str] = set()
|
||||
deduped: list[tuple[str, str]] = []
|
||||
for item in body.items[:CACHE_CHECK_MAX_ITEMS]:
|
||||
artist = item.artist[:CACHE_CHECK_MAX_STR_LEN]
|
||||
track = item.track[:CACHE_CHECK_MAX_STR_LEN]
|
||||
key = f"{artist.lower()}|{track.lower()}"
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
deduped.append((artist, track))
|
||||
|
||||
cache_results = yt_repo.are_cached(deduped)
|
||||
return TrackCacheCheckResponse(
|
||||
items=[
|
||||
TrackCacheCheckResponseItem(
|
||||
artist=artist,
|
||||
track=track,
|
||||
cached=cache_results.get(f"{artist.lower()}|{track.lower()}", False),
|
||||
)
|
||||
for artist, track in deduped
|
||||
]
|
||||
)
|
||||
Reference in New Issue
Block a user