CACHE_VERSION = "2026_fix_v3" import os import sys import logging from collections import deque import requests import json import sqlite3 import threading import time import signal import tarfile import random import re from datetime import datetime, timedelta from flask import Flask, request, jsonify, render_template_string, Response from apscheduler.schedulers.background import BackgroundScheduler try: from huggingface_hub import HfApi, hf_hub_download except ImportError: print("⚠️ 未安装 huggingface_hub,请执行: pip install huggingface_hub") sys.exit(1) app = Flask(__name__) # ========== 日志捕获 ========== LOG_BUFFER = deque(maxlen=5000) class ListHandler(logging.Handler): def emit(self, record): try: msg = self.format(record) LOG_BUFFER.append(msg) except Exception: self.handleError(record) logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') root_logger = logging.getLogger() list_handler = ListHandler() list_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) root_logger.addHandler(list_handler) # ========== 配置 ========== TMDB_API_KEY = os.environ.get("TMDB_API_KEY") HDHIVE_API_KEY = os.environ.get("HDHIVE_API_KEY") HF_TOKEN = os.environ.get("HF_TOKEN") DATASET_REPO = os.environ.get("DATASET_REPO", "hdhive-cache") UNLOCK_PASSWORD = os.environ.get("UNLOCK_PASSWORD", "") BACKUP_INTERVAL = 10 * 60 DB_PATH = "hdhive_cache.db" next_unlock_time = None processed_movies = set() scheduler = BackgroundScheduler() logging.info("🔧 启动配置检查:") logging.info(f" TMDB_API_KEY: {'✅' if TMDB_API_KEY else '❌'}") logging.info(f" HDHIVE_API_KEY: {'✅' if HDHIVE_API_KEY else '❌'}") logging.info(f" HF_TOKEN: {'✅' if HF_TOKEN else '❌'}") logging.info(f" DATASET_REPO: {DATASET_REPO}") logging.info(f" 解锁密码: {'✅ 已设置' if UNLOCK_PASSWORD else '❌ 未设置'}") def get_cache_stats(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT COUNT(*) FROM resource_cache") movie_count = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM unlock_cache") unlock_count = c.fetchone()[0] conn.close() return {'movies': movie_count, 'unlocks': unlock_count} def is_cached(media_type, tmdb_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT 1 FROM resource_cache WHERE media_type=? AND tmdb_id=?", (media_type, str(tmdb_id))) row = c.fetchone() conn.close() return row is not None def detect_pan_type(text): if not text: return 'unknown' t = str(text).lower().strip() PAN_RULES = { '115': ['115.com', '115cdn.com', 'anxia.com', 'v.anxia.com'], 'baidu': ['pan.baidu.com', '百度网盘'], 'aliyun': ['alipan.com', 'aliyundrive.com'], 'quark': ['pan.quark.cn'], 'uc': ['drive.uc.cn'], 'tianyi': ['cloud.189.cn'], '123pan': ['123684.com', '123865.com'], 'lanzou': ['lanzou'], 'pikpak': ['pikpak.me'] } for k, arr in PAN_RULES.items(): if any(x in t for x in arr): return k if 'magnet:' in t or 'ed2k://' in t: return 'magnet' return 'unknown' def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS resource_cache ( media_type TEXT, tmdb_id TEXT, resource_data TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(media_type, tmdb_id) )''') c.execute('''CREATE TABLE IF NOT EXISTS unlock_cache ( slug TEXT PRIMARY KEY, link TEXT, unlock_data TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )''') conn.commit() conn.close() logging.info("✅ 基础数据库表初始化完成") def ensure_tables(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS auto_unlocked ( slug TEXT PRIMARY KEY, unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )''') conn.commit() conn.close() logging.info("✅ 补充表检查完成(auto_unlocked)") def get_cached_resources(media_type, tmdb_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT resource_data FROM resource_cache WHERE media_type=? AND tmdb_id=?", (media_type, str(tmdb_id))) row = c.fetchone() conn.close() if row: try: return json.loads(row[0]) except Exception: return None return None def save_cached_resources(media_type, tmdb_id, resource_data): if not resource_data: print(f"ℹ️ 资源为空,不缓存: {media_type}/{tmdb_id}") return has_valid = any( item.get('link') or item.get('slug') or item.get('url') or item.get('download_url') or item.get('pan_url') for item in resource_data ) if not has_valid: print(f"⚠️ 资源无有效字段,不缓存: {media_type}/{tmdb_id}") return for item in resource_data: if not item.get('pan_type') or item['pan_type'] == 'unknown': texts = [] for field in ['link', 'slug', 'title', 'name', 'download_url', 'url', 'pan_url']: val = item.get(field) if val: texts.append(str(val)) combined = '; '.join(texts) item['pan_type'] = detect_pan_type(combined) conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("INSERT OR REPLACE INTO resource_cache (media_type, tmdb_id, resource_data, updated_at) VALUES (?,?,?, CURRENT_TIMESTAMP)", (media_type, str(tmdb_id), json.dumps(resource_data, ensure_ascii=False))) conn.commit() conn.close() print(f"💾 缓存保存: {media_type}/{tmdb_id}, 共 {len(resource_data)} 个资源") def delete_cached_resources(media_type, tmdb_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("DELETE FROM resource_cache WHERE media_type=? AND tmdb_id=?", (media_type, str(tmdb_id))) conn.commit() conn.close() print(f"🗑️ 已删除缓存: {media_type}/{tmdb_id}") def get_cached_unlock(slug): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT link, unlock_data FROM unlock_cache WHERE slug=?", (slug,)) row = c.fetchone() conn.close() if row and row[0]: print(f"📦 [缓存命中] 从数据库获取解锁链接: {slug}") return {'link': row[0], 'data': json.loads(row[1]) if row[1] else None} print(f"❌ [缓存未命中] 数据库中无记录: {slug}") return None def save_cached_unlock(slug, link, unlock_data): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("INSERT OR REPLACE INTO unlock_cache (slug, link, unlock_data, updated_at) VALUES (?,?,?, CURRENT_TIMESTAMP)", (slug, link, json.dumps(unlock_data, ensure_ascii=False) if unlock_data else None)) conn.commit() conn.close() print(f"💾 [缓存保存] 解锁链接已存入数据库: {slug} -> {link[:80]}...") api = HfApi() def backup_to_hf_dataset(): if not HF_TOKEN or not DATASET_REPO or DATASET_REPO == "hdhive-cache": return False try: day = datetime.now().strftime("%Y-%m-%d") backup_name = f"backup_{day}.tar.gz" with tarfile.open(backup_name, "w:gz") as tar: tar.add(DB_PATH, arcname=os.path.basename(DB_PATH)) api.upload_file(path_or_fileobj=backup_name, path_in_repo=backup_name, repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN) print(f"✅ 备份成功: {backup_name}") os.remove(backup_name) return True except Exception as e: print(f"❌ 备份失败: {e}") return False def restore_from_hf_dataset(): if not HF_TOKEN or not DATASET_REPO or DATASET_REPO == "hdhive-cache": print("⚠️ 跳过恢复") return False try: files = api.list_repo_files(repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN) now = datetime.now() for i in range(5): day = (now - timedelta(days=i)).strftime("%Y-%m-%d") backup_name = f"backup_{day}.tar.gz" if backup_name in files: print(f"📦 发现备份: {backup_name},下载中...") path = hf_hub_download(repo_id=DATASET_REPO, filename=backup_name, repo_type="dataset", token=HF_TOKEN) with tarfile.open(path, "r:gz") as tar: tar.extractall(path=".") print(f"✅ 数据库已恢复") return True print("📭 未找到备份") return False except Exception as e: print(f"❌ 恢复失败: {e}") return False def periodic_backup(): while True: time.sleep(BACKUP_INTERVAL) print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 执行备份...") backup_to_hf_dataset() def exit_backup(signum, frame): print(f"收到退出信号,执行最后备份...") backup_to_hf_dataset() sys.exit(0) signal.signal(signal.SIGTERM, exit_backup) signal.signal(signal.SIGINT, exit_backup) init_db() restore_from_hf_dataset() ensure_tables() backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() print(f"✅ 定时备份已启动,间隔 {BACKUP_INTERVAL // 60} 分钟") # ========== 图片代理接口 ========== @app.route('/api/image/proxy') def image_proxy(): img_url = request.args.get('url', '') if not img_url: return '', 400 if not img_url.startswith('https://image.tmdb.org/'): return '', 403 try: resp = requests.get(img_url, timeout=30, headers={ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.themoviedb.org/', }) if resp.status_code == 200: return Response(resp.content, status=200, content_type=resp.headers.get('Content-Type', 'image/jpeg')) else: print(f"图片代理失败: {img_url}, 状态码: {resp.status_code}") return '', 500 except requests.exceptions.Timeout: print(f"图片代理超时: {img_url}") return '', 504 except Exception as e: print(f"图片代理异常: {e}, URL: {img_url}") return '', 500 # ========== 密码验证接口 ========== @app.route('/api/verify-password', methods=['POST']) def verify_password(): data = request.json password = data.get('password', '') if UNLOCK_PASSWORD and password == UNLOCK_PASSWORD: return jsonify({'success': True, 'message': '密码正确'}) elif not UNLOCK_PASSWORD: return jsonify({'success': False, 'message': '未设置解锁密码,请联系管理员'}) else: return jsonify({'success': False, 'message': '密码错误'}) # ========== 自动解锁统计接口 ========== @app.route('/api/auto-unlock-stats', methods=['GET']) def auto_unlock_stats(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='auto_unlocked'") if not c.fetchone(): conn.close() return jsonify({'unlocked_resources': 0}) c.execute("SELECT COUNT(*) FROM auto_unlocked") unlocked_resources = c.fetchone()[0] conn.close() return jsonify({'unlocked_resources': unlocked_resources}) @app.route('/api/next-unlock-time', methods=['GET']) def get_next_unlock_time(): global next_unlock_time if next_unlock_time: remaining = max(0, int(next_unlock_time - time.time())) return jsonify({'remaining': remaining}) return jsonify({'remaining': 0}) # ========== 日志接口 ========== @app.route('/api/logs') def get_logs(): return jsonify({'logs': list(LOG_BUFFER)}) @app.route('/api/logs/clear', methods=['POST']) def clear_logs(): LOG_BUFFER.clear() return jsonify({'success': True}) # ========== TMDB 动态分类 API (仅 genre) ========== @app.route('/api/tmdb/genres/') def get_tmdb_genres(media_type): if media_type not in ['movie', 'tv']: return jsonify({'error': 'invalid media type'}), 400 url = f"https://api.themoviedb.org/3/genre/{media_type}/list?api_key={TMDB_API_KEY}&language=zh-CN" try: resp = requests.get(url, timeout=10) data = resp.json() return jsonify(data.get('genres', [])) except Exception as e: return jsonify({'error': str(e)}), 500 # ========== API 路由 ========== @app.route('/') def index(): return render_template_string(HTML_TEMPLATE) @app.route('/api/cache/stats') def cache_stats(): return jsonify(get_cache_stats()) @app.route('/api/discover', methods=['POST']) def discover(): data = request.json media_type = data.get('type', 'movie') page = data.get('page', 1) preset = data.get('preset') filters = data.get('filters', {}) sort_by = filters.get('sort_by', 'latest') def build_discover_url(base_params): url = f"https://api.themoviedb.org/3/discover/{media_type}?api_key={TMDB_API_KEY}&language=zh-CN&page={page}" for k, v in base_params.items(): url += f"&{k}={v}" region = filters.get('region') if region: region_code = { '大陆': 'CN', '香港': 'HK', '台湾': 'TW', '日本': 'JP', '韩国': 'KR', '欧美': 'US' }.get(region, '') if region_code: if media_type == 'movie': url += f"&with_origin_country={region_code}" else: url += f"&with_origin_country={region_code}" year = filters.get('year') if year: if media_type == 'movie': if year == '2010年前': url += "&primary_release_date.lte=2009-12-31" else: url += f"&primary_release_year={year}" else: if year == '2010年前': url += "&first_air_date.lte=2009-12-31" else: url += f"&first_air_date_year={year}" genres = filters.get('genres', []) if genres: genres_str = '|'.join(str(g) for g in genres) url += f"&with_genres={genres_str}" return url if preset: preset_map_movie = { 'popular': {'sort_by': 'popularity.desc'}, 'now_playing': {'sort_by': 'primary_release_date.desc'}, 'top_rated': {'sort_by': 'vote_average.desc', 'vote_count.gte': 30}, 'upcoming': {'sort_by': 'primary_release_date.asc'} } preset_map_tv = { 'popular': {'sort_by': 'popularity.desc'}, 'top_rated': {'sort_by': 'vote_average.desc', 'vote_count.gte': 30}, 'on_the_air': {'sort_by': 'first_air_date.desc'}, 'airing_today': {'sort_by': 'first_air_date.desc'} } if media_type == 'movie': base_params = preset_map_movie.get(preset, {'sort_by': 'popularity.desc'}) else: base_params = preset_map_tv.get(preset, {'sort_by': 'popularity.desc'}) if filters.get('sort_by'): user_sort = filters['sort_by'] if user_sort == 'latest': base_params['sort_by'] = 'primary_release_date.desc' if media_type == 'movie' else 'first_air_date.desc' elif user_sort == 'imdb': base_params['sort_by'] = 'vote_average.desc' base_params['vote_count.gte'] = 30 url = build_discover_url(base_params) resp = requests.get(url, timeout=15) result = resp.json() results = result.get('results', []) for item in results: mtype = media_type if media_type in ['movie', 'tv'] else (item.get('media_type') or 'movie') item['cached'] = is_cached(mtype, item['id']) result['results'] = results return jsonify(result) # 无预设 if media_type == 'movie': base_url = f"https://api.themoviedb.org/3/discover/movie?api_key={TMDB_API_KEY}&language=zh-CN&page={page}" if sort_by == 'latest': base_url += "&sort_by=primary_release_date.desc" else: base_url += "&sort_by=vote_average.desc&vote_count.gte=30" region = filters.get('region') if region: region_code = { '大陆': 'CN', '香港': 'HK', '台湾': 'TW', '日本': 'JP', '韩国': 'KR', '欧美': 'US' }.get(region, '') if region_code: base_url += f"&with_origin_country={region_code}" year = filters.get('year') if year: if year == '2010年前': base_url += "&primary_release_date.lte=2009-12-31" else: base_url += f"&primary_release_year={year}" genres = filters.get('genres', []) if genres: genres_str = '|'.join(str(g) for g in genres) base_url += f"&with_genres={genres_str}" else: base_url = f"https://api.themoviedb.org/3/discover/tv?api_key={TMDB_API_KEY}&language=zh-CN&page={page}" if sort_by == 'latest': base_url += "&sort_by=first_air_date.desc" else: base_url += "&sort_by=vote_average.desc&vote_count.gte=30" region = filters.get('region') if region: region_code = { '大陆': 'CN', '香港': 'HK', '台湾': 'TW', '日本': 'JP', '韩国': 'KR', '欧美': 'US' }.get(region, '') if region_code: base_url += f"&with_origin_country={region_code}" year = filters.get('year') if year: if year == '2010年前': base_url += "&first_air_date.lte=2009-12-31" else: base_url += f"&first_air_date_year={year}" genres = filters.get('genres', []) if genres: genres_str = '|'.join(str(g) for g in genres) base_url += f"&with_genres={genres_str}" resp = requests.get(base_url, timeout=15) result = resp.json() results = result.get('results', []) for item in results: mtype = media_type if media_type in ['movie', 'tv'] else (item.get('media_type') or 'movie') item['cached'] = is_cached(mtype, item['id']) result['results'] = results return jsonify(result) @app.route('/api/search', methods=['POST']) def search_api(): data = request.json query = data.get('query', '') page = data.get('page', 1) url = f"https://api.themoviedb.org/3/search/multi?api_key={TMDB_API_KEY}&query={query}&language=zh-CN&page={page}" resp = requests.get(url, timeout=15) result = resp.json() results = result.get('results', []) filtered = [] for item in results: if item.get('media_type') in ('movie', 'tv'): item['cached'] = is_cached(item['media_type'], item['id']) filtered.append(item) result['results'] = filtered return jsonify(result) @app.route('/api/cache/resources//') def cache_resources(type, tmdb_id): cached = get_cached_resources(type, tmdb_id) if cached: return jsonify({'success': True, 'from_cache': True, 'resources': cached}) if not HDHIVE_API_KEY: return jsonify({"success": False, "error": "HDHIVE_API_KEY not configured"}), 500 url = f"https://hdhive.com/api/open/resources/{type}/{tmdb_id}" headers = {"X-API-Key": HDHIVE_API_KEY, "Accept": "application/json"} resp = requests.get(url, headers=headers, timeout=30) result = resp.json() resources = result.get('data', []) if isinstance(result, dict) else result if not resources: resources = [] for item in resources: if not item.get('pan_type') or item['pan_type'] == 'unknown': texts = [] for field in ['link', 'slug', 'title', 'name', 'download_url', 'url', 'pan_url']: val = item.get(field) if val: texts.append(str(val)) combined = '; '.join(texts) item['pan_type'] = detect_pan_type(combined) save_cached_resources(type, tmdb_id, resources) return jsonify({'success': True, 'from_cache': False, 'resources': resources}) @app.route('/api/cache/refresh//', methods=['POST']) def refresh_cache(type, tmdb_id): delete_cached_resources(type, tmdb_id) if not HDHIVE_API_KEY: return jsonify({"success": False, "error": "HDHIVE_API_KEY not configured"}), 500 url = f"https://hdhive.com/api/open/resources/{type}/{tmdb_id}" headers = {"X-API-Key": HDHIVE_API_KEY, "Accept": "application/json"} try: resp = requests.get(url, headers=headers, timeout=30) result = resp.json() resources = result.get('data', []) if isinstance(result, dict) else result if not resources: resources = [] for item in resources: if not item.get('pan_type') or item['pan_type'] == 'unknown': texts = [] for field in ['link', 'slug', 'title', 'name', 'download_url', 'url', 'pan_url']: val = item.get(field) if val: texts.append(str(val)) combined = '; '.join(texts) item['pan_type'] = detect_pan_type(combined) save_cached_resources(type, tmdb_id, resources) return jsonify({"success": True, "message": "缓存已刷新"}) except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 @app.route('/api/cache/unlock', methods=['POST']) def cache_unlock(): data = request.json slug = data.get('slug') allow = data.get('allow_points', False) password = data.get('password', '') print(f"\n{'='*60}") print(f"🔓 解锁请求: slug={slug}, allow_points={allow}") # 第一步:检查本地缓存 cached = get_cached_unlock(slug) if cached and cached.get('link'): print(f"✅ 返回缓存链接 (不消耗积分)") print(f"{'='*60}\n") return jsonify({'success': True, 'from_cache': True, 'link': cached['link'], 'data': cached['data']}) if not HDHIVE_API_KEY: print(f"❌ HDHIVE_API_KEY 未配置") print(f"{'='*60}\n") return jsonify({"error": "HDHIVE_API_KEY not configured"}), 500 # 第二步:缓存未命中,需要调用 HDHive API # 先获取资源积分信息(用于判断是否需要密码) points = None # 从 resource_cache 中查找该 slug 的积分 conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT resource_data FROM resource_cache") for rrow in c.fetchall(): try: resources = json.loads(rrow[0]) for res in resources: if res.get('slug') == slug: points = res.get('unlock_points', 0) break if points is not None: break except: continue conn.close() if points is None: # 抓取页面获取积分 try: for prefix in ['115', 'baidu', 'magnet']: url = f"https://hdhive.com/resource/{prefix}/{slug}" resp = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'}) if resp.status_code == 200: html = resp.text match = re.search(r'所需积分[::]\s*(\d+)', html) if not match: match = re.search(r'unlock_points[::]\s*(\d+)', html, re.IGNORECASE) if not match: match = re.search(r']*class="[^"]*points?[^"]*"[^>]*>(\d+)', html, re.IGNORECASE) if match: points = int(match.group(1)) else: points = 0 break except Exception as e: print(f"获取 points 失败: {e}") points = 0 # 如果需要密码(设置了全局密码 且 资源需要积分) if UNLOCK_PASSWORD and points and points > 0: if not password or password != UNLOCK_PASSWORD: print(f"🔐 付费资源需要密码") return jsonify({'need_password': True, 'message': '付费资源需要解锁密码'}), 401 print(f"🌐 调用 HDHive API 解锁...") url = "https://hdhive.com/api/open/resources/unlock" headers = {"X-API-Key": HDHIVE_API_KEY, "Content-Type": "application/json"} payload = {"slug": slug} if allow: payload["allow_points"] = True resp = requests.post(url, headers=headers, json=payload, timeout=30) result = resp.json() if result.get('code') == 'OPENAPI_COOLDOWN': print(f"⏰ API 冷却中: {result}") print(f"{'='*60}\n") return jsonify(result) link = result.get('data', {}).get('full_url') or result.get('data', {}).get('url') or result.get('url') if link: save_cached_unlock(slug, link, result) print(f"✅ 解锁成功,已缓存 (消耗积分)") else: print(f"❌ 解锁失败: {result}") print(f"{'='*60}\n") return jsonify({'success': True, 'from_cache': False, 'link': link, 'data': result.get('data')}) @app.route('/api/backup', methods=['POST']) def manual_backup(): success = backup_to_hf_dataset() return jsonify({"message": "备份成功" if success else "备份失败"}), 200 if success else 500 # ========== 后台自动浏览并解锁免费资源 ========== MOVIE_GENRES = { 28: "动作", 12: "冒险", 16: "动画", 35: "喜剧", 80: "犯罪", 99: "纪录片", 18: "剧情", 10751: "家庭", 14: "奇幻", 36: "历史", 27: "恐怖", 10402: "音乐", 9648: "悬疑", 10749: "爱情", 878: "科幻", 53: "惊悚", 10752: "战争", 37: "西部" } TV_GENRES = { 10759: "动作冒险", 16: "动画", 35: "喜剧", 80: "犯罪", 99: "纪录片", 18: "剧情", 10751: "家庭", 10762: "儿童", 9648: "悬疑", 10763: "新闻", 10764: "真人秀", 10765: "科幻奇幻", 10766: "肥皂剧", 10767: "脱口秀", 10768: "政治" } def get_random_movie_from_tmdb(): global processed_movies max_attempts = 10 for attempt in range(max_attempts): year = random.randint(1900, 2026) genre_id = random.choice(list(MOVIE_GENRES.keys())) sort_options = ['popularity.desc', 'vote_average.desc', 'release_date.desc', 'primary_release_date.desc'] sort_by = random.choice(sort_options) page = random.randint(1, 1000) url = f"https://api.themoviedb.org/3/discover/movie?api_key={TMDB_API_KEY}&language=zh-CN&sort_by={sort_by}&page={page}&primary_release_year={year}&with_genres={genre_id}&vote_count.gte=1" print(f"[DEBUG] 电影尝试 {attempt+1}: year={year}, genre={genre_id}, sort={sort_by}, page={page}") try: resp = requests.get(url, timeout=15) data = resp.json() results = data.get('results', []) if not results: continue random.shuffle(results) for movie in results: tmdb_id = movie['id'] key = f"movie_{tmdb_id}" if key in processed_movies: continue conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT 1 FROM resource_cache WHERE media_type='movie' AND tmdb_id=?", (str(tmdb_id),)) exists = c.fetchone() is not None conn.close() if not exists: processed_movies.add(key) print(f"✅ 找到未缓存电影: {movie.get('title', '未知')} (ID: {tmdb_id})") return { 'media_type': 'movie', 'tmdb_id': tmdb_id, 'title': movie.get('title', '未知'), 'year': year, 'genre': MOVIE_GENRES.get(genre_id, '未知') } except Exception as e: print(f"❌ 电影尝试 {attempt+1} 失败: {e}") try: url = f"https://api.themoviedb.org/3/movie/now_playing?api_key={TMDB_API_KEY}&language=zh-CN&page={random.randint(1,5)}" resp = requests.get(url, timeout=15) data = resp.json() results = data.get('results', []) random.shuffle(results) for movie in results: tmdb_id = movie['id'] key = f"movie_{tmdb_id}" if key in processed_movies: continue conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT 1 FROM resource_cache WHERE media_type='movie' AND tmdb_id=?", (str(tmdb_id),)) exists = c.fetchone() is not None conn.close() if not exists: processed_movies.add(key) print(f"✅ [降级] 找到未缓存电影: {movie.get('title', '未知')} (ID: {tmdb_id})") return { 'media_type': 'movie', 'tmdb_id': tmdb_id, 'title': movie.get('title', '未知'), 'year': movie.get('release_date', '')[:4] if movie.get('release_date') else '', 'genre': '未知' } except Exception as e: print(f"❌ 降级获取电影失败: {e}") return None def get_random_tv_from_tmdb(): global processed_movies max_attempts = 10 for attempt in range(max_attempts): year = random.randint(1900, 2026) genre_id = random.choice(list(TV_GENRES.keys())) sort_options = ['popularity.desc', 'vote_average.desc', 'first_air_date.desc', 'name.asc'] sort_by = random.choice(sort_options) page = random.randint(1, 1000) url = f"https://api.themoviedb.org/3/discover/tv?api_key={TMDB_API_KEY}&language=zh-CN&sort_by={sort_by}&page={page}&first_air_date_year={year}&with_genres={genre_id}&vote_count.gte=1" print(f"[DEBUG] 电视剧尝试 {attempt+1}: year={year}, genre={genre_id}, sort={sort_by}, page={page}") try: resp = requests.get(url, timeout=15) data = resp.json() results = data.get('results', []) if not results: continue random.shuffle(results) for tv in results: tmdb_id = tv['id'] key = f"tv_{tmdb_id}" if key in processed_movies: continue conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT 1 FROM resource_cache WHERE media_type='tv' AND tmdb_id=?", (str(tmdb_id),)) exists = c.fetchone() is not None conn.close() if not exists: processed_movies.add(key) print(f"✅ 找到未缓存电视剧: {tv.get('name', '未知')} (ID: {tmdb_id})") return { 'media_type': 'tv', 'tmdb_id': tmdb_id, 'title': tv.get('name', '未知'), 'year': year, 'genre': TV_GENRES.get(genre_id, '未知') } except Exception as e: print(f"❌ 电视剧尝试 {attempt+1} 失败: {e}") try: url = f"https://api.themoviedb.org/3/tv/on_the_air?api_key={TMDB_API_KEY}&language=zh-CN&page={random.randint(1,5)}" resp = requests.get(url, timeout=15) data = resp.json() results = data.get('results', []) random.shuffle(results) for tv in results: tmdb_id = tv['id'] key = f"tv_{tmdb_id}" if key in processed_movies: continue conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("SELECT 1 FROM resource_cache WHERE media_type='tv' AND tmdb_id=?", (str(tmdb_id),)) exists = c.fetchone() is not None conn.close() if not exists: processed_movies.add(key) print(f"✅ [降级] 找到未缓存电视剧: {tv.get('name', '未知')} (ID: {tmdb_id})") return { 'media_type': 'tv', 'tmdb_id': tmdb_id, 'title': tv.get('name', '未知'), 'year': tv.get('first_air_date', '')[:4] if tv.get('first_air_date') else '', 'genre': '未知' } except Exception as e: print(f"❌ 降级获取电视剧失败: {e}") return None def auto_browse_and_unlock(): global next_unlock_time print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 🔄 [后台] 进入自动解锁任务") if not HDHIVE_API_KEY: print("❌ [后台] HDHIVE_API_KEY 缺失") schedule_next_job() return {'success': False, 'message': 'HDHIVE_API_KEY not configured'} print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 🔄 [后台] 开始随机获取未缓存影片...") media_type_choice = random.choice(['movie', 'tv']) if media_type_choice == 'movie': info = get_random_movie_from_tmdb() if not info: print(f"📭 [后台] 未找到新的未缓存电影") schedule_next_job() return {'success': False, 'message': '未找到新的未缓存电影'} media_type, tmdb_id, title = 'movie', info['tmdb_id'], info['title'] print(f"🎲 [后台] 选中未缓存电影: {title} (ID: {tmdb_id})") else: info = get_random_tv_from_tmdb() if not info: print(f"📭 [后台] 未找到新的未缓存电视剧") schedule_next_job() return {'success': False, 'message': '未找到新的未缓存电视剧'} media_type, tmdb_id, title = 'tv', info['tmdb_id'], info['title'] print(f"🎲 [后台] 选中未缓存电视剧: {title} (ID: {tmdb_id})") url = f"https://hdhive.com/api/open/resources/{media_type}/{tmdb_id}" headers = {"X-API-Key": HDHIVE_API_KEY, "Accept": "application/json"} try: resp = requests.get(url, headers=headers, timeout=30) result = resp.json() resources = result.get('data', []) if isinstance(result, dict) else result if not resources: print(f"📭 [后台] {title} 没有资源") schedule_next_job() return {'success': False, 'message': f'{title} 没有资源'} print(f"📊 [后台] {title} 共有 {len(resources)} 个资源") free_count = sum(1 for r in resources if r.get('unlock_points', 0) == 0) print(f"📊 [后台] 其中免费资源: {free_count} 个") save_cached_resources(media_type, tmdb_id, resources) print(f"💾 [后台] 已缓存 {media_type}/{tmdb_id}, 共 {len(resources)} 个资源") conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute("CREATE TABLE IF NOT EXISTS auto_unlocked (slug TEXT PRIMARY KEY, unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)") unlocked_any = False for res in resources: points = res.get('unlock_points', 0) slug = res.get('slug') if slug and points == 0: c.execute("SELECT 1 FROM auto_unlocked WHERE slug=?", (slug,)) if not c.fetchone(): print(f"🔓 [后台] 发现未解锁免费资源,开始解锁: {slug}") unlock_url = "https://hdhive.com/api/open/resources/unlock" unlock_payload = {"slug": slug, "allow_points": True} unlock_resp = requests.post(unlock_url, headers=headers, json=unlock_payload, timeout=30) unlock_result = unlock_resp.json() link = unlock_result.get('data', {}).get('full_url') or unlock_result.get('data', {}).get('url') or unlock_result.get('url') if link: c.execute("INSERT OR REPLACE INTO unlock_cache (slug, link, unlock_data, updated_at) VALUES (?,?,?, CURRENT_TIMESTAMP)", (slug, link, json.dumps(unlock_result, ensure_ascii=False))) c.execute("INSERT INTO auto_unlocked (slug) VALUES (?)", (slug,)) conn.commit() print(f"✅ [后台] 自动解锁成功: {title} - {slug}") unlocked_any = True break else: print(f"⚠️ [后台] 解锁失败: {slug} (无链接)") else: print(f"⏭️ [后台] 资源已解锁过,跳过: {slug}") conn.close() if unlocked_any: result = {'success': True, 'message': f'解锁成功: {title}', 'title': title} else: print(f"📭 [后台] {title} 没有可解锁的免费资源(可能已全部解锁)") result = {'success': False, 'message': f'{title} 没有可解锁的免费资源'} schedule_next_job() return result except Exception as e: print(f"❌ [后台] 处理失败: {e}") schedule_next_job() return {'success': False, 'message': str(e)} def schedule_next_job(): interval_minutes = random.randint(1, 3) next_run = datetime.now() + timedelta(minutes=interval_minutes) global next_unlock_time next_unlock_time = next_run.timestamp() print(f"⏰ [后台] 下次浏览将在 {interval_minutes} 分钟后执行({next_run.strftime('%H:%M:%S')})") if scheduler.get_job('auto_unlock_job'): scheduler.remove_job('auto_unlock_job') scheduler.add_job(auto_browse_and_unlock, 'date', run_date=next_run, id='auto_unlock_job') schedule_next_job() scheduler.start() print(f"✅ 后台自动浏览已启动,随机间隔 1-3 分钟,从 TMDB 所有影片中随机选择未缓存内容") # ========== HTML 模板 ========== HTML_TEMPLATE = r""" HDHive 影视资源

🎬 HDHive 影视资源

选择分类 → 浏览影片 → 获取资源链接
- 缓存影片
- 缓存资源
- 自动解锁资源
--:-- 下次浏览
📌 仅显示 115网盘、百度网盘、磁力链接 资源 | 图片通过服务器代理
🔓 快捷解锁(直接输入 HDHive 资源链接或 slug)
🔽 筛选条件 (地区/年份/类型/排序)
🌍 地区
📅 年份
🎭 类型 (TMDB)
📊 排序
🔥 热门电影
共 0 部
加载中...
点击上方影视获取资源
📥 资源列表
请先选择影视

🔐 请输入解锁密码

""" if __name__ == '__main__': app.run(host='0.0.0.0', port=7860) # SQLite WAL 优化 try: conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA synchronous=NORMAL") conn.close() print("✅ SQLite WAL 已启用") except Exception as e: print("WAL 初始化失败:", e)