mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
refactor: extract cache network and compatibility entrypoints
This commit is contained in:
@@ -72,6 +72,7 @@ from app.foundation.crypto import HashUtils
|
|||||||
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
|
from app.adapters.network.http import RequestUtils, AsyncRequestUtils
|
||||||
from app.adapters.system import rust as rust_accel
|
from app.adapters.system import rust as rust_accel
|
||||||
from app.application.security.url import SecurityUtils
|
from app.application.security.url import SecurityUtils
|
||||||
|
from app.application.network import NetworkTestService
|
||||||
from app.foundation.url import UrlUtils
|
from app.foundation.url import UrlUtils
|
||||||
from version import APP_VERSION
|
from version import APP_VERSION
|
||||||
|
|
||||||
@@ -1411,78 +1412,19 @@ async def nettest(
|
|||||||
target = _get_nettest_rule(url=url, target_id=target_id)
|
target = _get_nettest_rule(url=url, target_id=target_id)
|
||||||
if not target:
|
if not target:
|
||||||
return _SchemaResponse(success=False, message="测试目标不存在")
|
return _SchemaResponse(success=False, message="测试目标不存在")
|
||||||
# 记录开始的毫秒数
|
|
||||||
start_time = datetime.now()
|
|
||||||
url = target["url"]
|
url = target["url"]
|
||||||
invalid_message = _validate_nettest_url(url)
|
invalid_message = _validate_nettest_url(url)
|
||||||
if invalid_message:
|
if invalid_message:
|
||||||
logger.warning(f"拦截不安全的网络测试地址: {url}")
|
logger.warning(f"拦截不安全的网络测试地址: {url}")
|
||||||
return _SchemaResponse(success=False, message=invalid_message)
|
return _SchemaResponse(success=False, message=invalid_message)
|
||||||
if include:
|
success, message, data = await NetworkTestService(
|
||||||
logger.debug("nettest include 参数已忽略,改为服务端固定校验")
|
request_utils_cls=AsyncRequestUtils,
|
||||||
|
settings_getter=get_runtime_settings,
|
||||||
request_utils = AsyncRequestUtils(
|
logger=logger,
|
||||||
proxies=get_runtime_settings().get("PROXY") if target.get("proxy") else None,
|
redirect_checker=_is_allowed_nettest_redirect,
|
||||||
headers=target.get("headers"),
|
close_response=_close_nettest_response,
|
||||||
timeout=10,
|
).execute(target, include=include)
|
||||||
ua=get_runtime_settings().get("NORMAL_USER_AGENT"),
|
return _SchemaResponse(success=success, message=message, data=data)
|
||||||
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:
|
|
||||||
break
|
|
||||||
if result.status_code not in _NETTEST_REDIRECT_STATUS_CODES:
|
|
||||||
break
|
|
||||||
location = result.headers.get("location")
|
|
||||||
if not location:
|
|
||||||
break
|
|
||||||
next_url = urljoin(current_url, location)
|
|
||||||
if not _is_allowed_nettest_redirect(next_url, target):
|
|
||||||
await _close_nettest_response(result)
|
|
||||||
logger.warning(f"拦截网络测试重定向: {current_url} -> {next_url}")
|
|
||||||
return _SchemaResponse(success=False, message="测试目标发生了未授权跳转")
|
|
||||||
await _close_nettest_response(result)
|
|
||||||
current_url = next_url
|
|
||||||
redirect_count += 1
|
|
||||||
if redirect_count > 3:
|
|
||||||
return _SchemaResponse(success=False, message="测试目标重定向次数过多")
|
|
||||||
# 计时结束的毫秒数
|
|
||||||
end_time = datetime.now()
|
|
||||||
time = round((end_time - start_time).total_seconds() * 1000)
|
|
||||||
# 计算相关秒数
|
|
||||||
if result is None:
|
|
||||||
return _SchemaResponse(
|
|
||||||
success=False,
|
|
||||||
message=f"{target.get('proxy_name') or target.get('name')}无法连接",
|
|
||||||
data={"time": time},
|
|
||||||
)
|
|
||||||
elif result.status_code == 200:
|
|
||||||
expected_text = target.get("expected_text")
|
|
||||||
if expected_text and expected_text.lower() not in (result.text or "").lower():
|
|
||||||
return _SchemaResponse(
|
|
||||||
success=False,
|
|
||||||
message=target.get("invalid_message") or "无效响应",
|
|
||||||
data={"time": time},
|
|
||||||
)
|
|
||||||
return _SchemaResponse(success=True, data={"time": time})
|
|
||||||
else:
|
|
||||||
if target.get("proxy_name"):
|
|
||||||
# 加速代理失败
|
|
||||||
message = f"{target['proxy_name']}已失效,错误码:{result.status_code}"
|
|
||||||
else:
|
|
||||||
message = f"错误码:{result.status_code}"
|
|
||||||
if "github" in url:
|
|
||||||
# 非加速代理访问github
|
|
||||||
if result.status_code == 401:
|
|
||||||
message = "Github Token已失效,请检查配置"
|
|
||||||
elif result.status_code in {403, 429}:
|
|
||||||
message = "触发限流,请配置Github Token"
|
|
||||||
return _SchemaResponse(success=False, message=message, data={"time": time})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
+11
-129
@@ -9,22 +9,17 @@ from app.api.response import ResponseAPIRouter
|
|||||||
from app.chain.media import MediaChain
|
from app.chain.media import MediaChain
|
||||||
from app.chain.torrents import TorrentsChain
|
from app.chain.torrents import TorrentsChain
|
||||||
from app.application.configuration import get_api_runtime_config_snapshot
|
from app.application.configuration import get_api_runtime_config_snapshot
|
||||||
from app.domain.context import MediaInfo, MusicInfo
|
|
||||||
from app.domain.meta.metamusic import MetaMusic
|
|
||||||
from app.domain.metainfo import MetaInfo
|
|
||||||
from app.api.dependencies.auth import (
|
from app.api.dependencies.auth import (
|
||||||
get_current_active_superuser,
|
get_current_active_superuser,
|
||||||
get_current_active_superuser_async,
|
get_current_active_superuser_async,
|
||||||
)
|
)
|
||||||
from app.schemas.types import (
|
from app.schemas.types import (
|
||||||
MUSIC_ENTITY_RECORDING,
|
|
||||||
MediaSource,
|
MediaSource,
|
||||||
MediaType,
|
|
||||||
MusicTargetEntityType,
|
MusicTargetEntityType,
|
||||||
)
|
)
|
||||||
from app.foundation.crypto import HashUtils
|
from app.foundation.crypto import HashUtils
|
||||||
from app.domain.media import is_music_media_source, normalize_music_type
|
|
||||||
from app.schemas.media import resolve_media_identity
|
from app.schemas.media import resolve_media_identity
|
||||||
|
from app.application.torrent_cache import TorrentCacheRecognitionService
|
||||||
|
|
||||||
router = ResponseAPIRouter()
|
router = ResponseAPIRouter()
|
||||||
|
|
||||||
@@ -209,132 +204,19 @@ async def reidentify_cache(
|
|||||||
:param _: 当前用户,必须是超级用户
|
:param _: 当前用户,必须是超级用户
|
||||||
"""
|
"""
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
media_chain = MediaChain()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取当前缓存
|
service = TorrentCacheRecognitionService(TorrentsChain(), MediaChain())
|
||||||
cache_data = await torrents_chain.async_get_torrents()
|
success, message, data = await service.execute(
|
||||||
|
domain=domain,
|
||||||
if domain not in cache_data:
|
torrent_hash=torrent_hash,
|
||||||
return _SchemaResponse(success=False, message=f"站点 {domain} 缓存不存在")
|
media_source=media_source,
|
||||||
|
media_id=media_id,
|
||||||
# 查找指定种子
|
music_type=music_type,
|
||||||
target_context = None
|
|
||||||
for context in cache_data[domain]:
|
|
||||||
if (
|
|
||||||
HashUtils.md5(
|
|
||||||
f"{context.torrent_info.title}{context.torrent_info.description}"
|
|
||||||
)
|
|
||||||
== torrent_hash
|
|
||||||
):
|
|
||||||
target_context = context
|
|
||||||
break
|
|
||||||
|
|
||||||
if not target_context:
|
|
||||||
return _SchemaResponse(success=False, message="未找到指定的种子")
|
|
||||||
|
|
||||||
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 _SchemaResponse(
|
|
||||||
success=False,
|
|
||||||
message="音乐实体类型无效,仅支持 recording 或 album",
|
|
||||||
)
|
|
||||||
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 _SchemaResponse(
|
|
||||||
success=False,
|
|
||||||
message="音乐重新识别只能使用音乐元数据源",
|
|
||||||
)
|
|
||||||
if is_music and not normalized_music_type:
|
|
||||||
normalized_music_type = existing_music_type or MUSIC_ENTITY_RECORDING
|
|
||||||
|
|
||||||
# 重识别沿用原媒体域;音乐标题必须使用 MetaMusic,避免误入影视模块。
|
|
||||||
if is_music:
|
|
||||||
meta = (
|
|
||||||
target_context.meta_info
|
|
||||||
if isinstance(target_context.meta_info, MetaMusic)
|
|
||||||
else MetaMusic.parse_query(target_context.torrent_info.title)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
meta = 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 _SchemaResponse(
|
|
||||||
success=False,
|
|
||||||
message="媒体来源和媒体 ID 必须同时提供",
|
|
||||||
)
|
|
||||||
if has_explicit_id:
|
|
||||||
# 手动指定媒体身份时执行精确识别。
|
|
||||||
mediainfo = await 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:
|
|
||||||
# 未指定 ID 时按标题识别,请求级来源仍用于约束本次识别。
|
|
||||||
mediainfo = await 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 = torrents_chain.split_cache_contexts(cache_data)
|
|
||||||
video_file, music_file = torrents_chain.cache_files()
|
|
||||||
await torrents_chain.async_save_cache(video_cache, video_file)
|
|
||||||
await torrents_chain.async_save_cache(music_cache, music_file)
|
|
||||||
|
|
||||||
return _SchemaResponse(
|
return _SchemaResponse(
|
||||||
success=True,
|
success=success,
|
||||||
message="重新识别完成",
|
message=message,
|
||||||
data={
|
data=data,
|
||||||
"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),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _SchemaResponse(success=False, message=f"重新识别失败:{str(e)}")
|
return _SchemaResponse(success=False, message=f"重新识别失败:{str(e)}")
|
||||||
|
|||||||
@@ -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,
|
def parse(self, url, proxy: bool = False,
|
||||||
timeout: Optional[int] = 15, headers: dict = None, ua: str = None) -> Union[List[dict], None, bool]:
|
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中的种子信息
|
解析RSS订阅URL,获取RSS中的种子信息
|
||||||
:param url: RSS地址
|
:param url: RSS地址
|
||||||
|
|||||||
@@ -114,6 +114,23 @@ class CookieHelper:
|
|||||||
two_step_code: Optional[str] = None,
|
two_step_code: Optional[str] = None,
|
||||||
proxies: Optional[dict] = None,
|
proxies: Optional[dict] = None,
|
||||||
timeout: int = None) -> Tuple[Optional[str], Optional[str], str]:
|
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
|
获取站点cookie和ua
|
||||||
:param url: 站点地址
|
: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),
|
||||||
|
}
|
||||||
+2
-6
@@ -4,17 +4,13 @@
|
|||||||
"app/api/endpoints/media.py:scrape": 105,
|
"app/api/endpoints/media.py:scrape": 105,
|
||||||
"app/api/endpoints/openai.py:chat_completions": 95,
|
"app/api/endpoints/openai.py:chat_completions": 95,
|
||||||
"app/api/endpoints/openai.py:responses": 93,
|
"app/api/endpoints/openai.py:responses": 93,
|
||||||
"app/api/endpoints/system.py:get_logging": 111,
|
"app/api/endpoints/system.py:get_logging": 111
|
||||||
"app/api/endpoints/system.py:nettest": 87,
|
|
||||||
"app/api/endpoints/torrent.py:reidentify_cache": 147
|
|
||||||
},
|
},
|
||||||
"application_public": {
|
"application_public": {
|
||||||
"app/application/messaging/site.py:SiteInteractionHandler.handle_text_interaction": 227,
|
"app/application/messaging/site.py:SiteInteractionHandler.handle_text_interaction": 227,
|
||||||
"app/application/messaging/skill.py:SkillInteractionHandler.handle_callback_interaction": 158,
|
"app/application/messaging/skill.py:SkillInteractionHandler.handle_callback_interaction": 158,
|
||||||
"app/application/messaging/skill.py:SkillInteractionHandler.handle_text_interaction": 296,
|
"app/application/messaging/skill.py:SkillInteractionHandler.handle_text_interaction": 296,
|
||||||
"app/application/messaging/subscribe.py:SubscribeInteractionHandler.handle_text_interaction": 205,
|
"app/application/messaging/subscribe.py:SubscribeInteractionHandler.handle_text_interaction": 205
|
||||||
"app/application/rss.py:RssHelper.parse": 206,
|
|
||||||
"app/application/security/cookie.py:CookieHelper.get_site_cookie_ua": 221
|
|
||||||
},
|
},
|
||||||
"chain_public": {}
|
"chain_public": {}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-9
@@ -13,8 +13,8 @@
|
|||||||
"runtime_to_db": [],
|
"runtime_to_db": [],
|
||||||
"workflow_to_db": []
|
"workflow_to_db": []
|
||||||
},
|
},
|
||||||
"edge_count": 6398,
|
"edge_count": 6404,
|
||||||
"edge_sha256": "a830340a7981510b25b33e65c1a2d11a190028bfa616676fe935bc983a8a33cd",
|
"edge_sha256": "08caab762b3882534d0cdf43ea4e0fcaf28c580f5e04537346f742b212854123",
|
||||||
"edges": [
|
"edges": [
|
||||||
"app -> app.runtime",
|
"app -> app.runtime",
|
||||||
"app -> app.runtime.compat",
|
"app -> app.runtime.compat",
|
||||||
@@ -2226,6 +2226,7 @@
|
|||||||
"app.api.endpoints.system -> app.application.messaging",
|
"app.api.endpoints.system -> app.application.messaging",
|
||||||
"app.api.endpoints.system -> app.application.messaging.message",
|
"app.api.endpoints.system -> app.application.messaging.message",
|
||||||
"app.api.endpoints.system -> app.application.module",
|
"app.api.endpoints.system -> app.application.module",
|
||||||
|
"app.api.endpoints.system -> app.application.network",
|
||||||
"app.api.endpoints.system -> app.application.rules",
|
"app.api.endpoints.system -> app.application.rules",
|
||||||
"app.api.endpoints.system -> app.application.scheduling",
|
"app.api.endpoints.system -> app.application.scheduling",
|
||||||
"app.api.endpoints.system -> app.application.security",
|
"app.api.endpoints.system -> app.application.security",
|
||||||
@@ -2281,15 +2282,10 @@
|
|||||||
"app.api.endpoints.torrent -> app.api.response",
|
"app.api.endpoints.torrent -> app.api.response",
|
||||||
"app.api.endpoints.torrent -> app.application",
|
"app.api.endpoints.torrent -> app.application",
|
||||||
"app.api.endpoints.torrent -> app.application.configuration",
|
"app.api.endpoints.torrent -> app.application.configuration",
|
||||||
|
"app.api.endpoints.torrent -> app.application.torrent_cache",
|
||||||
"app.api.endpoints.torrent -> app.chain",
|
"app.api.endpoints.torrent -> app.chain",
|
||||||
"app.api.endpoints.torrent -> app.chain.media",
|
"app.api.endpoints.torrent -> app.chain.media",
|
||||||
"app.api.endpoints.torrent -> app.chain.torrents",
|
"app.api.endpoints.torrent -> app.chain.torrents",
|
||||||
"app.api.endpoints.torrent -> app.domain",
|
|
||||||
"app.api.endpoints.torrent -> app.domain.context",
|
|
||||||
"app.api.endpoints.torrent -> app.domain.media",
|
|
||||||
"app.api.endpoints.torrent -> app.domain.meta",
|
|
||||||
"app.api.endpoints.torrent -> app.domain.meta.metamusic",
|
|
||||||
"app.api.endpoints.torrent -> app.domain.metainfo",
|
|
||||||
"app.api.endpoints.torrent -> app.foundation",
|
"app.api.endpoints.torrent -> app.foundation",
|
||||||
"app.api.endpoints.torrent -> app.foundation.crypto",
|
"app.api.endpoints.torrent -> app.foundation.crypto",
|
||||||
"app.api.endpoints.torrent -> app.schemas",
|
"app.api.endpoints.torrent -> app.schemas",
|
||||||
@@ -2793,6 +2789,16 @@
|
|||||||
"app.application.torrent -> app.schemas",
|
"app.application.torrent -> app.schemas",
|
||||||
"app.application.torrent -> app.schemas.media",
|
"app.application.torrent -> app.schemas.media",
|
||||||
"app.application.torrent -> app.schemas.types",
|
"app.application.torrent -> app.schemas.types",
|
||||||
|
"app.application.torrent_cache -> app.domain",
|
||||||
|
"app.application.torrent_cache -> app.domain.context",
|
||||||
|
"app.application.torrent_cache -> app.domain.media",
|
||||||
|
"app.application.torrent_cache -> app.domain.meta",
|
||||||
|
"app.application.torrent_cache -> app.domain.meta.metamusic",
|
||||||
|
"app.application.torrent_cache -> app.domain.metainfo",
|
||||||
|
"app.application.torrent_cache -> app.foundation",
|
||||||
|
"app.application.torrent_cache -> app.foundation.crypto",
|
||||||
|
"app.application.torrent_cache -> app.schemas",
|
||||||
|
"app.application.torrent_cache -> app.schemas.types",
|
||||||
"app.application.transfer -> app.adapters",
|
"app.application.transfer -> app.adapters",
|
||||||
"app.application.transfer -> app.adapters.system",
|
"app.application.transfer -> app.adapters.system",
|
||||||
"app.application.transfer -> app.adapters.system.host",
|
"app.application.transfer -> app.adapters.system.host",
|
||||||
@@ -6415,7 +6421,7 @@
|
|||||||
"app.workflow.actions.transfer_file -> app.workflow",
|
"app.workflow.actions.transfer_file -> app.workflow",
|
||||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||||
],
|
],
|
||||||
"module_count": 792,
|
"module_count": 794,
|
||||||
"modules": [
|
"modules": [
|
||||||
"app",
|
"app",
|
||||||
"app.adapters",
|
"app.adapters",
|
||||||
@@ -6698,6 +6704,7 @@
|
|||||||
"app.application.module",
|
"app.application.module",
|
||||||
"app.application.music",
|
"app.application.music",
|
||||||
"app.application.music.catalog",
|
"app.application.music.catalog",
|
||||||
|
"app.application.network",
|
||||||
"app.application.notification",
|
"app.application.notification",
|
||||||
"app.application.outbox",
|
"app.application.outbox",
|
||||||
"app.application.plugin",
|
"app.application.plugin",
|
||||||
@@ -6744,6 +6751,7 @@
|
|||||||
"app.application.subscription.search",
|
"app.application.subscription.search",
|
||||||
"app.application.subscription.write",
|
"app.application.subscription.write",
|
||||||
"app.application.torrent",
|
"app.application.torrent",
|
||||||
|
"app.application.torrent_cache",
|
||||||
"app.application.transfer",
|
"app.application.transfer",
|
||||||
"app.application.workflow",
|
"app.application.workflow",
|
||||||
"app.chain",
|
"app.chain",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ _STUB_MODULES = dict([
|
|||||||
_stub("app.agent.llm", LLMHelper=_Dummy, LLMTestError=_DummyError, LLMTestTimeout=_DummyError),
|
_stub("app.agent.llm", LLMHelper=_Dummy, LLMTestError=_DummyError, LLMTestTimeout=_DummyError),
|
||||||
_stub("app.application.mediaserver", MediaServerHelper=_Dummy),
|
_stub("app.application.mediaserver", MediaServerHelper=_Dummy),
|
||||||
_stub("app.application.messaging.message", MessageHelper=_Dummy),
|
_stub("app.application.messaging.message", MessageHelper=_Dummy),
|
||||||
_stub("app.runtime.progress", ProgressHelper=_Dummy),
|
_stub("app.runtime.progress", ProgressHelper=_Dummy, AsyncProgressHelper=_Dummy),
|
||||||
_stub("app.application.rules", RuleHelper=_Dummy),
|
_stub("app.application.rules", RuleHelper=_Dummy),
|
||||||
_stub("app.adapters.external.server", MoviePilotServerHelper=_Dummy),
|
_stub("app.adapters.external.server", MoviePilotServerHelper=_Dummy),
|
||||||
_stub("app.runtime.state", SystemHelper=_Dummy),
|
_stub("app.runtime.state", SystemHelper=_Dummy),
|
||||||
|
|||||||
Reference in New Issue
Block a user