Merge remote-tracking branch 'origin/v2' into v2

This commit is contained in:
jxxghp
2026-06-29 07:08:08 +08:00
11 changed files with 1752 additions and 298 deletions

View File

@@ -941,6 +941,7 @@ class DownloadChain(ChainBase):
continue
# 种子季是需要季或者子集
if set(torrent_season).issubset(set(need_season)):
complete_coverage_matched = False
if len(torrent_season) == 1:
# 只有一季的可能是命名错误,需要打开种子鉴别,只有实际集数大于等于总集数才下载
logger.info(f"开始下载种子 {torrent.title} ...")
@@ -965,7 +966,13 @@ class DownloadChain(ChainBase):
required_episodes = __get_required_episodes(need_mid, torrent_season[0]) \
if __requires_complete_coverage(need_tv_info) else set()
need_total = __get_season_episodes(need_mid, torrent_season[0])
if required_episodes and not required_episodes.issubset(torrent_episodes_set):
complete_coverage_matched = bool(required_episodes) \
and required_episodes.issubset(torrent_episodes_set)
if complete_coverage_matched:
logger.info(
f"{meta.org_string} 解析文件集数已完整覆盖目标范围:"
f"{StringUtils.format_ep(sorted(required_episodes))}")
if required_episodes and not complete_coverage_matched:
missing_episodes = sorted(required_episodes.difference(torrent_episodes_set))
logger.info(
f"{meta.org_string} 解析文件集数未覆盖目标范围,"
@@ -998,6 +1005,8 @@ class DownloadChain(ChainBase):
if download_id:
# 下载成功
if complete_coverage_matched:
context.confirmed_full_coverage = True
logger.info(f"{torrent.title} 添加下载成功")
downloaded_list.append(context)
# 更新仍需季集
@@ -1079,6 +1088,8 @@ class DownloadChain(ChainBase):
downloader=downloader)
if download_id:
# 下载成功
if __requires_complete_coverage(tv):
context.confirmed_full_coverage = True
logger.info(f"{meta.title} 添加下载成功")
downloaded_list.append(context)
# 更新仍需集数

View File

@@ -58,7 +58,18 @@ def build_subscribe_meta(subscribe: Subscribe) -> MetaBase:
class SubscribeChain(ChainBase):
"""
订阅管理处理链
订阅管理处理链
订阅链路同时服务电影、普通电视剧、分集洗版和全集洗版。普通电视剧订阅与
分集洗版共享按集事实note 表示目标集已经存在或已经下载,
episode_priority 表示每集已知下载质量;二者可以互相切换。全集洗版关注
完整目标范围的整体质量,只有下载层确认整包完整覆盖目标范围后,才把资源
写成目标范围内的按集事实。
实现上保持三个入口分离:下载事实入口只写 note / episode_priority
progress 刷新入口只把当前事实计算为 lack_episode 和电视剧洗版
current_priority完成入口只根据最终事实和完成策略收敛订阅状态。电影没有
按集事实,电影洗版的 current_priority 由电影下载优先级 writer 单独维护。
"""
_rlock = threading.RLock()
@@ -150,7 +161,7 @@ class SubscribeChain(ChainBase):
continue
if episode_number in target_episodes:
downloaded.add(episode_number)
for episode, priority in (subscribe.episode_priority or {}).items():
for episode, priority in cls.__get_episode_priority(subscribe, total_episode=total_episode).items():
if not str(episode).isdigit():
continue
try:
@@ -191,42 +202,60 @@ class SubscribeChain(ChainBase):
return cls.__get_pending_best_version_episodes_with_priority(subscribe, total_episode=total_episode)
@classmethod
def compute_completed_episode(cls, subscribe: Subscribe) -> Optional[int]:
def compute_lack_episode(
cls,
subscribe: Subscribe,
no_exists: Optional[Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]]] = None,
) -> int:
"""
计算订阅"已完成"集数派生值,仅用于响应填充,不入库
计算订阅范围内尚未下载到任何版本的集数
语义:
- 普通订阅 (best_version=0)``max(total_episode - lack_episode, 0)``,即媒体库已入库集数。
- 洗版订阅 (best_version=1含分集与全集洗版)
``(start_episode - 1) + (episode_priority 中 priority==100 且 ep ∈ [start, total] 的命中数)``。
start_episode 之前的集不在订阅范围内,视为"逻辑上已完成",与主文案分母 total_episode 对齐。
- 入参:完整 Subscribe ORM/Schema 对象,需至少包含 best_version、type、start_episode、
total_episode、lack_episode、episode_priority 字段。
- 返回:完成集数;电影或缺少 total_episode 时返回 None。
普通电视剧订阅以媒体库缺失结果为准;调用方没有缺失结果时按空缺失处理,
避免入口级刷新失败把未知状态写成异常。洗版电视剧订阅按 note 与
episode_priority>0 判断是否已有任意版本落点priority<100 仍表示已下载过任意版本。
"""
total_episode = subscribe.total_episode or 0
if subscribe.type != MediaType.TV.value or not total_episode:
return None
start_episode = subscribe.start_episode or 1
if subscribe.type != MediaType.TV.value:
return 0
if not subscribe.best_version:
lack = subscribe.lack_episode or 0
return max(total_episode - lack, 0)
no_exists = no_exists or {}
mediakey = subscribe.tmdbid or subscribe.doubanid
left_seasons = no_exists.get(mediakey) or {}
for season_info in left_seasons.values():
if season_info.season != subscribe.season:
continue
left_episodes = season_info.episodes
if not left_episodes:
return season_info.total_episode or 0
return len(left_episodes)
return 0
# 洗版口径start 之前的集视为已完成 + 范围内 priority==100 命中。
# ``start_episode > total_episode`` 是异常配置,需把"起始集前"偏移截断到 total
# 避免 completed 越过分母 total_episode。
episode_priority = subscribe.episode_priority or {}
priority_completed = sum(
1
for ep_key, priority in episode_priority.items()
if str(ep_key).isdigit()
and start_episode <= int(ep_key) <= total_episode
and priority == 100
)
return min(max(start_episode - 1, 0), total_episode) + priority_completed
total_episode = subscribe.total_episode or 0
if not total_episode:
return 0
start_episode = subscribe.start_episode or 1
if total_episode < start_episode:
return 0
target_episodes = set(range(start_episode, total_episode + 1))
downloaded: set = set()
for episode in subscribe.note or []:
try:
episode_number = int(episode)
except (TypeError, ValueError):
continue
if episode_number in target_episodes:
downloaded.add(episode_number)
for episode, priority in cls.__get_episode_priority(subscribe).items():
try:
if float(priority) <= 0:
continue
episode_number = int(episode)
except (TypeError, ValueError):
continue
if episode_number in target_episodes:
downloaded.add(episode_number)
return len(target_episodes - downloaded)
@classmethod
def get_best_version_current_priority(
@@ -240,19 +269,54 @@ class SubscribeChain(ChainBase):
if not subscribe.best_version or subscribe.type != MediaType.TV.value:
return subscribe.current_priority or 0
pending_episodes = cls.__get_pending_best_version_episodes_with_priority(subscribe, episode_priority)
if not pending_episodes:
return 100
target_episodes = cls.__get_best_version_target_episodes(subscribe)
if not target_episodes:
return subscribe.current_priority or 0
if episode_priority is None:
normalized = cls.__get_episode_priority(subscribe)
else:
normalized = cls.__normalize_episode_priority(episode_priority)
return max(
(normalized.get(str(episode), 0) for episode in pending_episodes),
return min(
(normalized.get(str(episode), 0) for episode in target_episodes),
default=0,
)
@classmethod
def __prepare_best_version_total_expansion_fields(
cls,
subscribe: Subscribe,
total_episode: int,
) -> Dict[str, Any]:
"""
准备洗版电视剧总集数扩展后需要写库的字段。
该方法会同步传入对象上的 total_episode / episode_priority方便同一链路后续
按最终快照继续计算进度;实际数据库写入由调用方统一执行。
"""
update_data: Dict[str, Any] = {"total_episode": total_episode}
old_total_episode = subscribe.total_episode or 0
subscribe.total_episode = total_episode
if subscribe.best_version and subscribe.type == MediaType.TV.value:
episode_priority = cls.__get_episode_priority(
subscribe,
total_episode=old_total_episode,
)
if not episode_priority and subscribe.current_priority is not None:
episode_priority = {
str(episode): int(subscribe.current_priority)
for episode in cls.__get_best_version_target_episodes(
subscribe,
total_episode=old_total_episode,
)
}
subscribe.episode_priority = episode_priority
update_data["episode_priority"] = episode_priority
update_data.update(cls.__prepare_subscribe_progress_fields(subscribe=subscribe, no_exists={}))
return update_data
@classmethod
def __is_best_version_complete(cls, subscribe: Subscribe) -> bool:
"""
@@ -414,7 +478,7 @@ class SubscribeChain(ChainBase):
@classmethod
def __is_full_season_priority_higher_than_all_targets(cls, subscribe: Subscribe, priority: int) -> bool:
"""
判断整季资源优先级是否高于订阅目标范围内所有分集
判断整季资源优先级是否高于订阅目标范围的整体优先级门槛
"""
if subscribe.type != MediaType.TV.value:
return False
@@ -428,12 +492,13 @@ class SubscribeChain(ChainBase):
except (TypeError, ValueError):
resource_priority = 0
episode_priority = cls.__get_episode_priority(subscribe)
for episode in target_episodes:
current_priority = episode_priority.get(str(episode), 0)
if resource_priority <= current_priority:
return False
return True
try:
current_priority = int(subscribe.current_priority) if subscribe.current_priority is not None \
else cls.get_best_version_current_priority(subscribe)
except (TypeError, ValueError):
current_priority = 0
return resource_priority > current_priority
@classmethod
def __build_full_pack_first_no_exists(
@@ -462,6 +527,7 @@ class SubscribeChain(ChainBase):
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1,
require_complete_coverage=True,
)
}
}
@@ -486,16 +552,30 @@ class SubscribeChain(ChainBase):
if context.media_info.type == MediaType.TV
and self.__is_full_season_resource(meta=context.meta_info, subscribe=subscribe)
] if full_pack_no_exists else []
full_pack_contexts = [
context for context in full_season_contexts
if self.__is_full_season_priority_higher_than_all_targets(
target_episodes = self.__get_best_version_target_episodes(subscribe)
target_range = f"{target_episodes[0]}-{target_episodes[-1]}" if target_episodes else "empty"
try:
current_priority_gate = int(subscribe.current_priority) if subscribe.current_priority is not None \
else self.get_best_version_current_priority(subscribe)
except (TypeError, ValueError):
current_priority_gate = 0
full_pack_contexts = []
for context in full_season_contexts:
candidate_priority = getattr(context.torrent_info, "pri_order", 0)
accepted = self.__is_full_season_priority_higher_than_all_targets(
subscribe=subscribe,
priority=context.torrent_info.pri_order,
priority=candidate_priority,
)
]
logger.info(
f"{subscribe.name} 整包候选优先级判断candidate_priority={candidate_priority}"
f"current_priority={current_priority_gate}target_range={target_range}"
f"decision={'accept' if accepted else 'reject'}"
)
if accepted:
full_pack_contexts.append(context)
if full_season_contexts and not full_pack_contexts:
logger.info(f"{subscribe.name} 全集候选优先级未高于所有目标集,回退到分集洗版")
logger.info(f"{subscribe.name} 全集候选优先级未高于 current_priority 门槛,回退到分集洗版")
if full_pack_contexts:
logger.info(f"{subscribe.name} 分集洗版优先尝试全集资源,共匹配到 {len(full_pack_contexts)} 个候选")
@@ -1223,61 +1303,25 @@ class SubscribeChain(ChainBase):
self._rlock.release()
logger.debug(f"search Lock released at {datetime.now()}")
def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaBase,
mediainfo: MediaInfo, downloads: Optional[List[Context]]):
@staticmethod
def __update_movie_best_version_download_priority(
subscribe: Subscribe,
mediainfo: MediaInfo,
downloads: Optional[List[Context]],
):
"""
更新订阅已下载资源优先级
记录电影洗版本轮下载资源优先级
"""
if not downloads:
return
if not subscribe.best_version:
return
# 当前下载资源的优先级
priority = max([item.torrent_info.pri_order for item in downloads])
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if subscribe.type == MediaType.TV.value:
episode_priority = self.__get_episode_priority(subscribe)
updated = False
for download in downloads:
download_priority = download.torrent_info.pri_order
downloaded_episodes = self.__get_downloaded_episodes([download])
if not downloaded_episodes and self.__is_full_season_resource(download.meta_info, subscribe):
# 整包下载时资源标题常不携带集数,视为覆盖当前订阅的全部目标集。
downloaded_episodes = self.__get_best_version_target_episodes(subscribe)
if not downloaded_episodes:
continue
for episode in downloaded_episodes:
episode_key = str(episode)
old_priority = episode_priority.get(episode_key)
if old_priority is None or download_priority > old_priority:
episode_priority[episode_key] = download_priority
updated = True
if not updated and not episode_priority:
return
current_priority = self.get_best_version_current_priority(subscribe, episode_priority)
# lack_episode 由 finish_subscribe_or_not -> __update_lack_episodes 按媒体库实况维护,本处不写
update_data: Dict[str, Any] = {
"episode_priority": episode_priority,
"last_update": now,
"current_priority": current_priority,
}
SubscribeOper().update(subscribe.id, update_data)
subscribe.episode_priority = episode_priority
subscribe.current_priority = current_priority
subscribe.last_update = now
completed_episodes = self.__get_best_version_completed_episodes(subscribe)
if not self.__is_best_version_complete(subscribe):
logger.info(
f'{mediainfo.title_year} 正在洗版,更新剧集优先级为 {priority},已完成剧集:{completed_episodes}'
)
if subscribe.type != MediaType.MOVIE.value:
return
# 订阅存在待定策略,不管是否已完成,均需更新订阅信息
SubscribeOper().update(subscribe.id, {
"current_priority": priority,
"last_update": now
@@ -1298,17 +1342,20 @@ class SubscribeChain(ChainBase):
mediakey = subscribe.tmdbid or subscribe.doubanid
# 是否有剩余集
no_lefts = not lefts or not lefts.get(mediakey)
# 不论是否洗版,只要本轮有下载产生就要把集数追加进 subscribe.note
# 保证"已下载过哪些集"这条事实在所有订阅模式下都有可靠落点;洗版分支
# 之前只写 episode_priority导致用户切回普通订阅时丢失下载历史并让
# __get_downloaded 在洗版下无法从 note 拿到 priority 未达 100 但实际下过的集。
if downloads:
if downloads and meta.type == MediaType.TV:
self.__record_subscribe_download_facts(subscribe=subscribe, mediainfo=mediainfo, downloads=downloads)
elif downloads:
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
# 是否完成订阅
if not subscribe.best_version:
# 普通订阅:先按 lefts 写 lack再判断完成
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
update_date=bool(downloads))
if meta.type == MediaType.TV:
self.__refresh_subscribe_progress_with_no_exists(
no_exists=lefts,
subscribe=subscribe,
touch_last_update=bool(downloads),
scene="download",
)
if ((no_lefts and meta.type == MediaType.TV)
or (downloads and meta.type == MediaType.MOVIE)
or force):
@@ -1317,13 +1364,20 @@ class SubscribeChain(ChainBase):
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
return
# 洗版订阅:本轮若有下载先更新 episode_priority / current_priority让 __update_lack_episodes
# 读取到包含本轮新下载的集;否则 lack 会慢一个搜索周期才反映新下载
if downloads:
self.update_subscribe_priority(subscribe=subscribe, meta=meta,
mediainfo=mediainfo, downloads=downloads)
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
update_date=bool(downloads))
if downloads and meta.type == MediaType.MOVIE:
# 电影没有按集质量事实,只能用 current_priority 表达洗版下载质量
self.__update_movie_best_version_download_priority(
subscribe=subscribe,
mediainfo=mediainfo,
downloads=downloads,
)
if meta.type == MediaType.TV:
self.__refresh_subscribe_progress_with_no_exists(
no_exists=lefts,
subscribe=subscribe,
touch_last_update=bool(downloads),
scene="download",
)
if self.__is_best_version_complete(subscribe):
# 洗版完成
self.__finish_subscribe(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
@@ -1859,7 +1913,7 @@ class SubscribeChain(ChainBase):
continue
# 对于电视剧,获取当前季的总集数
episodes = mediainfo.seasons.get(subscribe.season) or []
current_priority = None
progress_update = {}
if not subscribe.manual_total_episode and len(episodes):
total_episode = len(episodes)
# 允许外部覆盖按 TMDB 算出的总集数(如待定集数)
@@ -1867,23 +1921,34 @@ class SubscribeChain(ChainBase):
total_episode, season=subscribe.season, mediainfo=mediainfo,
tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid,
subscribe_id=subscribe.id, scene="refresh")
lack_episode = max((subscribe.lack_episode or 0) + (total_episode - (subscribe.total_episode or 0)), 0)
if subscribe.best_version and subscribe.type == MediaType.TV.value:
# 为新增集补齐 episode_priority 初始项priority=0
old_total_episode = subscribe.total_episode or 0
episode_priority = self.__get_episode_priority(subscribe)
for episode in range(old_total_episode + 1, total_episode + 1):
episode_priority.setdefault(str(episode), 0)
subscribe.total_episode = total_episode
subscribe.episode_priority = episode_priority
current_priority = self.get_best_version_current_priority(subscribe, episode_priority)
if total_episode > (subscribe.total_episode or 0):
if subscribe.best_version and subscribe.type == MediaType.TV.value:
progress_update = self.__prepare_best_version_total_expansion_fields(
subscribe=subscribe,
total_episode=total_episode,
)
else:
old_total_episode = subscribe.total_episode or 0
progress_update = {
"total_episode": total_episode,
"lack_episode": max(
(subscribe.lack_episode or 0) + (total_episode - old_total_episode),
0,
),
}
else:
total_episode = subscribe.total_episode
progress_update = {"lack_episode": subscribe.lack_episode}
if subscribe.best_version and subscribe.type == MediaType.TV.value:
progress_update = self.__prepare_subscribe_progress_fields(subscribe=subscribe, no_exists={})
logger.info(
f'订阅 {subscribe.name} 总集数变化,更新总集数为{total_episode}缺失集数为{lack_episode} ...')
f'订阅 {subscribe.name} 总集数变化,更新总集数为{total_episode}'
f'缺失集数为{progress_update.get("lack_episode", subscribe.lack_episode)} ...')
else:
total_episode = subscribe.total_episode
lack_episode = subscribe.lack_episode
progress_update = {"lack_episode": subscribe.lack_episode}
if subscribe.best_version and subscribe.type == MediaType.TV.value:
current_priority = self.get_best_version_current_priority(subscribe)
progress_update = self.__prepare_subscribe_progress_fields(subscribe=subscribe, no_exists={})
# 更新TMDB信息
update_data = {
"name": mediainfo.title,
@@ -1895,15 +1960,10 @@ class SubscribeChain(ChainBase):
"imdbid": mediainfo.imdb_id,
"tvdbid": mediainfo.tvdb_id,
"total_episode": total_episode,
"lack_episode": lack_episode
}
if subscribe.best_version and subscribe.type == MediaType.TV.value:
update_data["current_priority"] = current_priority
if not subscribe.manual_total_episode and len(episodes):
update_data["episode_priority"] = subscribe.episode_priority
subscribe.current_priority = current_priority
subscribe.total_episode = total_episode
subscribe.lack_episode = lack_episode
update_data.update(progress_update)
for key, value in progress_update.items():
setattr(subscribe, key, value)
subscribeoper.update(subscribe.id, update_data)
logger.info(f'{subscribe.name} 订阅元数据更新完成')
if progress_callback:
@@ -2155,80 +2215,297 @@ class SubscribeChain(ChainBase):
return note
return []
@staticmethod
def __update_lack_episodes(lefts: Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]],
subscribe: Subscribe,
mediainfo: MediaInfo,
update_date: Optional[bool] = False):
@classmethod
def __prepare_subscribe_progress_fields(
cls,
subscribe: Subscribe,
no_exists: Optional[Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]]] = None,
touch_last_update: Optional[bool] = False,
) -> Dict[str, Any]:
"""
写入订阅 lack_episode可选同时刷新 last_update
准备订阅进度持久化字段
lack 统一语义为"订阅范围内尚未下载到任何版本的集数"
- 普通订阅lack 从 ``lefts`` 提取lefts 已在 ``__get_subscribe_no_exits`` 里扣过 note
- 洗版订阅lack = ``[start, total]`` 范围内既不在 note 也不在 episode_priority(>0) 命中的集数。
洗版的 lefts 由 ``check_and_handle_existing_media`` 按 priority<100 构造,承担"搜索目标"职责,
"未下载"维度并不同义——若复用会把"已下载但待升级"的集错算成 lack。
该方法只返回待写字段,不主动写库。普通电视剧的 no_exists 为空时表示当前缺失结果为空;
洗版电视剧按 note 与 episode_priority 计算未下载过任何版本的目标集数量。
"""
update_data = {}
if update_date:
update_data["last_update"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
update_data: Dict[str, Any] = {}
if subscribe.type == MediaType.TV.value:
if no_exists is None and not subscribe.best_version:
no_exists = {}
update_data["lack_episode"] = cls.compute_lack_episode(subscribe, no_exists=no_exists)
if subscribe.best_version:
lack_episode = SubscribeChain.__compute_best_version_lack_episode(subscribe)
logger.info(f"{mediainfo.title_year}{subscribe.season} 剩余未下载剧集数为{lack_episode} ...")
elif not lefts:
# lefts 为空:媒体库实缺为 0
lack_episode = 0
logger.info(f'{mediainfo.title_year} 没有缺失集数,直接更新为 0 ...')
else:
mediakey = subscribe.tmdbid or subscribe.doubanid
left_seasons = lefts.get(mediakey)
lack_episode = 0
if left_seasons:
for season_info in left_seasons.values():
season = season_info.season
if season == subscribe.season:
left_episodes = season_info.episodes
if not left_episodes:
lack_episode = season_info.total_episode
else:
lack_episode = len(left_episodes)
logger.info(f"{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...")
break
update_data["lack_episode"] = lack_episode
# 更新数据库
if update_data:
SubscribeOper().update(subscribe.id, update_data)
update_data["current_priority"] = cls.get_best_version_current_priority(subscribe)
if update_data and touch_last_update:
update_data["last_update"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
return update_data
@staticmethod
def __compute_best_version_lack_episode(subscribe: Subscribe) -> int:
def __apply_subscribe_update(subscribe: Subscribe, update_data: Dict[str, Any]) -> None:
"""
计算洗版订阅"未下载集数":在 ``[start, total]`` 范围内排除已在 ``note`` 或
``episode_priority`` (>0) 中记账的集。priority<100 但 >0 的集视为"已下载、待升级"
不计入 lack——与 UI 上"已下载 = total - lack"展示口径一致。
写入订阅字段并同步当前内存对象,保证后续事件和判断读取最终快照。
"""
total_episode = subscribe.total_episode or 0
if not total_episode:
return 0
start_episode = subscribe.start_episode or 1
if total_episode < start_episode:
return 0
target_episodes = set(range(start_episode, total_episode + 1))
downloaded: set = set()
for ep in (subscribe.note or []):
if not update_data:
return
SubscribeOper().update(subscribe.id, update_data)
for key, value in update_data.items():
setattr(subscribe, key, value)
def __refresh_subscribe_progress_with_no_exists(
self,
subscribe: Subscribe,
no_exists: Optional[Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]]] = None,
touch_last_update: Optional[bool] = False,
scene: str = "download",
) -> Dict[str, Any]:
"""
使用已解析的缺失信息刷新订阅进度,避免下载链路重复查询媒体库。
"""
old_lack_episode = subscribe.lack_episode
old_current_priority = subscribe.current_priority
update_data = self.__prepare_subscribe_progress_fields(
subscribe=subscribe,
no_exists=no_exists,
touch_last_update=touch_last_update,
)
if not update_data:
return {"scene": scene, "updated": False, "fields": [], "reason": "unsupported_subscribe_type"}
self.__apply_subscribe_update(subscribe, update_data)
logger.info(
f"订阅 {subscribe.id} 进度刷新scene={scene}fields={list(update_data)}"
f"lack_episode {old_lack_episode}->{subscribe.lack_episode}"
f"current_priority {old_current_priority}->{subscribe.current_priority}reason=updated"
)
return {
"scene": scene,
"updated": True,
"fields": list(update_data),
"lack_episode": update_data.get("lack_episode", subscribe.lack_episode),
"current_priority": update_data.get("current_priority", subscribe.current_priority),
"reason": "updated",
}
def refresh_subscribe_progress(self, subscribe: Subscribe, *, scene: str = "update") -> Dict[str, Any]:
"""
按主程序口径重新计算并持久化订阅进度。
"""
if subscribe.type != MediaType.TV.value:
return {"scene": scene, "updated": False, "fields": [], "reason": "unsupported_subscribe_type"}
no_exists = None
mediainfo = None
if not subscribe.best_version:
meta = build_subscribe_meta(subscribe)
mediainfo = self.recognize_media(
meta=meta,
mtype=meta.type,
tmdbid=subscribe.tmdbid,
doubanid=subscribe.doubanid,
bangumiid=getattr(subscribe, "bangumiid", None),
episode_group=getattr(subscribe, "episode_group", None),
cache=False,
)
if not mediainfo:
return {"scene": scene, "updated": False, "fields": [], "reason": "recognize_failed"}
mediakey = subscribe.tmdbid or subscribe.doubanid
exist_flag, no_exists = self.resolve_subscribe_missing(
subscribe=subscribe,
meta=meta,
mediainfo=mediainfo,
mediakey=mediakey,
)
if not exist_flag and not no_exists:
return {"scene": scene, "updated": False, "fields": [], "reason": "resolve_missing_failed"}
return self.__refresh_subscribe_progress_with_no_exists(
subscribe=subscribe,
no_exists=no_exists,
touch_last_update=False,
scene=scene,
)
def backfill_existing_episodes(
self,
subscribe: Subscribe,
episodes: List[Union[int, str]],
priority: Optional[int] = None,
scene: str = "backfill",
) -> Dict[str, Any]:
"""
将媒体库既有剧集补写为订阅下载事实,并按需刷新进度字段。
"""
accepted = []
ignored = []
priority_updated = []
priority_ignored = []
target_episodes = set(self.__get_best_version_target_episodes(subscribe))
note = sorted({int(episode) for episode in subscribe.note or [] if str(episode).isdigit()})
note_set = set(note)
priority_episodes = set()
for episode in episodes or []:
try:
downloaded.add(int(ep))
episode_number = int(episode)
except (TypeError, ValueError):
ignored.append({"episode": episode, "reason": "invalid"})
continue
for ep_str, priority in (subscribe.episode_priority or {}).items():
if not str(ep_str).isdigit():
if episode_number not in target_episodes:
ignored.append({"episode": episode, "reason": "out_of_range"})
continue
try:
if float(priority) > 0:
downloaded.add(int(ep_str))
except (TypeError, ValueError):
if episode_number in note_set:
ignored.append({"episode": episode, "reason": "duplicate"})
priority_episodes.add(episode_number)
continue
return len(target_episodes - downloaded)
accepted.append(episode_number)
note_set.add(episode_number)
priority_episodes.add(episode_number)
summary: Dict[str, Any] = {
"scene": scene,
"accepted": accepted,
"ignored": ignored,
"priority_updated": priority_updated,
"priority_ignored": priority_ignored,
"fields": [],
}
update_data: Dict[str, Any] = {}
if accepted:
note = sorted(note_set)
subscribe.note = note
update_data["note"] = note
priority_valid = isinstance(priority, int) and not isinstance(priority, bool) and 1 <= priority <= 100
if priority is not None and not priority_valid:
summary["ignored_priority"] = priority
if priority_valid:
episode_priority = self.__get_episode_priority(subscribe)
for episode_number in sorted(priority_episodes):
episode_key = str(episode_number)
old_priority = episode_priority.get(episode_key)
if old_priority is None or priority > old_priority:
episode_priority[episode_key] = priority
priority_updated.append(episode_number)
else:
priority_ignored.append({
"episode": episode_number,
"reason": "duplicate" if old_priority == priority else "not_higher_priority",
})
if priority_updated:
subscribe.episode_priority = episode_priority
update_data["episode_priority"] = episode_priority
should_refresh_progress = subscribe.type == MediaType.TV.value and (accepted or priority_updated)
progress_summary = None
if should_refresh_progress and subscribe.best_version:
update_data.update(self.__prepare_subscribe_progress_fields(
subscribe=subscribe,
touch_last_update=True,
))
if update_data:
self.__apply_subscribe_update(subscribe, update_data)
summary["fields"] = list(update_data)
if should_refresh_progress and not subscribe.best_version:
progress_summary = self.refresh_subscribe_progress(subscribe, scene=scene)
if progress_summary is not None:
summary["progress"] = progress_summary
summary["updated"] = bool(update_data)
if progress_summary:
summary["updated"] = summary["updated"] or bool(progress_summary.get("updated"))
return summary
def __record_subscribe_download_facts(
self,
subscribe: Subscribe,
*,
mediainfo: MediaInfo,
downloads: Optional[List[Context]],
) -> Dict[str, Any]:
"""
记录主程序本轮下载产生的订阅事实,并返回本轮覆盖摘要。
"""
if not downloads:
return {"episodes": [], "fields": [], "updated": False}
covered_episodes = set()
written_priorities: Dict[str, int] = {}
used_full_coverage_fallback = False
episode_priority = self.__get_episode_priority(subscribe)
note_set = {
int(episode)
for episode in subscribe.note or []
if str(episode).isdigit()
}
update_data: Dict[str, Any] = {}
for download in downloads:
media = download.media_info
if subscribe.tmdbid and getattr(media, "tmdb_id", None) and media.tmdb_id != subscribe.tmdbid:
continue
if subscribe.doubanid and getattr(media, "douban_id", None) and media.douban_id != subscribe.doubanid:
continue
if subscribe.type == MediaType.MOVIE.value and media.type == MediaType.MOVIE:
note_set.add(1)
covered_episodes.add(1)
continue
if subscribe.type != MediaType.TV.value or media.type != MediaType.TV:
continue
selected_episodes = getattr(download, "selected_episodes", None)
if selected_episodes:
episodes = selected_episodes
elif getattr(download, "meta_info", None) and download.meta_info.episode_list:
episodes = download.meta_info.episode_list
elif getattr(download, "confirmed_full_coverage", False):
episodes = self.__get_best_version_target_episodes(subscribe)
used_full_coverage_fallback = True
else:
episodes = []
valid_episodes = []
for episode in episodes:
try:
episode_number = int(episode)
except (TypeError, ValueError):
continue
if episode_number not in self.__get_best_version_target_episodes(subscribe):
continue
valid_episodes.append(episode_number)
if not valid_episodes:
continue
priority = getattr(download.torrent_info, "pri_order", None)
for episode_number in valid_episodes:
note_set.add(episode_number)
covered_episodes.add(episode_number)
episode_key = str(episode_number)
old_priority = episode_priority.get(episode_key)
if isinstance(priority, int) and not isinstance(priority, bool) \
and (old_priority is None or priority > old_priority):
episode_priority[episode_key] = priority
written_priorities[episode_key] = priority
if covered_episodes:
update_data["note"] = sorted(note_set)
if subscribe.type == MediaType.TV.value:
update_data["episode_priority"] = episode_priority
update_data["last_update"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if update_data:
self.__apply_subscribe_update(subscribe, update_data)
logger.info(
f"{mediainfo.title_year} 订阅 {subscribe.id}{subscribe.season} 季记录下载事实:"
f"mode=best_version:{subscribe.best_version},full:{subscribe.best_version_full}"
f"covered_episodes={sorted(covered_episodes)}episode_priority={written_priorities}"
f"confirmed_full_coverage_fallback={used_full_coverage_fallback}"
)
return {
"episodes": sorted(covered_episodes),
"fields": list(update_data),
"updated": bool(update_data),
}
def __finish_subscribe(self, subscribe: Subscribe, mediainfo: MediaInfo, meta: MetaBase):
"""
@@ -3507,19 +3784,28 @@ class SubscribeChain(ChainBase):
if not new_total_episode or new_total_episode <= old_total_episode:
return
old_lack_episode = subscribe.lack_episode or 0
new_lack_episode = old_lack_episode + (new_total_episode - old_total_episode)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
SubscribeOper().update(subscribe.id, {
"total_episode": new_total_episode,
"lack_episode": new_lack_episode,
"last_update": now
})
subscribe.total_episode = new_total_episode
subscribe.lack_episode = new_lack_episode
if subscribe.best_version and subscribe.type == MediaType.TV.value:
update_data = self.__prepare_best_version_total_expansion_fields(
subscribe=subscribe,
total_episode=new_total_episode,
)
else:
update_data = {
"total_episode": new_total_episode,
"lack_episode": max(
(subscribe.lack_episode or 0) + (new_total_episode - old_total_episode),
0,
),
}
update_data["last_update"] = now
SubscribeOper().update(subscribe.id, update_data)
for key, value in update_data.items():
setattr(subscribe, key, value)
subscribe.last_update = now
logger.info(
f"订阅 {subscribe.name}{subscribe.season}季 总集数更新为 {new_total_episode}缺失集数更新为 {new_lack_episode}"
f"订阅 {subscribe.name}{subscribe.season}季 总集数更新为 {new_total_episode}"
f"缺失集数更新为 {subscribe.lack_episode}"
)
@classmethod