refactor: normalize capability module paths

This commit is contained in:
jxxghp
2026-08-27 22:20:38 +08:00
parent ea58c245dd
commit f134e068e7
85 changed files with 749 additions and 691 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
from typing import Any, Optional
from app.application.chain.data import ChainDataPorts
from app.application.chain.durable_events import ChainDurableEventWriter
from app.application.chain.events import ChainDurableEventWriter
from app.application.configuration import ChainRuntimeConfig
from app.runtime.stop import StopState, runtime_stop_state
+2 -3
View File
@@ -10,9 +10,8 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional
from app.application.transfer import TransferAdmissionRepository
from app.application.transfer_execution import TransferExecutionRepository
from app.application.transfer.execution import TransferExecutionRepository
from app.application.transfer.workflow import TransferAdmissionRepository
OperFactory = Callable[[], Any]
TransferAdmissionRepositoryFactory = Callable[[], TransferAdmissionRepository]
@@ -1,4 +1,4 @@
"""Chain durable 事件的事务写端口与可重放 payload 转换。"""
"""Chain 持久事件的事务写端口与可重放 payload 转换。"""
from __future__ import annotations
@@ -11,7 +11,7 @@ from typing import Any, Protocol, cast
from uuid import uuid4
from app.application.history import TransferHistoryRecord, TransferHistoryWriter
from app.application.transfer_execution import TransferSettlementResult
from app.application.transfer.execution import TransferSettlementResult
from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
@@ -108,7 +108,11 @@ def snapshot_download_added(payload: dict[str, Any]) -> dict[str, Any]:
context = payload.get("context")
return cast(dict[str, Any], _json_snapshot({
"hash": payload.get("hash"),
"context": context.to_dict() if isinstance(context, Context) else context,
"context": (
cast(Callable[[], dict[str, Any]], context.to_dict)()
if isinstance(context, Context)
else context
),
"username": payload.get("username"),
"downloader": payload.get("downloader"),
"episodes": list(payload.get("episodes") or []),
@@ -211,11 +215,11 @@ def _restore_context(payload: dict[str, Any]) -> Context:
media_info=(
_restore_media(media_payload) if isinstance(media_payload, dict) else None
),
torrent_info=(
torrent_info=cast(TorrentInfo, (
_restore_torrent(torrent_payload)
if isinstance(torrent_payload, dict)
else None
),
)),
media_recognize_fail_count=int(
payload.get("media_recognize_fail_count") or 0
),
+1
View File
@@ -0,0 +1 @@
"""整理应用能力包,分离任务工作流与持久执行状态机职责。"""
@@ -22,6 +22,7 @@ from dataclasses import dataclass, field
from pathlib import Path
from time import monotonic
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
@@ -31,27 +32,29 @@ from typing import (
Tuple,
TypeAlias,
Union,
cast,
)
from pydantic import BaseModel, ConfigDict, PrivateAttr
from app.adapters.system.host import SystemUtils
from app.application.agent import get_prompt_manager, get_running_agent_manager
from app.application.transfer_execution import TransferExecutionCheckpoint
from app.application.transfer.execution import TransferExecutionCheckpoint
from app.domain.context import MediaInfo, MusicInfo
from app.domain.media import normalize_music_type
from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.foundation import text as text_tools
from app.runtime.log import logger
from app.schemas.context import MediaInfo as _SchemaMediaInfo
from app.schemas.context import MetaInfo as _SchemaMetaInfo
from app.schemas.file import FileItem
from app.schemas.history import DownloadHistory
from app.schemas.media import OptionalMediaIdentityMixin, resolve_media_identity
from app.schemas.music import MusicInfo as _SchemaMusicInfo
from app.schemas.music import MusicMeta as _SchemaMusicMeta
from app.schemas.system import TransferDirectoryConf
from app.schemas.tmdb import TmdbEpisode
from app.schemas.transfer import MetaInfo as _SchemaMetaInfo
from app.schemas.transfer import MusicInfo as _SchemaMusicInfo
from app.schemas.transfer import MusicMeta as _SchemaMusicMeta
from app.schemas.transfer import TransferInfo, TransferJob, TransferJobTask
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
@@ -60,9 +63,61 @@ from app.schemas.types import (
MediaType,
ReplyMode,
)
from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
if TYPE_CHECKING:
class _ApplicationModel:
"""描述当前模块依赖的最小 Pydantic 模型类型形状。"""
def __init__(self, **data: Any) -> None:
"""接受模型字段关键字参数。"""
raise NotImplementedError
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
"""返回模型字段字典。"""
raise NotImplementedError
else:
_ApplicationModel = BaseModel
JSONValue: TypeAlias = Union[None, bool, int, float, str, list["JSONValue"], dict[str, "JSONValue"]]
JobId: TypeAlias = tuple[object, ...]
FileKey: TypeAlias = tuple[str, str]
class _DictionarySerializable(Protocol):
"""描述领域对象沿用的字典投影能力。"""
def to_dict(self) -> dict[str, Any]:
"""返回领域对象的字典投影。"""
def _domain_to_dict(value: object) -> dict[str, Any]:
"""按领域对象既有 ``to_dict`` 合同生成字典投影。"""
return cast(_DictionarySerializable, value).to_dict()
def _job_tasks(job: TransferJob) -> list[TransferJobTask]:
"""声明进程内作业始终使用已初始化的任务列表。"""
return cast(list[TransferJobTask], job.tasks)
def _job_task_fileitem(task: TransferJobTask) -> FileItem:
"""声明进程内作业任务始终绑定源文件。"""
return cast(FileItem, task.fileitem)
def _job_task_size(task: TransferJobTask) -> int:
"""按既有本地目录回退规则返回已完成任务的文件大小。"""
fileitem = _job_task_fileitem(task)
if fileitem.size is not None:
return fileitem.size
if fileitem.storage == "local":
return SystemUtils.get_directory_size(Path(cast(str, fileitem.path)))
return 0
def _transfer_task_meta(task: "TransferTask") -> MetaBase:
"""声明进入作业管理器的整理任务已经完成元数据解析。"""
return cast(MetaBase, task.meta)
TRANSFER_ADMISSION_ACCEPTED = "accepted"
TRANSFER_ADMISSION_PROVIDER_PENDING = "provider_pending"
@@ -636,7 +691,7 @@ class TransferLeaseLostError(TransferPlanningStateError):
"""整理 worker 已失去持久租约、不得继续推进任务时抛出的错误。"""
class TransferTask(OptionalMediaIdentityMixin, BaseModel):
class TransferTask(OptionalMediaIdentityMixin, _ApplicationModel):
"""
文件整理任务
"""
@@ -748,7 +803,7 @@ class TransferTask(OptionalMediaIdentityMixin, BaseModel):
self._lease_owner = owner_id
self._lease_token = lease_token
def to_dict(self):
def to_dict(self) -> dict[str, Any]:
"""
返回字典
@@ -758,13 +813,16 @@ class TransferTask(OptionalMediaIdentityMixin, BaseModel):
"""
dicts = vars(self).copy()
dicts["fileitem"] = self.fileitem.model_dump() if self.fileitem else None
dicts["meta"] = self.meta.to_dict() if self.meta else None
dicts["mediainfo"] = self.mediainfo.to_dict() if self.mediainfo else None
dicts["meta"] = _domain_to_dict(self.meta) if self.meta else None
dicts["mediainfo"] = _domain_to_dict(self.mediainfo) if self.mediainfo else None
dicts["target_directory"] = self.target_directory.model_dump() if self.target_directory else None
return dicts
class TransferQueue(BaseModel):
TransferCallback: TypeAlias = Callable[[TransferTask, TransferInfo], tuple[bool, str]]
class TransferQueue(_ApplicationModel):
"""
异步整理队列信息
@@ -774,7 +832,7 @@ class TransferQueue(BaseModel):
# 任务信息
task: Optional[TransferTask] = None
# 回调函数
callback: Optional[Callable] = None
callback: Optional[TransferCallback] = None
# 整理结果
result: Optional[TransferInfo] = None
@@ -904,7 +962,7 @@ class TransferQueueService:
self._list_tasks = list_tasks
self._expire_tasks = expire_tasks
def put(self, task: TransferTask, callback: Callable) -> bool:
def put(self, task: TransferTask, callback: TransferCallback) -> bool:
"""先持久化准入事实再入队;任何前置失败都撤销内存作业视图。"""
if not task or not self._register_task(task):
return False
@@ -1124,15 +1182,15 @@ class JobManager:
"""
# 整理中的作业
_job_view: Dict[Tuple, TransferJob] = {}
_job_view: Dict[JobId, TransferJob] = {}
# 汇总季集清单
_season_episodes: Dict[Tuple, List[int]] = {}
_season_episodes: Dict[JobId, List[int]] = {}
# 记录从 meta 作业迁移到 media 作业的关系,用于清理提前失败后残留的 media 作业
_meta_to_media_ids: Dict[Tuple, set[Tuple]] = {}
_meta_to_media_ids: Dict[JobId, set[JobId]] = {}
# 记录任务最近一次状态心跳,供外部异步接管任务的失活检测使用
_task_state_changed_at: Dict[Tuple[str, str], float] = {}
_task_state_changed_at: Dict[FileKey, float] = {}
# 记录仍由主程序整理线程直接执行的任务,避免把阻塞中的本地任务误判为失活
_active_executions: set[Tuple[str, str]] = set()
_active_executions: set[FileKey] = set()
def __init__(self) -> None:
"""初始化当前进程内的整理作业状态。"""
@@ -1143,15 +1201,18 @@ class JobManager:
self._active_executions = set()
@staticmethod
def __get_meta_id(meta: MetaBase = None, season: Optional[int] = None) -> Tuple:
def __get_meta_id(
meta: Optional[MetaBase] = None,
season: Optional[int] = None,
) -> JobId:
"""
获取元数据ID
"""
return meta.name, season
return cast(MetaBase, meta).name, season
@staticmethod
def __get_media_id(media: Optional[Union[MediaInfo, MusicInfo]] = None,
season: Optional[int] = None) -> Tuple:
season: Optional[int] = None) -> JobId:
"""
获取媒体ID音乐额外区分实体类型并为无远端ID的曲目构造稳定身份
"""
@@ -1204,18 +1265,19 @@ class JobManager:
)
return fileitem.storage or "local", normalized_path
def __get_id(self, task: TransferTask = None) -> Tuple:
def __get_id(self, task: Optional[TransferTask] = None) -> JobId:
"""
获取作业ID
"""
if task.mediainfo:
resolved_task = cast(TransferTask, task)
meta = _transfer_task_meta(resolved_task)
if resolved_task.mediainfo:
return self.__get_media_id(
media=task.mediainfo, season=task.meta.begin_season
media=resolved_task.mediainfo, season=meta.begin_season
)
else:
return self.__get_meta_id(meta=task.meta, season=task.meta.begin_season)
return self.__get_meta_id(meta=meta, season=meta.begin_season)
def get_job_id(self, task: TransferTask) -> Tuple:
def get_job_id(self, task: TransferTask) -> JobId:
"""返回任务当前所属的稳定作业身份,供作业级附加状态隔离使用。"""
return self.__get_id(task)
@@ -1229,11 +1291,11 @@ class JobManager:
mediainfo = deepcopy(task.mediainfo)
mediainfo.clear()
if isinstance(mediainfo, MusicInfo):
return _SchemaMusicInfo(**mediainfo.to_dict())
return _SchemaMediaInfo(**mediainfo.to_dict())
return _SchemaMusicInfo(**_domain_to_dict(mediainfo))
return _SchemaMediaInfo(**_domain_to_dict(mediainfo))
else:
# 没有媒体信息
meta: MetaBase = task.meta
meta = _transfer_task_meta(task)
if isinstance(meta, MetaMusic):
# 未识别的音乐按已解析元数据兜底展示;音乐年份为 int,
# 不能复用 MediaInfoyear 为 str),否则触发 pydantic 校验异常
@@ -1262,7 +1324,7 @@ class JobManager:
"""
if isinstance(task.meta, MetaMusic):
return _SchemaMusicMeta(**task.meta.to_dict())
return _SchemaMetaInfo(**task.meta.to_dict())
return _SchemaMetaInfo(**_domain_to_dict(_transfer_task_meta(task)))
def add_task(self, task: TransferTask, state: Optional[str] = "waiting") -> bool:
"""
@@ -1278,16 +1340,16 @@ class JobManager:
__mediaid__ = self.__get_id(task)
# 同一个源文件可能在识别前后落入不同作业,必须跨作业去重。
if any(
self.__get_file_key(t.fileitem) == file_key
self.__get_file_key(_job_task_fileitem(t)) == file_key
for job in self._job_view.values()
for t in job.tasks
for t in _job_tasks(job)
):
logger.debug(f"任务 {task.fileitem.name} 已存在,跳过重复添加")
return False
if __mediaid__ not in self._job_view:
self._job_view[__mediaid__] = TransferJob(
media=self.__get_media(task),
season=task.meta.begin_season,
season=_transfer_task_meta(task).begin_season,
tasks=[
TransferJobTask(
fileitem=task.fileitem,
@@ -1302,13 +1364,13 @@ class JobManager:
# 不重复添加任务
if any(
[
self.__get_file_key(t.fileitem) == file_key
for t in self._job_view[__mediaid__].tasks
self.__get_file_key(_job_task_fileitem(t)) == file_key
for t in _job_tasks(self._job_view[__mediaid__])
]
):
logger.debug(f"任务 {task.fileitem.name} 已存在,跳过重复添加")
return False
self._job_view[__mediaid__].tasks.append(
_job_tasks(self._job_view[__mediaid__]).append(
TransferJobTask(
fileitem=task.fileitem,
meta=self.__get_meta(task),
@@ -1320,12 +1382,14 @@ class JobManager:
self._task_state_changed_at[file_key] = monotonic()
# 添加季集信息
if self._season_episodes.get(__mediaid__):
self._season_episodes[__mediaid__].extend(task.meta.episode_list)
self._season_episodes[__mediaid__].extend(
_transfer_task_meta(task).episode_list
)
self._season_episodes[__mediaid__] = list(
set(self._season_episodes[__mediaid__])
)
else:
self._season_episodes[__mediaid__] = task.meta.episode_list
self._season_episodes[__mediaid__] = _transfer_task_meta(task).episode_list
return True
def migrate_task(self, task: TransferTask) -> bool:
@@ -1338,16 +1402,15 @@ class JobManager:
if not self.add_task(task, state=curr_task.state if curr_task else "waiting"):
return False
if curr_task and task.mediainfo:
metaid = self.__get_meta_id(
meta=task.meta, season=task.meta.begin_season
)
meta = _transfer_task_meta(task)
metaid = self.__get_meta_id(meta=meta, season=meta.begin_season)
mediaid = self.__get_id(task)
if source_job_id == metaid and mediaid != metaid:
with job_lock:
self._meta_to_media_ids.setdefault(metaid, set()).add(mediaid)
return True
def __is_job_done(self, job_id: Tuple) -> bool:
def __is_job_done(self, job_id: JobId) -> bool:
"""
检查指定作业是否已完成
"""
@@ -1355,10 +1418,10 @@ class JobManager:
return True
return all(
task.state in ["completed", "failed"]
for task in self._job_view[job_id].tasks
for task in _job_tasks(self._job_view[job_id])
)
def __pop_job(self, job_id: Tuple):
def __pop_job(self, job_id: JobId) -> None:
"""
移除指定作业和对应季集缓存
"""
@@ -1366,13 +1429,13 @@ class JobManager:
self._season_episodes.pop(job_id, None)
if not job:
return
for task in job.tasks:
file_key = self.__get_file_key(task.fileitem)
for task in _job_tasks(job):
file_key = self.__get_file_key(_job_task_fileitem(task))
if file_key:
self._task_state_changed_at.pop(file_key, None)
self._active_executions.discard(file_key)
def __remove_done_job_groups(self, job_ids: set[Tuple]):
def __remove_done_job_groups(self, job_ids: set[JobId]) -> None:
"""
清理已进入终态的独立作业或关联作业组
"""
@@ -1396,7 +1459,7 @@ class JobManager:
if self.__is_job_done(job_id):
self.__pop_job(job_id)
def start_execution(self, task: TransferTask):
def start_execution(self, task: TransferTask) -> None:
"""
标记任务仍由主程序整理线程直接执行
@@ -1410,7 +1473,7 @@ class JobManager:
with job_lock:
self._active_executions.add(file_key)
def finish_execution(self, task: TransferTask):
def finish_execution(self, task: TransferTask) -> None:
"""
结束主程序整理线程对任务的直接执行标记
@@ -1426,7 +1489,7 @@ class JobManager:
def expire_stale_running_tasks(
self, timeout_seconds: int
) -> List[Tuple[FileItem, int]]:
) -> List[tuple[FileItem, int]]:
"""
将外部接管后长期无心跳的运行中任务标记失败并清理作业视图
@@ -1440,12 +1503,13 @@ class JobManager:
return []
current_time = monotonic()
expired: List[Tuple[FileItem, int]] = []
affected_job_ids: set[Tuple] = set()
expired: List[tuple[FileItem, int]] = []
affected_job_ids: set[JobId] = set()
with job_lock:
for mediaid, job in self._job_view.items():
for task in job.tasks:
file_key = self.__get_file_key(task.fileitem)
for task in _job_tasks(job):
fileitem = _job_task_fileitem(task)
file_key = self.__get_file_key(fileitem)
if (
not file_key
or task.state != "running"
@@ -1463,13 +1527,13 @@ class JobManager:
self._season_episodes[mediaid] = list(
set(self._season_episodes[mediaid]) - set(episodes)
)
expired.append((task.fileitem, int(inactive_seconds)))
expired.append((fileitem, int(inactive_seconds)))
affected_job_ids.add(mediaid)
self.__remove_done_job_groups(affected_job_ids)
return expired
def running_task(self, task: TransferTask):
def running_task(self, task: TransferTask) -> None:
"""
设置任务为运行中并刷新外部异步任务的状态心跳
"""
@@ -1478,15 +1542,15 @@ class JobManager:
if __mediaid__ not in self._job_view:
return
# 更新状态
for t in self._job_view[__mediaid__].tasks:
for t in _job_tasks(self._job_view[__mediaid__]):
if t.fileitem == task.fileitem:
t.state = "running"
file_key = self.__get_file_key(t.fileitem)
file_key = self.__get_file_key(_job_task_fileitem(t))
if file_key:
self._task_state_changed_at[file_key] = monotonic()
break
def finish_task(self, task: TransferTask):
def finish_task(self, task: TransferTask) -> None:
"""
设置任务为完成/成功
"""
@@ -1495,15 +1559,15 @@ class JobManager:
if __mediaid__ not in self._job_view:
return
# 更新状态
for t in self._job_view[__mediaid__].tasks:
for t in _job_tasks(self._job_view[__mediaid__]):
if t.fileitem == task.fileitem:
t.state = "completed"
file_key = self.__get_file_key(t.fileitem)
file_key = self.__get_file_key(_job_task_fileitem(t))
if file_key:
self._task_state_changed_at[file_key] = monotonic()
break
def fail_task(self, task: TransferTask):
def fail_task(self, task: TransferTask) -> None:
"""
设置任务为失败
"""
@@ -1512,10 +1576,10 @@ class JobManager:
if __mediaid__ not in self._job_view:
return
# 更新状态
for t in self._job_view[__mediaid__].tasks:
for t in _job_tasks(self._job_view[__mediaid__]):
if t.fileitem == task.fileitem:
t.state = "failed"
file_key = self.__get_file_key(t.fileitem)
file_key = self.__get_file_key(_job_task_fileitem(t))
if file_key:
self._task_state_changed_at[file_key] = monotonic()
break
@@ -1523,10 +1587,10 @@ class JobManager:
if __mediaid__ in self._season_episodes:
self._season_episodes[__mediaid__] = list(
set(self._season_episodes[__mediaid__])
- set(task.meta.episode_list)
- set(_transfer_task_meta(task).episode_list)
)
def fail_unfinished_task(self, task: TransferTask):
def fail_unfinished_task(self, task: TransferTask) -> None:
"""
将指定任务视图中的非终态任务标记为失败
"""
@@ -1537,8 +1601,8 @@ class JobManager:
return
with job_lock:
for mediaid, job in self._job_view.items():
for job_task in job.tasks:
if self.__get_file_key(job_task.fileitem) != file_key:
for job_task in _job_tasks(job):
if self.__get_file_key(_job_task_fileitem(job_task)) != file_key:
continue
if job_task.state not in ["completed", "failed"]:
job_task.state = "failed"
@@ -1546,7 +1610,7 @@ class JobManager:
if mediaid in self._season_episodes:
self._season_episodes[mediaid] = list(
set(self._season_episodes[mediaid])
- set(task.meta.episode_list)
- set(_transfer_task_meta(task).episode_list)
)
return
@@ -1561,7 +1625,7 @@ class JobManager:
self,
fileitem: FileItem,
preserve_execution: bool = False,
) -> Tuple[Optional[TransferJobTask], Optional[Tuple]]:
) -> tuple[Optional[TransferJobTask], Optional[JobId]]:
"""
根据文件项移除任务并返回任务所在的作业ID
"""
@@ -1571,14 +1635,14 @@ class JobManager:
with job_lock:
for mediaid in list(self._job_view):
job = self._job_view[mediaid]
for task in job.tasks:
if self.__get_file_key(task.fileitem) == file_key:
job.tasks.remove(task)
for task in _job_tasks(job):
if self.__get_file_key(_job_task_fileitem(task)) == file_key:
_job_tasks(job).remove(task)
self._task_state_changed_at.pop(file_key, None)
if not preserve_execution:
self._active_executions.discard(file_key)
# 如果没有作业了,则移除作业
if not job.tasks:
if not _job_tasks(job):
self._job_view.pop(mediaid)
# 移除季集信息
if mediaid in self._season_episodes:
@@ -1602,16 +1666,15 @@ class JobManager:
return job
return None
def try_remove_job(self, task: TransferTask):
def try_remove_job(self, task: TransferTask) -> None:
"""
尝试移除任务对应的作业严格检查未完成作业线程安全
"""
with job_lock:
__metaid__ = self.__get_meta_id(
meta=task.meta, season=task.meta.begin_season
)
meta = _transfer_task_meta(task)
__metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
media=task.mediainfo, season=task.meta.begin_season
media=task.mediainfo, season=meta.begin_season
)
related_media_ids = set(self._meta_to_media_ids.get(__metaid__, set()))
@@ -1634,23 +1697,22 @@ class JobManager:
检查任务对应的作业是否整理完成不管成功还是失败
"""
with job_lock:
__metaid__ = self.__get_meta_id(
meta=task.meta, season=task.meta.begin_season
)
meta = _transfer_task_meta(task)
__metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
media=task.mediainfo, season=task.meta.begin_season
media=task.mediainfo, season=meta.begin_season
)
if __metaid__ in self._job_view:
meta_done = all(
task.state in ["completed", "failed"]
for task in self._job_view[__metaid__].tasks
for task in _job_tasks(self._job_view[__metaid__])
)
else:
meta_done = True
if __mediaid__ in self._job_view:
media_done = all(
task.state in ["completed", "failed"]
for task in self._job_view[__mediaid__].tasks
for task in _job_tasks(self._job_view[__mediaid__])
)
else:
media_done = True
@@ -1661,21 +1723,20 @@ class JobManager:
检查任务对应的作业是否已完成且有成功的记录
"""
with job_lock:
__metaid__ = self.__get_meta_id(
meta=task.meta, season=task.meta.begin_season
)
meta = _transfer_task_meta(task)
__metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
media=task.mediainfo, season=task.meta.begin_season
media=task.mediainfo, season=meta.begin_season
)
if __metaid__ in self._job_view:
meta_finished = all(
task.state in ["completed", "failed"]
for task in self._job_view[__metaid__].tasks
for task in _job_tasks(self._job_view[__metaid__])
)
else:
meta_finished = True
if __mediaid__ in self._job_view:
tasks = self._job_view[__mediaid__].tasks
tasks = _job_tasks(self._job_view[__mediaid__])
media_finished = all(
task.state in ["completed", "failed"] for task in tasks
) and any(task.state == "completed" for task in tasks)
@@ -1688,23 +1749,22 @@ class JobManager:
检查任务对应的作业是否全部成功
"""
with job_lock:
__metaid__ = self.__get_meta_id(
meta=task.meta, season=task.meta.begin_season
)
meta = _transfer_task_meta(task)
__metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season)
__mediaid__ = self.__get_media_id(
media=task.mediainfo, season=task.meta.begin_season
media=task.mediainfo, season=meta.begin_season
)
if __metaid__ in self._job_view:
meta_success = all(
task.state in ["completed"]
for task in self._job_view[__metaid__].tasks
for task in _job_tasks(self._job_view[__metaid__])
)
else:
meta_success = True
if __mediaid__ in self._job_view:
media_success = all(
task.state in ["completed"]
for task in self._job_view[__mediaid__].tasks
for task in _job_tasks(self._job_view[__mediaid__])
)
else:
media_success = True
@@ -1716,9 +1776,9 @@ class JobManager:
"""
with job_lock:
return {
task.download_hash
cast(str, task.download_hash)
for job in self._job_view.values()
for task in job.tasks
for task in _job_tasks(job)
}
def is_torrent_done(self, download_hash: str) -> bool:
@@ -1729,7 +1789,7 @@ class JobManager:
if any(
task.state not in {"completed", "failed"}
for job in self._job_view.values()
for task in job.tasks
for task in _job_tasks(job)
if task.download_hash == download_hash
):
return False
@@ -1743,7 +1803,7 @@ class JobManager:
if any(
task.state != "completed"
for job in self._job_view.values()
for task in job.tasks
for task in _job_tasks(job)
if task.download_hash == download_hash
):
return False
@@ -1767,7 +1827,7 @@ class JobManager:
__metaid__ = self.__get_meta_id(meta=meta, season=season)
return (
__metaid__ in self._job_view
and len(self._job_view[__metaid__].tasks) > 0
and len(_job_tasks(self._job_view[__metaid__])) > 0
)
def success_tasks(
@@ -1782,7 +1842,7 @@ class JobManager:
return []
return [
task
for task in self._job_view[__mediaid__].tasks
for task in _job_tasks(self._job_view[__mediaid__])
if task.state == "completed"
]
@@ -1796,7 +1856,7 @@ class JobManager:
__mediaid__ = self.__get_media_id(media=media, season=season)
if __mediaid__ not in self._job_view:
return []
return self._job_view[__mediaid__].tasks
return _job_tasks(self._job_view[__mediaid__])
def count(self, media: Union[MediaInfo, MusicInfo], season: Optional[int] = None) -> int:
"""
@@ -1809,7 +1869,7 @@ class JobManager:
return len(
[
task
for task in self._job_view[__mediaid__].tasks
for task in _job_tasks(self._job_view[__mediaid__])
if task.state == "completed"
]
)
@@ -1824,14 +1884,8 @@ class JobManager:
return 0
return sum(
[
task.fileitem.size
if task.fileitem.size is not None
else (
SystemUtils.get_directory_size(Path(task.fileitem.path))
if task.fileitem.storage == "local"
else 0
)
for task in self._job_view[__mediaid__].tasks
_job_task_size(task)
for task in _job_tasks(self._job_view[__mediaid__])
if task.state == "completed"
]
)
@@ -1841,7 +1895,7 @@ class JobManager:
获取所有任务总数
"""
with job_lock:
return sum([len(job.tasks) for job in self._job_view.values()])
return sum([len(_job_tasks(job)) for job in self._job_view.values()])
def pending_total(self) -> int:
"""
@@ -1855,7 +1909,7 @@ class JobManager:
return sum(
1
for job in self._job_view.values()
for task in job.tasks
for task in _job_tasks(job)
if task.state not in ("completed", "failed")
)
@@ -1960,10 +2014,10 @@ class FailedRetryScheduler:
def _build_retry_transfer_prompt(self, history_ids: list[int]) -> str:
"""根据失败记录数量构建统一的重试整理后台任务提示词。"""
task_type, template_context = self._build_retry_transfer_template_context(history_ids)
return get_prompt_manager().render_system_task_message(
return cast(str, get_prompt_manager().render_system_task_message(
task_type,
template_context=template_context,
)
))
async def schedule_retry(self, history_id: int, group_key: str = "") -> None:
"""