mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor(architecture): 修复模块依赖违规并强化架构守护
- 字幕编排上移 DownloadChain.download_site_subtitles,SubtitleModule 仅保留站点链接解析 - TransferChain.recommend_name 上移 TV episodes_info 获取,filemanager 模块不再导入 TmdbChain - endpoint 穿透修复:WXBizMsgCrypt3 迁至 adapters/external/wechat_crypt.py; music/tmdb 缓存管理、listenbrainz 常量、TMDbException、WechatClawBot 辅助统一经 chain 包装 - RuleParser 与 builtin_rules 合并为 application/filter_rules.py; fsproxy/fsworker 迁至 adapters/system/ - chain/__init__.py 删除 qbittorrentapi/transmission_rpc 导入,消除后端协议类型泄漏 - 架构守护测试新增三项检查:模块间隔离、入口层穿透、下载器 SDK 泄漏 - 文档同步:05-architecture.md 记录 DB/Oper 聚合例外与迁移文件位置,AGENTS.md 更新所有权表
This commit is contained in:
@@ -29,7 +29,10 @@ from app.domain.metainfo import MetaInfo
|
||||
from app.db.oper.downloadfailure import DownloadFailureOper
|
||||
from app.db.oper.downloadhistory import DownloadHistoryOper
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.directory import DirectoryHelper, validate_download_save_path
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.modules.indexer.spider.mtorrent import MTorrentSpider
|
||||
from app.runtime.thread import ThreadHelper
|
||||
from app.application.torrent import TorrentHelper
|
||||
from app.runtime.log import logger
|
||||
@@ -530,6 +533,11 @@ class DownloadChain(ChainBase):
|
||||
download_dir=download_dir,
|
||||
torrent_content=torrent_content,
|
||||
)
|
||||
self.download_site_subtitles(
|
||||
context=context,
|
||||
download_dir=download_dir,
|
||||
torrent_content=torrent_content,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"执行下载成功后处理失败:{str(e)}")
|
||||
|
||||
@@ -538,6 +546,154 @@ class DownloadChain(ChainBase):
|
||||
except Exception as err:
|
||||
logger.error(f"提交下载成功后处理后台任务失败:{str(err)}")
|
||||
|
||||
# 字幕压缩包扩展名与解压格式映射
|
||||
_SUBTITLE_ARCHIVE_FORMATS = {
|
||||
".zip": "zip",
|
||||
".rar": "rar",
|
||||
}
|
||||
|
||||
def _site_subtitle_links(self, context: Context) -> Optional[List[str]]:
|
||||
"""
|
||||
解析站点详情页的字幕下载链接,API 站点直接调用对应爬虫,
|
||||
普通站点通过模块分发解析页面代码
|
||||
"""
|
||||
torrent = context.torrent_info
|
||||
if torrent.site is not None:
|
||||
site = SiteOper().get(torrent.site)
|
||||
if indexer := SitesHelper().get_indexer(site.domain):
|
||||
if indexer.get("parser") == "mTorrent":
|
||||
return MTorrentSpider(indexer).get_subtitle_links(
|
||||
torrent.page_url
|
||||
)
|
||||
# TODO 其它采用API访问的站点
|
||||
return self.run_module("site_subtitle_links", context=context)
|
||||
|
||||
def download_site_subtitles(
|
||||
self,
|
||||
context: Context,
|
||||
download_dir: Path,
|
||||
torrent_content: Union[str, bytes] = None,
|
||||
) -> None:
|
||||
"""
|
||||
添加下载任务成功后,从站点下载字幕,保存到下载目录
|
||||
:param context: 上下文,包括识别信息、媒体信息、种子信息
|
||||
:param download_dir: 下载目录
|
||||
:param torrent_content: 种子内容,如果是种子文件,则为文件内容,否则为种子字符串
|
||||
"""
|
||||
if not settings.DOWNLOAD_SUBTITLE:
|
||||
return
|
||||
|
||||
# 没有种子文件不处理
|
||||
if not torrent_content:
|
||||
return
|
||||
|
||||
# 没有详情页不处理
|
||||
torrent = context.torrent_info
|
||||
if not torrent.page_url:
|
||||
return
|
||||
# 字幕下载目录
|
||||
logger.info("开始从站点下载字幕:%s" % torrent.page_url)
|
||||
# 获取种子信息
|
||||
folder_name, _ = TorrentHelper().get_fileinfo_from_torrent_content(torrent_content)
|
||||
# 文件保存目录,如果是单文件种子,则folder_name是空,此时文件保存目录就是下载目录
|
||||
storage_chain = StorageChain()
|
||||
# 等待目录存在
|
||||
working_dir_item = None
|
||||
# split download_dir into storage and path
|
||||
fileURI = FileURI.from_uri(download_dir.as_posix())
|
||||
storage = fileURI.storage
|
||||
download_dir = Path(fileURI.path)
|
||||
for _ in range(30):
|
||||
found = storage_chain.get_file_item(storage, download_dir / folder_name)
|
||||
if found:
|
||||
working_dir_item = found
|
||||
break
|
||||
time.sleep(1)
|
||||
# 目录仍然不存在,且有文件夹名,则创建目录
|
||||
if not working_dir_item and folder_name:
|
||||
parent_dir_item = storage_chain.get_folder(storage, download_dir)
|
||||
if parent_dir_item:
|
||||
working_dir_item = storage_chain.create_folder(
|
||||
parent_dir_item,
|
||||
folder_name
|
||||
)
|
||||
else:
|
||||
logger.error(f"下载根目录不存在,无法创建字幕文件夹:{download_dir}")
|
||||
return
|
||||
if not working_dir_item:
|
||||
logger.error(f"下载目录不存在,无法保存字幕:{download_dir / folder_name}")
|
||||
return
|
||||
# 解析字幕下载链接
|
||||
sublink_list = self._site_subtitle_links(context)
|
||||
if not sublink_list:
|
||||
logger.warn(f"{torrent.page_url} 页面未找到字幕下载链接")
|
||||
return
|
||||
# 下载所有字幕文件
|
||||
request = RequestUtils(
|
||||
cookies=torrent.site_cookie,
|
||||
ua=torrent.site_ua,
|
||||
proxies=settings.PROXY if torrent.site_proxy else None,
|
||||
)
|
||||
settings.TEMP_PATH.mkdir(parents=True, exist_ok=True)
|
||||
for sublink in sublink_list:
|
||||
logger.info(f"找到字幕下载链接:{sublink},开始下载...")
|
||||
# 下载
|
||||
ret = request.get_res(sublink)
|
||||
if ret and ret.status_code == 200:
|
||||
file_name = TorrentHelper.get_url_filename(ret, sublink)
|
||||
if not file_name:
|
||||
logger.warn(f"链接不是字幕文件:{sublink}")
|
||||
continue
|
||||
archive_format = self._SUBTITLE_ARCHIVE_FORMATS.get(Path(file_name).suffix.lower())
|
||||
if archive_format:
|
||||
archive_file = settings.TEMP_PATH / file_name
|
||||
# 保存
|
||||
archive_file.write_bytes(ret.content)
|
||||
# 解压路径
|
||||
archive_path = archive_file.with_name(archive_file.stem)
|
||||
try:
|
||||
# 解压文件
|
||||
SystemUtils.unpack_archive(
|
||||
archive_file,
|
||||
archive_path,
|
||||
archive_format=archive_format,
|
||||
)
|
||||
# 遍历转移文件
|
||||
for sub_file in SystemUtils.list_files(archive_path, settings.RMT_SUBEXT):
|
||||
target_sub_file = Path(working_dir_item.path) / Path(sub_file.name)
|
||||
if storage_chain.get_file_item(storage, target_sub_file):
|
||||
logger.info(f"字幕文件已存在:{target_sub_file}")
|
||||
continue
|
||||
logger.info(f"转移字幕 {sub_file} 到 {target_sub_file} ...")
|
||||
storage_chain.upload_file(working_dir_item, sub_file)
|
||||
except Exception as err:
|
||||
logger.error(f"字幕压缩包解压失败:{archive_file} - {str(err)}")
|
||||
# 删除临时文件
|
||||
try:
|
||||
if archive_path.exists():
|
||||
shutil.rmtree(archive_path)
|
||||
if archive_file.exists():
|
||||
archive_file.unlink()
|
||||
except Exception as err:
|
||||
logger.error(f"删除临时文件失败:{str(err)}")
|
||||
else:
|
||||
if Path(file_name).suffix.lower() not in settings.RMT_SUBEXT:
|
||||
logger.warn(f"链接不是支持的字幕文件:{sublink} - {file_name}")
|
||||
continue
|
||||
sub_file = settings.TEMP_PATH / file_name
|
||||
# 保存
|
||||
sub_file.write_bytes(ret.content)
|
||||
target_sub_file = Path(working_dir_item.path) / Path(sub_file.name)
|
||||
if storage_chain.get_file_item(storage, target_sub_file):
|
||||
logger.info(f"字幕文件已存在:{target_sub_file}")
|
||||
continue
|
||||
logger.info(f"转移字幕 {sub_file} 到 {target_sub_file} ...")
|
||||
storage_chain.upload_file(working_dir_item, sub_file)
|
||||
else:
|
||||
logger.error(f"下载字幕文件失败:{sublink}")
|
||||
continue
|
||||
logger.info(f"{torrent.page_url} 页面字幕下载完成")
|
||||
|
||||
@staticmethod
|
||||
def _is_subscribe_source(source: Optional[str]) -> bool:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user