mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-01 11:17:20 +08:00
Merge remote-tracking branch 'origin/v2' into v2
This commit is contained in:
@@ -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)
|
||||
# 更新仍需集数
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user