From c1a602256309ec4d959c0688d657d7a6ef65754c Mon Sep 17 00:00:00 2001 From: 120318 <20685540+getupbuzz@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:10:43 +0800 Subject: [PATCH] Fix danmu source availability refresh --- .../views/library/resource_list.py | 25 +++++-- .../workflows/danmu/source_resolver.py | 12 ++-- backend/tests/test_danmu_addon_contracts.py | 67 +++++++++++++++++++ backend/tests/test_library_list.py | 25 +++++++ 4 files changed, 116 insertions(+), 13 deletions(-) diff --git a/backend/app/services/application/views/library/resource_list.py b/backend/app/services/application/views/library/resource_list.py index fb8d763..b0b6216 100644 --- a/backend/app/services/application/views/library/resource_list.py +++ b/backend/app/services/application/views/library/resource_list.py @@ -310,6 +310,15 @@ async def _build_action_availability_context( media_id: MediaID, season_number: int | None, ) -> _LibraryActionAvailabilityContext: + if not library_files: + return _LibraryActionAvailabilityContext( + media_server_open_enabled_directory_ids=set(), + media_server_sync_enabled_directory_ids=set(), + danmu_enabled_directory_ids=set(), + danmu_media_available=False, + existing_task_ids=set(), + ) + task_ids = sorted({file.task_id for file in library_files if file.task_id}) existing_tasks = await download_service.get_tasks_by_ids(task_ids) open_enabled_media_server_ids = { @@ -334,12 +343,18 @@ async def _build_action_availability_context( } danmu_config = danmu_application_service.config() danmu_enabled_directory_ids = set(danmu_config.directory_ids or []) if danmu_config.enabled else set() - danmu_media_available = await self._resolve_danmu_media_available( - media_id, - season_number=season_number, - danmu_enabled_directory_ids=danmu_enabled_directory_ids, - danmu_config=danmu_config, + has_danmu_candidate_file = any( + library_service.is_primary_file(file) and file.directory_id in danmu_enabled_directory_ids + for file in library_files ) + danmu_media_available = False + if has_danmu_candidate_file: + danmu_media_available = await self._resolve_danmu_media_available( + media_id, + season_number=season_number, + danmu_enabled_directory_ids=danmu_enabled_directory_ids, + danmu_config=danmu_config, + ) return _LibraryActionAvailabilityContext( media_server_open_enabled_directory_ids=media_server_open_enabled_directory_ids, media_server_sync_enabled_directory_ids=media_server_sync_enabled_directory_ids, diff --git a/backend/app/services/application/workflows/danmu/source_resolver.py b/backend/app/services/application/workflows/danmu/source_resolver.py index b5d2761..94211e8 100644 --- a/backend/app/services/application/workflows/danmu/source_resolver.py +++ b/backend/app/services/application/workflows/danmu/source_resolver.py @@ -17,15 +17,11 @@ async def media_with_fetchable_source( season_number: int | None, config: DanmuAddonConfig, ) -> MediaFullInfo | None: - if media_id.media_type.value == "tv" and positive_season_number(season_number) is None: + resolved_season = positive_season_number(season_number) if media_id.media_type.value == "tv" else None + if media_id.media_type.value == "tv" and resolved_season is None: return None - media = await media_service.info(media_id, season_number=season_number) - if media and self.has_fetchable_vendor(media, config): - return media - refreshed = await media_service.refresh_profile(media_id, season_number=season_number) - if refreshed and self.has_fetchable_vendor(refreshed, config): - return await media_service.info(media_id, season_number=season_number) or media - return await media_service.info(media_id, season_number=season_number) + + return await media_service.info(media_id, season_number=resolved_season) danmu_source_resolver = DanmuSourceResolver() diff --git a/backend/tests/test_danmu_addon_contracts.py b/backend/tests/test_danmu_addon_contracts.py index d3437e1..fbf7bc4 100644 --- a/backend/tests/test_danmu_addon_contracts.py +++ b/backend/tests/test_danmu_addon_contracts.py @@ -18,6 +18,7 @@ from app.schemas.domain.media_types import MediaType from app.schemas.domain.vendor import Vendor from app.schemas.media_id import MediaID +from app.services.application.workflows.danmu.source_resolver import DanmuSourceResolver from app.services.application.workflows.danmu.service import DanmuApplicationService from app.services.application.workflows.danmu.duration_guard import danmu_duration_guard from app.services.application.workflows.danmu.formatters import build_ass, build_xml @@ -220,6 +221,72 @@ def test_danmu_service_fetchable_vendor_rejects_platform_homepage(self): ) ) + def test_danmu_source_resolver_does_not_refresh_cached_media_without_fetchable_vendor(self): + async def run(): + resolver = DanmuSourceResolver() + media_id = MediaID.parse("tmdb:tv:94997") + config = AddonsConfig.model_validate({"danmu": {"enabled": True, "directory_ids": ["dir-1"]}}).danmu + media = MediaFullInfo( + media_id=media_id, + provider=media_id.provider, + media_type=MediaType.tv, + id=media_id.id, + title="House of the Dragon", + year=2022, + season_number=3, + vendors=[], + ) + + with ( + patch( + "app.services.application.workflows.danmu.source_resolver.media_service.info", + new=AsyncMock(return_value=media), + ) as info_mock, + patch( + "app.services.application.workflows.danmu.source_resolver.media_service.refresh_profile", + new=AsyncMock(), + ) as refresh_mock, + ): + resolved = await resolver.media_with_fetchable_source( + media_id, + season_number=3, + config=config, + ) + + self.assertEqual(media, resolved) + info_mock.assert_awaited_once_with(media_id, season_number=3) + refresh_mock.assert_not_awaited() + + asyncio.run(run()) + + def test_danmu_source_resolver_delegates_cache_miss_refresh_to_media_info(self): + async def run(): + resolver = DanmuSourceResolver() + media_id = MediaID.parse("tmdb:tv:94997") + config = AddonsConfig.model_validate({"danmu": {"enabled": True, "directory_ids": ["dir-1"]}}).danmu + + with ( + patch( + "app.services.application.workflows.danmu.source_resolver.media_service.info", + new=AsyncMock(return_value=None), + ) as info_mock, + patch( + "app.services.application.workflows.danmu.source_resolver.media_service.refresh_profile", + new=AsyncMock(), + ) as refresh_mock, + ): + resolved = await resolver.media_with_fetchable_source( + media_id, + season_number=3, + config=config, + ) + + self.assertIsNone(resolved) + info_mock.assert_awaited_once_with(media_id, season_number=3) + refresh_mock.assert_not_awaited() + + asyncio.run(run()) + def test_provider_entry_id_parsers(self): self.assertEqual("1lr0jb5ixi8", IqiyiDanmuProvider()._extract_page_id("http://www.iqiyi.com/v_1lr0jb5ixi8.html")) self.assertEqual("3561185", BilibiliDanmuProvider()._extract_ep_id("https://m.bilibili.com/bangumi/play/ep3561185")) diff --git a/backend/tests/test_library_list.py b/backend/tests/test_library_list.py index 8410698..f5d5dad 100644 --- a/backend/tests/test_library_list.py +++ b/backend/tests/test_library_list.py @@ -380,6 +380,31 @@ async def test_library_list_resolves_movie_danmu_actions_from_simple_media(monke assert LibraryResourceAction.DANMU_GENERATE in response.resources[0].actions +@pytest.mark.asyncio +async def test_library_list_skips_danmu_resolution_without_library_files(monkeypatch): + media_id = MediaID.parse("tmdb:tv:94997") + service = LibraryResourceListService() + resolver_mock = AsyncMock() + + monkeypatch.setattr( + "app.services.application.views.library.resource_list.danmu_source_resolver.media_with_fetchable_source", + resolver_mock, + ) + monkeypatch.setattr( + "app.services.application.views.library.resource_list.download_service.get_tasks_by_ids", + AsyncMock(), + ) + + context = await service._build_action_availability_context( + [], + media_id=media_id, + season_number=3, + ) + + assert context.danmu_media_available is False + resolver_mock.assert_not_awaited() + + def test_library_list_groups_original_disc_internal_files(): media_id = MediaID.parse("tmdb:movie:1") attrs = ResourceAttributes(resource_form="BluRay Disc", package_layout="BDMV", disc_number=1, disc_total=2)