import base64
import copy
import json
import re
import time
from pathlib import Path
from typing import List, Optional, Tuple, Set, Dict, Union

from app import schemas
from app.chain import ChainBase
from app.core.cache import FileCache
from app.core.config import settings, global_vars
from app.core.context import MediaInfo, TorrentInfo, Context
from app.core.event import eventmanager, Event
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.mediaserver_oper import MediaServerOper
from app.helper.directory import DirectoryHelper
from app.helper.torrent import TorrentHelper
from app.log import logger
from app.schemas import ExistMediaInfo, FileURI, NotExistMediaInfo, DownloadingTorrent, Notification, ResourceSelectionEventData, \
    ResourceDownloadEventData
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType, ContentType, \
    ChainEventType
from app.utils.http import RequestUtils
from app.utils.string import StringUtils


class DownloadChain(ChainBase):
    """
    下载处理链
    """

    def download_torrent(self, torrent: TorrentInfo,
                         channel: MessageChannel = None,
                         source: Optional[str] = None,
                         userid: Union[str, int] = None
                         ) -> Tuple[Optional[Union[str, bytes]], str, list]:
        """
        下载种子文件，如果是磁力链，会返回磁力链接本身
        :return: 种子内容，种子目录名，种子文件清单
        """

        def __get_redict_url(url: str, ua: Optional[str] = None, cookie: Optional[str] = None) -> Optional[str]:
            """
            获取下载链接， url格式：[base64]url
            """
            # 获取[]中的内容
            m = re.search(r"\[(.*)](.*)", url)
            if m:
                # 参数
                base64_str = m.group(1)
                # URL
                url = m.group(2)
                if not base64_str:
                    return url
                # 解码参数
                req_str = base64.b64decode(base64_str.encode('utf-8')).decode('utf-8')
                req_params: Dict[str, dict] = json.loads(req_str)
                # 是否使用cookie
                if not req_params.get('cookie'):
                    cookie = None
                # 代理
                proxy = req_params.get('proxy')
                # 请求头
                if req_params.get('header'):
                    headers = req_params.get('header')
                else:
                    headers = None
                if req_params.get('method') == 'get':
                    # GET请求
                    res = RequestUtils(
                        ua=ua,
                        cookies=cookie,
                        headers=headers,
                        proxies=settings.PROXY if proxy else None
                    ).get_res(url, params=req_params.get('params'))
                else:
                    # POST请求
                    res = RequestUtils(
                        ua=ua,
                        cookies=cookie,
                        headers=headers,
                        proxies=settings.PROXY if proxy else None
                    ).post_res(url, params=req_params.get('params'))
                if not res:
                    return None
                if not req_params.get('result'):
                    return res.text
                else:
                    data = res.json()
                    for key in str(req_params.get('result')).split("."):
                        data = data.get(key)
                        if not data:
                            return None
                    logger.info(f"获取到下载地址：{data}")
                    return data
            return None

        # 获取下载链接
        if not torrent.enclosure:
            return None, "", []
        if torrent.enclosure.startswith("magnet:"):
            return torrent.enclosure, "", []
        # Cookie
        site_cookie = torrent.site_cookie
        if torrent.enclosure.startswith("["):
            # 需要解码获取下载地址
            torrent_url = __get_redict_url(url=torrent.enclosure,
                                           ua=torrent.site_ua,
                                           cookie=site_cookie)
            # 涉及解析地址的不使用Cookie下载种子，否则MT会出错
            site_cookie = None
        else:
            torrent_url = torrent.enclosure
        if not torrent_url:
            logger.error(f"{torrent.title} 无法获取下载地址：{torrent.enclosure}！")
            return None, "", []
        # 下载种子文件
        _, content, download_folder, files, error_msg = TorrentHelper().download_torrent(
            url=torrent_url,
            cookie=site_cookie,
            ua=torrent.site_ua or settings.USER_AGENT,
            proxy=torrent.site_proxy)

        if isinstance(content, str):
            # 磁力链
            return content, "", []

        if not content:
            logger.error(f"下载种子文件失败：{torrent.title} - {torrent_url}")
            self.post_message(Notification(
                channel=channel,
                source=source if channel else None,
                mtype=NotificationType.Manual,
                title=f"{torrent.title} 种子下载失败！",
                text=f"错误信息：{error_msg}\n站点：{torrent.site_name}",
                userid=userid))
            return None, "", []

        # 返回 种子文件路径，种子目录名，种子文件清单
        return content, download_folder, files

    def download_single(self, context: Context,
                        torrent_file: Path = None,
                        torrent_content: Optional[Union[str, bytes]] = None,
                        episodes: Set[int] = None,
                        channel: MessageChannel = None,
                        source: Optional[str] = None,
                        downloader: Optional[str] = None,
                        save_path: Optional[str] = None,
                        userid: Union[str, int] = None,
                        username: Optional[str] = None,
                        label: Optional[str] = None,
                        return_detail: bool = False) -> Union[Optional[str], Tuple[Optional[str], Optional[str]]]:
        """
        下载及发送通知
        :param context: 资源上下文
        :param torrent_file: 种子文件路径
        :param torrent_content: 种子内容（磁力链或种子文件内容）
        :param episodes: 需要下载的集数
        :param channel: 通知渠道
        :param source: 来源（消息通知、Subscribe、Manual等）
        :param downloader: 下载器
        :param save_path: 保存路径, 支持<storage>:<path>, 如rclone:/MP, smb:/server/share/Movies等
        :param userid: 用户ID
        :param username: 调用下载的用户名/插件名
        :param label: 自定义标签
        :param return_detail: 是否返回详细结果；False 时返回下载任务 hash 或 None，True 时返回 (hash, error_msg)
        :return: return_detail=False 时返回下载任务 hash 或 None；return_detail=True 时返回 (hash, error_msg)
        """
        _torrent = context.torrent_info
        _media = context.media_info
        _meta = context.meta_info
        _site_downloader = _torrent.site_downloader

        # 发送资源下载事件，允许外部拦截下载
        event_data = ResourceDownloadEventData(
            context=context,
            episodes=episodes or context.meta_info.episode_list,
            channel=channel,
            origin=source,
            downloader=downloader,
            options={
                "save_path": save_path,
                "userid": userid,
                "username": username,
                "media_category": _media.category
            }
        )
        # 触发资源下载事件
        event = eventmanager.send_event(ChainEventType.ResourceDownload, event_data)
        if event and event.event_data:
            event_data: ResourceDownloadEventData = event.event_data
            # 如果事件被取消，跳过资源下载
            if event_data.cancel:
                logger.debug(
                    f"Resource download canceled by event: {event_data.source},"
                    f"Reason: {event_data.reason}")
                return (None, "下载被事件取消") if return_detail else None
            # 如果事件修改了下载路径，使用新路径
            if event_data.options and event_data.options.get("save_path"):
                save_path = event_data.options.get("save_path")

        # 补充完整的media数据
        if not _media.genre_ids:
            new_media = self.recognize_media(mtype=_media.type, tmdbid=_media.tmdb_id,
                                             doubanid=_media.douban_id, bangumiid=_media.bangumi_id,
                                             episode_group=_media.episode_group)
            if new_media:
                _media = new_media

        # 实际下载的集数
        download_episodes = StringUtils.format_ep(list(episodes)) if episodes else None
        _folder_name = ""
        if not torrent_file and not torrent_content:
            # 下载种子文件，得到的可能是文件也可能是磁力链
            torrent_content, _folder_name, _file_list = self.download_torrent(_torrent,
                                                                              channel=channel,
                                                                              source=source,
                                                                              userid=userid)
        elif torrent_file:
            if torrent_file.exists():
                torrent_content = torrent_file.read_bytes()
            else:
                # 缓存处理器
                cache_backend = FileCache()
                # 读取缓存的种子文件
                torrent_content = cache_backend.get(torrent_file.as_posix(), region="torrents")

        if not torrent_content:
            return (None, "下载种子内容为空") if return_detail else None

        # 获取种子文件的文件夹名和文件清单
        _folder_name, _file_list = TorrentHelper().get_fileinfo_from_torrent_content(torrent_content)

        storage = 'local'
        # 下载目录
        if save_path:
            download_dir = Path(save_path)
        else:
            # 根据媒体信息查询下载目录配置
            dir_info = DirectoryHelper().get_dir(_media, include_unsorted=True)
            storage = dir_info.storage if dir_info else storage
            # 拼装子目录
            if dir_info:
                # 一级目录
                if not dir_info.media_type and dir_info.download_type_folder:
                    # 一级自动分类
                    download_dir = Path(dir_info.download_path) / _media.type.value
                else:
                    # 一级不分类
                    download_dir = Path(dir_info.download_path)

                # 二级目录
                if not dir_info.media_category and dir_info.download_category_folder and _media and _media.category:
                    # 二级自动分类
                    download_dir = download_dir / _media.category
            else:
                # 未找到下载目录，且没有自定义下载目录
                logger.error(f"未找到下载目录：{_media.type.value} {_media.title_year}")
                self.messagehelper.put(f"{_media.type.value} {_media.title_year} 未找到下载目录！",
                                       title="下载失败", role="system")
                return (None, "未找到下载目录") if return_detail else None
            fileURI = FileURI(storage=storage, path=download_dir.as_posix())
            download_dir = Path(fileURI.uri)

        # 添加下载
        result: Optional[tuple] = self.download(content=torrent_content,
                                                cookie=_torrent.site_cookie,
                                                episodes=episodes,
                                                download_dir=download_dir,
                                                category=_media.category,
                                                label=label,
                                                downloader=downloader or _site_downloader)
        if result:
            _downloader, _hash, _layout, error_msg = result
        else:
            _downloader, _hash, _layout, error_msg = None, None, None, "未找到下载器"

        if _hash:
            # `不创建子文件夹` 或 `不存在子文件夹`
            if _layout == "NoSubfolder" or not _folder_name:
                # 下载路径记录至文件
                download_path = download_dir / _file_list[0] if _file_list else download_dir
            # 原始布局
            elif _folder_name:
                download_path = download_dir / _folder_name
            # 创建子文件夹
            else:
                download_path = download_dir / Path(_file_list[0]).stem if _file_list else download_dir
            # 文件保存路径
            _save_path = download_dir if _layout == "NoSubfolder" or not _folder_name else download_path

            # 登记下载记录
            downloadhis = DownloadHistoryOper()
            downloadhis.add(
                path=download_path.as_posix(),
                type=_media.type.value,
                title=_media.title,
                year=_media.year,
                tmdbid=_media.tmdb_id,
                imdbid=_media.imdb_id,
                tvdbid=_media.tvdb_id,
                doubanid=_media.douban_id,
                seasons=_meta.season,
                episodes=download_episodes or _meta.episode,
                image=_media.get_backdrop_image(),
                downloader=_downloader,
                download_hash=_hash,
                torrent_name=_torrent.title,
                torrent_description=_torrent.description,
                torrent_site=_torrent.site_name,
                userid=userid,
                username=username,
                channel=channel.value if channel else None,
                date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
                media_category=_media.category,
                episode_group=_media.episode_group,
                note={"source": source}
            )

            # 登记下载文件
            files_to_add = []
            for file in _file_list:
                if episodes:
                    # 识别文件集
                    file_meta = MetaInfo(Path(file).stem)
                    if not file_meta.begin_episode \
                            or file_meta.begin_episode not in episodes:
                        continue
                # 只处理音视频、字幕格式
                media_exts = settings.RMT_MEDIAEXT + settings.RMT_SUBEXT + settings.RMT_AUDIOEXT
                if not Path(file).suffix \
                        or Path(file).suffix.lower() not in media_exts:
                    continue
                files_to_add.append({
                    "download_hash": _hash,
                    "downloader": _downloader,
                    "fullpath": (_save_path / file).as_posix(),
                    "savepath": _save_path.as_posix(),
                    "filepath": file,
                    "torrentname": _meta.org_string,
                })
            if files_to_add:
                downloadhis.add_files(files_to_add)

            # 下载成功发送消息
            self.post_message(
                Notification(
                    channel=channel,
                    source=source if channel else None,
                    mtype=NotificationType.Download,
                    ctype=ContentType.DownloadAdded,
                    image=_media.get_message_image(),
                    link=settings.MP_DOMAIN('/#/downloading'),
                    userid=userid,
                    username=username
                ),
                meta=_meta,
                mediainfo=_media,
                torrentinfo=_torrent,
                download_episodes=download_episodes,
                username=username,
            )
            # 下载成功后处理
            self.download_added(context=context, download_dir=download_dir, torrent_content=torrent_content)
            # 广播事件
            self.eventmanager.send_event(EventType.DownloadAdded, {
                "hash": _hash,
                "context": context,
                "username": username,
                "downloader": _downloader,
                "episodes": episodes or _meta.episode_list,
                "source": source
            })
        else:
            # 下载失败
            logger.error(f"{_media.title_year} 添加下载任务失败："
                         f"{_torrent.title} - {_torrent.enclosure}，{error_msg}")
            # 只发送给对应渠道和用户
            self.post_message(Notification(
                channel=channel,
                source=source if channel else None,
                mtype=NotificationType.Manual,
                title="添加下载任务失败：%s %s"
                      % (_media.title_year, _meta.season_episode),
                text=f"站点：{_torrent.site_name}\n"
                     f"种子名称：{_meta.org_string}\n"
                     f"错误信息：{error_msg}",
                image=_media.get_message_image(),
                userid=userid))
        if return_detail:
            return _hash, error_msg
        return _hash

    def batch_download(self,
                       contexts: List[Context],
                       no_exists: Dict[Union[int, str], Dict[int, NotExistMediaInfo]] = None,
                       save_path: Optional[str] = None,
                       channel: MessageChannel = None,
                       source: Optional[str] = None,
                       userid: Optional[str] = None,
                       username: Optional[str] = None,
                       downloader: Optional[str] = None
                       ) -> Tuple[List[Context], Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
        """
        根据缺失数据，自动种子列表中组合择优下载
        :param contexts:  资源上下文列表
        :param no_exists:  缺失的剧集信息
        :param save_path:  保存路径, 支持<storage>:<path>, 如rclone:/MP, smb:/server/share/Movies等
        :param channel:  通知渠道
        :param source:  来源（消息通知、订阅、手工下载等）
        :param userid:  用户ID
        :param username: 调用下载的用户名/插件名
        :param downloader: 下载器
        :return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id/douban_id] = {season: NotExistMediaInfo}
        """
        # 已下载的项目
        downloaded_list: List[Context] = []

        def __update_seasons(_mid: Union[int, str], _need: list, _current: list) -> list:
            """
            更新need_tvs季数，返回剩余季数
            :param _mid: TMDBID
            :param _need: 需要下载的季数
            :param _current: 已经下载的季数
            """
            # 剩余季数
            need = list(set(_need).difference(set(_current)))
            # 清除已下载的季信息
            seas = copy.deepcopy(no_exists.get(_mid))
            if seas:
                for _sea in list(seas):
                    if _sea not in need:
                        no_exists[_mid].pop(_sea)
                    if not no_exists.get(_mid) and no_exists.get(_mid) is not None:
                        no_exists.pop(_mid)
                        break
            return need

        def __update_episodes(_mid: Union[int, str], _sea: int, _need: list, _current: set) -> list:
            """
            更新need_tvs集数，返回剩余集数
            :param _mid: TMDBID
            :param _sea: 季数
            :param _need: 需要下载的集数
            :param _current: 已经下载的集数
            """
            # 剩余集数
            need = list(set(_need).difference(set(_current)))
            if need:
                not_exist = no_exists[_mid][_sea]
                no_exists[_mid][_sea] = NotExistMediaInfo(
                    season=not_exist.season,
                    episodes=need,
                    total_episode=not_exist.total_episode,
                    start_episode=not_exist.start_episode
                )
            else:
                no_exists[_mid].pop(_sea)
                if not no_exists.get(_mid) and no_exists.get(_mid) is not None:
                    no_exists.pop(_mid)
            return need

        def __get_season_episodes(_mid: Union[int, str], season: int) -> int:
            """
            获取需要的季的集数
            """
            if not no_exists.get(_mid):
                return 9999
            no_exist = no_exists.get(_mid)
            if not no_exist.get(season):
                return 9999
            return no_exist[season].total_episode

        # 发送资源选择事件，允许外部修改上下文数据
        logger.debug(f"Initial contexts: {len(contexts)} items, Downloader: {downloader}")
        event_data = ResourceSelectionEventData(
            contexts=contexts,
            downloader=downloader,
            origin=source
        )
        event = eventmanager.send_event(ChainEventType.ResourceSelection, event_data)
        # 如果事件修改了上下文数据，使用更新后的数据
        if event and event.event_data:
            event_data: ResourceSelectionEventData = event.event_data
            if event_data.updated and event_data.updated_contexts is not None:
                logger.debug(f"Contexts updated by event: "
                             f"{len(event_data.updated_contexts)} items (source: {event_data.source})")
                contexts = event_data.updated_contexts

        # 分组排序
        contexts = TorrentHelper().sort_group_torrents(contexts)

        # 如果是电影，直接下载
        for context in contexts:
            if global_vars.is_system_stopped:
                break
            if context.media_info.type == MediaType.MOVIE:
                logger.info(f"开始下载电影 {context.torrent_info.title} ...")
                if self.download_single(context, save_path=save_path, channel=channel,
                                        source=source, userid=userid, username=username,
                                        downloader=downloader):
                    # 下载成功
                    logger.info(f"{context.torrent_info.title} 添加下载成功")
                    downloaded_list.append(context)

        # 电视剧整季匹配
        if no_exists:
            logger.info(f"开始匹配电视剧整季：{no_exists}")
            # 先把整季缺失的拿出来，看是否刚好有所有季都满足的种子 {tmdbid: [seasons]}
            need_seasons: Dict[int, list] = {}
            for need_mid, need_tv in no_exists.items():
                for tv in need_tv.values():
                    if not tv:
                        continue
                    # 季列表为空的，代表全季缺失
                    if not tv.episodes:
                        if not need_seasons.get(need_mid):
                            need_seasons[need_mid] = []
                        need_seasons[need_mid].append(tv.season or 1)
            logger.info(f"缺失整季：{need_seasons}")
            # 查找整季包含的种子，只处理整季没集的种子或者是集数超过季的种子
            for need_mid, need_season in need_seasons.items():
                # 循环种子
                for context in contexts:
                    if global_vars.is_system_stopped:
                        break
                    # 媒体信息
                    media = context.media_info
                    # 识别元数据
                    meta = context.meta_info
                    # 种子信息
                    torrent = context.torrent_info
                    # 排除电视剧
                    if media.type != MediaType.TV:
                        continue
                    # 种子的季清单
                    torrent_season = meta.season_list
                    # 没有季的默认为第1季
                    if not torrent_season:
                        torrent_season = [1]
                    # 种子有集的不要
                    if meta.episode_list:
                        continue
                    # 匹配TMDBID
                    if need_mid == media.tmdb_id or need_mid == media.douban_id:
                        # 不重复添加
                        if context in downloaded_list:
                            continue
                        # 种子季是需要季或者子集
                        if set(torrent_season).issubset(set(need_season)):
                            if len(torrent_season) == 1:
                                # 只有一季的可能是命名错误，需要打开种子鉴别，只有实际集数大于等于总集数才下载
                                logger.info(f"开始下载种子 {torrent.title} ...")
                                content, _, torrent_files = self.download_torrent(torrent)
                                if not content:
                                    logger.warn(f"{torrent.title} 种子下载失败！")
                                    continue
                                if isinstance(content, str):
                                    logger.warn(f"{meta.org_string} 下载地址是磁力链，无法确定种子文件集数")
                                    continue
                                torrent_episodes = TorrentHelper().get_torrent_episodes(torrent_files)
                                logger.info(f"{meta.org_string} 解析种子文件集数为 {torrent_episodes}")
                                if not torrent_episodes:
                                    continue
                                # 更新集数范围
                                begin_ep = min(torrent_episodes)
                                end_ep = max(torrent_episodes)
                                meta.set_episodes(begin=begin_ep, end=end_ep)
                                # 需要总集数
                                need_total = __get_season_episodes(need_mid, torrent_season[0])
                                if len(torrent_episodes) < need_total:
                                    logger.info(
                                        f"{meta.org_string} 解析文件集数发现不是完整合集，先放弃这个种子")
                                    continue
                                else:
                                    # 下载
                                    logger.info(f"开始下载 {torrent.title} ...")
                                    download_id = self.download_single(
                                        context=context,
                                        torrent_content=content,
                                        save_path=save_path,
                                        channel=channel,
                                        source=source,
                                        userid=userid,
                                        username=username,
                                        downloader=downloader
                                    )
                            else:
                                # 下载
                                logger.info(f"开始下载 {torrent.title} ...")
                                download_id = self.download_single(context, save_path=save_path,
                                                                   channel=channel, source=source,
                                                                   userid=userid, username=username,
                                                                   downloader=downloader)

                            if download_id:
                                # 下载成功
                                logger.info(f"{torrent.title} 添加下载成功")
                                downloaded_list.append(context)
                                # 更新仍需季集
                                need_season = __update_seasons(_mid=need_mid,
                                                               _need=need_season,
                                                               _current=torrent_season)
                                logger.info(f"{need_mid} 剩余需要季：{need_season}")
                                if not need_season:
                                    # 全部下载完成
                                    break
        # 电视剧季内的集匹配
        if no_exists:
            logger.info(f"开始电视剧完整集匹配：{no_exists}")
            # TMDBID列表
            need_tv_list = list(no_exists)
            for need_mid in need_tv_list:
                # dict[season, [NotExistMediaInfo]]
                need_tv = no_exists.get(need_mid)
                if not need_tv:
                    continue
                need_tv_copy = copy.deepcopy(no_exists.get(need_mid))
                # 循环每一季
                for sea, tv in need_tv_copy.items():
                    # 当前需要季
                    need_season = sea
                    # 当前需要集
                    need_episodes = tv.episodes
                    # TMDB总集数
                    total_episode = tv.total_episode
                    # 需要开始集
                    start_episode = tv.start_episode or 1
                    # 缺失整季的转化为缺失集进行比较
                    if not need_episodes:
                        need_episodes = list(range(start_episode, total_episode + 1))
                    # 循环种子
                    for context in contexts:
                        if global_vars.is_system_stopped:
                            break
                        # 媒体信息
                        media = context.media_info
                        # 识别元数据
                        meta = context.meta_info
                        # 非剧集不处理
                        if media.type != MediaType.TV:
                            continue
                        # 匹配TMDB
                        if media.tmdb_id == need_mid or media.douban_id == need_mid:
                            # 不重复添加
                            if context in downloaded_list:
                                continue
                            # 种子季
                            torrent_season = meta.season_list
                            # 只处理单季含集的种子
                            if len(torrent_season) != 1 or torrent_season[0] != need_season:
                                continue
                            # 种子集列表
                            torrent_episodes = set(meta.episode_list)
                            # 整季的不处理
                            if not torrent_episodes:
                                continue
                            # 为需要集的子集则下载
                            if torrent_episodes.issubset(set(need_episodes)):
                                # 下载
                                logger.info(f"开始下载 {meta.title} ...")
                                download_id = self.download_single(context, save_path=save_path,
                                                                   channel=channel, source=source,
                                                                   userid=userid, username=username,
                                                                   downloader=downloader)
                                if download_id:
                                    # 下载成功
                                    logger.info(f"{meta.title} 添加下载成功")
                                    downloaded_list.append(context)
                                    # 更新仍需集数
                                    need_episodes = __update_episodes(_mid=need_mid,
                                                                      _need=need_episodes,
                                                                      _sea=need_season,
                                                                      _current=torrent_episodes)
                                    logger.info(f"季 {need_season} 剩余需要集：{need_episodes}")

        # 仍然缺失的剧集，从整季中选择需要的集数文件下载，仅支持QB和TR
        if no_exists:
            logger.info(f"开始电视剧多集拆包匹配：{no_exists}")
            # TMDBID列表
            no_exists_list = list(no_exists)
            for need_mid in no_exists_list:
                # dict[season, [NotExistMediaInfo]]
                need_tv = no_exists.get(need_mid)
                if not need_tv:
                    continue
                # 需要季列表
                need_tv_list = list(need_tv)
                # 循环需要季
                for sea in need_tv_list:
                    # NotExistMediaInfo
                    tv = need_tv.get(sea)
                    # 当前需要季
                    need_season = sea
                    # 当前需要集
                    need_episodes = tv.episodes
                    # 没有集的不处理
                    if not need_episodes:
                        continue
                    # 循环种子
                    for context in contexts:
                        if global_vars.is_system_stopped:
                            break
                        # 媒体信息
                        media = context.media_info
                        # 识别元数据
                        meta = context.meta_info
                        # 种子信息
                        torrent = context.torrent_info
                        # 非剧集不处理
                        if media.type != MediaType.TV:
                            continue
                        # 不重复添加
                        if context in downloaded_list:
                            continue
                        # 没有需要集后退出
                        if not need_episodes:
                            break
                        # 选中一个单季整季的或单季包括需要的所有集的
                        if (media.tmdb_id == need_mid or media.douban_id == need_mid) \
                                and (not meta.episode_list
                                     or set(meta.episode_list).intersection(set(need_episodes))) \
                                and len(meta.season_list) == 1 \
                                and meta.season_list[0] == need_season:
                            # 检查种子看是否有需要的集
                            logger.info(f"开始下载种子 {torrent.title} ...")
                            content, _, torrent_files = self.download_torrent(torrent)
                            if not content:
                                logger.info(f"{torrent.title} 种子下载失败！")
                                continue
                            if isinstance(content, str):
                                logger.warn(f"{meta.org_string} 下载地址是磁力链，无法解析种子文件集数")
                                continue
                            # 种子全部集
                            torrent_episodes = TorrentHelper().get_torrent_episodes(torrent_files)
                            logger.info(f"{torrent.site_name} - {meta.org_string} 解析种子文件集数：{torrent_episodes}")
                            # 选中的集
                            selected_episodes = set(torrent_episodes).intersection(set(need_episodes))
                            if not selected_episodes:
                                logger.info(f"{torrent.site_name} - {torrent.title} 没有需要的集，跳过...")
                                continue
                            logger.info(f"{torrent.site_name} - {torrent.title} 选中集数：{selected_episodes}")
                            # 添加下载
                            logger.info(f"开始下载 {torrent.title} ...")
                            download_id = self.download_single(
                                context=context,
                                torrent_content=content,
                                episodes=selected_episodes,
                                save_path=save_path,
                                channel=channel,
                                source=source,
                                userid=userid,
                                username=username,
                                downloader=downloader
                            )
                            if not download_id:
                                continue
                            # 下载成功
                            logger.info(f"{torrent.title} 添加下载成功")
                            downloaded_list.append(context)
                            # 更新种子集数范围
                            begin_ep = min(torrent_episodes)
                            end_ep = max(torrent_episodes)
                            meta.set_episodes(begin=begin_ep, end=end_ep)
                            # 更新仍需集数
                            need_episodes = __update_episodes(_mid=need_mid,
                                                              _need=need_episodes,
                                                              _sea=need_season,
                                                              _current=selected_episodes)
                            logger.info(f"季 {need_season} 剩余需要集：{need_episodes}")

        # 返回下载的资源，剩下没下完的
        logger.info(f"成功下载种子数：{len(downloaded_list)}，剩余未下载的剧集：{no_exists}")
        return downloaded_list, no_exists

    def get_no_exists_info(self, meta: MetaBase,
                           mediainfo: MediaInfo,
                           no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
                           totals: Dict[int, int] = None
                           ) -> Tuple[bool, Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
        """
        检查媒体库，查询是否存在，对于剧集同时返回不存在的季集信息
        :param meta: 元数据
        :param mediainfo: 已识别的媒体信息
        :param no_exists: 在调用该方法前已经存储的不存在的季集信息，有传入时该函数搜索的内容将会叠加后输出
        :param totals: 电视剧每季的总集数
        :return: 当前媒体是否缺失，各标题总的季集和缺失的季集
        """

        def __append_no_exists(_season: int, _episodes: list, _total: int, _start: int):
            """
            添加不存在的季集信息
            {tmdbid: [
                "season": int,
                "episodes": list,
                "total_episode": int,
                "start_episode": int
            ]}
            """
            mediakey = mediainfo.tmdb_id or mediainfo.douban_id
            if not no_exists.get(mediakey):
                no_exists[mediakey] = {
                    _season: NotExistMediaInfo(
                        season=_season,
                        episodes=_episodes,
                        total_episode=_total,
                        start_episode=_start
                    )
                }
            else:
                no_exists[mediakey][_season] = NotExistMediaInfo(
                    season=_season,
                    episodes=_episodes,
                    total_episode=_total,
                    start_episode=_start
                )

        if not no_exists:
            no_exists = {}

        if not totals:
            totals = {}

        mediaserver = MediaServerOper()
        if mediainfo.type == MediaType.MOVIE:
            # 电影
            itemid = mediaserver.get_item_id(mtype=mediainfo.type.value,
                                             title=mediainfo.title,
                                             tmdbid=mediainfo.tmdb_id)
            exists_movies: Optional[ExistMediaInfo] = self.media_exists(mediainfo=mediainfo, itemid=itemid)
            if exists_movies:
                logger.info(f"媒体库中已存在电影：{mediainfo.title_year}")
                return True, {}
            return False, {}
        else:
            if not mediainfo.seasons:
                # 补充媒体信息
                mediainfo: MediaInfo = self.recognize_media(mtype=mediainfo.type,
                                                            tmdbid=mediainfo.tmdb_id,
                                                            doubanid=mediainfo.douban_id,
                                                            episode_group=mediainfo.episode_group)
                if not mediainfo:
                    logger.error(f"媒体信息识别失败！")
                    return False, {}
                if not mediainfo.seasons:
                    logger.error(f"媒体信息中没有季集信息：{mediainfo.title_year}")
                    return False, {}
            # 电视剧
            itemid = mediaserver.get_item_id(mtype=mediainfo.type.value,
                                             title=mediainfo.title,
                                             tmdbid=mediainfo.tmdb_id,
                                             season=mediainfo.season)
            # 媒体库已存在的剧集
            exists_tvs: Optional[ExistMediaInfo] = self.media_exists(mediainfo=mediainfo, itemid=itemid)
            if not exists_tvs:
                # 所有季集均缺失
                for season, episodes in mediainfo.seasons.items():
                    if not episodes:
                        continue
                    # 全季不存在
                    if meta.sea \
                            and season not in meta.season_list:
                        continue
                    # 总集数
                    total_ep = totals.get(season) or len(episodes)
                    __append_no_exists(_season=season, _episodes=[],
                                       _total=total_ep, _start=min(episodes))
                return False, no_exists
            else:
                # 存在一些，检查每季缺失的季集
                for season, episodes in mediainfo.seasons.items():
                    if meta.sea \
                            and season not in meta.season_list:
                        continue
                    if not episodes:
                        continue
                    # 该季总集数
                    season_total = totals.get(season) or len(episodes)
                    # 该季已存在的集
                    exist_episodes = exists_tvs.seasons.get(season)
                    if exist_episodes:
                        # 已存在取差集
                        if totals.get(season):
                            # 按总集数计算缺失集（开始集为TMDB中的最小集）
                            lack_episodes = list(set(range(min(episodes),
                                                           season_total + min(episodes))
                                                     ).difference(set(exist_episodes)))
                        else:
                            # 按TMDB集数计算缺失集
                            lack_episodes = list(set(episodes).difference(set(exist_episodes)))
                        if not lack_episodes:
                            # 全部集存在
                            continue
                        # 添加不存在的季集信息
                        __append_no_exists(_season=season, _episodes=lack_episodes,
                                           _total=season_total, _start=min(lack_episodes))
                    else:
                        # 全季不存在
                        __append_no_exists(_season=season, _episodes=[],
                                           _total=season_total, _start=min(episodes))
            # 存在不完整的剧集
            if no_exists:
                logger.debug(f"媒体库中已存在部分剧集，缺失：{no_exists}")
                return False, no_exists
            # 全部存在
            return True, no_exists

    def remote_downloading(self, channel: MessageChannel, userid: Union[str, int] = None, source: Optional[str] = None):
        """
        查询正在下载的任务，并发送消息
        """
        torrents = self.list_torrents(status=TorrentStatus.DOWNLOADING)
        if not torrents:
            self.post_message(Notification(
                channel=channel,
                source=source,
                mtype=NotificationType.Download,
                title="没有正在下载的任务！",
                userid=userid,
                link=settings.MP_DOMAIN('#/downloading')
            ))
            return
        # 发送消息
        title = f"共 {len(torrents)} 个任务正在下载："
        messages = []
        index = 1
        for torrent in torrents:
            messages.append(f"{index}. {torrent.title} "
                            f"{StringUtils.str_filesize(torrent.size)} "
                            f"{round(torrent.progress, 1)}%")
            index += 1
        self.post_message(Notification(
            channel=channel,
            source=source,
            mtype=NotificationType.Download,
            title=title,
            text="\n".join(messages),
            userid=userid,
            link=settings.MP_DOMAIN('#/downloading')
        ))

    def downloading(self, name: Optional[str] = None) -> List[DownloadingTorrent]:
        """
        查询正在下载的任务
        """
        torrents = self.list_torrents(downloader=name, status=TorrentStatus.DOWNLOADING)
        if not torrents:
            return []
        ret_torrents = []
        for torrent in torrents:
            history = DownloadHistoryOper().get_by_hash(torrent.hash)
            if history:
                # 媒体信息
                torrent.media = {
                    "tmdbid": history.tmdbid,
                    "type": history.type,
                    "title": history.title,
                    "season": history.seasons,
                    "episode": history.episodes,
                    "image": history.image,
                }
                # 下载用户
                torrent.userid = history.userid
                torrent.username = history.username
            ret_torrents.append(torrent)
        return ret_torrents

    def set_downloading(self, hash_str, oper: str, name: Optional[str] = None) -> bool:
        """
        控制下载任务 start/stop
        """
        if oper == "start":
            return self.start_torrents(hashs=[hash_str], downloader=name)
        elif oper == "stop":
            return self.stop_torrents(hashs=[hash_str], downloader=name)
        return False

    def remove_downloading(self, hash_str: str, name: Optional[str] = None) -> bool:
        """
        删除下载任务
        """
        return self.remove_torrents(hashs=[hash_str], downloader=name)

    @eventmanager.register(EventType.DownloadFileDeleted)
    def download_file_deleted(self, event: Event):
        """
        下载文件删除时，同步删除下载任务
        """
        if not event:
            return
        hash_str = event.event_data.get("hash")
        if not hash_str:
            return
        logger.warn(f"检测到下载源文件被删除，删除下载任务（不含文件）：{hash_str}")
        # 先查询种子
        torrents: List[schemas.TransferTorrent] = self.list_torrents(hashs=[hash_str])
        if torrents:
            self.remove_torrents(hashs=[hash_str], delete_file=False)
            # 发出下载任务删除事件，如需处理辅种，可监听该事件
            self.eventmanager.send_event(EventType.DownloadDeleted, {
                "hash": hash_str,
                    "torrents": [torrent.model_dump() for torrent in torrents]
            })
        else:
            logger.info(f"没有在下载器中查询到 {hash_str} 对应的下载任务")
