mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
237 lines
8.4 KiB
Python
237 lines
8.4 KiB
Python
"""
|
|
已有下载任务的资源目录分类应用服务。
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Optional, cast
|
|
|
|
from app.application.classification.reference import (
|
|
apply_persisted_classification_snapshot,
|
|
persisted_classification_snapshot,
|
|
)
|
|
from app.application.directory import (
|
|
DirectoryHelper,
|
|
build_media_download_path,
|
|
validate_download_save_path,
|
|
)
|
|
from app.application.download.validation import validate_torrent_hash
|
|
from app.application.history import DownloadHistorySnapshot
|
|
from app.domain.classification.validation import validate_classification_category_path
|
|
from app.domain.context import MediaInfo, MusicInfo
|
|
from app.schemas.transfer import DownloaderTorrent
|
|
from app.schemas.types import MediaType
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DownloadSourceClassificationPlan:
|
|
"""
|
|
下载器资源目录重新分类的无副作用计划。
|
|
|
|
:param current_save_path: 下载器当前的资源保存目录
|
|
:param target_save_path: 按分类结果计算的目标保存目录
|
|
:param category: 拟应用的资源目录分类
|
|
:param changed: 目标目录与当前目录是否不同
|
|
"""
|
|
|
|
current_save_path: str
|
|
target_save_path: str
|
|
category: str
|
|
changed: bool
|
|
|
|
|
|
def _history_media(history: DownloadHistorySnapshot) -> MediaInfo | MusicInfo:
|
|
"""
|
|
从下载历史恢复计算目录所需的媒体与冻结分类事实。
|
|
"""
|
|
try:
|
|
media_type = MediaType(history.type)
|
|
except ValueError as error:
|
|
raise ValueError(f"下载历史媒体类型无效:{history.type}") from error
|
|
|
|
if media_type == MediaType.MUSIC:
|
|
note = history.note
|
|
music_note = note.get("music") if isinstance(note, dict) else None
|
|
if isinstance(music_note, dict):
|
|
media_payload = music_note.get("media")
|
|
music_version = music_note.get("version")
|
|
else:
|
|
media_payload = None
|
|
music_version = None
|
|
if isinstance(media_payload, dict) and music_version == 1:
|
|
media: MediaInfo | MusicInfo = MusicInfo.from_dict(media_payload)
|
|
else:
|
|
try:
|
|
year = int(history.year) if history.year else None
|
|
except (TypeError, ValueError):
|
|
year = None
|
|
media = MusicInfo(
|
|
media_source=history.media_source,
|
|
media_id=history.media_id,
|
|
music_type=history.music_type or "recording",
|
|
title=history.title,
|
|
year=year,
|
|
)
|
|
else:
|
|
media = MediaInfo(
|
|
type=media_type,
|
|
title=history.title or "",
|
|
year=history.year or "",
|
|
)
|
|
if history.media_source and history.media_id:
|
|
media.media_source = history.media_source
|
|
media.media_id = history.media_id
|
|
|
|
snapshot = persisted_classification_snapshot(
|
|
category_id=history.media_category_id,
|
|
category_path=history.media_category,
|
|
rule_id=history.classification_rule_id,
|
|
policy_revision=history.classification_policy_revision,
|
|
source=history.classification_source,
|
|
)
|
|
return cast(
|
|
MediaInfo | MusicInfo,
|
|
apply_persisted_classification_snapshot(media, snapshot) or media,
|
|
)
|
|
|
|
|
|
def resolve_download_source_classification(
|
|
torrent: DownloaderTorrent,
|
|
history: DownloadHistorySnapshot,
|
|
media_category: Optional[str] = None,
|
|
directory_helper: Optional[DirectoryHelper] = None,
|
|
) -> DownloadSourceClassificationPlan:
|
|
"""
|
|
按已配置下载根目录与历史分类快照计算任务目标位置。
|
|
"""
|
|
current_save_path = str(torrent.save_path or "").strip()
|
|
if not current_save_path:
|
|
raise ValueError("下载器未返回任务保存目录")
|
|
|
|
helper = directory_helper or DirectoryHelper()
|
|
media = _history_media(history)
|
|
if media_category:
|
|
manual_path = validate_classification_category_path(
|
|
tuple(segment.strip() for segment in media_category.split("/") if segment.strip())
|
|
)
|
|
if manual_path not in helper.classification_category_paths(media.type):
|
|
raise ValueError("手动指定的媒体分类不存在、已停用或与媒体类型不匹配")
|
|
media = cast(
|
|
MediaInfo | MusicInfo,
|
|
apply_persisted_classification_snapshot(
|
|
media,
|
|
persisted_classification_snapshot(
|
|
category_path=manual_path,
|
|
source="manual",
|
|
),
|
|
) or media,
|
|
)
|
|
directory = helper.get_download_dir_by_task_path(media, current_save_path)
|
|
if not directory or not directory.download_path:
|
|
raise ValueError("当前保存目录不在已配置的资源目录中")
|
|
if not helper.has_fixed_category(directory) and not directory.download_category_folder:
|
|
raise ValueError("当前资源目录未开启按类别分类")
|
|
|
|
category = (
|
|
helper.resolve_directory_category(directory, media)
|
|
if helper.has_fixed_category(directory)
|
|
else helper.resolve_media_category(media)
|
|
)
|
|
if not category.usable or not category.path:
|
|
raise ValueError("下载历史没有可用的媒体分类")
|
|
|
|
target_path = build_media_download_path(
|
|
Path(directory.download_path),
|
|
directory,
|
|
media,
|
|
directory_helper=helper,
|
|
).as_posix()
|
|
target_save_path = validate_download_save_path(target_path)
|
|
return DownloadSourceClassificationPlan(
|
|
current_save_path=current_save_path,
|
|
target_save_path=target_save_path,
|
|
category="/".join(category.path),
|
|
changed=current_save_path.rstrip("/") != target_save_path.rstrip("/"),
|
|
)
|
|
|
|
|
|
class DownloadSourceClassificationService:
|
|
"""
|
|
预览并通过下载器安全应用资源目录分类。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
list_torrents: Callable[..., list[DownloaderTorrent]],
|
|
get_history_by_hash: Callable[[str], Optional[DownloadHistorySnapshot]],
|
|
update_torrent: Callable[..., dict[str, bool]],
|
|
resolve_plan: Callable[
|
|
[DownloaderTorrent, DownloadHistorySnapshot, Optional[str]],
|
|
DownloadSourceClassificationPlan,
|
|
] = resolve_download_source_classification,
|
|
) -> None:
|
|
"""
|
|
注入下载器、历史与路径规划端口。
|
|
"""
|
|
self._list_torrents = list_torrents
|
|
self._get_history_by_hash = get_history_by_hash
|
|
self._update_torrent = update_torrent
|
|
self._resolve_plan = resolve_plan
|
|
|
|
def plan(
|
|
self,
|
|
*,
|
|
hash_value: str,
|
|
downloader: Optional[str] = None,
|
|
execute: bool = False,
|
|
media_category: Optional[str] = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
生成分类计划,只有 execute 为真时才请求下载器移动。
|
|
"""
|
|
validate_torrent_hash(hash_value)
|
|
torrents = self._list_torrents(
|
|
hashs=[hash_value],
|
|
downloader=downloader,
|
|
include_all_tags=True,
|
|
) or []
|
|
torrent = next(
|
|
(
|
|
item
|
|
for item in torrents
|
|
if str(item.hash or "").lower() == hash_value.lower()
|
|
),
|
|
None,
|
|
)
|
|
if torrent is None:
|
|
raise ValueError("未在下载器中找到该任务")
|
|
resolved_downloader = downloader or torrent.downloader
|
|
if not resolved_downloader:
|
|
raise ValueError("下载任务未标记所属下载器")
|
|
|
|
history = self._get_history_by_hash(hash_value)
|
|
if history is None:
|
|
raise ValueError("未找到该任务的下载历史")
|
|
plan = self._resolve_plan(torrent, history, media_category)
|
|
executed = False
|
|
if execute and plan.changed:
|
|
result = self._update_torrent(
|
|
hash_string=hash_value,
|
|
downloader=resolved_downloader,
|
|
save_path=plan.target_save_path,
|
|
) or {}
|
|
if not result.get("save_path"):
|
|
raise ValueError("下载器移动资源目录失败或不支持修改保存位置")
|
|
executed = True
|
|
|
|
return {
|
|
"hash": hash_value,
|
|
"downloader": resolved_downloader,
|
|
"current_save_path": plan.current_save_path,
|
|
"target_save_path": plan.target_save_path,
|
|
"category": plan.category,
|
|
"changed": plan.changed,
|
|
"executed": executed,
|
|
}
|