fix(scheduler): disable wallpaper cache without wallpaper

This commit is contained in:
jxxghp
2026-09-13 21:02:15 +08:00
parent 65907328d2
commit 62fabea378
11 changed files with 118 additions and 12 deletions

View File

@@ -183,6 +183,7 @@ class SchedulerRuntimeConfig:
site_link: str | None
auto_update: bool = False
auto_update_resource: bool = True
wallpaper: str = ""
@property
def update_check_enabled(self) -> bool:

View File

@@ -184,6 +184,10 @@ class WallpaperHelper(metaclass=Singleton):
壁纸帮助类
"""
def clear_cache(self) -> None:
"""清除全部壁纸来源缓存,使壁纸配置变更即时生效。"""
_clear_wallpaper_caches()
def get_wallpaper(self) -> Optional[str]:
"""
获取登录页面壁纸

View File

@@ -62,6 +62,16 @@ def _subscription_search_job_specs(
)
def _wallpaper_job_specs(
services: SchedulerServices,
wallpaper: str,
) -> tuple[JobSpec, ...]:
"""仅在启用壁纸时返回壁纸缓存任务声明。"""
if not wallpaper:
return ()
return (JobSpec("random_wallpager", "壁纸缓存", services.get_wallpapers, "image"),)
class SchedulerCatalogOwner(_SchedulerOwnerBase):
"""调度器静态任务目录与 APScheduler 投影。"""
@@ -196,7 +206,7 @@ class SchedulerCatalogOwner(_SchedulerOwnerBase):
JobSpec("data_cleanup", "数据表清理", services.cleanup_data, "database"),
JobSpec("user_auth", "用户认证检查", self.user_auth, "security"),
JobSpec("scheduler_job", "公共定时服务", services.run_modules, "module"),
JobSpec("random_wallpager", "壁纸缓存", services.get_wallpapers, "image"),
*_wallpaper_job_specs(services, config.wallpaper),
JobSpec("sitedata_refresh", "站点数据刷新", services.refresh_site_data, "site"),
JobSpec("recommend_refresh", "推荐缓存", services.refresh_recommend, "recommend"),
JobSpec(
@@ -360,16 +370,17 @@ class SchedulerCatalogOwner(_SchedulerOwnerBase):
kwargs={"job_id": "transfer"},
)
# 后台刷新TMDB壁纸
self._scheduler.add_job(
self.start,
"interval",
id="random_wallpager",
name="壁纸缓存",
minutes=30,
next_run_time=datetime.now(pytz.timezone(config.timezone)) + timedelta(seconds=1),
kwargs={"job_id": "random_wallpager"},
)
# 仅在启用壁纸时后台刷新壁纸缓存。
if config.wallpaper:
self._scheduler.add_job(
self.start,
"interval",
id="random_wallpager",
name="壁纸缓存",
minutes=30,
next_run_time=datetime.now(pytz.timezone(config.timezone)) + timedelta(seconds=1),
kwargs={"job_id": "random_wallpager"},
)
# 公共定时服务
self._scheduler.add_job(

View File

@@ -49,6 +49,10 @@ class Scheduler(
CONFIG_WATCH = {
"DEV",
"TZ",
# 壁纸来源或地址变化时重建任务并清理来源缓存。
"WALLPAPER",
"WALLPAPER_IMAGE_URL",
"CUSTOMIZE_WALLPAPER_API_URL",
"COOKIECLOUD_INTERVAL",
"MEDIASERVER_SYNC_INTERVAL",
SystemConfigKey.MediaServers.value,

View File

@@ -30,6 +30,10 @@ class SchedulerLifecycleOwner(_SchedulerOwnerBase):
with self._lock:
if self._lifecycle_state != "reloading":
return
services = getattr(self, "_services", None)
clear_wallpaper_cache = getattr(services, "clear_wallpaper_cache", None)
if callable(clear_wallpaper_cache):
clear_wallpaper_cache()
self.init(_already_stopped=True)
def init(self, *, _already_stopped: bool = False) -> None:

View File

@@ -32,3 +32,4 @@ class SchedulerServices:
process_workflow: JobCallable
put_message: JobCallable
post_message: Callable[[Message], Any]
clear_wallpaper_cache: Callable[[], None] | None = None

View File

@@ -197,6 +197,7 @@ def build_scheduler_runtime_config(settings: Settings) -> SchedulerRuntimeConfig
site_link=settings.MP_DOMAIN("#/site"),
auto_update=settings.MOVIEPILOT_AUTO_UPDATE,
auto_update_resource=settings.AUTO_UPDATE_RESOURCE,
wallpaper=settings.WALLPAPER,
)

View File

@@ -69,6 +69,7 @@ def configure_scheduler_services() -> None:
cleanup_data=scheduler_chain.cleanup,
run_modules=scheduler_chain.scheduler_job,
get_wallpapers=WallpaperHelper().get_wallpapers,
clear_wallpaper_cache=WallpaperHelper().clear_cache,
refresh_site_data=site_chain.refresh_userdatas,
refresh_recommend=recommend_chain.refresh_recommend,
cache_subscribe_calendar=subscribe_chain.cache_calendar,

View File

@@ -82,6 +82,38 @@ def test_auto_update_setting_is_hot_reloadable() -> None:
assert "AUTO_UPDATE_RESOURCE" in Scheduler.CONFIG_WATCH
def test_wallpaper_settings_are_hot_reloadable() -> None:
"""壁纸来源或地址变化时必须重建调度器并清理缓存。"""
assert Scheduler.CONFIG_WATCH.issuperset({
"WALLPAPER",
"WALLPAPER_IMAGE_URL",
"CUSTOMIZE_WALLPAPER_API_URL",
})
@pytest.mark.parametrize("wallpaper", ["", "tmdb"])
def test_wallpaper_schedule_follows_wallpaper_setting(monkeypatch, wallpaper) -> None:
"""无壁纸时不注册缓存任务,启用来源后恢复注册。"""
scheduler = _scheduler()
scheduler._services = Mock()
background_scheduler = Mock()
monkeypatch.setattr(scheduler_catalog, "BackgroundScheduler", lambda **_kwargs: background_scheduler)
monkeypatch.setattr(scheduler_catalog, "get_plugin_manager", lambda: Mock())
monkeypatch.setattr(scheduler_catalog, "get_mediaserver_configs", lambda **_kwargs: [])
monkeypatch.setattr(scheduler, "init_workflow_jobs", lambda: None)
monkeypatch.setattr(scheduler, "init_agent_task_jobs", lambda: None)
monkeypatch.setattr(scheduler, "init_plugin_jobs", lambda: None)
scheduler._initialize_catalog(_config(wallpaper=wallpaper))
registered = wallpaper != ""
assert ("random_wallpager" in scheduler._jobs) is registered
assert any(
call.kwargs.get("id") == "random_wallpager"
for call in background_scheduler.add_job.call_args_list
) is registered
def test_disabled_database_backup_does_not_register_job() -> None:
"""关闭备份时不注册作业。"""
scheduler = _scheduler()

View File

@@ -587,6 +587,26 @@ async def test_config_reload_does_not_restart_scheduler_during_shutdown() -> Non
assert scheduler._lifecycle_state == "stopping"
@pytest.mark.anyio
async def test_config_reload_clears_wallpaper_cache_before_reinitializing() -> None:
"""壁纸配置热重载应先清理旧缓存,再建立新任务目录。"""
scheduler = _scheduler("reload-wallpaper", lambda: None)
calls = []
scheduler._services = SimpleNamespace(
clear_wallpaper_cache=lambda: calls.append("clear"),
)
def init(**_kwargs) -> None:
calls.append("init")
scheduler._lifecycle_state = "running"
scheduler.init = init
await scheduler.on_config_changed()
assert calls == ["clear", "init"]
@pytest.mark.anyio
async def test_concurrent_config_reload_waits_for_old_scheduler_shutdown(
monkeypatch,

View File

@@ -13,8 +13,10 @@ from app.application.image import (
configure_image_ports,
configure_wallpaper_providers,
reset_image_ports,
reset_wallpaper_providers,
)
from app.runtime.config import ConfigModel
from app.runtime.config import ConfigModel, Settings
from app.startup.composition.configuration import build_scheduler_runtime_config
class _WallpaperTransport:
@@ -119,6 +121,24 @@ def test_reconfigure_image_ports_clears_wallpaper_cache():
reset_image_ports(*previous)
def test_wallpaper_helper_clear_cache_refreshes_provider_result():
"""壁纸配置热重载后必须丢弃旧来源缓存。"""
helper = WallpaperHelper()
values = iter(("old-wallpaper", "new-wallpaper"))
configure_wallpaper_providers(
tmdb_wallpaper=lambda: next(values),
tmdb_wallpapers=lambda _count: [],
mediaserver_wallpaper=lambda: None,
mediaserver_wallpapers=lambda _count: [],
)
try:
assert helper.get_tmdb_wallpaper() == "old-wallpaper"
helper.clear_cache()
assert helper.get_tmdb_wallpaper() == "new-wallpaper"
finally:
reset_wallpaper_providers()
def test_static_wallpaper_returns_local_or_remote_address_directly(monkeypatch):
"""静态模式不发起 API 请求,直接沿用用户填写的本地路径或 URL。"""
config = SimpleNamespace(wallpaper="static", wallpaper_image_url="/local/wallpaper.jpg")
@@ -141,6 +161,13 @@ def test_wallpaper_settings_define_backward_compatible_defaults():
assert ConfigModel.model_fields["WALLPAPER_IMAGE_URL"].default is None
def test_scheduler_runtime_config_carries_wallpaper_setting():
"""调度器快照必须读取当前壁纸来源,供任务目录决定是否注册缓存。"""
settings = Settings(WALLPAPER="tmdb")
assert build_scheduler_runtime_config(settings).wallpaper == "tmdb"
def test_login_global_settings_expose_wallpaper_rotation_interval(monkeypatch):
"""登录前全局设置必须下发轮换间隔,避免前端退回硬编码时钟。"""
values = {