mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-09 09:26:55 +08:00
fix(download): detect dotted torrent root folders
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""下载器已有任务的媒体识别、资源归类与根目录重命名。"""
|
"""下载器已有任务的媒体识别、资源归类与根目录重命名。"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.application.configuration import get_configured_system_config
|
from app.application.configuration import get_configured_system_config
|
||||||
@@ -150,12 +150,59 @@ def _downloader_kind(name: str) -> str | None:
|
|||||||
return str(match.get("type") or "").casefold() if match else None
|
return str(match.get("type") or "").casefold() if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def _qb_module(chain: Any) -> Any:
|
||||||
|
"""返回当前运行的 qBittorrent 模块。"""
|
||||||
|
return chain.modulemanager.get_running_module("QbittorrentModule")
|
||||||
|
|
||||||
|
|
||||||
|
def _qb_root_folder(
|
||||||
|
chain: Any,
|
||||||
|
downloader: str,
|
||||||
|
hash_value: str,
|
||||||
|
current: PurePosixPath,
|
||||||
|
content: PurePosixPath | None,
|
||||||
|
) -> str | None:
|
||||||
|
"""确认 qB 任务是否拥有一个独立顶层目录。
|
||||||
|
|
||||||
|
文件夹名称允许包含点号;不能使用 ``Path.suffix`` 猜测它是不是文件。
|
||||||
|
优先检查已挂载文件系统,路径尚不存在时再用 qB 文件清单确认。
|
||||||
|
"""
|
||||||
|
if _downloader_kind(downloader) != "qbittorrent" or not content:
|
||||||
|
return None
|
||||||
|
if not content.is_absolute() or not content.is_relative_to(current):
|
||||||
|
return None
|
||||||
|
relative_content = content.relative_to(current)
|
||||||
|
if len(relative_content.parts) != 1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
local_content = Path(content.as_posix())
|
||||||
|
if local_content.is_dir():
|
||||||
|
return relative_content.name
|
||||||
|
if local_content.is_file():
|
||||||
|
return None
|
||||||
|
|
||||||
|
module = _qb_module(chain)
|
||||||
|
try:
|
||||||
|
torrent_files = module.torrent_files(tid=hash_value, downloader=downloader) if module else None
|
||||||
|
except Exception:
|
||||||
|
torrent_files = None
|
||||||
|
file_paths = [
|
||||||
|
PurePosixPath(str(getattr(item, "name", "")).replace("\\", "/"))
|
||||||
|
for item in (torrent_files or [])
|
||||||
|
if str(getattr(item, "name", "")).strip()
|
||||||
|
]
|
||||||
|
if not file_paths or not all(len(path.parts) >= 2 for path in file_paths):
|
||||||
|
return None
|
||||||
|
top_levels = {path.parts[0] for path in file_paths}
|
||||||
|
return relative_content.name if top_levels == {relative_content.name} else None
|
||||||
|
|
||||||
|
|
||||||
def _rename_qb_root(chain: Any, downloader: str, hash_value: str, old_name: str, new_name: str) -> bool:
|
def _rename_qb_root(chain: Any, downloader: str, hash_value: str, old_name: str, new_name: str) -> bool:
|
||||||
"""通过已运行的 qBittorrent 模块调用官方 renameFolder API。
|
"""通过已运行的 qBittorrent 模块调用官方 renameFolder API。
|
||||||
|
|
||||||
此变更由下载器维护任务与文件的对应关系,不直接操作文件系统。
|
此变更由下载器维护任务与文件的对应关系,不直接操作文件系统。
|
||||||
"""
|
"""
|
||||||
module = chain.modulemanager.get_running_module("QbittorrentModule")
|
module = _qb_module(chain)
|
||||||
server = module.get_instance(downloader) if module else None
|
server = module.get_instance(downloader) if module else None
|
||||||
client = getattr(server, "qbc", None)
|
client = getattr(server, "qbc", None)
|
||||||
if client is None:
|
if client is None:
|
||||||
@@ -219,12 +266,8 @@ def organize_existing_source(hash_value: str, request: Any, chain: Any, media_ch
|
|||||||
|
|
||||||
content_text = str(torrent.content_path or torrent.path or "").strip()
|
content_text = str(torrent.content_path or torrent.path or "").strip()
|
||||||
content = PurePosixPath(content_text) if content_text else None
|
content = PurePosixPath(content_text) if content_text else None
|
||||||
current_root_name = None
|
current_root_name = _qb_root_folder(chain, downloader, hash_value, current, content)
|
||||||
if content and content.is_absolute() and content.is_relative_to(current):
|
rename_supported = current_root_name is not None
|
||||||
relative_content = content.relative_to(current)
|
|
||||||
if len(relative_content.parts) == 1 and not content.suffix:
|
|
||||||
current_root_name = relative_content.name
|
|
||||||
rename_supported = _downloader_kind(downloader) == "qbittorrent" and current_root_name is not None
|
|
||||||
proposed_root_name = _root_name(media) if request.smart_rename and media else current_root_name
|
proposed_root_name = _root_name(media) if request.smart_rename and media else current_root_name
|
||||||
rename_required = bool(
|
rename_required = bool(
|
||||||
request.smart_rename
|
request.smart_rename
|
||||||
@@ -233,7 +276,9 @@ def organize_existing_source(hash_value: str, request: Any, chain: Any, media_ch
|
|||||||
and proposed_root_name != current_root_name
|
and proposed_root_name != current_root_name
|
||||||
)
|
)
|
||||||
if request.smart_rename and not rename_supported:
|
if request.smart_rename and not rename_supported:
|
||||||
raise ValueError("当前任务不是 qBittorrent 的单根目录任务,无法安全智能重命名")
|
if _downloader_kind(downloader) != "qbittorrent":
|
||||||
|
raise ValueError("智能根目录重命名目前仅支持 qBittorrent")
|
||||||
|
raise ValueError("该 qBittorrent 任务没有可重命名的独立顶层目录;单文件或散列文件任务请关闭智能重命名")
|
||||||
|
|
||||||
target_text = target.as_posix()
|
target_text = target.as_posix()
|
||||||
changed = current.as_posix() != target_text or rename_required
|
changed = current.as_posix() != target_text or rename_required
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ class SourceOrganizationTests(unittest.TestCase):
|
|||||||
self.media_chain = Mock()
|
self.media_chain = Mock()
|
||||||
self.media_chain.recognize_by_meta.return_value = self.media
|
self.media_chain.recognize_by_meta.return_value = self.media
|
||||||
self.qbc = Mock()
|
self.qbc = Mock()
|
||||||
module = NS(get_instance=lambda _name: NS(qbc=self.qbc))
|
self.torrent_files = [NS(name="Karen Mok - Loving Gaze 2002 FLAC/01.flac")]
|
||||||
|
module = NS(
|
||||||
|
get_instance=lambda _name: NS(qbc=self.qbc),
|
||||||
|
torrent_files=lambda **_kwargs: self.torrent_files,
|
||||||
|
)
|
||||||
self.chain = Mock()
|
self.chain = Mock()
|
||||||
self.chain.download_history_repository.get_by_hash.return_value = self.history
|
self.chain.download_history_repository.get_by_hash.return_value = self.history
|
||||||
self.chain.list_torrents.return_value = [self.torrent]
|
self.chain.list_torrents.return_value = [self.torrent]
|
||||||
@@ -210,7 +214,21 @@ class SourceOrganizationTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_smart_rename_rejects_multi_root_tasks(self):
|
def test_smart_rename_rejects_multi_root_tasks(self):
|
||||||
self.torrent.content_path = self.torrent.save_path
|
self.torrent.content_path = self.torrent.save_path
|
||||||
with self.assertRaisesRegex(ValueError, "无法安全智能重命名"):
|
with self.assertRaisesRegex(ValueError, "单文件或散列文件"):
|
||||||
|
self.preview()
|
||||||
|
|
||||||
|
def test_dotted_folder_name_is_not_treated_as_a_file(self):
|
||||||
|
folder = "Eagles.2011 - Hotel California SACD"
|
||||||
|
self.torrent.content_path = f"/volume1/UT/Musics/{folder}"
|
||||||
|
self.torrent_files = [NS(name=f"{folder}/01.dsf"), NS(name=f"{folder}/02.dsf")]
|
||||||
|
result = self.preview()
|
||||||
|
self.assertEqual(result["current_root_name"], folder)
|
||||||
|
self.assertTrue(result["rename_supported"])
|
||||||
|
|
||||||
|
def test_single_file_task_is_not_treated_as_a_folder(self):
|
||||||
|
self.torrent.content_path = "/volume1/UT/Musics/Hotel California.dsf"
|
||||||
|
self.torrent_files = [NS(name="Hotel California.dsf")]
|
||||||
|
with self.assertRaisesRegex(ValueError, "单文件或散列文件"):
|
||||||
self.preview()
|
self.preview()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user