mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-30 20:54:32 +08:00
refactor: unify emby family item formatting
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import re
|
||||
from collections.abc import Iterable, Mapping
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
|
||||
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
|
||||
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.application.service import ServiceBaseHelper
|
||||
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]):
|
||||
"""管理媒体服务器配置,并按类型发现已启用的服务实例。"""
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
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.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 MediaServerPlayItem as _SchemaMediaServerPlayItem
|
||||
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
|
||||
|
||||
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.schemas.mediaserver import MediaServerItem
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
@@ -731,50 +729,12 @@ class Emby:
|
||||
|
||||
@staticmethod
|
||||
def __format_item_info(item) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
格式化item
|
||||
"""
|
||||
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="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
|
||||
"""通过统一 Emby 系转换契约生成条目,并保留 Emby 的服务器 ID。"""
|
||||
return format_emby_family_item(
|
||||
item,
|
||||
server="emby",
|
||||
include_server_id=True,
|
||||
)
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import json
|
||||
import posixpath
|
||||
from datetime import datetime
|
||||
from typing import List, Union, Optional, Dict, Generator, Tuple, Any
|
||||
|
||||
from requests import Response
|
||||
|
||||
from app.schemas.dashboard import Statistic as _SchemaStatistic
|
||||
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 MediaServerPlayItem as _SchemaMediaServerPlayItem
|
||||
from app.schemas.mediaserver import WebhookEventInfo as _SchemaWebhookEventInfo
|
||||
from app.runtime.settings import 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.schemas.types import MediaType
|
||||
from app.schemas.types import MediaSource
|
||||
@@ -889,49 +887,8 @@ class Jellyfin:
|
||||
|
||||
@staticmethod
|
||||
def __format_item_info(item) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
格式化item
|
||||
"""
|
||||
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
|
||||
"""通过统一 Emby 系转换契约生成 Jellyfin 条目。"""
|
||||
return format_emby_family_item(item, server="jellyfin")
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
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.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 MediaServerPlayItem as _SchemaMediaServerPlayItem
|
||||
from app.schemas.mediaserver import RefreshMediaItem as _SchemaRefreshMediaItem
|
||||
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.schemas.mediaserver import MediaServerItem
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
@@ -813,49 +811,8 @@ class ZSpace:
|
||||
|
||||
@staticmethod
|
||||
def __format_item_info(item) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
格式化item
|
||||
"""
|
||||
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
|
||||
"""通过统一 Emby 系转换契约生成极影视条目。"""
|
||||
return format_emby_family_item(item, server="zspace")
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
|
||||
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
|
||||
> 相关文档:`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)
|
||||
|
||||
@@ -299,13 +299,24 @@
|
||||
`app.adapters.external.server -> app.runtime.config/tasks` 两条允许边,模块保持 `806`、内部边为 `6543`,
|
||||
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 当前均通过。
|
||||
- 依赖图当前为 `806` 个 Python 模块、`6543` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
|
||||
- 依赖图当前为 `806` 个 Python 模块、`6545` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
|
||||
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
|
||||
|
||||
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
|
||||
|
||||
+4
-2
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6543,
|
||||
"edge_sha256": "b9b6ba268382e7027f5557643defb09848c054cb68079345f2394d9b7faeb42c",
|
||||
"edge_count": 6545,
|
||||
"edge_sha256": "5a57ed0ff52fda67abf49f5ca956fea0adeb0ac595054e0f8f81374ad843a2bd",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -2595,6 +2595,8 @@
|
||||
"app.application.mediaserver -> app.application.service",
|
||||
"app.application.mediaserver -> app.domain",
|
||||
"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.media",
|
||||
"app.application.mediaserver -> app.schemas.mediaserver",
|
||||
|
||||
@@ -17,11 +17,7 @@ MIN_FINGERPRINT_SIZE = 1000
|
||||
# 参与告警的最小函数体节点数:过滤 setter/getter 等小函数。
|
||||
MIN_FUNCTION_SIZE = 40
|
||||
# 存量白名单:(模块名, 函数名) 集合,各 Phase 清理后同步移除。
|
||||
KNOWN_DUPLICATES = {
|
||||
# 服务实现类(非模块类)的条目信息格式化样板(待后续 Phase 清理)。
|
||||
("app.modules.jellyfin.jellyfin", "__format_item_info"),
|
||||
("app.modules.zspace.zspace", "__format_item_info"),
|
||||
}
|
||||
KNOWN_DUPLICATES: set[tuple[str, str]] = set()
|
||||
|
||||
|
||||
def _normalize(node: ast.AST) -> str:
|
||||
|
||||
@@ -14,8 +14,8 @@ from app.modules.jellyfin import JellyfinModule
|
||||
from app.modules.jellyfin.jellyfin import Jellyfin
|
||||
from app.modules.plex import PlexModule
|
||||
from app.modules.plex.plex import Plex
|
||||
from app.modules.trimemedia import TrimeMediaModule
|
||||
from app.modules.ugreen import UgreenModule
|
||||
from app.modules.trimemedia import TrimeMediaModule # pylint: disable=no-name-in-module
|
||||
from app.modules.ugreen import UgreenModule # pylint: disable=no-name-in-module
|
||||
from app.modules.zspace import ZSpaceModule
|
||||
from app.modules.zspace.zspace import ZSpace
|
||||
|
||||
@@ -97,25 +97,42 @@ def test_music_media_server_helper_normalizes_emby_music_fields():
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"formatter",
|
||||
("formatter", "server", "server_id"),
|
||||
[
|
||||
Emby._Emby__format_item_info,
|
||||
Jellyfin._Jellyfin__format_item_info,
|
||||
ZSpace._ZSpace__format_item_info,
|
||||
(Emby._Emby__format_item_info, "emby", "server-1"),
|
||||
(Jellyfin._Jellyfin__format_item_info, "jellyfin", None),
|
||||
(ZSpace._ZSpace__format_item_info, "zspace", None),
|
||||
],
|
||||
)
|
||||
def test_emby_family_clients_preserve_music_match_fields(formatter):
|
||||
"""Emby 系客户端格式化音乐结果时必须保留艺术家、专辑和曲目数。"""
|
||||
def test_emby_family_clients_preserve_item_contract(formatter, server, server_id):
|
||||
"""Emby 系客户端复用统一转换时必须保留服务身份及音乐匹配字段。"""
|
||||
item = formatter({
|
||||
"Id": "album-1",
|
||||
"ServerId": "server-1",
|
||||
"ParentId": "library-1",
|
||||
"Type": "MusicAlbum",
|
||||
"Name": "叶惠美",
|
||||
"AlbumArtists": [{"Name": "周杰伦"}],
|
||||
"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.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 == {
|
||||
"artist": "周杰伦",
|
||||
"artists": ["周杰伦"],
|
||||
|
||||
Reference in New Issue
Block a user