mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
feat: 支持 TheMovieDb 识别缓存管理
This commit is contained in:
@@ -5,11 +5,61 @@ from fastapi import APIRouter, Depends
|
||||
from app import schemas
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.core.security import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.db.user_oper import get_current_active_superuser_async
|
||||
from app.modules.themoviedb.tmdb_cache import TmdbCache
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cache", summary="查询 TheMovieDb 识别缓存", response_model=schemas.Response
|
||||
)
|
||||
async def tmdb_recognition_cache(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
) -> schemas.Response:
|
||||
"""查询可管理的 TheMovieDb 识别缓存。"""
|
||||
cache_items = TmdbCache().list_items()
|
||||
recognized_count = sum(1 for item in cache_items if item["tmdb_id"])
|
||||
return schemas.Response(
|
||||
success=True,
|
||||
data={
|
||||
"count": len(cache_items),
|
||||
"recognized": recognized_count,
|
||||
"unrecognized": len(cache_items) - recognized_count,
|
||||
"data": cache_items,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/cache/{cache_key:path}",
|
||||
summary="删除指定 TheMovieDb 识别缓存",
|
||||
response_model=schemas.Response,
|
||||
)
|
||||
async def delete_tmdb_recognition_cache(
|
||||
cache_key: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
) -> schemas.Response:
|
||||
"""按缓存键删除单条 TheMovieDb 识别缓存。"""
|
||||
deleted_item = TmdbCache().delete(cache_key)
|
||||
if not deleted_item:
|
||||
return schemas.Response(success=False, message="TheMovieDb 识别缓存不存在")
|
||||
return schemas.Response(success=True, message="TheMovieDb 识别缓存删除成功")
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/cache", summary="清空 TheMovieDb 识别缓存", response_model=schemas.Response
|
||||
)
|
||||
async def clear_tmdb_recognition_cache(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
) -> schemas.Response:
|
||||
"""清空全部 TheMovieDb 识别缓存。"""
|
||||
TmdbCache().clear()
|
||||
return schemas.Response(success=True, message="TheMovieDb 识别缓存清理完成")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/seasons/{tmdbid}", summary="TMDB所有季", response_model=List[schemas.TmdbSeason]
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ class TmdbCache(metaclass=WeakSingleton):
|
||||
_tmdb_cache_expire: bool = True
|
||||
|
||||
def __init__(self):
|
||||
"""初始化 TMDB 识别缓存并恢复本地持久化数据。"""
|
||||
self.maxsize = settings.CONF.douban
|
||||
self.ttl = settings.CONF.meta
|
||||
self.region = "__tmdb_cache__"
|
||||
@@ -44,6 +45,33 @@ class TmdbCache(metaclass=WeakSingleton):
|
||||
"""
|
||||
with lock:
|
||||
self._cache.clear()
|
||||
self.save(force=True)
|
||||
|
||||
def list_items(self) -> list[dict]:
|
||||
"""
|
||||
返回可供管理界面展示的 TMDB 识别缓存列表。
|
||||
"""
|
||||
with lock:
|
||||
cache_items = []
|
||||
for key, value in self._cache.items():
|
||||
if not isinstance(value, dict):
|
||||
continue
|
||||
media_type = value.get("type")
|
||||
if not isinstance(media_type, MediaType):
|
||||
try:
|
||||
media_type = MediaType(media_type)
|
||||
except (TypeError, ValueError):
|
||||
media_type = None
|
||||
cache_items.append({
|
||||
"key": key,
|
||||
"tmdb_id": value.get("id") or 0,
|
||||
"title": value.get("title") or "",
|
||||
"year": value.get("year") or "",
|
||||
"media_type": media_type.to_agent() if media_type else "unknown",
|
||||
"poster_path": value.get("poster_path") or "",
|
||||
"backdrop_path": value.get("backdrop_path") or "",
|
||||
})
|
||||
return sorted(cache_items, key=lambda item: item["key"])
|
||||
|
||||
@staticmethod
|
||||
def __get_key(meta: MetaBase) -> str:
|
||||
@@ -71,6 +99,7 @@ class TmdbCache(metaclass=WeakSingleton):
|
||||
redis_data = self._cache.get(key)
|
||||
if redis_data:
|
||||
self._cache.delete(key)
|
||||
self.save(force=True)
|
||||
return redis_data
|
||||
return {}
|
||||
|
||||
@@ -156,4 +185,5 @@ class TmdbCache(metaclass=WeakSingleton):
|
||||
pickle.dump(new_meta_data, f, pickle.HIGHEST_PROTOCOL) # type: ignore
|
||||
|
||||
def __del__(self):
|
||||
"""实例释放前保存非 Redis 缓存。"""
|
||||
self.save()
|
||||
|
||||
@@ -157,6 +157,16 @@ FastAPI 异常响应保留 `detail` 字段,并在错误详情为文本时返
|
||||
| POST | `/api/v1/message/agent/mcp/servers` | 管理员保存 Agent 外部 MCP 服务器配置 |
|
||||
| POST | `/api/v1/message/agent/mcp/servers/test` | 管理员测试单个 Agent 外部 MCP 服务器并读取工具列表 |
|
||||
|
||||
#### 缓存管理
|
||||
|
||||
以下接口使用登录态鉴权,并要求当前用户为超级管理员。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| GET | `/api/v1/tmdb/cache` | 查询 TheMovieDb 识别缓存及识别成功、失败条目统计 |
|
||||
| DELETE | `/api/v1/tmdb/cache/{cache_key}` | 按缓存键删除单条 TheMovieDb 识别缓存,缓存键需要进行 URL 编码 |
|
||||
| DELETE | `/api/v1/tmdb/cache` | 清空全部 TheMovieDb 识别缓存 |
|
||||
|
||||
### 插件补充接口
|
||||
|
||||
**GET** `/api/v1/plugin/history/{plugin_id}`
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
from app.api.endpoints import tmdb as tmdb_endpoint
|
||||
from app.db.user_oper import get_current_active_superuser_async
|
||||
from app.modules.themoviedb.tmdb_cache import TmdbCache
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
class _MemoryCacheStub:
|
||||
"""提供 TMDB 缓存管理测试所需的最小内存后端。"""
|
||||
|
||||
def __init__(self, data: dict):
|
||||
"""使用给定字典初始化测试缓存。"""
|
||||
self.data = data
|
||||
|
||||
def items(self):
|
||||
"""返回全部缓存条目。"""
|
||||
return self.data.items()
|
||||
|
||||
def get(self, key: str):
|
||||
"""读取指定缓存条目。"""
|
||||
return self.data.get(key)
|
||||
|
||||
def delete(self, key: str):
|
||||
"""删除指定缓存条目。"""
|
||||
self.data.pop(key, None)
|
||||
|
||||
def clear(self):
|
||||
"""清空全部缓存条目。"""
|
||||
self.data.clear()
|
||||
|
||||
|
||||
def _build_tmdb_cache(data: dict) -> TmdbCache:
|
||||
"""构造绕过单例初始化的 TMDB 缓存测试实例。"""
|
||||
cache = object.__new__(TmdbCache)
|
||||
cache._cache = _MemoryCacheStub(data)
|
||||
cache.save = lambda force=False: None
|
||||
return cache
|
||||
|
||||
|
||||
def test_tmdb_cache_management_endpoints_require_superuser():
|
||||
"""识别缓存管理接口必须仅允许超级管理员访问。"""
|
||||
endpoints = [
|
||||
tmdb_endpoint.tmdb_recognition_cache,
|
||||
tmdb_endpoint.delete_tmdb_recognition_cache,
|
||||
tmdb_endpoint.clear_tmdb_recognition_cache,
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
dependency = inspect.signature(endpoint).parameters["_"].default.dependency
|
||||
assert dependency is get_current_active_superuser_async
|
||||
|
||||
|
||||
def test_tmdb_cache_list_items_normalizes_media_type_and_sorting():
|
||||
"""管理列表应输出稳定顺序和前端可识别的媒体类型。"""
|
||||
cache = _build_tmdb_cache({
|
||||
"[电视剧][zh-CN]Zulu-2024-1": {
|
||||
"id": 2,
|
||||
"title": "Zulu",
|
||||
"type": MediaType.TV,
|
||||
"year": "2024",
|
||||
},
|
||||
"[电影][zh-CN]Alpha-2023-None": {
|
||||
"id": 1,
|
||||
"title": "Alpha",
|
||||
"type": "电影",
|
||||
"year": "2023",
|
||||
},
|
||||
"[电影][zh-CN]Missing-2022-None": {"id": 0},
|
||||
})
|
||||
|
||||
items = cache.list_items()
|
||||
|
||||
assert [item["title"] for item in items] == ["Alpha", "", "Zulu"]
|
||||
assert [item["media_type"] for item in items] == ["movie", "unknown", "tv"]
|
||||
assert items[1]["tmdb_id"] == 0
|
||||
|
||||
|
||||
def test_tmdb_cache_delete_and_clear_persist_immediately(monkeypatch):
|
||||
"""管理操作应修改运行时缓存并立即触发本地持久化。"""
|
||||
cache = _build_tmdb_cache({"first": {"id": 1}, "second": {"id": 2}})
|
||||
saved_forces = []
|
||||
monkeypatch.setattr(cache, "save", lambda force=False: saved_forces.append(force))
|
||||
|
||||
assert cache.delete("first") == {"id": 1}
|
||||
assert cache.delete("missing") == {}
|
||||
cache.clear()
|
||||
|
||||
assert cache.list_items() == []
|
||||
assert saved_forces == [True, True]
|
||||
|
||||
|
||||
def test_tmdb_cache_endpoint_returns_management_statistics(monkeypatch):
|
||||
"""查询接口应返回识别成功和失败条目的统计。"""
|
||||
cache = _build_tmdb_cache({
|
||||
"recognized": {"id": 1, "title": "Alpha", "type": MediaType.MOVIE},
|
||||
"unrecognized": {"id": 0},
|
||||
})
|
||||
monkeypatch.setattr(tmdb_endpoint, "TmdbCache", lambda: cache)
|
||||
|
||||
response = asyncio.run(tmdb_endpoint.tmdb_recognition_cache(None))
|
||||
|
||||
assert response.success is True
|
||||
assert response.data["count"] == 2
|
||||
assert response.data["recognized"] == 1
|
||||
assert response.data["unrecognized"] == 1
|
||||
|
||||
|
||||
def test_tmdb_cache_delete_endpoint_reports_missing_item(monkeypatch):
|
||||
"""删除接口应区分成功删除与缓存不存在。"""
|
||||
cache = _build_tmdb_cache({"existing": {"id": 1}})
|
||||
monkeypatch.setattr(tmdb_endpoint, "TmdbCache", lambda: cache)
|
||||
|
||||
deleted_response = asyncio.run(
|
||||
tmdb_endpoint.delete_tmdb_recognition_cache("existing", None)
|
||||
)
|
||||
missing_response = asyncio.run(
|
||||
tmdb_endpoint.delete_tmdb_recognition_cache("missing", None)
|
||||
)
|
||||
|
||||
assert deleted_response.success is True
|
||||
assert missing_response.success is False
|
||||
|
||||
|
||||
def test_tmdb_cache_clear_endpoint_removes_all_items(monkeypatch):
|
||||
"""清空接口应删除全部识别缓存。"""
|
||||
cache = _build_tmdb_cache({"existing": {"id": 1}})
|
||||
monkeypatch.setattr(tmdb_endpoint, "TmdbCache", lambda: cache)
|
||||
|
||||
response = asyncio.run(tmdb_endpoint.clear_tmdb_recognition_cache(None))
|
||||
|
||||
assert response.success is True
|
||||
assert cache.list_items() == []
|
||||
Reference in New Issue
Block a user