mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
fix(types): preserve Plex SDK return contracts
This commit is contained in:
@@ -8,10 +8,13 @@ from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi.audio import Album, Artist, Track
|
||||
from plexapi.base import PlexPartialObject
|
||||
from plexapi.exceptions import NotFound
|
||||
from plexapi.library import Library
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.video import Episode, Movie, Season, Show
|
||||
from requests import RequestException
|
||||
|
||||
from app.adapters.network.http import RequestUtils
|
||||
@@ -103,7 +106,7 @@ class Plex:
|
||||
return False
|
||||
return True if not self._plex else False
|
||||
|
||||
def reconnect(self):
|
||||
def reconnect(self) -> None:
|
||||
"""
|
||||
重建 Plex SDK 客户端并刷新媒体库目录缓存;失败时清空 SDK 客户端。
|
||||
"""
|
||||
@@ -136,7 +139,11 @@ class Plex:
|
||||
logger.error(f"Authentication failed: {e}")
|
||||
return None
|
||||
|
||||
@cached(maxsize=32, ttl=86400)
|
||||
_image_cache: Callable[
|
||||
[Callable[..., Optional[List[str]]]], Callable[..., Optional[List[str]]]
|
||||
] = cached(maxsize=32, ttl=86400)
|
||||
|
||||
@_image_cache
|
||||
def __get_library_images(self, library_key: str, mtype: int) -> Optional[List[str]]:
|
||||
"""
|
||||
分页读取最近新增媒体,选取最多 4 张不同的库封面。
|
||||
@@ -150,7 +157,7 @@ class Plex:
|
||||
if not self._plex:
|
||||
return None
|
||||
# 返回结果
|
||||
poster_urls = {}
|
||||
poster_urls: dict[str, None] = {}
|
||||
# 页码计数
|
||||
container_start = 0
|
||||
# 需要的总条数/每页的条数
|
||||
@@ -370,7 +377,7 @@ class Plex:
|
||||
year: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
season: Optional[int] = None) -> Tuple[Optional[str], Optional[Dict[int, list]]]:
|
||||
season: Optional[int] = None) -> Tuple[Optional[str], Optional[Dict[int, List[int]]]]:
|
||||
"""
|
||||
查询电视剧及其各季已有的分集编号。
|
||||
|
||||
@@ -413,7 +420,7 @@ class Plex:
|
||||
):
|
||||
return None, {}
|
||||
episodes = videos.episodes()
|
||||
season_episodes = {}
|
||||
season_episodes: Dict[int, List[int]] = {}
|
||||
for episode in episodes:
|
||||
if season is not None and episode.seasonNumber != int(season):
|
||||
continue
|
||||
@@ -463,6 +470,8 @@ class Plex:
|
||||
:param year: 年份,可以为空,为空时不按年份过滤
|
||||
:return: Plex搜索返回的电视剧条目列表
|
||||
"""
|
||||
if self._plex is None:
|
||||
raise AttributeError("'NoneType' object has no attribute 'library'")
|
||||
# 兼容年份为空的场景
|
||||
kwargs = {"year": year} if year else {}
|
||||
# 根据标题和年份搜索候选,后续仍需核对规范媒体来源身份。
|
||||
@@ -491,7 +500,11 @@ class Plex:
|
||||
:param plex_url: 是否返回Plex的URL,默认为True(仅在配置了外网地址和Token时有效)
|
||||
:return: 图片对应在plex服务器或TMDB中的URL
|
||||
"""
|
||||
if not self._plex or depth > 2 or not item_id:
|
||||
if not self._plex:
|
||||
return None
|
||||
if depth is None:
|
||||
raise TypeError("'>' not supported between instances of 'NoneType' and 'int'")
|
||||
if depth > 2 or not item_id:
|
||||
return None
|
||||
try:
|
||||
image_url = None
|
||||
@@ -579,7 +592,7 @@ class Plex:
|
||||
return None
|
||||
return UrlUtils.combine_url(host=image_host, path=image_path, query=query)
|
||||
|
||||
def refresh_root_library(self) -> bool:
|
||||
def refresh_root_library(self) -> Union[bool, Library]:
|
||||
"""
|
||||
请求 Plex 刷新整个媒体库。
|
||||
|
||||
@@ -587,7 +600,8 @@ class Plex:
|
||||
"""
|
||||
if not self._plex:
|
||||
return False
|
||||
return self._plex.library.update()
|
||||
updated: Library = self._plex.library.update()
|
||||
return updated
|
||||
|
||||
def refresh_library_by_items(self, items: List[_SchemaRefreshMediaItem]) -> Optional[bool]:
|
||||
"""
|
||||
@@ -615,13 +629,13 @@ class Plex:
|
||||
return refreshed is not False
|
||||
else:
|
||||
# 否则一个一个刷新
|
||||
for path, lib_key in result_dict.items():
|
||||
logger.info(f"刷新媒体库:{lib_key} - {path}")
|
||||
self._plex.query(f'/library/sections/{lib_key}/refresh?path={quote_plus(Path(path).parent.as_posix())}')
|
||||
for refresh_path, lib_key in result_dict.items():
|
||||
logger.info(f"刷新媒体库:{lib_key} - {refresh_path}")
|
||||
self._plex.query(f'/library/sections/{lib_key}/refresh?path={quote_plus(Path(refresh_path).parent.as_posix())}')
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __find_librarie(path: Path, libraries: List[Any]) -> Tuple[str, Optional[Path]]:
|
||||
def __find_librarie(path: Optional[Path], libraries: List[Any]) -> Tuple[str, Optional[Path]]:
|
||||
"""
|
||||
按路径归属查找第一个匹配的媒体库。
|
||||
|
||||
@@ -670,14 +684,14 @@ class Plex:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __get_ids(guids: List[Any]) -> dict:
|
||||
def __get_ids(guids: List[Any]) -> dict[str, Union[str, int, None]]:
|
||||
"""
|
||||
从 Plex Guid 提取 IMDb、TMDB 和 TVDB 来源标识。
|
||||
|
||||
:param guids: 包含 id 的字典或 SDK Guid 对象列表
|
||||
:return: 含 imdb_id、tmdb_id、tvdb_id 的字典;缺失项为 None,TMDB 标识转换为整数,无法转换时忽略
|
||||
"""
|
||||
def parse_tmdb_id(value: str) -> tuple[bool, int]:
|
||||
def parse_tmdb_id(value: str) -> tuple[bool, Optional[int]]:
|
||||
"""
|
||||
尝试将TMDB ID字符串转换为整数。如果成功,返回(True, int),失败则返回(False, None)。
|
||||
"""
|
||||
@@ -692,7 +706,7 @@ class Plex:
|
||||
"tmdb://": "tmdb_id",
|
||||
"tvdb://": "tvdb_id"
|
||||
}
|
||||
ids = {varname: None for varname in guid_mapping.values()}
|
||||
ids: dict[str, Union[str, int, None]] = {varname: None for varname in guid_mapping.values()}
|
||||
for guid in guids:
|
||||
guid_id = guid['id'] if isinstance(guid, dict) else guid.id
|
||||
for prefix, varname in guid_mapping.items():
|
||||
@@ -709,7 +723,7 @@ class Plex:
|
||||
|
||||
return ids
|
||||
|
||||
def __fetch_item(self, item_id: Union[int, str]):
|
||||
def __fetch_item(self, item_id: Union[int, str]) -> Union[Movie, Show, Season, Episode, Artist, Album, Track]:
|
||||
"""
|
||||
根据给定的item_id获取媒体项。
|
||||
|
||||
@@ -721,9 +735,12 @@ class Plex:
|
||||
elif not item_id.startswith("/library/metadata/"):
|
||||
# 其他媒体服务器的 UUID 不能作为 Plex metadata key,直接按失效缓存回退。
|
||||
raise NotFound(f"invalid Plex metadata id: {item_id}")
|
||||
return self._plex.fetchItem(item_id)
|
||||
if self._plex is None:
|
||||
raise AttributeError("'NoneType' object has no attribute 'fetchItem'")
|
||||
item: Union[Movie, Show, Season, Episode, Artist, Album, Track] = self._plex.fetchItem(item_id)
|
||||
return item
|
||||
|
||||
def __build_media_server_item(self, item) -> Optional[_SchemaMediaServerItem]:
|
||||
def __build_media_server_item(self, item: Union[Movie, Show, Season, Episode, Artist, Album, Track, None]) -> Optional[_SchemaMediaServerItem]:
|
||||
"""
|
||||
按 Plex 条目实际具备的字段构造媒体信息,兼容艺人、专辑和曲目。
|
||||
|
||||
@@ -965,7 +982,7 @@ class Plex:
|
||||
|
||||
return eventItem
|
||||
|
||||
def get_plex(self):
|
||||
def get_plex(self) -> Optional[PlexServer]:
|
||||
"""
|
||||
提供当前 Plex SDK 客户端供扩展调用。
|
||||
|
||||
@@ -980,6 +997,8 @@ class Plex:
|
||||
:param item_id: Plex 条目标识,可以是数字标识或 /library/metadata/ 路径
|
||||
:return: 以外网播放地址或服务地址为根的条目链接;调用前须已经建立 SDK 连接
|
||||
"""
|
||||
if self._plex is None:
|
||||
raise AttributeError("'NoneType' object has no attribute 'machineIdentifier'")
|
||||
return f'{self._playhost or self._host}web/index.html#!/server/{self._plex.machineIdentifier}/details?key={item_id}&X-Plex-Token={self._token}'
|
||||
|
||||
def get_resume(self, num: Optional[int] = 12) -> Optional[List[_SchemaMediaServerPlayItem]]:
|
||||
@@ -1002,7 +1021,7 @@ class Plex:
|
||||
container_size=num,
|
||||
maxresults=num,
|
||||
params=params)
|
||||
ret_resume = []
|
||||
ret_resume: List[_SchemaMediaServerPlayItem] = []
|
||||
for item in items:
|
||||
item_type = MediaType.MOVIE.value if item.TYPE == "movie" else MediaType.TV.value
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
@@ -1046,9 +1065,11 @@ class Plex:
|
||||
"count": num,
|
||||
"excludeContinueWatching": 1
|
||||
}
|
||||
ret_resume = []
|
||||
ret_resume: List[_SchemaMediaServerPlayItem] = []
|
||||
sub_result = []
|
||||
offset = 0
|
||||
if num is None:
|
||||
raise TypeError("'>=' not supported between instances of 'int' and 'NoneType'")
|
||||
while True:
|
||||
if len(ret_resume) >= num:
|
||||
break
|
||||
@@ -1103,7 +1124,7 @@ class Plex:
|
||||
offset += num
|
||||
return ret_resume[:num]
|
||||
|
||||
def get_data(self, endpoint: str, **kwargs) -> Optional[Any]:
|
||||
def get_data(self, endpoint: str, **kwargs: object) -> Optional[Any]:
|
||||
"""
|
||||
使用复用的 HTTP 会话发送自定义 GET 请求。
|
||||
|
||||
@@ -1113,7 +1134,7 @@ class Plex:
|
||||
"""
|
||||
return self.__request(method="get", endpoint=endpoint, **kwargs)
|
||||
|
||||
def post_data(self, endpoint: str, **kwargs) -> Optional[Any]:
|
||||
def post_data(self, endpoint: str, **kwargs: object) -> Optional[Any]:
|
||||
"""
|
||||
使用复用的 HTTP 会话发送自定义 POST 请求。
|
||||
|
||||
@@ -1123,7 +1144,7 @@ class Plex:
|
||||
"""
|
||||
return self.__request(method="post", endpoint=endpoint, **kwargs)
|
||||
|
||||
def put_data(self, endpoint: str, **kwargs) -> Optional[Any]:
|
||||
def put_data(self, endpoint: str, **kwargs: object) -> Optional[Any]:
|
||||
"""
|
||||
使用复用的 HTTP 会话发送自定义 PUT 请求。
|
||||
|
||||
@@ -1133,7 +1154,7 @@ class Plex:
|
||||
"""
|
||||
return self.__request(method="put", endpoint=endpoint, **kwargs)
|
||||
|
||||
def __request(self, method: str, endpoint: str, **kwargs) -> Optional[Any]:
|
||||
def __request(self, method: str, endpoint: str, **kwargs: object) -> Optional[Any]:
|
||||
"""
|
||||
为自定义 Plex 请求补充令牌请求头,并记录请求异常。
|
||||
|
||||
@@ -1158,7 +1179,7 @@ class Plex:
|
||||
logger.error("连接Plex出错:" + str(e))
|
||||
return None
|
||||
|
||||
def __get_request_headers(self) -> dict:
|
||||
def __get_request_headers(self) -> dict[str, str]:
|
||||
"""
|
||||
获取请求头。
|
||||
"""
|
||||
@@ -1168,7 +1189,7 @@ class Plex:
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""
|
||||
关闭自定义 HTTP 请求会话;不主动关闭 Plex SDK 客户端。
|
||||
"""
|
||||
|
||||
@@ -108,7 +108,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
|
||||
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
|
||||
| Python 源码量 | 373,625 行 | 本次重新扫描物理行数,排除 `app/plugins/**`;74 个文件超过 1,000 行,12 个超过 2,000 行;源码尺寸仅作观察 |
|
||||
| 长方法 | 309 个超过 80 行 | 本次 AST 扫描排除 `app/plugins/**`;61 个超过 150 行,17 个超过 250 行;新增函数仍受 C901/嵌套深度门禁约束 |
|
||||
| 全量 mypy 历史债务 | 4,187 / 246 文件 | 本分支为 Pydantic 字段补充描述并保留默认值,减少 3 项 assignment 错误;本次 NFO 返回类型与契约对齐,减少 2 项历史类型错误;本轮在消息、站点、诊断、备份、用户配置、订阅、AcoustID、TMDB、仪表板和命令生命周期边界补齐类型,历史债务再减少 53 项;TMDB v3 API 显式导出并保留集合 parts 的列表合同,逐文件逐错误码无增长;此前工作流筛选、动态插件路由和 stdio 流补齐边界类型,历史债务减少 3 项;Bitpt 与 CookieCloud 补齐边界类型,历史债务减少 16 项;知行站点解析器补齐数值属性与基类合同,历史债务减少 18 项;通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项;本批清理 Servarr、认证、存储、订阅策略和搜索/规则/通知 Schema 的 18 项类型错误,并修复系统配置中的 Python 2 异常语法;本批再清理订阅元数据、订阅写入、交互宿主、整理路径、删除统计和工作流动作的 14 项类型错误,逐文件逐错误码无增长;本批补齐 OCR 请求参数、工具 Schema 摘要和宿主模块配置快照的 5 项边界类型,逐文件逐错误码无增长;本批为 Rousi 用户消息分页解析补齐返回类型与数据结构兜底,减少 2 项类型错误;本批优先处理 TMDB v3 电影与电视剧对象模型,补齐 98 个同步/异步方法注解并减少 122 项历史类型错误;随后补齐剧集、季度、人物、搜索和趋势对象的 142 个方法注解,再减少 142 项类型错误;绿联 API 响应边界补齐具体泛型与运行时字典收窄,减少 31 项类型错误;运行时限流器补齐状态方法、装饰器边界和时间队列类型,减少 31 项类型错误;QQ Bot API 响应边界补齐具体泛型与 JSON 形状校验,减少 17 项类型错误;rTorrent 客户端清理 182 项错误,调用端同步明确字典合同,累计再减少 190 项错误;豆瓣 API、中心服务接口及订阅消费端补齐返回边界和参数收窄,结合 Context 方法签名与缓存代理类型清理,累计再减少 455 项错误;共享 HTTP 客户端清理 110 项错误,TMDB API 清理 84 项错误,索引器爬虫清理 87 项错误,并补充 U115、AliPan、OpenList 对可空流响应的防守;U115 其余存储基类契约债务暂留待测试完备后处理;缓存同步/异步继承冲突保留为设计债务,未纳入严格门禁 |
|
||||
| 全量 mypy 历史债务 | 4,155 / 245 文件 | 本分支为 Pydantic 字段补充描述并保留默认值,减少 3 项 assignment 错误;本次 NFO 返回类型与契约对齐,减少 2 项历史类型错误;本轮在消息、站点、诊断、备份、用户配置、订阅、AcoustID、TMDB、仪表板和命令生命周期边界补齐类型,历史债务再减少 53 项;TMDB v3 API 显式导出并保留集合 parts 的列表合同,逐文件逐错误码无增长;此前工作流筛选、动态插件路由和 stdio 流补齐边界类型,历史债务减少 3 项;Bitpt 与 CookieCloud 补齐边界类型,历史债务减少 16 项;知行站点解析器补齐数值属性与基类合同,历史债务减少 18 项;通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项;本批清理 Servarr、认证、存储、订阅策略和搜索/规则/通知 Schema 的 18 项类型错误,并修复系统配置中的 Python 2 异常语法;本批再清理订阅元数据、订阅写入、交互宿主、整理路径、删除统计和工作流动作的 14 项类型错误,逐文件逐错误码无增长;本批补齐 OCR 请求参数、工具 Schema 摘要和宿主模块配置快照的 5 项边界类型,逐文件逐错误码无增长;本批为 Rousi 用户消息分页解析补齐返回类型与数据结构兜底,减少 2 项类型错误;本批优先处理 TMDB v3 电影与电视剧对象模型,补齐 98 个同步/异步方法注解并减少 122 项历史类型错误;随后补齐剧集、季度、人物、搜索和趋势对象的 142 个方法注解,再减少 142 项类型错误;绿联 API 响应边界补齐具体泛型与运行时字典收窄,减少 31 项类型错误;运行时限流器补齐状态方法、装饰器边界和时间队列类型,减少 31 项类型错误;QQ Bot API 响应边界补齐具体泛型与 JSON 形状校验,减少 17 项类型错误;rTorrent 客户端清理 182 项错误,调用端同步明确字典合同,累计再减少 190 项错误;豆瓣 API、中心服务接口及订阅消费端补齐返回边界和参数收窄,结合 Context 方法签名与缓存代理类型清理,累计再减少 455 项错误;共享 HTTP 客户端清理 110 项错误,TMDB API 清理 84 项错误,索引器爬虫清理 87 项错误,并补充 U115、AliPan、OpenList 对可空流响应的防守;U115 其余存储基类契约债务暂留待测试完备后处理;缓存同步/异步继承冲突保留为设计债务,未纳入严格门禁 |
|
||||
| Ruff 历史诊断 | 73 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率逐文件低水位 | 全 app(排除 app/plugins)当前聚合 77.68% | 存量文件比例不降;语句减少且未覆盖行数不增时允许比例变化,新文件至少 80% |
|
||||
|
||||
|
||||
1
mypy.ini
1
mypy.ini
@@ -7,6 +7,7 @@ follow_imports = skip
|
||||
show_error_codes = True
|
||||
pretty = True
|
||||
files =
|
||||
app/modules/plex/plex.py,
|
||||
app/modules/feishu/feishu.py,
|
||||
app/modules/emby/emby.py,
|
||||
app/modules/discord/discord.py,
|
||||
|
||||
13
tests/fixtures/architecture/mypy-baseline.json
vendored
13
tests/fixtures/architecture/mypy-baseline.json
vendored
@@ -1073,22 +1073,9 @@
|
||||
"arg-type": 7,
|
||||
"assignment": 16,
|
||||
"empty-body": 1,
|
||||
"no-untyped-call": 1,
|
||||
"no-untyped-def": 5,
|
||||
"type-arg": 1
|
||||
},
|
||||
"app/modules/plex/plex.py": {
|
||||
"arg-type": 2,
|
||||
"assignment": 2,
|
||||
"misc": 1,
|
||||
"no-any-return": 1,
|
||||
"no-untyped-def": 9,
|
||||
"operator": 5,
|
||||
"return-value": 1,
|
||||
"type-arg": 3,
|
||||
"union-attr": 4,
|
||||
"var-annotated": 3
|
||||
},
|
||||
"app/modules/qqbot/gateway.py": {
|
||||
"no-untyped-def": 3,
|
||||
"type-arg": 2
|
||||
|
||||
71
tests/test_plex_type_contracts.py
Normal file
71
tests/test_plex_type_contracts.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Plex SDK 返回值与类型收窄的异常边界回归。
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.plex.plex import Plex
|
||||
|
||||
|
||||
def test_root_refresh_preserves_sdk_return_identity():
|
||||
"""
|
||||
整库刷新原样返回 SDK 对象,不能转换为布尔值。
|
||||
"""
|
||||
client = Plex.__new__(Plex)
|
||||
result = object()
|
||||
client._plex = SimpleNamespace(library=SimpleNamespace(update=Mock(return_value=result)))
|
||||
assert client.refresh_root_library() is result
|
||||
client._plex = None
|
||||
assert client.refresh_root_library() is False
|
||||
|
||||
|
||||
def test_missing_refresh_path_keeps_root_refresh_success_semantics():
|
||||
"""
|
||||
目标路径缺失时继续回退根库,SDK 空返回值仍算请求成功。
|
||||
"""
|
||||
client = Plex.__new__(Plex)
|
||||
update = Mock(return_value=None)
|
||||
client._plex = SimpleNamespace(library=SimpleNamespace(update=update))
|
||||
client._libraries = []
|
||||
assert client.refresh_library_by_items([SimpleNamespace(target_path=None)]) is True
|
||||
update.assert_called_once_with()
|
||||
|
||||
|
||||
def test_optional_image_depth_preserves_short_circuit_and_error():
|
||||
"""
|
||||
未连接仍提前返回,连接后空递归深度仍在 SDK 查询前报错。
|
||||
"""
|
||||
client = Plex.__new__(Plex)
|
||||
client._plex = None
|
||||
assert client.get_remote_image_by_id("1", "Poster", depth=None) is None
|
||||
client._plex = Mock()
|
||||
with pytest.raises(TypeError):
|
||||
client.get_remote_image_by_id("1", "Poster", depth=None)
|
||||
client._plex.fetchItem.assert_not_called()
|
||||
|
||||
|
||||
def test_latest_none_count_keeps_library_query_before_error():
|
||||
"""
|
||||
空数量仍先读取库范围,只有库可用时才到比较数量的失败点。
|
||||
"""
|
||||
client = Plex.__new__(Plex)
|
||||
client._plex = Mock()
|
||||
client.get_librarys = Mock(return_value=None)
|
||||
assert client.get_latest(num=None) is None
|
||||
client.get_librarys.return_value = []
|
||||
with pytest.raises(TypeError):
|
||||
client.get_latest(num=None)
|
||||
client._plex.fetchItems.assert_not_called()
|
||||
|
||||
|
||||
def test_guid_parsing_retains_null_and_identifier_types():
|
||||
"""
|
||||
非法 TMDB 标识仍忽略,其他标识保持文本,合法 TMDB 转为整数。
|
||||
"""
|
||||
assert Plex._Plex__get_ids([{"id": "tmdb://bad"}, {"id": "imdb://tt1"}]) == {
|
||||
"tmdb_id": None, "imdb_id": "tt1", "tvdb_id": None,
|
||||
}
|
||||
assert Plex._Plex__get_ids([{"id": "tmdb://123"}])["tmdb_id"] == 123
|
||||
Reference in New Issue
Block a user