refactor: unify emby family item formatting

This commit is contained in:
jxxghp
2026-08-24 06:08:04 +08:00
parent 073ff7ff81
commit 1047bfa821
8 changed files with 115 additions and 157 deletions
+58
View File
@@ -1,9 +1,12 @@
import re import re
from collections.abc import Iterable, Mapping from collections.abc import Iterable, Mapping
from datetime import datetime
from typing import Any, Optional, Protocol from typing import Any, Optional, Protocol
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
from app.domain.context import MusicInfo from app.domain.context import MusicInfo
from app.runtime.log import logger
from app.schemas.media import normalize_media_source, resolve_media_identity from app.schemas.media import normalize_media_source, resolve_media_identity
from app.application.service import ServiceBaseHelper from app.application.service import ServiceBaseHelper
from app.schemas.system import MediaServerConf from app.schemas.system import MediaServerConf
@@ -284,6 +287,61 @@ class MusicMediaServerHelper:
) )
def format_emby_family_item(
item: Mapping[str, Any],
*,
server: str,
include_server_id: bool = False,
) -> Optional[_SchemaMediaServerItem]:
"""把 Emby 系服务条目转换为统一媒体服务器模型,并保留各服务既有字段差异。"""
try:
user_data = item.get("UserData") or {}
if not user_data:
user_state = None
else:
resume = (
user_data.get("PlaybackPositionTicks")
and user_data.get("PlaybackPositionTicks") > 0
)
last_played_date = user_data.get("LastPlayedDate")
if last_played_date is not None and "." in last_played_date:
last_played_date = last_played_date.split(".")[0]
user_state = _SchemaMediaServerItemUserState(
played=user_data.get("Played"),
resume=resume,
last_played_date=datetime.strptime(
last_played_date,
"%Y-%m-%dT%H:%M:%S",
).strftime("%Y-%m-%d %H:%M:%S") if last_played_date else None,
play_count=user_data.get("PlayCount"),
percentage=user_data.get("PlayedPercentage"),
)
media_source, media_id = MediaServerIdentityHelper.from_provider_ids(
item.get("ProviderIds")
)
fields = {
"server": server,
"library": item.get("ParentId"),
"item_id": item.get("Id"),
"item_type": item.get("Type"),
"title": item.get("Name"),
"original_title": item.get("OriginalTitle"),
"year": item.get("ProductionYear"),
"media_source": media_source,
"media_id": media_id,
"path": item.get("Path"),
"note": MusicMediaServerHelper.build_note(item)
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
"user_state": user_state,
}
if include_server_id:
fields["server_id"] = item.get("ServerId")
return _SchemaMediaServerItem(**fields)
except Exception as error:
logger.error(error)
return None
class MediaServerHelper(ServiceBaseHelper[MediaServerConf]): class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
"""管理媒体服务器配置,并按类型发现已启用的服务实例。""" """管理媒体服务器配置,并按类型发现已启用的服务实例。"""
+7 -47
View File
@@ -1,7 +1,6 @@
import json import json
import re import re
import traceback import traceback
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List, Optional, Union, Dict, Generator, Tuple, Any from typing import List, Optional, Union, Dict, Generator, Tuple, Any
@@ -9,7 +8,6 @@ from requests import Response
from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem
@@ -17,7 +15,7 @@ from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.application.mediaserver import MediaServerIdentityHelper, MusicMediaServerHelper from app.application.mediaserver import MediaServerIdentityHelper, format_emby_family_item
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.mediaserver import MediaServerItem from app.schemas.mediaserver import MediaServerItem
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
@@ -731,50 +729,12 @@ class Emby:
@staticmethod @staticmethod
def __format_item_info(item) -> Optional[_SchemaMediaServerItem]: def __format_item_info(item) -> Optional[_SchemaMediaServerItem]:
""" """通过统一 Emby 系转换契约生成条目,并保留 Emby 的服务器 ID。"""
格式化item return format_emby_family_item(
""" item,
try: server="emby",
user_data = item.get("UserData", {}) include_server_id=True,
if not user_data: )
user_state = None
else:
resume = item.get("UserData", {}).get("PlaybackPositionTicks") and item.get("UserData", {}).get(
"PlaybackPositionTicks") > 0
last_played_date = item.get("UserData", {}).get("LastPlayedDate")
if last_played_date is not None and "." in last_played_date:
last_played_date = last_played_date.split(".")[0]
user_state = _SchemaMediaServerItemUserState(
played=item.get("UserData", {}).get("Played"),
resume=resume,
last_played_date=datetime.strptime(last_played_date, "%Y-%m-%dT%H:%M:%S").strftime(
"%Y-%m-%d %H:%M:%S") if last_played_date else None,
play_count=item.get("UserData", {}).get("PlayCount"),
percentage=item.get("UserData", {}).get("PlayedPercentage"),
)
media_source, media_id = MediaServerIdentityHelper.from_provider_ids(
item.get("ProviderIds")
)
return _SchemaMediaServerItem(
server="emby",
library=item.get("ParentId"),
server_id=item.get("ServerId"),
item_id=item.get("Id"),
item_type=item.get("Type"),
title=item.get("Name"),
original_title=item.get("OriginalTitle"),
year=item.get("ProductionYear"),
media_source=media_source,
media_id=media_id,
path=item.get("Path"),
note=MusicMediaServerHelper.build_note(item)
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
user_state=user_state
)
except Exception as e:
logger.error(e)
return None
def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]: def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]:
""" """
+3 -46
View File
@@ -1,20 +1,18 @@
import json import json
import posixpath import posixpath
from datetime import datetime
from typing import List, Union, Optional, Dict, Generator, Tuple, Any from typing import List, Union, Optional, Dict, Generator, Tuple, Any
from requests import Response from requests import Response
from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo
from app.runtime.settings import RuntimeSettingsCompat from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat() settings = RuntimeSettingsCompat()
from app.application.mediaserver import MediaServerIdentityHelper, MusicMediaServerHelper from app.application.mediaserver import MediaServerIdentityHelper, format_emby_family_item
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.schemas.types import MediaSource from app.schemas.types import MediaSource
@@ -889,49 +887,8 @@ class Jellyfin:
@staticmethod @staticmethod
def __format_item_info(item) -> Optional[_SchemaMediaServerItem]: def __format_item_info(item) -> Optional[_SchemaMediaServerItem]:
""" """通过统一 Emby 系转换契约生成 Jellyfin 条目。"""
格式化item return format_emby_family_item(item, server="jellyfin")
"""
try:
user_data = item.get("UserData", {})
if not user_data:
user_state = None
else:
resume = item.get("UserData", {}).get("PlaybackPositionTicks") and item.get("UserData", {}).get(
"PlaybackPositionTicks") > 0
last_played_date = item.get("UserData", {}).get("LastPlayedDate")
if last_played_date is not None and "." in last_played_date:
last_played_date = last_played_date.split(".")[0]
user_state = _SchemaMediaServerItemUserState(
played=item.get("UserData", {}).get("Played"),
resume=resume,
last_played_date=datetime.strptime(last_played_date, "%Y-%m-%dT%H:%M:%S").strftime(
"%Y-%m-%d %H:%M:%S") if last_played_date else None,
play_count=item.get("UserData", {}).get("PlayCount"),
percentage=item.get("UserData", {}).get("PlayedPercentage"),
)
media_source, media_id = MediaServerIdentityHelper.from_provider_ids(
item.get("ProviderIds")
)
return _SchemaMediaServerItem(
server="jellyfin",
library=item.get("ParentId"),
item_id=item.get("Id"),
item_type=item.get("Type"),
title=item.get("Name"),
original_title=item.get("OriginalTitle"),
year=item.get("ProductionYear"),
media_source=media_source,
media_id=media_id,
path=item.get("Path"),
note=MusicMediaServerHelper.build_note(item)
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
user_state=user_state
)
except Exception as e:
logger.error(e)
return None
def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]: def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]:
""" """
+3 -46
View File
@@ -1,7 +1,6 @@
import json import json
import re import re
import traceback import traceback
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List, Optional, Union, Dict, Generator, Tuple, Any from typing import List, Optional, Union, Dict, Generator, Tuple, Any
@@ -9,12 +8,11 @@ from requests import Response
from app.schemas.dashboard import Statistic as _SchemaStatistic from app.schemas.dashboard import Statistic as _SchemaStatistic
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary from app.schemas.mediaserver import MediaServerLibrary as _SchemaMediaServerLibrary
from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem from app.schemas.mediaserver import MediaServerPlayItem as _SchemaMediaServerPlayItem
from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem
from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo
from app.application.mediaserver import MediaServerIdentityHelper, MusicMediaServerHelper from app.application.mediaserver import MediaServerIdentityHelper, format_emby_family_item
from app.runtime.log import logger from app.runtime.log import logger
from app.schemas.mediaserver import MediaServerItem from app.schemas.mediaserver import MediaServerItem
from app.schemas.types import MediaSource, MediaType from app.schemas.types import MediaSource, MediaType
@@ -813,49 +811,8 @@ class ZSpace:
@staticmethod @staticmethod
def __format_item_info(item) -> Optional[_SchemaMediaServerItem]: def __format_item_info(item) -> Optional[_SchemaMediaServerItem]:
""" """通过统一 Emby 系转换契约生成极影视条目。"""
格式化item return format_emby_family_item(item, server="zspace")
"""
try:
user_data = item.get("UserData", {})
if not user_data:
user_state = None
else:
resume = item.get("UserData", {}).get("PlaybackPositionTicks") and item.get("UserData", {}).get(
"PlaybackPositionTicks") > 0
last_played_date = item.get("UserData", {}).get("LastPlayedDate")
if last_played_date is not None and "." in last_played_date:
last_played_date = last_played_date.split(".")[0]
user_state = _SchemaMediaServerItemUserState(
played=item.get("UserData", {}).get("Played"),
resume=resume,
last_played_date=datetime.strptime(last_played_date, "%Y-%m-%dT%H:%M:%S").strftime(
"%Y-%m-%d %H:%M:%S") if last_played_date else None,
play_count=item.get("UserData", {}).get("PlayCount"),
percentage=item.get("UserData", {}).get("PlayedPercentage"),
)
media_source, media_id = MediaServerIdentityHelper.from_provider_ids(
item.get("ProviderIds")
)
return _SchemaMediaServerItem(
server="zspace",
library=item.get("ParentId"),
item_id=item.get("Id"),
item_type=item.get("Type"),
title=item.get("Name"),
original_title=item.get("OriginalTitle"),
year=item.get("ProductionYear"),
media_source=media_source,
media_id=media_id,
path=item.get("Path"),
note=MusicMediaServerHelper.build_note(item)
if item.get("Type") in {"MusicAlbum", "Audio"} else None,
user_state=user_state
)
except Exception as e:
logger.error(e)
return None
def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]: def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]:
""" """
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` > 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md`
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程。 > 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单
## 当前复核结论(2026-08-24 ## 当前复核结论(2026-08-24
@@ -299,13 +299,24 @@
`app.adapters.external.server -> app.runtime.config/tasks` 两条允许边,模块保持 `806`、内部边为 `6543` `app.adapters.external.server -> app.runtime.config/tasks` 两条允许边,模块保持 `806`、内部边为 `6543`
12 组禁止边与唯一隔离 TMDB SCC 均未变化。 12 组禁止边与唯一隔离 TMDB SCC 均未变化。
### 长期整改阶段 29:Emby 系条目转换协议统一(2026-08-24)
- Emby、Jellyfin 与极影视原先各自维护一套用户播放状态、Provider ID、音乐备注和媒体服务器条目投影;
Jellyfin/极影视的同构函数还占用了重复代码门禁中最后一组“待后续 Phase 清理”白名单。
- 转换规则现统一由 `app.application.mediaserver.format_emby_family_item()` 所有,三个服务类只保留带各自
`server` 标识的薄适配器;Emby 独有的 `ServerId` 投影继续显式开启,Jellyfin/极影视仍保持不投影该字段。
- 三个私有静态入口的名称、参数与返回类型均保留,服务查询、统一媒体身份和音乐匹配行为不变;插件仓、
SDK/Compat 映射和 V1/V2/V3 插件公开合同均未修改。重复代码白名单因此清零,后续新增同构实现会直接失败。
- 依赖基线只新增 `app.application.mediaserver -> app.runtime.log` 及父包边,用于保持原有转换异常日志;模块
保持 `806`、内部边为 `6545`,12 组禁止边与唯一隔离 TMDB SCC 均未变化。
### 总体判断 ### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 - 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。
- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 - `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。
- 依赖图当前为 `806` 个 Python 模块、`6543` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 - 依赖图当前为 `806` 个 Python 模块、`6545` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 - 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
+4 -2
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [], "runtime_to_db": [],
"workflow_to_db": [] "workflow_to_db": []
}, },
"edge_count": 6543, "edge_count": 6545,
"edge_sha256": "b9b6ba268382e7027f5557643defb09848c054cb68079345f2394d9b7faeb42c", "edge_sha256": "5a57ed0ff52fda67abf49f5ca956fea0adeb0ac595054e0f8f81374ad843a2bd",
"edges": [ "edges": [
"app -> app.runtime", "app -> app.runtime",
"app -> app.runtime.compat", "app -> app.runtime.compat",
@@ -2595,6 +2595,8 @@
"app.application.mediaserver -> app.application.service", "app.application.mediaserver -> app.application.service",
"app.application.mediaserver -> app.domain", "app.application.mediaserver -> app.domain",
"app.application.mediaserver -> app.domain.context", "app.application.mediaserver -> app.domain.context",
"app.application.mediaserver -> app.runtime",
"app.application.mediaserver -> app.runtime.log",
"app.application.mediaserver -> app.schemas", "app.application.mediaserver -> app.schemas",
"app.application.mediaserver -> app.schemas.media", "app.application.mediaserver -> app.schemas.media",
"app.application.mediaserver -> app.schemas.mediaserver", "app.application.mediaserver -> app.schemas.mediaserver",
+1 -5
View File
@@ -17,11 +17,7 @@ MIN_FINGERPRINT_SIZE = 1000
# 参与告警的最小函数体节点数:过滤 setter/getter 等小函数。 # 参与告警的最小函数体节点数:过滤 setter/getter 等小函数。
MIN_FUNCTION_SIZE = 40 MIN_FUNCTION_SIZE = 40
# 存量白名单:(模块名, 函数名) 集合,各 Phase 清理后同步移除。 # 存量白名单:(模块名, 函数名) 集合,各 Phase 清理后同步移除。
KNOWN_DUPLICATES = { KNOWN_DUPLICATES: set[tuple[str, str]] = set()
# 服务实现类(非模块类)的条目信息格式化样板(待后续 Phase 清理)。
("app.modules.jellyfin.jellyfin", "__format_item_info"),
("app.modules.zspace.zspace", "__format_item_info"),
}
def _normalize(node: ast.AST) -> str: def _normalize(node: ast.AST) -> str:
+26 -9
View File
@@ -14,8 +14,8 @@ from app.modules.jellyfin import JellyfinModule
from app.modules.jellyfin.jellyfin import Jellyfin from app.modules.jellyfin.jellyfin import Jellyfin
from app.modules.plex import PlexModule from app.modules.plex import PlexModule
from app.modules.plex.plex import Plex from app.modules.plex.plex import Plex
from app.modules.trimemedia import TrimeMediaModule from app.modules.trimemedia import TrimeMediaModule # pylint: disable=no-name-in-module
from app.modules.ugreen import UgreenModule from app.modules.ugreen import UgreenModule # pylint: disable=no-name-in-module
from app.modules.zspace import ZSpaceModule from app.modules.zspace import ZSpaceModule
from app.modules.zspace.zspace import ZSpace from app.modules.zspace.zspace import ZSpace
@@ -97,25 +97,42 @@ def test_music_media_server_helper_normalizes_emby_music_fields():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"formatter", ("formatter", "server", "server_id"),
[ [
Emby._Emby__format_item_info, (Emby._Emby__format_item_info, "emby", "server-1"),
Jellyfin._Jellyfin__format_item_info, (Jellyfin._Jellyfin__format_item_info, "jellyfin", None),
ZSpace._ZSpace__format_item_info, (ZSpace._ZSpace__format_item_info, "zspace", None),
], ],
) )
def test_emby_family_clients_preserve_music_match_fields(formatter): def test_emby_family_clients_preserve_item_contract(formatter, server, server_id):
"""Emby 系客户端格式化音乐结果时必须保留艺术家、专辑和曲目数""" """Emby 系客户端复用统一转换时必须保留服务身份及音乐匹配字段"""
item = formatter({ item = formatter({
"Id": "album-1", "Id": "album-1",
"ServerId": "server-1",
"ParentId": "library-1",
"Type": "MusicAlbum", "Type": "MusicAlbum",
"Name": "叶惠美", "Name": "叶惠美",
"AlbumArtists": [{"Name": "周杰伦"}], "AlbumArtists": [{"Name": "周杰伦"}],
"ChildCount": 11, "ChildCount": 11,
"ProviderIds": {}, "ProviderIds": {"Tmdb": "123"},
"UserData": {
"Played": False,
"PlaybackPositionTicks": 1,
"LastPlayedDate": "2026-08-24T12:34:56.123456Z",
"PlayCount": 2,
"PlayedPercentage": 25.5,
},
}) })
assert item is not None assert item is not None
assert item.server == server
assert item.server_id == server_id
assert item.library == "library-1"
assert item.media_source == "themoviedb"
assert item.media_id == "123"
assert item.user_state is not None
assert item.user_state.resume is True
assert item.user_state.last_played_date == "2026-08-24 12:34:56"
assert item.note == { assert item.note == {
"artist": "周杰伦", "artist": "周杰伦",
"artists": ["周杰伦"], "artists": ["周杰伦"],