diff --git a/app/core/cache.py b/app/core/cache.py index 21bfd803..85f6e9bd 100644 --- a/app/core/cache.py +++ b/app/core/cache.py @@ -28,6 +28,49 @@ class CacheBackend(ABC): 缓存后端基类,定义通用的缓存接口 """ + # Dict-like operations + def __getitem__(self, key: str) -> Any: + """ + 获取缓存项,类似 dict[key] + """ + value = self.get(key) + if value is None: + raise KeyError(key) + return value + + def __setitem__(self, key: str, value: Any) -> None: + """ + 设置缓存项,类似 dict[key] = value + """ + self.set(key, value) + + def __delitem__(self, key: str) -> None: + """ + 删除缓存项,类似 del dict[key] + """ + if not self.exists(key): + raise KeyError(key) + self.delete(key) + + def __contains__(self, key: str) -> bool: + """ + 检查键是否存在,类似 key in dict + """ + return self.exists(key) + + def __iter__(self): + """ + 返回缓存的迭代器,类似 iter(dict) + """ + for key, _ in self.items(): + yield key + + def __len__(self) -> int: + """ + 返回缓存项的数量,类似 len(dict) + """ + return sum(1 for _ in self.items()) + @abstractmethod def set(self, key: str, value: Any, ttl: Optional[int] = None, region: Optional[str] = DEFAULT_CACHE_REGION, **kwargs) -> None: @@ -74,15 +117,6 @@ class CacheBackend(ABC): """ pass - @abstractmethod - def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None: - """ - 清除指定区域的缓存或全部缓存 - - :param region: 缓存的区,为None时清空所有区缓存 - """ - pass - @abstractmethod def items(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[Tuple[str, Any], None, None]: """ @@ -93,56 +127,6 @@ class CacheBackend(ABC): """ pass - @abstractmethod - def close(self) -> None: - """ - 关闭缓存连接 - """ - pass - - # Dict-like operations - def __getitem__(self, key: str) -> Any: - """ - 获取缓存项,类似 dict[key] - """ - value = self.get(key) - if value is None: - raise KeyError(key) - return value - - def __setitem__(self, key: str, value: Any) -> None: - """ - 设置缓存项,类似 dict[key] = value - """ - self.set(key, value) - - def __delitem__(self, key: str) -> None: - """ - 删除缓存项,类似 del dict[key] - """ - if not self.exists(key): - raise KeyError(key) - self.delete(key) - - def __contains__(self, key: str) -> bool: - """ - 检查键是否存在,类似 key in dict - """ - return self.exists(key) - - def __iter__(self): - """ - 返回缓存的迭代器,类似 iter(dict) - """ - for key, _ in self.items(): - yield key - - def __len__(self) -> int: - """ - 返回缓存项的数量,类似 len(dict) - """ - return sum(1 for _ in self.items()) - def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[str, None, None]: """ 获取所有缓存键,类似 dict.keys() @@ -157,7 +141,7 @@ class CacheBackend(ABC): for _, value in self.items(region=region): yield value - def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, + def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, ttl: Optional[int] = None, **kwargs) -> None: """ 更新缓存,类似 dict.update() @@ -199,13 +183,31 @@ class CacheBackend(ABC): return default return value - def get_region(self, region: Optional[str] = None) -> str: + @abstractmethod + def clear(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> None: + """ + 清除指定区域的缓存或全部缓存 + + :param region: 缓存的区,为None时清空所有区缓存 + """ + pass + + @abstractmethod + def close(self) -> None: + """ + 关闭缓存连接 + """ + pass + + @staticmethod + def get_region(region: Optional[str] = None) -> str: """ 获取缓存的区 """ return f"region:{region}" if region else "region:default" - def get_cache_key(self, func, args, kwargs) -> str: + @staticmethod + def get_cache_key(func, args, kwargs) -> str: """ 根据函数和参数生成缓存键 @@ -229,14 +231,15 @@ class CacheBackend(ABC): # 使用有序参数生成缓存键 return f"{func.__name__}_{hashkey(*keys)}" - def is_redis(self) -> bool: + @staticmethod + def is_redis() -> bool: """ 判断当前缓存后端是否为 Redis """ - return isinstance(self, RedisBackend) or isinstance(self, AsyncRedisBackend) + return settings.CACHE_BACKEND_TYPE == "redis" -class AsyncCacheBackend(ABC): +class AsyncCacheBackend(CacheBackend): """ 缓存后端基类,定义通用的缓存接口(异步) """ @@ -313,67 +316,21 @@ class AsyncCacheBackend(ABC): """ pass - # Async dict-like operations - async def __getitem__(self, key: str) -> Any: - """ - 获取缓存项,类似 dict[key](异步) - """ - value = await self.get(key) - if value is None: - raise KeyError(key) - return value - - async def __setitem__(self, key: str, value: Any) -> None: - """ - 设置缓存项,类似 dict[key] = value(异步) - """ - await self.set(key, value) - - async def __delitem__(self, key: str) -> None: - """ - 删除缓存项,类似 del dict[key](异步) - """ - if not await self.exists(key): - raise KeyError(key) - await self.delete(key) - - async def __contains__(self, key: str) -> bool: - """ - 检查键是否存在,类似 key in dict(异步) - """ - return await self.exists(key) - - async def __aiter__(self): - """ - 返回缓存的异步迭代器,类似 aiter(dict) - """ - async for key, _ in self.items(): - yield key - - async def __len__(self) -> int: - """ - 返回缓存项的数量,类似 len(dict)(异步) - """ - count = 0 - async for _ in self.items(): - count += 1 - return count - async def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[str, None]: """ 获取所有缓存键,类似 dict.keys()(异步) """ - async for key, _ in self.items(region=region): + async for key, _ in await self.items(region=region): yield key async def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[Any, None]: """ 获取所有缓存值,类似 dict.values()(异步) """ - async for _, value in self.items(region=region): + async for _, value in await self.items(region=region): yield value - async def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, + async def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, ttl: Optional[int] = None, **kwargs) -> None: """ 更新缓存,类似 dict.update()(异步) @@ -398,7 +355,7 @@ class AsyncCacheBackend(ABC): 弹出最后一个缓存项,类似 dict.popitem()(异步) """ items = [] - async for item in self.items(region=region): + async for item in await self.items(region=region): items.append(item) if not items: raise KeyError("popitem(): cache is empty") @@ -417,42 +374,6 @@ class AsyncCacheBackend(ABC): return default return value - def get_region(self, region: Optional[str] = None) -> str: - """ - 获取缓存的区 - """ - return f"region:{region}" if region else "region:default" - - def get_cache_key(self, func, args, kwargs) -> str: - """ - 根据函数和参数生成缓存键 - - :param func: 函数对象 - :param args: 位置参数 - :param kwargs: 关键字参数 - :return: 缓存键 - """ - signature = inspect.signature(func) - # 绑定传入的参数并应用默认值 - bound = signature.bind(*args, **kwargs) - bound.apply_defaults() - # 忽略第一个参数,如果它是实例(self)或类(cls) - parameters = list(signature.parameters.keys()) - if parameters and parameters[0] in ("self", "cls"): - bound.arguments.pop(parameters[0], None) - # 按照函数签名顺序提取参数值列表 - keys = [ - bound.arguments[param] for param in signature.parameters if param in bound.arguments - ] - # 使用有序参数生成缓存键 - return f"{func.__name__}_{hashkey(*keys)}" - - def is_redis(self) -> bool: - """ - 判断当前缓存后端是否为 Redis - """ - return isinstance(self, RedisBackend) or isinstance(self, AsyncRedisBackend) - class MemoryBackend(CacheBackend): """ @@ -1005,84 +926,6 @@ def Cache(maxsize: Optional[int] = None, ttl: Optional[int] = None) -> CacheBack return MemoryBackend(maxsize=maxsize, ttl=ttl) -class TTLCache: - """ - TTL缓存类,现在只是一个简单的包装器,建议直接使用Cache类 - - 注意:此类已过时,建议直接使用Cache类,它提供了完整的dict操作特性 - """ - - def __init__(self, region: Optional[str] = DEFAULT_CACHE_REGION, - maxsize: int = None, ttl: int = None): - """ - 初始化TTL缓存 - - :param region: 缓存的区,默认为 DEFAULT_CACHE_REGION - :param maxsize: 缓存的最大条目数 - :param ttl: 缓存的存活时间,单位秒 - """ - self.region = region - self.maxsize = maxsize - self.ttl = ttl - self._backend = Cache(maxsize=maxsize, ttl=ttl) - - def set(self, key: str, value: Any, ttl: Optional[int] = None): - """ - 设置缓存项,支持自定义 TTL - """ - try: - ttl = ttl or self.ttl - self._backend.set(key, value, ttl=ttl, region=self.region) - except Exception as e: - logger.warning(f"缓存设置失败: {e}") - - def get(self, key: str, default: Any = None): - """ - 获取缓存项,如果不存在返回默认值 - """ - try: - value = self._backend.get(key, region=self.region) - if value is not None: - return value - except Exception as e: - logger.warning(f"缓存获取失败: {e}") - - return default - - def delete(self, key: str): - """ - 删除缓存项 - """ - try: - self._backend.delete(key, region=self.region) - except Exception as e: - logger.warning(f"缓存删除失败: {e}") - - def clear(self): - """ - 清空缓存 - """ - try: - self._backend.clear(region=self.region) - except Exception as e: - logger.warning(f"缓存清空失败: {e}") - - def is_redis(self) -> bool: - """ - 判断当前缓存后端是否为 Redis - """ - return self._backend.is_redis() - - def close(self): - """ - 关闭缓存连接 - """ - try: - self._backend.close() - except Exception as e: - logger.warning(f"缓存关闭失败: {e}") - - def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Optional[int] = None, skip_none: Optional[bool] = True, skip_empty: Optional[bool] = False): """ diff --git a/app/helper/torrent.py b/app/helper/torrent.py index 8989f2ca..4b4d372f 100644 --- a/app/helper/torrent.py +++ b/app/helper/torrent.py @@ -16,11 +16,10 @@ from app.db.systemconfig_oper import SystemConfigOper from app.log import logger from app.schemas.types import MediaType, SystemConfigKey from app.utils.http import RequestUtils -from app.utils.singleton import WeakSingleton from app.utils.string import StringUtils -class TorrentHelper(metaclass=WeakSingleton): +class TorrentHelper: """ 种子帮助类 """ @@ -199,17 +198,14 @@ class TorrentHelper(metaclass=WeakSingleton): :param torrent_content: 种子内容 :return: 文件夹名、文件清单,单文件种子返回空文件夹名 """ + if not torrent_content: return "", [] - + # 检查是否为磁力链接 - if isinstance(torrent_content, str) and torrent_content.startswith("magnet:"): - # 磁力链接无法预先获取文件信息,返回空 + if StringUtils.is_magnet_link(torrent_content): return "", [] - elif isinstance(torrent_content, bytes) and torrent_content.startswith(b"magnet:"): - # 磁力链接无法预先获取文件信息,返回空 - return "", [] - + try: # 解析种子内容 torrentinfo = Torrent.from_string(torrent_content) diff --git a/app/modules/qbittorrent/__init__.py b/app/modules/qbittorrent/__init__.py index f018541d..ac568a1f 100644 --- a/app/modules/qbittorrent/__init__.py +++ b/app/modules/qbittorrent/__init__.py @@ -118,23 +118,17 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): if content.exists(): torrent_content = content.read_bytes() else: - # 缓存处理器 - cache_backend = FileCache() # 读取缓存的种子文件 - torrent_content = cache_backend.get(content.as_posix(), region="torrents") + torrent_content = FileCache().get(content.as_posix(), region="torrents") else: torrent_content = content - # 检查是否为磁力链接 - if isinstance(torrent_content, str) and torrent_content.startswith("magnet:"): - # 磁力链接不需要解析种子文件 - return None, torrent_content - elif isinstance(torrent_content, bytes) and torrent_content.startswith(b"magnet:"): - # 磁力链接不需要解析种子文件 - return None, torrent_content - if torrent_content: - torrent_info = Torrent.from_string(torrent_content) + # 检查是否为磁力链接 + if StringUtils.is_magnet_link(torrent_content): + return None, torrent_content + else: + torrent_info = Torrent.from_string(torrent_content) return torrent_info, torrent_content except Exception as e: @@ -147,7 +141,9 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]): # 读取种子的名称 torrent, content = __get_torrent_info() # 检查是否为磁力链接 - is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content, bytes) and content.startswith(b"magnet:") + is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content, + bytes) and content.startswith( + b"magnet:") if not torrent and not is_magnet: return None, None, None, f"添加种子任务失败:无法读取种子文件" diff --git a/app/modules/transmission/__init__.py b/app/modules/transmission/__init__.py index 9b5fd654..5294fae6 100644 --- a/app/modules/transmission/__init__.py +++ b/app/modules/transmission/__init__.py @@ -119,23 +119,17 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]): if content.exists(): torrent_content = content.read_bytes() else: - # 缓存处理器 - cache_backend = FileCache() # 读取缓存的种子文件 - torrent_content = cache_backend.get(content.as_posix(), region="torrents") + torrent_content = FileCache().get(content.as_posix(), region="torrents") else: torrent_content = content - # 检查是否为磁力链接 - if isinstance(torrent_content, str) and torrent_content.startswith("magnet:"): - # 磁力链接不需要解析种子文件 - return None, torrent_content - elif isinstance(torrent_content, bytes) and torrent_content.startswith(b"magnet:"): - # 磁力链接不需要解析种子文件 - return None, torrent_content - if torrent_content: - torrent_info = Torrent.from_string(torrent_content) + # 检查是否为磁力链接 + if StringUtils.is_magnet_link(torrent_content): + return None, torrent_content + else: + torrent_info = Torrent.from_string(torrent_content) return torrent_info, torrent_content except Exception as e: @@ -148,7 +142,9 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]): # 读取种子的名称 torrent, content = __get_torrent_info() # 检查是否为磁力链接 - is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content, bytes) and content.startswith(b"magnet:") + is_magnet = isinstance(content, str) and content.startswith("magnet:") or isinstance(content, + bytes) and content.startswith( + b"magnet:") if not torrent and not is_magnet: return None, None, None, f"添加种子任务失败:无法读取种子文件" diff --git a/app/utils/string.py b/app/utils/string.py index 4adf193c..353f807c 100644 --- a/app/utils/string.py +++ b/app/utils/string.py @@ -229,7 +229,7 @@ class StringUtils: size = float(size) d = [(1024 - 1, 'K'), (1024 ** 2 - 1, 'M'), (1024 ** 3 - 1, 'G'), (1024 ** 4 - 1, 'T')] s = [x[0] for x in d] - index = bisect.bisect_left(s, size) - 1 # noqa + index = bisect.bisect_left(s, size) - 1 # noqa if index == -1: return str(size) + "B" else: @@ -925,3 +925,16 @@ class StringUtils: if re.match(r'^[a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})?$', text): return True return False + + @staticmethod + def is_magnet_link(content: Union[str, bytes]) -> bool: + """ + 判断内容是否为磁力链接 + """ + if not content: + return False + if isinstance(content, str) and content.startswith("magnet:"): + return True + if isinstance(content, bytes) and content.startswith(b"magnet:"): + return True + return False