mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
refactor: extract cache network and compatibility entrypoints
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""受控外部网络探测应用服务。"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Optional
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
|
||||
class NetworkTestService:
|
||||
"""执行服务端预定义目标的 HTTPS 连通性测试。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request_utils_cls: Callable[..., Any],
|
||||
settings_getter: Callable[[], Any],
|
||||
logger: Any,
|
||||
redirect_checker: Callable[[str, dict[str, Any]], bool],
|
||||
close_response: Callable[[Any], Any],
|
||||
):
|
||||
"""注入网络客户端、配置和安全边界,方便隔离测试。"""
|
||||
self.request_utils_cls = request_utils_cls
|
||||
self.settings_getter = settings_getter
|
||||
self.logger = logger
|
||||
self.redirect_checker = redirect_checker
|
||||
self.close_response = close_response
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
target: dict[str, Any],
|
||||
include: Optional[str] = None,
|
||||
) -> tuple[bool, Optional[str], Optional[dict[str, int]]]:
|
||||
"""请求目标并处理受控重定向,返回旧端点使用的结果三元组。"""
|
||||
start_time = datetime.now()
|
||||
url = target["url"]
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme.lower() != "https" or not parsed.netloc or parsed.username or parsed.password:
|
||||
return False, "测试地址无效", {"time": 0}
|
||||
if include:
|
||||
self.logger.debug("nettest include 参数已忽略,改为服务端固定校验")
|
||||
|
||||
settings = self.settings_getter()
|
||||
request_utils = self.request_utils_cls(
|
||||
proxies=settings.get("PROXY") if target.get("proxy") else None,
|
||||
headers=target.get("headers"),
|
||||
timeout=10,
|
||||
ua=settings.get("NORMAL_USER_AGENT"),
|
||||
verify=True,
|
||||
follow_redirects=False,
|
||||
)
|
||||
result = None
|
||||
current_url = url
|
||||
redirect_count = 0
|
||||
while redirect_count <= 3:
|
||||
result = await request_utils.get_res(current_url, allow_redirects=False)
|
||||
if result is None or result.status_code not in {301, 302, 303, 307, 308}:
|
||||
break
|
||||
location = result.headers.get("location")
|
||||
if not location:
|
||||
break
|
||||
next_url = urljoin(current_url, location)
|
||||
if not self.redirect_checker(next_url, target):
|
||||
await self.close_response(result)
|
||||
self.logger.warning(f"拦截网络测试重定向: {current_url} -> {next_url}")
|
||||
return False, "测试目标发生了未授权跳转", None
|
||||
await self.close_response(result)
|
||||
current_url = next_url
|
||||
redirect_count += 1
|
||||
|
||||
elapsed = round((datetime.now() - start_time).total_seconds() * 1000)
|
||||
timing = {"time": elapsed}
|
||||
if redirect_count > 3:
|
||||
return False, "测试目标重定向次数过多", None
|
||||
if result is None:
|
||||
return False, f"{target.get('proxy_name') or target.get('name')}无法连接", timing
|
||||
if result.status_code == 200:
|
||||
expected_text = target.get("expected_text")
|
||||
if expected_text and expected_text.lower() not in (result.text or "").lower():
|
||||
return False, target.get("invalid_message") or "无效响应", timing
|
||||
return True, None, timing
|
||||
if target.get("proxy_name"):
|
||||
message = f"{target['proxy_name']}已失效,错误码:{result.status_code}"
|
||||
else:
|
||||
message = f"错误码:{result.status_code}"
|
||||
if "github" in url:
|
||||
if result.status_code == 401:
|
||||
message = "Github Token已失效,请检查配置"
|
||||
elif result.status_code in {403, 429}:
|
||||
message = "触发限流,请配置Github Token"
|
||||
return False, message, timing
|
||||
@@ -264,6 +264,12 @@ class RssHelper:
|
||||
|
||||
def parse(self, url, proxy: bool = False,
|
||||
timeout: Optional[int] = 15, headers: dict = None, ua: str = None) -> Union[List[dict], None, bool]:
|
||||
"""解析 RSS 地址并保留插件兼容的返回约定。"""
|
||||
return self._parse_impl(url, proxy=proxy, timeout=timeout, headers=headers, ua=ua)
|
||||
|
||||
def _parse_impl(self, url, proxy: bool = False,
|
||||
timeout: Optional[int] = 15, headers: dict = None,
|
||||
ua: str = None) -> Union[List[dict], None, bool]:
|
||||
"""
|
||||
解析RSS订阅URL,获取RSS中的种子信息
|
||||
:param url: RSS地址
|
||||
|
||||
@@ -114,6 +114,23 @@ class CookieHelper:
|
||||
two_step_code: Optional[str] = None,
|
||||
proxies: Optional[dict] = None,
|
||||
timeout: int = None) -> Tuple[Optional[str], Optional[str], str]:
|
||||
"""获取站点 Cookie、User-Agent 和兼容错误消息。"""
|
||||
return self._get_site_cookie_ua_impl(
|
||||
url=url,
|
||||
username=username,
|
||||
password=password,
|
||||
two_step_code=two_step_code,
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
def _get_site_cookie_ua_impl(self,
|
||||
url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
two_step_code: Optional[str] = None,
|
||||
proxies: Optional[dict] = None,
|
||||
timeout: int = None) -> Tuple[Optional[str], Optional[str], str]:
|
||||
"""
|
||||
获取站点cookie和ua
|
||||
:param url: 站点地址
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""种子缓存相关的应用用例。"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.domain.context import MediaInfo, MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.foundation.crypto import HashUtils
|
||||
from app.domain.media import is_music_media_source, normalize_music_type
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType, MusicTargetEntityType
|
||||
|
||||
|
||||
class TorrentCacheRecognitionService:
|
||||
"""执行种子缓存条目的媒体重新识别用例。"""
|
||||
|
||||
def __init__(self, torrents_chain: Any, media_chain: Any):
|
||||
"""初始化缓存和媒体识别依赖。"""
|
||||
self.torrents_chain = torrents_chain
|
||||
self.media_chain = media_chain
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
domain: str,
|
||||
torrent_hash: str,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
music_type: Optional[MusicTargetEntityType] = None,
|
||||
) -> tuple[bool, str, Optional[dict]]:
|
||||
"""重新识别缓存条目并持久化影视、音乐分离后的缓存。
|
||||
|
||||
返回值保持端点原有的成功标识、用户消息和响应数据三元组,便于旧插件
|
||||
继续消费原始 HTTP 响应结构。
|
||||
"""
|
||||
cache_data = await self.torrents_chain.async_get_torrents()
|
||||
if domain not in cache_data:
|
||||
return False, f"站点 {domain} 缓存不存在", None
|
||||
|
||||
target_context = next(
|
||||
(
|
||||
context
|
||||
for context in cache_data[domain]
|
||||
if HashUtils.md5(
|
||||
f"{context.torrent_info.title}{context.torrent_info.description}"
|
||||
)
|
||||
== torrent_hash
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not target_context:
|
||||
return False, "未找到指定的种子", None
|
||||
|
||||
existing_music_type = normalize_music_type(
|
||||
getattr(target_context.media_info, "music_type", None), allow_artist=False
|
||||
)
|
||||
normalized_music_type = normalize_music_type(music_type, allow_artist=False)
|
||||
if music_type is not None and not normalized_music_type:
|
||||
return False, "音乐实体类型无效,仅支持 recording 或 album", None
|
||||
|
||||
is_music = (
|
||||
getattr(target_context.media_info, "type", None) == MediaType.MUSIC
|
||||
or isinstance(target_context.meta_info, MetaMusic)
|
||||
or target_context.torrent_info.category
|
||||
in (MediaType.MUSIC, MediaType.MUSIC.value, "music")
|
||||
or is_music_media_source(media_source)
|
||||
or normalized_music_type is not None
|
||||
)
|
||||
if is_music and media_source and not is_music_media_source(media_source):
|
||||
return False, "音乐重新识别只能使用音乐元数据源", None
|
||||
if is_music and not normalized_music_type:
|
||||
normalized_music_type = existing_music_type or MUSIC_ENTITY_RECORDING
|
||||
|
||||
meta = (
|
||||
target_context.meta_info
|
||||
if is_music and isinstance(target_context.meta_info, MetaMusic)
|
||||
else MetaMusic.parse_query(target_context.torrent_info.title)
|
||||
if is_music
|
||||
else MetaInfo(
|
||||
title=target_context.torrent_info.title,
|
||||
subtitle=target_context.torrent_info.description,
|
||||
)
|
||||
)
|
||||
has_explicit_id = media_source is not None or media_id is not None
|
||||
if has_explicit_id and (not media_source or not media_id):
|
||||
return False, "媒体来源和媒体 ID 必须同时提供", None
|
||||
if has_explicit_id:
|
||||
mediainfo = await self.media_chain.async_recognize_media(
|
||||
meta=meta,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
mtype=MediaType.MUSIC if is_music else None,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
else:
|
||||
mediainfo = await self.media_chain.async_recognize_by_meta(
|
||||
meta,
|
||||
media_source=media_source,
|
||||
mtype=MediaType.MUSIC if is_music else None,
|
||||
music_type=normalized_music_type,
|
||||
)
|
||||
|
||||
if not mediainfo:
|
||||
mediainfo = (
|
||||
MusicInfo(music_type=normalized_music_type or MUSIC_ENTITY_RECORDING)
|
||||
if is_music
|
||||
else MediaInfo()
|
||||
)
|
||||
else:
|
||||
mediainfo.clear()
|
||||
target_context.media_info = mediainfo
|
||||
|
||||
video_cache, music_cache = self.torrents_chain.split_cache_contexts(cache_data)
|
||||
video_file, music_file = self.torrents_chain.cache_files()
|
||||
await self.torrents_chain.async_save_cache(video_cache, video_file)
|
||||
await self.torrents_chain.async_save_cache(music_cache, music_file)
|
||||
return True, "重新识别完成", {
|
||||
"media_name": mediainfo.title if mediainfo else "",
|
||||
"media_year": mediainfo.year if mediainfo else "",
|
||||
"media_type": mediainfo.type.value if mediainfo and mediainfo.type else "",
|
||||
"media_source": getattr(mediainfo, "media_source", None),
|
||||
"media_id": getattr(mediainfo, "media_id", None),
|
||||
"music_type": getattr(mediainfo, "music_type", None),
|
||||
}
|
||||
Reference in New Issue
Block a user