diff --git a/app/api/endpoints/douban.py b/app/api/endpoints/douban.py index c8e45db4..65ab4cc1 100644 --- a/app/api/endpoints/douban.py +++ b/app/api/endpoints/douban.py @@ -21,7 +21,7 @@ def start_douban_chain(): DoubanChain().sync() -@router.get("/sync", response_model=schemas.Response) +@router.get("/sync", summary="同步豆瓣想看", response_model=schemas.Response) async def sync_douban( background_tasks: BackgroundTasks, _: User = Depends(get_current_active_superuser)) -> Any: @@ -32,7 +32,7 @@ async def sync_douban( return schemas.Response(success=True, message="任务已启动") -@router.get("/id", response_model=schemas.Context) +@router.get("/id", summary="豆瓣ID识别", response_model=schemas.Context) async def recognize_doubanid(doubanid: str, _: User = Depends(get_current_active_user)) -> Any: """ @@ -43,7 +43,7 @@ async def recognize_doubanid(doubanid: str, return context.to_dict() -@router.get("/info", response_model=schemas.MediaInfo) +@router.get("/info", summary="查询豆瓣详情", response_model=schemas.MediaInfo) async def douban_info(doubanid: str) -> Any: """ 根据豆瓣ID查询豆瓣媒体信息 @@ -55,7 +55,7 @@ async def douban_info(doubanid: str) -> Any: return schemas.MediaInfo() -@router.get("/movies", response_model=List[schemas.MediaInfo]) +@router.get("/movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo]) async def douban_movies(sort: str = "R", tags: str = "", start: int = 0, @@ -71,7 +71,7 @@ async def douban_movies(sort: str = "R", return [MediaInfo(douban_info=movie).to_dict() for movie in movies] -@router.get("/tvs", response_model=List[schemas.MediaInfo]) +@router.get("/tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo]) async def douban_tvs(sort: str = "R", tags: str = "", start: int = 0, @@ -87,7 +87,7 @@ async def douban_tvs(sort: str = "R", return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] -@router.get("/movie_top250", response_model=List[schemas.MediaInfo]) +@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo]) async def movie_top250(page: int = 1, count: int = 30, _: User = Depends(get_current_active_user)) -> Any: @@ -98,7 +98,7 @@ async def movie_top250(page: int = 1, return [MediaInfo(douban_info=movie).to_dict() for movie in movies] -@router.get("/tv_weekly_chinese", response_model=List[schemas.MediaInfo]) +@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo]) async def tv_weekly_chinese(page: int = 1, count: int = 30, _: User = Depends(get_current_active_user)) -> Any: @@ -109,7 +109,7 @@ async def tv_weekly_chinese(page: int = 1, return [MediaInfo(douban_info=tv).to_dict() for tv in tvs] -@router.get("/tv_weekly_global", response_model=List[schemas.MediaInfo]) +@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo]) async def tv_weekly_global(page: int = 1, count: int = 30, _: User = Depends(get_current_active_user)) -> Any: diff --git a/app/api/endpoints/login.py b/app/api/endpoints/login.py index a6fae7de..ce4f5525 100644 --- a/app/api/endpoints/login.py +++ b/app/api/endpoints/login.py @@ -14,7 +14,7 @@ from app.db.models.user import User router = APIRouter() -@router.post("/login/access-token", response_model=schemas.Token) +@router.post("/login/access-token", summary="获取token", response_model=schemas.Token) async def login_access_token( db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends() ) -> Any: diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index fb38a4e6..afdd3e1f 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -10,19 +10,19 @@ from app.db.userauth import get_current_active_user router = APIRouter() -@router.get("/recognize", response_model=schemas.Context) +@router.get("/recognize", summary="识别媒体信息", response_model=schemas.Context) async def recognize(title: str, subtitle: str = None, _: User = Depends(get_current_active_user)) -> Any: """ - 识别媒体信息 + 根据标题、副标题识别媒体信息 """ # 识别媒体信息 context = MediaChain().recognize_by_title(title=title, subtitle=subtitle) return context.to_dict() -@router.get("/search", response_model=List[schemas.MediaInfo]) +@router.get("/search", summary="搜索媒体信息", response_model=List[schemas.MediaInfo]) async def search_by_title(title: str, _: User = Depends(get_current_active_user)) -> Any: """ diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 69a7ca4e..a0f9cffa 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -20,7 +20,7 @@ def start_message_chain(body: Any, form: Any, args: Any): MessageChain().process(body=body, form=form, args=args) -@router.post("/", response_model=schemas.Response) +@router.post("/", summary="接收用户消息", response_model=schemas.Response) async def user_message(background_tasks: BackgroundTasks, request: Request): """ 用户消息响应 @@ -32,7 +32,7 @@ async def user_message(background_tasks: BackgroundTasks, request: Request): return schemas.Response(success=True) -@router.get("/") +@router.get("/", summary="微信验证") async def wechat_verify(echostr: str, msg_signature: str, timestamp: Union[str, int], nonce: str) -> Any: """ diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index d418fd28..7d3305ea 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -10,7 +10,7 @@ from app.db.userauth import get_current_active_user router = APIRouter() -@router.get("/", response_model=schemas.Response) +@router.get("/", summary="运行插件方法", response_model=schemas.Response) @router.post("/") async def run_plugin_method(plugin_id: str, method: str, _: User = Depends(get_current_active_user), @@ -23,3 +23,7 @@ async def run_plugin_method(plugin_id: str, method: str, method=method, *args, **kwargs) + +# 注册插件API +for api in PluginManager().get_plugin_apis(): + router.add_api_route(**api) diff --git a/app/api/endpoints/search.py b/app/api/endpoints/search.py index 3b234587..0a13c65c 100644 --- a/app/api/endpoints/search.py +++ b/app/api/endpoints/search.py @@ -11,7 +11,7 @@ from app.schemas.types import MediaType router = APIRouter() -@router.get("/tmdbid", response_model=List[schemas.Context]) +@router.get("/tmdbid", summary="精确搜索资源", response_model=List[schemas.Context]) async def search_by_tmdbid(tmdbid: int, mtype: str = None, _: User = Depends(get_current_active_user)) -> Any: @@ -24,7 +24,7 @@ async def search_by_tmdbid(tmdbid: int, return [torrent.to_dict() for torrent in torrents] -@router.get("/title", response_model=List[schemas.TorrentInfo]) +@router.get("/title", summary="模糊搜索资源", response_model=List[schemas.TorrentInfo]) async def search_by_title(title: str, _: User = Depends(get_current_active_user)) -> Any: """ diff --git a/app/api/endpoints/site.py b/app/api/endpoints/site.py index e63209fe..60f23a31 100644 --- a/app/api/endpoints/site.py +++ b/app/api/endpoints/site.py @@ -14,7 +14,7 @@ from app.db.userauth import get_current_active_user, get_current_active_superuse router = APIRouter() -@router.get("/", response_model=List[schemas.Site]) +@router.get("/", summary="所有站点", response_model=List[schemas.Site]) async def read_sites(db: Session = Depends(get_db), _: User = Depends(get_current_active_user)) -> List[dict]: """ @@ -23,7 +23,7 @@ async def read_sites(db: Session = Depends(get_db), return Site.list(db) -@router.put("/", response_model=schemas.Site) +@router.put("/", summary="更新站点", response_model=schemas.Site) async def update_site( *, db: Session = Depends(get_db), @@ -43,7 +43,7 @@ async def update_site( return site -@router.get("/{site_id}", response_model=schemas.Site) +@router.get("/{site_id}", summary="站点详情", response_model=schemas.Site) async def read_site( site_id: int, db: Session = Depends(get_db), @@ -61,7 +61,7 @@ async def read_site( return site -@router.get("/cookiecloud", response_model=schemas.Response) +@router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response) async def cookie_cloud_sync(_: User = Depends(get_current_active_user)) -> Any: """ 运行CookieCloud同步站点信息 @@ -72,7 +72,7 @@ async def cookie_cloud_sync(_: User = Depends(get_current_active_user)) -> Any: return schemas.Response(success=True, message="同步成功!") -@router.get("/cookie", response_model=schemas.Response) +@router.get("/cookie", summary="更新站点Cookie&UA", response_model=schemas.Response) async def update_cookie( site_id: int, username: str, diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index 8e2a6221..ee9214bc 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -24,7 +24,7 @@ def start_subscribe_chain(title: str, year: str, mtype=mtype, tmdbid=tmdbid, season=season, username=username) -@router.get("/", response_model=List[schemas.Subscribe]) +@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe]) async def read_subscribes( db: Session = Depends(get_db), _: User = Depends(get_current_active_superuser)) -> Any: @@ -34,7 +34,7 @@ async def read_subscribes( return Subscribe.list(db) -@router.post("/", response_model=schemas.Response) +@router.post("/", summary="新增订阅", response_model=schemas.Response) async def create_subscribe( *, subscribe_in: schemas.Subscribe, @@ -47,7 +47,7 @@ async def create_subscribe( return schemas.Response(success=result) -@router.put("/", response_model=schemas.Subscribe) +@router.put("/", summary="更新订阅", response_model=schemas.Subscribe) async def update_subscribe( *, db: Session = Depends(get_db), @@ -67,7 +67,7 @@ async def update_subscribe( return subscribe -@router.delete("/", response_model=schemas.Response) +@router.delete("/", summary="删除订阅", response_model=schemas.Response) async def delete_subscribe( *, db: Session = Depends(get_db), @@ -81,7 +81,7 @@ async def delete_subscribe( return schemas.Response(success=True) -@router.post("/seerr", response_model=schemas.Response) +@router.post("/seerr", summary="OverSeerr/JellySeerr通知订阅", response_model=schemas.Response) async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks, authorization: str = Header(None)) -> Any: """ @@ -134,7 +134,7 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks, return schemas.Response(success=True) -@router.get("/refresh", response_model=schemas.Response) +@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response) async def refresh_subscribes( _: User = Depends(get_current_active_superuser)) -> Any: """ @@ -144,7 +144,7 @@ async def refresh_subscribes( return schemas.Response(success=True) -@router.get("/search", response_model=schemas.Response) +@router.get("/search", summary="搜索订阅", response_model=schemas.Response) async def search_subscribes( _: User = Depends(get_current_active_superuser)) -> Any: """ diff --git a/app/api/endpoints/tmdb.py b/app/api/endpoints/tmdb.py index dde14298..bda89b38 100644 --- a/app/api/endpoints/tmdb.py +++ b/app/api/endpoints/tmdb.py @@ -12,10 +12,10 @@ from app.schemas.types import MediaType router = APIRouter() -@router.get("/info", response_model=schemas.MediaInfo) +@router.get("/info", summary="TMDB详情", response_model=schemas.MediaInfo) async def tmdb_info(tmdbid: int, type_name: str) -> Any: """ - 根据TMDBID查询themoviedb媒体信息 + 根据TMDBID查询themoviedb媒体信息,type_name: 电影/电视剧 """ mtype = MediaType.MOVIE if type_name == MediaType.MOVIE.value else MediaType.TV tmdbinfo = TmdbChain().tmdb_info(tmdbid=tmdbid, mtype=mtype) @@ -25,7 +25,7 @@ async def tmdb_info(tmdbid: int, type_name: str) -> Any: return MediaInfo(tmdb_info=tmdbinfo).to_dict() -@router.get("/movies", response_model=List[schemas.MediaInfo]) +@router.get("/movies", summary="TMDB电影", response_model=List[schemas.MediaInfo]) async def tmdb_movies(sort_by: str = "popularity.desc", with_genres: str = "", with_original_language: str = "", @@ -44,7 +44,7 @@ async def tmdb_movies(sort_by: str = "popularity.desc", return [MediaInfo(tmdb_info=movie).to_dict() for movie in movies] -@router.get("/tvs", response_model=List[schemas.MediaInfo]) +@router.get("/tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo]) async def tmdb_tvs(sort_by: str = "popularity.desc", with_genres: str = "", with_original_language: str = "", @@ -63,7 +63,7 @@ async def tmdb_tvs(sort_by: str = "popularity.desc", return [MediaInfo(tmdb_info=tv).to_dict() for tv in tvs] -@router.get("/trending", response_model=List[schemas.MediaInfo]) +@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo]) async def tmdb_trending(page: int = 1, _: User = Depends(get_current_active_user)) -> Any: """ diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index e0f7abe2..fca30e7f 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -12,7 +12,7 @@ from app.db.userauth import get_current_active_superuser, get_current_active_use router = APIRouter() -@router.get("/", response_model=List[schemas.User]) +@router.get("/", summary="所有用户", response_model=List[schemas.User]) async def read_users( db: Session = Depends(get_db), current_user: User = Depends(get_current_active_superuser), @@ -24,7 +24,7 @@ async def read_users( return users -@router.post("/", response_model=schemas.User) +@router.post("/", summary="新增用户", response_model=schemas.User) async def create_user( *, db: Session = Depends(get_db), @@ -49,7 +49,7 @@ async def create_user( return user -@router.put("/", response_model=schemas.User) +@router.put("/", summary="更新用户", response_model=schemas.User) async def update_user( *, db: Session = Depends(get_db), @@ -73,7 +73,7 @@ async def update_user( return user -@router.delete("/", response_model=schemas.Response) +@router.delete("/", summary="删除用户", response_model=schemas.Response) async def delete_user( *, db: Session = Depends(get_db), @@ -93,7 +93,7 @@ async def delete_user( return schemas.Response(success=True) -@router.get("/{user_id}", response_model=schemas.User) +@router.get("/{user_id}", summary="用户详情", response_model=schemas.User) async def read_user_by_id( user_id: int, current_user: User = Depends(get_current_active_user), diff --git a/app/api/endpoints/webhook.py b/app/api/endpoints/webhook.py index 954c3af5..b1daf0ca 100644 --- a/app/api/endpoints/webhook.py +++ b/app/api/endpoints/webhook.py @@ -16,7 +16,7 @@ def start_webhook_chain(body: Any, form: Any, args: Any): WebhookChain().message(body=body, form=form, args=args) -@router.post("/", response_model=schemas.Response) +@router.post("/", summary="Webhook消息响应", response_model=schemas.Response) async def webhook_message(background_tasks: BackgroundTasks, token: str, request: Request) -> Any: """ diff --git a/app/core/plugin.py b/app/core/plugin.py index 6c89929b..775a570d 100644 --- a/app/core/plugin.py +++ b/app/core/plugin.py @@ -102,6 +102,27 @@ class PluginManager(metaclass=Singleton): ret_commands += plugin.get_command() return ret_commands + def get_plugin_apis(self) -> List[Dict[str, Any]]: + """ + 获取插件API + [{ + "path": "/xx", + "endpoint": self.xxx, + "methods": ["GET", "POST"], + "summary": "API名称", + "description": "API说明" + }] + """ + ret_apis = [] + for pid, plugin in self._running_plugins.items(): + if hasattr(plugin, "get_api") \ + and ObjectUtils.check_method(plugin.get_api): + apis = plugin.get_api() + for api in apis: + api["path"] = f"/{pid}{api['path']}" + ret_apis.extend(apis) + return ret_apis + def run_plugin_method(self, pid: str, method: str, *args, **kwargs) -> Any: """ 运行插件方法 diff --git a/app/main.py b/app/main.py index 98bac2c3..370a7741 100644 --- a/app/main.py +++ b/app/main.py @@ -3,8 +3,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from uvicorn import Config -from app.api.apiv1 import api_router -from app.api.servarr import arr_router from app.command import Command from app.core.config import settings from app.core.module import ModuleManager @@ -27,16 +25,22 @@ App.add_middleware( allow_headers=["*"], ) -# API路由 -App.include_router(api_router, prefix=settings.API_V1_STR) - -# Radarr、Sonarr路由 -App.include_router(arr_router, prefix="/api/v3") - # uvicorn服务 Server = uvicorn.Server(Config(App, host=settings.HOST, port=settings.PORT, reload=settings.RELOAD)) +def init_routers(): + """ + 初始化路由 + """ + from app.api.apiv1 import api_router + from app.api.servarr import arr_router + # API路由 + App.include_router(api_router, prefix=settings.API_V1_STR) + # Radarr、Sonarr路由 + App.include_router(arr_router, prefix="/api/v3") + + @App.on_event("shutdown") def shutdown_server(): """ @@ -71,6 +75,8 @@ def start_module(): Command() # 站点管理 SitesHelper() + # 初始化路由 + init_routers() if __name__ == '__main__': diff --git a/app/plugins/__init__.py b/app/plugins/__init__.py index cc76cca0..76da7d15 100644 --- a/app/plugins/__init__.py +++ b/app/plugins/__init__.py @@ -60,6 +60,20 @@ class _PluginBase(metaclass=ABCMeta): """ pass + @abstractmethod + def get_api(self) -> List[Dict[str, Any]]: + """ + 获取插件API + [{ + "path": "/xx", + "endpoint": self.xxx, + "methods": ["GET", "POST"], + "summary": "API名称", + "description": "API说明" + }] + """ + pass + @abstractmethod def stop_service(self): """ diff --git a/app/plugins/autosignin/__init__.py b/app/plugins/autosignin/__init__.py index 966894be..3a268ac4 100644 --- a/app/plugins/autosignin/__init__.py +++ b/app/plugins/autosignin/__init__.py @@ -25,6 +25,7 @@ from app.schemas.types import EventType class AutoSignIn(_PluginBase): + # 插件名称 plugin_name = "站点自动签到" # 插件描述 @@ -78,6 +79,24 @@ class AutoSignIn(_PluginBase): "data": {} }] + def get_api(self) -> List[Dict[str, Any]]: + """ + 获取插件API + [{ + "path": "/xx", + "endpoint": self.xxx, + "methods": ["GET", "POST"], + "summary": "API说明" + }] + """ + return [{ + "path": "/signin_by_domain", + "endpoint": self.signin_by_domain, + "methods": ["GET"], + "summary": "站点签到", + "description": "使用站点域名签到站点", + }] + @eventmanager.register(EventType.SiteSignin) def sign_in(self, event: Event = None): """ diff --git a/app/plugins/sitestatistic/__init__.py b/app/plugins/sitestatistic/__init__.py index 2c01ad64..ef84b72c 100644 --- a/app/plugins/sitestatistic/__init__.py +++ b/app/plugins/sitestatistic/__init__.py @@ -77,6 +77,24 @@ class SiteStatistic(_PluginBase): "data": {} }] + def get_api(self) -> List[Dict[str, Any]]: + """ + 获取插件API + [{ + "path": "/xx", + "endpoint": self.xxx, + "methods": ["GET", "POST"], + "summary": "API说明" + }] + """ + return [{ + "path": "/refresh_by_domain", + "endpoint": self.refresh_by_domain, + "methods": ["GET"], + "summary": "刷新站点数据", + "description": "刷新对应域名的站点数据", + }] + def stop_service(self): pass