from typing import Any, List, Optional from fastapi import Depends, HTTPException, status from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo from app.schemas.mediaserver import ExistMediaInfo as _SchemaExistMediaInfo from app.schemas.mediaserver import MediaServerExistingEpisodes as _SchemaMediaServerExistingEpisodes from app.schemas.mediaserver import MediaServerExistsData as _SchemaMediaServerExistsData from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary from app.schemas.mediaserver import MediaServerPlayData as _SchemaMediaServerPlayData from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem from app.schemas.mediaserver import NotExistMediaInfo as _SchemaNotExistMediaInfo from app.schemas.response import Response as _SchemaResponse from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.workflow import MediaInfo as _SchemaMediaInfo from app.api.response import ResponseAPIRouter from app.chain.download import DownloadChain from app.chain.mediaserver import MediaServerChain from app.domain.context import MediaInfo from app.domain.metainfo import MetaInfo from app.adapters.web.security.access import verify_token from app.application.configuration import get_configured_system_config from app.application.mediaserver import ( MediaServerQueryService, get_mediaserver_configs, ) from app.api.dependencies.history import get_mediaserver_query_service from app.schemas.mediaserver import NotExistMediaInfo from app.schemas.types import MediaSource, MediaType, SystemConfigKey from app.schemas.media import build_media_key, resolve_media_identity router = ResponseAPIRouter() def _require_mediaserver_result(result: Optional[List[Any]]) -> List[Any]: """ 保留媒体服务器成功空列表,并把提供方失败转换为明确的网关错误。 """ if result is None: raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail="媒体服务器请求失败", ) return result @router.get( "/play/{itemid:path}", summary="在线播放", response_model=_SchemaResponse[_SchemaMediaServerPlayData], ) def play_item( itemid: str, _: _SchemaTokenPayload = Depends(verify_token) ) -> _SchemaResponse: """ 获取媒体服务器播放页面地址 """ if not itemid: return _SchemaResponse(success=False, message="参数错误") configs = get_mediaserver_configs() if not configs: return _SchemaResponse(success=False, message="未配置媒体服务器") media_chain = MediaServerChain() for config in configs: name = config.name item = media_chain.iteminfo(server=name, item_id=itemid) if item: play_url = media_chain.get_play_url(server=name, item_id=itemid) if play_url: return _SchemaResponse( success=True, data={ "url": play_url, "item_id": item.item_id or itemid, "server_id": item.server_id, "server_type": item.server, }, ) return _SchemaResponse(success=False, message="未找到播放地址") @router.get( "/exists", summary="查询本地是否存在(数据库)", response_model=_SchemaResponse[_SchemaMediaServerExistsData], ) async def exists_local( title: Optional[str] = None, year: Optional[str] = None, mtype: Optional[str] = None, media_source: Optional[MediaSource] = None, media_id: Optional[str] = None, season: Optional[int] = None, service: MediaServerQueryService = Depends(get_mediaserver_query_service), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 判断本地是否存在 """ if bool(media_source) != bool(media_id): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="media_source 和 media_id 必须同时提供", ) meta = MetaInfo(title) if title else None if season is None: season = meta.begin_season if meta else None # 返回对象 ret_info = {} # 本地数据库是否存在 item_id = await service.find_item_id( title=meta.name if meta else None, year=year, mtype=mtype, media_source=media_source, media_id=media_id, season=season, ) if item_id: ret_info = {"id": item_id} return _SchemaResponse(success=True, data={"item": ret_info}) @router.post( "/exists_remote", summary="查询已存在的剧集信息(媒体服务器)", response_model=_SchemaMediaServerExistingEpisodes, ) def exists( media_in: _SchemaMediaInfo, _: _SchemaTokenPayload = Depends(verify_token) ) -> Any: """ 根据媒体信息查询媒体库已存在的剧集信息 """ # 转化为媒体信息对象 mediainfo = MediaInfo() mediainfo.from_dict(media_in.model_dump()) existsinfo: _SchemaExistMediaInfo = MediaServerChain().media_exists( mediainfo=mediainfo ) if not existsinfo: return {} if media_in.season is not None: return {media_in.season: existsinfo.seasons.get(media_in.season) or []} return existsinfo.seasons @router.post( "/notexists", summary="查询媒体库缺失信息(媒体服务器)", response_model=List[_SchemaNotExistMediaInfo], ) def not_exists( media_in: _SchemaMediaInfo, _: _SchemaTokenPayload = Depends(verify_token) ) -> Any: """ 根据媒体信息查询缺失电影/剧集 """ # 媒体信息 meta = MetaInfo(title=media_in.title) mtype = MediaType(media_in.type) if media_in.type else None if mtype: meta.type = mtype if media_in.season is not None: meta.begin_season = media_in.season meta.type = MediaType.TV if media_in.year: meta.year = media_in.year # 转化为媒体信息对象 mediainfo = MediaInfo() mediainfo.from_dict(media_in.model_dump()) exist_flag, no_exists = DownloadChain().get_no_exists_info( meta=meta, mediainfo=mediainfo ) media_source, media_id = resolve_media_identity(media=mediainfo) mediakey = build_media_key(media_source, media_id) if mediainfo.type in {MediaType.MOVIE, MediaType.MUSIC}: # 电影和音乐都是原子存在性结果;专辑内部曲目完整性由下载入口校验。 return [] if exist_flag else [NotExistMediaInfo()] elif no_exists and no_exists.get(mediakey): # 电视剧返回缺失的剧集 return list(no_exists.get(mediakey).values()) return [] @router.get( "/latest", summary="最新入库条目", response_model=List[_SchemaMediaServerPlayItem] ) def latest( server: str, count: Optional[int] = 20, userinfo: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 获取媒体服务器最新入库条目 """ return _require_mediaserver_result( MediaServerChain().latest( server=server, count=count, username=userinfo.username, ) ) @router.get( "/playing", summary="正在播放条目", response_model=List[_SchemaMediaServerPlayItem] ) def playing( server: str, count: Optional[int] = 12, userinfo: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 获取媒体服务器正在播放条目 """ return _require_mediaserver_result( MediaServerChain().playing( server=server, count=count, username=userinfo.username, ) ) @router.get( "/library", summary="媒体库列表", response_model=List[_SchemaMediaServerLibrary] ) def library( server: str, hidden: Optional[bool] = False, userinfo: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 获取媒体服务器媒体库列表 """ return _require_mediaserver_result( MediaServerChain().librarys( server=server, username=userinfo.username, hidden=hidden, ) ) @router.get( "/clients", summary="查询可用媒体服务器", response_model=List[_SchemaServiceClientInfo], ) async def clients(_: _SchemaTokenPayload = Depends(verify_token)) -> Any: """ 查询可用媒体服务器 """ mediaservers: List[dict] = get_configured_system_config().get(SystemConfigKey.MediaServers) if mediaservers: return [ {"name": d.get("name"), "type": d.get("type")} for d in mediaservers if d.get("enabled") ] return []