mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-21 04:31:59 +08:00
fix: avoid blocking plugin release history refresh (#6084)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import shutil
|
||||
from typing import Annotated, Any, List, Optional
|
||||
@@ -39,6 +40,67 @@ PROTECTED_ROUTES = {"/api/v1/openapi.json", "/docs", "/docs/oauth2-redirect", "/
|
||||
PLUGIN_PREFIX = f"{settings.API_V1_STR}/plugin"
|
||||
|
||||
router = APIRouter()
|
||||
_plugin_release_refresh_tasks: set[asyncio.Task] = set()
|
||||
|
||||
|
||||
async def _get_market_plugin_from_repo(
|
||||
plugin_manager: PluginManager,
|
||||
plugin_id: str,
|
||||
repo_url: str,
|
||||
force: bool,
|
||||
) -> Optional[schemas.Plugin]:
|
||||
"""
|
||||
只读取指定插件仓库的市场元数据,避免单插件详情触发全部市场刷新。
|
||||
"""
|
||||
market_plugins = await plugin_manager.async_get_plugins_from_market(
|
||||
repo_url, settings.VERSION_FLAG, force
|
||||
)
|
||||
market_plugin = next(
|
||||
(
|
||||
plugin
|
||||
for plugin in market_plugins or []
|
||||
if plugin.id == plugin_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if market_plugin or not settings.VERSION_FLAG:
|
||||
return market_plugin
|
||||
|
||||
compatible_plugins = await plugin_manager.async_get_plugins_from_market(
|
||||
repo_url, None, force
|
||||
)
|
||||
return next(
|
||||
(
|
||||
plugin
|
||||
for plugin in compatible_plugins or []
|
||||
if plugin.id == plugin_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
async def _refresh_plugin_release_versions(plugin_id: str, repo_url: str) -> None:
|
||||
"""
|
||||
后台强制刷新 Release 缓存,接口响应路径优先返回已有缓存。
|
||||
"""
|
||||
try:
|
||||
async with async_fresh(True):
|
||||
await PluginHelper().async_get_plugin_release_versions(plugin_id, repo_url)
|
||||
except Exception as e:
|
||||
logger.warning(f"后台刷新插件 {plugin_id} Release 列表失败:{e}")
|
||||
|
||||
|
||||
def _schedule_plugin_release_refresh(plugin_id: str, repo_url: str) -> None:
|
||||
"""
|
||||
保留后台任务引用,避免任务被回收,同时让 helper 负责同仓库强刷合并。
|
||||
"""
|
||||
task = asyncio.create_task(_refresh_plugin_release_versions(plugin_id, repo_url))
|
||||
_plugin_release_refresh_tasks.add(task)
|
||||
|
||||
def _discard_task(completed_task: asyncio.Task) -> None:
|
||||
_plugin_release_refresh_tasks.discard(completed_task)
|
||||
|
||||
task.add_done_callback(_discard_task)
|
||||
|
||||
|
||||
def register_plugin_api(plugin_id: Optional[str] = None):
|
||||
@@ -239,6 +301,15 @@ async def _get_plugin_history_detail(
|
||||
if local_repo_plugin:
|
||||
return _merge_plugin_market_metadata(installed_plugin, local_repo_plugin)
|
||||
|
||||
if installed_plugin.repo_url:
|
||||
market_plugin = await _get_market_plugin_from_repo(
|
||||
plugin_manager, plugin_id, installed_plugin.repo_url, force
|
||||
)
|
||||
if not market_plugin:
|
||||
logger.debug(f"插件 {plugin_id} 未从来源仓库获取到更新说明,返回本地插件信息")
|
||||
return installed_plugin
|
||||
return _merge_plugin_market_metadata(installed_plugin, market_plugin)
|
||||
|
||||
market_plugin = next(
|
||||
(
|
||||
plugin
|
||||
@@ -359,30 +430,9 @@ async def plugin_releases(
|
||||
}
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
market_plugins = await plugin_manager.async_get_plugins_from_market(
|
||||
repo_url, settings.VERSION_FLAG, force
|
||||
market_plugin = await _get_market_plugin_from_repo(
|
||||
plugin_manager, plugin_id, repo_url, force
|
||||
)
|
||||
market_plugin = next(
|
||||
(
|
||||
plugin
|
||||
for plugin in market_plugins or []
|
||||
if plugin.id == plugin_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not market_plugin and settings.VERSION_FLAG:
|
||||
compatible_plugins = await plugin_manager.async_get_plugins_from_market(
|
||||
repo_url, None, force
|
||||
)
|
||||
market_plugin = next(
|
||||
(
|
||||
plugin
|
||||
for plugin in compatible_plugins or []
|
||||
if plugin.id == plugin_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
latest_version = market_plugin.plugin_version if market_plugin else None
|
||||
current_version = plugin_manager.get_local_plugin_version(plugin_id)
|
||||
if not getattr(market_plugin, "release", False):
|
||||
@@ -393,8 +443,15 @@ async def plugin_releases(
|
||||
"items": [],
|
||||
}
|
||||
|
||||
async with async_fresh(force):
|
||||
release_items = await PluginHelper().async_get_plugin_release_versions(plugin_id, repo_url)
|
||||
plugin_helper = PluginHelper()
|
||||
has_release_cache = (
|
||||
await plugin_helper.async_has_plugin_release_cache(repo_url)
|
||||
if force
|
||||
else False
|
||||
)
|
||||
release_items = await plugin_helper.async_get_plugin_release_versions(plugin_id, repo_url)
|
||||
if force and has_release_cache:
|
||||
_schedule_plugin_release_refresh(plugin_id, repo_url)
|
||||
items = []
|
||||
for item in release_items:
|
||||
version = item.get("version")
|
||||
|
||||
@@ -1214,8 +1214,19 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
"""
|
||||
await cache_backend.clear(region=cache_region)
|
||||
|
||||
async def cache_exists(*args, **kwargs) -> bool:
|
||||
"""
|
||||
判断当前参数对应的有效缓存是否存在。
|
||||
"""
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
cached_value = await cache_backend.get(cache_key, region=cache_region)
|
||||
return should_cache(cached_value) and await async_is_valid_cache_value(
|
||||
cache_key, cached_value, cache_region
|
||||
)
|
||||
|
||||
async_wrapper.cache_region = cache_region
|
||||
async_wrapper.cache_clear = cache_clear
|
||||
async_wrapper.cache_exists = cache_exists
|
||||
return async_wrapper
|
||||
else:
|
||||
# 同步函数使用同步缓存后端
|
||||
@@ -1246,8 +1257,19 @@ def cached(region: Optional[str] = None, maxsize: Optional[int] = 1024, ttl: Opt
|
||||
"""
|
||||
cache_backend.clear(region=cache_region)
|
||||
|
||||
def cache_exists(*args, **kwargs) -> bool:
|
||||
"""
|
||||
判断当前参数对应的有效缓存是否存在。
|
||||
"""
|
||||
cache_key = __get_cache_key(args, kwargs)
|
||||
cached_value = cache_backend.get(cache_key, region=cache_region)
|
||||
return should_cache(cached_value) and is_valid_cache_value(
|
||||
cache_key, cached_value, cache_region
|
||||
)
|
||||
|
||||
wrapper.cache_region = cache_region
|
||||
wrapper.cache_clear = cache_clear
|
||||
wrapper.cache_exists = cache_exists
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -2218,35 +2218,48 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
normal_task_key = (loop, normalized_repo_url, False)
|
||||
force_task_key = (loop, normalized_repo_url, True)
|
||||
with self._release_task_lock:
|
||||
force_task = self._release_tasks.get(force_task_key)
|
||||
if force_task and not force_task.done():
|
||||
task_key = force_task_key
|
||||
task = force_task
|
||||
elif is_fresh():
|
||||
pending_normal_task = self._release_tasks.get(normal_task_key)
|
||||
if pending_normal_task and pending_normal_task.done():
|
||||
pending_normal_task = None
|
||||
task_key = force_task_key
|
||||
task = loop.create_task(
|
||||
self._async_refresh_plugin_repo_releases(normalized_repo_url, pending_normal_task)
|
||||
)
|
||||
self._release_tasks[task_key] = task
|
||||
task.add_done_callback(
|
||||
lambda completed_task: self._remove_release_task(task_key, completed_task)
|
||||
)
|
||||
if is_fresh():
|
||||
force_task = self._release_tasks.get(force_task_key)
|
||||
if force_task and not force_task.done():
|
||||
task_key = force_task_key
|
||||
task = force_task
|
||||
else:
|
||||
pending_normal_task = self._release_tasks.get(normal_task_key)
|
||||
if pending_normal_task and pending_normal_task.done():
|
||||
pending_normal_task = None
|
||||
task_key = force_task_key
|
||||
task = loop.create_task(
|
||||
self._async_refresh_plugin_repo_releases(normalized_repo_url, pending_normal_task)
|
||||
)
|
||||
self._release_tasks[task_key] = task
|
||||
task.add_done_callback(
|
||||
lambda completed_task: self._remove_release_task(task_key, completed_task)
|
||||
)
|
||||
else:
|
||||
task_key = normal_task_key
|
||||
task = self._release_tasks.get(task_key)
|
||||
if task is None or task.done():
|
||||
pending_normal_task = self._release_tasks.get(normal_task_key)
|
||||
if pending_normal_task is None or pending_normal_task.done():
|
||||
task = loop.create_task(self._async_get_plugin_repo_releases(normalized_repo_url))
|
||||
self._release_tasks[task_key] = task
|
||||
task.add_done_callback(
|
||||
lambda completed_task: self._remove_release_task(task_key, completed_task)
|
||||
)
|
||||
else:
|
||||
task = pending_normal_task
|
||||
|
||||
payload = await asyncio.shield(task)
|
||||
return self.__parse_plugin_release_response(pid, payload)
|
||||
|
||||
async def async_has_plugin_release_cache(self, repo_url: str) -> bool:
|
||||
"""
|
||||
判断指定仓库的 Release 列表缓存是否已经存在。
|
||||
"""
|
||||
if not repo_url:
|
||||
return False
|
||||
return await self._async_get_plugin_repo_releases.cache_exists(
|
||||
self, repo_url.rstrip("/")
|
||||
)
|
||||
|
||||
async def _async_refresh_plugin_repo_releases(
|
||||
self,
|
||||
repo_url: str,
|
||||
|
||||
Reference in New Issue
Block a user