mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-31 04:57:23 +08:00
162 lines
5.3 KiB
Python
162 lines
5.3 KiB
Python
"""批量下载候选的缺集记账与覆盖判定规则。
|
||
|
||
本模块承载批量择优下载的纯决策规则:缺集字典(no_exists)的季/集
|
||
记账更新、整季候选的目标范围推导、允许集裁剪和下载去重键构造。
|
||
规则只操作传入数据,不做网络/下载器 I/O;下载执行顺序编排仍属于链层。
|
||
"""
|
||
|
||
from copy import deepcopy
|
||
from typing import Dict, List, Optional, Set
|
||
|
||
from app.domain.context import Context
|
||
from app.schemas.media import build_media_key, resolve_media_identity
|
||
from app.schemas.mediaserver import NotExistMediaInfo
|
||
|
||
|
||
def update_no_exists_seasons(
|
||
no_exists: Dict[str, Dict[int, NotExistMediaInfo]],
|
||
media_key: str,
|
||
needed_seasons: List[int],
|
||
current_seasons: List[int],
|
||
) -> List[int]:
|
||
"""
|
||
更新缺失字典中的季数记账,返回剩余需要下载的季数。
|
||
|
||
就地维护传入的 no_exists:已满足的季从记账中移除,媒体条目清空后整体删除。
|
||
"""
|
||
# 剩余季数
|
||
need = list(set(needed_seasons).difference(set(current_seasons)))
|
||
# 清除已下载的季信息
|
||
seas = deepcopy(no_exists.get(media_key))
|
||
if seas:
|
||
for season in list(seas):
|
||
if season not in need:
|
||
no_exists[media_key].pop(season)
|
||
if not no_exists.get(media_key) and no_exists.get(media_key) is not None:
|
||
no_exists.pop(media_key)
|
||
break
|
||
return need
|
||
|
||
|
||
def update_no_exists_episodes(
|
||
no_exists: Dict[str, Dict[int, NotExistMediaInfo]],
|
||
media_key: str,
|
||
season: int,
|
||
needed_episodes: List[int],
|
||
current_episodes: Set[int],
|
||
) -> List[int]:
|
||
"""
|
||
更新缺失字典中的集数记账,返回剩余需要下载的集数。
|
||
|
||
就地维护传入的 no_exists:仍有缺集时重写该季缺失信息,集齐后移除季条目,
|
||
媒体条目清空后整体删除。
|
||
"""
|
||
# 剩余集数
|
||
need = list(set(needed_episodes).difference(set(current_episodes)))
|
||
if need:
|
||
not_exist = no_exists[media_key][season]
|
||
no_exists[media_key][season] = NotExistMediaInfo(
|
||
season=not_exist.season,
|
||
episodes=need,
|
||
total_episode=not_exist.total_episode,
|
||
start_episode=not_exist.start_episode,
|
||
require_complete_coverage=not_exist.require_complete_coverage
|
||
)
|
||
else:
|
||
no_exists[media_key].pop(season)
|
||
if not no_exists.get(media_key) and no_exists.get(media_key) is not None:
|
||
no_exists.pop(media_key)
|
||
return need
|
||
|
||
|
||
def get_season_episodes(
|
||
no_exists: Dict[str, Dict[int, NotExistMediaInfo]],
|
||
media_key: str,
|
||
season: int,
|
||
) -> int:
|
||
"""
|
||
获取需要的季的集数;缺失信息不存在时返回 9999 表示不构成约束。
|
||
"""
|
||
no_exist = no_exists.get(media_key)
|
||
if not no_exist:
|
||
return 9999
|
||
season_info = no_exist.get(season)
|
||
if not season_info:
|
||
return 9999
|
||
total = season_info.total_episode
|
||
return int(total) if total is not None else 9999
|
||
|
||
|
||
def get_no_exist_media(
|
||
no_exists: Optional[Dict[str, Dict[int, NotExistMediaInfo]]],
|
||
media_key: str,
|
||
season: int,
|
||
) -> Optional[NotExistMediaInfo]:
|
||
"""
|
||
获取指定媒体和季的缺失信息。
|
||
"""
|
||
if not no_exists:
|
||
return None
|
||
media = no_exists.get(media_key)
|
||
if not media:
|
||
return None
|
||
return media.get(season)
|
||
|
||
|
||
def get_required_episodes(
|
||
no_exists: Dict[str, Dict[int, NotExistMediaInfo]],
|
||
media_key: str,
|
||
season: int,
|
||
) -> Set[int]:
|
||
"""
|
||
获取整季候选必须覆盖的目标集范围。
|
||
"""
|
||
tv = get_no_exist_media(no_exists, media_key, season)
|
||
if not tv:
|
||
return set()
|
||
if not tv.total_episode:
|
||
return set()
|
||
start = tv.start_episode or 1
|
||
return set(range(start, tv.total_episode + 1))
|
||
|
||
|
||
def requires_complete_coverage(tv: Optional[NotExistMediaInfo]) -> bool:
|
||
"""
|
||
判断当前缺失范围是否要求候选资源完整覆盖目标范围。
|
||
"""
|
||
if not tv:
|
||
return False
|
||
return bool(tv.require_complete_coverage)
|
||
|
||
|
||
def apply_allowed_episodes(need_episodes: Set[int], context: Context) -> Set[int]:
|
||
"""
|
||
根据候选携带的允许集裁剪 need_episodes,返回真正可下载的剧集集合。
|
||
|
||
语义:allowed_episodes 为 None 表示调用方未约束,沿用 need_episodes;
|
||
非空集合则与 need_episodes 取交集;空集合(显式拒绝)会被交集自然消解为空。
|
||
调用方根据返回集合是否为空决定是否跳过当前候选。
|
||
"""
|
||
effective = set(need_episodes)
|
||
allowed = context.allowed_episodes
|
||
if allowed is not None:
|
||
effective &= set(allowed)
|
||
return effective
|
||
|
||
|
||
def get_movie_download_key(context: Context) -> str:
|
||
"""
|
||
获取电影下载去重键,确保失败候选不会阻断后续同名资源尝试。
|
||
"""
|
||
if context.media_info is None:
|
||
return ""
|
||
return str(context.media_info.title_year)
|
||
|
||
|
||
def get_music_download_key(context: Context) -> str:
|
||
"""获取音乐下载去重键,同一订阅目标失败后仍可尝试后续候选。"""
|
||
if context.media_info is None:
|
||
return ""
|
||
media_source, media_id = resolve_media_identity(media=context.media_info)
|
||
return str(build_media_key(media_source, media_id) or context.media_info.title_year)
|