diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index b19c36bf..da058ed4 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -462,6 +462,17 @@ def subscribe_share( return schemas.Response(success=state, message=errmsg) +@router.delete("/share/{share_id}", summary="删除分享", response_model=schemas.Response) +def subscribe_share_delete( + share_id: int, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 删除分享 + """ + state, errmsg = SubscribeHelper().share_delete(share_id=share_id) + return schemas.Response(success=state, message=errmsg) + + @router.post("/fork", summary="复用订阅", response_model=schemas.Response) def subscribe_fork( sub: schemas.SubscribeShare, diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 0254fa0a..429fad8f 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -174,6 +174,10 @@ def get_global_setting(): exclude={"SECRET_KEY", "RESOURCE_SECRET_KEY", "API_TOKEN", "TMDB_API_KEY", "TVDB_API_KEY", "FANART_API_KEY", "COOKIECLOUD_KEY", "COOKIECLOUD_PASSWORD", "GITHUB_TOKEN", "REPO_GITHUB_TOKEN"} ) + # 追加用户唯一ID + info.update({ + "USER_UNIQUE_ID": SystemUtils.generate_user_unique_id() + }) return schemas.Response(success=True, data=info) diff --git a/app/helper/subscribe.py b/app/helper/subscribe.py index fe64022b..9ac6377a 100644 --- a/app/helper/subscribe.py +++ b/app/helper/subscribe.py @@ -9,6 +9,7 @@ from app.db.systemconfig_oper import SystemConfigOper from app.schemas.types import SystemConfigKey from app.utils.http import RequestUtils from app.utils.singleton import Singleton +from app.utils.system import SystemUtils class SubscribeHelper(metaclass=Singleton): @@ -34,6 +35,7 @@ class SubscribeHelper(metaclass=Singleton): def __init__(self): self.systemconfig = SystemConfigOper() + self.share_user_id = SystemUtils.generate_user_unique_id() if settings.SUBSCRIBE_STATISTIC_SHARE: if not self.systemconfig.get(SystemConfigKey.SubscribeReport): if self.sub_report(): @@ -133,6 +135,7 @@ class SubscribeHelper(metaclass=Singleton): "share_title": share_title, "share_comment": share_comment, "share_user": share_user, + "share_uid": self.share_user_id, **subscribe_dict }) if res is None: @@ -144,6 +147,24 @@ class SubscribeHelper(metaclass=Singleton): else: return False, res.json().get("message") + def share_delete(self, share_id: int) -> Tuple[bool, str]: + """ + 删除分享 + """ + if not settings.SUBSCRIBE_STATISTIC_SHARE: + return False, "当前没有开启订阅数据共享功能" + res = RequestUtils(proxies=settings.PROXY, + timeout=5).delete_res(f"{self._sub_share}/{share_id}", + params={"share_uid": self.share_user_id}) + if res is None: + return False, "连接MoviePilot服务器失败" + if res.ok: + # 清除 get_shares 的缓存,以便实时看到结果 + self._shares_cache.clear() + return True, "" + else: + return False, res.json().get("message") + def sub_fork(self, share_id: int) -> Tuple[bool, str]: """ 复用分享的订阅 diff --git a/app/schemas/subscribe.py b/app/schemas/subscribe.py index 1e2886be..5c717b85 100644 --- a/app/schemas/subscribe.py +++ b/app/schemas/subscribe.py @@ -88,6 +88,8 @@ class SubscribeShare(BaseModel): share_comment: Optional[str] = None # 分享人 share_user: Optional[str] = None + # 分享人唯一ID + share_uid: Optional[str] = None # 订阅名称 name: Optional[str] = None # 订阅年份 diff --git a/app/utils/http.py b/app/utils/http.py index 61532f34..0bed8cce 100644 --- a/app/utils/http.py +++ b/app/utils/http.py @@ -207,6 +207,32 @@ class RequestUtils: raise_exception=raise_exception, **kwargs) + def delete_res(self, + url: str, + data: Any = None, + params: dict = None, + allow_redirects: bool = True, + raise_exception: bool = False, + **kwargs) -> Optional[Response]: + """ + 发送DELETE请求并返回响应对象 + :param url: 请求的URL + :param data: 请求的数据 + :param params: 请求的参数 + :param allow_redirects: 是否允许重定向 + :param raise_exception: 是否在发生异常时抛出异常,否则默认拦截异常返回None + :param kwargs: 其他请求参数,如headers, cookies, proxies等 + :return: HTTP响应对象,若发生RequestException则返回None + :raises: requests.exceptions.RequestException 仅raise_exception为True时会抛出 + """ + return self.request(method="delete", + url=url, + data=data, + params=params, + allow_redirects=allow_redirects, + raise_exception=raise_exception, + **kwargs) + @staticmethod def cookie_parse(cookies_str: str, array: bool = False) -> Union[list, dict]: """ diff --git a/app/utils/system.py b/app/utils/system.py index e8cd68f0..ef5617e8 100644 --- a/app/utils/system.py +++ b/app/utils/system.py @@ -1,10 +1,12 @@ import datetime +import hashlib import os import platform import re import shutil import subprocess import sys +import uuid from glob import glob from pathlib import Path from typing import List, Optional, Tuple, Union @@ -556,3 +558,45 @@ class SystemUtils: # 确保是空文件夹 if folder.is_dir() and not any(folder.iterdir()): folder.rmdir() + + @staticmethod + def generate_user_unique_id(): + """ + 根据优先级依次尝试生成稳定唯一ID: + 1. 文件系统唯一标识符。 + 2. MAC 地址。 + 3. 主机名。 + """ + + def get_filesystem_unique_id(): + """ + 获取文件系统的唯一标识符。 + 使用根目录的设备号和 inode。 + """ + try: + stat_info = os.stat("/") + fs_id = f"{stat_info.st_dev}-{stat_info.st_ino}" + return hashlib.sha256(fs_id.encode("utf-8")).hexdigest() + except Exception as e: + print(str(e)) + return None + + def get_mac_address_id(): + """ + 获取设备的 MAC 地址并生成唯一标识符。 + """ + try: + mac_address = uuid.getnode() + if (mac_address >> 40) % 2: # 检查是否是虚拟MAC地址 + raise ValueError("MAC地址可能是虚拟地址") + mac_str = f"{mac_address:012x}" + return hashlib.sha256(mac_str.encode("utf-8")).hexdigest() + except Exception as e: + print(str(e)) + return None + + for method in [get_filesystem_unique_id, get_mac_address_id]: + unique_id = method() + if unique_id: + return unique_id + return None