import concurrent.futures import math import os import re import string import random import jvav as jv import yaml import threading import langdetect import html import telebot from telebot import apihelper, types from telebot.types import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto from database import BotFileDb, BotCacheDb from ai import AiClient from pikpak import PikPakClient from requests import get from requests.compat import quote from bs4 import BeautifulSoup import logging from logging.handlers import RotatingFileHandler class Logger: def __init__(self, path_log_file: str, log_level=logging.INFO): self.logger = logging.getLogger() formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s") stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) r_file_handler = RotatingFileHandler( path_log_file, maxBytes=1024 * 1024 * 16, backupCount=1 ) r_file_handler.setFormatter(formatter) self.logger.addHandler(r_file_handler) self.logger.addHandler(stream_handler) self.logger.setLevel(log_level) class BotConfig: def __init__(self, path_config_file: str): with open(path_config_file, "r", encoding="utf8") as f: config = yaml.safe_load(f) self.tg_chat_id = str(config["tg_chat_id"]) if config["tg_chat_id"] else "" self.tg_bot_token = ( str(config["tg_bot_token"]) if config["tg_bot_token"] else "" ) self.enable_nsfw = str(config["enable_nsfw"]) if config["enable_nsfw"] else "0" self.use_proxy = str(config["use_proxy"]) if config["use_proxy"] else "0" self.proxy_addr = str(config["proxy_addr"]) if config["proxy_addr"] else "" self.pikpak_username = ( str(config["pikpak_username"]) if config.get("pikpak_username") else "" ) self.pikpak_password = ( str(config["pikpak_password"]) if config.get("pikpak_password") else "" ) self.ai_base_url = ( str(config["ai_base_url"]) if config.get("ai_base_url") else "" ) self.ai_api_key = str(config["ai_api_key"]) if config.get("ai_api_key") else "" self.ai_model = ( str(config["ai_model"]) if config.get("ai_model") else "gpt-4o-mini" ) # set self.proxy_json = {"http": "", "https": ""} if self.use_proxy == "1": self.proxy_json = {"http": self.proxy_addr, "https": self.proxy_addr} LOG.info(f'Set proxy: "{self.proxy_addr}"') else: self.proxy_addr = "" LOG.info("Successfully read and loaded the configuration file.") # URL BASE_URL_TG = "https://t.me" PIKPAK_BOT_NAME = "PikPak6_Bot" URL_PROJECT_ADDRESS = "https://github.com/akynazh/tg-search-bot" URL_PIKPAK_BOT = f"{BASE_URL_TG}/{PIKPAK_BOT_NAME}" # PATH PATH_ROOT = f'{os.path.expanduser("~")}/.tg_search_bot' PATH_LOG_FILE = f"{PATH_ROOT}/log.txt" PATH_RECORD_FILE = f"{PATH_ROOT}/record.json" PATH_PIKPAK_TOKEN_FILE = f"{PATH_ROOT}/pikpak_token.json" PATH_CACHE_FILE = f"{PATH_ROOT}/cache.db" PATH_CONFIG_FILE = f"{PATH_ROOT}/config.yaml" # BASE LOG = Logger(path_log_file=PATH_LOG_FILE).logger BOT_CFG = BotConfig(PATH_CONFIG_FILE) apihelper.proxy = BOT_CFG.proxy_json BOT = telebot.TeleBot(BOT_CFG.tg_bot_token) BOT_DB = BotFileDb(PATH_RECORD_FILE) BOT_CACHE_DB = BotCacheDb( path_cache_file=PATH_CACHE_FILE, use_cache="1", ) BASE_UTIL = jv.BaseUtil(BOT_CFG.proxy_addr) DMM_UTIL = jv.DmmUtil(BOT_CFG.proxy_addr) JBUS_UTIL = jv.JavBusUtil(BOT_CFG.proxy_addr) JDB_UTIL = jv.JavDbUtil(BOT_CFG.proxy_addr) JLIB_UTIL = jv.JavLibUtil(BOT_CFG.proxy_addr) SUKEBEI_UTIL = jv.SukebeiUtil(BOT_CFG.proxy_addr) TRANS_UTIL = jv.TransUtil(BOT_CFG.proxy_addr) WIKI_UTIL = jv.WikiUtil(BOT_CFG.proxy_addr) VGLE_UTIL = jv.AvgleUtil(BOT_CFG.proxy_addr) EXECUTOR = concurrent.futures.ThreadPoolExecutor() PIKPAK = PikPakClient( username=BOT_CFG.pikpak_username, password=BOT_CFG.pikpak_password, proxy=BOT_CFG.proxy_addr, token_path=PATH_PIKPAK_TOKEN_FILE, ) AI = AiClient( base_url=BOT_CFG.ai_base_url, api_key=BOT_CFG.ai_api_key, model=BOT_CFG.ai_model, proxy=BOT_CFG.proxy_addr, ) # BT 搜索结果分页会话:token -> {"q": ..., "bts": [...], "page": n} BT_SESSIONS = {} CHAT_BT_SESSION = {} ID_PAT = re.compile(r"[a-z0-9]+[-_](?:ppv-)?[a-z0-9]+") BOT_CMDS = { "help": "查看帮助", "stars": "查看已收藏的演职人员", "ids": "查看已收藏的编号", "record": "导出收藏记录文件", } MSG_HELP = f"""直接发送片名、关键词或编号,剩下的交给我! """ for cmd, content in BOT_CMDS.items(): MSG_HELP += f"""/{cmd} {content} """ MSG_HELP += f""" [NSFW: {"已开启" if BOT_CFG.enable_nsfw == "1" else "已关闭"}]""" class BotKey: KEY_GET_SAMPLE_BY_ID = "k0_0" KEY_GET_MORE_MAGNETS_BY_ID = "k0_1" KEY_SEARCH_STAR_BY_NAME = "k0_2" KEY_WATCH_PV_BY_ID = "k1_0" KEY_WATCH_FV_BY_ID = "k1_1" KEY_GET_V_BY_ID = "k2_0" KEY_RANDOM_GET_V_BY_STAR_ID = "k2_1" KEY_RANDOM_GET_V_NICE = "k2_2" KEY_RANDOM_GET_V_NEW = "k2_3" KEY_GET_NEW_VS_BY_STAR_NAME_ID = "k2_4" KEY_GET_NICE_VS_BY_STAR_NAME = "k2_5" KEY_RECORD_STAR_BY_STAR_NAME_ID = "k3_0" KEY_RECORD_V_BY_ID_STAR_IDS = "k3_1" KEY_GET_STARS_RECORD = "k3_2" KEY_GET_VS_RECORD = "k3_3" KEY_GET_STAR_DETAIL_RECORD_BY_STAR_NAME_ID = "k3_4" KEY_GET_V_DETAIL_RECORD_BY_ID = "k3_5" KEY_UNDO_RECORD_STAR_BY_STAR_NAME_ID = "k3_6" KEY_UNDO_RECORD_V_BY_ID = "k3_7" KEY_DEL_V_CACHE = "k4_1" KEY_BT_PAGE = "k5_0" class BotUtils: v_utils = [JDB_UTIL, JBUS_UTIL, SUKEBEI_UTIL] def send_action_typing(self): BOT.send_chat_action(chat_id=BOT_CFG.tg_chat_id, action="typing") def send_msg(self, msg: str, pv=False, markup=None): BOT.send_message( chat_id=BOT_CFG.tg_chat_id, text=msg, disable_web_page_preview=not pv, parse_mode="HTML", reply_markup=markup, ) def send_msg_code_op(self, code: int, op: str): if code == 200: self.send_msg(f"✅ 操作成功:{op}") elif code == 404: self.send_msg(f"没有找到相关结果,{op} 未完成 Q_Q") elif code == 500: self.send_msg(f"服务器出错,{op} 未完成,请稍后重试或查看日志 Q_Q") elif code == 502: self.send_msg(f"网络请求失败,{op} 未完成,请检查网络后重试 Q_Q") def send_msg_success_op(self, op: str): self.send_msg(f"✅ 操作成功:{op}") def send_msg_fail_reason_op(self, reason: str, op: str): self.send_msg(f"{op} 失败:{reason} Q_Q") def send_msg_v_not_found(self, v_id: str): self.send_msg( f"没有找到 {html.escape(v_id)} 的相关结果,请稍后重试或检查编号是否正确。" ) def check_success(self, code: int, op: str): if code == 200: return True if code == 404: self.send_msg_fail_reason_op(reason="未找到相关结果", op=op) elif code == 500: self.send_msg_fail_reason_op(reason="服务器错误", op=op) elif code == 502: self.send_msg_fail_reason_op(reason="网络请求失败", op=op) return False def create_btn_by_key(self, key_type: str, obj): if key_type == BotKey.KEY_GET_STAR_DETAIL_RECORD_BY_STAR_NAME_ID: return InlineKeyboardButton( text=obj["name"], callback_data=f'{obj["name"]}|{obj["id"]}:{key_type}' ) elif key_type == BotKey.KEY_GET_V_DETAIL_RECORD_BY_ID: return InlineKeyboardButton(text=obj, callback_data=f"{obj}:{key_type}") elif key_type == BotKey.KEY_SEARCH_STAR_BY_NAME: return InlineKeyboardButton(text=obj, callback_data=f"{obj}:{key_type}") elif key_type == BotKey.KEY_GET_V_BY_ID: return InlineKeyboardButton( text=f'{obj["id"]} | {obj["rate"]}', callback_data=f'{obj["id"]}:{key_type}', ) def send_msg_btns( self, max_btn_per_row: int, max_row_per_msg: int, key_type: str, title: str, objs: list, extra_btns=[], page_btns=[], ): markup = InlineKeyboardMarkup() row_count = 0 btns = [] for obj in objs: btns.append(self.create_btn_by_key(key_type, obj)) if len(btns) == max_btn_per_row: markup.row(*btns) row_count += 1 btns = [] if row_count == max_row_per_msg: for extra_btn in extra_btns: markup.row(*extra_btn) if page_btns != []: markup.row(*page_btns) self.send_msg(msg=title, markup=markup) row_count = 0 markup = InlineKeyboardMarkup() if btns != []: markup.row(*btns) row_count += 1 if row_count != 0: for extra_btn in extra_btns: markup.row(*extra_btn) if page_btns != []: markup.row(*page_btns) self.send_msg(msg=title, markup=markup) def get_page_elements( self, objs: list, page: int, col: int, row: int, key_type: str ): """ Get the list of objects on the current page, list of pagination buttons, and the title of quantity. :param list objs: All objects :param int page: Current page :param int col: Number of columns on the current page :param int row: Number of rows on the current page :param str key_type: Key type :return tuple[list, list, str]: List of objects on the current page, list of pagination buttons, title of quantity """ record_count_total = len(objs) record_count_per_page = col * row if record_count_per_page > record_count_total: page_count = 1 else: page_count = math.ceil(record_count_total / record_count_per_page) if page > page_count: page = page_count start_idx = (page - 1) * record_count_per_page objs = objs[start_idx : start_idx + record_count_per_page] if page == 1: to_previous = 1 else: to_previous = page - 1 if page == page_count: to_next = page_count else: to_next = page + 1 btn_to_first = InlineKeyboardButton(text="<<", callback_data=f"1:{key_type}") btn_to_previous = InlineKeyboardButton( text="<", callback_data=f"{to_previous}:{key_type}" ) btn_to_current = InlineKeyboardButton( text=f"-{page}-", callback_data=f"{page}:{key_type}" ) btn_to_next = InlineKeyboardButton( text=">", callback_data=f"{to_next}:{key_type}" ) btn_to_last = InlineKeyboardButton( text=">>", callback_data=f"{page_count}:{key_type}" ) # Get the title of quantity title = f"共 {record_count_total} 条,共 {page_count} 页" return ( objs, [btn_to_first, btn_to_previous, btn_to_current, btn_to_next, btn_to_last], title, ) def check_if_enable_nsfw(self): if BOT_CFG.enable_nsfw == "0": self.send_msg("[NSFW] 已关闭,无法访问该内容。") return False return True def get_stars_record(self, page=1): record, is_star_exists, _ = BOT_DB.check_has_record() if not record or not is_star_exists: self.send_msg_fail_reason_op( reason="还没有收藏任何演职人员", op="查看收藏的演职人员" ) return stars = record["stars"] stars.reverse() col, row = 4, 5 objs, page_btns, title = self.get_page_elements( objs=stars, page=page, col=col, row=row, key_type=BotKey.KEY_GET_STARS_RECORD, ) self.send_msg_btns( max_btn_per_row=col, max_row_per_msg=row, key_type=BotKey.KEY_GET_STAR_DETAIL_RECORD_BY_STAR_NAME_ID, title="已收藏的演职人员:" + title, objs=objs, page_btns=page_btns, ) def get_star_detail_record_by_name_id(self, star_name: str, star_id: str): record, is_stars_exists, is_vs_exists = BOT_DB.check_has_record() if not record: self.send_msg_fail_reason_op( reason="还没有收藏该演职人员", op=f"查看演职人员 {star_name} 的详情", ) return star_vs = [] cur_star_exists = False if is_vs_exists: vs = record["vs"] vs.reverse() for v in vs: if star_id in v["stars"]: star_vs.append(v["id"]) if is_stars_exists: stars = record["stars"] for star in stars: if star["id"].lower() == star_id.lower(): cur_star_exists = True extra_btn1 = InlineKeyboardButton( text="随机一部", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_RANDOM_GET_V_BY_STAR_ID}", ) extra_btn2 = InlineKeyboardButton( text="最新作品", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_GET_NEW_VS_BY_STAR_NAME_ID}", ) extra_btn3 = InlineKeyboardButton( text="高分作品", callback_data=f"{star_name}:{BotKey.KEY_GET_NICE_VS_BY_STAR_NAME}", ) if cur_star_exists: extra_btn4 = InlineKeyboardButton( text="取消收藏", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_UNDO_RECORD_STAR_BY_STAR_NAME_ID}", ) else: extra_btn4 = InlineKeyboardButton( text="收藏", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_RECORD_STAR_BY_STAR_NAME_ID}", ) title = f'{star_name} | Wiki | Javbus' if len(star_vs) == 0: markup = InlineKeyboardMarkup() markup.row(extra_btn1, extra_btn2, extra_btn3, extra_btn4) self.send_msg(msg=title, markup=markup) return self.send_msg_btns( max_btn_per_row=4, max_row_per_msg=10, key_type=BotKey.KEY_GET_V_DETAIL_RECORD_BY_ID, title=title, objs=star_vs, extra_btns=[[extra_btn1, extra_btn2, extra_btn3, extra_btn4]], ) def get_vs_record(self, page=1): record, _, is_vs_exists = BOT_DB.check_has_record() if not record or not is_vs_exists: self.send_msg_fail_reason_op( reason="还没有收藏任何编号", op="查看收藏的编号", ) return vs = [v["id"] for v in record["vs"]] vs.reverse() extra_btn1 = InlineKeyboardButton( text="随机高分", callback_data=f"0:{BotKey.KEY_RANDOM_GET_V_NICE}", ) extra_btn2 = InlineKeyboardButton( text="随机最新", callback_data=f"0:{BotKey.KEY_RANDOM_GET_V_NEW}" ) col, row = 4, 10 objs, page_btns, title = self.get_page_elements( objs=vs, page=page, col=col, row=row, key_type=BotKey.KEY_GET_VS_RECORD ) self.send_msg_btns( max_btn_per_row=col, max_row_per_msg=row, key_type=BotKey.KEY_GET_V_DETAIL_RECORD_BY_ID, title="已收藏的编号:" + title, objs=objs, extra_btns=[[extra_btn1, extra_btn2]], page_btns=page_btns, ) def get_v_detail_record_by_id(self, id: str): record, _, is_vs_exists = BOT_DB.check_has_record() vs = record["vs"] cur_v_exists = False for v in vs: if id.lower() == v["id"].lower(): cur_v_exists = True markup = InlineKeyboardMarkup() btn = InlineKeyboardButton( text=f"查看", callback_data=f"{id}:{BotKey.KEY_GET_V_BY_ID}", ) if cur_v_exists: markup.row( btn, InlineKeyboardButton( text=f"取消收藏", callback_data=f"{id}:{BotKey.KEY_UNDO_RECORD_V_BY_ID}", ), ) else: markup.row(btn) self.send_msg(msg=f"{id}", markup=markup) def search_bts(self, q): def append_trackers(): """Returns the base tracker list""" trackers = [ "udp://tracker.coppersurfer.tk:6969/announce", "udp://tracker.openbittorrent.com:6969/announce", "udp://9.rarbg.to:2710/announce", "udp://9.rarbg.me:2780/announce", "udp://9.rarbg.to:2730/announce", "udp://tracker.opentrackr.org:1337", "http://p4p.arenabg.com:1337/announce", "udp://tracker.torrent.eu.org:451/announce", "udp://tracker.tiny-vps.com:6969/announce", "udp://open.stealth.si:80/announce", ] trackers = [quote(tr) for tr in trackers] return "&tr=".join(trackers) def category_name(category): """Translates the category code to a name""" names = ["", "audio", "video", "apps", "games", "nsfw", "other"] category = int(category[0]) category = category if category < len(names) - 1 else -1 return names[category] def size_as_str(size): """Formats the file size in bytes to kb, mb or gb accordingly""" size = int(size) size_str = f"{size} b" if size >= 1024: size_str = f"{(size / 1024):.2f} kb" if size >= 1024**2: size_str = f"{(size / 1024 ** 2):.2f} mb" if size >= 1024**3: size_str = f"{(size / 1024 ** 3):.2f} gb" return size_str def magnet_link(ih, name): """Creates the magnet URI""" return f"magnet:?xt=urn:btih:{ih}&dn={quote(name)}&tr={append_trackers()}" agent = BASE_UTIL.ua() url = f"https://apibay.org/q.php?q={quote(q)}" try: results = get(url, headers={"agent": agent}, timeout=10) except Exception as e: LOG.error(f"apibay search request failed: {e}") return None if not results.status_code == 200: return None matches = [] data = results.json() if data and "no results" in data[0]["name"].lower(): return matches for d in data: matches.append( { "seeders": d["seeders"], "leechers": d["leechers"], "name": d["name"], "category": category_name(d["category"]), "size": size_as_str(d["size"]), "magnet": magnet_link(d["info_hash"], d["name"]), } ) return matches def search_bts_torrentkitty(self, q): """通过 TorrentKitty 搜索中文 BT 资源,返回与 search_bts 相同格式的列表或 None。 TorrentKitty 聚合国内多家 BT 站,中文关键词命中率远高于 apibay。 """ url = f"https://www.torrentkitty.tv/search/{quote(q)}" try: resp = get( url, headers={"user-agent": BASE_UTIL.ua()}, timeout=10, ) except Exception as e: LOG.error(f"TorrentKitty search request failed: {e}") return None if resp.status_code != 200: return None try: soup = BeautifulSoup(resp.text, "html.parser") matches = [] for tr in soup.find_all("tr"): name_td = tr.find("td", class_="name") size_td = tr.find("td", class_="size") magnet_a = tr.find("a", rel="magnet") if not name_td or not magnet_a: continue matches.append( { "seeders": 0, "leechers": 0, "name": name_td.get_text(strip=True), "category": "general", "size": size_td.get_text(strip=True) if size_td else "", "magnet": magnet_a.get("href"), } ) return matches if matches else None except Exception as e: LOG.error(f"TorrentKitty search parse failed: {e}") return None def get_v_by_id( self, id: str, send_to_pikpak=True, is_nice=True, is_uncensored=True, magnet_max_count=3, not_send=False, ): """ Get based on id :param str id: Number :param bool send_to_pikpak: Whether to send to pikpak, default is yes :param bool is_nice: Whether to filter out HD, subtitled magnet links, default is yes :param bool is_uncensored: Whether to filter out uncensored magnet links, default is yes :param int magnet_max_count: Maximum id of magnet links after filtering, default is 3 :param not_send: Whether not to send results, default is to send :return dict: When not sending results, return the obtained results (if any) """ if not self.check_if_enable_nsfw(): return {} op_get_v_by_id = f"搜索编号 {id}" v = BOT_CACHE_DB.get_cache(key=id, type=BotCacheDb.TYPE_V) v_score = None is_cache = False if not v or not_send: v_util = None for util in self.v_utils: code, v = util.get_av_by_id( id=id, is_nice=is_nice, is_uncensored=is_uncensored, magnet_max_count=magnet_max_count, ) if code == 200: v_util = util break if not v_util: if not not_send: self.send_msg_v_not_found(v_id=id) return if "score" not in v.keys(): _, v["score"] = DMM_UTIL.get_score_by_id(id) if not not_send: if len(v["magnets"]) == 0: BOT_CACHE_DB.set_cache( key=id, value=v, type=BotCacheDb.TYPE_V, expire=3600 * 24 * 1 ) else: BOT_CACHE_DB.set_cache(key=id, value=v, type=BotCacheDb.TYPE_V) else: v_score = v["score"] is_cache = True if not_send: return v v_id = id v_title = v["title"] v_img = v["img"] v_date = v["date"] v_tags = v["tags"] v_stars = v["stars"] v_magnets = v["magnets"] v_url = v["url"] LOG.info(f"AV 搜索结果: {v_id} | {v_title}") for _m in v_magnets: LOG.info(f" 磁力: {_m['link']}") msg = "" if v_title != "": v_title = v_title.replace("<", "").replace(">", "") msg += f"""【标题】{v_title} """ msg += f"""【编号】{v_id} """ if v_date != "": msg += f"""【日期】{v_date} """ if v_score: msg += f"""【评分】{v_score} """ if v_stars != []: show_star_name = v_stars[0]["name"] show_star_id = v_stars[0]["id"] stars_msg = "" for star in v_stars: stars_msg += f"""【演职人员】{star["name"]} """ msg += stars_msg if v_tags: v_tags = " ".join(v_tags).replace("<", "").replace(">", "") msg += f"""【标签】{v_tags} """ msg += f"""【其他】Pikpak | 项目主页 """ magnet_send_to_pikpak = "" for i, magnet in enumerate(v_magnets): if i == 0: magnet_send_to_pikpak = magnet["link"] magnet_tags = "" if magnet["uc"] == "1": magnet_tags += "无码 " if magnet["hd"] == "1": magnet_tags += "HD " if magnet["zm"] == "1": magnet_tags += "中字 " msg_tmp = f"""【{magnet_tags}磁力-{string.ascii_letters[i].upper()} {magnet["size"]}】{magnet["link"]} """ if len(msg + msg_tmp) >= 2000: break msg += msg_tmp pv_btn = InlineKeyboardButton( text="预览", callback_data=f"{v_id}:{BotKey.KEY_WATCH_PV_BY_ID}" ) fv_btn = InlineKeyboardButton( text="在线观看", callback_data=f"{v_id}:{BotKey.KEY_WATCH_FV_BY_ID}" ) sample_btn = InlineKeyboardButton( text="截图", callback_data=f"{v_id}:{BotKey.KEY_GET_SAMPLE_BY_ID}" ) more_btn = InlineKeyboardButton( text="更多磁力", callback_data=f"{v_id}:{BotKey.KEY_GET_MORE_MAGNETS_BY_ID}", ) if len(v_magnets) != 0: markup = InlineKeyboardMarkup().row(sample_btn, pv_btn, fv_btn, more_btn) else: markup = InlineKeyboardMarkup().row(sample_btn, pv_btn, fv_btn) star_record_btn = None if len(v_stars) == 1: if BOT_DB.check_star_exists_by_id(star_id=show_star_id): star_record_btn = InlineKeyboardButton( text="详情", callback_data=f"{show_star_name}|{show_star_id}:{BotKey.KEY_GET_STAR_DETAIL_RECORD_BY_STAR_NAME_ID}", ) else: star_record_btn = InlineKeyboardButton( text="收藏演职人员", callback_data=f"{show_star_name}|{show_star_id}:{BotKey.KEY_RECORD_STAR_BY_STAR_NAME_ID}", ) star_ids = "" for i, star in enumerate(v_stars): star_ids += star["id"] + "|" if i >= 5: star_ids += "...|" break if star_ids != "": star_ids = star_ids[: len(star_ids) - 1] v_record_btn = None if BOT_DB.check_id_exists(id=v_id): v_record_btn = InlineKeyboardButton( text="详情", callback_data=f"{v_id}:{BotKey.KEY_GET_V_DETAIL_RECORD_BY_ID}", ) else: v_record_btn = InlineKeyboardButton( text="收藏", callback_data=f"{v_id}|{star_ids}:{BotKey.KEY_RECORD_V_BY_ID_STAR_IDS}", ) renew_btn = None if is_cache: renew_btn = InlineKeyboardButton( text="刷新", callback_data=f"{v_id}:{BotKey.KEY_DEL_V_CACHE}" ) if star_record_btn and renew_btn: markup.row(v_record_btn, star_record_btn, renew_btn) elif star_record_btn: markup.row(v_record_btn, star_record_btn) elif renew_btn: markup.row(v_record_btn, renew_btn) else: markup.row(v_record_btn) if v_img == "": self.send_msg(msg=msg, markup=markup) else: try: BOT.send_photo( chat_id=BOT_CFG.tg_chat_id, photo=v_img, caption=msg, parse_mode="HTML", reply_markup=markup, ) except Exception: self.send_msg(msg=msg, markup=markup) if magnet_send_to_pikpak != "" and send_to_pikpak: self.send_magnet_to_pikpak(magnet_send_to_pikpak, v_id) def send_magnet_to_pikpak(self, magnet: str, id: str): if not PIKPAK.enabled: self.send_msg_fail_reason_op( reason="请在 config.yaml 中配置 PikPak 账号 (pikpak_username / pikpak_password)", op=f"保存编号 {id} 的磁力链接到 PikPak", ) return self.send_msg("💾 正在保存到 PikPak 网盘 ...") ok, msg = PIKPAK.offline_download(magnet) if ok: self.send_msg(f"✅ 保存结果:{msg}") else: self.send_msg_fail_reason_op( reason=msg, op=f"保存编号 {id} 的磁力链接到 PikPak" ) def get_bts_by_keyword(self, q): """按关键词搜索 BT 资源(带缓存),返回资源列表或 None。 并行搜索 TorrentKitty(中文)与 apibay(英文)两个来源的结果,按磁力 info_hash 去重。 """ bts = BOT_CACHE_DB.get_cache(key=q, type=BotCacheDb.TYPE_BT) if bts: return bts merged = [] seen = set() # 并行搜索两个来源,但每个来源最多等待短时间,避免长时间无响应。 with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: tk_future = executor.submit(self.search_bts_torrentkitty, q) ab_future = executor.submit(self.search_bts, q) srcs = [] for name, future in (("torrentkitty", tk_future), ("apibay", ab_future)): try: srcs.append(future.result(timeout=8)) except concurrent.futures.TimeoutError: LOG.warning(f"BT source {name} timed out for query: {q}") srcs.append([]) for src in srcs: for bt in src or []: if not bt.get("magnet"): continue m = re.search(r"urn:btih:([A-Za-z0-9]+)", bt["magnet"]) key = m.group(1).lower() if m else bt["magnet"] if key in seen: continue seen.add(key) merged.append(bt) if not merged: return None if BOT_CFG.enable_nsfw == "0": merged = list(filter(lambda bt: bt["category"] != "nsfw", merged)) merged = list(filter(lambda bt: "nsfw" not in bt["name"].lower(), merged)) merged = merged[:30] BOT_CACHE_DB.set_cache(key=q, value=merged, type=BotCacheDb.TYPE_BT) LOG.info(f"BT 搜索结果 {len(merged)} 条: {q}") for _bt in merged: LOG.info(f" {_bt['name']} | {_bt['size']} | {_bt['magnet']}") return merged def search_bts_and_save(self, q): try: bts = self.get_bts_by_keyword(q) except Exception as e: LOG.error(f"BT 搜索异常: {e}") self.send_msg_fail_reason_op( reason=f"搜索超时或网络错误:{str(e)[:50]}", op=f"搜索 {html.escape(q)}", ) return False if not bts: self.send_msg_fail_reason_op( reason="没有找到相关资源", op=f"搜索 {html.escape(q)}" ) return False self.show_bt_results(q, bts, save_first=True) return True def search_av_keyword(self, q): """按关键词搜索 AV(JavDB 标签搜索),找到则返回 True。""" code, ids = JDB_UTIL.get_ids_by_tag(q) if code == 200 and ids: self.get_v_by_id(id=ids[0]) return True return False def handle_nlp_search(self, msg): """AI 自然语言理解搜索流程。返回 True 表示已处理。""" if not AI.enabled: return False LOG.info(f'AI 搜索字符: "{msg}"') intent = AI.understand(msg) LOG.info(f"AI 返回结果: {intent}") if not intent: return False target = (intent.get("target") or msg).strip() kind = (intent.get("intent") or "general").strip().lower() explain = (intent.get("explain") or target).strip() source = (intent.get("source") or "auto").strip().lower() # AV 相关意图需先检查 NSFW 开关 if kind in ("av_id", "actor") and not self.check_if_enable_nsfw(): return True self.send_msg(f"🔍 搜索:{html.escape(explain)}") self.send_msg("⏳ 搜索中 ...") if kind == "av_id": self.get_v_by_id(id=target) return True if kind == "actor": # 仅在 AI 明确判定为日本 AV 演员时,才走演员搜索;其他“演员/明星”类请求继续走一般搜索 self.search_star_by_name(target) return True # 类型 / 关键词 / 剧情 / 其他:优先 AV 关键词搜索,失败则 BT 搜索 if ( source in ("javdb", "javbus", "sukebei", "dmm") and BOT_CFG.enable_nsfw == "1" ): if self.search_av_keyword(target): return True # BT 搜索(总是返回 True,表示已处理;搜索结果由 search_bts_and_save 内部反馈给用户) self.search_bts_and_save(target) return True def get_sample_by_id(self, id: str): op_get_sample = f"获取编号 {id} 的截图" samples = BOT_CACHE_DB.get_cache(key=id, type=BotCacheDb.TYPE_SAMPLE) if not samples: code, samples = JBUS_UTIL.get_samples_by_id(id) if not self.check_success(code, op_get_sample): return BOT_CACHE_DB.set_cache(key=id, value=samples, type=BotCacheDb.TYPE_SAMPLE) samples_imp = [] sample_error = False for sample in samples: samples_imp.append(InputMediaPhoto(sample)) if len(samples_imp) == 10: try: BOT.send_media_group(chat_id=BOT_CFG.tg_chat_id, media=samples_imp) samples_imp = [] except Exception: sample_error = True self.send_msg_fail_reason_op( reason="图片解析失败", op=op_get_sample ) break if samples_imp != [] and not sample_error: try: BOT.send_media_group(chat_id=BOT_CFG.tg_chat_id, media=samples_imp) except Exception: self.send_msg_fail_reason_op(reason="图片解析失败", op=op_get_sample) def watch_v_by_id(self, id: str, type: int): id = id.lower() if id.find("fc2") != -1 and id.find("ppv") == -1: id = id.replace("fc2", "fc2-ppv") if type == 0: pv = BOT_CACHE_DB.get_cache(key=id, type=BotCacheDb.TYPE_PV) if not pv: op_watch_v = f"获取编号 {id} 的预览视频" futures = {} with concurrent.futures.ThreadPoolExecutor() as executor: futures[executor.submit(DMM_UTIL.get_pv_by_id, id)] = 1 futures[executor.submit(VGLE_UTIL.get_pv_by_id, id)] = 2 for future in concurrent.futures.as_completed(futures): if futures[future] == 1: code_dmm, pv_dmm = future.result() elif futures[future] == 2: code_vgle, pv_vgle = future.result() if code_dmm != 200 and code_vgle != 200: if code_dmm == 502 or code_vgle == 502: self.send_msg_code_op(502, op_watch_v) else: self.send_msg_code_op(404, op_watch_v) return from_site = "" pv_src = "" if code_dmm == 200: from_site = "dmm" pv_src = pv_dmm elif code_vgle == 200: from_site = "avgle" pv_src = pv_vgle pv_cache = {"from_site": from_site, "src": pv_src} BOT_CACHE_DB.set_cache(key=id, value=pv_cache, type=BotCacheDb.TYPE_PV) else: from_site = pv["from_site"] pv_src = pv["src"] if from_site == "dmm": try: pv_src_nice = DMM_UTIL.get_nice_pv_by_src(pv_src) BOT.send_video( chat_id=BOT_CFG.tg_chat_id, video=pv_src, caption=f'来自 DMM 的搜索结果。点此观看更清晰的版本。', parse_mode="HTML", ) except Exception: self.send_msg( f'来自 DMM 的搜索结果,但视频解析失败:视频链接 Q_Q。' ) elif from_site == "avgle": try: BOT.send_video( chat_id=BOT_CFG.tg_chat_id, video=pv_src, caption=f'来自 Avgle 的搜索结果:视频链接。', parse_mode="HTML", ) except Exception: self.send_msg( f'来自 Avgle 的搜索结果,但视频解析失败:视频链接 Q_Q。' ) elif type == 1: video = BOT_CACHE_DB.get_cache(key=id, type=BotCacheDb.TYPE_FV) if not video: code, video = VGLE_UTIL.get_fv_by_id(id) if code != 200: self.send_msg("没有找到相关结果。") return BOT_CACHE_DB.set_cache(key=id, value=video, type=BotCacheDb.TYPE_FV) self.send_msg(video) def search_star_by_name(self, star_name: str): if not self.check_if_enable_nsfw(): return False op_search_star = f"搜索演职人员 {star_name}" star = BOT_CACHE_DB.get_cache(key=star_name, type=BotCacheDb.TYPE_STAR) if not star: star_name_origin = star_name star_name = self.get_star_ja_name_by_zh_name(star_name) code, star = JBUS_UTIL.check_star_exists(star_name) if not self.check_success(code, op_search_star): return BOT_CACHE_DB.set_cache(key=star_name, value=star, type=BotCacheDb.TYPE_STAR) if star_name_origin != star_name: BOT_CACHE_DB.set_cache( key=star_name_origin, value=star, type=BotCacheDb.TYPE_STAR, ) star_id = star["star_id"] star_name = star["star_name"] if BOT_DB.check_star_exists_by_id(star_id=star_id): self.get_star_detail_record_by_name_id(star_name=star_name, star_id=star_id) return True markup = InlineKeyboardMarkup() markup.row( InlineKeyboardButton( text="随机一部", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_RANDOM_GET_V_BY_STAR_ID}", ), InlineKeyboardButton( text="最新作品", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_GET_NEW_VS_BY_STAR_NAME_ID}", ), InlineKeyboardButton( text="高分作品", callback_data=f"{star_name}:{BotKey.KEY_GET_NICE_VS_BY_STAR_NAME}", ), InlineKeyboardButton( text=f"收藏 {star_name}", callback_data=f"{star_name}|{star_id}:{BotKey.KEY_RECORD_STAR_BY_STAR_NAME_ID}", ), ) star_wiki = f"{WIKI_UTIL.BASE_URL_CHINA_WIKI}/{star_name}" if langdetect.detect(star_name) == "ja": star_wiki = f"{WIKI_UTIL.BASE_URL_JAPAN_WIKI}/{star_name}" self.send_msg( msg=f'{star_name} | Wiki | Javbus', markup=markup, ) return True def get_more_magnets_by_id(self, id: str): magnets = BOT_CACHE_DB.get_cache(key=id, type=BotCacheDb.TYPE_MAGNET) if not magnets: v = self.get_v_by_id( id=id, is_nice=False, is_uncensored=False, not_send=True ) if not v: return magnets = v["magnets"] BOT_CACHE_DB.set_cache(key=id, value=magnets, type=BotCacheDb.TYPE_MAGNET) msg = "" for magnet in magnets: magnet_tags = "" if magnet["uc"] == "1": magnet_tags += "无码 " if magnet["hd"] == "1": magnet_tags += "HD " if magnet["zm"] == "1": magnet_tags += "中字 " star_tag = "" if magnet["hd"] == "1" and magnet["zm"] == "1": star_tag = "*" msg_tmp = f"""【{star_tag}{magnet_tags}磁力 {magnet["size"]}】{magnet["link"]} """ if len(msg + msg_tmp) >= 4000: self.send_msg(msg) msg = msg_tmp else: msg += msg_tmp if msg != "": self.send_msg(msg) def get_star_new_vs_by_name_id(self, star_name: str, star_id: str): op_get_star_new_vs = f"获取 {star_name} 的最新作品" ids = BOT_CACHE_DB.get_cache(key=star_id, type=BotCacheDb.TYPE_NEW_VS_OF_STAR) if not ids: code, ids = JBUS_UTIL.get_new_ids_by_star_id(star_id=star_id) if not self.check_success(code, op_get_star_new_vs): return BOT_CACHE_DB.set_cache( key=star_id, value=ids, type=BotCacheDb.TYPE_NEW_VS_OF_STAR ) title = f"{star_name} 的最新作品" btns = [ InlineKeyboardButton( text=id, callback_data=f"{id}:{BotKey.KEY_GET_V_BY_ID}" ) for id in ids ] if len(btns) <= 4: self.send_msg(msg=title, markup=InlineKeyboardMarkup().row(*btns)) else: markup = InlineKeyboardMarkup() markup.row(*btns[:4]) markup.row(*btns[4:]) self.send_msg(msg=title, markup=markup) def get_star_ja_name_by_zh_name(self, star_name: str): if langdetect.detect(star_name) == "ja": return star_name star_ja_name = BOT_CACHE_DB.get_cache( key=star_name, type=BotCacheDb.TYPE_STAR_JA_NAME ) if star_ja_name: return star_ja_name wiki_json = WIKI_UTIL.get_wiki_page_by_lang( topic=star_name, from_lang="zh", to_lang="ja" ) if wiki_json and wiki_json["lang"] == "ja": BOT_CACHE_DB.set_cache( key=star_name, value=wiki_json["title"], type=BotCacheDb.TYPE_STAR_JA_NAME, ) return wiki_json["title"] return star_name def register_bt_session(self, q, bts): token = "".join(random.choices(string.ascii_letters + string.digits, k=8)) BT_SESSIONS[token] = {"q": q, "bts": bts, "page": 1} CHAT_BT_SESSION[BOT_CFG.tg_chat_id] = token return token def show_bt_results(self, q, bts, save_first=False): token = self.register_bt_session(q, bts) self.show_bt_page(token, 1) if save_first and bts: self.send_magnet_to_pikpak(bts[0]["magnet"], q) return token def show_bt_page(self, token, page): sess = BT_SESSIONS.get(token) if not sess: self.send_msg("结果会话已过期,请重新搜索。") return bts = sess["bts"] page_size = 5 page_count = math.ceil(len(bts) / page_size) if bts else 1 if page < 1: page = 1 if page > page_count: page = 1 # 换一批:最后一页回到第一页 sess["page"] = page start = (page - 1) * page_size items = bts[start : start + page_size] res = ( f"「{html.escape(sess['q'])}」的搜索结果" f"(第 {page}/{page_count} 页):\n\n" ) for i, bt in enumerate(items): idx = start + i + 1 name = bt.get("name") or "" if len(name) > 80: name = name[:77] + "..." magnet = bt.get("magnet") or "" magnet_preview = magnet if len(magnet_preview) > 80: magnet_preview = magnet_preview[:77] + "..." res += ( f"{idx}. {html.escape(name)}\n" f" 大小: {bt.get('size', '')} | 分类: {bt.get('category', 'general')}\n" f" 磁力: {html.escape(magnet_preview)}\n\n" ) res = res[:3500] + "\n\n..." if len(res) > 3500 else res markup = InlineKeyboardMarkup() btns = [] if page > 1: btns.append( InlineKeyboardButton( "◀ 上一页", callback_data=f"{token}|{page - 1}:{BotKey.KEY_BT_PAGE}", ) ) btns.append( InlineKeyboardButton( "🔄 换一批", callback_data=f"{token}|{page + 1}:{BotKey.KEY_BT_PAGE}", ) ) markup.row(*btns) markup.row(InlineKeyboardButton("保存到 PikPak 网盘", url=URL_PIKPAK_BOT)) self.send_msg(res, markup=markup) def handle_bt_followup(self, msg): """处理 BT 搜索结果的多轮追问:换一批 / 上一页 / 保存第 N 个。""" token = CHAT_BT_SESSION.get(BOT_CFG.tg_chat_id) if not token or token not in BT_SESSIONS: return False sess = BT_SESSIONS[token] m = msg.strip() save_match = re.match(r"^(?:保存|save)\s*第?\s*(\d+)\s*个?$", m) if save_match: n = int(save_match.group(1)) bts = sess["bts"] if 1 <= n <= len(bts): self.send_magnet_to_pikpak(bts[n - 1]["magnet"], sess["q"]) else: self.send_msg_fail_reason_op( reason=f"没有第 {n} 个结果", op="保存到 PikPak" ) return True if m in ("下一页", "下一批", "next"): self.show_bt_page(token, sess["page"] + 1) return True if m in ("上一页", "prev"): self.show_bt_page(token, sess["page"] - 1) return True if m in ("换一批", "换一页", "还有吗", "再来", "more"): self.show_bt_page(token, sess["page"] + 1) return True return False def random_get_new_v(self): page = random.randint(1, JLIB_UTIL.MAX_RANK_PAGE) ids = BOT_CACHE_DB.get_cache(key=page, type=BotCacheDb.TYPE_JLIB_PAGE_NEW_VS) if not ids: code, ids = JLIB_UTIL.get_random_ids_from_rank_by_page( page=page, list_type=1 ) if self.check_success(code, "随机获取最新影片"): BOT_CACHE_DB.set_cache( key=page, value=ids, type=BotCacheDb.TYPE_JLIB_PAGE_NEW_VS, ) else: return self.get_v_by_id(id=random.choice(ids)) def random_get_nice_v(self): page = random.randint(1, JLIB_UTIL.MAX_RANK_PAGE) ids = BOT_CACHE_DB.get_cache(key=page, type=BotCacheDb.TYPE_JLIB_PAGE_NICE_VS) if not ids: code, ids = JLIB_UTIL.get_random_ids_from_rank_by_page( page=page, list_type=0 ) if self.check_success(code, "随机获取高分影片"): BOT_CACHE_DB.set_cache( key=page, value=ids, type=BotCacheDb.TYPE_JLIB_PAGE_NICE_VS, ) else: return self.get_v_by_id(id=random.choice(ids)) def random_get_nice_star_vs(self, star_name_ori): vs = BOT_CACHE_DB.get_cache( key=star_name_ori, type=BotCacheDb.TYPE_NICE_VS_OF_STAR ) if not vs: star_name_ja = self.get_star_ja_name_by_zh_name(star_name_ori) code, vs = DMM_UTIL.get_nice_vs_by_star_name(star_name=star_name_ja) if self.check_success(code, f"获取演职人员 {star_name_ori} 的高分作品"): vs = vs[:60] BOT_CACHE_DB.set_cache( key=star_name_ori, value=vs, type=BotCacheDb.TYPE_NICE_VS_OF_STAR, ) if star_name_ja != star_name_ori: BOT_CACHE_DB.set_cache( key=star_name_ja, value=vs, type=BotCacheDb.TYPE_NICE_VS_OF_STAR, ) else: return self.send_msg_btns( max_btn_per_row=3, max_row_per_msg=20, key_type=BotKey.KEY_GET_V_BY_ID, title=f"{star_name_ori} 的高分作品", objs=vs, ) def handle_callback(call): bot_utils = BotUtils() bot_utils.send_action_typing() LOG.info(f"Handle callback: {call.data}") s = call.data.rfind(":") content = call.data[:s] key_type = call.data[s + 1 :] if key_type == BotKey.KEY_WATCH_PV_BY_ID: bot_utils.watch_v_by_id(id=content, type=0) elif key_type == BotKey.KEY_WATCH_FV_BY_ID: bot_utils.watch_v_by_id(id=content, type=1) elif key_type == BotKey.KEY_GET_SAMPLE_BY_ID: bot_utils.get_sample_by_id(id=content) elif key_type == BotKey.KEY_GET_MORE_MAGNETS_BY_ID: bot_utils.get_more_magnets_by_id(id=content) elif key_type == BotKey.KEY_RANDOM_GET_V_BY_STAR_ID: tmp = content.split("|") star_name = tmp[0] star_id = tmp[1] code, id = JBUS_UTIL.get_id_by_star_id(star_id=star_id) if bot_utils.check_success( code, f"从演职人员 {star_name} 随机选取作品" ): bot_utils.get_v_by_id(id=id) elif key_type == BotKey.KEY_GET_NEW_VS_BY_STAR_NAME_ID: tmp = content.split("|") star_name = tmp[0] star_id = tmp[1] bot_utils.get_star_new_vs_by_name_id(star_name=star_name, star_id=star_id) elif key_type == BotKey.KEY_RECORD_STAR_BY_STAR_NAME_ID: s = content.find("|") star_name = content[:s] star_id = content[s + 1 :] if BOT_DB.record_star_by_name_id(star_name=star_name, star_id=star_id): bot_utils.get_star_detail_record_by_name_id( star_name=star_name, star_id=star_id ) else: bot_utils.send_msg_code_op(500, f"收藏演职人员 {star_name}") elif key_type == BotKey.KEY_RECORD_V_BY_ID_STAR_IDS: res = content.split("|") id = res[0] stars = [] if res[1] != "": stars = [s for s in res[1:]] if BOT_DB.record_id_by_id_stars(id=id, stars=stars): bot_utils.get_v_detail_record_by_id(id=id) else: bot_utils.send_msg_code_op(500, f"收藏编号 {id}") elif key_type == BotKey.KEY_GET_STARS_RECORD: bot_utils.get_stars_record(page=int(content)) elif key_type == BotKey.KEY_GET_VS_RECORD: bot_utils.get_vs_record(page=int(content)) elif key_type == BotKey.KEY_GET_STAR_DETAIL_RECORD_BY_STAR_NAME_ID: s = content.find("|") bot_utils.get_star_detail_record_by_name_id( star_name=content[:s], star_id=content[s + 1 :] ) elif key_type == BotKey.KEY_GET_V_DETAIL_RECORD_BY_ID: bot_utils.get_v_detail_record_by_id(id=content) elif key_type == BotKey.KEY_GET_V_BY_ID: bot_utils.get_v_by_id(id=content) elif key_type == BotKey.KEY_RANDOM_GET_V_NICE: code, id = JLIB_UTIL.get_random_id_from_rank(0) if bot_utils.check_success(code, "随机获取高分影片"): bot_utils.get_v_by_id(id=id) elif key_type == BotKey.KEY_RANDOM_GET_V_NEW: code, id = JLIB_UTIL.get_random_id_from_rank(1) if bot_utils.check_success(code, "随机获取最新影片"): bot_utils.get_v_by_id(id=id) elif key_type == BotKey.KEY_UNDO_RECORD_V_BY_ID: op_undo_record_v = f"取消收藏编号 {content}" if BOT_DB.undo_record_id(id=content): bot_utils.send_msg_success_op(op_undo_record_v) else: bot_utils.send_msg_fail_reason_op( reason="文件解析错误", op=op_undo_record_v ) elif key_type == BotKey.KEY_UNDO_RECORD_STAR_BY_STAR_NAME_ID: s = content.find("|") op_undo_record_star = f"取消收藏演职人员 {content[:s]}" if BOT_DB.undo_record_star_by_id(star_id=content[s + 1 :]): bot_utils.send_msg_success_op(op_undo_record_star) else: bot_utils.send_msg_fail_reason_op( reason="文件解析错误", op=op_undo_record_star ) elif key_type == BotKey.KEY_SEARCH_STAR_BY_NAME: star_name = content star_name_alias = "" idx_alias = star_name.find("(") if idx_alias != -1: star_name_alias = star_name[idx_alias + 1 : -1] star_name = star_name[:idx_alias] if not bot_utils.search_star_by_name(star_name) and star_name_alias != "": bot_utils.send_msg( f"尝试搜索演职人员 {star_name} 的别名 {star_name_alias}..." ) bot_utils.search_star_by_name(star_name_alias) elif key_type == BotKey.KEY_GET_NICE_VS_BY_STAR_NAME: bot_utils.random_get_nice_star_vs(content) elif key_type == BotKey.KEY_DEL_V_CACHE: BOT_CACHE_DB.remove_cache(key=content, type=BotCacheDb.TYPE_V) BOT_CACHE_DB.remove_cache(key=content, type=BotCacheDb.TYPE_STARS_MSG) bot_utils.get_v_by_id(id=content) elif key_type == BotKey.KEY_BT_PAGE: tmp = content.split("|") bot_utils.show_bt_page(token=tmp[0], page=int(tmp[1])) def handle_message(message): bot_utils = BotUtils() bot_utils.send_action_typing() chat_id = str(message.chat.id) if chat_id.lower() != BOT_CFG.tg_chat_id.lower(): return bot_utils = BotUtils() if message.content_type != "text": msg = message.caption else: msg = message.text if not msg: return LOG.info(f'Get message: "{msg}"') msg = msg.lower().strip() msgs = msg.split(" ", 1) msg_cmd = msgs[0] msg_param = "" if len(msgs) > 1: msg_param = msgs[1].strip() if msg_cmd == "/help" or msg_cmd == "/start": bot_utils.send_msg(MSG_HELP) elif msg_cmd == "/stars": bot_utils.get_stars_record() elif msg_cmd == "/ids": bot_utils.get_vs_record() elif msg_cmd == "/record": if not os.path.exists(PATH_RECORD_FILE): bot_utils.send_msg_fail_reason_op( reason="还没有任何收藏记录", op="导出收藏记录" ) return BOT.send_document( chat_id=BOT_CFG.tg_chat_id, document=types.InputFile(PATH_RECORD_FILE) ) else: # 多轮会话:换一批 / 上一页 / 保存第 N 个 if bot_utils.handle_bt_followup(msg): return ids = ID_PAT.findall(msg) if not ids or len(ids) == 0: # 优先走 AI 自然语言理解搜索,失败则退回 BT 关键词搜索 if bot_utils.handle_nlp_search(msg): return bts = bot_utils.get_bts_by_keyword(msg) if not bts: bot_utils.send_msg_fail_reason_op( reason="没有找到相关资源", op=f"搜索 {html.escape(msg)}", ) return bot_utils.show_bt_results(msg, bts) else: ids = [id.lower() for id in ids] ids = set(ids) ids_msg = ", ".join(ids) bot_utils.send_msg(f"已识别到编号:{ids_msg},开始搜索...") for i, id in enumerate(ids): threading.Thread(target=bot_utils.get_v_by_id, args=(id,)).start() @BOT.callback_query_handler(func=lambda call: True) def my_callback_handler(call): EXECUTOR.submit(handle_callback, call) @BOT.message_handler(content_types=["text", "photo", "animation", "video", "document"]) def my_message_handler(message): EXECUTOR.submit(handle_message, message) def main(): try: bot_info = BOT.get_me() LOG.info(f"Connected to bot: @{bot_info.username} (ID: {bot_info.id})") if PIKPAK.enabled: ok, msg = PIKPAK.ensure_login() if ok: LOG.info(f"PikPak: {msg}") else: LOG.error(f"PikPak login failed: {msg}") LOG.info("Connected to api") except Exception as e: LOG.error(f"Unable to connect to bot or api: {e}") return BOT.set_my_commands([types.BotCommand(cmd, BOT_CMDS[cmd]) for cmd in BOT_CMDS]) BOT.infinity_polling() if __name__ == "__main__": main()