From bb63b40213d6bff2cd64daecfa971f05b686c50a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 15 Aug 2026 10:20:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(transfer):=20=E4=BF=AE=E5=A4=8D=E9=9F=B3?= =?UTF-8?q?=E4=B9=90=E6=95=B4=E7=90=86=E5=9B=A0=E7=BC=BA=E5=B0=91=20tmdb?= =?UTF-8?q?=5Fid=20=E5=AD=97=E6=AE=B5=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MusicInfo 不含 tmdb_id 等 60 个影视专用字段,整理链访问 task.mediainfo.tmdb_id 直接 AttributeError,导致无分类音乐自动 整理全部失败。 源头修复:为 domain 与 schemas 两层 MusicInfo 增加 __getattr__ 兜底,缺失字段返回 None,避免下游逐点安全访问;dunder 方法 除外以保持 copy/pickle 钩子探测正常。显式声明 recognize_cache_hit 字段保留 getattr(..., False) 默认值语义。 --- app/chain/transfer.py | 1 + app/domain/context.py | 15 +++++++++++++++ app/schemas/music.py | 11 +++++++++++ 3 files changed, 27 insertions(+) diff --git a/app/chain/transfer.py b/app/chain/transfer.py index c934b739a..dfa761468 100755 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -2509,6 +2509,7 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton): task.target_storage = task.target_directory.library_storage if self._requires_automatic_category(task) and not task.mediainfo.category: + # MusicInfo 无 tmdb_id 字段,但模型 __getattr__ 已兜底返回 None if task.mediainfo.tmdb_id: error_message = "TMDB 信息未匹配到媒体分类,无法按媒体类别整理" else: diff --git a/app/domain/context.py b/app/domain/context.py index ebf51474e..abec1d13f 100644 --- a/app/domain/context.py +++ b/app/domain/context.py @@ -175,11 +175,26 @@ class MusicInfo: detail_link: str | None = None listen_count: int | None = None raw_data: dict[str, Any] = field(default_factory=dict) + # 内部标记:是否命中本地识别缓存,不参与序列化;与 MediaInfo 保持一致, + # 显式声明以保留 getattr(..., False) 的默认值语义(__getattr__ 兜底会覆盖它) + recognize_cache_hit = False def __post_init__(self) -> None: """将构造参数中的媒体身份规范化为统一成对字段。""" self.media_source, self.media_id = resolve_media_identity(media=self) + def __getattr__(self, name: str) -> None: + """影视专用字段兜底返回 None:音乐模型不存在这些字段,避免下游逐点安全访问。 + + 兼容影视媒体共享的通用读写路径(通知、整理、历史等),任何缺失字段按 + 空值处理;真实字段仍由类定义接管,不受影响。dunder 特殊方法除外—— + copy/pickle 等机制依赖 hasattr 探测 __setstate__ 等钩子,兜底返回 None + 会导致探测误判而调用失败。注意这会掩盖属性名拼写错误,属于权衡取舍。 + """ + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + return None + @property def artist(self) -> str: """返回兼容现有展示组件的艺术家文本。""" diff --git a/app/schemas/music.py b/app/schemas/music.py index 3e3de4c48..cb43df5d8 100644 --- a/app/schemas/music.py +++ b/app/schemas/music.py @@ -83,6 +83,17 @@ class MusicInfo(OptionalMediaIdentityMixin, BaseModel): overview: Optional[str] = None vote_average: float = 0.0 + def __getattr__(self, name: str) -> None: + """影视专用字段兜底返回 None:音乐模型不存在这些字段,避免下游逐点安全访问。 + + 与 domain 层的 MusicInfo 保持一致:通知、整理等共享影视字段的读写路径 + 直接访问缺失属性时按空值处理,而不是抛 AttributeError。dunder 特殊方法 + 除外,避免 copy/pickle 等机制对钩子的 hasattr 探测误判。 + """ + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + return None + class MusicRelease(BaseModel): """音乐专辑下的单个发行版本。"""