refactor(chain): type download and media server ports

This commit is contained in:
jxxghp
2026-08-28 07:47:37 +08:00
parent c3f115d4fb
commit 5fb62108ab
25 changed files with 1262 additions and 331 deletions
+39 -22
View File
@@ -3,8 +3,13 @@
from __future__ import annotations
from collections.abc import Callable
from typing import Any, cast
from sqlalchemy.orm import Session
from app.application.download.failures import (
DownloadFailureSnapshot,
DownloadFailureWrite,
)
from app.db.oper.downloadfailure import DownloadFailureOper
from app.db.uow import SqlAlchemyUnitOfWork
@@ -12,7 +17,7 @@ from app.db.uow import SqlAlchemyUnitOfWork
class TransactionalDownloadFailureRepository:
"""为 Chain 下载失败读写创建短生命周期会话并显式收口事务。"""
def __init__(self, session_factory: Callable[[], Any]) -> None:
def __init__(self, session_factory: Callable[[], Session]) -> None:
"""保存由启动组合根提供的同步会话工厂。"""
self._session_factory = session_factory
@@ -20,36 +25,48 @@ class TransactionalDownloadFailureRepository:
self,
fingerprints: list[str],
now_time: str,
) -> dict[str, Any]:
) -> dict[str, DownloadFailureSnapshot]:
"""在独立只读会话中查询仍处于冷却期的失败记录。"""
with self._session_factory() as session:
return cast(
dict[str, Any],
DownloadFailureOper(db=session).get_active_by_fingerprints(
fingerprints=fingerprints,
now_time=now_time,
),
records = DownloadFailureOper(db=session).get_active_by_fingerprints(
fingerprints=fingerprints,
now_time=now_time,
)
return {
fingerprint: DownloadFailureSnapshot(
fingerprint=record.fingerprint,
error_message=record.error_message,
next_retry_at=record.next_retry_at,
)
for fingerprint, record in records.items()
}
def record_failure(
self,
fingerprint: str,
now_time: str,
next_retry_at: str,
**kwargs: object,
) -> Any:
def record_failure(self, failure: DownloadFailureWrite) -> None:
"""在一个显式 UoW 中新增或更新下载失败记录。"""
with self._session_factory() as session:
transaction = SqlAlchemyUnitOfWork(session)
try:
failure = DownloadFailureOper(db=session).record_failure(
fingerprint=fingerprint,
now_time=now_time,
next_retry_at=next_retry_at,
**kwargs,
DownloadFailureOper(db=session).record_failure(
fingerprint=failure.fingerprint,
now_time=failure.failed_at,
next_retry_at=failure.next_retry_at,
type=failure.media_type,
title=failure.title,
year=failure.year,
media_source=failure.media_source,
media_id=failure.media_id,
seasons=failure.seasons,
episodes=failure.episodes,
site=failure.site,
site_name=failure.site_name,
torrent_id=failure.torrent_id,
torrent_name=failure.torrent_name,
torrent_size=failure.torrent_size,
downloader=failure.downloader,
source=failure.source,
error_message=failure.error_message,
)
transaction.commit()
return failure
except Exception:
transaction.rollback()
raise
+110
View File
@@ -0,0 +1,110 @@
"""媒体服务器本地缓存的显式短会话与事务适配器。"""
from __future__ import annotations
import json
from collections.abc import Callable
from typing import Optional, TypeVar, Union
from sqlalchemy.orm import Session
from app.application.mediaserver import MediaServerSyncItem
from app.db.oper.mediaserver import MediaServerOper
from app.db.uow import SqlAlchemyUnitOfWork
from app.schemas.types import MediaSource
T = TypeVar("T")
class TransactionalMediaServerRepository:
"""让每次媒体库缓存查询或变更各自拥有一个短生命周期 Session。"""
def __init__(self, session_factory: Callable[[], Session]) -> None:
"""保存由启动组合根提供的同步会话工厂。"""
self._session_factory = session_factory
def _read(self, operation: Callable[[MediaServerOper], T]) -> T:
"""在独立只读会话中执行媒体库缓存查询。"""
with self._session_factory() as session:
return operation(MediaServerOper(db=session))
def _write(self, operation: Callable[[MediaServerOper], T]) -> T:
"""在独立 UoW 中执行并提交一项媒体库缓存变更。"""
with self._session_factory() as session:
unit_of_work = SqlAlchemyUnitOfWork(session)
try:
result = operation(MediaServerOper(db=session))
unit_of_work.commit()
return result
except Exception:
unit_of_work.rollback()
raise
def get_item_id(
self,
*,
title: Optional[str] = None,
year: Optional[Union[str, int]] = None,
mtype: Optional[str] = None,
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
season: Optional[int] = None,
) -> Optional[str]:
"""在短会话中返回匹配条目的服务器 item_id。"""
item_id: Optional[str] = self._read(
lambda repository: repository.get_item_id(
title=title,
year=year,
mtype=mtype,
media_source=media_source,
media_id=media_id,
season=season,
)
)
return item_id
def upsert(self, item: MediaServerSyncItem) -> bool:
"""在单个短事务中新增或更新一个媒体库同步条目。"""
payload = {
"server": item.server,
"library": item.library,
"item_id": item.item_id,
"item_type": item.item_type,
"title": item.title,
"original_title": item.original_title,
"year": item.year,
"media_source": item.media_source,
"media_id": item.media_id,
"path": item.path,
"seasoninfo": {
season: list(episodes)
for season, episodes in item.seasoninfo
},
"note": (
json.loads(item.note_json)
if item.note_json is not None
else None
),
"lst_mod_date": item.lst_mod_date,
}
created: bool = self._write(
lambda repository: repository.upsert(**payload)
)
return created
def delete_stale(self, *, server: str, sync_time: str) -> int:
"""在短事务中删除指定服务器本轮未更新的条目。"""
deleted: int = self._write(
lambda repository: repository.delete_stale(
server=server,
sync_time=sync_time,
),
)
return deleted
def delete_excluded_servers(self, servers: list[str]) -> int:
"""在短事务中删除已停用或已移除服务器的条目。"""
deleted: int = self._write(
lambda repository: repository.delete_excluded_servers(servers)
)
return deleted