import json import random import re import time from datetime import datetime from typing import Dict, List, Optional, Union, Tuple from app.chain import ChainBase from app.chain.download import DownloadChain from app.chain.media import MediaChain from app.chain.search import SearchChain from app.chain.torrents import TorrentsChain from app.core.config import settings from app.core.context import TorrentInfo, Context, MediaInfo from app.core.meta import MetaBase from app.core.metainfo import MetaInfo from app.db.models.subscribe import Subscribe from app.db.subscribe_oper import SubscribeOper from app.db.systemconfig_oper import SystemConfigOper from app.helper.message import MessageHelper from app.helper.torrent import TorrentHelper from app.log import logger from app.schemas import NotExistMediaInfo, Notification from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType from app.utils.string import StringUtils class SubscribeChain(ChainBase): """ 订阅管理处理链 """ def __init__(self): super().__init__() self.downloadchain = DownloadChain() self.searchchain = SearchChain() self.subscribeoper = SubscribeOper() self.torrentschain = TorrentsChain() self.mediachain = MediaChain() self.message = MessageHelper() self.systemconfig = SystemConfigOper() self.torrenthelper = TorrentHelper() def add(self, title: str, year: str, mtype: MediaType = None, tmdbid: int = None, doubanid: str = None, season: int = None, channel: MessageChannel = None, userid: str = None, username: str = None, message: bool = True, exist_ok: bool = False, **kwargs) -> Tuple[Optional[int], str]: """ 识别媒体信息并添加订阅 """ logger.info(f'开始添加订阅,标题:{title} ...') mediainfo = None metainfo = MetaInfo(title) if year: metainfo.year = year if mtype: metainfo.type = mtype if season: metainfo.type = MediaType.TV metainfo.begin_season = season # 识别媒体信息 if settings.RECOGNIZE_SOURCE == "themoviedb": # TMDB识别模式 if not tmdbid and doubanid: # 将豆瓣信息转换为TMDB信息 tmdbinfo = self.mediachain.get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype) if tmdbinfo: mediainfo = MediaInfo(tmdb_info=tmdbinfo) else: # 识别TMDB信息,不使用缓存 mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid, cache=False) else: # 豆瓣识别模式,不使用缓存 mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, doubanid=doubanid, cache=False) if mediainfo: # 豆瓣标题处理 meta = MetaInfo(mediainfo.title) mediainfo.title = meta.name if not season: season = meta.begin_season # 识别失败 if not mediainfo: logger.warn(f'未识别到媒体信息,标题:{title},tmdbid:{tmdbid},doubanid:{doubanid}') return None, "未识别到媒体信息" # 总集数 if mediainfo.type == MediaType.TV: if not season: season = 1 # 总集数 if not kwargs.get('total_episode'): if not mediainfo.seasons: # 补充媒体信息 mediainfo = self.recognize_media(mtype=mediainfo.type, tmdbid=mediainfo.tmdb_id, doubanid=mediainfo.douban_id, cache=False) if not mediainfo: logger.error(f"媒体信息识别失败!") return None, "媒体信息识别失败" if not mediainfo.seasons: logger.error(f"媒体信息中没有季集信息,标题:{title},tmdbid:{tmdbid},doubanid:{doubanid}") return None, "媒体信息中没有季集信息" total_episode = len(mediainfo.seasons.get(season) or []) if not total_episode: logger.error(f'未获取到总集数,标题:{title},tmdbid:{tmdbid}, doubanid:{doubanid}') return None, f"未获取到第 {season} 季的总集数" kwargs.update({ 'total_episode': total_episode }) # 缺失集 if not kwargs.get('lack_episode'): kwargs.update({ 'lack_episode': kwargs.get('total_episode') }) # 更新媒体图片 self.obtain_images(mediainfo=mediainfo) # 合并信息 if doubanid: mediainfo.douban_id = doubanid # 添加订阅 sid, err_msg = self.subscribeoper.add(mediainfo, season=season, username=username, **kwargs) if not sid: logger.error(f'{mediainfo.title_year} {err_msg}') if not exist_ok and message: # 发回原用户 self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe, title=f"{mediainfo.title_year} {metainfo.season} " f"添加订阅失败!", text=f"{err_msg}", image=mediainfo.get_message_image(), userid=userid)) elif message: logger.info(f'{mediainfo.title_year} {metainfo.season} 添加订阅成功') if username or userid: text = f"评分:{mediainfo.vote_average},来自用户:{username or userid}" else: text = f"评分:{mediainfo.vote_average}" # 广而告之 self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe, title=f"{mediainfo.title_year} {metainfo.season} 已添加订阅", text=text, image=mediainfo.get_message_image())) # 返回结果 return sid, "" def exists(self, mediainfo: MediaInfo, meta: MetaBase = None): """ 判断订阅是否已存在 """ if self.subscribeoper.exists(tmdbid=mediainfo.tmdb_id, doubanid=mediainfo.douban_id, season=meta.begin_season if meta else None): return True return False def search(self, sid: int = None, state: str = 'N', manual: bool = False): """ 订阅搜索 :param sid: 订阅ID,有值时只处理该订阅 :param state: 订阅状态 N:未搜索 R:已搜索 :param manual: 是否手动搜索 :return: 更新订阅状态为R或删除订阅 """ if sid: subscribes = [self.subscribeoper.get(sid)] else: subscribes = self.subscribeoper.list(state) # 遍历订阅 for subscribe in subscribes: mediakey = subscribe.tmdbid or subscribe.doubanid # 校验当前时间减订阅创建时间是否大于1分钟,否则跳过先,留出编辑订阅的时间 if subscribe.date: now = datetime.now() subscribe_time = datetime.strptime(subscribe.date, '%Y-%m-%d %H:%M:%S') if (now - subscribe_time).total_seconds() < 60: logger.debug(f"订阅标题:{subscribe.name} 新增小于1分钟,暂不搜索...") continue # 随机休眠1-5分钟 if not sid and state == 'R': sleep_time = random.randint(60, 300) logger.info(f'订阅搜索随机休眠 {sleep_time} 秒 ...') time.sleep(sleep_time) logger.info(f'开始搜索订阅,标题:{subscribe.name} ...') # 如果状态为N则更新为R if subscribe.state == 'N': self.subscribeoper.update(subscribe.id, {'state': 'R'}) # 生成元数据 meta = MetaInfo(subscribe.name) meta.year = subscribe.year meta.begin_season = subscribe.season or None meta.type = MediaType(subscribe.type) # 识别媒体信息 mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid, cache=False) if not mediainfo: logger.warn( f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}') continue # 非洗版状态 if not subscribe.best_version: # 每季总集数 totals = {} if subscribe.season and subscribe.total_episode: totals = { subscribe.season: subscribe.total_episode } # 查询媒体库缺失的媒体信息 exist_flag, no_exists = self.downloadchain.get_no_exists_info( meta=meta, mediainfo=mediainfo, totals=totals ) else: # 洗版状态 exist_flag = False if meta.type == MediaType.TV: no_exists = { mediakey: { subscribe.season: NotExistMediaInfo( season=subscribe.season, episodes=[], total_episode=subscribe.total_episode, start_episode=subscribe.start_episode or 1) } } else: no_exists = {} # 已存在 if exist_flag: logger.info(f'{mediainfo.title_year} 媒体库中已存在') self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True) continue # 电视剧订阅处理缺失集 if meta.type == MediaType.TV: # 实际缺失集与订阅开始结束集范围进行整合 no_exists = self.__get_subscribe_no_exits( no_exists=no_exists, mediakey=mediakey, begin_season=meta.begin_season, total_episode=subscribe.total_episode, start_episode=subscribe.start_episode, ) # 打印汇总缺失集信息 if no_exists and no_exists.get(mediakey): no_exists_info = no_exists.get(mediakey).get(subscribe.season) if no_exists_info: logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}') # 站点范围 sites = self.get_sub_sites(subscribe) # 优先级过滤规则 if subscribe.best_version: priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules) else: priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules) # 过滤规则 filter_rule = self.get_filter_rule(subscribe) # 搜索,同时电视剧会过滤掉不需要的剧集 contexts = self.searchchain.process(mediainfo=mediainfo, keyword=subscribe.keyword, no_exists=no_exists, sites=sites, priority_rule=priority_rule, filter_rule=filter_rule, area="imdbid" if subscribe.search_imdbid else "title") if not contexts: logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源') self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, lefts=no_exists) continue # 过滤搜索结果 matched_contexts = [] for context in contexts: torrent_meta = context.meta_info torrent_info = context.torrent_info torrent_mediainfo = context.media_info # 非洗版 if not subscribe.best_version: # 如果是电视剧过滤掉已经下载的集数 if torrent_mediainfo.type == MediaType.TV: if self.__check_subscribe_note(subscribe, torrent_meta.episode_list): logger.info(f'{torrent_info.title} 对应剧集 {torrent_meta.episode_list} 已下载过') continue else: # 洗版时,非整季不要 if torrent_mediainfo.type == MediaType.TV: if torrent_meta.episode_list: logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 不是整季') continue # 洗版时,优先级小于已下载优先级的不要 if subscribe.current_priority \ and torrent_info.pri_order < subscribe.current_priority: logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于已下载优先级') continue matched_contexts.append(context) if not matched_contexts: logger.warn(f'订阅 {subscribe.name} 没有符合过滤条件的资源') self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, lefts=no_exists) continue # 自动下载 downloads, lefts = self.downloadchain.batch_download( contexts=matched_contexts, no_exists=no_exists, userid=subscribe.username, username=subscribe.username, save_path=subscribe.save_path ) # 判断是否应完成订阅 self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, downloads=downloads, lefts=lefts) # 手动触发时发送系统消息 if manual: if sid: self.message.put(f'订阅 {subscribes[0].name} 搜索完成!') else: self.message.put('所有订阅搜索完成!') def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaInfo, mediainfo: MediaInfo, downloads: List[Context]): """ 更新订阅已下载资源的优先级 """ if not downloads: return if not subscribe.best_version: return # 当前下载资源的优先级 priority = max([item.torrent_info.pri_order for item in downloads]) if priority == 100: logger.info(f'{mediainfo.title_year} 洗版完成,删除订阅') self.subscribeoper.delete(subscribe.id) # 发送通知 self.post_message(Notification(mtype=NotificationType.Subscribe, title=f'{mediainfo.title_year} {meta.season} 已洗版完成', image=mediainfo.get_message_image())) else: # 正在洗版,更新资源优先级 logger.info(f'{mediainfo.title_year} 正在洗版,更新资源优先级为 {priority}') self.subscribeoper.update(subscribe.id, { "current_priority": priority }) def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaInfo, mediainfo: MediaInfo, downloads: List[Context] = None, lefts: Dict[Union[int | str], Dict[int, NotExistMediaInfo]] = None, force: bool = False): """ 判断是否应完成订阅 """ mediakey = subscribe.tmdbid or subscribe.doubanid # 是否有剩余集 no_lefts = not lefts or not lefts.get(mediakey) # 是否完成订阅 if not subscribe.best_version: # 非洗板 if ((no_lefts and meta.type == MediaType.TV) or (downloads and meta.type == MediaType.MOVIE) or force): # 全部下载完成 logger.info(f'{mediainfo.title_year} 完成订阅') self.subscribeoper.delete(subscribe.id) # 发送通知 self.post_message(Notification(mtype=NotificationType.Subscribe, title=f'{mediainfo.title_year} {meta.season} 已完成订阅', image=mediainfo.get_message_image())) elif downloads and meta.type == MediaType.TV: # 电视剧更新已下载集数 self.__update_subscribe_note(subscribe=subscribe, downloads=downloads) # 更新订阅剩余集数和时间 self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo, update_date=True) else: # 未下载到内容且不完整 logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...') if meta.type == MediaType.TV: # 更新订阅剩余集数 self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo, update_date=False) elif downloads: # 洗板,下载到了内容,更新资源优先级 self.update_subscribe_priority(subscribe=subscribe, meta=meta, mediainfo=mediainfo, downloads=downloads) else: # 洗版,未下载到内容 logger.info(f'{mediainfo.title_year} 继续洗版 ...') def refresh(self): """ 订阅刷新 """ # 触发刷新站点资源,从缓存中匹配订阅 sites = self.get_subscribed_sites() if sites is None: return self.match( self.torrentschain.refresh(sites=sites) ) def get_sub_sites(self, subscribe: Subscribe) -> List[int]: """ 获取订阅中涉及的站点清单 """ if subscribe.sites: return json.loads(subscribe.sites) # 默认站点 return self.systemconfig.get(SystemConfigKey.RssSites) or [] def get_subscribed_sites(self) -> Optional[List[int]]: """ 获取订阅中涉及的所有站点清单(节约资源) :return: 返回[]代表所有站点命中,返回None代表没有订阅 """ # 查询所有订阅 subscribes = self.subscribeoper.list('R') if not subscribes: return None ret_sites = [] # 刷新订阅选中的Rss站点 for subscribe in subscribes: # 刷新选中的站点 ret_sites.extend(self.get_sub_sites(subscribe)) # 去重 if ret_sites: ret_sites = list(set(ret_sites)) return ret_sites def get_filter_rule(self, subscribe: Subscribe): """ 获取订阅过滤规则,同时组合默认规则 """ # 默认过滤规则 default_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {} return { "include": subscribe.include or default_rule.get("include"), "exclude": subscribe.exclude or default_rule.get("exclude"), "quality": subscribe.quality or default_rule.get("quality"), "resolution": subscribe.resolution or default_rule.get("resolution"), "effect": subscribe.effect or default_rule.get("effect"), "tv_size": default_rule.get("tv_size"), "movie_size": default_rule.get("movie_size"), "min_seeders": default_rule.get("min_seeders"), } def match(self, torrents: Dict[str, List[Context]]): """ 从缓存中匹配订阅,并自动下载 """ if not torrents: logger.warn('没有缓存资源,无法匹配订阅') return # 所有订阅 subscribes = self.subscribeoper.list('R') # 遍历订阅 for subscribe in subscribes: logger.info(f'开始匹配订阅,标题:{subscribe.name} ...') mediakey = subscribe.tmdbid or subscribe.doubanid # 生成元数据 meta = MetaInfo(subscribe.name) meta.year = subscribe.year meta.begin_season = subscribe.season or None meta.type = MediaType(subscribe.type) # 识别媒体信息 mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid, cache=False) if not mediainfo: logger.warn( f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}') continue # 非洗版 if not subscribe.best_version: # 每季总集数 totals = {} if subscribe.season and subscribe.total_episode: totals = { subscribe.season: subscribe.total_episode } # 查询缺失的媒体信息 exist_flag, no_exists = self.downloadchain.get_no_exists_info( meta=meta, mediainfo=mediainfo, totals=totals ) else: # 洗版 exist_flag = False if meta.type == MediaType.TV: no_exists = { mediakey: { subscribe.season: NotExistMediaInfo( season=subscribe.season, episodes=[], total_episode=subscribe.total_episode, start_episode=subscribe.start_episode or 1) } } else: no_exists = {} # 已存在 if exist_flag: logger.info(f'{mediainfo.title_year} 媒体库中已存在') self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True) continue # 电视剧订阅 if meta.type == MediaType.TV: # 整合实际缺失集与订阅开始集结束集 no_exists = self.__get_subscribe_no_exits( no_exists=no_exists, mediakey=mediakey, begin_season=meta.begin_season, total_episode=subscribe.total_episode, start_episode=subscribe.start_episode, ) # 打印汇总缺失集信息 if no_exists and no_exists.get(mediakey): no_exists_info = no_exists.get(mediakey).get(subscribe.season) if no_exists_info: logger.info(f'订阅 {mediainfo.title_year} {meta.season} 缺失集:{no_exists_info.episodes}') # 过滤规则 filter_rule = self.get_filter_rule(subscribe) # 遍历缓存种子 _match_context = [] for domain, contexts in torrents.items(): logger.info(f'开始匹配站点:{domain},共缓存了 {len(contexts)} 个种子...') for context in contexts: # 检查是否匹配 torrent_meta = context.meta_info torrent_mediainfo = context.media_info torrent_info = context.torrent_info # 如果识别了媒体信息,则比对TMDBID和类型 if torrent_mediainfo.tmdb_id or torrent_mediainfo.douban_id: if torrent_mediainfo.type != mediainfo.type: continue if torrent_mediainfo.tmdb_id \ and torrent_mediainfo.tmdb_id != mediainfo.tmdb_id: continue if torrent_mediainfo.douban_id \ and torrent_mediainfo.douban_id != mediainfo.douban_id: continue logger.info(f'{mediainfo.title_year} 通过媒体信ID匹配到资源:{torrent_info.site_name} - {torrent_info.title}') else: # 按标题匹配 # 比对种子识别类型 if torrent_meta.type == MediaType.TV and mediainfo.type != MediaType.TV: continue # 比对种子在站点中的类型 if torrent_info.category == MediaType.TV.value and mediainfo.type != MediaType.TV: continue # 比对年份 if mediainfo.year: if mediainfo.type == MediaType.TV: # 剧集年份,每季的年份可能不同 if torrent_meta.year and torrent_meta.year not in [year for year in mediainfo.season_years.values()]: continue else: # 电影年份,上下浮动1年 if torrent_meta.year not in [str(int(mediainfo.year) - 1), mediainfo.year, str(int(mediainfo.year) + 1)]: continue # 标题匹配标志 title_match = False # 比对标题和原语种标题 meta_name = StringUtils.clear_upper(torrent_meta.name) if meta_name in [ StringUtils.clear_upper(mediainfo.title), StringUtils.clear_upper(mediainfo.original_title) ]: title_match = True # 在副标题中判断是否存在标题与原语种标题 if not title_match and torrent_info.description: subtitle = re.split(r'[\s/|]+', torrent_info.description) if (StringUtils.is_chinese(mediainfo.title) and str(mediainfo.title) in subtitle) \ or (StringUtils.is_chinese(mediainfo.original_title) and str(mediainfo.original_title) in subtitle): title_match = True # 比对别名和译名 if not title_match: for name in mediainfo.names: if StringUtils.clear_upper(name) == meta_name: title_match = True break if not title_match: continue # 标题匹配成功 logger.info(f'{mediainfo.title_year} 通过名称匹配到资源:{torrent_info.site_name} - {torrent_info.title}') # 优先级过滤规则 if subscribe.best_version: priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules) else: priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules) result: List[TorrentInfo] = self.filter_torrents( rule_string=priority_rule, torrent_list=[torrent_info], mediainfo=torrent_mediainfo) if result is not None and not result: # 不符合过滤规则 logger.info(f"{torrent_info.title} 不匹配当前过滤规则") continue # 不在订阅站点范围的不处理 sub_sites = self.get_sub_sites(subscribe) if sub_sites and torrent_info.site not in sub_sites: logger.info(f"{torrent_info.site_name} - {torrent_info.title} 不符合订阅站点要求") continue # 如果是电视剧 if torrent_mediainfo.type == MediaType.TV: # 有多季的不要 if len(torrent_meta.season_list) > 1: logger.info(f'{torrent_info.title} 有多季,不处理') continue # 比对季 if torrent_meta.begin_season: if meta.begin_season != torrent_meta.begin_season: logger.info(f'{torrent_info.title} 季不匹配') continue elif meta.begin_season != 1: logger.info(f'{torrent_info.title} 季不匹配') continue # 非洗版 if not subscribe.best_version: # 不是缺失的剧集不要 if no_exists and no_exists.get(mediakey): # 缺失集 no_exists_info = no_exists.get(mediakey).get(subscribe.season) if no_exists_info: # 是否有交集 if no_exists_info.episodes and \ torrent_meta.episode_list and \ not set(no_exists_info.episodes).intersection( set(torrent_meta.episode_list) ): logger.info( f'{torrent_info.title} 对应剧集 {torrent_meta.episode_list} 未包含缺失的剧集' ) continue # 过滤掉已经下载的集数 if self.__check_subscribe_note(subscribe, torrent_meta.episode_list): logger.info(f'{torrent_info.title} 对应剧集 {torrent_meta.episode_list} 已下载过') continue else: # 洗版时,非整季不要 if meta.type == MediaType.TV: if torrent_meta.episode_list: logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 不是整季') continue # 过滤规则 if not self.torrenthelper.filter_torrent(torrent_info=torrent_info, filter_rule=filter_rule, mediainfo=torrent_mediainfo): continue # 洗版时,优先级小于已下载优先级的不要 if subscribe.best_version: if subscribe.current_priority \ and torrent_info.pri_order < subscribe.current_priority: logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于已下载优先级') continue # 匹配成功 logger.info(f'{mediainfo.title_year} 匹配成功:{torrent_info.title}') _match_context.append(context) if not _match_context: # 未匹配到资源 logger.info(f'{mediainfo.title_year} 未匹配到符合条件的资源') self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, lefts=no_exists) continue # 开始批量择优下载 logger.info(f'{mediainfo.title_year} 匹配完成,共匹配到{len(_match_context)}个资源') downloads, lefts = self.downloadchain.batch_download(contexts=_match_context, no_exists=no_exists, userid=subscribe.username, username=subscribe.username, save_path=subscribe.save_path) # 判断是否要完成订阅 self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, downloads=downloads, lefts=lefts) def check(self): """ 定时检查订阅,更新订阅信息 """ # 查询所有订阅 subscribes = self.subscribeoper.list() if not subscribes: # 没有订阅不运行 return # 遍历订阅 for subscribe in subscribes: logger.info(f'开始更新订阅元数据:{subscribe.name} ...') # 生成元数据 meta = MetaInfo(subscribe.name) meta.year = subscribe.year meta.begin_season = subscribe.season or None meta.type = MediaType(subscribe.type) # 识别媒体信息 mediainfo: MediaInfo = self.recognize_media(meta=meta, mtype=meta.type, tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid, cache=False) if not mediainfo: logger.warn( f'未识别到媒体信息,标题:{subscribe.name},tmdbid:{subscribe.tmdbid},doubanid:{subscribe.doubanid}') continue # 对于电视剧,获取当前季的总集数 episodes = mediainfo.seasons.get(subscribe.season) or [] if not subscribe.manual_total_episode and len(episodes): total_episode = len(episodes) lack_episode = subscribe.lack_episode + (total_episode - subscribe.total_episode) logger.info( f'订阅 {subscribe.name} 总集数变化,更新总集数为{total_episode},缺失集数为{lack_episode} ...') else: total_episode = subscribe.total_episode lack_episode = subscribe.lack_episode # 更新TMDB信息 self.subscribeoper.update(subscribe.id, { "name": mediainfo.title, "year": mediainfo.year, "vote": mediainfo.vote_average, "poster": mediainfo.get_poster_image(), "backdrop": mediainfo.get_backdrop_image(), "description": mediainfo.overview, "imdbid": mediainfo.imdb_id, "tvdbid": mediainfo.tvdb_id, "total_episode": total_episode, "lack_episode": lack_episode }) logger.info(f'{subscribe.name} 订阅元数据更新完成') def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]): """ 更新已下载集数到note字段 """ # 查询现有Note if not downloads: return note = [] if subscribe.note: note = json.loads(subscribe.note) for context in downloads: meta = context.meta_info mediainfo = context.media_info if mediainfo.type != MediaType.TV: continue if subscribe.tmdbid and mediainfo.tmdb_id \ and mediainfo.tmdb_id != subscribe.tmdbid: continue if subscribe.doubanid and mediainfo.douban_id \ and mediainfo.douban_id != subscribe.doubanid: continue episodes = meta.episode_list if not episodes: continue # 合并已下载集 note = list(set(note).union(set(episodes))) # 更新订阅 self.subscribeoper.update(subscribe.id, { "note": json.dumps(note) }) @staticmethod def __check_subscribe_note(subscribe: Subscribe, episodes: List[int]) -> bool: """ 检查当前集是否已下载过 """ if not subscribe.note: return False if not episodes: return False note = json.loads(subscribe.note) if set(episodes).issubset(set(note)): return True return False def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, NotExistMediaInfo]], subscribe: Subscribe, mediainfo: MediaInfo, update_date: bool = False): """ 更新订阅剩余集数 """ if not lefts: return mediakey = subscribe.tmdbid or subscribe.doubanid left_seasons = lefts.get(mediakey) 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} ...') if update_date: # 同时更新最后时间 self.subscribeoper.update(subscribe.id, { "lack_episode": lack_episode, "last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S') }) else: self.subscribeoper.update(subscribe.id, { "lack_episode": lack_episode }) def remote_list(self, channel: MessageChannel, userid: Union[str, int] = None): """ 查询订阅并发送消息 """ subscribes = self.subscribeoper.list() if not subscribes: self.post_message(Notification(channel=channel, title='没有任何订阅!', userid=userid)) return title = f"共有 {len(subscribes)} 个订阅,回复对应指令操作: " \ f"\n- 删除订阅:/subscribe_delete [id]" \ f"\n- 搜索订阅:/subscribe_search [id]" \ f"\n- 刷新订阅:/subscribe_refresh" messages = [] for subscribe in subscribes: if subscribe.type == MediaType.MOVIE.value: if subscribe.tmdbid: link = f"https://www.themoviedb.org/movie/{subscribe.tmdbid}" else: link = f"https://movie.douban.com/subject/{subscribe.doubanid}" messages.append(f"{subscribe.id}. [{subscribe.name}({subscribe.year})]({link})") else: if subscribe.tmdbid: link = f"https://www.themoviedb.org/tv/{subscribe.tmdbid}" else: link = f"https://movie.douban.com/subject/{subscribe.doubanid}" messages.append(f"{subscribe.id}. [{subscribe.name}({subscribe.year})]({link}) " f"第{subscribe.season}季 " f"_{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}" f"/{subscribe.total_episode}_") # 发送列表 self.post_message(Notification(channel=channel, title=title, text='\n'.join(messages), userid=userid)) def remote_delete(self, arg_str: str, channel: MessageChannel, userid: Union[str, int] = None): """ 删除订阅 """ if not arg_str: self.post_message(Notification(channel=channel, title="请输入正确的命令格式:/subscribe_delete [id]," "[id]为订阅编号", userid=userid)) return arg_strs = str(arg_str).split() for arg_str in arg_strs: arg_str = arg_str.strip() if not arg_str.isdigit(): continue subscribe_id = int(arg_str) subscribe = self.subscribeoper.get(subscribe_id) if not subscribe: self.post_message(Notification(channel=channel, title=f"订阅编号 {subscribe_id} 不存在!", userid=userid)) return # 删除订阅 self.subscribeoper.delete(subscribe_id) # 重新发送消息 self.remote_list(channel, userid) @staticmethod def __get_subscribe_no_exits(no_exists: Dict[Union[int, str], Dict[int, NotExistMediaInfo]], mediakey: Union[str, int], begin_season: int, total_episode: int, start_episode: int): """ 根据订阅开始集数和总集数,结合TMDB信息计算当前订阅的缺失集数 :param no_exists: 缺失季集列表 :param mediakey: TMDB ID或豆瓣ID :param begin_season: 开始季 :param total_episode: 订阅设定总集数 :param start_episode: 订阅设定开始集数 """ # 使用订阅的总集数和开始集数替换no_exists if no_exists \ and no_exists.get(mediakey) \ and (total_episode or start_episode): # 该季原缺失信息 no_exist_season = no_exists.get(mediakey).get(begin_season) if no_exist_season: # 原集列表 episode_list = no_exist_season.episodes # 原总集数 total = no_exist_season.total_episode # 原开始集数 start = no_exist_season.start_episode # 更新剧集列表、开始集数、总集数 if not episode_list: # 整季缺失 episodes = [] start_episode = start_episode or start total_episode = total_episode or total else: # 部分缺失 if not start_episode \ and not total_episode: # 无需调整 return no_exists if not start_episode: # 没有自定义开始集 start_episode = start if not total_episode: # 没有自定义总集数 total_episode = total # 新的集列表 new_episodes = list(range(max(start_episode, start), total_episode + 1)) # 与原集列表取交集 episodes = list(set(episode_list).intersection(set(new_episodes))) # 更新集合 no_exists[mediakey][begin_season] = NotExistMediaInfo( season=begin_season, episodes=episodes, total_episode=total_episode, start_episode=start_episode ) return no_exists