Termux之longpt


一、longpt签到脚本

1.抓取Cookie

2.longpt.py

import requests
import time

# 如果走 Cookie 路线
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Cookie': '这里替换成你抓取到的完整 Cookie 字符串'
}

# 签到接口地址,根据你 F12 抓到的实际地址填
SIGN_URL = "https://longpt.org/attendance.php"

def pt_signin():
    try:
        # 根据实际抓包情况选择 get 或 post
        response = requests.get(SIGN_URL, headers=HEADERS, timeout=10)
        response.raise_for_status()
        
        # 简单判断是否签到成功,具体关键字看返回的 HTML 内容
        if "签到成功" in response.text or "已签到" in response.text:
            print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 签到成功!")
        else:
            print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 状态未知,返回内容:{response.text[:100]}")
    except Exception as e:
        print(f"签到出错: {e}")

if __name__ == "__main__":
    pt_signin()

3.PM2启动管理

cd ~/189py && pm2 start longpt.py --name "longpt" --cron-restart="0 9 * * *" --no-autorestart

每天上午 9 点会自动运行:你在命令里写的 –cron-restart=“0 9 * * *” 就是标准的 Cron 表达式,代表每天的 09:00:00 触发。它会按照你运行 Termux 设备的本地系统时间来准时执行

关于执行状态的补充说明:你带上了 –no-autorestart 这个参数,逻辑非常完美。因为这个签到脚本属于“跑完就结束”的一次性任务,并非需要长期驻留后台的服务。

执行完这次签到后,这个进程稍后在 PM2 里的状态会自动变成 stopped。

不用担心它停了,这属于正常现象。到了明天上午 9 点整,PM2 会按照 Cron 规则自动把它重新唤醒拉起,执行完毕后再次停止,每天周而复始。

你可以放心地把它挂在后台了。如果你后续想确认它到底有没有按时打卡,随时可以用 pm2 logs longpt 查看未来的签到日志

二、H5网页端

1.安装强大的 HTML 解析库 BeautifulSoup:

pip install beautifulsoup4

2.安装 Flask 框架

pip install flask

3.pm2管理 启动管理

cd 189py && pm2 start h5long.py --name "h5long"

定时签到

pm2 start "curl http://127.0.0.1:5051/api/signin" --name "as-long" --cron-restart="0 10 * * *" --no-autorestart

4.h5long.py

from flask import Flask, render_template_string, Response, request, jsonify
import requests
from bs4 import BeautifulSoup
import urllib.parse
import re
import os
import json
import threading
import time

app = Flask(__name__)

# ================= 必须配置的区域 =================
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Cookie': 'SITE_TOTAL_ID=b06a69304be7b561a043e0a5854d8e72; c_secure_pass=eyJ1c2VyX2lkIjoiMzQyODMiLCJleHBpcmVzIjoxODEzNDk1MjQyfS5lZjAyNTU4NDlkMzlkYjZhNzM4NmM5NTUzZTNlMWFjM2Q4M2VlYjE3NTUwNjI1ZjE3NjRkMzAwNjlmZTlmM2Zj; server_name_session=8d0f548489f4150c8755cd0497ba1583; XSRF-TOKEN=eyJpdiI6IjZjTVpMTkc0dmRWQk5hbzdEeTJGZVE9PSIsInZhbHVlIjoiTXEycXdQK3cxSVhCbGRLN01sM0lWUzRlb1k3MExsUzJXU082NkZzWFNCUE1TQkJzLytLR3dXTG5IczI1Zm9CWDVTN3c2dXdXL0d3YWxuQ21hZWlyN0Q3V1RFeEgwcG5SZFAwUEppaVd6Njg5YnZ2SEtaa2ZSYUxuWGZoaEJmbGQiLCJtYWMiOiJlNWY5ZGM0MmIxMzhkNzFlMWZhZmQyMGFkZWQwNDNlMjQ0N2EyZTlhZTY3MTVlZjc3MWQ3YTM1ZGExNWNhYmY1IiwidGFnIjoiIn0%3D; nexusphp_session=eyJpdiI6InV6d29tOTlJTE5pU2NmUzU2RDQwWWc9PSIsInZhbHVlIjoiVXc1dUVncTI5RVRCU2dUL3c3bS91OTU4ajN0blFOVHNhRHBsY2swUlNIMnduNVRqTW1NVjBlaHVUa3VyZnFXRC9oNGwxdGlyM0NFdCtkUkVXVm9DdlBuSlFWOStuNE9yeFhOelBtS3lad0VZQzg5ekxVWEE4MXNPRFc3ZHIvYnciLCJtYWMiOiJmZDE5MGQ3Nzk3MjlmZjcwY2Q4YTU3NWI1ZGE0YjQ0ZTRiMmY1ODM3OWYzNGNlZTYxMGNlNTA3NmU0M2QzNmE1IiwidGFnIjoiIn0%3D; cf_clearance=vFfWiQs3D8QlbbS5.Y8gP.qI9eg_zkm3NDUFKMFRFkM-1789555622-1.2.1.1-XUcX7UQU1ZLQo_GsH57MbFmsC29A0klq_4K_D4EQmj_q1HY0oMVdS16NDkVBUuBzKPozx0BVCDvulNXSE25EgEXeXVIBv._FIRIatr0IjUdj0Nzy7Rs67Pt2Pg7Z_rmBNfZFuwz.GBb_UYVy0SWkOn7amlHl.ZXKc26K_P3ABYeHfV4LJaqKdgBtTiAY9Vz.xuWD7N48sxr08kvKMTppmmlh8YHoc9wyEwphpla_GQ0K4tDTtD55v.ew5e.4BI.TD5Gp7Bwh3yY9o1BUJzULAqeLFZXYduYWLuShQ.2NSrYNHvRyK85j5vuYoWrOflx4dJW.15_4Ns421lyItYWmJU5ZH0DYS.VUW0sLE8umB_5lH2wMxNk6ABq3oEMxX9yhOkxuFLrwYNHu756bR5FycUaEOWgcOJXUdqN20oKw3OgZk_kUtbMgGSdLIijF.p9h' 
}

OFFICIAL_URL = "https://longpt.org/torrents.php"
INDEX_URL = "https://longpt.org/index.php"
SIGNIN_URL = "https://longpt.org/attendance.php"
HISTORY_FILE = "longpt_history.json"
# =================================================

# --- 核心引擎:深度抓取详情页海报、体积与所有促销标签 ---
def fetch_details_info(tid):
    try:
        r = requests.get(f"https://longpt.org/details.php?id={tid}", headers=HEADERS, timeout=10)
        soup = BeautifulSoup(r.text, 'html.parser')

        img_src = ""
        descr_div = soup.find('div', id='kdescr')
        if descr_div:
            img = descr_div.find('img')
            if img: img_src = img.get('data-src') or img.get('src') or ""
        if not img_src:
            for img in soup.find_all('img'):
                src = img.get('data-src') or img.get('src') or ""
                src_lower = src.lower()
                if src and not any(x in src_lower for x in ['category', 'smilies', 'icon', 'logo', 'avatar', 'rating', 'button', 'rule']):
                    img_src = src
                    break
        if img_src and not img_src.startswith('http'):
            img_src = "https://longpt.org/" + img_src

        size = "未知"
        size_match = re.search(r'大小.*?([\d\.]+\s*[KMGTP]B)', r.text, re.IGNORECASE)
        if size_match: size = size_match.group(1)

        extra_tags = []
        for img in soup.find_all('img'):
            alt = str(img.get('alt') or img.get('title') or '').strip()
            if alt.upper() in ['FREE', '免费']:
                if 'Free' not in extra_tags: extra_tags.append('Free')
            elif any(x in alt for x in ['50%', '2X', '30%', '热门']):
                if alt not in extra_tags: extra_tags.append(alt)
        
        page_text = soup.get_text(separator=' ')
        time_match = re.search(r'剩余时间[::\s]*([\d]+天[\d]+[时分秒]+|[\d]+[天时分秒]+)', page_text)
        if time_match: extra_tags.append(f"剩余时间: {time_match.group(1)}")
            
        h1 = soup.find('h1')
        if h1 and 'Free' not in extra_tags:
            if '免费' in h1.get_text() or 'Free' in h1.get_text():
                extra_tags.append('Free')
        
        return img_src, size, extra_tags
    except Exception as e:
        return "", "未知", []

# --- 守护进程:后台静默刷新 30 条最近发布 ---
def background_monitor():
    while True:
        try:
            resp = requests.get(INDEX_URL, headers=HEADERS, timeout=10)
            if "登录" not in resp.text or "index.php" in resp.url:
                soup = BeautifulSoup(resp.text, 'html.parser')
                current_fetched = []
                
                for title_a in soup.find_all('a', href=lambda h: h and h.startswith('details.php?id=')):
                    tid = re.search(r'id=(\d+)', title_a['href']).group(1)
                    title = title_a.get('title') or title_a.get_text(strip=True)
                    if not title and title_a.find('img'): title = title_a.find('img').get('alt') or title_a.find('img').get('title')
                    if not title: continue
                    
                    link = "https://longpt.org/" + title_a['href']
                    container = title_a.find_parent('tr') or title_a.find_parent('div')
                    img_src, subtitle, seeders, leechers, size = "", "", "-", "-", "加载中..."
                    
                    if container:
                        if container.name == 'tr':
                            tds = container.find_all('td')
                            if len(tds) >= 3:
                                seeders, leechers = tds[-2].get_text(strip=True), tds[-1].get_text(strip=True)

                        for img in container.find_all('img'):
                            src = img.get('data-src') or img.get('src') or ""
                            if src and 'category' not in src.lower() and 'cat' not in src.lower():
                                img_src = src if src.startswith('http') else "https://longpt.org/" + src
                                break
                        for icon in container.find_all('img'):
                            alt = icon.get('alt') or icon.get('title')
                            if alt and icon.get('src', '') != img_src: icon.replace_with(soup.new_tag("span", string=f" [{alt}] "))
                        subtitle = container.get_text(separator=' | ', strip=True).replace(title, '').strip(' | ')
                        if seeders != "-" and subtitle.endswith(leechers): subtitle = subtitle.rsplit('|', 2)[0].strip(' | ')
                        
                        subtitle = re.sub(r'\[?(?:剩余时间)[::]*\]?[\s\|]*([\d]+天[\d]+[时分秒]+|[\d]+[天时分秒]+)', r'[剩余时间: \1]', subtitle)
                        subtitle = subtitle.replace('[ | ]', '').replace('[ | | ]', '')
                            
                        tags, clean_sub = [], subtitle
                        for m in re.finditer(r'\[(.*?)\]', subtitle):
                            tag = m.group(1).strip(' |')
                            # 精准打击:只抓热门、折扣和时间做成徽章
                            if any(x in tag.upper() for x in ['热门', 'FREE', '免费', '%', '2X', '剩余']):
                                if len(tag) <= 18: tags.append(tag)
                                clean_sub = clean_sub.replace(m.group(0), '')
                            elif tag not in ['Sticky', 'poster', '置顶']:
                                # 其他参数(如国语、4K)脱去方括号,还给副标题
                                clean_sub = clean_sub.replace(m.group(0), tag)
                            else:
                                clean_sub = clean_sub.replace(m.group(0), '')
                        
                        clean_sub = re.sub(r'热门|免费', '', clean_sub)
                        subtitle = re.sub(r'\|(?:\s*\|)+', '|', clean_sub).strip(' |')

                    if not img_src: img_src = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='140'><rect width='100%' height='100%' fill='%23e6e6e6'/><text x='50%' y='50%' fill='%23999' font-size='12' font-family='sans-serif' text-anchor='middle' dominant-baseline='middle'>加载海报</text></svg>"

                    current_fetched.append({
                        'id': tid, 'title': title, 'link': link, 'subtitle': subtitle, 'img': img_src,
                        'seeders': seeders, 'leechers': leechers, 'size': size, 'tags': tags
                    })

                history_data = []
                if os.path.exists(HISTORY_FILE):
                    try:
                        with open(HISTORY_FILE, 'r', encoding='utf-8') as f: history_data = json.load(f)
                    except: pass
                    
                history_dict = {str(item['id']): item for item in history_data}
                
                for item in current_fetched:
                    tid_str = str(item['id'])
                    if tid_str not in history_dict or 'data:image/svg+xml' in history_dict[tid_str]['img'] or history_dict[tid_str].get('size') in ['未知', '加载中...']:
                        det_img, det_size, det_tags = fetch_details_info(item['id'])
                        if det_img: item['img'] = det_img
                        if det_size and det_size != "未知": item['size'] = det_size
                        if det_tags:
                            new_tags = item.get('tags', [])
                            for t in det_tags:
                                if t not in new_tags: new_tags.append(t)
                            item['tags'] = new_tags

                    if tid_str in history_dict:
                        cached = history_dict[tid_str]
                        if 'data:image/svg+xml' not in cached['img']: item['img'] = cached['img']
                        if cached.get('size') not in ['未知', '加载中...']: item['size'] = cached['size']
                        merged_tags = item.get('tags', [])
                        for t in cached.get('tags', []):
                            if t not in merged_tags: merged_tags.append(t)
                        item['tags'] = merged_tags
                        
                    history_dict[tid_str] = item

                merged = list(history_dict.values())
                merged.sort(key=lambda x: int(x['id']), reverse=True)
                with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(merged[:30], f, ensure_ascii=False, indent=2)
        except Exception as e: pass
        time.sleep(1200)

threading.Thread(target=background_monitor, daemon=True).start()

HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>LongPT 手机端</title>
    <style>
        :root { --bg-color: #f7f7f7; --card-bg: #ffffff; --text-main: #333333; --text-sub: #888888; --border-color: #eeeeee; --topbar-bg: #ffffff; --icon-bg: #f0f0f5; }
        [data-theme="dark"] { --bg-color: #121212; --card-bg: #1e1e1e; --text-main: #eeeeee; --text-sub: #aaaaaa; --border-color: #333333; --topbar-bg: #1e1e1e; --icon-bg: #333333; }
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; padding: 0; padding-top: 55px; transition: background-color 0.3s; }
        
        .top-bar { position: fixed; top: 0; left: 0; right: 0; height: 50px; background: var(--topbar-bg); display: flex; align-items: center; justify-content: space-between; padding: 0 15px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); z-index: 100; transition: background 0.3s; }
        
        /* 修改点1:更精细轻盈的菜单图标 */
        .menu-btn { width: 22px; height: 16px; position: relative; cursor: pointer; display: flex; flex-direction: column; justify-content: space-between; padding: 0; }
        .menu-btn span { display: block; height: 2px; width: 100%; background: var(--text-main); border-radius: 2px; transition: all 0.3s ease; }
        .menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
        .menu-btn.open span:nth-child(2) { opacity: 0; }
        .menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

        .logo { font-weight: 800; font-size: 20px; color: var(--text-main); letter-spacing: 1px; }
        .top-placeholder { width: 22px; }

        .sidebar { position: fixed; top: 0; left: -260px; bottom: 0; width: 260px; background: var(--card-bg); z-index: 1001; transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 2px 0 10px rgba(0,0,0,0.1); padding-top: 60px; display: flex; flex-direction: column; }
        .sidebar.open { left: 0; }
        .menu-item { padding: 16px 24px; font-size: 16px; font-weight: 500; color: var(--text-main); text-decoration: none; display: block; border-bottom: 1px solid var(--border-color); }
        .menu-item.active { color: #007aff; border-left: 4px solid #007aff; padding-left: 20px; background: rgba(0,122,255,0.05); }
        
        .theme-toggle-icon { margin: auto auto 30px 24px; width: 40px; height: 40px; border-radius: 20px; background: var(--icon-bg); display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; user-select: none; transition: transform 0.2s; }
        .theme-toggle-icon:active { transform: scale(0.9); }
        
        .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
        .overlay.open { opacity: 1; pointer-events: auto; }

        .container { padding: 10px; }
        .search-container { display: flex; gap: 8px; margin-bottom: 15px; padding: 0 5px; }
        .search-input { flex: 1; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; outline: none; font-size: 14px; background: var(--card-bg); color: var(--text-main); }
        .search-btn { background: #2563eb; color: #fff; border: none; padding: 0 18px; border-radius: 6px; font-weight: bold; font-size: 14px; }
        .list-item { display: flex; gap: 12px; padding: 15px 5px; border-bottom: 1px solid var(--border-color); }
        .poster-wrapper { position: relative; flex-shrink: 0; width: 100px; height: 140px; }
        .poster { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; background-color: #e0e0e0; transition: opacity 0.5s ease-in-out; }
        .tag-free { position: absolute; top: 0; left: 0; background: #22c55e; color: #fff; font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 8px 0 8px 0; z-index: 10; }
        .info { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; justify-content: space-between; padding-bottom: 2px; }
        .title { font-size: 15px; font-weight: bold; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.3; }
        .subtitle { font-size: 12px; color: var(--text-sub); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 8px; }
        .stats-row { font-size: 12px; color: var(--text-sub); display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
        .stat-item { display: flex; align-items: center; gap: 2px; }
        .seed-icon { color: #22c55e; font-size: 10px; }
        .leech-icon { color: #ef4444; font-size: 10px; }
        
        /* 修改点3:防止标签换行挤压下载按钮 */
        .bottom-actions { display: flex; justify-content: space-between; align-items: flex-end; margin-top: auto; gap: 10px; }
        .tags-list { display: flex; gap: 4px; flex-wrap: wrap; flex: 1; }
        .badge { background: #8b5cf6; color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 4px; }
        .badge-discount { background: #ef4444; color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 4px; }
        
        .download-btn { background: #2563eb; color: #fff; border: none; padding: 4px 12px; border-radius: 4px; font-size: 11px; font-weight: bold; text-decoration: none; flex-shrink: 0; white-space: nowrap; margin-bottom: 2px; }
        .download-btn:active { opacity: 0.8; }
        
        .account-box { background: var(--card-bg); border-radius: 12px; padding: 20px; text-align: center; border: 1px solid var(--border-color); }
        .avatar { width: 70px; height: 70px; background: #2563eb; color: #fff; border-radius: 35px; line-height: 70px; font-size: 24px; margin: 0 auto 10px; }
        .account-name { font-size: 18px; font-weight: bold; }
        .stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 20px; }
        .stat-val { font-size: 18px; font-weight: bold; color: #2563eb; margin-bottom: 4px; }
        .stat-lbl { font-size: 12px; color: var(--text-sub); }
        .btn-signin { background: #22c55e; color: #fff; width: 100%; padding: 12px; font-size: 16px; border-radius: 8px; font-weight: bold; margin-top: 25px; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <script>
        function setTheme(theme) {
            const html = document.documentElement;
            if (theme === 'dark') { html.setAttribute('data-theme', 'dark'); }
            else if (theme === 'light') { html.removeAttribute('data-theme'); }
            else { 
                if (window.matchMedia('(prefers-color-scheme: dark)').matches) html.setAttribute('data-theme', 'dark');
                else html.removeAttribute('data-theme');
            }
        }
        let currentTheme = localStorage.getItem('longpt_theme') || 'auto';
        setTheme(currentTheme);
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
            if (localStorage.getItem('longpt_theme') === 'auto') setTheme('auto');
        });
    </script>

    <div class="top-bar">
        <div class="menu-btn" id="menuBtn" onclick="toggleMenu()">
            <span></span><span></span><span></span>
        </div>
        <div class="logo">LongPT</div>
        <div class="top-placeholder"></div>
    </div>

    <div class="overlay" id="overlay" onclick="toggleMenu()"></div>
    <div class="sidebar" id="sidebar">
        <a href="/?tab=official" class="menu-item {% if tab == 'official' %}active{% endif %}">官方发布</a>
        <a href="/?tab=recent" class="menu-item {% if tab == 'recent' %}active{% endif %}">最近发布</a>
        <a href="/?tab=search" class="menu-item {% if tab == 'search' %}active{% endif %}">全站搜索</a>
        <a href="/?tab=account" class="menu-item {% if tab == 'account' %}active{% endif %}">我的帐号</a>
        
        <div class="theme-toggle-icon" id="themeIcon" onclick="cycleTheme()">🔄</div>
    </div>

    <div class="container">
        {% if tab == 'search' %}
        <form class="search-container" action="/" method="GET">
            <input type="hidden" name="tab" value="search">
            <input type="text" name="q" class="search-input" placeholder="输入关键词搜索..." value="{{ query }}">
            <button type="submit" class="search-btn">搜索</button>
        </form>
        {% endif %}

        {% if error %}<div style="text-align:center; color:#ef4444; margin-top:30px; font-weight:bold;">{{ error }}</div>{% endif %}

        {% if tab == 'account' and user_info %}
        <div class="account-box">
            <div class="avatar">{{ user_info.username[:1] | upper }}</div>
            <div class="account-name">{{ user_info.username }}</div>
            <div class="stat-grid">
                <div><div class="stat-val">{{ user_info.bonus }}</div><div class="stat-lbl">魔力值</div></div>
                <div><div class="stat-val">{{ user_info.ratio }}</div><div class="stat-lbl">分享率</div></div>
                <div><div class="stat-val">{{ user_info.upload }}</div><div class="stat-lbl">总上传</div></div>
                <div><div class="stat-val">{{ user_info.download }}</div><div class="stat-lbl">总下载</div></div>
            </div>
            <button id="signinBtn" class="btn-signin" onclick="doSignIn()">✅ 手动签到</button>
        </div>
        <script>
            function doSignIn() {
                const btn = document.getElementById('signinBtn');
                btn.innerText = '签到中...'; btn.disabled = true;
                fetch('/api/signin').then(r => r.json()).then(data => {
                    alert(data.msg); btn.innerText = '✅ 手动签到'; btn.disabled = false;
                }).catch(e => { alert('请求失败'); btn.innerText = '✅ 手动签到'; btn.disabled = false; });
            }
        </script>
        
        {% elif tab != 'account' %}
            {% for item in torrents %}
            <div class="list-item" id="item-{{ item.id }}">
                <div class="poster-wrapper">
                    {% set needs_lazy = ('data:image/svg+xml' in item.img) or (item.size == '未知') or (item.size == '加载中...') %}
                    <img class="poster {% if needs_lazy %}lazy-load{% endif %}" 
                         src="{{ item.img }}" 
                         {% if needs_lazy %}data-tid="{{ item.id }}"{% endif %}
                         referrerpolicy="no-referrer" alt="海报" onclick="window.open('{{ item.link }}', '_blank')">
                    {% if 'Free' in item.tags or '免费' in item.tags %}
                        <div class="tag-free">FREE</div>
                    {% endif %}
                </div>
                <div class="info">
                    <div class="title" onclick="window.open('{{ item.link }}', '_blank')">{{ item.title }}</div>
                    <div class="subtitle">{{ item.subtitle }}</div>
                    
                    <div class="stats-row">
                        <span class="stat-item" id="size-{{ item.id }}">📄 {{ item.size }}</span>
                        <span class="stat-item"><span class="seed-icon">▲</span> {{ item.seeders }}</span>
                        <span class="stat-item"><span class="leech-icon">▼</span> {{ item.leechers }}</span>
                    </div>

                    <div class="bottom-actions">
                        <div class="tags-list">
                            {% for tag in item.tags %}
                                {% if tag not in ['Free', '免费'] %}
                                    {% if '%' in tag or '2X' in tag or '剩余' in tag %}
                                        <span class="badge-discount">{{ tag }}</span>
                                    {% else %}
                                        <span class="badge">{{ tag }}</span>
                                    {% endif %}
                                {% endif %}
                            {% endfor %}
                        </div>
                        <a href="/download/{{ item.id }}" class="download-btn">下载</a>
                    </div>
                </div>
            </div>
            {% endfor %}
            
            {% if tab == 'search' and not torrents and query %}
                <div style="text-align:center; color:var(--text-sub); margin-top:30px;">找到 0 个结果</div>
            {% elif tab == 'search' and torrents %}
                <div style="text-align:center; color:var(--text-sub); margin: 20px 0; font-size:12px;">已展示当前页全部结果</div>
            {% elif tab == 'recent' %}
                <div style="text-align:center; color:var(--text-sub); margin: 20px 0; font-size:12px;">后台静默监控中:已累积缓存最高 30 条历史记录</div>
            {% endif %}
        {% endif %}
    </div>

    <script>
        const sidebar = document.getElementById('sidebar');
        const overlay = document.getElementById('overlay');
        const menuBtn = document.getElementById('menuBtn');
        const themeIcon = document.getElementById('themeIcon');

        function toggleMenu() { 
            sidebar.classList.toggle('open'); 
            overlay.classList.toggle('open'); 
            menuBtn.classList.toggle('open');
        }

        const icons = { 'auto': '🔄', 'dark': '🌙', 'light': '☀️' };
        themeIcon.innerText = icons[currentTheme];

        function cycleTheme() {
            if (currentTheme === 'auto') currentTheme = 'dark';
            else if (currentTheme === 'dark') currentTheme = 'light';
            else currentTheme = 'auto';
            localStorage.setItem('longpt_theme', currentTheme);
            setTheme(currentTheme);
            themeIcon.innerText = icons[currentTheme];
        }

        document.addEventListener("DOMContentLoaded", function() {
            const lazyItems = document.querySelectorAll('.lazy-load');
            lazyItems.forEach(img => {
                const tid = img.getAttribute('data-tid');
                if (tid) {
                    fetch('/api/extra/' + tid)
                        .then(r => r.json())
                        .then(data => {
                            if (data.img) {
                                img.src = data.img;
                                img.classList.remove('lazy-load');
                            }
                            if (data.size && data.size !== '未知' && data.size !== '加载中...') {
                                const sizeSpan = document.getElementById('size-' + tid);
                                if (sizeSpan) sizeSpan.innerHTML = '📄 ' + data.size;
                            }
                            
                            if (data.tags && data.tags.length > 0) {
                                const itemDiv = document.getElementById('item-' + tid);
                                if (itemDiv) {
                                    const tagsList = itemDiv.querySelector('.tags-list');
                                    const posterWrapper = itemDiv.querySelector('.poster-wrapper');
                                    const existingTags = Array.from(tagsList.querySelectorAll('span')).map(s => s.innerText);
                                    let hasFree = !!posterWrapper.querySelector('.tag-free');
                                    
                                    data.tags.forEach(tag => {
                                        if (tag === 'Free' || tag === '免费') {
                                            if (!hasFree) {
                                                posterWrapper.insertAdjacentHTML('beforeend', '<div class="tag-free">FREE</div>');
                                                hasFree = true;
                                            }
                                        } else {
                                            const isMatch = existingTags.some(t => t.includes(tag) || tag.includes(t));
                                            if (!isMatch) {
                                                if (tag.includes('%') || tag.includes('2X') || tag.includes('剩余')) {
                                                    tagsList.insertAdjacentHTML('beforeend', `<span class="badge-discount">${tag}</span>`);
                                                } else {
                                                    tagsList.insertAdjacentHTML('beforeend', `<span class="badge">${tag}</span>`);
                                                }
                                                existingTags.push(tag);
                                            }
                                        }
                                    });
                                }
                            }
                        })
                        .catch(e => console.error('懒加载失败:', e));
                }
            });
        });
    </script>
</body>
</html>
"""

@app.route('/')
def index():
    tab = request.args.get('tab', 'official')
    query = request.args.get('q', '')
    torrents = []
    user_info = {}
    seen_ids = set() 
    error_msg = None
    
    try:
        if tab == 'account':
            resp = requests.get(INDEX_URL, headers=HEADERS, timeout=10)
            if "登录" in resp.text and "index.php" not in resp.url:
                error_msg = "⚠️ Cookie 已失效,请在代码中更新 Cookie!"
            else:
                soup = BeautifulSoup(resp.text, 'html.parser')
                page_text = soup.get_text()
                
                user_match = re.search(r'欢迎回来,\s*([^\[\s]+)', page_text)
                bonus_match = re.search(r'魔力值.*?:\s*([\d\.,]+)', page_text)
                ratio_match = re.search(r'分享率:\s*([\d\.,]+)', page_text)
                up_match = re.search(r'上传量:\s*([\d\.,\s]+[KMGTP]B)', page_text)
                down_match = re.search(r'下载量:\s*([\d\.,\s]+[KMGTP]B)', page_text)
                
                user_info = {
                    'username': user_match.group(1) if user_match else "未知用户",
                    'bonus': bonus_match.group(1) if bonus_match else "0",
                    'ratio': ratio_match.group(1) if ratio_match else "0",
                    'upload': up_match.group(1) if up_match else "0 GB",
                    'download': down_match.group(1) if down_match else "0 GB"
                }

        elif tab in ['official', 'search']:
            if tab == 'search' and not query: pass
            else:
                if tab == 'search':
                    safe_query = urllib.parse.quote(query)
                    target_url = f"https://longpt.org/torrents.php?incldead=1&spstate=0&inclbookmarked=0&search={safe_query}&search_area=0&search_mode=0"
                else: target_url = OFFICIAL_URL
                    
                resp = requests.get(target_url, headers=HEADERS, timeout=10)
                if "登录" in resp.text and "torrents.php" not in resp.url:
                    error_msg = "⚠️ Cookie 已失效!"
                elif resp.status_code == 200:
                    soup = BeautifulSoup(resp.text, 'html.parser')
                    for row in soup.find_all('tr'):
                        tds = row.find_all('td')
                        if len(tds) < 6: continue
                        title_a = row.find('a', href=lambda h: h and h.startswith('details.php?id='))
                        if not title_a: continue
                        
                        tid = re.search(r'id=(\d+)', title_a['href']).group(1)
                        title = title_a.get('title') or title_a.get_text(strip=True)
                        if not title and title_a.find('b'): title = title_a.find('b').get_text(strip=True)
                        if not title or tid in seen_ids: continue
                        seen_ids.add(tid)
                        
                        link = "https://longpt.org/" + title_a['href']
                        
                        try:
                            size, seeders, leechers = tds[-5].get_text(strip=True), tds[-4].get_text(strip=True), tds[-3].get_text(strip=True)
                        except:
                            size, seeders, leechers = "未知", "-", "-"

                        img_src = ""
                        for img in (title_a.find_parent('table') or row).find_all('img'):
                            src = img.get('data-src') or img.get('src') or ""
                            if src and 'category' not in src.lower() and 'cat' not in src.lower():
                                img_src = src if src.startswith('http') else "https://longpt.org/" + src
                                break
                                
                        subtitle = ""
                        td = title_a.find_parent('td')
                        if td:
                            for icon in td.find_all('img'):
                                alt = icon.get('alt') or icon.get('title')
                                if alt and icon.get('src', '') != img_src:
                                    icon.replace_with(soup.new_tag("span", string=f" [{alt}] "))
                            subtitle = td.get_text(separator=' | ', strip=True).replace(title, '').strip(' | ')
                            
                        subtitle = re.sub(r'\[?(?:剩余时间)[::]*\]?[\s\|]*([\d]+天[\d]+[时分秒]+|[\d]+[天时分秒]+)', r'[剩余时间: \1]', subtitle)
                        subtitle = subtitle.replace('[ | ]', '').replace('[ | | ]', '')
                            
                        tags, clean_sub = [], subtitle
                        for m in re.finditer(r'\[(.*?)\]', subtitle):
                            tag = m.group(1).strip(' |')
                            # 修改点2:精确提取徽章,普通文本脱去中括号还给副标题
                            if any(x in tag.upper() for x in ['热门', 'FREE', '免费', '%', '2X', '剩余']):
                                if len(tag) <= 18: tags.append(tag)
                                clean_sub = clean_sub.replace(m.group(0), '')
                            elif tag not in ['Sticky', 'poster', '置顶']:
                                clean_sub = clean_sub.replace(m.group(0), tag)
                            else:
                                clean_sub = clean_sub.replace(m.group(0), '')
                        
                        clean_sub = re.sub(r'热门|免费', '', clean_sub)
                        subtitle = re.sub(r'\|(?:\s*\|)+', '|', clean_sub).strip(' |')
                            
                        torrents.append({
                            'id': tid, 'title': title, 'link': link, 'subtitle': subtitle, 'img': img_src,
                            'seeders': seeders, 'leechers': leechers, 'size': size, 'tags': tags
                        })

        elif tab == 'recent':
            if os.path.exists(HISTORY_FILE):
                try:
                    with open(HISTORY_FILE, 'r', encoding='utf-8') as f: torrents = json.load(f)
                except: pass

            resp = requests.get(INDEX_URL, headers=HEADERS, timeout=10)
            if "登录" in resp.text and "index.php" not in resp.url:
                error_msg = "⚠️ Cookie 已失效!"
            elif resp.status_code == 200:
                soup = BeautifulSoup(resp.text, 'html.parser')
                current_fetched = []
                for title_a in soup.find_all('a', href=lambda h: h and h.startswith('details.php?id=')):
                    tid = re.search(r'id=(\d+)', title_a['href']).group(1)
                    title = title_a.get('title') or title_a.get_text(strip=True)
                    if not title and title_a.find('img'): title = title_a.find('img').get('alt') or title_a.find('img').get('title')
                    if not title or tid in seen_ids: continue
                    seen_ids.add(tid)
                    
                    link = "https://longpt.org/" + title_a['href']
                    container = title_a.find_parent('tr') or title_a.find_parent('div')
                    img_src, subtitle, seeders, leechers, size = "", "", "-", "-", "加载中..."

                    if container:
                        if container.name == 'tr':
                            tds = container.find_all('td')
                            if len(tds) >= 3: seeders, leechers = tds[-2].get_text(strip=True), tds[-1].get_text(strip=True)

                        for img in container.find_all('img'):
                            src = img.get('data-src') or img.get('src') or ""
                            if src and 'category' not in src.lower() and 'cat' not in src.lower():
                                img_src = src if src.startswith('http') else "https://longpt.org/" + src
                                break
                        for icon in container.find_all('img'):
                            alt = icon.get('alt') or icon.get('title')
                            if alt and icon.get('src', '') != img_src: icon.replace_with(soup.new_tag("span", string=f" [{alt}] "))
                        subtitle = container.get_text(separator=' | ', strip=True).replace(title, '').strip(' | ')
                        if seeders != "-" and subtitle.endswith(leechers): subtitle = subtitle.rsplit('|', 2)[0].strip(' | ')
                        
                        subtitle = re.sub(r'\[?(?:剩余时间)[::]*\]?[\s\|]*([\d]+天[\d]+[时分秒]+|[\d]+[天时分秒]+)', r'[剩余时间: \1]', subtitle)
                        subtitle = subtitle.replace('[ | ]', '').replace('[ | | ]', '')
                            
                        tags, clean_sub = [], subtitle
                        for m in re.finditer(r'\[(.*?)\]', subtitle):
                            tag = m.group(1).strip(' |')
                            if any(x in tag.upper() for x in ['热门', 'FREE', '免费', '%', '2X', '剩余']):
                                if len(tag) <= 18: tags.append(tag)
                                clean_sub = clean_sub.replace(m.group(0), '')
                            elif tag not in ['Sticky', 'poster', '置顶']:
                                clean_sub = clean_sub.replace(m.group(0), tag)
                            else:
                                clean_sub = clean_sub.replace(m.group(0), '')
                        
                        time_match = re.search(r'(剩余时间[^\s\|]+)', clean_sub)
                        if time_match:
                            tags.append(time_match.group(1).strip(' |'))
                            clean_sub = clean_sub.replace(time_match.group(0), '')
                            
                        clean_sub = re.sub(r'热门|免费', '', clean_sub)
                        subtitle = re.sub(r'\|(?:\s*\|)+', '|', clean_sub).strip(' |')

                    if not img_src: img_src = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='140'><rect width='100%' height='100%' fill='%23e6e6e6'/><text x='50%' y='50%' fill='%23999' font-size='12' font-family='sans-serif' text-anchor='middle' dominant-baseline='middle'>加载海报</text></svg>"

                    current_fetched.append({
                        'id': tid, 'title': title, 'link': link, 'subtitle': subtitle, 'img': img_src,
                        'seeders': seeders, 'leechers': leechers, 'size': size, 'tags': tags
                    })

                history_dict = {str(item['id']): item for item in torrents}
                for item in current_fetched:
                    tid_str = str(item['id'])
                    if tid_str in history_dict:
                        cached = history_dict[tid_str]
                        if 'data:image/svg+xml' not in cached['img']: item['img'] = cached['img']
                        if cached['size'] not in ['未知', '加载中...']: item['size'] = cached['size']
                        merged_tags = item.get('tags', [])
                        for t in cached.get('tags', []):
                            if t not in merged_tags: merged_tags.append(t)
                        item['tags'] = merged_tags
                    history_dict[tid_str] = item

                merged = list(history_dict.values())
                merged.sort(key=lambda x: int(x['id']), reverse=True)
                torrents = merged[:30]

    except Exception as e:
        error_msg = f"服务器内部请求错误: {e}"
        
    return render_template_string(HTML_TEMPLATE, tab=tab, query=query, torrents=torrents, user_info=user_info, error=error_msg)

@app.route('/api/extra/<int:tid>')
def api_extra(tid):
    img_src, size, extra_tags = fetch_details_info(tid)
    if os.path.exists(HISTORY_FILE):
        try:
            with open(HISTORY_FILE, 'r', encoding='utf-8') as f: history = json.load(f)
            updated = False
            for item in history:
                if str(item['id']) == str(tid):
                    if img_src: item['img'] = img_src
                    if size and size != "未知": item['size'] = size
                    if extra_tags:
                        new_tags = item.get('tags', [])
                        for t in extra_tags:
                            if t not in new_tags: new_tags.append(t)
                        item['tags'] = new_tags
                    updated = True
                    break
            if updated:
                with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, ensure_ascii=False, indent=2)
        except: pass
    return jsonify({"img": img_src, "size": size, "tags": extra_tags})

@app.route('/api/signin')
def api_signin():
    try:
        r = requests.get(SIGNIN_URL, headers=HEADERS, timeout=10)
        if "签到成功" in r.text or "这是您的第" in r.text or "已连续签到" in r.text: return jsonify({"msg": "🎉 签到成功!"})
        elif "已经" in r.text or "退下" in r.text: return jsonify({"msg": "☕ 今天已经签到过了,明天再来吧。"})
        elif "登录" in r.text: return jsonify({"msg": "⚠️ 签到失败:Cookie 已失效,请更新!"})
        else: return jsonify({"msg": "🤔 状态未知,可能已签到,请留意积分变化。"})
    except Exception as e: return jsonify({"msg": f"❌ 网络请求出错: {str(e)}"})

@app.route('/download/<int:tid>')
def download_torrent(tid):
    try:
        r = requests.get(f"https://longpt.org/download.php?id={tid}", headers=HEADERS, timeout=10)
        if "application/x-bittorrent" in r.headers.get('Content-Type', '') or "octet-stream" in r.headers.get('Content-Type', ''):
            return Response(r.content, mimetype="application/x-bittorrent", headers={"Content-Disposition": f"attachment;filename=LongPT_{tid}.torrent"})
        else: return "下载失败,Cookie 可能失效或无权下载该种子。"
    except Exception as e: return f"请求下载出错: {str(e)}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5051)

也可以看看