diff --git a/app/api/apiv1.py b/app/api/apiv1.py index 8336e190..1dce06d1 100644 --- a/app/api/apiv1.py +++ b/app/api/apiv1.py @@ -2,7 +2,7 @@ from fastapi import APIRouter from app.api.endpoints import login, user, site, message, webhook, subscribe, \ media, douban, search, plugin, tmdb, history, system, download, dashboard, \ - local, transfer, mediaserver, bangumi, aliyun + local, transfer, mediaserver, bangumi, aliyun, u115 api_router = APIRouter() api_router.include_router(login.router, prefix="/login", tags=["login"]) @@ -25,3 +25,4 @@ api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"] api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"]) api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"]) api_router.include_router(aliyun.router, prefix="/aliyun", tags=["aliyun"]) +api_router.include_router(u115.router, prefix="/u115", tags=["115"]) diff --git a/app/api/endpoints/u115.py b/app/api/endpoints/u115.py new file mode 100644 index 00000000..aa193b8f --- /dev/null +++ b/app/api/endpoints/u115.py @@ -0,0 +1,198 @@ +from pathlib import Path +from typing import Any, List + +from fastapi import APIRouter, Depends +from starlette.responses import Response + +from app import schemas +from app.chain.transfer import TransferChain +from app.core.config import settings +from app.core.metainfo import MetaInfoPath +from app.core.security import verify_token, verify_uri_token +from app.helper.u115 import U115Helper +from app.utils.string import StringUtils + +router = APIRouter() + + +@router.get("/qrcode", summary="生成二维码内容", response_model=schemas.Response) +def qrcode(_: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 生成二维码 + """ + qrcode_data, errmsg = U115Helper().generate_qrcode() + if qrcode_data: + return schemas.Response(success=True, data=qrcode_data) + return schemas.Response(success=False, message=errmsg) + + +@router.get("/check", summary="二维码登录确认", response_model=schemas.Response) +def check(ck: str, t: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 二维码登录确认 + """ + if not ck or not t: + return schemas.Response(success=False, message="参数错误") + data, errmsg = U115Helper().check_login(ck, t) + if data: + return schemas.Response(success=True, data=data) + return schemas.Response(success=False, message=errmsg) + + +@router.get("/userinfo", summary="查询用户信息", response_model=schemas.Response) +def userinfo(_: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 查询用户信息 + """ + pass + + +@router.get("/list", summary="所有目录和文件(115网盘)", response_model=List[schemas.FileItem]) +def list_115(path: str, + fileid: str, + filetype: str = "dir", + sort: str = 'updated_at', + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 查询当前目录下所有目录和文件 + :param path: 当前路径 + :param fileid: 文件ID + :param filetype: 文件类型 + :param sort: 排序方式,name:按名称排序,time:按修改时间排序 + :param _: token + :return: 所有目录和文件 + """ + if not fileid: + return [] + if not path: + path = "/" + if sort == "time": + sort = "updated_at" + if filetype == "file": + fileinfo = U115Helper().get_file_detail(fileid) + if fileinfo: + return [schemas.FileItem( + fileid=fileinfo.get("file_id"), + parent_fileid=fileinfo.get("parent_file_id"), + type="file", + path=f"{path}{fileinfo.get('name')}", + name=fileinfo.get("name"), + size=fileinfo.get("size"), + extension=fileinfo.get("file_extension"), + modify_time=StringUtils.str_to_timestamp(fileinfo.get("updated_at")), + thumbnail=fileinfo.get("thumbnail") + )] + return [] + items = U115Helper().list_files(parent_file_id=fileid) + if not items: + return [] + return [schemas.FileItem( + fileid=item.get("file_id"), + parent_fileid=item.get("parent_file_id"), + type="dir" if item.get("type") == "folder" else "file", + path=f"{path}{item.get('name')}" + "/" if item.get("type") == "folder" else "", + name=item.get("name"), + size=item.get("size"), + extension=item.get("file_extension"), + modify_time=StringUtils.str_to_timestamp(item.get("updated_at")), + thumbnail=item.get("thumbnail") + ) for item in items] + + +@router.get("/mkdir", summary="创建目录(115网盘)", response_model=schemas.Response) +def mkdir_115(fileid: str, + name: str, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 创建目录 + """ + if not fileid or not name: + return schemas.Response(success=False) + result = U115Helper().create_folder(parent_file_id=fileid, name=name) + if result: + return schemas.Response(success=True) + return schemas.Response(success=False) + + +@router.get("/delete", summary="删除文件或目录(115网盘)", response_model=schemas.Response) +def delete_115(fileid: str, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 删除文件或目录 + """ + if not fileid: + return schemas.Response(success=False) + result = U115Helper().delete_file(fileid) + if result: + return schemas.Response(success=True) + return schemas.Response(success=False) + + +@router.get("/download", summary="下载文件(115网盘)") +def download_115(fileid: str, + _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: + """ + 下载文件或目录 + """ + if not fileid: + return schemas.Response(success=False) + url = U115Helper().get_download_url(fileid) + if url: + # 重定向 + return Response(status_code=302, headers={"Location": url}) + return schemas.Response(success=False) + + +@router.get("/rename", summary="重命名文件或目录(115网盘)", response_model=schemas.Response) +def rename_115(fileid: str, new_name: str, path: str, + recursive: bool = False, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 重命名文件或目录 + """ + if not fileid or not new_name: + return schemas.Response(success=False) + result = U115Helper().rename_file(fileid, new_name) + if result: + if recursive: + transferchain = TransferChain() + media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIO_TRACK_EXT + # 递归修改目录内文件(智能识别命名) + sub_files: List[schemas.FileItem] = list_115(path=path, fileid=fileid) + for sub_file in sub_files: + if sub_file.type == "dir": + continue + if not sub_file.extension: + continue + if f".{sub_file.extension.lower()}" not in media_exts: + continue + sub_path = Path(f"{path}{sub_file.name}") + meta = MetaInfoPath(sub_path) + mediainfo = transferchain.recognize_media(meta) + if not mediainfo: + return schemas.Response(success=False, message=f"{sub_path.name} 未识别到媒体信息") + new_path = transferchain.recommend_name(meta=meta, mediainfo=mediainfo) + if not new_path: + return schemas.Response(success=False, message=f"{sub_path.name} 未识别到新名称") + ret: schemas.Response = rename_115(fileid=sub_file.fileid, + path=path, + new_name=Path(new_path).name, + recursive=False) + if not ret.success: + return schemas.Response(success=False, message=f"{sub_path.name} 重命名失败!") + return schemas.Response(success=True) + return schemas.Response(success=False) + + +@router.get("/image", summary="读取图片(115网盘)", response_model=schemas.Response) +def image_115(fileid: str, _: schemas.TokenPayload = Depends(verify_uri_token)) -> Any: + """ + 读取图片 + """ + if not fileid: + return schemas.Response(success=False) + url = U115Helper().get_download_url(fileid) + if url: + # 重定向 + return Response(status_code=302, headers={"Location": url}) + return schemas.Response(success=False) diff --git a/app/helper/u115.py b/app/helper/u115.py new file mode 100644 index 00000000..d539d8d5 --- /dev/null +++ b/app/helper/u115.py @@ -0,0 +1,142 @@ +from typing import Optional, Tuple, List + +import py115 +from py115 import Cloud +from py115.types import LoginTarget, QrcodeSession, QrcodeStatus + +from app.db.systemconfig_oper import SystemConfigOper +from app.schemas.types import SystemConfigKey +from app.utils.singleton import Singleton +from app.utils.system import SystemUtils + + +class U115Helper(metaclass=Singleton): + """ + 115相关操作 + """ + + cloud: Cloud = None + session: QrcodeSession = None + + def __init__(self): + self.systemconfig = SystemConfigOper() + + @property + def cookies(self): + """ + 获取115认证参数并初始化参数格式 + """ + return self.systemconfig.get(SystemConfigKey.User115Params) or {} + + def save_credentail(self, cookies: dict): + """ + 设置115认证参数 + """ + self.systemconfig.set(SystemConfigKey.User115Params, cookies) + + def clear_params(self): + """ + 清除115认证参数 + """ + self.systemconfig.delete(SystemConfigKey.User115Params) + + def generate_qrcode(self) -> Optional[Tuple[dict, str]]: + """ + 生成二维码 + """ + + def __get_os(): + """ + 获取操作系统名称 + """ + if SystemUtils.is_windows(): + return LoginTarget.Windows + elif SystemUtils.is_macos(): + return LoginTarget.Mac + else: + return LoginTarget.Linux + + try: + self.cloud = py115.connect() + self.session = self.cloud.qrcode_login(__get_os) + return self.session.image_data, "" + except Exception as e: + return None, f"115生成二维码失败:{str(e)}" + + def check_login(self, ck: str, t: str) -> Optional[Tuple[dict, str]]: + """ + 二维码登录确认 + """ + if not self.session: + return None, "请先生成二维码!" + try: + status = self.cloud.qrcode_poll(self.session) + if status == QrcodeStatus.Done: + # 确认完成,保存认证信息 + self.save_credentail(self.cloud.export_credentail()) + elif status == QrcodeStatus.Waiting: + return { + "status": 0, + "tip": "等待扫码确认..." + }, "" + elif status == QrcodeStatus.Expired: + return { + "status": -1, + "tip": "二维码已过期,请重新刷新!" + }, "" + elif status == QrcodeStatus.Failed: + return { + "status": -2, + "tip": "登录失败,请重试!" + }, "" + return None, "登录确认失败!" + except Exception as e: + return None, f"115登录确认失败:{str(e)}" + + def list_files(self, parent_file_id: str = '0') -> List[dict]: + """ + 浏览文件 + """ + cookies = self.cookies + if not cookies: + return [] + return self.cloud.storage().list(dir_id=parent_file_id) + + def create_folder(self, parent_file_id: str, name: str) -> bool: + """ + 创建目录 + """ + cookies = self.cookies + if not cookies: + return False + return self.cloud.storage().make_dir(parent_file_id, name) + + def delete_file(self, file_id: str) -> bool: + """ + 删除文件 + """ + cookies = self.cookies + if not cookies: + return False + return self.cloud.storage().delete(file_id) + + def get_file_detail(self, file_id: str) -> Optional[dict]: + """ + 获取文件详情 + """ + pass + + def rename_file(self, file_id: str, name: str) -> bool: + """ + 重命名文件 + """ + cookies = self.cookies + if not cookies: + return False + return self.cloud.storage().rename(file_id, name) + + def get_download_url(self, file_id: str) -> Optional[str]: + """ + 获取下载链接 + """ + pass diff --git a/app/schemas/types.py b/app/schemas/types.py index a4922ff6..313b04e1 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -96,6 +96,8 @@ class SystemConfigKey(Enum): LibraryDirectories = "LibraryDirectories" # 阿里云盘认证参数 UserAliyunParams = "UserAliyunParams" + # 115网盘认证参数 + User115Params = "User115Params" # 处理进度Key字典 diff --git a/requirements.txt b/requirements.txt index 71cbdd64..c7c321d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,3 +58,4 @@ pystray~=0.19.5 pyotp~=2.9.0 Pinyin2Hanzi~=0.1.1 pywebpush~=2.0.0 + py115~=0.0.4 \ No newline at end of file