import logging from typing import Literal from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query from fastapi.responses import Response from api.v1.schemas.jellyfin import ( JellyfinAlbumDetail, JellyfinAlbumMatch, JellyfinAlbumSummary, JellyfinArtistIndexResponse, JellyfinArtistPage, JellyfinArtistSummary, JellyfinFavoritesExpanded, JellyfinFilterFacets, JellyfinHubResponse, JellyfinImportResult, JellyfinLibraryStats, JellyfinLyricsResponse, JellyfinPaginatedResponse, JellyfinPlaylistDetail, JellyfinPlaylistSummary, JellyfinSearchResponse, JellyfinSessionsResponse, JellyfinTrackInfo, JellyfinTrackPage, ) from core.dependencies import ( get_jellyfin_library_service, get_jellyfin_repository, get_local_files_service, get_navidrome_library_service, get_plex_library_service, get_playlist_service, ) from core.exceptions import ExternalServiceError, ResourceNotFoundError from infrastructure.msgspec_fastapi import MsgSpecRoute from repositories.jellyfin_repository import JellyfinRepository from services.jellyfin_library_service import JellyfinLibraryService from services.playlist_service import PlaylistService logger = logging.getLogger(__name__) router = APIRouter(route_class=MsgSpecRoute, prefix="/jellyfin", tags=["jellyfin-library"]) @router.get("/hub", response_model=JellyfinHubResponse) async def get_jellyfin_hub( service: JellyfinLibraryService = Depends(get_jellyfin_library_service), playlist_service: PlaylistService = Depends(get_playlist_service), ) -> JellyfinHubResponse: try: hub = await service.get_hub_data() imported_ids = await playlist_service.get_imported_source_ids("jellyfin:") for p in hub.playlists: if p.id in imported_ids: p.is_imported = True return hub except ExternalServiceError as e: logger.error("Jellyfin service error getting hub data: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/image/{item_id}") async def get_jellyfin_image( item_id: str, size: int = Query(default=500, ge=32, le=1200), repo: JellyfinRepository = Depends(get_jellyfin_repository), ) -> Response: try: image_bytes, content_type = await repo.proxy_image(item_id, size) return Response( content=image_bytes, media_type=content_type, headers={"Cache-Control": "public, max-age=31536000, immutable"}, ) except ExternalServiceError as e: logger.warning("Jellyfin image failed for %s: %s", item_id, e) raise HTTPException(status_code=502, detail="Failed to fetch image") @router.get("/recently-added", response_model=list[JellyfinAlbumSummary]) async def get_jellyfin_recently_added( limit: int = Query(default=20, ge=1, le=50), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinAlbumSummary]: try: return await service.get_recently_added(limit=limit) except ExternalServiceError as e: logger.error("Jellyfin service error getting recently added: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/most-played/artists", response_model=list[JellyfinArtistSummary]) async def get_jellyfin_most_played_artists( limit: int = Query(default=10, ge=1, le=50), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinArtistSummary]: try: return await service.get_most_played_artists(limit=limit) except ExternalServiceError as e: logger.error("Jellyfin service error getting most played artists: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/most-played/albums", response_model=list[JellyfinAlbumSummary]) async def get_jellyfin_most_played_albums( limit: int = Query(default=10, ge=1, le=50), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinAlbumSummary]: try: return await service.get_most_played_albums(limit=limit) except ExternalServiceError as e: logger.error("Jellyfin service error getting most played albums: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/albums", response_model=JellyfinPaginatedResponse) async def get_jellyfin_albums( limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), sort_by: Literal["SortName", "DateCreated", "PlayCount", "ProductionYear"] = Query(default="SortName"), sort_order: Literal["Ascending", "Descending"] = Query(default="Ascending"), genre: str | None = Query(default=None), year: int | None = Query(default=None), tags: str | None = Query(default=None), studios: str | None = Query(default=None), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinPaginatedResponse: try: items, total = await service.get_albums( limit=limit, offset=offset, sort_by=sort_by, sort_order=sort_order, genre=genre, year=year, tags=tags, studios=studios, ) return JellyfinPaginatedResponse( items=items, total=total, offset=offset, limit=limit ) except ExternalServiceError as e: logger.error("Jellyfin service error getting albums: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/albums/{album_id}", response_model=JellyfinAlbumDetail) async def get_jellyfin_album_detail( album_id: str, service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinAlbumDetail: result = await service.get_album_detail(album_id) if not result: raise HTTPException(status_code=404, detail="Album not found") return result @router.get( "/albums/{album_id}/tracks", response_model=list[JellyfinTrackInfo] ) async def get_jellyfin_album_tracks( album_id: str, service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinTrackInfo]: try: return await service.get_album_tracks(album_id) except ExternalServiceError as e: logger.error("Jellyfin service error getting album tracks %s: %s", album_id, e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get( "/albums/match/{musicbrainz_id}", response_model=JellyfinAlbumMatch ) async def match_jellyfin_album( musicbrainz_id: str, service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinAlbumMatch: try: return await service.match_album_by_mbid(musicbrainz_id) except ExternalServiceError as e: logger.error("Failed to match Jellyfin album %s: %s", musicbrainz_id, e) raise HTTPException(status_code=502, detail="Failed to match Jellyfin album") @router.get("/artists/browse", response_model=JellyfinArtistPage) async def browse_jellyfin_artists( limit: int = Query(48, ge=1, le=100), offset: int = Query(0, ge=0), sort_by: str = Query("SortName"), sort_order: str = Query("Ascending"), search: str = Query(""), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinArtistPage: try: items, total = await service.browse_artists( limit=limit, offset=offset, sort_by=sort_by, sort_order=sort_order, search=search ) return JellyfinArtistPage(items=items, total=total, offset=offset, limit=limit) except ExternalServiceError as e: logger.error("Jellyfin service error browsing artists: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/artists", response_model=list[JellyfinArtistSummary]) async def get_jellyfin_artists( limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinArtistSummary]: return await service.get_artists(limit=limit, offset=offset) @router.get("/artists/index", response_model=JellyfinArtistIndexResponse) async def get_jellyfin_artists_index( service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinArtistIndexResponse: try: return await service.get_artists_index() except ExternalServiceError as e: logger.error("Jellyfin service error getting artist index: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/tracks", response_model=JellyfinTrackPage) async def browse_jellyfin_tracks( limit: int = Query(48, ge=1, le=100), offset: int = Query(0, ge=0), sort_by: str = Query("SortName"), sort_order: str = Query("Ascending"), search: str = Query(""), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinTrackPage: try: items, total = await service.browse_tracks( limit=limit, offset=offset, sort_by=sort_by, sort_order=sort_order, search=search ) return JellyfinTrackPage(items=items, total=total, offset=offset, limit=limit) except ExternalServiceError as e: logger.error("Jellyfin service error browsing tracks: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/search", response_model=JellyfinSearchResponse) async def search_jellyfin( q: str = Query(..., min_length=1), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinSearchResponse: return await service.search(q) @router.get("/recent", response_model=list[JellyfinAlbumSummary]) async def get_jellyfin_recent( limit: int = Query(default=20, ge=1, le=50), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinAlbumSummary]: return await service.get_recently_played(limit=limit) @router.get("/favorites", response_model=list[JellyfinAlbumSummary]) async def get_jellyfin_favorites( limit: int = Query(default=20, ge=1, le=50), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinAlbumSummary]: return await service.get_favorites(limit=limit) @router.get("/favorites/expanded", response_model=JellyfinFavoritesExpanded) async def get_jellyfin_favorites_expanded( limit: int = Query(default=50, ge=1, le=100), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinFavoritesExpanded: try: return await service.get_favorites_expanded(limit=limit) except ExternalServiceError as e: logger.error("Jellyfin service error getting expanded favorites: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") except Exception: # noqa: BLE001 logger.exception("Unexpected error in expanded favorites") raise HTTPException(status_code=500, detail="Internal error fetching expanded favorites") @router.get("/filters", response_model=JellyfinFilterFacets) async def get_jellyfin_filter_facets( service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinFilterFacets: try: return await service.get_filter_facets() except ExternalServiceError as e: logger.error("Jellyfin service error getting filter facets: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/genres", response_model=list[str]) async def get_jellyfin_genres( service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[str]: try: return await service.get_genres() except ExternalServiceError as e: logger.error("Jellyfin service error getting genres: %s", e) raise HTTPException(status_code=502, detail="Failed to communicate with Jellyfin") @router.get("/genres/songs", response_model=JellyfinTrackPage) async def get_jellyfin_genre_songs( genre: str = Query(..., min_length=1), limit: int = Query(default=50, ge=1, le=100), offset: int = Query(default=0, ge=0), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinTrackPage: tracks, total = await service.get_songs_by_genre(genre, limit=limit, offset=offset) return JellyfinTrackPage(items=tracks, total=total, offset=offset, limit=limit) @router.get("/instant-mix/artist/{artist_id}", response_model=list[JellyfinTrackInfo]) async def get_jellyfin_instant_mix_by_artist( artist_id: str, limit: int = Query(default=50, ge=1, le=100), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinTrackInfo]: return await service.get_instant_mix_by_artist(artist_id, limit=limit) @router.get("/instant-mix/genre", response_model=list[JellyfinTrackInfo]) async def get_jellyfin_instant_mix_by_genre( genre: str = Query(..., min_length=1), limit: int = Query(default=50, ge=1, le=100), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinTrackInfo]: return await service.get_instant_mix_by_genre(genre, limit=limit) @router.get("/instant-mix/{item_id}", response_model=list[JellyfinTrackInfo]) async def get_jellyfin_instant_mix( item_id: str, limit: int = Query(default=50, ge=1, le=100), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinTrackInfo]: return await service.get_instant_mix(item_id, limit=limit) @router.get("/stats", response_model=JellyfinLibraryStats) async def get_jellyfin_stats( service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinLibraryStats: return await service.get_stats() @router.get("/playlists", response_model=list[JellyfinPlaylistSummary]) async def get_jellyfin_playlists( limit: int = Query(default=50, ge=1, le=200), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), playlist_service: PlaylistService = Depends(get_playlist_service), ) -> list[JellyfinPlaylistSummary]: try: playlists = await service.list_playlists(limit=limit) imported_ids = await playlist_service.get_imported_source_ids("jellyfin:") for p in playlists: if p.id in imported_ids: p.is_imported = True return playlists except ExternalServiceError as e: logger.error("Failed to get Jellyfin playlists: %s", e) raise HTTPException(status_code=502, detail="Failed to get Jellyfin playlists") @router.get("/playlists/{playlist_id}", response_model=JellyfinPlaylistDetail) async def get_jellyfin_playlist_detail( playlist_id: str, service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinPlaylistDetail: try: return await service.get_playlist_detail(playlist_id) except ResourceNotFoundError: raise HTTPException(status_code=404, detail="Jellyfin playlist not found") except ExternalServiceError as e: logger.error("Failed to get Jellyfin playlist %s: %s", playlist_id, e) raise HTTPException(status_code=502, detail="Failed to get Jellyfin playlist") @router.post("/playlists/{playlist_id}/import", response_model=JellyfinImportResult) async def import_jellyfin_playlist( playlist_id: str, background_tasks: BackgroundTasks, service: JellyfinLibraryService = Depends(get_jellyfin_library_service), playlist_service: PlaylistService = Depends(get_playlist_service), local_service=Depends(get_local_files_service), nd_service=Depends(get_navidrome_library_service), plex_service=Depends(get_plex_library_service), ) -> JellyfinImportResult: try: result = await service.import_playlist(playlist_id, playlist_service) except ResourceNotFoundError: raise HTTPException(status_code=404, detail="Jellyfin playlist not found") except ExternalServiceError as e: logger.error("Failed to import Jellyfin playlist %s: %s", playlist_id, e) raise HTTPException(status_code=502, detail="Failed to import Jellyfin playlist") if not result.already_imported: background_tasks.add_task( playlist_service.resolve_track_sources, result.musicseerr_playlist_id, jf_service=service, local_service=local_service, nd_service=nd_service, plex_service=plex_service, ) return result @router.get("/sessions", response_model=JellyfinSessionsResponse) async def get_jellyfin_sessions( service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinSessionsResponse: return await service.get_sessions() @router.get("/similar/{item_id}", response_model=list[JellyfinAlbumSummary]) async def get_jellyfin_similar_items( item_id: str, limit: int = Query(10, ge=1, le=50), service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> list[JellyfinAlbumSummary]: return await service.get_similar_items(item_id, limit=limit) @router.get("/lyrics/{item_id}", response_model=JellyfinLyricsResponse) async def get_jellyfin_lyrics( item_id: str, service: JellyfinLibraryService = Depends(get_jellyfin_library_service), ) -> JellyfinLyricsResponse: lyrics = await service.get_lyrics(item_id) if lyrics is None: raise HTTPException(status_code=404, detail="Lyrics not available") return lyrics