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
+12 -8
View File
@@ -10,10 +10,14 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional
from app.application.download.failures import DownloadFailureRepository
from app.application.mediaserver import MediaServerRepository
from app.application.transfer.execution import TransferExecutionRepository
from app.application.transfer.workflow import TransferAdmissionRepository
OperFactory = Callable[[], Any]
DownloadFailureRepositoryFactory = Callable[[], DownloadFailureRepository]
MediaServerRepositoryFactory = Callable[[], MediaServerRepository]
TransferAdmissionRepositoryFactory = Callable[[], TransferAdmissionRepository]
TransferExecutionRepositoryFactory = Callable[[], TransferExecutionRepository]
@@ -28,8 +32,8 @@ class ChainDataPorts:
transfer_history: OperFactory
transfer_pending: TransferAdmissionRepositoryFactory
transfer_execution: TransferExecutionRepositoryFactory
media_server: OperFactory
download_failure: OperFactory
media_server: MediaServerRepositoryFactory
download_failure: DownloadFailureRepositoryFactory
user: OperFactory
@@ -44,8 +48,8 @@ def configure_chain_data_ports(
transfer_history: OperFactory,
transfer_pending: TransferAdmissionRepositoryFactory,
transfer_execution: TransferExecutionRepositoryFactory,
media_server: OperFactory,
download_failure: OperFactory,
media_server: MediaServerRepositoryFactory,
download_failure: DownloadFailureRepositoryFactory,
user: OperFactory,
) -> None:
"""由启动组合根登记显式命名的 Chain 数据端口实现。"""
@@ -100,13 +104,13 @@ def get_chain_transfer_execution_port() -> TransferExecutionRepository:
return get_chain_data_ports().transfer_execution()
def get_chain_media_server_port() -> Any:
"""创建媒体服务器数据端口实例。"""
def get_chain_media_server_port() -> MediaServerRepository:
"""创建类型化的媒体服务器本地缓存端口实例。"""
return get_chain_data_ports().media_server()
def get_chain_download_failure_port() -> Any:
"""创建下载失败数据端口实例。"""
def get_chain_download_failure_port() -> DownloadFailureRepository:
"""创建类型化的下载失败冷却持久化端口实例。"""
return get_chain_data_ports().download_failure()
+57
View File
@@ -0,0 +1,57 @@
"""下载失败冷却记录的类型化应用契约。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Protocol, Union
from app.schemas.types import MediaSource
@dataclass(frozen=True, slots=True)
class DownloadFailureWrite:
"""一次下载失败冷却写入所需的完整稳定数据。"""
fingerprint: str
failed_at: str
next_retry_at: str
media_type: Optional[str] = None
title: Optional[str] = None
year: Optional[str] = None
media_source: Optional[MediaSource] = None
media_id: Optional[str] = None
seasons: Optional[str] = None
episodes: Optional[str] = None
site: Optional[int] = None
site_name: Optional[str] = None
torrent_id: Optional[str] = None
torrent_name: Optional[str] = None
torrent_size: Optional[Union[float, int]] = None
downloader: Optional[str] = None
source: Optional[str] = None
error_message: Optional[str] = None
@dataclass(frozen=True, slots=True)
class DownloadFailureSnapshot:
"""脱离数据库会话后供资源冷却判断使用的只读快照。"""
fingerprint: str
error_message: Optional[str]
next_retry_at: Optional[str]
class DownloadFailureRepository(Protocol):
"""下载链读写失败冷却状态所需的最小持久化端口。"""
def get_active_by_fingerprints(
self,
fingerprints: list[str],
now_time: str,
) -> dict[str, DownloadFailureSnapshot]:
"""返回仍处于冷却期的不可变失败快照。"""
...
def record_failure(self, failure: DownloadFailureWrite) -> None:
"""持久化一次失败事实,完成提交后不暴露数据库记录。"""
...
+102 -12
View File
@@ -1,16 +1,17 @@
import json
import re
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Optional, Protocol
from typing import Any, Optional, Protocol, Union
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
from app.application.service import ServiceBaseHelper
from app.domain.context import MusicInfo
from app.runtime.log import logger
from app.schemas.media import normalize_media_source, resolve_media_identity
from app.application.service import ServiceBaseHelper
from app.schemas.system import MediaServerConf
from app.schemas.system import ServiceInfo
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
from app.schemas.mediaserver import MediaServerItemUserState as _SchemaMediaServerItemUserState
from app.schemas.system import MediaServerConf, ServiceInfo
from app.schemas.types import (
MUSIC_ENTITY_ALBUM,
MediaSource,
@@ -19,16 +20,106 @@ from app.schemas.types import (
)
@dataclass(frozen=True, slots=True)
class MediaServerSyncItem:
"""媒体库同步写端使用的脱离远端响应与数据库会话的冻结条目。"""
server: str
library: Optional[str]
item_id: str
item_type: Optional[str]
title: Optional[str]
original_title: Optional[str]
year: Optional[Union[str, int]]
media_source: Optional[MediaSource]
media_id: Optional[str]
path: Optional[str]
seasoninfo: tuple[tuple[int, tuple[int, ...]], ...]
note_json: Optional[str]
lst_mod_date: str
@classmethod
def from_item(
cls,
item: _SchemaMediaServerItem,
*,
item_type: Optional[str],
seasoninfo: Mapping[int, Optional[list[int]]],
sync_time: str,
) -> "MediaServerSyncItem":
"""冻结远端媒体条目和本轮剧集快照,供短事务持久化。"""
return cls(
server=str(item.server or ""),
library=str(item.library) if item.library is not None else None,
item_id=str(item.item_id or ""),
item_type=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=tuple(
(season, tuple(episodes or ()))
for season, episodes in seasoninfo.items()
),
note_json=(
json.dumps(item.note, ensure_ascii=False)
if item.note is not None
else None
),
lst_mod_date=sync_time,
)
class MediaServerRepository(Protocol):
"""Chain 查询与同步媒体服务器本地缓存所需的最小持久化端口。"""
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,未命中时返回 None。"""
...
def upsert(self, item: MediaServerSyncItem) -> bool:
"""在短事务中新增或更新一个冻结媒体条目。"""
...
def delete_stale(self, *, server: str, sync_time: str) -> int:
"""删除指定服务器本轮同步未更新的本地条目。"""
...
def delete_excluded_servers(self, servers: list[str]) -> int:
"""删除已停用或已移除服务器的本地条目。"""
...
class AsyncMediaServerQueryRepository(Protocol):
"""媒体服务器本地条目查询所需的异步持久化端口。"""
async def async_exists(self, **kwargs: Any) -> Any | None:
"""按标题或统一媒体身份查找已同步条目。"""
async def async_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]:
"""按标题或统一媒体身份返回已同步条目的标量 ID。"""
...
class MediaServerQueryService:
"""封装媒体服务器本地存在性查询与 ORM 投影"""
"""封装媒体服务器本地存在性标量查询。"""
def __init__(self, repository: AsyncMediaServerQueryRepository):
"""使用显式媒体服务器查询端口初始化服务。"""
@@ -38,14 +129,14 @@ class MediaServerQueryService:
self,
*,
title: Optional[str] = None,
year: 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,未命中时返回 None。"""
item = await self._repository.async_exists(
return await self._repository.async_get_item_id(
title=title,
year=year,
mtype=mtype,
@@ -53,7 +144,6 @@ class MediaServerQueryService:
media_id=media_id,
season=season,
)
return item.item_id if item else None
class MediaServerIdentityHelper:
+54 -35
View File
@@ -6,7 +6,7 @@ import re
import shutil
import time
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
from typing import Dict, List, Optional, Set, Tuple, Union
from urllib.parse import parse_qs, urlencode, urljoin, urlparse
from app.adapters.network.http import RequestUtils
@@ -19,6 +19,11 @@ from app.application.chain.data import (
from app.application.configuration import get_chain_runtime_config_snapshot
from app.application.directory import DirectoryHelper, validate_download_save_path
from app.application.download import selection as _selection
from app.application.download.failures import (
DownloadFailureRepository,
DownloadFailureSnapshot,
DownloadFailureWrite,
)
from app.application.download.tasks import DownloadTaskService
from app.application.torrent import TorrentHelper
from app.chain import ChainBase
@@ -63,12 +68,6 @@ from app.schemas.types import (
)
from app.schemas.workflow import FileItem as _SchemaFileItem
if TYPE_CHECKING:
from typing import Any
DownloadFailure = Any
DOWNLOAD_FAILURE_RESOURCE_TTL_SECONDS = 24 * 60 * 60
DOWNLOAD_FAILURE_TRANSIENT_TTL_SECONDS = 60 * 60
DOWNLOAD_FAILURE_RESOURCE_ERROR_KEYWORDS = (
@@ -880,25 +879,40 @@ class DownloadChain(ChainBase):
torrent = context.torrent_info
site = getattr(torrent, "site", None)
try:
get_chain_download_failure_port().record_failure(
fingerprint=fingerprint,
now_time=now_time,
next_retry_at=next_retry_at,
type=getattr(getattr(media, "type", None), "value", getattr(media, "type", None)),
title=getattr(media, "title", None),
year=getattr(media, "year", None),
media_source=media_source,
media_id=media_id,
seasons=getattr(meta, "season", None),
episodes=episode_rules.format_ranges(list(episodes)) if episodes else self._format_failure_episodes(meta),
site=site if isinstance(site, int) else None,
site_name=getattr(torrent, "site_name", None),
torrent_id=self._torrent_resource_key(torrent),
torrent_name=getattr(torrent, "title", None),
torrent_size=getattr(torrent, "size", None),
downloader=downloader,
source=str(source)[:1000] if source else None,
error_message=str(error_msg or "")[:1000],
repository: DownloadFailureRepository = get_chain_download_failure_port()
repository.record_failure(
DownloadFailureWrite(
fingerprint=fingerprint,
failed_at=now_time,
next_retry_at=next_retry_at,
media_type=getattr(
getattr(media, "type", None),
"value",
getattr(media, "type", None),
),
title=getattr(media, "title", None),
year=getattr(media, "year", None),
media_source=media_source,
media_id=media_id,
seasons=(
str(getattr(meta, "season", None))
if getattr(meta, "season", None) is not None
else None
),
episodes=(
episode_rules.format_ranges(list(episodes))
if episodes
else self._format_failure_episodes(meta)
),
site=site if isinstance(site, int) else None,
site_name=getattr(torrent, "site_name", None),
torrent_id=self._torrent_resource_key(torrent),
torrent_name=getattr(torrent, "title", None),
torrent_size=getattr(torrent, "size", None),
downloader=downloader,
source=str(source)[:1000] if source else None,
error_message=str(error_msg or "")[:1000],
)
)
except Exception as err:
logger.error(f"记录下载失败冷却失败:{str(err)}")
@@ -908,7 +922,7 @@ class DownloadChain(ChainBase):
self,
contexts: List[Context],
source: Optional[str],
) -> Dict[str, "DownloadFailure"]:
) -> Dict[str, DownloadFailureSnapshot]:
"""
查询当前订阅候选中仍处于冷却期的失败记录,返回指纹到失败记录的映射。
"""
@@ -926,7 +940,8 @@ class DownloadChain(ChainBase):
return {}
now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
try:
return get_chain_download_failure_port().get_active_by_fingerprints(
repository: DownloadFailureRepository = get_chain_download_failure_port()
return repository.get_active_by_fingerprints(
fingerprints=fingerprints, now_time=now_time,
)
except Exception as err:
@@ -936,7 +951,7 @@ class DownloadChain(ChainBase):
@staticmethod
def _log_download_failure_cooldown(
context: Context,
failure: Optional["DownloadFailure"],
failure: Optional[DownloadFailureSnapshot],
) -> None:
"""记录候选资源处于失败冷却期时的跳过原因和下次重试时间。"""
reason = getattr(failure, "error_message", None) or "未知原因"
@@ -957,7 +972,7 @@ class DownloadChain(ChainBase):
contexts: List[Context],
downloader: Optional[str],
source: Optional[str],
) -> Tuple[List[Context], Dict[str, "DownloadFailure"]]:
) -> Tuple[List[Context], Dict[str, Optional[DownloadFailureSnapshot]]]:
"""
执行批量下载前的资源选择、排序和失败冷却准备。
@@ -979,16 +994,20 @@ class DownloadChain(ChainBase):
)
contexts = event_data.updated_contexts
contexts = TorrentHelper().sort_torrents(contexts)
return contexts, self._active_download_failure_fingerprints(
contexts=contexts,
source=source,
)
active_failures: Dict[str, Optional[DownloadFailureSnapshot]] = {
fingerprint: failure
for fingerprint, failure in self._active_download_failure_fingerprints(
contexts=contexts,
source=source,
).items()
}
return contexts, active_failures
def _download_movie_music_candidates(
self,
contexts: List[Context],
downloaded_list: List[Context],
active_failure_records: Dict[str, "DownloadFailure"],
active_failure_records: Dict[str, Optional[DownloadFailureSnapshot]],
save_path: Optional[str],
channel: Optional[NotificationChannel],
source: Optional[str],
+26 -16
View File
@@ -3,7 +3,11 @@ from datetime import datetime
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
from app.application.chain.data import get_chain_media_server_port
from app.application.mediaserver import get_mediaserver_configs
from app.application.mediaserver import (
MediaServerRepository,
MediaServerSyncItem,
get_mediaserver_configs,
)
from app.application.security.url import SecurityUtils
from app.chain import ChainBase
from app.runtime.log import logger
@@ -271,6 +275,7 @@ class MediaServerChain(ChainBase):
self,
mediaservers: List[Any],
server: Optional[str],
repository: MediaServerRepository,
) -> Tuple[List[Any], int, Dict[str, Any], Optional[int]]:
"""
准备启用媒体服务器的同步库和进度总量。
@@ -281,8 +286,7 @@ class MediaServerChain(ChainBase):
item.name for item in mediaservers
if item and item.enabled and item.name
]
dboper = get_chain_media_server_port()
dboper.delete_excluded_servers(enabled_servers)
repository.delete_excluded_servers(enabled_servers)
selected_servers = [
item for item in mediaservers
if item and item.enabled and (not server or item.name == server)
@@ -332,7 +336,7 @@ class MediaServerChain(ChainBase):
server_name: str,
selected_libraries: List[Any],
library_media_counts: Dict[str, Optional[int]],
dboper: Any,
repository: MediaServerRepository,
sync_time: str,
progress_callback: Optional[Callable[..., None]],
server_index: int,
@@ -363,14 +367,17 @@ class MediaServerChain(ChainBase):
item_type = self._normalize_item_type(item.item_type)
if item_type == MediaType.TV.value:
for episode in self.episodes(server_name, item.item_id) or []:
seasoninfo[episode.season] = episode.episodes
item_dict = item.model_dump()
item_dict.update({
"seasoninfo": seasoninfo,
"item_type": item_type,
"lst_mod_date": sync_time,
})
dboper.upsert(**item_dict)
if episode.season is None:
continue
seasoninfo[episode.season] = episode.episodes or []
repository.upsert(
MediaServerSyncItem.from_item(
item,
item_type=item_type,
seasoninfo=seasoninfo,
sync_time=sync_time,
)
)
if progress_callback:
if global_media_total:
progress_value = min(global_media_finished / global_media_total, 1) * 100
@@ -423,7 +430,10 @@ class MediaServerChain(ChainBase):
"media_finished": global_media_finished,
},
)
stale_count = dboper.delete_stale(server=server_name, sync_time=sync_time)
stale_count = repository.delete_stale(
server=server_name,
sync_time=sync_time,
)
logger.info(f"媒体服务器 {server_name} 清理陈旧数据完成,删除数量:{stale_count}")
return total_count, global_media_finished
@@ -463,9 +473,9 @@ class MediaServerChain(ChainBase):
with lock:
# 汇总统计
total_count = 0
dboper = get_chain_media_server_port()
repository = get_chain_media_server_port()
mediaservers, total_servers, server_sync_contexts, global_media_total = (
self._prepare_sync_contexts(mediaservers, server)
self._prepare_sync_contexts(mediaservers, server, repository)
)
if progress_callback:
progress_callback(
@@ -543,7 +553,7 @@ class MediaServerChain(ChainBase):
server_name=server_name,
selected_libraries=selected_libraries,
library_media_counts=library_media_counts,
dboper=dboper,
repository=repository,
sync_time=sync_time,
progress_callback=progress_callback,
server_index=server_index,
+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
+8 -8
View File
@@ -29,6 +29,12 @@ class MediaServerOper(DbOper):
if hasattr(MediaServerItem, k) and k != "id"
}
@staticmethod
def _contains_season(item: MediaServerItem, season: int) -> bool:
"""兼容 JSON 往返后变为字符串的 seasoninfo 键。"""
seasoninfo = item.seasoninfo or {}
return season in seasoninfo or str(season) in seasoninfo
def add(self, **kwargs) -> bool:
"""
新增媒体服务器数据
@@ -132,10 +138,7 @@ class MediaServerOper(DbOper):
if kwargs.get("season") is not None:
# 判断季是否存在
if not item.seasoninfo:
return None
seasoninfo = item.seasoninfo or {}
if kwargs.get("season") not in seasoninfo.keys():
if not self._contains_season(item, kwargs["season"]):
return None
return item
@@ -169,10 +172,7 @@ class MediaServerOper(DbOper):
if kwargs.get("season") is not None:
# 判断季是否存在
if not item.seasoninfo:
return None
seasoninfo = item.seasoninfo or {}
if kwargs.get("season") not in seasoninfo.keys():
if not self._contains_season(item, kwargs["season"]):
return None
return item
+2 -1
View File
@@ -114,6 +114,7 @@ from app.application.workflow import (
from app.command import CommandChain
from app.db.adapters.chain import TransactionalChainDurableEventWriter
from app.db.adapters.download import TransactionalDownloadFailureRepository
from app.db.adapters.mediaserver import TransactionalMediaServerRepository
from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
from app.db.adapters.query import SqlAlchemyDataQueryAdapter
from app.db.adapters.site import TransactionalSiteRepository
@@ -892,7 +893,7 @@ async def init_modules() -> HostRuntime:
transfer_execution=lambda: TransactionalTransferExecutionRepository(
SessionFactory
),
media_server=lambda: MediaServerOper(),
media_server=lambda: TransactionalMediaServerRepository(SessionFactory),
download_failure=lambda: TransactionalDownloadFailureRepository(
SessionFactory
),