mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 20:17:13 +08:00
1573 lines
53 KiB
Python
1573 lines
53 KiB
Python
import math
|
||
import re
|
||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||
|
||
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.subscribe import SubscribeChain
|
||
from app.application.directory import DirectoryHelper
|
||
from app.application.messaging.media import (
|
||
PendingMediaInteraction,
|
||
media_interaction_manager,
|
||
)
|
||
from app.application.torrent import TorrentHelper
|
||
from app.application.chain.data import get_chain_user_port
|
||
from app.domain import episode as episode_rules
|
||
from app.domain import title as title_rules
|
||
from app.domain.context import Context, MediaInfo
|
||
from app.domain.meta.metabase import MetaBase
|
||
from app.foundation import url as url_tools
|
||
from app.runtime.log import logger
|
||
from app.schemas.download import DownloadDirectory
|
||
from app.schemas.file import FileURI
|
||
from app.schemas.mediaserver import NotExistMediaInfo
|
||
from app.schemas.message import Message
|
||
from app.schemas.media import build_media_key, resolve_media_identity
|
||
from app.schemas.notification import ChannelCapabilityManager
|
||
from app.schemas.system import TransferDirectoryConf
|
||
from app.schemas.types import MediaType, NotificationChannel
|
||
|
||
|
||
class MediaInteractionChain(ChainBase):
|
||
"""
|
||
处理媒体搜索、订阅、资源选择和翻页等交互流程。
|
||
"""
|
||
|
||
_button_page_size = 8
|
||
_text_page_size = 8
|
||
_auto_download_dir_name = "自动匹配目录"
|
||
|
||
@staticmethod
|
||
def has_pending_interaction(user_id: Union[str, int]) -> bool:
|
||
"""
|
||
判断用户当前是否存在未结束的媒体交互。
|
||
"""
|
||
return media_interaction_manager.get_by_user(user_id) is not None
|
||
|
||
@staticmethod
|
||
def _get_noexits_info(
|
||
meta: MetaBase, mediainfo: MediaInfo
|
||
) -> Dict[Union[int, str], Dict[int, NotExistMediaInfo]]:
|
||
"""
|
||
构造媒体缺失集信息,用于全量重搜或自动下载补全集数。
|
||
"""
|
||
if mediainfo.type == MediaType.TV:
|
||
if not mediainfo.seasons:
|
||
mediainfo = MediaChain().recognize_media(
|
||
mtype=mediainfo.type,
|
||
media_source=resolve_media_identity(media=mediainfo)[0],
|
||
media_id=resolve_media_identity(media=mediainfo)[1],
|
||
cache=False,
|
||
)
|
||
if not mediainfo:
|
||
logger.warn("媒体信息识别失败,无法补充季集信息")
|
||
return {}
|
||
if not mediainfo.seasons:
|
||
logger.warn(
|
||
"媒体信息中没有季集信息,标题:%s,tmdbid:%s,doubanid:%s",
|
||
mediainfo.title,
|
||
mediainfo.tmdb_id,
|
||
mediainfo.douban_id,
|
||
)
|
||
return {}
|
||
|
||
media_source, media_id = resolve_media_identity(media=mediainfo)
|
||
mediakey = build_media_key(media_source, media_id)
|
||
no_exists = {mediakey: {}}
|
||
if meta.begin_season is not None:
|
||
episodes = mediainfo.seasons.get(meta.begin_season)
|
||
if not episodes:
|
||
return {}
|
||
no_exists[mediakey][meta.begin_season] = NotExistMediaInfo(
|
||
season=meta.begin_season,
|
||
episodes=[],
|
||
total_episode=len(episodes),
|
||
start_episode=episodes[0],
|
||
)
|
||
else:
|
||
for sea, eps in mediainfo.seasons.items():
|
||
if not eps:
|
||
continue
|
||
no_exists[mediakey][sea] = NotExistMediaInfo(
|
||
season=sea,
|
||
episodes=[],
|
||
total_episode=len(eps),
|
||
start_episode=eps[0],
|
||
)
|
||
return no_exists
|
||
return {}
|
||
|
||
@staticmethod
|
||
def parse_callback(
|
||
callback_data: str,
|
||
) -> Optional[Tuple[Optional[str], str, Optional[int]]]:
|
||
"""
|
||
解析新旧两种媒体交互按钮格式。
|
||
"""
|
||
if callback_data.startswith("media:"):
|
||
parts = callback_data.split(":")
|
||
if len(parts) < 3:
|
||
return None
|
||
request_id = parts[1]
|
||
action = parts[2]
|
||
index = None
|
||
if len(parts) >= 4 and parts[3].isdigit():
|
||
index = int(parts[3])
|
||
return request_id, action, index
|
||
|
||
match = re.match(r"^(select|download)_(\d+)$", callback_data)
|
||
if match:
|
||
return None, match.group(1), int(match.group(2))
|
||
if callback_data == "page_p":
|
||
return None, "page-prev", None
|
||
if callback_data == "page_n":
|
||
return None, "page-next", None
|
||
return None
|
||
|
||
def handle_callback_interaction(
|
||
self,
|
||
callback_data: str,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> bool:
|
||
"""
|
||
处理按钮回调,并将当前视图刷新到原消息上。
|
||
"""
|
||
parsed = self.parse_callback(callback_data)
|
||
if not parsed:
|
||
return False
|
||
|
||
request_id, action, index = parsed
|
||
if request_id:
|
||
request = media_interaction_manager.get_by_id(request_id, userid)
|
||
else:
|
||
request = media_interaction_manager.get_by_user(userid)
|
||
|
||
if not request:
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="交互已失效,请重新搜索或订阅",
|
||
save_history=False,
|
||
)
|
||
)
|
||
return True
|
||
|
||
request.channel = channel
|
||
request.source = source
|
||
request.username = username
|
||
|
||
if action == "page-prev":
|
||
if request.page <= 0:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="已经是第一页了!",
|
||
)
|
||
return True
|
||
request.page -= 1
|
||
self._render_interaction(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
return True
|
||
|
||
if action == "page-next":
|
||
if not self._has_next_page(request):
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="已经是最后一页了!",
|
||
)
|
||
return True
|
||
request.page += 1
|
||
self._render_interaction(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
return True
|
||
|
||
if action == "select":
|
||
self._handle_media_selection(
|
||
request=request,
|
||
page_index=index,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
return True
|
||
|
||
if action == "download":
|
||
self._handle_torrent_selection(
|
||
request=request,
|
||
page_index=index,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
|
||
if action == "download-dir":
|
||
self._handle_download_dir_selection(
|
||
request=request,
|
||
page_index=index,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
|
||
return False
|
||
|
||
def handle_text_interaction(
|
||
self,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
text: str,
|
||
) -> bool:
|
||
"""
|
||
处理文本式交互。
|
||
|
||
有会话时优先处理数字选择和翻页;无会话时负责识别搜索/订阅类入口。
|
||
"""
|
||
request = media_interaction_manager.get_by_user(userid)
|
||
normalized = (text or "").strip()
|
||
lowered = normalized.lower()
|
||
|
||
if request and lowered in {"退出", "关闭", "q", "quit", "exit"}:
|
||
media_interaction_manager.remove(request.request_id)
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="媒体交互已结束",
|
||
save_history=False,
|
||
)
|
||
)
|
||
return True
|
||
|
||
if normalized.isdigit():
|
||
if not request:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
request.channel = channel
|
||
request.source = source
|
||
request.username = username
|
||
index = int(normalized)
|
||
if request.phase == "download-dir":
|
||
self._handle_download_dir_selection(
|
||
request=request,
|
||
page_index=index,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
elif request.phase == "torrent":
|
||
self._handle_torrent_selection(
|
||
request=request,
|
||
page_index=index,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
else:
|
||
self._handle_media_selection(
|
||
request=request,
|
||
page_index=index,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
|
||
if lowered in {"p", "prev", "上一页"}:
|
||
if not request:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
if request.page <= 0:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="已经是第一页了!",
|
||
)
|
||
return True
|
||
request.page -= 1
|
||
request.channel = channel
|
||
request.source = source
|
||
request.username = username
|
||
self._render_interaction(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
)
|
||
return True
|
||
|
||
if lowered in {"n", "next", "下一页"}:
|
||
if not request:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
if not self._has_next_page(request):
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="已经是最后一页了!",
|
||
)
|
||
return True
|
||
request.page += 1
|
||
request.channel = channel
|
||
request.source = source
|
||
request.username = username
|
||
self._render_interaction(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
)
|
||
return True
|
||
|
||
action, content = self._resolve_action(normalized)
|
||
if not action:
|
||
return False
|
||
|
||
self._start_media_interaction(
|
||
action=action,
|
||
content=content,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return True
|
||
|
||
@staticmethod
|
||
def _resolve_action(text: str) -> Tuple[Optional[str], str]:
|
||
"""
|
||
将用户输入归类为搜索、订阅或普通聊天。
|
||
"""
|
||
if text.startswith("订阅"):
|
||
return "Subscribe", re.sub(r"订阅[::\s]*", "", text)
|
||
if text.startswith("洗版"):
|
||
return "ReSubscribe", re.sub(r"洗版[::\s]*", "", text)
|
||
if text.startswith("搜索") or text.startswith("下载"):
|
||
return "ReSearch", re.sub(r"(搜索|下载)[::\s]*", "", text)
|
||
if url_tools.is_link(text):
|
||
return None, text
|
||
if not title_rules.is_media_title_like(text):
|
||
return None, text
|
||
return "Search", text
|
||
|
||
def _start_media_interaction(
|
||
self,
|
||
action: str,
|
||
content: str,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
) -> None:
|
||
"""
|
||
根据用户输入搜索媒体,并进入媒体选择阶段。
|
||
"""
|
||
meta, medias = MediaChain().search(content)
|
||
if not meta.name:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="无法识别输入内容!",
|
||
)
|
||
return
|
||
if not medias:
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=f"{meta.name} 没有找到对应的媒体信息!",
|
||
save_history=False,
|
||
)
|
||
)
|
||
return
|
||
|
||
logger.info("搜索到 %s 条相关媒体信息", len(medias))
|
||
request = media_interaction_manager.create_or_replace(
|
||
user_id=userid,
|
||
channel=channel,
|
||
source=source,
|
||
username=username,
|
||
action=action,
|
||
keyword=content,
|
||
title=meta.name,
|
||
meta=meta,
|
||
items=medias,
|
||
)
|
||
self._render_interaction(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
)
|
||
|
||
def _handle_media_selection(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
page_index: Optional[int],
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
处理媒体选择阶段的序号输入。
|
||
"""
|
||
page_items, page, _ = self._page_items(
|
||
items=request.items,
|
||
page=request.page,
|
||
page_size=self._page_size(request.channel),
|
||
)
|
||
request.page = page
|
||
if not page_index or page_index < 1 or page_index > len(page_items):
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return
|
||
|
||
mediainfo: MediaInfo = page_items[page_index - 1]
|
||
request.current_media = mediainfo
|
||
|
||
if request.action in {"Search", "ReSearch"}:
|
||
self._search_media_resources(
|
||
request=request,
|
||
mediainfo=mediainfo,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
return
|
||
|
||
if request.action in {"Subscribe", "ReSubscribe"}:
|
||
self._subscribe_media(
|
||
request=request,
|
||
mediainfo=mediainfo,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
|
||
def _search_media_resources(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
mediainfo: MediaInfo,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
根据已选媒体搜索资源,并切换到资源选择阶段。
|
||
"""
|
||
exist_flag, no_exists = DownloadChain().get_no_exists_info(
|
||
meta=request.meta,
|
||
mediainfo=mediainfo,
|
||
)
|
||
if exist_flag and request.action == "Search":
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=f"【{mediainfo.title_year}{request.meta.sea} 媒体库中已存在,如需重新下载请发送:搜索 名称 或 下载 名称】",
|
||
save_history=False,
|
||
)
|
||
)
|
||
return
|
||
if exist_flag:
|
||
no_exists = self._get_noexits_info(request.meta, mediainfo)
|
||
|
||
messages = self._build_no_exists_messages(
|
||
mediainfo=mediainfo,
|
||
no_exists=no_exists,
|
||
show_missing_only=request.action == "Search",
|
||
)
|
||
if messages:
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=f"{mediainfo.title_year}:\n" + "\n".join(messages),
|
||
save_history=False,
|
||
)
|
||
)
|
||
|
||
logger.info("开始搜索 %s ...", mediainfo.title_year)
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...",
|
||
save_history=False,
|
||
)
|
||
)
|
||
|
||
contexts = SearchChain().process(mediainfo=mediainfo, no_exists=no_exists)
|
||
if not contexts:
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=f"{mediainfo.title}{request.meta.sea} 未搜索到需要的资源!",
|
||
save_history=False,
|
||
)
|
||
)
|
||
return
|
||
|
||
contexts = TorrentHelper().sort_torrents(contexts)
|
||
if self._should_auto_download(userid):
|
||
logger.info("用户 %s 在自动下载用户中,开始自动择优下载 ...", userid)
|
||
request.phase = "torrent"
|
||
request.page = 0
|
||
request.title = mediainfo.title
|
||
request.items = list(contexts)
|
||
if self._prompt_download_dir_selection(
|
||
request=request,
|
||
download_mode="auto",
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
no_exists=no_exists,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
):
|
||
return
|
||
self._auto_download(
|
||
request=request,
|
||
cache_list=contexts,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
no_exists=no_exists,
|
||
)
|
||
return
|
||
|
||
request.phase = "torrent"
|
||
request.page = 0
|
||
request.title = mediainfo.title
|
||
request.items = list(contexts)
|
||
self._render_interaction(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
|
||
def _subscribe_media(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
mediainfo: MediaInfo,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
) -> None:
|
||
"""
|
||
根据已选媒体创建订阅或洗版订阅。
|
||
"""
|
||
best_version = request.action == "ReSubscribe"
|
||
if not best_version:
|
||
exist_flag, _ = DownloadChain().get_no_exists_info(
|
||
meta=request.meta,
|
||
mediainfo=mediainfo,
|
||
)
|
||
if exist_flag:
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=f"【{mediainfo.title_year}{request.meta.sea} 媒体库中已存在,如需洗版请发送:洗版 XXX】",
|
||
save_history=False,
|
||
)
|
||
)
|
||
return
|
||
|
||
mp_name = (
|
||
get_chain_user_port().get_name(
|
||
**{f"{channel.name.lower()}_userid": userid}
|
||
)
|
||
if channel
|
||
else None
|
||
)
|
||
SubscribeChain().add(
|
||
title=mediainfo.title,
|
||
year=mediainfo.year,
|
||
mtype=mediainfo.type,
|
||
media_source=mediainfo.media_source,
|
||
media_id=mediainfo.media_id,
|
||
season=request.meta.begin_season,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=mp_name or username,
|
||
best_version=best_version,
|
||
)
|
||
|
||
def _handle_torrent_selection(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
page_index: Optional[int],
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
) -> None:
|
||
"""
|
||
处理资源选择阶段的下载操作。
|
||
"""
|
||
if request.phase != "torrent":
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return
|
||
|
||
if page_index == 0:
|
||
if self._prompt_download_dir_selection(
|
||
request=request,
|
||
download_mode="auto",
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
):
|
||
return
|
||
self._auto_download(
|
||
request=request,
|
||
cache_list=request.items,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return
|
||
|
||
page_items, page, _ = self._page_items(
|
||
items=request.items,
|
||
page=request.page,
|
||
page_size=self._page_size(request.channel),
|
||
)
|
||
request.page = page
|
||
if not page_index or page_index < 1 or page_index > len(page_items):
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return
|
||
|
||
context: Context = page_items[page_index - 1]
|
||
if self._prompt_download_dir_selection(
|
||
request=request,
|
||
download_mode="single",
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
context=context,
|
||
):
|
||
return
|
||
DownloadChain().download_single(
|
||
context,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
|
||
def _prompt_download_dir_selection(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
download_mode: str,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
context: Optional[Context] = None,
|
||
no_exists: Optional[Dict[Union[int, str], Dict[int, NotExistMediaInfo]]] = None,
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> bool:
|
||
"""
|
||
在下载前进入目录选择阶段;没有配置下载目录时保持原下载流程。
|
||
"""
|
||
media_info = context.media_info if context else request.current_media
|
||
download_dirs = self._get_download_dirs(media_info)
|
||
if not download_dirs:
|
||
return False
|
||
if len(download_dirs) == 1 and not self._is_auto_download_dir(download_dirs[0]):
|
||
return False
|
||
|
||
request.pending_torrent_page = request.page
|
||
request.phase = "download-dir"
|
||
request.page = 0
|
||
request.download_dirs = download_dirs
|
||
request.pending_download_mode = download_mode
|
||
request.pending_download_context = context
|
||
request.pending_no_exists = no_exists
|
||
self._post_download_dirs_message(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
return True
|
||
|
||
def _handle_download_dir_selection(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
page_index: Optional[int],
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
) -> None:
|
||
"""
|
||
处理下载目录阶段的序号输入,并继续执行挂起的下载动作。
|
||
"""
|
||
if request.phase != "download-dir":
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return
|
||
|
||
page_items, page, _ = self._page_items(
|
||
items=request.download_dirs,
|
||
page=request.page,
|
||
page_size=self._page_size(request.channel),
|
||
)
|
||
request.page = page
|
||
if not page_index or page_index < 1 or page_index > len(page_items):
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
return
|
||
|
||
download_dir = page_items[page_index - 1]
|
||
if self._is_auto_download_dir(download_dir):
|
||
self._execute_pending_download(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
save_path=None,
|
||
)
|
||
return
|
||
|
||
save_path = download_dir.save_path or download_dir.download_path
|
||
if not save_path:
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="下载目录配置无效!",
|
||
)
|
||
return
|
||
self._execute_pending_download(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
save_path=save_path,
|
||
)
|
||
|
||
def _execute_pending_download(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
save_path: Optional[str],
|
||
) -> None:
|
||
"""
|
||
使用用户确认的下载目录执行单资源下载或自动择优下载。
|
||
"""
|
||
download_mode = request.pending_download_mode
|
||
if download_mode == "single" and request.pending_download_context:
|
||
context = request.pending_download_context
|
||
self._restore_torrent_phase(request)
|
||
DownloadChain().download_single(
|
||
context,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
save_path=save_path,
|
||
)
|
||
return
|
||
|
||
if download_mode == "auto":
|
||
cache_list = list(request.items or [])
|
||
no_exists = request.pending_no_exists
|
||
self._restore_torrent_phase(request)
|
||
self._auto_download(
|
||
request=request,
|
||
cache_list=cache_list,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
no_exists=no_exists,
|
||
save_path=save_path,
|
||
)
|
||
return
|
||
|
||
self._restore_torrent_phase(request)
|
||
self._post_invalid_input(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title="下载操作已失效,请重新选择资源",
|
||
)
|
||
|
||
@staticmethod
|
||
def _restore_torrent_phase(request: PendingMediaInteraction) -> None:
|
||
"""
|
||
下载动作完成或失效后恢复到资源列表阶段,便于用户继续选择其它资源。
|
||
"""
|
||
request.phase = "torrent"
|
||
request.page = request.pending_torrent_page
|
||
request.download_dirs = []
|
||
request.pending_download_mode = None
|
||
request.pending_download_context = None
|
||
request.pending_no_exists = None
|
||
request.pending_torrent_page = 0
|
||
|
||
def _auto_download(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
cache_list: List[Context],
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: str,
|
||
no_exists: Optional[Dict[Union[int, str], Dict[int, NotExistMediaInfo]]] = None,
|
||
save_path: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
自动择优下载当前资源列表,并在未完成时补建订阅。
|
||
"""
|
||
downloadchain = DownloadChain()
|
||
if no_exists is None:
|
||
exist_flag, no_exists = downloadchain.get_no_exists_info(
|
||
meta=request.meta,
|
||
mediainfo=request.current_media,
|
||
)
|
||
if exist_flag:
|
||
no_exists = self._get_noexits_info(request.meta, request.current_media)
|
||
|
||
downloads, lefts = downloadchain.batch_download(
|
||
contexts=cache_list,
|
||
no_exists=no_exists,
|
||
save_path=save_path,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
)
|
||
if downloads and not lefts:
|
||
logger.info("%s 下载完成", request.current_media.title_year)
|
||
return
|
||
|
||
logger.info("%s 未下载未完整,添加订阅 ...", request.current_media.title_year)
|
||
if downloads and request.current_media.type == MediaType.TV:
|
||
note = [
|
||
download.meta_info.begin_episode
|
||
for download in downloads
|
||
if download.meta_info.begin_episode
|
||
]
|
||
else:
|
||
note = None
|
||
|
||
mp_name = (
|
||
get_chain_user_port().get_name(
|
||
**{f"{channel.name.lower()}_userid": userid}
|
||
)
|
||
if channel
|
||
else None
|
||
)
|
||
SubscribeChain().add(
|
||
title=request.current_media.title,
|
||
year=request.current_media.year,
|
||
mtype=request.current_media.type,
|
||
media_source=request.current_media.media_source,
|
||
media_id=request.current_media.media_id,
|
||
season=request.meta.begin_season,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=mp_name or username,
|
||
state="R",
|
||
note=note,
|
||
)
|
||
|
||
def _render_interaction(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
按当前阶段渲染媒体列表或资源列表。
|
||
"""
|
||
if request.phase == "download-dir":
|
||
self._post_download_dirs_message(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
elif request.phase == "torrent":
|
||
self._post_torrents_message(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
else:
|
||
self._post_medias_message(
|
||
request=request,
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
)
|
||
|
||
def _post_medias_message(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
发送或更新媒体选择列表。
|
||
"""
|
||
page_items, page, total_pages = self._page_items(
|
||
items=request.items,
|
||
page=request.page,
|
||
page_size=self._page_size(channel),
|
||
)
|
||
request.page = page
|
||
total = len(request.items)
|
||
if self._supports_interactive_buttons(channel):
|
||
title = f"【{request.title}】共找到{total}条相关信息,请选择操作"
|
||
buttons = self._create_media_buttons(
|
||
channel=channel,
|
||
request=request,
|
||
items=page_items,
|
||
total=total,
|
||
total_pages=total_pages,
|
||
)
|
||
else:
|
||
if total > self._page_size(channel):
|
||
title = f"【{request.title}】共找到{total}条相关信息,请回复对应数字选择(p: 上一页 n: 下一页)"
|
||
else:
|
||
title = f"【{request.title}】共找到{total}条相关信息,请回复对应数字选择"
|
||
buttons = None
|
||
|
||
self.post_medias_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
title=title,
|
||
userid=userid,
|
||
buttons=buttons,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
save_history=False,
|
||
),
|
||
medias=page_items,
|
||
)
|
||
|
||
def _post_torrents_message(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
发送或更新资源选择列表。
|
||
"""
|
||
page_items, page, total_pages = self._page_items(
|
||
items=request.items,
|
||
page=request.page,
|
||
page_size=self._page_size(channel),
|
||
)
|
||
request.page = page
|
||
total = len(request.items)
|
||
if self._supports_interactive_buttons(channel):
|
||
title = f"【{request.title}】共找到{total}条相关资源,请选择下载"
|
||
buttons = self._create_torrent_buttons(
|
||
channel=channel,
|
||
request=request,
|
||
items=page_items,
|
||
total=total,
|
||
total_pages=total_pages,
|
||
)
|
||
else:
|
||
if total > self._page_size(channel):
|
||
title = f"【{request.title}】共找到{total}条相关资源,请回复对应数字下载(0: 自动选择 p: 上一页 n: 下一页)"
|
||
else:
|
||
title = f"【{request.title}】共找到{total}条相关资源,请回复对应数字下载(0: 自动选择)"
|
||
buttons = None
|
||
|
||
self.post_torrents_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
title=title,
|
||
userid=userid,
|
||
link=self.runtime_config.resource_url,
|
||
buttons=buttons,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
save_history=False,
|
||
),
|
||
torrents=page_items,
|
||
)
|
||
|
||
def _post_download_dirs_message(
|
||
self,
|
||
request: PendingMediaInteraction,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
original_message_id: Optional[Union[str, int]] = None,
|
||
original_chat_id: Optional[str] = None,
|
||
) -> None:
|
||
"""
|
||
发送或更新下载目录选择列表。
|
||
"""
|
||
page_items, page, total_pages = self._page_items(
|
||
items=request.download_dirs,
|
||
page=request.page,
|
||
page_size=self._page_size(channel),
|
||
)
|
||
request.page = page
|
||
total = len(request.download_dirs)
|
||
if self._supports_interactive_buttons(channel):
|
||
title = f"【{request.title}】请选择下载目录"
|
||
buttons = self._create_download_dir_buttons(
|
||
channel=channel,
|
||
request=request,
|
||
items=page_items,
|
||
total=total,
|
||
total_pages=total_pages,
|
||
)
|
||
else:
|
||
if total > self._page_size(channel):
|
||
title = f"【{request.title}】请选择下载目录,请回复对应数字(p: 上一页 n: 下一页)"
|
||
else:
|
||
title = f"【{request.title}】请选择下载目录,请回复对应数字"
|
||
buttons = None
|
||
|
||
text = "\n".join(
|
||
f"{index}. {self._format_download_dir_label(download_dir)}"
|
||
for index, download_dir in enumerate(page_items, start=1)
|
||
)
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
title=title,
|
||
text=text,
|
||
userid=userid,
|
||
buttons=buttons,
|
||
original_message_id=original_message_id,
|
||
original_chat_id=original_chat_id,
|
||
save_history=False,
|
||
)
|
||
)
|
||
|
||
def _create_media_buttons(
|
||
self,
|
||
channel: NotificationChannel,
|
||
request: PendingMediaInteraction,
|
||
items: List[MediaInfo],
|
||
total: int,
|
||
total_pages: int,
|
||
) -> List[List[Dict[str, str]]]:
|
||
"""
|
||
为媒体列表生成选择和翻页按钮。
|
||
"""
|
||
buttons: List[List[Dict[str, str]]] = []
|
||
max_text_length = ChannelCapabilityManager.get_max_button_text_length(channel)
|
||
max_per_row = ChannelCapabilityManager.get_max_buttons_per_row(channel)
|
||
|
||
current_row: List[Dict[str, str]] = []
|
||
for index, media in enumerate(items, start=1):
|
||
if max_per_row == 1:
|
||
button_text = f"{index}. {media.title_year}"
|
||
if len(button_text) > max_text_length:
|
||
button_text = button_text[: max_text_length - 3] + "..."
|
||
buttons.append(
|
||
[
|
||
{
|
||
"text": button_text,
|
||
"callback_data": f"media:{request.request_id}:select:{index}",
|
||
}
|
||
]
|
||
)
|
||
continue
|
||
|
||
current_row.append(
|
||
{
|
||
"text": f"{index}",
|
||
"callback_data": f"media:{request.request_id}:select:{index}",
|
||
}
|
||
)
|
||
if len(current_row) == max_per_row or index == len(items):
|
||
buttons.append(current_row)
|
||
current_row = []
|
||
|
||
if total > self._page_size(channel):
|
||
buttons.extend(self._navigation_buttons(request, total_pages))
|
||
return buttons
|
||
|
||
def _create_torrent_buttons(
|
||
self,
|
||
channel: NotificationChannel,
|
||
request: PendingMediaInteraction,
|
||
items: List[Context],
|
||
total: int,
|
||
total_pages: int,
|
||
) -> List[List[Dict[str, str]]]:
|
||
"""
|
||
为资源列表生成下载和翻页按钮。
|
||
"""
|
||
buttons: List[List[Dict[str, str]]] = [
|
||
[
|
||
{
|
||
"text": "🤖 自动选择下载",
|
||
"callback_data": f"media:{request.request_id}:download:0",
|
||
}
|
||
]
|
||
]
|
||
max_text_length = ChannelCapabilityManager.get_max_button_text_length(channel)
|
||
max_per_row = ChannelCapabilityManager.get_max_buttons_per_row(channel)
|
||
|
||
current_row: List[Dict[str, str]] = []
|
||
for index, context in enumerate(items, start=1):
|
||
torrent = context.torrent_info
|
||
if max_per_row == 1:
|
||
button_text = f"{index}. {torrent.site_name} - {torrent.seeders}↑"
|
||
if len(button_text) > max_text_length:
|
||
button_text = button_text[: max_text_length - 3] + "..."
|
||
buttons.append(
|
||
[
|
||
{
|
||
"text": button_text,
|
||
"callback_data": f"media:{request.request_id}:download:{index}",
|
||
}
|
||
]
|
||
)
|
||
continue
|
||
|
||
current_row.append(
|
||
{
|
||
"text": f"{index}",
|
||
"callback_data": f"media:{request.request_id}:download:{index}",
|
||
}
|
||
)
|
||
if len(current_row) == max_per_row or index == len(items):
|
||
buttons.append(current_row)
|
||
current_row = []
|
||
|
||
if total > self._page_size(channel):
|
||
buttons.extend(self._navigation_buttons(request, total_pages))
|
||
return buttons
|
||
|
||
def _create_download_dir_buttons(
|
||
self,
|
||
channel: NotificationChannel,
|
||
request: PendingMediaInteraction,
|
||
items: List[DownloadDirectory],
|
||
total: int,
|
||
total_pages: int,
|
||
) -> List[List[Dict[str, str]]]:
|
||
"""
|
||
为下载目录列表生成选择和翻页按钮。
|
||
"""
|
||
buttons: List[List[Dict[str, str]]] = []
|
||
max_text_length = ChannelCapabilityManager.get_max_button_text_length(channel)
|
||
max_per_row = ChannelCapabilityManager.get_max_buttons_per_row(channel)
|
||
|
||
current_row: List[Dict[str, str]] = []
|
||
for index, download_dir in enumerate(items, start=1):
|
||
if max_per_row == 1:
|
||
button_text = f"{index}. {self._format_download_dir_label(download_dir)}"
|
||
if len(button_text) > max_text_length:
|
||
button_text = button_text[: max_text_length - 3] + "..."
|
||
buttons.append(
|
||
[
|
||
{
|
||
"text": button_text,
|
||
"callback_data": f"media:{request.request_id}:download-dir:{index}",
|
||
}
|
||
]
|
||
)
|
||
continue
|
||
|
||
current_row.append(
|
||
{
|
||
"text": f"{index}",
|
||
"callback_data": f"media:{request.request_id}:download-dir:{index}",
|
||
}
|
||
)
|
||
if len(current_row) == max_per_row or index == len(items):
|
||
buttons.append(current_row)
|
||
current_row = []
|
||
|
||
if total > self._page_size(channel):
|
||
buttons.extend(self._navigation_buttons(request, total_pages))
|
||
return buttons
|
||
|
||
def _has_next_page(self, request: PendingMediaInteraction) -> bool:
|
||
"""
|
||
判断当前视图是否还有下一页。
|
||
"""
|
||
_, page, total_pages = self._page_items(
|
||
items=self._get_current_phase_items(request),
|
||
page=request.page,
|
||
page_size=self._page_size(request.channel),
|
||
)
|
||
return page < total_pages - 1
|
||
|
||
@staticmethod
|
||
def _get_current_phase_items(request: PendingMediaInteraction) -> List[Any]:
|
||
"""
|
||
获取当前阶段用于分页的数据列表。
|
||
"""
|
||
if request.phase == "download-dir":
|
||
return request.download_dirs
|
||
return request.items
|
||
|
||
@staticmethod
|
||
def _navigation_buttons(
|
||
request: PendingMediaInteraction,
|
||
total_pages: int,
|
||
) -> List[List[Dict[str, str]]]:
|
||
"""
|
||
按当前页状态生成上一页和下一页按钮。
|
||
"""
|
||
buttons: List[List[Dict[str, str]]] = []
|
||
nav_row: List[Dict[str, str]] = []
|
||
if request.page > 0:
|
||
nav_row.append(
|
||
{
|
||
"text": "⬅️ 上一页",
|
||
"callback_data": f"media:{request.request_id}:page-prev",
|
||
}
|
||
)
|
||
if request.page < total_pages - 1:
|
||
nav_row.append(
|
||
{
|
||
"text": "下一页 ➡️",
|
||
"callback_data": f"media:{request.request_id}:page-next",
|
||
}
|
||
)
|
||
if nav_row:
|
||
buttons.append(nav_row)
|
||
return buttons
|
||
|
||
@staticmethod
|
||
def _page_items(
|
||
items: List[Any],
|
||
page: int,
|
||
page_size: int,
|
||
) -> Tuple[List[Any], int, int]:
|
||
"""
|
||
返回当前页数据,并把页码限制在有效范围内。
|
||
"""
|
||
total_pages = max(1, math.ceil(len(items) / page_size)) if page_size else 1
|
||
page = min(max(0, page), total_pages - 1)
|
||
start = page * page_size
|
||
end = start + page_size
|
||
return items[start:end], page, total_pages
|
||
|
||
@classmethod
|
||
def _get_download_dirs(cls, media_info: Optional[MediaInfo] = None) -> List[DownloadDirectory]:
|
||
"""
|
||
获取可供消息交互选择的下载目录。
|
||
"""
|
||
dir_infos = [
|
||
dir_info
|
||
for dir_info in DirectoryHelper().get_download_dirs()
|
||
if dir_info.download_path
|
||
]
|
||
download_dirs = [
|
||
DownloadDirectory(
|
||
name=dir_info.name,
|
||
storage=dir_info.storage or "local",
|
||
download_path=dir_info.download_path,
|
||
save_path=FileURI(
|
||
storage=dir_info.storage or "local",
|
||
path=dir_info.download_path,
|
||
).uri,
|
||
priority=dir_info.priority,
|
||
media_type=dir_info.media_type,
|
||
media_category=dir_info.media_category,
|
||
)
|
||
for dir_info in dir_infos
|
||
if cls._match_download_dir_media(dir_info, media_info)
|
||
]
|
||
if not download_dirs:
|
||
return []
|
||
if len(download_dirs) == 1:
|
||
return download_dirs
|
||
return [cls._build_auto_download_dir(), *download_dirs]
|
||
|
||
@classmethod
|
||
def _build_auto_download_dir(cls) -> DownloadDirectory:
|
||
"""
|
||
构造自动匹配下载目录选项。
|
||
"""
|
||
return DownloadDirectory(
|
||
name=cls._auto_download_dir_name,
|
||
storage="local",
|
||
priority=-1,
|
||
)
|
||
|
||
@classmethod
|
||
def _is_auto_download_dir(cls, download_dir: DownloadDirectory) -> bool:
|
||
"""
|
||
判断是否为自动匹配下载目录选项。
|
||
"""
|
||
return (
|
||
download_dir.name == cls._auto_download_dir_name
|
||
and not download_dir.download_path
|
||
and not download_dir.save_path
|
||
)
|
||
|
||
@staticmethod
|
||
def _match_download_dir_media(
|
||
dir_info: TransferDirectoryConf,
|
||
media_info: Optional[MediaInfo],
|
||
) -> bool:
|
||
"""
|
||
判断下载目录是否适用于当前媒体。
|
||
"""
|
||
if not media_info or not media_info.type:
|
||
return True
|
||
|
||
if dir_info.media_type:
|
||
media_type_values = (
|
||
{media_info.type.value, media_info.type.to_agent()}
|
||
if isinstance(media_info.type, MediaType)
|
||
else {str(media_info.type)}
|
||
)
|
||
if dir_info.media_type not in media_type_values:
|
||
return False
|
||
|
||
if dir_info.media_category and dir_info.media_category != media_info.category:
|
||
return False
|
||
|
||
return True
|
||
|
||
@staticmethod
|
||
def _format_download_dir_label(download_dir: DownloadDirectory) -> str:
|
||
"""
|
||
格式化下载目录展示名称,优先显示用户配置的目录名称。
|
||
"""
|
||
save_path = download_dir.save_path or download_dir.download_path or ""
|
||
name = download_dir.name or save_path or "下载目录"
|
||
if save_path and name != save_path:
|
||
return f"{name} ({save_path})"
|
||
return name
|
||
|
||
def _page_size(self, channel: Optional[NotificationChannel]) -> int:
|
||
"""
|
||
按渠道交互能力选择分页大小。
|
||
"""
|
||
return (
|
||
self._button_page_size
|
||
if self._supports_interactive_buttons(channel)
|
||
else self._text_page_size
|
||
)
|
||
|
||
@staticmethod
|
||
def _supports_interactive_buttons(channel: Optional[NotificationChannel]) -> bool:
|
||
"""
|
||
判断渠道是否同时支持按钮展示与按钮回调。
|
||
"""
|
||
return bool(
|
||
channel
|
||
and ChannelCapabilityManager.supports_buttons(channel)
|
||
and ChannelCapabilityManager.supports_callbacks(channel)
|
||
)
|
||
|
||
@staticmethod
|
||
def _build_no_exists_messages(
|
||
mediainfo: MediaInfo,
|
||
no_exists: Optional[Dict[Union[int, str], Dict[int, NotExistMediaInfo]]],
|
||
show_missing_only: bool,
|
||
) -> List[str]:
|
||
"""
|
||
将缺失集信息转换为可发送的文案。
|
||
"""
|
||
if not no_exists:
|
||
return []
|
||
media_source, media_id = resolve_media_identity(media=mediainfo)
|
||
mediakey = build_media_key(media_source, media_id)
|
||
season_map = no_exists.get(mediakey) or {}
|
||
if show_missing_only:
|
||
return [
|
||
f"第 {sea} 季缺失 {episode_rules.compact_numbers(no_exist.episodes) if no_exist.episodes else no_exist.total_episode} 集"
|
||
for sea, no_exist in season_map.items()
|
||
]
|
||
return [
|
||
f"第 {sea} 季总 {no_exist.total_episode} 集"
|
||
for sea, no_exist in season_map.items()
|
||
]
|
||
|
||
def _should_auto_download(self, userid: Union[str, int]) -> bool:
|
||
"""
|
||
判断当前用户是否命中自动下载名单。
|
||
"""
|
||
auto_download_user = self.runtime_config.auto_download_user
|
||
return bool(
|
||
auto_download_user
|
||
and (
|
||
auto_download_user == "all"
|
||
or any(userid == user for user in auto_download_user.split(","))
|
||
)
|
||
)
|
||
|
||
def _post_invalid_input(
|
||
self,
|
||
channel: NotificationChannel,
|
||
source: str,
|
||
userid: Union[str, int],
|
||
username: Optional[str],
|
||
title: str = "输入有误!",
|
||
) -> None:
|
||
"""
|
||
发送统一的非法输入提示。
|
||
"""
|
||
self.post_message(
|
||
Message(
|
||
channel=channel,
|
||
source=source,
|
||
userid=userid,
|
||
username=username,
|
||
title=title,
|
||
save_history=False,
|
||
)
|
||
)
|