Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 47 additions & 17 deletions module/offline_download/offline_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,50 @@
DOWNLOAD_CACHE = {}
STEP_SELECTING_TOOL = "tool"

# ─── 路径 ID 注册(避免 callback_data 超过 64 字节限制)─────────────────────────
OD_PATH_COUNTER = 0
OD_PATH_MAP: dict[str, str] = {}


def od_register_path(path: str) -> str:
global OD_PATH_COUNTER
OD_PATH_COUNTER += 1
pid = str(OD_PATH_COUNTER)
OD_PATH_MAP[pid] = path
return pid


def od_get_path(pid: str) -> str:
return OD_PATH_MAP.get(pid, "")


# ─── 下载完成通知 ──────────────────────────────────────────────────────────────
PREV_UNDONE_IDS: set[str] = set()
PREV_DONE_IDS: set[str] = set()
_COMPLETION_INITIALIZED = False


async def check_download_completion(context) -> None:
"""定时检查离线下载任务,完成时主动推送通知(JobQueue 回调)"""
global PREV_UNDONE_IDS
global PREV_DONE_IDS, _COMPLETION_INITIALIZED
try:
undone = await openlist.get_offline_download_undone_task()
done = await openlist.get_offline_download_done_task()

current_undone_ids: set[str] = set()
if undone.data and isinstance(undone.data, list):
current_undone_ids = {str(t.get("id", "")) for t in undone.data if t.get("id")}

current_done_ids: set[str] = set()
done_map: dict[str, dict] = {}
if done.data and isinstance(done.data, list):
done_map = {str(t.get("id", "")): t for t in done.data if t.get("id")}
for t in done.data:
if t.get("id"):
tid = str(t["id"])
current_done_ids.add(tid)
done_map[tid] = t

# 首次运行:记录已有完成任务,不触发通知
if not _COMPLETION_INITIALIZED:
PREV_DONE_IDS = current_done_ids
_COMPLETION_INITIALIZED = True
return

newly_done = PREV_UNDONE_IDS & done_map.keys()
newly_done = current_done_ids - PREV_DONE_IDS
for task_id in newly_done:
task = done_map[task_id]
name, path = extract_file_info(task.get("name", ""))
Expand All @@ -52,7 +76,7 @@ async def check_download_completion(context) -> None:
except Exception as e:
logger.error(f"发送下载通知失败: {e}")

PREV_UNDONE_IDS = current_undone_ids
PREV_DONE_IDS = current_done_ids
except Exception as e:
logger.error(f"check_download_completion 失败: {e}")
STEP_BROWSING_PATH = "browse_path"
Expand Down Expand Up @@ -233,25 +257,27 @@ async def show_path_browser(query, cache, path):
files.append({"name": name})

buttons = []

# 添加子目录
for d in dirs[:10]:
name = d["name"]
sub_path = f"{path.rstrip('/')}/{name}/"
buttons.append([InlineKeyboardButton(f"📁 {name}", callback_data=f"od_cd_{sub_path}")])

path_id = od_register_path(sub_path)
buttons.append([InlineKeyboardButton(f"📁 {name}", callback_data=f"od_cd_{path_id}")])

# 添加文件(只显示前几个作为参考)
for f in files[:5]:
name = f["name"]
buttons.append([InlineKeyboardButton(f"📄 {name}", callback_data=f"od_file_{name}")])

# 添加确认按钮
buttons.append([InlineKeyboardButton("✅ 确认路径", callback_data="od_confirm_path")])

# 添加返回按钮
if path != "/":
parent_path = "/".join(path.rstrip("/").split("/")[:-1]) or "/"
buttons.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data=f"od_cd_{parent_path}")])
parent_id = od_register_path(parent_path)
buttons.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data=f"od_cd_{parent_id}")])

cache["path"] = path

Expand All @@ -268,7 +294,11 @@ async def show_path_browser(query, cache, path):
async def handle_path_browse(query, cache, data):
"""处理路径浏览回调"""
if data.startswith("od_cd_"):
path = data.replace("od_cd_", "")
path_id = data.replace("od_cd_", "")
path = od_get_path(path_id)
if not path:
await query.answer("路径已过期,请重新选择", show_alert=True)
return
await show_path_browser(query, cache, path)
return

Expand Down
100 changes: 83 additions & 17 deletions module/search/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

PAGE: dict[str, "Page"] = {}

# 结果缓存,用于下载按钮回调(key: "{cmid}_{global_index}")
S_RESULT_CACHE: dict[str, PanSouResult] = {}

PAN_TYPE_EMOJI = {
"baidu": "🔵百度",
"aliyun": "🟢阿里",
Expand Down Expand Up @@ -58,10 +61,11 @@ def build_result_item_sync(count: int, item: PanSouResult) -> str:

class Page:
PER_PAGE = 5
def __init__(self, results: list[PanSouResult], keyword: str = ""):

def __init__(self, results: list[PanSouResult], keyword: str = "", cmid: str = ""):
self.all_results = results
self.keyword = keyword
self.cmid = cmid
self.filter_type: Optional[str] = None
self.index = 0
self.per_page = self.PER_PAGE
Expand Down Expand Up @@ -115,31 +119,49 @@ def previous_page(self) -> str:

@property
def btn(self) -> list:
buttons = []

# 为当前页的磁力/电驴结果添加下载按钮
if self.cmid:
start = self.index * self.per_page
items = self.filtered_results[start:start + self.per_page]
dl_row = []
for i, item in enumerate(items):
if item.pan_type in ("magnet", "ed2k") and item.url:
global_idx = self.all_results.index(item)
count = start + i + 1
dl_row.append(InlineKeyboardButton(
f"📥#{count}",
callback_data=f"s_dl_{self.cmid}_{global_idx}"
))
for j in range(0, len(dl_row), 3):
buttons.append(dl_row[j:j + 3])

pan_types = list(set(r.pan_type for r in self.all_results))
pan_types.sort()

filter_buttons = []
row = [InlineKeyboardButton("全部", callback_data="search_filter_all")]

filter_row = [InlineKeyboardButton("全部", callback_data="search_filter_all")]
for pt in pan_types[:5]:
emoji = PAN_TYPE_EMOJI.get(pt, "📦")
row.append(InlineKeyboardButton(emoji, callback_data=f"search_filter_{pt}"))
filter_buttons.append(row)
filter_row.append(InlineKeyboardButton(emoji, callback_data=f"search_filter_{pt}"))
buttons.append(filter_row)

if len(pan_types) > 5:
row = []
for pt in pan_types[5:10]:
emoji = PAN_TYPE_EMOJI.get(pt, "📦")
row.append(InlineKeyboardButton(emoji, callback_data=f"search_filter_{pt}"))
if row:
filter_buttons.append(row)
buttons.append(row)

nav_buttons = [
InlineKeyboardButton("⬆️上一页", callback_data="search_previous_page"),
InlineKeyboardButton(f"{self.index + 1}/{self.page_count}", callback_data="search_pages"),
InlineKeyboardButton("⬇️下一页", callback_data="search_next_page"),
]

return filter_buttons + [nav_buttons]
buttons.append(nav_buttons)

return buttons


async def s_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
Expand Down Expand Up @@ -172,12 +194,18 @@ async def build_result(content: list[PanSouResult], msg, keyword: str):
chat_id = msg.chat.id
message_id = msg.message_id
cmid = f"{chat_id}|{message_id}"

page = Page(content, keyword)

# 清理旧缓存,填充新结果
for k in [k for k in S_RESULT_CACHE if k.startswith(f"{cmid}_")]:
del S_RESULT_CACHE[k]
for i, item in enumerate(content):
S_RESULT_CACHE[f"{cmid}_{i}"] = item

page = Page(content, keyword, cmid)
PAGE[cmid] = page

text = page.get_text_with_info()

return text, page.btn


Expand Down Expand Up @@ -212,9 +240,47 @@ async def search_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
pan_type = data.replace("search_filter_", "")
page.set_filter(pan_type)
text = page.get_text_with_info()
elif data.startswith("s_dl_"):
# 解析 s_dl_{cmid}_{global_idx},cmid 含 "|" 故从右截取最后一段
rest = data[5:]
last_sep = rest.rfind("_")
cmid_part = rest[:last_sep]
try:
global_idx = int(rest[last_sep + 1:])
except ValueError:
return

item = S_RESULT_CACHE.get(f"{cmid_part}_{global_idx}")
if not item or not item.url:
await query.answer("链接已过期,请重新搜索", show_alert=True)
return

try:
from config.config import reload_od_cfg
cfg = reload_od_cfg()
dl_path = cfg.download_path if cfg and cfg.download_path else "/"
dl_tool = cfg.download_tool if cfg and cfg.download_tool else "qbittorrent"
except Exception:
dl_path = "/"
dl_tool = "qbittorrent"

file_name = item.name[:30] + "..." if len(item.name) > 30 else item.name
from module.torrent_search.torrent_search import DOWNLOAD_INFO, build_download_confirm_message
DOWNLOAD_INFO[msg.chat.id] = {
"result": item,
"index": global_idx,
"cmid": cmid_part,
"path": dl_path,
"tool": dl_tool,
"url": item.url,
"file_name": file_name,
}
text_confirm, keyboard = await build_download_confirm_message(msg.chat.id, DOWNLOAD_INFO[msg.chat.id])
await msg.edit_text(text_confirm, reply_markup=keyboard, parse_mode="Markdown")
return
else:
return

try:
await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(page.btn), disable_web_page_preview=True)
except Exception:
Expand All @@ -223,4 +289,4 @@ async def search_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):

def register_handlers(app: Application):
app.add_handler(CommandHandler("s", s_command))
app.add_handler(CallbackQueryHandler(search_callback, pattern=r"^search"))
app.add_handler(CallbackQueryHandler(search_callback, pattern=r"^(search|s_dl_)"))