Skip to content

Commit dc23977

Browse files
committed
fix: 修复站点用户数据转换并完善媒体来源扩展
1 parent c2a27f7 commit dc23977

11 files changed

Lines changed: 179 additions & 2 deletions

File tree

app/api/endpoints/media.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,62 @@
2828
from app.adapters.web.security.access import verify_token, verify_apitoken
2929
from app.api.deps import get_current_active_user, get_current_active_superuser
3030
from app.schemas.category import CategoryConfig
31+
from app.schemas.event import MediaSourceInfo as _SchemaMediaSourceInfo
3132
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType
3233
from app.domain.media import is_music_media_source, normalize_music_type, parse_media_source_selection
3334
from app.schemas.media import normalize_media_source, resolve_media_identity
3435

3536
router = ResponseAPIRouter()
3637

3738

39+
_BUILTIN_MEDIA_SOURCES = (
40+
_SchemaMediaSourceInfo(name="TheMovieDb", media_source=MediaSource.TMDB),
41+
_SchemaMediaSourceInfo(name="豆瓣", media_source=MediaSource.Douban),
42+
_SchemaMediaSourceInfo(name="Bangumi", media_source=MediaSource.Bangumi),
43+
_SchemaMediaSourceInfo(name="AniList", media_source=MediaSource.AniList),
44+
_SchemaMediaSourceInfo(name="IMDb", media_source=MediaSource.IMDb),
45+
_SchemaMediaSourceInfo(name="TVDB", media_source=MediaSource.TVDB),
46+
_SchemaMediaSourceInfo(
47+
name="MusicBrainz",
48+
media_source=MediaSource.MusicBrainz,
49+
media_types=[MediaType.MUSIC],
50+
),
51+
_SchemaMediaSourceInfo(
52+
name="TheAudioDB",
53+
media_source=MediaSource.TheAudioDB,
54+
media_types=[MediaType.MUSIC],
55+
),
56+
_SchemaMediaSourceInfo(
57+
name="豆瓣音乐",
58+
media_source=MediaSource.DoubanMusic,
59+
media_types=[MediaType.MUSIC],
60+
),
61+
_SchemaMediaSourceInfo(name="哔哩哔哩", media_source=MediaSource.Bilibili),
62+
_SchemaMediaSourceInfo(name="芒果TV", media_source=MediaSource.MangoTV),
63+
_SchemaMediaSourceInfo(name="咪咕视频", media_source=MediaSource.MiguVideo),
64+
_SchemaMediaSourceInfo(name="腾讯视频", media_source=MediaSource.TencentVideo),
65+
_SchemaMediaSourceInfo(name="爱奇艺", media_source=MediaSource.Iqiyi),
66+
)
67+
68+
69+
def _registered_media_sources() -> list[_SchemaMediaSourceInfo]:
70+
"""合并内置与启用插件声明的媒体来源,并按来源标识去重。"""
71+
from app.runtime.extensions.plugin_manager import PluginManager
72+
73+
result = list(_BUILTIN_MEDIA_SOURCES)
74+
seen = {source.media_source for source in result}
75+
for raw_source in PluginManager().get_media_sources():
76+
try:
77+
source = _SchemaMediaSourceInfo.model_validate(raw_source)
78+
except Exception:
79+
continue
80+
if source.media_source in seen:
81+
continue
82+
result.append(source)
83+
seen.add(source.media_source)
84+
return result
85+
86+
3887
def _split_media_source_query(value: object) -> tuple[str, ...]:
3988
"""展开重复或逗号分隔的来源参数,并在枚举校验前规范历史别名。"""
4089
if value in (None, ""):
@@ -324,6 +373,16 @@ def __get_source(obj: Union[_SchemaMediaInfo, _SchemaMediaPerson, dict]):
324373
return sorted_result[(page - 1) * count : page * count]
325374

326375

376+
@router.get(
377+
"/source",
378+
summary="获取媒体数据源",
379+
response_model=list[_SchemaMediaSourceInfo],
380+
)
381+
def source(_: _SchemaTokenPayload = Depends(verify_token)) -> list[_SchemaMediaSourceInfo]:
382+
"""返回内置及启用插件注册的媒体数据源,供前端统一构造来源选项。"""
383+
return _registered_media_sources()
384+
385+
327386
@router.post(
328387
"/scrape/{storage}", summary="刮削媒体信息", response_model=_SchemaResponse[None]
329388
)

app/chain/scraping.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,17 @@ def metadata_img(
567567
获取图片名称和url,合并所有模块的结果。
568568
优先使用高优先级模块的图片,低优先级模块补充缺失的图片类型。
569569
"""
570+
# 插件扩展来源没有宿主内置刮削器,优先让插件按自身来源返回图片地址;
571+
# 内置来源仍保留原有多模块合并逻辑,避免改变既有图片补全顺序。
572+
if mediainfo and mediainfo.media_source not in tuple(MediaSource):
573+
plugin_images = self.run_module(
574+
"metadata_img",
575+
mediainfo=mediainfo,
576+
season=season,
577+
episode=episode,
578+
)
579+
if isinstance(plugin_images, dict):
580+
return plugin_images or None
570581
merged = {}
571582
for module in sorted(
572583
self.modulemanager.get_running_modules("metadata_img"),

app/plugins/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,15 @@ def get_module(self) -> Dict[str, Any]:
199199
"""
200200
pass
201201

202+
def get_media_source(self) -> List[Dict[str, Any]]:
203+
"""
204+
注册插件提供的媒体数据源。
205+
206+
返回的每项至少包含 ``name``、``media_source`` 和 ``media_types``;实际的
207+
搜索、识别、图片和 NFO 刮削实现通过 ``get_module`` 暴露对应方法。
208+
"""
209+
pass
210+
202211
def get_actions(self) -> List[Dict[str, Any]]:
203212
"""
204213
获取插件工作流动作

app/runtime/extensions/plugin/contracts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class PluginHookContract:
3434
PluginHookContract("get_api"),
3535
PluginHookContract("get_service", requires_enabled=True),
3636
PluginHookContract("get_module", requires_enabled=True),
37+
PluginHookContract("get_media_source", requires_enabled=True),
3738
PluginHookContract("get_actions", requires_enabled=True),
3839
PluginHookContract("get_agent_tools", requires_enabled=True),
3940
PluginHookContract("get_auth_providers", requires_enabled=True),

app/runtime/extensions/plugin/projection.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ def modules(self, pid: Optional[str] = None) -> Dict[tuple, Dict[str, Any]]:
9292
self._logger.error(f"获取插件 {plugin_id} 模块出错:{str(error)}")
9393
return modules
9494

95+
def media_sources(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
96+
"""聚合启用插件声明的媒体数据源。"""
97+
sources: list[dict] = []
98+
for plugin_id, plugin in self._items(pid):
99+
if not supports_plugin_hook(plugin, "get_media_source"):
100+
continue
101+
try:
102+
if not plugin.get_state():
103+
continue
104+
for source in plugin.get_media_source() or []:
105+
if isinstance(source, dict):
106+
item = source.copy()
107+
item.setdefault("plugin_id", plugin_id)
108+
sources.append(item)
109+
except Exception as error:
110+
self._logger.error(f"获取插件 {plugin_id} 媒体数据源出错:{str(error)}")
111+
return sources
112+
95113
def actions(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
96114
"""聚合启用插件的工作流动作。"""
97115
actions: list[dict] = []

app/runtime/extensions/plugin_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,10 @@ def get_plugin_modules(self, pid: Optional[str] = None) -> Dict[tuple, Dict[str,
616616
"""
617617
return self._plugin_projection().modules(pid)
618618

619+
def get_media_sources(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
620+
"""获取运行中插件声明的媒体数据源。"""
621+
return self._plugin_projection().media_sources(pid)
622+
619623
def get_plugin_actions(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
620624
"""
621625
获取插件动作

app/schemas/event.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pydantic import BaseModel, Field, field_validator, model_validator
55

66
from app.schemas.common import JsonData
7-
from app.schemas.types import NotificationChannel
7+
from app.schemas.types import MediaType, NotificationChannel
88
from app.schemas.file import FileItem
99
from app.schemas.media import OptionalMediaIdentityMixin, RequiredMediaIdentityMixin
1010
from app.schemas.types import MediaSource
@@ -527,6 +527,22 @@ def _media_source_from_prefix(mediaid_prefix: str) -> MediaSource:
527527
return MediaSource(mediaid_prefix)
528528

529529

530+
class MediaSourceInfo(BaseModel):
531+
"""
532+
媒体数据源注册描述。
533+
534+
插件通过该描述声明来源的展示名称和支持的媒体类型;识别、搜索和刮削的
535+
实际实现仍由插件模块方法提供,宿主只负责把来源传递到统一媒体链路。
536+
"""
537+
538+
name: str = Field(..., description="数据源展示名称")
539+
media_source: MediaSource = Field(..., description="规范媒体来源标识")
540+
media_types: List[MediaType] = Field(
541+
default_factory=lambda: [MediaType.MOVIE, MediaType.TV],
542+
description="支持的媒体类型",
543+
)
544+
545+
530546
class DiscoverSourceEventData(ChainEventData):
531547
"""
532548
DiscoverSource 事件的数据模型

app/schemas/exports.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
'MediaServerPlayItem': ('app.schemas.mediaserver', 'MediaServerPlayItem'),
187187
'MediaServerSeasonInfo': ('app.schemas.mediaserver', 'MediaServerSeasonInfo'),
188188
'MediaSource': ('app.schemas.transfer', 'MediaSource'),
189+
'MediaSourceInfo': ('app.schemas.event', 'MediaSourceInfo'),
189190
'MediaType': ('app.schemas.subscribe', 'MediaType'),
190191
'Message': ('app.schemas.message', 'Message'),
191192
'MessageClearBefore': ('app.schemas.message', 'MessageClearBefore'),
@@ -420,7 +421,7 @@
420421
'LocaleHelper': ['app.schemas.dashboard', 'app.schemas.response'],
421422
'MediaInfo': ['app.schemas.context', 'app.schemas.system', 'app.schemas.transfer', 'app.schemas.workflow'],
422423
'MediaSource': ['app.schemas.cache', 'app.schemas.context', 'app.schemas.event', 'app.schemas.history', 'app.schemas.mediaserver', 'app.schemas.music', 'app.schemas.subscribe', 'app.schemas.transfer'],
423-
'MediaType': ['app.schemas.mediaserver', 'app.schemas.subscribe'],
424+
'MediaType': ['app.schemas.event', 'app.schemas.mediaserver', 'app.schemas.subscribe'],
424425
'MetaInfo': ['app.schemas.context', 'app.schemas.system', 'app.schemas.transfer'],
425426
'MusicInfo': ['app.schemas.context', 'app.schemas.music', 'app.schemas.transfer'],
426427
'MusicMeta': ['app.schemas.context', 'app.schemas.music', 'app.schemas.transfer'],

app/schemas/site.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ class SiteUserData(BaseModel):
124124
# 更新时间
125125
updated_time: Optional[str] = None
126126

127+
# 查询仓储返回 SQLAlchemy ORM 对象时,从对象属性读取字段。
128+
model_config = ConfigDict(from_attributes=True)
129+
127130

128131
class SiteAuth(BaseModel):
129132
"""站点认证模块及其参数。"""

tests/test_plugin_projection.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,29 @@ def test_projection_preserves_services_modules_actions_and_pid_filter():
8080
}]
8181

8282

83+
def test_projection_collects_enabled_media_source_declarations():
84+
"""只投影启用插件的媒体来源声明,并附带插件 ID 便于诊断。"""
85+
demo = _Plugin(
86+
get_media_source=lambda: [{
87+
"name": "Acme Video",
88+
"media_source": "acme.video",
89+
"media_types": ["电影", "电视剧"],
90+
}],
91+
)
92+
disabled = _Plugin(
93+
enabled=False,
94+
get_media_source=lambda: [{"name": "Disabled", "media_source": "disabled"}],
95+
)
96+
projection = PluginProjection({"Demo": demo, "Disabled": disabled})
97+
98+
assert projection.media_sources() == [{
99+
"name": "Acme Video",
100+
"media_source": "acme.video",
101+
"media_types": ["电影", "电视剧"],
102+
"plugin_id": "Demo",
103+
}]
104+
105+
83106
def test_projection_isolates_one_plugin_hook_failure():
84107
"""单个插件 hook 失败只记日志,不阻断其他插件投影。"""
85108
errors = []

0 commit comments

Comments
 (0)