mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
feat: 统计共享媒体识别命中次数
This commit is contained in:
@@ -4,12 +4,15 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from app import schemas
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.security import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import get_current_active_superuser_async
|
||||
from app.modules.douban.douban_cache import DoubanCache
|
||||
from app.schemas import MediaType
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -29,6 +32,10 @@ async def douban_recognition_cache(
|
||||
"count": len(cache_items),
|
||||
"recognized": recognized_count,
|
||||
"unrecognized": len(cache_items) - recognized_count,
|
||||
"shared_recognized": SystemConfigOper().get(
|
||||
SystemConfigKey.MediaRecognizeShareCount
|
||||
) or 0,
|
||||
"shared_recognize_enabled": settings.MEDIA_RECOGNIZE_SHARE,
|
||||
"data": cache_items,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,11 +4,13 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from app import schemas
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.core.config import settings
|
||||
from app.core.security import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
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
|
||||
from app.schemas.types import MediaType, SystemConfigKey
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -28,6 +30,10 @@ async def tmdb_recognition_cache(
|
||||
"count": len(cache_items),
|
||||
"recognized": recognized_count,
|
||||
"unrecognized": len(cache_items) - recognized_count,
|
||||
"shared_recognized": SystemConfigOper().get(
|
||||
SystemConfigKey.MediaRecognizeShareCount
|
||||
) or 0,
|
||||
"shared_recognize_enabled": settings.MEDIA_RECOGNIZE_SHARE,
|
||||
"data": cache_items,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ from app.core.meta import MetaBase
|
||||
from app.core.module import ModuleManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.db.message_oper import MessageOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import UserOper
|
||||
from app.helper.message import MessageHelper, MessageQueueManager, MessageTemplateHelper
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
@@ -49,6 +50,7 @@ from app.schemas.types import (
|
||||
MediaImageType,
|
||||
EventType,
|
||||
MessageChannel,
|
||||
SystemConfigKey,
|
||||
)
|
||||
from app.utils.object import ObjectUtils
|
||||
|
||||
@@ -572,6 +574,14 @@ class ChainBase(metaclass=ABCMeta):
|
||||
mediainfo=mediainfo,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _record_media_recognize_share_hit() -> None:
|
||||
"""记录一次共享媒体识别成功命中,统计失败不影响识别结果。"""
|
||||
try:
|
||||
SystemConfigOper().increment(SystemConfigKey.MediaRecognizeShareCount)
|
||||
except Exception as err:
|
||||
logger.error(f"记录共享媒体识别命中次数失败:{str(err)}")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_media_source_params(
|
||||
source: Optional[str] = None,
|
||||
@@ -730,6 +740,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
)
|
||||
if mediainfo:
|
||||
self._update_local_recognize_cache(shared_cache_meta, mediainfo)
|
||||
self._record_media_recognize_share_hit()
|
||||
return mediainfo
|
||||
return None
|
||||
|
||||
@@ -839,6 +850,7 @@ class ChainBase(metaclass=ABCMeta):
|
||||
)
|
||||
if mediainfo:
|
||||
await self._async_update_local_recognize_cache(shared_cache_meta, mediainfo)
|
||||
await run_in_threadpool(self._record_media_recognize_share_hit)
|
||||
return mediainfo
|
||||
return None
|
||||
|
||||
|
||||
@@ -101,6 +101,19 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
|
||||
# 避免将__SYSTEMCONF内的值引用出去,会导致set时误判没有变动
|
||||
return copy.deepcopy(self.__SYSTEMCONF.get(key))
|
||||
|
||||
def increment(self, key: SystemConfigKey, step: int = 1) -> int:
|
||||
"""
|
||||
原子递增整数系统设置
|
||||
|
||||
:param key: 配置键
|
||||
:param step: 递增步长
|
||||
:return: 递增后的整数值
|
||||
"""
|
||||
with self._rlock:
|
||||
value = int(self.get(key) or 0) + step
|
||||
self.set(key, value)
|
||||
return value
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
获取所有系统设置
|
||||
|
||||
@@ -281,6 +281,8 @@ class SystemConfigKey(Enum):
|
||||
SetupWizardState = "SetupWizardState"
|
||||
# 绿联影视登录会话缓存
|
||||
UgreenSessionCache = "UgreenSessionCache"
|
||||
# 共享媒体识别成功次数
|
||||
MediaRecognizeShareCount = "MediaRecognizeShareCount"
|
||||
|
||||
|
||||
# 处理进度Key字典
|
||||
|
||||
+7
-1
@@ -197,9 +197,15 @@ AniList 榜单、探索、详情、人物和推荐接口优先通过 `anilist-ch
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| GET | `/api/v1/tmdb/cache` | 查询 TheMovieDb 识别缓存及识别成功、失败条目统计 |
|
||||
| GET | `/api/v1/tmdb/cache` | 查询 TheMovieDb 识别缓存统计、共享识别累计成功命中次数及开关状态 |
|
||||
| DELETE | `/api/v1/tmdb/cache/{cache_key}` | 按缓存键删除单条 TheMovieDb 识别缓存,缓存键需要进行 URL 编码 |
|
||||
| DELETE | `/api/v1/tmdb/cache` | 清空全部 TheMovieDb 识别缓存 |
|
||||
| GET | `/api/v1/douban/cache` | 查询豆瓣识别缓存统计、共享识别累计成功命中次数及开关状态 |
|
||||
| DELETE | `/api/v1/douban/cache/{cache_key}` | 按缓存键删除单条豆瓣识别缓存,缓存键需要进行 URL 编码 |
|
||||
| DELETE | `/api/v1/douban/cache` | 清空全部豆瓣识别缓存 |
|
||||
|
||||
缓存查询响应的 `data` 包含 `count`、`recognized`、`unrecognized`、`data`,以及共享识别统计字段
|
||||
`shared_recognized` 和开关字段 `shared_recognize_enabled`。共享命中次数仅在共享结果驱动的二次媒体识别成功后累计。
|
||||
|
||||
### 插件补充接口
|
||||
|
||||
|
||||
@@ -441,6 +441,20 @@ Streaming search sends `{"type":"heartbeat"}` every 15 seconds without business
|
||||
| POST | `/api/v1/torrent/cache/refresh` | Refresh torrent cache |
|
||||
| POST | `/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}` | Re-identify torrent. Params: `tmdbid`, `doubanid` |
|
||||
|
||||
### Recognition Cache (6 endpoints)
|
||||
|
||||
The two list endpoints return local cache totals plus `shared_recognized` and
|
||||
`shared_recognize_enabled` for the persisted successful shared-recognition count.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/tmdb/cache` | Get TheMovieDb recognition cache statistics |
|
||||
| DELETE | `/api/v1/tmdb/cache/{cache_key}` | Delete one URL-encoded TheMovieDb recognition cache key |
|
||||
| DELETE | `/api/v1/tmdb/cache` | Clear TheMovieDb recognition cache |
|
||||
| GET | `/api/v1/douban/cache` | Get Douban recognition cache statistics |
|
||||
| DELETE | `/api/v1/douban/cache/{cache_key}` | Delete one URL-encoded Douban recognition cache key |
|
||||
| DELETE | `/api/v1/douban/cache` | Clear Douban recognition cache |
|
||||
|
||||
### Message (8 endpoints)
|
||||
|
||||
| Method | Path | Description |
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from unittest.mock import Mock
|
||||
|
||||
from app.api.endpoints import douban as douban_endpoint
|
||||
from app.db.user_oper import get_current_active_superuser_async
|
||||
from app.modules.douban.douban_cache import DoubanCache
|
||||
from app.schemas.types import MediaType
|
||||
from app.schemas.types import MediaType, SystemConfigKey
|
||||
|
||||
|
||||
class _MemoryCacheStub:
|
||||
@@ -116,7 +117,14 @@ def test_douban_cache_endpoint_returns_management_statistics(monkeypatch):
|
||||
"recognized": {"id": "1", "title": "Alpha", "type": MediaType.MOVIE},
|
||||
"unrecognized": {"id": 0},
|
||||
})
|
||||
get_system_config = Mock(return_value=None)
|
||||
monkeypatch.setattr(douban_endpoint, "DoubanCache", lambda: cache)
|
||||
monkeypatch.setattr(
|
||||
douban_endpoint,
|
||||
"SystemConfigOper",
|
||||
lambda: type("SystemConfigStub", (), {"get": get_system_config})(),
|
||||
)
|
||||
monkeypatch.setattr(douban_endpoint.settings, "MEDIA_RECOGNIZE_SHARE", False)
|
||||
|
||||
response = asyncio.run(douban_endpoint.douban_recognition_cache(None))
|
||||
|
||||
@@ -124,6 +132,11 @@ def test_douban_cache_endpoint_returns_management_statistics(monkeypatch):
|
||||
assert response.data["count"] == 2
|
||||
assert response.data["recognized"] == 1
|
||||
assert response.data["unrecognized"] == 1
|
||||
assert response.data["shared_recognized"] == 0
|
||||
assert response.data["shared_recognize_enabled"] is False
|
||||
get_system_config.assert_called_once_with(
|
||||
SystemConfigKey.MediaRecognizeShareCount
|
||||
)
|
||||
|
||||
|
||||
def test_douban_cache_delete_endpoint_reports_missing_item(monkeypatch):
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.meta import MetaBase
|
||||
from app.helper.server import MoviePilotServerHelper
|
||||
from app.schemas.types import MediaType, SystemConfigKey
|
||||
|
||||
|
||||
def _build_meta(name: str) -> MetaBase:
|
||||
"""构造共享识别统计测试所需的媒体元数据。"""
|
||||
meta = MetaBase(name)
|
||||
meta.name = name
|
||||
meta.type = MediaType.UNKNOWN
|
||||
return meta
|
||||
|
||||
|
||||
def _shared_params(tmdb_id: int) -> dict:
|
||||
"""构造共享识别结果转换后的模块参数。"""
|
||||
return {
|
||||
"mtype": MediaType.MOVIE,
|
||||
"source": "themoviedb",
|
||||
"mediaid": str(tmdb_id),
|
||||
"tmdbid": tmdb_id,
|
||||
"doubanid": None,
|
||||
"bangumiid": None,
|
||||
"anilistid": None,
|
||||
}
|
||||
|
||||
|
||||
def _mock_counter(monkeypatch) -> Mock:
|
||||
"""替换系统配置持久化入口并返回递增调用桩。"""
|
||||
increment = Mock()
|
||||
monkeypatch.setattr(
|
||||
"app.chain.SystemConfigOper",
|
||||
lambda: SimpleNamespace(increment=increment),
|
||||
)
|
||||
return increment
|
||||
|
||||
|
||||
def test_sync_shared_recognize_success_increments_persisted_count(monkeypatch):
|
||||
"""同步共享识别二次识别成功后应累计一次命中。"""
|
||||
chain = object.__new__(ChainBase)
|
||||
meta = _build_meta("共享识别电影")
|
||||
media = MediaInfo(
|
||||
title="共享识别电影",
|
||||
year="2026",
|
||||
tmdb_id=101,
|
||||
type=MediaType.MOVIE,
|
||||
)
|
||||
increment = _mock_counter(monkeypatch)
|
||||
monkeypatch.setattr("app.chain.settings.MEDIA_RECOGNIZE_SHARE", True)
|
||||
monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, media]))
|
||||
monkeypatch.setattr(chain, "_update_local_recognize_cache", Mock())
|
||||
monkeypatch.setattr(
|
||||
MoviePilotServerHelper,
|
||||
"query_recognize_share",
|
||||
Mock(return_value={"type": "movie", "tmdbid": 101}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
MoviePilotServerHelper,
|
||||
"to_recognize_params",
|
||||
Mock(return_value=_shared_params(101)),
|
||||
)
|
||||
|
||||
result = chain.recognize_media(meta=meta, cache=False)
|
||||
|
||||
assert result is media
|
||||
increment.assert_called_once_with(SystemConfigKey.MediaRecognizeShareCount)
|
||||
|
||||
|
||||
def test_sync_shared_result_without_local_match_does_not_increment(monkeypatch):
|
||||
"""共享接口返回数据但二次识别失败时不应累计命中。"""
|
||||
chain = object.__new__(ChainBase)
|
||||
meta = _build_meta("共享识别失败电影")
|
||||
increment = _mock_counter(monkeypatch)
|
||||
monkeypatch.setattr("app.chain.settings.MEDIA_RECOGNIZE_SHARE", True)
|
||||
monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, None]))
|
||||
monkeypatch.setattr(
|
||||
MoviePilotServerHelper,
|
||||
"query_recognize_share",
|
||||
Mock(return_value={"type": "movie", "tmdbid": 102}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
MoviePilotServerHelper,
|
||||
"to_recognize_params",
|
||||
Mock(return_value=_shared_params(102)),
|
||||
)
|
||||
|
||||
result = chain.recognize_media(meta=meta, cache=False)
|
||||
|
||||
assert result is None
|
||||
increment.assert_not_called()
|
||||
|
||||
|
||||
def test_async_shared_recognize_success_increments_persisted_count(monkeypatch):
|
||||
"""异步共享识别二次识别成功后应累计一次命中。"""
|
||||
chain = object.__new__(ChainBase)
|
||||
meta = _build_meta("异步共享识别电影")
|
||||
media = MediaInfo(
|
||||
title="异步共享识别电影",
|
||||
year="2026",
|
||||
tmdb_id=103,
|
||||
type=MediaType.MOVIE,
|
||||
)
|
||||
increment = _mock_counter(monkeypatch)
|
||||
monkeypatch.setattr("app.chain.settings.MEDIA_RECOGNIZE_SHARE", True)
|
||||
monkeypatch.setattr(
|
||||
chain,
|
||||
"async_run_module",
|
||||
AsyncMock(side_effect=[None, media]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
chain,
|
||||
"_async_update_local_recognize_cache",
|
||||
AsyncMock(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
MoviePilotServerHelper,
|
||||
"async_query_recognize_share",
|
||||
AsyncMock(return_value={"type": "movie", "tmdbid": 103}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
MoviePilotServerHelper,
|
||||
"to_recognize_params",
|
||||
Mock(return_value=_shared_params(103)),
|
||||
)
|
||||
|
||||
result = asyncio.run(chain.async_recognize_media(meta=meta, cache=False))
|
||||
|
||||
assert result is media
|
||||
increment.assert_called_once_with(SystemConfigKey.MediaRecognizeShareCount)
|
||||
@@ -0,0 +1,51 @@
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
def test_increment_serializes_concurrent_counter_updates(monkeypatch):
|
||||
"""并发递增系统计数时不应丢失更新。"""
|
||||
oper = object.__new__(SystemConfigOper)
|
||||
oper._rlock = threading.RLock()
|
||||
stored_value = {"value": 0}
|
||||
|
||||
monkeypatch.setattr(oper, "get", lambda _key: stored_value["value"])
|
||||
monkeypatch.setattr(
|
||||
oper,
|
||||
"set",
|
||||
lambda _key, value: stored_value.update(value=value),
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
results = list(
|
||||
executor.map(
|
||||
lambda _index: oper.increment(
|
||||
SystemConfigKey.MediaRecognizeShareCount
|
||||
),
|
||||
range(100),
|
||||
)
|
||||
)
|
||||
|
||||
assert sorted(results) == list(range(1, 101))
|
||||
assert stored_value["value"] == 100
|
||||
|
||||
|
||||
def test_increment_supports_custom_step(monkeypatch):
|
||||
"""整数系统计数应支持指定递增步长。"""
|
||||
oper = object.__new__(SystemConfigOper)
|
||||
oper._rlock = threading.RLock()
|
||||
stored_value = {"value": 4}
|
||||
|
||||
monkeypatch.setattr(oper, "get", lambda _key: stored_value["value"])
|
||||
monkeypatch.setattr(
|
||||
oper,
|
||||
"set",
|
||||
lambda _key, value: stored_value.update(value=value),
|
||||
)
|
||||
|
||||
result = oper.increment(SystemConfigKey.MediaRecognizeShareCount, step=3)
|
||||
|
||||
assert result == 7
|
||||
assert stored_value["value"] == 7
|
||||
@@ -1,10 +1,11 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from unittest.mock import Mock
|
||||
|
||||
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
|
||||
from app.schemas.types import MediaType, SystemConfigKey
|
||||
|
||||
|
||||
class _MemoryCacheStub:
|
||||
@@ -97,7 +98,14 @@ def test_tmdb_cache_endpoint_returns_management_statistics(monkeypatch):
|
||||
"recognized": {"id": 1, "title": "Alpha", "type": MediaType.MOVIE},
|
||||
"unrecognized": {"id": 0},
|
||||
})
|
||||
get_system_config = Mock(return_value=7)
|
||||
monkeypatch.setattr(tmdb_endpoint, "TmdbCache", lambda: cache)
|
||||
monkeypatch.setattr(
|
||||
tmdb_endpoint,
|
||||
"SystemConfigOper",
|
||||
lambda: type("SystemConfigStub", (), {"get": get_system_config})(),
|
||||
)
|
||||
monkeypatch.setattr(tmdb_endpoint.settings, "MEDIA_RECOGNIZE_SHARE", True)
|
||||
|
||||
response = asyncio.run(tmdb_endpoint.tmdb_recognition_cache(None))
|
||||
|
||||
@@ -105,6 +113,11 @@ def test_tmdb_cache_endpoint_returns_management_statistics(monkeypatch):
|
||||
assert response.data["count"] == 2
|
||||
assert response.data["recognized"] == 1
|
||||
assert response.data["unrecognized"] == 1
|
||||
assert response.data["shared_recognized"] == 7
|
||||
assert response.data["shared_recognize_enabled"] is True
|
||||
get_system_config.assert_called_once_with(
|
||||
SystemConfigKey.MediaRecognizeShareCount
|
||||
)
|
||||
|
||||
|
||||
def test_tmdb_cache_delete_endpoint_reports_missing_item(monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user