diff --git a/backend/src/module/api/__init__.py b/backend/src/module/api/__init__.py index 741548a9..818621b5 100644 --- a/backend/src/module/api/__init__.py +++ b/backend/src/module/api/__init__.py @@ -6,6 +6,7 @@ from .config import router as config_router from .download import router as download_router from .log import router as log_router from .program import router as program_router +from .rss import router as rss_router __all__ = "v1" @@ -17,3 +18,4 @@ v1.include_router(program_router) v1.include_router(download_router) v1.include_router(bangumi_router) v1.include_router(config_router) +v1.include_router(rss_router) diff --git a/backend/src/module/api/auth.py b/backend/src/module/api/auth.py index d670a8c7..19ed3593 100644 --- a/backend/src/module/api/auth.py +++ b/backend/src/module/api/auth.py @@ -3,7 +3,7 @@ from datetime import timedelta from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from module.models.user import User +from module.models.user import User, UserUpdate from module.security.api import ( auth_user, get_current_user, @@ -16,10 +16,11 @@ router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/login", response_model=dict) async def login(form_data: OAuth2PasswordRequestForm = Depends()): - username = form_data.username - password = form_data.password - auth_user(username, password) - token = create_access_token(data={"sub": username}, expires_delta=timedelta(days=1)) + user = User(username=form_data.username, password=form_data.password) + auth_user(user) + token = create_access_token( + data={"sub": user.username}, expires_delta=timedelta(days=1) + ) return {"access_token": token, "token_type": "bearer", "expire": 86400} @@ -44,7 +45,9 @@ async def logout(current_user: User = Depends(get_current_user)): @router.post("/update", response_model=dict) -async def update_user(user_data: User, current_user: User = Depends(get_current_user)): +async def update_user( + user_data: UserUpdate, current_user: User = Depends(get_current_user) +): if not current_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token" diff --git a/backend/src/module/api/bangumi.py b/backend/src/module/api/bangumi.py index dceea760..f4112fca 100644 --- a/backend/src/module/api/bangumi.py +++ b/backend/src/module/api/bangumi.py @@ -2,13 +2,13 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import JSONResponse from module.manager import TorrentManager -from module.models import Bangumi +from module.models import Bangumi, BangumiUpdate from module.security.api import get_current_user router = APIRouter(prefix="/bangumi", tags=["bangumi"]) -@router.get("/getAll", response_model=list[Bangumi]) +@router.get("/get/all", response_model=list[Bangumi]) async def get_all_data(current_user=Depends(get_current_user)): if not current_user: raise HTTPException( @@ -18,7 +18,7 @@ async def get_all_data(current_user=Depends(get_current_user)): return manager.bangumi.search_all() -@router.get("/getData/{bangumi_id}", response_model=Bangumi) +@router.get("/get/{bangumi_id}", response_model=Bangumi) async def get_data(bangumi_id: str, current_user=Depends(get_current_user)): if not current_user: raise HTTPException( @@ -28,17 +28,19 @@ async def get_data(bangumi_id: str, current_user=Depends(get_current_user)): return manager.search_one(bangumi_id) -@router.post("/updateRule") -async def update_rule(data: Bangumi, current_user=Depends(get_current_user)): +@router.patch("/update/{bangumi_id}") +async def update_rule( + bangumi_id: int, data: BangumiUpdate, current_user=Depends(get_current_user) +): if not current_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token" ) with TorrentManager() as manager: - return manager.update_rule(data) + return manager.update_rule(bangumi_id, data) -@router.delete("/deleteRule/{bangumi_id}") +@router.delete("/delete/{bangumi_id}") async def delete_rule( bangumi_id: str, file: bool = False, current_user=Depends(get_current_user) ): @@ -50,7 +52,7 @@ async def delete_rule( return manager.delete_rule(bangumi_id, file) -@router.delete("/disableRule/{bangumi_id}") +@router.delete("/disable/{bangumi_id}") async def disable_rule( bangumi_id: str, file: bool = False, current_user=Depends(get_current_user) ): @@ -62,7 +64,7 @@ async def disable_rule( return manager.disable_rule(bangumi_id, file) -@router.get("/enableRule/{bangumi_id}") +@router.get("/enable/{bangumi_id}") async def enable_rule(bangumi_id: str, current_user=Depends(get_current_user)): if not current_user: raise HTTPException( @@ -72,7 +74,7 @@ async def enable_rule(bangumi_id: str, current_user=Depends(get_current_user)): return manager.enable_rule(bangumi_id) -@router.get("/resetAll") +@router.get("/reset/all") async def reset_all(current_user=Depends(get_current_user)): if not current_user: raise HTTPException( diff --git a/backend/src/module/api/program.py b/backend/src/module/api/program.py index b45f8978..f9422126 100644 --- a/backend/src/module/api/program.py +++ b/backend/src/module/api/program.py @@ -1,7 +1,6 @@ import logging import os import signal -import sys from fastapi import APIRouter, Depends, HTTPException, status diff --git a/backend/src/module/api/rss.py b/backend/src/module/api/rss.py index 4e4b1102..7aa33d9a 100644 --- a/backend/src/module/api/rss.py +++ b/backend/src/module/api/rss.py @@ -1,9 +1,9 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import JSONResponse -from module.models import RSSItem +from module.models import RSSItem, RSSUpdate from module.rss import RSSEngine -from module.security import get_current_user +from module.security.api import get_current_user from module.downloader import DownloadClient @@ -20,34 +20,36 @@ async def get_rss(current_user=Depends(get_current_user)): return engine.rss.search_all() -@router.delete("/delete/{id}") -async def delete_rss(_id: int, current_user=Depends(get_current_user)): +@router.delete("/delete/{rss_id}") +async def delete_rss(rss_id: int, current_user=Depends(get_current_user)): if not current_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token" ) with RSSEngine() as engine: - result = engine.rss.delete(_id) + result = engine.rss.delete(rss_id) if result: return JSONResponse( status_code=status.HTTP_200_OK, - content={"status": f"Success deleted {_id}"}, + content={"status": f"Success deleted {rss_id}"}, ) else: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"status": f"Failed to delete {_id}"}, + content={"status": f"Failed to delete {rss_id}"}, ) -@router.put("/update/{id}") -async def update_rss(data: RSSItem, current_user=Depends(get_current_user)): +@router.patch("/update/{rss_id}") +async def update_rss( + rss_id: int, data: RSSUpdate, current_user=Depends(get_current_user) +): if not current_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token" ) with RSSEngine() as engine: - result = engine.rss.update(data) + result = engine.rss.update(rss_id, data) if result: return JSONResponse( status_code=status.HTTP_200_OK, @@ -66,8 +68,8 @@ async def refresh_all(current_user=Depends(get_current_user)): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token" ) - with RSSEngine() as engine: - response = engine.refresh_rss() + with RSSEngine() as engine, DownloadClient() as client: + response = engine.refresh_rss(client) if response: return JSONResponse( status_code=status.HTTP_200_OK, @@ -80,21 +82,21 @@ async def refresh_all(current_user=Depends(get_current_user)): ) -@router.get("/refresh/{id}") -async def refresh_rss(_id: int, current_user=Depends(get_current_user)): +@router.get("/refresh/{rss_id}") +async def refresh_rss(rss_id: int, current_user=Depends(get_current_user)): if not current_user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token" ) - with RSSEngine() as engine: - response = engine.refresh_rss(_id) + with RSSEngine() as engine, DownloadClient() as client: + response = engine.refresh_rss(client, rss_id) if response: return JSONResponse( status_code=status.HTTP_200_OK, - content={"status": f"Success refresh {_id}"}, + content={"status": f"Success refresh {rss_id}"}, ) else: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"status": f"Failed to refresh {_id}"}, - ) \ No newline at end of file + content={"status": f"Failed to refresh {rss_id}"}, + ) diff --git a/backend/src/module/database/rss.py b/backend/src/module/database/rss.py index f213a2e6..4d9087bc 100644 --- a/backend/src/module/database/rss.py +++ b/backend/src/module/database/rss.py @@ -2,8 +2,7 @@ import logging from sqlmodel import Session, select, delete -from .engine import engine -from module.models import RSSItem +from module.models import RSSItem, RSSUpdate logger = logging.getLogger(__name__) @@ -25,12 +24,21 @@ class RSSDatabase: self.session.commit() self.session.refresh(data) - def update(self, data: RSSItem): - self.session.add(data) + def update(self, _id: int, data: RSSUpdate): + # Check if exists + statement = select(RSSItem).where(RSSItem.id == _id) + db_data = self.session.exec(statement).first() + if not db_data: + return False + # Update + dict_data = data.dict(exclude_unset=True) + for key, value in dict_data.items(): + setattr(db_data, key, value) + self.session.add(db_data) self.session.commit() - self.session.refresh(data) + self.session.refresh(db_data) + return True - # TODO: Check if this is needed def search_id(self, _id: int) -> RSSItem: return self.session.get(RSSItem, _id) diff --git a/backend/src/module/database/user.py b/backend/src/module/database/user.py index 9664915a..62bc8ba2 100644 --- a/backend/src/module/database/user.py +++ b/backend/src/module/database/user.py @@ -20,7 +20,7 @@ class UserDatabase: raise HTTPException(status_code=404, detail="User not found") return result - def auth_user(self, user: UserLogin) -> bool: + def auth_user(self, user: User) -> bool: statement = select(User).where(User.username == user.username) result = self.session.exec(statement).first() if not result: diff --git a/backend/src/module/downloader/path.py b/backend/src/module/downloader/path.py index 8f6fcc16..86d2e8e2 100644 --- a/backend/src/module/downloader/path.py +++ b/backend/src/module/downloader/path.py @@ -4,7 +4,7 @@ import re from pathlib import Path from module.conf import settings -from module.models import Bangumi +from module.models import Bangumi, BangumiUpdate logger = logging.getLogger(__name__) @@ -49,7 +49,7 @@ class TorrentPath: return self._file_depth(file_path) <= 2 @staticmethod - def _gen_save_path(data: Bangumi): + def _gen_save_path(data: Bangumi | BangumiUpdate): folder = ( f"{data.official_title} ({data.year})" if data.year else data.official_title ) diff --git a/backend/src/module/manager/torrent.py b/backend/src/module/manager/torrent.py index 72022d70..6492bb41 100644 --- a/backend/src/module/manager/torrent.py +++ b/backend/src/module/manager/torrent.py @@ -4,14 +4,14 @@ from fastapi.responses import JSONResponse from module.database import Database from module.downloader import DownloadClient -from module.models import Bangumi +from module.models import Bangumi, BangumiUpdate logger = logging.getLogger(__name__) class TorrentManager(Database): @staticmethod - def __match_torrents_list(data: Bangumi) -> list: + def __match_torrents_list(data: Bangumi | BangumiUpdate) -> list: with DownloadClient() as client: torrents = client.get_torrent_info(status_filter=None) return [ @@ -97,16 +97,14 @@ class TorrentManager(Database): status_code=406, content={"msg": f"Can't find bangumi id {_id}"} ) - def update_rule(self, data: Bangumi): - old_data = self.bangumi.search_id(data.id) + def update_rule(self, bangumi_id, data: BangumiUpdate): + old_data = self.bangumi.search_id(bangumi_id) if not old_data: - logger.error(f"[Manager] Can't find data with {data.id}") - return JSONResponse( - status_code=406, content={"msg": f"Can't find data with {data.id}"} - ) + logger.error(f"[Manager] Can't find data with {bangumi_id}") + return {"status": False, "msg": f"Can't find data with {bangumi_id}"} else: # Move torrent - match_list = self.__match_torrents_list(data) + match_list = self.__match_torrents_list(old_data.save_path) with DownloadClient() as client: path = client._gen_save_path(data) if match_list: diff --git a/backend/src/module/models/bangumi.py b/backend/src/module/models/bangumi.py index a4984111..82ee9747 100644 --- a/backend/src/module/models/bangumi.py +++ b/backend/src/module/models/bangumi.py @@ -43,7 +43,7 @@ class BangumiUpdate(SQLModel): subtitle: Optional[str] = Field(alias="subtitle", title="字幕") eps_collect: bool = Field(default=False, alias="eps_collect", title="是否已收集") offset: int = Field(default=0, alias="offset", title="番剧偏移量") - filter: str = Field(default="720, \\d+-\\d+", alias="filter", title="番剧过滤器") + filter: str = Field(default="720,\\d+-\\d+", alias="filter", title="番剧过滤器") rss_link: str = Field(default="", alias="rss_link", title="番剧RSS链接") poster_link: Optional[str] = Field(alias="poster_link", title="番剧海报链接") added: bool = Field(default=False, alias="added", title="是否已添加") diff --git a/backend/src/module/models/response.py b/backend/src/module/models/response.py new file mode 100644 index 00000000..4ade25d6 --- /dev/null +++ b/backend/src/module/models/response.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, Field + + +class ResponseModel(BaseModel): + status: bool = Field(..., example=True) + status_code: int = Field(..., example=200) + msg_en: str + msg_zh: str diff --git a/backend/src/module/rss/engine.py b/backend/src/module/rss/engine.py index 802d0bbe..9e82a19b 100644 --- a/backend/src/module/rss/engine.py +++ b/backend/src/module/rss/engine.py @@ -58,6 +58,7 @@ class RSSEngine(Database): rss_item = self.rss.search_id(rss_id) rss_items = [rss_item] if rss_item else [] # From RSS Items, get all torrents + logger.debug(f"[Engine] Get {len(rss_items)} RSS items") for rss_item in rss_items: new_torrents = self.pull_rss(rss_item) # Get all enabled bangumi data @@ -65,6 +66,7 @@ class RSSEngine(Database): matched_data = self.match_torrent(torrent) if matched_data: if client.add_torrent(torrent, matched_data): + logger.debug(f"[Engine] Add torrent {torrent.name} to client") torrent.downloaded = True # Add all torrents to database self.torrent.add_all(new_torrents) diff --git a/backend/src/module/security/api.py b/backend/src/module/security/api.py index 3fd59331..16e749fc 100644 --- a/backend/src/module/security/api.py +++ b/backend/src/module/security/api.py @@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from module.database import Database -from module.models.user import User +from module.models.user import User, UserUpdate from .jwt import verify_token @@ -38,7 +38,7 @@ async def get_token_data(token: str = Depends(oauth2_scheme)): return payload -def update_user_info(user_data: User, current_user): +def update_user_info(user_data: UserUpdate, current_user): try: with Database() as db: db.user.update_user(current_user.username, user_data) @@ -47,6 +47,6 @@ def update_user_info(user_data: User, current_user): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -def auth_user(username, password): +def auth_user(user: User): with Database() as db: - db.user.auth_user(username, password) + db.user.auth_user(user) diff --git a/webui/src/api/bangumi.ts b/webui/src/api/bangumi.ts index 2ae3268a..ca45c6d6 100644 --- a/webui/src/api/bangumi.ts +++ b/webui/src/api/bangumi.ts @@ -1,4 +1,4 @@ -import type { BangumiRule } from '#/bangumi'; +import type { BangumiRule, BangumiUpdate } from '#/bangumi'; import type { ApiSuccess } from '#/api'; export const apiBangumi = { @@ -7,7 +7,7 @@ export const apiBangumi = { * @returns 所有 bangumi 数据 */ async getAll() { - const { data } = await axios.get('api/v1/bangumi/getAll'); + const { data } = await axios.get('api/v1/bangumi/get/all'); return data; }, @@ -19,7 +19,7 @@ export const apiBangumi = { */ async getRule(bangumiId: number) { const { data } = await axios.get( - `api/v1/bangumi/getRule/${bangumiId}` + `api/v1/bangumi/get/${bangumiId}` ); return data; @@ -27,12 +27,13 @@ export const apiBangumi = { /** * 更新指定 bangumiId 的规则 - * @param bangumiData - 需要更新的规则 + * @param bangumiId - 需要更新的 bangumi 的 id + * @param bangumiRule * @returns axios 请求返回的数据 */ - async updateRule(bangumiRule: BangumiRule) { - const { data } = await axios.post( - 'api/v1/bangumi/updateRule', + async updateRule(bangumiId: number, bangumiRule: BangumiUpdate) { + const { data } = await axios.patch( + `api/v1/bangumi/update/${bangumiId}`, bangumiRule ); return data; @@ -46,7 +47,7 @@ export const apiBangumi = { */ async deleteRule(bangumiId: number, file: boolean) { const { data } = await axios.delete( - `api/v1/bangumi/deleteRule/${bangumiId}`, + `api/v1/bangumi/delete/${bangumiId}`, { params: { file, @@ -64,7 +65,7 @@ export const apiBangumi = { */ async disableRule(bangumiId: number, file: boolean) { const { data } = await axios.delete( - `api/v1/bangumi/disableRule/${bangumiId}`, + `api/v1/bangumi/disable/${bangumiId}`, { params: { file, @@ -80,7 +81,7 @@ export const apiBangumi = { */ async enableRule(bangumiId: number) { const { data } = await axios.get( - `api/v1/bangumi/enableRule/${bangumiId}` + `api/v1/bangumi/enable/${bangumiId}` ); return data; }, diff --git a/webui/types/bangumi.ts b/webui/types/bangumi.ts index eeed433d..17c56924 100644 --- a/webui/types/bangumi.ts +++ b/webui/types/bangumi.ts @@ -19,3 +19,24 @@ export interface BangumiRule { title_raw: string; year: string | null; } + +export interface BangumiUpdate { + added: boolean; + deleted: boolean; + dpi: string; + eps_collect: boolean; + filter: string; + group_name: string; + official_title: string; + offset: number; + poster_link: string | null; + rss_link: string; + rule_name: string; + save_path: string; + season: number; + season_raw: string; + source: string | null; + subtitle: string; + title_raw: string; + year: string | null; +}