From 4074a52600a46b90ff44464c112f671d47c2ef8b Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 13 Jun 2023 17:26:19 +0800 Subject: [PATCH] add sonarr/radarr api --- app/api/endpoints/sites.py | 2 +- app/api/endpoints/subscribes.py | 4 +- app/api/servarr.py | 387 ++++++++++++++++++++++++++++++++ app/main.py | 4 + app/schemas/__init__.py | 1 + app/schemas/servarr.py | 54 +++++ 6 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 app/api/servarr.py create mode 100644 app/schemas/servarr.py diff --git a/app/api/endpoints/sites.py b/app/api/endpoints/sites.py index 08f86269..7ad5a2e2 100644 --- a/app/api/endpoints/sites.py +++ b/app/api/endpoints/sites.py @@ -22,7 +22,7 @@ async def read_sites(db: Session = Depends(get_db), return Site.list(db) -@router.post("/update", response_model=schemas.Site) +@router.put("/", response_model=schemas.Site) async def update_site( *, db: Session = Depends(get_db), diff --git a/app/api/endpoints/subscribes.py b/app/api/endpoints/subscribes.py index a17b16d0..22711b5a 100644 --- a/app/api/endpoints/subscribes.py +++ b/app/api/endpoints/subscribes.py @@ -47,7 +47,7 @@ async def create_subscribe( return {"success": result} -@router.post("/update", response_model=schemas.Subscribe) +@router.put("/", response_model=schemas.Subscribe) async def update_subscribe( *, db: Session = Depends(get_db), @@ -67,7 +67,7 @@ async def update_subscribe( return subscribe -@router.post("/delete", response_model=schemas.Response) +@router.delete("/", response_model=schemas.Response) async def delete_subscribe( *, db: Session = Depends(get_db), diff --git a/app/api/servarr.py b/app/api/servarr.py new file mode 100644 index 00000000..2e753e12 --- /dev/null +++ b/app/api/servarr.py @@ -0,0 +1,387 @@ +from typing import Any, List + +from fastapi import APIRouter, HTTPException + +from app import schemas +from app.core.config import settings +from version import APP_VERSION + +arr_router = APIRouter() + + +@arr_router.get("/system/status") +async def arr_system_status(apiKey: str) -> Any: + """ + 模拟Radarr、Sonarr系统状态 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + return { + "appName": "MoviePilot", + "instanceName": "moviepilot", + "version": APP_VERSION, + "urlBase": "/api/v3" + } + + +@arr_router.get("/qualityProfile") +async def arr_qualityProfile(apiKey: str) -> Any: + """ + 模拟Radarr、Sonarr质量配置 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + return [ + { + "id": 1, + "name": "默认" + } + ] + + +@arr_router.get("/rootfolder") +async def arr_rootfolder(apiKey: str) -> Any: + """ + 模拟Radarr、Sonarr根目录 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + return [ + { + "id": 1, + "path": settings.LIBRARY_PATH, + "accessible": True, + "freeSpace": 0, + "unmappedFolders": [] + } + ] + + +@arr_router.get("/tag") +async def arr_tag(apiKey: str) -> Any: + """ + 模拟Radarr、Sonarr标签 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + return [ + { + "id": 1, + "label": "默认" + } + ] + + +@arr_router.get("/languageprofile") +async def arr_languageprofile(apiKey: str) -> Any: + """ + 模拟Radarr、Sonarr语言 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + return { + "id": 1, + "name": "中文" + } + + +@arr_router.get("/movie", response_model=List[schemas.RadarrMovie]) +async def arr_movies(apiKey: str) -> Any: + """ + 查询Rardar电影 + """ + """ + [ + { + "id": 0, + "title": "string", + "originalTitle": "string", + "originalLanguage": { + "id": 0, + "name": "string" + }, + "secondaryYear": 0, + "secondaryYearSourceId": 0, + "sortTitle": "string", + "sizeOnDisk": 0, + "status": "tba", + "overview": "string", + "inCinemas": "2023-06-13T09:23:41.494Z", + "physicalRelease": "2023-06-13T09:23:41.494Z", + "digitalRelease": "2023-06-13T09:23:41.494Z", + "physicalReleaseNote": "string", + "images": [ + { + "coverType": "unknown", + "url": "string", + "remoteUrl": "string" + } + ], + "website": "string", + "remotePoster": "string", + "year": 0, + "hasFile": true, + "youTubeTrailerId": "string", + "studio": "string", + "path": "string", + "qualityProfileId": 0, + "monitored": true, + "minimumAvailability": "tba", + "isAvailable": true, + "folderName": "string", + "runtime": 0, + "cleanTitle": "string", + "imdbId": "string", + "tmdbId": 0, + "titleSlug": "string", + "rootFolderPath": "string", + "folder": "string", + "certification": "string", + "genres": [ + "string" + ], + "tags": [ + 0 + ], + "added": "2023-06-13T09:23:41.494Z", + "addOptions": { + "ignoreEpisodesWithFiles": true, + "ignoreEpisodesWithoutFiles": true, + "monitor": "movieOnly", + "searchForMovie": true, + "addMethod": "manual" + }, + "popularity": 0 + } + ] + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.get("/movie/{mid}", response_model=schemas.RadarrMovie) +async def arr_movie(apiKey: str) -> Any: + """ + 查询Rardar电影 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.get("/movie/lookup", response_model=List[schemas.RadarrMovie]) +async def arr_movie_lookup(apiKey: str, term: str) -> Any: + """ + 查询Rardar电影 term: `tmdb:${id}` + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.put("/movie", response_model=schemas.Response) +async def arr_add_movie(apiKey: str, title: str, tmdbId: int, year: int) -> Any: + """ + 新增Rardar电影订阅 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.delete("/movie/{mid}", response_model=schemas.Response) +async def arr_remove_movie(apiKey: str, mid: int) -> Any: + """ + 删除Rardar电影订阅 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.get("/series", response_model=List[schemas.SonarrSeries]) +async def arr_series(apiKey: str) -> Any: + """ + 查询Sonarr剧集 + """ + """ + [ + { + "id": 0, + "title": "string", + "sortTitle": "string", + "status": "continuing", + "ended": true, + "profileName": "string", + "overview": "string", + "nextAiring": "2023-06-13T09:08:17.624Z", + "previousAiring": "2023-06-13T09:08:17.624Z", + "network": "string", + "airTime": "string", + "images": [ + { + "coverType": "unknown", + "url": "string", + "remoteUrl": "string" + } + ], + "originalLanguage": { + "id": 0, + "name": "string" + }, + "remotePoster": "string", + "seasons": [ + { + "seasonNumber": 0, + "monitored": true, + "statistics": { + "nextAiring": "2023-06-13T09:08:17.624Z", + "previousAiring": "2023-06-13T09:08:17.624Z", + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 0, + "sizeOnDisk": 0, + "releaseGroups": [ + "string" + ], + "percentOfEpisodes": 0 + }, + "images": [ + { + "coverType": "unknown", + "url": "string", + "remoteUrl": "string" + } + ] + } + ], + "year": 0, + "path": "string", + "qualityProfileId": 0, + "seasonFolder": true, + "monitored": true, + "useSceneNumbering": true, + "runtime": 0, + "tvdbId": 0, + "tvRageId": 0, + "tvMazeId": 0, + "firstAired": "2023-06-13T09:08:17.624Z", + "seriesType": "standard", + "cleanTitle": "string", + "imdbId": "string", + "titleSlug": "string", + "rootFolderPath": "string", + "folder": "string", + "certification": "string", + "genres": [ + "string" + ], + "tags": [ + 0 + ], + "added": "2023-06-13T09:08:17.624Z", + "addOptions": { + "ignoreEpisodesWithFiles": true, + "ignoreEpisodesWithoutFiles": true, + "monitor": "unknown", + "searchForMissingEpisodes": true, + "searchForCutoffUnmetEpisodes": true + }, + "ratings": { + "votes": 0, + "value": 0 + }, + "statistics": { + "seasonCount": 0, + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 0, + "sizeOnDisk": 0, + "releaseGroups": [ + "string" + ], + "percentOfEpisodes": 0 + }, + "episodesChanged": true + } + ] + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.get("/series/{tid}") +async def arr_serie(apiKey: str) -> Any: + """ + 查询Sonarr剧集 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.get("/series/lookup") +async def arr_series_lookup(apiKey: str, term: str) -> Any: + """ + 查询Sonarr剧集 term: `tvdb:${id}` title + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.put("/series") +async def arr_add_series(apiKey: str, title: str, seasons: list, year: int) -> Any: + """ + 新增Sonarr剧集订阅 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) + + +@arr_router.delete("/series/{tid}") +async def arr_remove_series(apiKey: str, tid: int) -> Any: + """ + 删除Sonarr剧集订阅 + """ + if not apiKey or apiKey != settings.API_TOKEN: + raise HTTPException( + status_code=403, + detail="认证失败!", + ) diff --git a/app/main.py b/app/main.py index 7365e2f8..75d78f9f 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ from fastapi import FastAPI 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 @@ -19,6 +20,9 @@ App = FastAPI(title=settings.PROJECT_NAME, # 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)) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 1a3b83bf..59ab5919 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -4,3 +4,4 @@ from .response import Response from .site import Site from .subscribe import Subscribe from .context import Context +from .servarr import RadarrMovie, SonarrSeries diff --git a/app/schemas/servarr.py b/app/schemas/servarr.py new file mode 100644 index 00000000..28e4f77c --- /dev/null +++ b/app/schemas/servarr.py @@ -0,0 +1,54 @@ +from typing import Optional +from pydantic import BaseModel + + +class RadarrMovie(BaseModel): + id: int + title: str + isAvailable: bool + monitored: bool + tmdbId: Optional[int] + imdbId: Optional[str] + titleSlug: Optional[str] + folderName: Optional[str] + path: Optional[str] + profileId: Optional[int] + qualityProfileId: Optional[int] + added: Optional[str] + hasFile: bool + + +class SonarrSeries: + title: str + sortTitle: Optional[str] + seasonCount: Optional[int] + status: Optional[str] + overview: Optional[str] + network: Optional[str] + airTime: Optional[str] + images: Optional[list] + remotePoster: Optional[str] + seasons: Optional[list] + year: int + path: Optional[str] + profileId: Optional[int] + languageProfileId: Optional[int] + seasonFolder: Optional[bool] + monitored: Optional[bool] + useSceneNumbering: Optional[bool] + runtime: Optional[int] + tvdbId: Optional[int] + tvRageId: Optional[int] + tvMazeId: Optional[int] + firstAired: Optional[str] + seriesType: Optional[str] + cleanTitle: Optional[str] + imdbId: Optional[str] + titleSlug: Optional[str] + certification: Optional[str] + genres: Optional[list] + tags: Optional[list] + added: Optional[str] + ratings: Optional[dict] + qualityProfileId: Optional[int] + statistics: Optional[dict]