wd23 commited on
Commit
c786b90
·
verified ·
1 Parent(s): 7718241

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1855
app.py DELETED
@@ -1,1855 +0,0 @@
1
- CACHE_VERSION = "2026_fix_v3"
2
-
3
- import os
4
- import sys
5
- import logging
6
- from collections import deque
7
- import requests
8
- import json
9
- import sqlite3
10
- import threading
11
- import time
12
- import signal
13
- import tarfile
14
- import random
15
- import re
16
- from datetime import datetime, timedelta
17
- from flask import Flask, request, jsonify, render_template_string, Response
18
- from apscheduler.schedulers.background import BackgroundScheduler
19
-
20
- try:
21
- from huggingface_hub import HfApi, hf_hub_download
22
- except ImportError:
23
- print("⚠️ 未安装 huggingface_hub,请执行: pip install huggingface_hub")
24
- sys.exit(1)
25
-
26
- app = Flask(__name__)
27
-
28
- # ========== 日志捕获 ==========
29
- LOG_BUFFER = deque(maxlen=5000)
30
-
31
- class ListHandler(logging.Handler):
32
- def emit(self, record):
33
- try:
34
- msg = self.format(record)
35
- LOG_BUFFER.append(msg)
36
- except Exception:
37
- self.handleError(record)
38
-
39
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
40
- root_logger = logging.getLogger()
41
- list_handler = ListHandler()
42
- list_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
43
- root_logger.addHandler(list_handler)
44
-
45
- # ========== 配置 ==========
46
- TMDB_API_KEY = os.environ.get("TMDB_API_KEY")
47
- HDHIVE_API_KEY = os.environ.get("HDHIVE_API_KEY")
48
- HF_TOKEN = os.environ.get("HF_TOKEN")
49
- DATASET_REPO = os.environ.get("DATASET_REPO", "hdhive-cache")
50
- UNLOCK_PASSWORD = os.environ.get("UNLOCK_PASSWORD", "")
51
-
52
- BACKUP_INTERVAL = 10 * 60
53
- DB_PATH = "hdhive_cache.db"
54
-
55
- next_unlock_time = None
56
- processed_movies = set()
57
- scheduler = BackgroundScheduler()
58
-
59
- logging.info("🔧 启动配置检查:")
60
- logging.info(f" TMDB_API_KEY: {'✅' if TMDB_API_KEY else '❌'}")
61
- logging.info(f" HDHIVE_API_KEY: {'✅' if HDHIVE_API_KEY else '❌'}")
62
- logging.info(f" HF_TOKEN: {'✅' if HF_TOKEN else '❌'}")
63
- logging.info(f" DATASET_REPO: {DATASET_REPO}")
64
- logging.info(f" 解锁密码: {'✅ 已设置' if UNLOCK_PASSWORD else '❌ 未设置'}")
65
-
66
- def get_cache_stats():
67
- conn = sqlite3.connect(DB_PATH)
68
- c = conn.cursor()
69
- c.execute("SELECT COUNT(*) FROM resource_cache")
70
- movie_count = c.fetchone()[0]
71
- c.execute("SELECT COUNT(*) FROM unlock_cache")
72
- unlock_count = c.fetchone()[0]
73
- conn.close()
74
- return {'movies': movie_count, 'unlocks': unlock_count}
75
-
76
- def is_cached(media_type, tmdb_id):
77
- conn = sqlite3.connect(DB_PATH)
78
- c = conn.cursor()
79
- c.execute("SELECT 1 FROM resource_cache WHERE media_type=? AND tmdb_id=?", (media_type, str(tmdb_id)))
80
- row = c.fetchone()
81
- conn.close()
82
- return row is not None
83
-
84
- def detect_pan_type(text):
85
- if not text:
86
- return 'unknown'
87
- t = str(text).lower().strip()
88
- PAN_RULES = {
89
- '115': ['115.com', '115cdn.com', 'anxia.com', 'v.anxia.com'],
90
- 'baidu': ['pan.baidu.com', '百度网盘'],
91
- 'aliyun': ['alipan.com', 'aliyundrive.com'],
92
- 'quark': ['pan.quark.cn'],
93
- 'uc': ['drive.uc.cn'],
94
- 'tianyi': ['cloud.189.cn'],
95
- '123pan': ['123684.com', '123865.com'],
96
- 'lanzou': ['lanzou'],
97
- 'pikpak': ['pikpak.me']
98
- }
99
- for k, arr in PAN_RULES.items():
100
- if any(x in t for x in arr):
101
- return k
102
- if 'magnet:' in t or 'ed2k://' in t:
103
- return 'magnet'
104
- return 'unknown'
105
-
106
- def init_db():
107
- conn = sqlite3.connect(DB_PATH)
108
- c = conn.cursor()
109
- c.execute('''CREATE TABLE IF NOT EXISTS resource_cache (
110
- media_type TEXT, tmdb_id TEXT, resource_data TEXT,
111
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
112
- UNIQUE(media_type, tmdb_id)
113
- )''')
114
- c.execute('''CREATE TABLE IF NOT EXISTS unlock_cache (
115
- slug TEXT PRIMARY KEY, link TEXT, unlock_data TEXT,
116
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
117
- )''')
118
- conn.commit()
119
- conn.close()
120
- logging.info("✅ 基础数据库表初始化完成")
121
-
122
- def ensure_tables():
123
- conn = sqlite3.connect(DB_PATH)
124
- c = conn.cursor()
125
- c.execute('''CREATE TABLE IF NOT EXISTS auto_unlocked (
126
- slug TEXT PRIMARY KEY,
127
- unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
128
- )''')
129
- conn.commit()
130
- conn.close()
131
- logging.info("✅ 补充表检查完成(auto_unlocked)")
132
-
133
- def get_cached_resources(media_type, tmdb_id):
134
- conn = sqlite3.connect(DB_PATH)
135
- c = conn.cursor()
136
- c.execute("SELECT resource_data FROM resource_cache WHERE media_type=? AND tmdb_id=?", (media_type, str(tmdb_id)))
137
- row = c.fetchone()
138
- conn.close()
139
- if row:
140
- try:
141
- return json.loads(row[0])
142
- except Exception:
143
- return None
144
- return None
145
-
146
- def save_cached_resources(media_type, tmdb_id, resource_data):
147
- if not resource_data:
148
- print(f"ℹ️ 资源为空,不缓存: {media_type}/{tmdb_id}")
149
- return
150
- has_valid = any(
151
- item.get('link') or item.get('slug') or item.get('url') or
152
- item.get('download_url') or item.get('pan_url')
153
- for item in resource_data
154
- )
155
- if not has_valid:
156
- print(f"⚠️ 资源无有效字段,不缓存: {media_type}/{tmdb_id}")
157
- return
158
- for item in resource_data:
159
- if not item.get('pan_type') or item['pan_type'] == 'unknown':
160
- texts = []
161
- for field in ['link', 'slug', 'title', 'name', 'download_url', 'url', 'pan_url']:
162
- val = item.get(field)
163
- if val:
164
- texts.append(str(val))
165
- combined = '; '.join(texts)
166
- item['pan_type'] = detect_pan_type(combined)
167
- conn = sqlite3.connect(DB_PATH)
168
- c = conn.cursor()
169
- c.execute("INSERT OR REPLACE INTO resource_cache (media_type, tmdb_id, resource_data, updated_at) VALUES (?,?,?, CURRENT_TIMESTAMP)",
170
- (media_type, str(tmdb_id), json.dumps(resource_data, ensure_ascii=False)))
171
- conn.commit()
172
- conn.close()
173
- print(f"💾 缓存保存: {media_type}/{tmdb_id}, 共 {len(resource_data)} 个资源")
174
-
175
- def delete_cached_resources(media_type, tmdb_id):
176
- conn = sqlite3.connect(DB_PATH)
177
- c = conn.cursor()
178
- c.execute("DELETE FROM resource_cache WHERE media_type=? AND tmdb_id=?", (media_type, str(tmdb_id)))
179
- conn.commit()
180
- conn.close()
181
- print(f"🗑️ 已删除缓存: {media_type}/{tmdb_id}")
182
-
183
- def get_cached_unlock(slug):
184
- conn = sqlite3.connect(DB_PATH)
185
- c = conn.cursor()
186
- c.execute("SELECT link, unlock_data FROM unlock_cache WHERE slug=?", (slug,))
187
- row = c.fetchone()
188
- conn.close()
189
- if row and row[0]:
190
- print(f"📦 [缓存命中] 从数据库获取解锁链接: {slug}")
191
- return {'link': row[0], 'data': json.loads(row[1]) if row[1] else None}
192
- print(f"❌ [缓存未命中] 数据库中无记录: {slug}")
193
- return None
194
-
195
- def save_cached_unlock(slug, link, unlock_data):
196
- conn = sqlite3.connect(DB_PATH)
197
- c = conn.cursor()
198
- c.execute("INSERT OR REPLACE INTO unlock_cache (slug, link, unlock_data, updated_at) VALUES (?,?,?, CURRENT_TIMESTAMP)",
199
- (slug, link, json.dumps(unlock_data, ensure_ascii=False) if unlock_data else None))
200
- conn.commit()
201
- conn.close()
202
- print(f"💾 [缓存保存] 解锁链接已存入数据库: {slug} -> {link[:80]}...")
203
-
204
- api = HfApi()
205
-
206
- def backup_to_hf_dataset():
207
- if not HF_TOKEN or not DATASET_REPO or DATASET_REPO == "hdhive-cache":
208
- return False
209
- try:
210
- day = datetime.now().strftime("%Y-%m-%d")
211
- backup_name = f"backup_{day}.tar.gz"
212
- with tarfile.open(backup_name, "w:gz") as tar:
213
- tar.add(DB_PATH, arcname=os.path.basename(DB_PATH))
214
- api.upload_file(path_or_fileobj=backup_name, path_in_repo=backup_name, repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN)
215
- print(f"✅ 备份成功: {backup_name}")
216
- os.remove(backup_name)
217
- return True
218
- except Exception as e:
219
- print(f"❌ 备份失败: {e}")
220
- return False
221
-
222
- def restore_from_hf_dataset():
223
- if not HF_TOKEN or not DATASET_REPO or DATASET_REPO == "hdhive-cache":
224
- print("⚠️ 跳过恢复")
225
- return False
226
- try:
227
- files = api.list_repo_files(repo_id=DATASET_REPO, repo_type="dataset", token=HF_TOKEN)
228
- now = datetime.now()
229
- for i in range(5):
230
- day = (now - timedelta(days=i)).strftime("%Y-%m-%d")
231
- backup_name = f"backup_{day}.tar.gz"
232
- if backup_name in files:
233
- print(f"📦 发现备份: {backup_name},下载中...")
234
- path = hf_hub_download(repo_id=DATASET_REPO, filename=backup_name, repo_type="dataset", token=HF_TOKEN)
235
- with tarfile.open(path, "r:gz") as tar:
236
- tar.extractall(path=".")
237
- print(f"✅ 数据库已恢复")
238
- return True
239
- print("📭 未找到备份")
240
- return False
241
- except Exception as e:
242
- print(f"❌ 恢复失败: {e}")
243
- return False
244
-
245
- def periodic_backup():
246
- while True:
247
- time.sleep(BACKUP_INTERVAL)
248
- print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 执行备份...")
249
- backup_to_hf_dataset()
250
-
251
- def exit_backup(signum, frame):
252
- print(f"收到退出信号,执行最后备份...")
253
- backup_to_hf_dataset()
254
- sys.exit(0)
255
-
256
- signal.signal(signal.SIGTERM, exit_backup)
257
- signal.signal(signal.SIGINT, exit_backup)
258
-
259
- init_db()
260
- restore_from_hf_dataset()
261
- ensure_tables()
262
-
263
- backup_thread = threading.Thread(target=periodic_backup, daemon=True)
264
- backup_thread.start()
265
- print(f"✅ 定时备份已启动,间隔 {BACKUP_INTERVAL // 60} 分钟")
266
-
267
- # ========== 图片代理接口 ==========
268
- @app.route('/api/image/proxy')
269
- def image_proxy():
270
- img_url = request.args.get('url', '')
271
- if not img_url:
272
- return '', 400
273
- if not img_url.startswith('https://image.tmdb.org/'):
274
- return '', 403
275
- try:
276
- resp = requests.get(img_url, timeout=30, headers={
277
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
278
- 'Referer': 'https://www.themoviedb.org/',
279
- })
280
- if resp.status_code == 200:
281
- return Response(resp.content, status=200, content_type=resp.headers.get('Content-Type', 'image/jpeg'))
282
- else:
283
- print(f"图片代理失败: {img_url}, 状态码: {resp.status_code}")
284
- return '', 500
285
- except requests.exceptions.Timeout:
286
- print(f"图片代理超时: {img_url}")
287
- return '', 504
288
- except Exception as e:
289
- print(f"图片代理异常: {e}, URL: {img_url}")
290
- return '', 500
291
-
292
- # ========== 密码验证接口 ==========
293
- @app.route('/api/verify-password', methods=['POST'])
294
- def verify_password():
295
- data = request.json
296
- password = data.get('password', '')
297
- if UNLOCK_PASSWORD and password == UNLOCK_PASSWORD:
298
- return jsonify({'success': True, 'message': '密码正确'})
299
- elif not UNLOCK_PASSWORD:
300
- return jsonify({'success': False, 'message': '未设置解锁密码,请联系管理员'})
301
- else:
302
- return jsonify({'success': False, 'message': '密码错误'})
303
-
304
- # ========== 自动解锁统计接口 ==========
305
- @app.route('/api/auto-unlock-stats', methods=['GET'])
306
- def auto_unlock_stats():
307
- conn = sqlite3.connect(DB_PATH)
308
- c = conn.cursor()
309
- c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='auto_unlocked'")
310
- if not c.fetchone():
311
- conn.close()
312
- return jsonify({'unlocked_resources': 0})
313
- c.execute("SELECT COUNT(*) FROM auto_unlocked")
314
- unlocked_resources = c.fetchone()[0]
315
- conn.close()
316
- return jsonify({'unlocked_resources': unlocked_resources})
317
-
318
- @app.route('/api/next-unlock-time', methods=['GET'])
319
- def get_next_unlock_time():
320
- global next_unlock_time
321
- if next_unlock_time:
322
- remaining = max(0, int(next_unlock_time - time.time()))
323
- return jsonify({'remaining': remaining})
324
- return jsonify({'remaining': 0})
325
-
326
- # ========== 日志接口 ==========
327
- @app.route('/api/logs')
328
- def get_logs():
329
- return jsonify({'logs': list(LOG_BUFFER)})
330
-
331
- @app.route('/api/logs/clear', methods=['POST'])
332
- def clear_logs():
333
- LOG_BUFFER.clear()
334
- return jsonify({'success': True})
335
-
336
- # ========== TMDB 动态分类 API (仅 genre) ==========
337
- @app.route('/api/tmdb/genres/<media_type>')
338
- def get_tmdb_genres(media_type):
339
- if media_type not in ['movie', 'tv']:
340
- return jsonify({'error': 'invalid media type'}), 400
341
- url = f"https://api.themoviedb.org/3/genre/{media_type}/list?api_key={TMDB_API_KEY}&language=zh-CN"
342
- try:
343
- resp = requests.get(url, timeout=10)
344
- data = resp.json()
345
- return jsonify(data.get('genres', []))
346
- except Exception as e:
347
- return jsonify({'error': str(e)}), 500
348
-
349
- # ========== API 路由 ==========
350
- @app.route('/')
351
- def index():
352
- return render_template_string(HTML_TEMPLATE)
353
-
354
- @app.route('/api/cache/stats')
355
- def cache_stats():
356
- return jsonify(get_cache_stats())
357
-
358
- @app.route('/api/discover', methods=['POST'])
359
- def discover():
360
- data = request.json
361
- media_type = data.get('type', 'movie')
362
- page = data.get('page', 1)
363
- preset = data.get('preset')
364
- filters = data.get('filters', {})
365
- sort_by = filters.get('sort_by', 'latest')
366
-
367
- def build_discover_url(base_params):
368
- url = f"https://api.themoviedb.org/3/discover/{media_type}?api_key={TMDB_API_KEY}&language=zh-CN&page={page}"
369
- for k, v in base_params.items():
370
- url += f"&{k}={v}"
371
- region = filters.get('region')
372
- if region:
373
- region_code = {
374
- '大陆': 'CN', '香港': 'HK', '台湾': 'TW',
375
- '日本': 'JP', '韩国': 'KR', '欧美': 'US'
376
- }.get(region, '')
377
- if region_code:
378
- if media_type == 'movie':
379
- url += f"&with_origin_country={region_code}"
380
- else:
381
- url += f"&with_origin_country={region_code}"
382
- year = filters.get('year')
383
- if year:
384
- if media_type == 'movie':
385
- if year == '2010年前':
386
- url += "&primary_release_date.lte=2009-12-31"
387
- else:
388
- url += f"&primary_release_year={year}"
389
- else:
390
- if year == '2010年前':
391
- url += "&first_air_date.lte=2009-12-31"
392
- else:
393
- url += f"&first_air_date_year={year}"
394
- genres = filters.get('genres', [])
395
- if genres:
396
- genres_str = '|'.join(str(g) for g in genres)
397
- url += f"&with_genres={genres_str}"
398
- return url
399
-
400
- if preset:
401
- preset_map_movie = {
402
- 'popular': {'sort_by': 'popularity.desc'},
403
- 'now_playing': {'sort_by': 'primary_release_date.desc'},
404
- 'top_rated': {'sort_by': 'vote_average.desc', 'vote_count.gte': 30},
405
- 'upcoming': {'sort_by': 'primary_release_date.asc'}
406
- }
407
- preset_map_tv = {
408
- 'popular': {'sort_by': 'popularity.desc'},
409
- 'top_rated': {'sort_by': 'vote_average.desc', 'vote_count.gte': 30},
410
- 'on_the_air': {'sort_by': 'first_air_date.desc'},
411
- 'airing_today': {'sort_by': 'first_air_date.desc'}
412
- }
413
- if media_type == 'movie':
414
- base_params = preset_map_movie.get(preset, {'sort_by': 'popularity.desc'})
415
- else:
416
- base_params = preset_map_tv.get(preset, {'sort_by': 'popularity.desc'})
417
- if filters.get('sort_by'):
418
- user_sort = filters['sort_by']
419
- if user_sort == 'latest':
420
- base_params['sort_by'] = 'primary_release_date.desc' if media_type == 'movie' else 'first_air_date.desc'
421
- elif user_sort == 'imdb':
422
- base_params['sort_by'] = 'vote_average.desc'
423
- base_params['vote_count.gte'] = 30
424
- url = build_discover_url(base_params)
425
- resp = requests.get(url, timeout=15)
426
- result = resp.json()
427
- results = result.get('results', [])
428
- for item in results:
429
- mtype = media_type if media_type in ['movie', 'tv'] else (item.get('media_type') or 'movie')
430
- item['cached'] = is_cached(mtype, item['id'])
431
- result['results'] = results
432
- return jsonify(result)
433
-
434
- # 无预设
435
- if media_type == 'movie':
436
- base_url = f"https://api.themoviedb.org/3/discover/movie?api_key={TMDB_API_KEY}&language=zh-CN&page={page}"
437
- if sort_by == 'latest':
438
- base_url += "&sort_by=primary_release_date.desc"
439
- else:
440
- base_url += "&sort_by=vote_average.desc&vote_count.gte=30"
441
- region = filters.get('region')
442
- if region:
443
- region_code = {
444
- '大陆': 'CN', '香港': 'HK', '台湾': 'TW',
445
- '日本': 'JP', '韩国': 'KR', '欧美': 'US'
446
- }.get(region, '')
447
- if region_code:
448
- base_url += f"&with_origin_country={region_code}"
449
- year = filters.get('year')
450
- if year:
451
- if year == '2010年前':
452
- base_url += "&primary_release_date.lte=2009-12-31"
453
- else:
454
- base_url += f"&primary_release_year={year}"
455
- genres = filters.get('genres', [])
456
- if genres:
457
- genres_str = '|'.join(str(g) for g in genres)
458
- base_url += f"&with_genres={genres_str}"
459
- else:
460
- base_url = f"https://api.themoviedb.org/3/discover/tv?api_key={TMDB_API_KEY}&language=zh-CN&page={page}"
461
- if sort_by == 'latest':
462
- base_url += "&sort_by=first_air_date.desc"
463
- else:
464
- base_url += "&sort_by=vote_average.desc&vote_count.gte=30"
465
- region = filters.get('region')
466
- if region:
467
- region_code = {
468
- '大陆': 'CN', '香港': 'HK', '台湾': 'TW',
469
- '日本': 'JP', '韩国': 'KR', '欧美': 'US'
470
- }.get(region, '')
471
- if region_code:
472
- base_url += f"&with_origin_country={region_code}"
473
- year = filters.get('year')
474
- if year:
475
- if year == '2010年前':
476
- base_url += "&first_air_date.lte=2009-12-31"
477
- else:
478
- base_url += f"&first_air_date_year={year}"
479
- genres = filters.get('genres', [])
480
- if genres:
481
- genres_str = '|'.join(str(g) for g in genres)
482
- base_url += f"&with_genres={genres_str}"
483
- resp = requests.get(base_url, timeout=15)
484
- result = resp.json()
485
- results = result.get('results', [])
486
- for item in results:
487
- mtype = media_type if media_type in ['movie', 'tv'] else (item.get('media_type') or 'movie')
488
- item['cached'] = is_cached(mtype, item['id'])
489
- result['results'] = results
490
- return jsonify(result)
491
-
492
- @app.route('/api/search', methods=['POST'])
493
- def search_api():
494
- data = request.json
495
- query = data.get('query', '')
496
- page = data.get('page', 1)
497
- url = f"https://api.themoviedb.org/3/search/multi?api_key={TMDB_API_KEY}&query={query}&language=zh-CN&page={page}"
498
- resp = requests.get(url, timeout=15)
499
- result = resp.json()
500
- results = result.get('results', [])
501
- filtered = []
502
- for item in results:
503
- if item.get('media_type') in ('movie', 'tv'):
504
- item['cached'] = is_cached(item['media_type'], item['id'])
505
- filtered.append(item)
506
- result['results'] = filtered
507
- return jsonify(result)
508
-
509
- @app.route('/api/cache/resources/<type>/<tmdb_id>')
510
- def cache_resources(type, tmdb_id):
511
- cached = get_cached_resources(type, tmdb_id)
512
- if cached:
513
- return jsonify({'success': True, 'from_cache': True, 'resources': cached})
514
- if not HDHIVE_API_KEY:
515
- return jsonify({"success": False, "error": "HDHIVE_API_KEY not configured"}), 500
516
- url = f"https://hdhive.com/api/open/resources/{type}/{tmdb_id}"
517
- headers = {"X-API-Key": HDHIVE_API_KEY, "Accept": "application/json"}
518
- resp = requests.get(url, headers=headers, timeout=30)
519
- result = resp.json()
520
- resources = result.get('data', []) if isinstance(result, dict) else result
521
- if not resources:
522
- resources = []
523
- for item in resources:
524
- if not item.get('pan_type') or item['pan_type'] == 'unknown':
525
- texts = []
526
- for field in ['link', 'slug', 'title', 'name', 'download_url', 'url', 'pan_url']:
527
- val = item.get(field)
528
- if val:
529
- texts.append(str(val))
530
- combined = '; '.join(texts)
531
- item['pan_type'] = detect_pan_type(combined)
532
- save_cached_resources(type, tmdb_id, resources)
533
- return jsonify({'success': True, 'from_cache': False, 'resources': resources})
534
-
535
- @app.route('/api/cache/refresh/<type>/<tmdb_id>', methods=['POST'])
536
- def refresh_cache(type, tmdb_id):
537
- delete_cached_resources(type, tmdb_id)
538
- if not HDHIVE_API_KEY:
539
- return jsonify({"success": False, "error": "HDHIVE_API_KEY not configured"}), 500
540
- url = f"https://hdhive.com/api/open/resources/{type}/{tmdb_id}"
541
- headers = {"X-API-Key": HDHIVE_API_KEY, "Accept": "application/json"}
542
- try:
543
- resp = requests.get(url, headers=headers, timeout=30)
544
- result = resp.json()
545
- resources = result.get('data', []) if isinstance(result, dict) else result
546
- if not resources:
547
- resources = []
548
- for item in resources:
549
- if not item.get('pan_type') or item['pan_type'] == 'unknown':
550
- texts = []
551
- for field in ['link', 'slug', 'title', 'name', 'download_url', 'url', 'pan_url']:
552
- val = item.get(field)
553
- if val:
554
- texts.append(str(val))
555
- combined = '; '.join(texts)
556
- item['pan_type'] = detect_pan_type(combined)
557
- save_cached_resources(type, tmdb_id, resources)
558
- return jsonify({"success": True, "message": "缓存已刷新"})
559
- except Exception as e:
560
- return jsonify({"success": False, "error": str(e)}), 500
561
-
562
- @app.route('/api/cache/unlock', methods=['POST'])
563
- def cache_unlock():
564
- data = request.json
565
- slug = data.get('slug')
566
- allow = data.get('allow_points', False)
567
- password = data.get('password', '')
568
-
569
- print(f"\n{'='*60}")
570
- print(f"🔓 解锁请求: slug={slug}, allow_points={allow}")
571
-
572
- # 第一步:检查本地缓存
573
- cached = get_cached_unlock(slug)
574
- if cached and cached.get('link'):
575
- print(f"✅ 返回缓存链接 (不消耗积分)")
576
- print(f"{'='*60}\n")
577
- return jsonify({'success': True, 'from_cache': True, 'link': cached['link'], 'data': cached['data']})
578
-
579
- if not HDHIVE_API_KEY:
580
- print(f"❌ HDHIVE_API_KEY 未配置")
581
- print(f"{'='*60}\n")
582
- return jsonify({"error": "HDHIVE_API_KEY not configured"}), 500
583
-
584
- # 第二步:缓存未命中,需要调用 HDHive API
585
- # 先获取资源积分信息(用于判断是否需要密码)
586
- points = None
587
- # 从 resource_cache 中查找该 slug 的积分
588
- conn = sqlite3.connect(DB_PATH)
589
- c = conn.cursor()
590
- c.execute("SELECT resource_data FROM resource_cache")
591
- for rrow in c.fetchall():
592
- try:
593
- resources = json.loads(rrow[0])
594
- for res in resources:
595
- if res.get('slug') == slug:
596
- points = res.get('unlock_points', 0)
597
- break
598
- if points is not None:
599
- break
600
- except:
601
- continue
602
- conn.close()
603
- if points is None:
604
- # 抓取页面获取积分
605
- try:
606
- for prefix in ['115', 'baidu', 'magnet']:
607
- url = f"https://hdhive.com/resource/{prefix}/{slug}"
608
- resp = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
609
- if resp.status_code == 200:
610
- html = resp.text
611
- match = re.search(r'所需积分[::]\s*(\d+)', html)
612
- if not match:
613
- match = re.search(r'unlock_points[::]\s*(\d+)', html, re.IGNORECASE)
614
- if not match:
615
- match = re.search(r'<span[^>]*class="[^"]*points?[^"]*"[^>]*>(\d+)</span>', html, re.IGNORECASE)
616
- if match:
617
- points = int(match.group(1))
618
- else:
619
- points = 0
620
- break
621
- except Exception as e:
622
- print(f"获取 points 失败: {e}")
623
- points = 0
624
-
625
- # 如果需要密码(设置了全局密码 且 资源需要积分)
626
- if UNLOCK_PASSWORD and points and points > 0:
627
- if not password or password != UNLOCK_PASSWORD:
628
- print(f"🔐 付费资源需要密码")
629
- return jsonify({'need_password': True, 'message': '付费资源需要解锁密码'}), 401
630
-
631
- print(f"🌐 调用 HDHive API 解锁...")
632
- url = "https://hdhive.com/api/open/resources/unlock"
633
- headers = {"X-API-Key": HDHIVE_API_KEY, "Content-Type": "application/json"}
634
- payload = {"slug": slug}
635
- if allow:
636
- payload["allow_points"] = True
637
-
638
- resp = requests.post(url, headers=headers, json=payload, timeout=30)
639
- result = resp.json()
640
-
641
- if result.get('code') == 'OPENAPI_COOLDOWN':
642
- print(f"⏰ API 冷却中: {result}")
643
- print(f"{'='*60}\n")
644
- return jsonify(result)
645
-
646
- link = result.get('data', {}).get('full_url') or result.get('data', {}).get('url') or result.get('url')
647
- if link:
648
- save_cached_unlock(slug, link, result)
649
- print(f"✅ 解锁成功,已缓存 (消耗积分)")
650
- else:
651
- print(f"❌ 解锁失败: {result}")
652
-
653
- print(f"{'='*60}\n")
654
- return jsonify({'success': True, 'from_cache': False, 'link': link, 'data': result.get('data')})
655
-
656
- @app.route('/api/backup', methods=['POST'])
657
- def manual_backup():
658
- success = backup_to_hf_dataset()
659
- return jsonify({"message": "备份成功" if success else "备份失败"}), 200 if success else 500
660
-
661
- # ========== 后台自动浏览并解锁免费资源 ==========
662
- MOVIE_GENRES = {
663
- 28: "动作", 12: "冒险", 16: "动画", 35: "喜剧", 80: "犯罪", 99: "纪录片",
664
- 18: "剧情", 10751: "家庭", 14: "奇幻", 36: "历史", 27: "恐怖", 10402: "音乐",
665
- 9648: "悬疑", 10749: "爱情", 878: "科幻", 53: "惊悚", 10752: "战争", 37: "西部"
666
- }
667
- TV_GENRES = {
668
- 10759: "动作冒险", 16: "动画", 35: "喜剧", 80: "犯罪", 99: "纪录片",
669
- 18: "剧情", 10751: "家庭", 10762: "儿童", 9648: "悬疑", 10763: "新闻",
670
- 10764: "真人秀", 10765: "科幻奇幻", 10766: "肥皂剧", 10767: "脱口秀", 10768: "政治"
671
- }
672
-
673
- def get_random_movie_from_tmdb():
674
- global processed_movies
675
- max_attempts = 10
676
- for attempt in range(max_attempts):
677
- year = random.randint(1900, 2026)
678
- genre_id = random.choice(list(MOVIE_GENRES.keys()))
679
- sort_options = ['popularity.desc', 'vote_average.desc', 'release_date.desc', 'primary_release_date.desc']
680
- sort_by = random.choice(sort_options)
681
- page = random.randint(1, 1000)
682
- 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"
683
- print(f"[DEBUG] 电影尝试 {attempt+1}: year={year}, genre={genre_id}, sort={sort_by}, page={page}")
684
- try:
685
- resp = requests.get(url, timeout=15)
686
- data = resp.json()
687
- results = data.get('results', [])
688
- if not results:
689
- continue
690
- random.shuffle(results)
691
- for movie in results:
692
- tmdb_id = movie['id']
693
- key = f"movie_{tmdb_id}"
694
- if key in processed_movies:
695
- continue
696
- conn = sqlite3.connect(DB_PATH)
697
- c = conn.cursor()
698
- c.execute("SELECT 1 FROM resource_cache WHERE media_type='movie' AND tmdb_id=?", (str(tmdb_id),))
699
- exists = c.fetchone() is not None
700
- conn.close()
701
- if not exists:
702
- processed_movies.add(key)
703
- print(f"✅ 找到未缓存电影: {movie.get('title', '未知')} (ID: {tmdb_id})")
704
- return {
705
- 'media_type': 'movie',
706
- 'tmdb_id': tmdb_id,
707
- 'title': movie.get('title', '未知'),
708
- 'year': year,
709
- 'genre': MOVIE_GENRES.get(genre_id, '未知')
710
- }
711
- except Exception as e:
712
- print(f"❌ 电影尝试 {attempt+1} 失败: {e}")
713
- try:
714
- url = f"https://api.themoviedb.org/3/movie/now_playing?api_key={TMDB_API_KEY}&language=zh-CN&page={random.randint(1,5)}"
715
- resp = requests.get(url, timeout=15)
716
- data = resp.json()
717
- results = data.get('results', [])
718
- random.shuffle(results)
719
- for movie in results:
720
- tmdb_id = movie['id']
721
- key = f"movie_{tmdb_id}"
722
- if key in processed_movies:
723
- continue
724
- conn = sqlite3.connect(DB_PATH)
725
- c = conn.cursor()
726
- c.execute("SELECT 1 FROM resource_cache WHERE media_type='movie' AND tmdb_id=?", (str(tmdb_id),))
727
- exists = c.fetchone() is not None
728
- conn.close()
729
- if not exists:
730
- processed_movies.add(key)
731
- print(f"✅ [降级] 找到未缓存电影: {movie.get('title', '未知')} (ID: {tmdb_id})")
732
- return {
733
- 'media_type': 'movie',
734
- 'tmdb_id': tmdb_id,
735
- 'title': movie.get('title', '未知'),
736
- 'year': movie.get('release_date', '')[:4] if movie.get('release_date') else '',
737
- 'genre': '未知'
738
- }
739
- except Exception as e:
740
- print(f"❌ 降级获取电影失败: {e}")
741
- return None
742
-
743
- def get_random_tv_from_tmdb():
744
- global processed_movies
745
- max_attempts = 10
746
- for attempt in range(max_attempts):
747
- year = random.randint(1900, 2026)
748
- genre_id = random.choice(list(TV_GENRES.keys()))
749
- sort_options = ['popularity.desc', 'vote_average.desc', 'first_air_date.desc', 'name.asc']
750
- sort_by = random.choice(sort_options)
751
- page = random.randint(1, 1000)
752
- 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"
753
- print(f"[DEBUG] 电视剧尝试 {attempt+1}: year={year}, genre={genre_id}, sort={sort_by}, page={page}")
754
- try:
755
- resp = requests.get(url, timeout=15)
756
- data = resp.json()
757
- results = data.get('results', [])
758
- if not results:
759
- continue
760
- random.shuffle(results)
761
- for tv in results:
762
- tmdb_id = tv['id']
763
- key = f"tv_{tmdb_id}"
764
- if key in processed_movies:
765
- continue
766
- conn = sqlite3.connect(DB_PATH)
767
- c = conn.cursor()
768
- c.execute("SELECT 1 FROM resource_cache WHERE media_type='tv' AND tmdb_id=?", (str(tmdb_id),))
769
- exists = c.fetchone() is not None
770
- conn.close()
771
- if not exists:
772
- processed_movies.add(key)
773
- print(f"✅ 找到未缓存电视剧: {tv.get('name', '未知')} (ID: {tmdb_id})")
774
- return {
775
- 'media_type': 'tv',
776
- 'tmdb_id': tmdb_id,
777
- 'title': tv.get('name', '未知'),
778
- 'year': year,
779
- 'genre': TV_GENRES.get(genre_id, '未知')
780
- }
781
- except Exception as e:
782
- print(f"❌ 电视剧尝试 {attempt+1} 失败: {e}")
783
- try:
784
- url = f"https://api.themoviedb.org/3/tv/on_the_air?api_key={TMDB_API_KEY}&language=zh-CN&page={random.randint(1,5)}"
785
- resp = requests.get(url, timeout=15)
786
- data = resp.json()
787
- results = data.get('results', [])
788
- random.shuffle(results)
789
- for tv in results:
790
- tmdb_id = tv['id']
791
- key = f"tv_{tmdb_id}"
792
- if key in processed_movies:
793
- continue
794
- conn = sqlite3.connect(DB_PATH)
795
- c = conn.cursor()
796
- c.execute("SELECT 1 FROM resource_cache WHERE media_type='tv' AND tmdb_id=?", (str(tmdb_id),))
797
- exists = c.fetchone() is not None
798
- conn.close()
799
- if not exists:
800
- processed_movies.add(key)
801
- print(f"✅ [降级] 找到未缓存电视剧: {tv.get('name', '未知')} (ID: {tmdb_id})")
802
- return {
803
- 'media_type': 'tv',
804
- 'tmdb_id': tmdb_id,
805
- 'title': tv.get('name', '未知'),
806
- 'year': tv.get('first_air_date', '')[:4] if tv.get('first_air_date') else '',
807
- 'genre': '未知'
808
- }
809
- except Exception as e:
810
- print(f"❌ 降级获取电视剧失败: {e}")
811
- return None
812
-
813
- def auto_browse_and_unlock():
814
- global next_unlock_time
815
- print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 🔄 [后台] 进入自动解锁任务")
816
- if not HDHIVE_API_KEY:
817
- print("❌ [后台] HDHIVE_API_KEY 缺失")
818
- schedule_next_job()
819
- return {'success': False, 'message': 'HDHIVE_API_KEY not configured'}
820
-
821
- print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 🔄 [后台] 开始随机获取未缓存影片...")
822
-
823
- media_type_choice = random.choice(['movie', 'tv'])
824
- if media_type_choice == 'movie':
825
- info = get_random_movie_from_tmdb()
826
- if not info:
827
- print(f"📭 [后台] 未找到新的未缓存电影")
828
- schedule_next_job()
829
- return {'success': False, 'message': '未找到新的未缓存电影'}
830
- media_type, tmdb_id, title = 'movie', info['tmdb_id'], info['title']
831
- print(f"🎲 [后台] 选中未缓存电影: {title} (ID: {tmdb_id})")
832
- else:
833
- info = get_random_tv_from_tmdb()
834
- if not info:
835
- print(f"📭 [后台] 未找到新的未缓存电视剧")
836
- schedule_next_job()
837
- return {'success': False, 'message': '未找到新的未缓存电视剧'}
838
- media_type, tmdb_id, title = 'tv', info['tmdb_id'], info['title']
839
- print(f"🎲 [后台] 选中未缓存电视剧: {title} (ID: {tmdb_id})")
840
-
841
- url = f"https://hdhive.com/api/open/resources/{media_type}/{tmdb_id}"
842
- headers = {"X-API-Key": HDHIVE_API_KEY, "Accept": "application/json"}
843
-
844
- try:
845
- resp = requests.get(url, headers=headers, timeout=30)
846
- result = resp.json()
847
- resources = result.get('data', []) if isinstance(result, dict) else result
848
-
849
- if not resources:
850
- print(f"📭 [后台] {title} 没有资源")
851
- schedule_next_job()
852
- return {'success': False, 'message': f'{title} 没有资源'}
853
-
854
- print(f"📊 [后台] {title} 共有 {len(resources)} 个资源")
855
- free_count = sum(1 for r in resources if r.get('unlock_points', 0) == 0)
856
- print(f"📊 [后台] 其中免费资源: {free_count} 个")
857
-
858
- save_cached_resources(media_type, tmdb_id, resources)
859
- print(f"💾 [后台] 已缓存 {media_type}/{tmdb_id}, 共 {len(resources)} 个资源")
860
-
861
- conn = sqlite3.connect(DB_PATH)
862
- c = conn.cursor()
863
- c.execute("CREATE TABLE IF NOT EXISTS auto_unlocked (slug TEXT PRIMARY KEY, unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
864
-
865
- unlocked_any = False
866
- for res in resources:
867
- points = res.get('unlock_points', 0)
868
- slug = res.get('slug')
869
- if slug and points == 0:
870
- c.execute("SELECT 1 FROM auto_unlocked WHERE slug=?", (slug,))
871
- if not c.fetchone():
872
- print(f"🔓 [后台] 发现未解锁免费资源,开始解锁: {slug}")
873
- unlock_url = "https://hdhive.com/api/open/resources/unlock"
874
- unlock_payload = {"slug": slug, "allow_points": True}
875
- unlock_resp = requests.post(unlock_url, headers=headers, json=unlock_payload, timeout=30)
876
- unlock_result = unlock_resp.json()
877
- link = unlock_result.get('data', {}).get('full_url') or unlock_result.get('data', {}).get('url') or unlock_result.get('url')
878
- if link:
879
- c.execute("INSERT OR REPLACE INTO unlock_cache (slug, link, unlock_data, updated_at) VALUES (?,?,?, CURRENT_TIMESTAMP)",
880
- (slug, link, json.dumps(unlock_result, ensure_ascii=False)))
881
- c.execute("INSERT INTO auto_unlocked (slug) VALUES (?)", (slug,))
882
- conn.commit()
883
- print(f"✅ [后台] 自动解锁成功: {title} - {slug}")
884
- unlocked_any = True
885
- break
886
- else:
887
- print(f"⚠️ [后台] 解锁失败: {slug} (无链接)")
888
- else:
889
- print(f"⏭️ [后台] 资源已解锁过,跳过: {slug}")
890
-
891
- conn.close()
892
- if unlocked_any:
893
- result = {'success': True, 'message': f'解锁成功: {title}', 'title': title}
894
- else:
895
- print(f"📭 [后台] {title} 没有可解锁的免费资源(可能已全部解锁)")
896
- result = {'success': False, 'message': f'{title} 没有可解锁的免费资源'}
897
-
898
- schedule_next_job()
899
- return result
900
-
901
- except Exception as e:
902
- print(f"❌ [后台] 处理失败: {e}")
903
- schedule_next_job()
904
- return {'success': False, 'message': str(e)}
905
-
906
- def schedule_next_job():
907
- interval_minutes = random.randint(1, 3)
908
- next_run = datetime.now() + timedelta(minutes=interval_minutes)
909
- global next_unlock_time
910
- next_unlock_time = next_run.timestamp()
911
- print(f"⏰ [后台] 下次浏览将在 {interval_minutes} 分钟后执行({next_run.strftime('%H:%M:%S')})")
912
- if scheduler.get_job('auto_unlock_job'):
913
- scheduler.remove_job('auto_unlock_job')
914
- scheduler.add_job(auto_browse_and_unlock, 'date', run_date=next_run, id='auto_unlock_job')
915
-
916
- schedule_next_job()
917
- scheduler.start()
918
- print(f"✅ 后台自动浏览已启动,随机间隔 1-3 分钟,从 TMDB 所有影片中随机选择未缓存内容")
919
-
920
- # ========== HTML 模板 ==========
921
- HTML_TEMPLATE = r"""<!DOCTYPE html>
922
- <html>
923
- <head>
924
- <meta charset="UTF-8">
925
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
926
- <title>HDHive 影视资源</title>
927
- <style>
928
- * { margin: 0; padding: 0; box-sizing: border-box; }
929
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f0f0f; color: #e0e0e0; padding: 16px; }
930
- .container { max-width: 1200px; margin: 0 auto; }
931
- .header { text-align: center; margin-bottom: 20px; }
932
- h1 { font-size: 28px; font-weight: 600; margin-bottom: 6px; }
933
- .sub { font-size: 14px; color: #aaa; }
934
- .stats-row { display: flex; justify-content: center; gap: 32px; background: #1a1a2e; border-radius: 12px; padding: 10px 20px; margin-bottom: 20px; border: 1px solid #2a2a4a; flex-wrap: wrap; }
935
- .stat-item { text-align: center; }
936
- .stat-number { font-size: 24px; font-weight: 700; color: #e50914; }
937
- .stat-label { font-size: 13px; color: #aaa; }
938
- .filter-notice { background: #1a1a2e; border-radius: 12px; padding: 8px 16px; margin-bottom: 16px; text-align: center; font-size: 13px; border: 1px solid #2a2a4a; }
939
- .categories-section { background: #1a1a1a; border-radius: 14px; padding: 16px; margin-bottom: 16px; border: 1px solid #2a2a2a; }
940
- .type-switch { display: flex; gap: 12px; justify-content: center; margin-bottom: 20px; }
941
- .type-btn { padding: 8px 24px; border-radius: 30px; background: #2a2a2a; border: none; color: #ccc; cursor: pointer; font-size: 15px; font-weight: 500; }
942
- .type-btn.active { background: #e50914; color: #fff; }
943
- .preset-bar { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid #2a2a2a; }
944
- .preset-btn { padding: 6px 18px; border-radius: 30px; background: #2a2a2a; border: none; color: #ccc; cursor: pointer; font-size: 13px; transition: 0.2s; }
945
- .preset-btn.active { background: #e50914; color: #fff; }
946
- .filter-collapse { margin-top: 8px; border-top: 1px solid #2a2a2a; }
947
- .filter-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; cursor: pointer; user-select: none; color: #e50914; font-size: 14px; font-weight: 500; }
948
- .filter-header:hover { opacity: 0.8; }
949
- .filter-header span:first-child { display: flex; align-items: center; gap: 8px; }
950
- .toggle-icon { font-size: 18px; transition: transform 0.2s; }
951
- .filter-content { transition: all 0.2s ease; overflow: hidden; }
952
- .filter-content.collapsed { display: none; }
953
- .movie-cats, .tv-cats { display: flex; flex-direction: column; gap: 15px; }
954
- .filter-row { margin-bottom: 15px; }
955
- .filter-label { font-size: 13px; color: #aaa; margin-bottom: 8px; text-align: center; font-weight: 600; }
956
- .filter-buttons { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
957
- .filter-btn-item { padding: 6px 16px; border-radius: 30px; background: #2a2a2a; border: none; color: #aaa; cursor: pointer; font-size: 13px; transition: 0.2s; }
958
- .filter-btn-item.active { background: #e50914; color: #fff; }
959
- .search-section { background: #1a1a1a; border-radius: 14px; padding: 14px 16px; margin-bottom: 16px; border: 1px solid #2a2a2a; text-align: center; }
960
- .search-bar { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
961
- .search-bar input { width: 300px; padding: 12px 16px; border-radius: 40px; background: #2a2a2a; border: none; color: #fff; font-size: 15px; }
962
- .search-bar button { padding: 12px 24px; border-radius: 40px; background: #e50914; border: none; color: #fff; font-weight: 600; cursor: pointer; font-size: 15px; }
963
- /* 快捷解锁面板 */
964
- .quick-unlock { background: #1a1a1a; border-radius: 14px; padding: 14px 16px; margin-bottom: 16px; border: 1px solid #2a2a2a; }
965
- .quick-unlock-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; color: #e50914; font-size: 14px; font-weight: 500; margin-bottom: 12px; }
966
- .quick-unlock-content { transition: all 0.2s; }
967
- .quick-unlock-content.collapsed { display: none; }
968
- .url-input-group { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
969
- .url-input-group input { flex: 1; min-width: 200px; padding: 10px 14px; border-radius: 40px; background: #2a2a2a; border: none; color: #fff; font-size: 14px; }
970
- .url-input-group button { padding: 10px 24px; border-radius: 40px; background: #e50914; border: none; color: #fff; font-weight: 600; cursor: pointer; }
971
- .unlock-result { background: #0f0f0f; border-radius: 12px; padding: 12px; margin-top: 12px; font-size: 13px; word-break: break-all; border-left: 3px solid #e50914; }
972
- .unlock-result a { color: #e50914; text-decoration: none; }
973
- .copy-result-btn { background: #555; border: none; border-radius: 16px; padding: 4px 12px; color: #fff; cursor: pointer; font-size: 11px; margin-left: 10px; }
974
- .resource-detail { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
975
- .resource-detail .pan-badge { font-size: 12px; }
976
- .two-columns { display: flex; gap: 20px; flex-wrap: wrap; }
977
- .left-panel, .right-panel { flex: 1; min-width: 280px; background: #141414; border-radius: 16px; border: 1px solid #2a2a2a; overflow: visible; }
978
- .panel-header { padding: 14px 18px; border-bottom: 1px solid #2a2a2a; display: flex; justify-content: space-between; align-items: baseline; }
979
- .panel-title { font-size: 16px; font-weight: 600; }
980
- .panel-count { font-size: 13px; color: #888; }
981
- .movies-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding: 16px; }
982
- @media (max-width: 700px) { .movies-grid { grid-template-columns: repeat(2, 1fr); } }
983
- .movie-card { background: #1e1e1e; border-radius: 12px; overflow: hidden; cursor: pointer; border: 2px solid transparent; transition: 0.2s; position: relative; }
984
- .movie-card.selected { border-color: #e50914; background: #2a2a2a; }
985
- .movie-poster { width: 100%; aspect-ratio: 2/3; object-fit: cover; background: #2a2a2a; display: block; }
986
- .movie-info { padding: 10px; }
987
- .movie-title { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
988
- .movie-year { font-size: 12px; color: #aaa; margin-top: 2px; }
989
- .refresh-cache-btn {
990
- position: absolute;
991
- top: 6px;
992
- right: 6px;
993
- background: rgba(0,0,0,0.6);
994
- border: none;
995
- border-radius: 20px;
996
- padding: 4px 8px;
997
- font-size: 12px;
998
- cursor: pointer;
999
- color: #fff;
1000
- z-index: 10;
1001
- backdrop-filter: blur(4px);
1002
- transition: 0.2s;
1003
- }
1004
- .refresh-cache-btn:hover { background: #e50914; transform: scale(1.05); }
1005
- .selected-media { padding: 14px; border-bottom: 1px solid #2a2a2a; }
1006
- .media-info { display: flex; gap: 14px; }
1007
- .media-poster { width: 70px; height: 105px; object-fit: cover; border-radius: 8px; background: #2a2a2a; }
1008
- .media-details { flex: 1; }
1009
- .media-title { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
1010
- .media-meta { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
1011
- .media-badge { background: #e50914; padding: 4px 10px; border-radius: 16px; font-size: 12px; }
1012
- .media-overview { font-size: 13px; color: #aaa; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; overflow: hidden; }
1013
- .resource-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; border-bottom: 1px solid #2a2a2a; }
1014
- .checkbox-label { display: flex; align-items: center; gap: 6px; font-size: 13px; cursor: pointer; }
1015
- .resource-list { display: flex; flex-direction: column; gap: 12px; padding: 14px; }
1016
- .resource-card { background: #1e1e1e; border-radius: 12px; padding: 12px; border-left: 3px solid; }
1017
- .resource-card.free { border-left-color: #2ecc71; }
1018
- .resource-card.paid { border-left-color: #e50914; }
1019
- .pan-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 16px; font-size: 11px; font-weight: 600; background: #333; }
1020
- .pan-badge-baidu { background: #f39c12; color: #000; }
1021
- .pan-badge-115 { background: #4caf50; color: #fff; }
1022
- .pan-badge-magnet { background: #9e9e9e; color: #fff; }
1023
- .pan-badge-unknown { background: #555; color: #fff; }
1024
- .resource-title { font-weight: 600; margin-bottom: 8px; font-size: 14px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1025
- .resource-meta { font-size: 12px; color: #aaa; margin-bottom: 10px; display: flex; gap: 12px; flex-wrap: wrap; }
1026
- .resource-link { background: #2a2a2a; border-radius: 8px; padding: 10px; font-size: 12px; word-break: break-all; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
1027
- .resource-link a { color: #e50914; text-decoration: none; flex: 1; }
1028
- .copy-btn { background: #555; border: none; border-radius: 16px; padding: 4px 12px; color: #fff; cursor: pointer; font-size: 11px; transition: 0.2s; }
1029
- .copy-btn:hover { background: #e50914; }
1030
- .unlock-btn { background: #e50914; border: none; border-radius: 30px; padding: 8px 14px; color: white; font-weight: 600; cursor: pointer; width: 100%; font-size: 13px; }
1031
- .pagination { display: flex; justify-content: center; gap: 8px; padding: 14px; border-top: 1px solid #2a2a2a; flex-wrap: wrap; }
1032
- .page-btn { padding: 6px 14px; border-radius: 30px; background: #2a2a2a; border: none; color: #fff; cursor: pointer; font-size: 13px; }
1033
- .page-btn.active { background: #e50914; }
1034
- .loading, .empty { text-align: center; padding: 40px; color: #aaa; font-size: 14px; }
1035
- .cache-badge { background: #2ecc71; color: #000; padding: 2px 8px; border-radius: 12px; font-size: 10px; margin-left: 8px; }
1036
- .backup-btn { background: #2ecc71; border: none; border-radius: 30px; padding: 6px 14px; color: #000; cursor: pointer; font-size: 13px; margin: 0 6px; }
1037
- .status-msg { margin-top: 8px; font-size: 13px; color: #aaa; }
1038
- .bottom-bar { text-align: center; margin: 20px 0; }
1039
- .debug-btn { background: #e50914; border: none; border-radius: 30px; padding: 6px 14px; color: #fff; cursor: pointer; font-size: 13px; margin: 0 6px; }
1040
- .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); }
1041
- .modal-content { background-color: #1e1e1e; margin: 5% auto; padding: 0; border: 1px solid #e50914; border-radius: 12px; width: 90%; max-width: 600px; max-height: 80%; display: flex; flex-direction: column; overflow: hidden; color: #e0e0e0; font-family: monospace; font-size: 12px; }
1042
- .modal-header { position: sticky; top: 0; background: #1e1e1e; z-index: 1; display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-bottom: 1px solid #333; }
1043
- .modal-title { font-size: 18px; font-weight: bold; color: #e50914; }
1044
- .close-modal, .clear-logs { background: #333; border: none; padding: 5px 12px; border-radius: 20px; cursor: pointer; color: #fff; margin-left: 8px; }
1045
- .close-modal:hover, .clear-logs:hover { background: #e50914; }
1046
- .modal-body { flex: 1; overflow-y: auto; padding: 12px 20px; }
1047
- .log-entry { margin: 5px 0; padding: 4px; border-left: 3px solid; word-break: break-word; }
1048
- .log-success { border-left-color: #2ecc71; }
1049
- .log-error { border-left-color: #e74c3c; }
1050
- .log-warning { border-left-color: #f39c12; }
1051
- .log-info { border-left-color: #3498db; }
1052
- .password-modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); }
1053
- .password-modal-content { background-color: #1e1e1e; margin: 20% auto; padding: 20px; border: 1px solid #e50914; border-radius: 12px; width: 90%; max-width: 350px; text-align: center; }
1054
- .password-modal-content input { width: 100%; padding: 10px; margin: 15px 0; background: #2a2a2a; border: 1px solid #555; border-radius: 8px; color: #fff; font-size: 16px; }
1055
- .password-modal-content button { background: #e50914; border: none; padding: 10px 20px; border-radius: 30px; color: #fff; cursor: pointer; margin: 5px; }
1056
- .toast-msg {
1057
- position: fixed;
1058
- top: 20px;
1059
- left: 50%;
1060
- transform: translateX(-50%);
1061
- background: rgba(0,0,0,0.85);
1062
- color: #fff;
1063
- padding: 8px 20px;
1064
- border-radius: 40px;
1065
- font-size: 14px;
1066
- z-index: 2000;
1067
- backdrop-filter: blur(8px);
1068
- border: 1px solid #e50914;
1069
- pointer-events: none;
1070
- transition: opacity 0.3s;
1071
- opacity: 0;
1072
- }
1073
- </style>
1074
- </head>
1075
- <body>
1076
- <div class="container">
1077
- <div class="header">
1078
- <h1>🎬 HDHive 影视资源</h1>
1079
- <div class="sub">选择分类 → 浏览影片 → 获取资源链接</div>
1080
- </div>
1081
- <div class="stats-row">
1082
- <div class="stat-item"><span class="stat-number" id="stat-movies">-</span><span class="stat-label"> 缓存影片</span></div>
1083
- <div class="stat-item"><span class="stat-number" id="stat-unlocks">-</span><span class="stat-label"> 缓存资源</span></div>
1084
- <div class="stat-item"><span class="stat-number" id="stat-auto-unlocked">-</span><span class="stat-label"> 自动解锁资源</span></div>
1085
- <div class="stat-item"><span class="stat-number" id="next-unlock-countdown">--:--</span><span class="stat-label"> 下次浏览</span></div>
1086
- </div>
1087
- <div class="filter-notice">📌 仅显示 <strong>115网盘、百度网盘、磁力链接</strong> 资源 | 图片通过服务器代理</div>
1088
-
1089
- <!-- 快捷解锁面板 -->
1090
- <div class="quick-unlock">
1091
- <div class="quick-unlock-header" onclick="toggleQuickUnlock()">
1092
- <span>🔓 快捷解锁(直接输入 HDHive 资源链接或 slug)</span>
1093
- <span class="toggle-icon" id="quickUnlockIcon">▼</span>
1094
- </div>
1095
- <div class="quick-unlock-content" id="quickUnlockContent">
1096
- <div class="url-input-group">
1097
- <input type="text" id="quickSlugInput" placeholder="粘贴链接或输入 slug,例如: https://hdhive.com/resource/115/6d09726061d24c69b98b58941e2c8a1e 或 6d09726061d24c69b98b58941e2c8a1e">
1098
- <button onclick="quickUnlock()">🚀 解锁</button>
1099
- </div>
1100
- <div id="quickUnlockResult" class="unlock-result" style="display: none;"></div>
1101
- </div>
1102
- </div>
1103
-
1104
- <div class="categories-section">
1105
- <div class="type-switch">
1106
- <button class="type-btn active" id="btn-movie" onclick="switchType('movie')">🎬 电影</button>
1107
- <button class="type-btn" id="btn-tv" onclick="switchType('tv')">📺 电视剧</button>
1108
- </div>
1109
- <!-- 快捷榜单栏 -->
1110
- <div id="movie-presets" class="preset-bar" style="display: flex;">
1111
- <button class="preset-btn active" data-preset="popular">🔥 热门电影</button>
1112
- <button class="preset-btn" data-preset="now_playing">🎬 正在热映</button>
1113
- <button class="preset-btn" data-preset="top_rated">⭐ 评分最高</button>
1114
- <button class="preset-btn" data-preset="upcoming">📅 即将上映</button>
1115
- </div>
1116
- <div id="tv-presets" class="preset-bar" style="display: none;">
1117
- <button class="preset-btn active" data-preset="popular">🔥 热门剧集</button>
1118
- <button class="preset-btn" data-preset="top_rated">⭐ 评分最高</button>
1119
- <button class="preset-btn" data-preset="on_the_air">📺 正在播出</button>
1120
- <button class="preset-btn" data-preset="airing_today">📅 今日播出</button>
1121
- </div>
1122
- <!-- 折叠面板 -->
1123
- <div class="filter-collapse">
1124
- <div class="filter-header" onclick="toggleFilterPanel()">
1125
- <span>🔽 筛选条件 (地区/年份/类型/排序)</span>
1126
- <span class="toggle-icon" id="filterToggleIcon">▼</span>
1127
- </div>
1128
- <div class="filter-content" id="filterContent">
1129
- <div id="movie-cats" class="movie-cats">
1130
- <div class="filter-row"><div class="filter-label">🌍 地区</div><div class="filter-buttons" id="movie-region"></div></div>
1131
- <div class="filter-row"><div class="filter-label">📅 年份</div><div class="filter-buttons" id="movie-year"></div></div>
1132
- <div class="filter-row"><div class="filter-label">🎭 类型 (TMDB)</div><div class="filter-buttons" id="movie-genre"></div></div>
1133
- <div class="filter-row"><div class="filter-label">📊 排序</div><div class="filter-buttons" id="movie-sort"></div></div>
1134
- </div>
1135
- <div id="tv-cats" class="tv-cats" style="display: none;">
1136
- <div class="filter-row"><div class="filter-label">🌍 地区</div><div class="filter-buttons" id="tv-region"></div></div>
1137
- <div class="filter-row"><div class="filter-label">📅 年份</div><div class="filter-buttons" id="tv-year"></div></div>
1138
- <div class="filter-row"><div class="filter-label">🎭 类型 (TMDB)</div><div class="filter-buttons" id="tv-genre"></div></div>
1139
- <div class="filter-row"><div class="filter-label">📊 排序</div><div class="filter-buttons" id="tv-sort"></div></div>
1140
- </div>
1141
- </div>
1142
- </div>
1143
- </div>
1144
- <div class="search-section">
1145
- <div class="search-bar">
1146
- <input type="text" id="search-input" placeholder="输入电影/剧集名称..." onkeypress="if(event.key==='Enter') searchByKeyword()">
1147
- <button onclick="searchByKeyword()">🔍 搜索</button>
1148
- </div>
1149
- </div>
1150
- <div class="two-columns">
1151
- <div class="left-panel">
1152
- <div class="panel-header"><div class="panel-title" id="panel-title">🔥 热门电影</div><div class="panel-count" id="panel-count">共 0 部</div></div>
1153
- <div class="movies-grid" id="movies-grid"><div class="loading">加载中...</div></div>
1154
- <div class="pagination" id="pagination"></div>
1155
- </div>
1156
- <div class="right-panel">
1157
- <div class="selected-media" id="selected-media"><div class="empty">点击上方影视获取资源</div></div>
1158
- <div class="resource-header"><div class="panel-title">📥 资源列表</div><label class="checkbox-label"><input type="checkbox" id="show-paid" checked><span>显示付费资源</span></label></div>
1159
- <div id="resource-list" class="resource-list"><div class="empty">请先选择影视</div></div>
1160
- </div>
1161
- </div>
1162
- <div class="bottom-bar">
1163
- <button class="backup-btn" onclick="manualBackup()">💾 手动备份</button>
1164
- <button class="debug-btn" onclick="showDebugModal()">🐛 调试日志</button>
1165
- <span id="backupMsg" class="status-msg"></span>
1166
- </div>
1167
- </div>
1168
- <div id="passwordModal" class="password-modal">
1169
- <div class="password-modal-content">
1170
- <h3>🔐 请输入解锁密码</h3>
1171
- <input type="password" id="unlockPassword" placeholder="输入密码" autocomplete="off">
1172
- <div>
1173
- <button onclick="submitPassword()">确认</button>
1174
- <button onclick="closePasswordModal()">取消</button>
1175
- </div>
1176
- </div>
1177
- </div>
1178
- <div id="debugModal" class="modal">
1179
- <div class="modal-content">
1180
- <div class="modal-header">
1181
- <span class="modal-title">📋 系统调试日志</span>
1182
- <div>
1183
- <button class="clear-logs" onclick="clearAllLogs()">清除日志</button>
1184
- <button class="close-modal" onclick="closeDebugModal()">关闭</button>
1185
- </div>
1186
- </div>
1187
- <div class="modal-body" id="debugLogsList"></div>
1188
- </div>
1189
- </div>
1190
- <div id="toastMsg" class="toast-msg"></div>
1191
-
1192
- <script>
1193
- // 折叠面板状态
1194
- let filterPanelCollapsed = true;
1195
- function toggleFilterPanel() {
1196
- const content = document.getElementById('filterContent');
1197
- const icon = document.getElementById('filterToggleIcon');
1198
- if (filterPanelCollapsed) {
1199
- content.classList.remove('collapsed');
1200
- content.style.display = 'block';
1201
- icon.innerHTML = '▲';
1202
- filterPanelCollapsed = false;
1203
- } else {
1204
- content.classList.add('collapsed');
1205
- content.style.display = 'none';
1206
- icon.innerHTML = '▼';
1207
- filterPanelCollapsed = true;
1208
- }
1209
- }
1210
- // 快捷解锁折叠
1211
- let quickUnlockCollapsed = false;
1212
- function toggleQuickUnlock() {
1213
- const content = document.getElementById('quickUnlockContent');
1214
- const icon = document.getElementById('quickUnlockIcon');
1215
- if (quickUnlockCollapsed) {
1216
- content.classList.remove('collapsed');
1217
- content.style.display = 'block';
1218
- icon.innerHTML = '▼';
1219
- quickUnlockCollapsed = false;
1220
- } else {
1221
- content.classList.add('collapsed');
1222
- content.style.display = 'none';
1223
- icon.innerHTML = '▶';
1224
- quickUnlockCollapsed = true;
1225
- }
1226
- }
1227
- // 初始化折叠状态
1228
- document.addEventListener('DOMContentLoaded', function() {
1229
- const filterContent = document.getElementById('filterContent');
1230
- filterContent.classList.add('collapsed');
1231
- filterContent.style.display = 'none';
1232
- const quickContent = document.getElementById('quickUnlockContent');
1233
- quickContent.classList.remove('collapsed');
1234
- quickContent.style.display = 'block';
1235
- quickUnlockCollapsed = false;
1236
- });
1237
-
1238
- // 提示函数:改为 alert
1239
- function showToast(msg, isError = false) {
1240
- alert(msg);
1241
- }
1242
-
1243
- // 获取网盘徽章 HTML
1244
- function getPanBadgeHtml(panType) {
1245
- const map = {
1246
- baidu: ['百度网盘', '🐻', 'pan-badge-baidu'],
1247
- 115: ['115网盘', '☁️', 'pan-badge-115'],
1248
- magnet: ['磁力链接', '🧲', 'pan-badge-magnet']
1249
- };
1250
- let [name, icon, cls] = map[panType] || ['其他网盘', '📎', 'pan-badge-unknown'];
1251
- return `<span class="pan-badge ${cls}">${icon} ${name}</span>`;
1252
- }
1253
-
1254
- // 增强版链接网盘类型检测(支持 115cdn.com, anxia.com 等)
1255
- function detectPanTypeFromLink(link) {
1256
- if (!link) return 'unknown';
1257
- let l = link.toLowerCase();
1258
- if (l.includes('pan.baidu.com') || l.includes('baidu.com/s/')) return 'baidu';
1259
- if (l.includes('115.com') || l.includes('115cdn.com') || l.includes('anxia.com') || l.includes('v.anxia.com')) return '115';
1260
- if (l.includes('magnet:') || l.includes('ed2k://')) return 'magnet';
1261
- return 'unknown';
1262
- }
1263
-
1264
- // 日志相关
1265
- let logRefreshInterval = null;
1266
- async function fetchLogs() {
1267
- try {
1268
- let resp = await fetch('/api/logs');
1269
- let data = await resp.json();
1270
- let container = document.getElementById('debugLogsList');
1271
- if (!container) return;
1272
- let logs = data.logs || [];
1273
- if (logs.length === 0) {
1274
- container.innerHTML = '<div style="text-align:center;color:#aaa;">暂无日志</div>';
1275
- return;
1276
- }
1277
- let wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 10;
1278
- container.innerHTML = logs.map(line => {
1279
- let cls = 'log-info';
1280
- let lowerLine = line.toLowerCase();
1281
- if (lowerLine.includes('error') || lowerLine.includes('exception') || lowerLine.includes('失败')) cls = 'log-error';
1282
- else if (lowerLine.includes('warning') || lowerLine.includes('警告')) cls = 'log-warning';
1283
- else if (lowerLine.includes('✅') || lowerLine.includes('成功')) cls = 'log-success';
1284
- return `<div class="log-entry ${cls}">${escapeHtml(line)}</div>`;
1285
- }).join('');
1286
- if (wasAtBottom) setTimeout(() => container.scrollTop = container.scrollHeight, 10);
1287
- } catch(e) { console.error(e); }
1288
- }
1289
- function showDebugModal() {
1290
- let modal = document.getElementById('debugModal');
1291
- modal.style.display = 'block';
1292
- fetchLogs();
1293
- if (logRefreshInterval) clearInterval(logRefreshInterval);
1294
- logRefreshInterval = setInterval(fetchLogs, 2000);
1295
- setTimeout(() => { let c = document.getElementById('debugLogsList'); if(c) c.scrollTop = c.scrollHeight; }, 100);
1296
- }
1297
- function closeDebugModal() {
1298
- document.getElementById('debugModal').style.display = 'none';
1299
- if (logRefreshInterval) clearInterval(logRefreshInterval);
1300
- }
1301
- async function clearAllLogs() {
1302
- await fetch('/api/logs/clear', { method: 'POST' });
1303
- fetchLogs();
1304
- }
1305
- // 通用函数
1306
- let pendingAction = null;
1307
- function showPasswordModal(action) { pendingAction=action; document.getElementById('passwordModal').style.display='block'; document.getElementById('unlockPassword').focus(); }
1308
- function closePasswordModal() { document.getElementById('passwordModal').style.display='none'; pendingAction=null; }
1309
- async function submitPassword() {
1310
- let pwd=document.getElementById('unlockPassword').value;
1311
- if(!pwd){ alert('请输入密码'); return; }
1312
- try {
1313
- let r=await fetch('/api/verify-password',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pwd})});
1314
- let d=await r.json();
1315
- if(d.success) {
1316
- document.getElementById('passwordModal').style.display='none';
1317
- if(pendingAction) {
1318
- if(pendingAction.type === 'unlock') await doUnlockResource(pendingAction.slug, pendingAction.points, pwd);
1319
- else if(pendingAction.type === 'refresh') await doRefreshCache(pendingAction.mediaType, pendingAction.tmdbId, pendingAction.btn);
1320
- else if(pendingAction.type === 'quickUnlock') await doQuickUnlock(pendingAction.slug, pwd);
1321
- }
1322
- pendingAction = null;
1323
- } else { alert(d.message); }
1324
- } catch(e){ alert(`验证失败: ${e.message}`); }
1325
- }
1326
- function getPosterUrl(path) { if(!path) return ''; let tmdbUrl=`https://image.tmdb.org/t/p/w342${path}`; return `/api/image/proxy?url=${encodeURIComponent(tmdbUrl)}`; }
1327
- function attachImageListeners(img,path) { img.addEventListener('error',()=>{ img.style.display='none'; if(img.parentElement) img.parentElement.style.background='#2a2a2a'; }); }
1328
- let currentType='movie', currentPage=1, totalPages=1;
1329
- let currentMovies=[], selectedMovie=null, showPaid=true, currentQuery='';
1330
- let movieFilters={ region:null, year:null, genres:[], sort_by:'latest' };
1331
- let tvFilters={ region:null, year:null, genres:[], sort_by:'latest' };
1332
- let currentPreset = 'popular';
1333
- let movieGenres = [];
1334
- let tvGenres = [];
1335
- const regionOptions = ['大陆','香港','台湾','日本','韩国','欧美'];
1336
- const currentYear = new Date().getFullYear();
1337
- const yearList = (() => {
1338
- let years = [];
1339
- for (let y = currentYear; y >= 2010; y--) years.push(y.toString());
1340
- years.push('2010年前');
1341
- return years;
1342
- })();
1343
- const sortOptions = [{value:'latest', label:'最新'},{value:'imdb', label:'IMDb 评分'},{value:'douban', label:'豆瓣评分'}];
1344
- const ALLOWED_PAN_TYPES=['115','baidu','magnet'];
1345
- function extractLinkFromResource(r){return r.link||r.download_url||r.url||r.pan_url||'';}
1346
- // 加载 TMDB 类型
1347
- async function loadMovieGenres() {
1348
- try { let resp = await fetch('/api/tmdb/genres/movie'); let data = await resp.json(); movieGenres = data; } catch(e) { console.error(e); movieGenres = []; }
1349
- }
1350
- async function loadTVGenres() {
1351
- try { let resp = await fetch('/api/tmdb/genres/tv'); let data = await resp.json(); tvGenres = data; } catch(e) { console.error(e); tvGenres = []; }
1352
- }
1353
- // 渲染静态筛选按钮
1354
- function renderMovieRegionButtons() {
1355
- const container = document.getElementById('movie-region');
1356
- if (!container) return;
1357
- container.innerHTML = regionOptions.map(r => `<button class="filter-btn-item ${movieFilters.region===r?'active':''}" onclick="selectMovieRegion('${r}')">${r}</button>`).join('');
1358
- }
1359
- function renderTVRegionButtons() {
1360
- const container = document.getElementById('tv-region');
1361
- if (!container) return;
1362
- container.innerHTML = regionOptions.map(r => `<button class="filter-btn-item ${tvFilters.region===r?'active':''}" onclick="selectTVRegion('${r}')">${r}</button>`).join('');
1363
- }
1364
- function renderMovieYearButtons() {
1365
- const container = document.getElementById('movie-year');
1366
- if (!container) return;
1367
- container.innerHTML = yearList.map(y => `<button class="filter-btn-item ${movieFilters.year===y?'active':''}" onclick="selectMovieYear('${y}')">${y}</button>`).join('');
1368
- }
1369
- function renderTVYearButtons() {
1370
- const container = document.getElementById('tv-year');
1371
- if (!container) return;
1372
- container.innerHTML = yearList.map(y => `<button class="filter-btn-item ${tvFilters.year===y?'active':''}" onclick="selectTVYear('${y}')">${y}</button>`).join('');
1373
- }
1374
- function renderMovieGenreButtons() {
1375
- const container = document.getElementById('movie-genre');
1376
- if (!container) return;
1377
- container.innerHTML = movieGenres.map(g => `<button class="filter-btn-item ${movieFilters.genres.includes(g.id)?'active':''}" onclick="toggleMovieGenre(${g.id})">${g.name}</button>`).join('');
1378
- }
1379
- function renderTVGenreButtons() {
1380
- const container = document.getElementById('tv-genre');
1381
- if (!container) return;
1382
- container.innerHTML = tvGenres.map(g => `<button class="filter-btn-item ${tvFilters.genres.includes(g.id)?'active':''}" onclick="toggleTVGenre(${g.id})">${g.name}</button>`).join('');
1383
- }
1384
- function renderMovieSortButtons() {
1385
- const container = document.getElementById('movie-sort');
1386
- if (!container) return;
1387
- container.innerHTML = sortOptions.map(s => `<button class="filter-btn-item ${movieFilters.sort_by===s.value?'active':''}" onclick="selectMovieSort('${s.value}')">${s.label}</button>`).join('');
1388
- }
1389
- function renderTVSortButtons() {
1390
- const container = document.getElementById('tv-sort');
1391
- if (!container) return;
1392
- container.innerHTML = sortOptions.map(s => `<button class="filter-btn-item ${tvFilters.sort_by===s.value?'active':''}" onclick="selectTVSort('${s.value}')">${s.label}</button>`).join('');
1393
- }
1394
- function updatePresetButtons() {
1395
- const moviePresetDiv = document.getElementById('movie-presets');
1396
- const tvPresetDiv = document.getElementById('tv-presets');
1397
- if (currentType === 'movie') {
1398
- const btns = moviePresetDiv.querySelectorAll('.preset-btn');
1399
- btns.forEach(btn => {
1400
- if (btn.dataset.preset === currentPreset) btn.classList.add('active');
1401
- else btn.classList.remove('active');
1402
- });
1403
- } else {
1404
- const btns = tvPresetDiv.querySelectorAll('.preset-btn');
1405
- btns.forEach(btn => {
1406
- if (btn.dataset.preset === currentPreset) btn.classList.add('active');
1407
- else btn.classList.remove('active');
1408
- });
1409
- }
1410
- }
1411
- function resetFilters() {
1412
- if (currentType === 'movie') {
1413
- movieFilters = { region:null, year:null, genres:[], sort_by:'latest' };
1414
- renderMovieRegionButtons();
1415
- renderMovieYearButtons();
1416
- renderMovieGenreButtons();
1417
- renderMovieSortButtons();
1418
- } else {
1419
- tvFilters = { region:null, year:null, genres:[], sort_by:'latest' };
1420
- renderTVRegionButtons();
1421
- renderTVYearButtons();
1422
- renderTVGenreButtons();
1423
- renderTVSortButtons();
1424
- }
1425
- }
1426
- function selectPreset(preset) {
1427
- if (currentPreset === preset) {
1428
- currentPreset = null;
1429
- } else {
1430
- currentPreset = preset;
1431
- }
1432
- updatePresetButtons();
1433
- currentPage = 1;
1434
- selectedMovie = null;
1435
- loadMovies();
1436
- }
1437
- function selectMovieRegion(code) {
1438
- movieFilters.region = (movieFilters.region===code ? null : code);
1439
- renderMovieRegionButtons();
1440
- loadMovies();
1441
- }
1442
- function selectMovieYear(year) {
1443
- movieFilters.year = (movieFilters.year===year ? null : year);
1444
- renderMovieYearButtons();
1445
- loadMovies();
1446
- }
1447
- function toggleMovieGenre(id) {
1448
- if(movieFilters.genres.includes(id)) movieFilters.genres = movieFilters.genres.filter(g=>g!==id);
1449
- else movieFilters.genres.push(id);
1450
- renderMovieGenreButtons();
1451
- loadMovies();
1452
- }
1453
- function selectMovieSort(sort) {
1454
- movieFilters.sort_by = sort;
1455
- renderMovieSortButtons();
1456
- loadMovies();
1457
- }
1458
- function selectTVRegion(code) {
1459
- tvFilters.region = (tvFilters.region===code ? null : code);
1460
- renderTVRegionButtons();
1461
- loadMovies();
1462
- }
1463
- function selectTVYear(year) {
1464
- tvFilters.year = (tvFilters.year===year ? null : year);
1465
- renderTVYearButtons();
1466
- loadMovies();
1467
- }
1468
- function toggleTVGenre(id) {
1469
- if(tvFilters.genres.includes(id)) tvFilters.genres = tvFilters.genres.filter(g=>g!==id);
1470
- else tvFilters.genres.push(id);
1471
- renderTVGenreButtons();
1472
- loadMovies();
1473
- }
1474
- function selectTVSort(sort) {
1475
- tvFilters.sort_by = sort;
1476
- renderTVSortButtons();
1477
- loadMovies();
1478
- }
1479
- async function switchType(type){
1480
- currentType=type; currentPage=1; currentQuery=''; selectedMovie=null;
1481
- currentPreset = 'popular';
1482
- resetFilters();
1483
- document.getElementById('search-input').value='';
1484
- document.getElementById('btn-movie').classList.toggle('active',type==='movie');
1485
- document.getElementById('btn-tv').classList.toggle('active',type==='tv');
1486
- document.getElementById('movie-presets').style.display = type==='movie' ? 'flex' : 'none';
1487
- document.getElementById('tv-presets').style.display = type==='tv' ? 'flex' : 'none';
1488
- if(type==='movie') {
1489
- document.getElementById('movie-cats').style.display='flex';
1490
- document.getElementById('tv-cats').style.display='none';
1491
- renderMovieRegionButtons(); renderMovieYearButtons(); renderMovieGenreButtons(); renderMovieSortButtons();
1492
- } else {
1493
- document.getElementById('movie-cats').style.display='none';
1494
- document.getElementById('tv-cats').style.display='block';
1495
- renderTVRegionButtons(); renderTVYearButtons(); renderTVGenreButtons(); renderTVSortButtons();
1496
- }
1497
- updatePresetButtons();
1498
- loadMovies();
1499
- document.getElementById('selected-media').innerHTML='<div class="empty">点击上方影视获取资源</div>';
1500
- document.getElementById('resource-list').innerHTML='<div class="empty">请先选择影视</div>';
1501
- document.querySelectorAll('.movie-card').forEach(c=>c.classList.remove('selected'));
1502
- }
1503
- async function loadMovies(){
1504
- document.getElementById('movies-grid').innerHTML='<div class="loading">加载中...</div>';
1505
- try {
1506
- let params, resp;
1507
- if(currentQuery) {
1508
- params = {query: currentQuery, page: currentPage};
1509
- resp = await fetch('/api/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(params)});
1510
- let data = await resp.json();
1511
- currentMovies = data.results || [];
1512
- totalPages = data.total_pages || 1;
1513
- document.getElementById('panel-title').innerHTML = `🔍 搜索结果: ${currentQuery}`;
1514
- } else {
1515
- let filters = (currentType === 'movie') ? { ...movieFilters } : { ...tvFilters };
1516
- params = {type: currentType, page: currentPage, filters: filters};
1517
- if (currentPreset) {
1518
- params.preset = currentPreset;
1519
- }
1520
- resp = await fetch('/api/discover',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(params)});
1521
- let data = await resp.json();
1522
- currentMovies = data.results || [];
1523
- totalPages = data.total_pages || 1;
1524
- let title = '';
1525
- if (currentPreset) {
1526
- const presetNames = {
1527
- popular: '热门', now_playing: '正在热映', top_rated: '评分最高', upcoming: '即将上映',
1528
- on_the_air: '正在播出', airing_today: '今日播出'
1529
- };
1530
- title = presetNames[currentPreset] || (currentType==='movie'?'电影':'电视剧');
1531
- } else {
1532
- if (currentType === 'movie') {
1533
- if(movieFilters.region) title += movieFilters.region + ' ';
1534
- if(movieFilters.year) title += (movieFilters.year==='2010年前'?'2010年前':movieFilters.year+'年') + ' ';
1535
- if(movieFilters.genres.length) title += movieGenres.find(g=>g.id===movieFilters.genres[0])?.name || '';
1536
- if(!title) title = '电影';
1537
- } else {
1538
- if(tvFilters.region) title += tvFilters.region + ' ';
1539
- if(tvFilters.year) title += (tvFilters.year==='2010年前'?'2010年前':tvFilters.year+'年') + ' ';
1540
- if(tvFilters.genres.length) title += tvGenres.find(g=>g.id===tvFilters.genres[0])?.name || '';
1541
- if(!title) title = '电视剧';
1542
- }
1543
- }
1544
- document.getElementById('panel-title').innerHTML = title;
1545
- }
1546
- renderMovies();
1547
- renderPagination();
1548
- document.getElementById('panel-count').innerHTML = `共 ${currentMovies.length} 部`;
1549
- } catch(e){
1550
- document.getElementById('movies-grid').innerHTML='<div class="empty">❌ 加载失败</div>';
1551
- console.error(e);
1552
- }
1553
- }
1554
- async function searchByKeyword(){
1555
- let kw=document.getElementById('search-input').value.trim();
1556
- if(!kw) return;
1557
- currentQuery=kw; currentPage=1; selectedMovie=null;
1558
- currentPreset = null;
1559
- resetFilters();
1560
- updatePresetButtons();
1561
- await loadMovies();
1562
- document.getElementById('selected-media').innerHTML='<div class="empty">点击上方影视获取资源</div>';
1563
- document.getElementById('resource-list').innerHTML='<div class="empty">请先选择影视</div>';
1564
- document.querySelectorAll('.movie-card').forEach(c=>c.classList.remove('selected'));
1565
- }
1566
- function renderMovies(){
1567
- if(!currentMovies.length){ document.getElementById('movies-grid').innerHTML='<div class="empty">暂无数据</div>'; return; }
1568
- document.getElementById('movies-grid').innerHTML=currentMovies.map(m=>{
1569
- let posterUrl=m.poster_path?getPosterUrl(m.poster_path):'';
1570
- let title=m.title||m.name||'未知';
1571
- let year=(m.release_date||m.first_air_date||'').slice(0,4);
1572
- let isSelected=selectedMovie&&selectedMovie.id===m.id;
1573
- let mediaType=m.media_type||(m.title?'movie':'tv');
1574
- let cached=m.cached===true;
1575
- let refreshBtnHtml=cached?`<button class="refresh-cache-btn" onclick="event.stopPropagation(); refreshCache('${mediaType}',${m.id},this)">🔄</button>`:'';
1576
- return `<div class="movie-card ${isSelected?'selected':''}" data-id="${m.id}" data-type="${mediaType}">
1577
- ${refreshBtnHtml}
1578
- ${posterUrl?`<img class="movie-poster" src="${posterUrl}" data-poster-path="${m.poster_path}" loading="lazy">`:'<div class="movie-poster" style="background:#2a2a2a;"></div>'}
1579
- <div class="movie-info"><div class="movie-title">${escapeHtml(title)}</div><div class="movie-year">${year}</div><div class="movie-year" style="font-size:10px;color:#e50914;">${mediaType==='movie'?'电影':'电视剧'}</div></div>
1580
- </div>`;
1581
- }).join('');
1582
- document.querySelectorAll('.movie-poster').forEach(img=>{let path=img.getAttribute('data-poster-path');if(path)attachImageListeners(img,path);});
1583
- document.querySelectorAll('.movie-card').forEach(card=>card.addEventListener('click',(e)=>{if(e.target.classList.contains('refresh-cache-btn'))return;let id=parseInt(card.dataset.id);let type=card.dataset.type;selectMovie(id,type);}));
1584
- }
1585
- async function refreshCache(mediaType,tmdbId,btn){
1586
- showPasswordModal({type:'refresh', mediaType, tmdbId, btn});
1587
- }
1588
- async function doRefreshCache(mediaType,tmdbId,btn){
1589
- if(!confirm('确定刷新缓存?')) return;
1590
- let msgDiv=document.getElementById('backupMsg');
1591
- msgDiv.innerText='🔄 刷新中...';
1592
- try{
1593
- let resp=await fetch(`/api/cache/refresh/${mediaType}/${tmdbId}`,{method:'POST'});
1594
- let data=await resp.json();
1595
- if(data.success){
1596
- msgDiv.innerText='✅ 刷新成功!';
1597
- setTimeout(()=>{if(msgDiv.innerText==='✅ 刷新成功!')msgDiv.innerText='';},3000);
1598
- if(selectedMovie&&selectedMovie.id===tmdbId) await selectMovie(tmdbId,mediaType);
1599
- await loadMovies();
1600
- await updateAllStats();
1601
- } else { msgDiv.innerText=`❌ 失败: ${data.error||'未知'}`; }
1602
- }catch(e){ msgDiv.innerText=`❌ 错误: ${e.message}`; }
1603
- }
1604
- async function selectMovie(id,forcedType){
1605
- let movie=currentMovies.find(m=>m.id===id);
1606
- if(!movie) return;
1607
- selectedMovie=movie;
1608
- let mediaType=forcedType||movie.media_type||(movie.title?'movie':'tv');
1609
- selectedMovie.media_type = mediaType;
1610
- document.querySelectorAll('.movie-card').forEach(c=>c.classList.toggle('selected',parseInt(c.dataset.id)===id));
1611
- let poster=movie.poster_path?getPosterUrl(movie.poster_path):'';
1612
- let title=movie.title||movie.name||'未知';
1613
- let year=(movie.release_date||movie.first_air_date||'').slice(0,4);
1614
- let overview=movie.overview||'暂无简介';
1615
- document.getElementById('selected-media').innerHTML=`
1616
- <div class="media-info">
1617
- ${poster?`<img class="media-poster" src="${poster}" onerror="this.style.display='none'">`:'<div class="media-poster" style="background:#2a2a2a;"></div>'}
1618
- <div class="media-details">
1619
- <div class="media-title">${escapeHtml(title)}</div>
1620
- <div class="media-meta"><span class="media-badge">${mediaType==='movie'?'电影':'电视剧'}</span><span class="media-badge">${year||'未知'}</span><span class="media-badge">ID: ${movie.id}</span></div>
1621
- <div class="media-overview">${escapeHtml(overview)}</div>
1622
- </div>
1623
- </div>`;
1624
- document.getElementById('resource-list').innerHTML='<div class="loading">获取资源中...</div>';
1625
- try{
1626
- let resp=await fetch(`/api/cache/resources/${mediaType}/${movie.id}`);
1627
- let data=await resp.json();
1628
- if(!data.success){document.getElementById('resource-list').innerHTML='<div class="empty">暂无资源</div>';return;}
1629
- let resources=data.resources||[];
1630
- if(resources.length===0){document.getElementById('resource-list').innerHTML='<div class="empty">暂无资源</div>';return;}
1631
- let freeItems=[],paidItems=[];
1632
- for(let r of resources){
1633
- let points=r.unlock_points||0;
1634
- let link=extractLinkFromResource(r);
1635
- let panType=detectPanTypeFromLink(link);
1636
- if(panType==='unknown') panType=(r.pan_type||'unknown').toLowerCase();
1637
- if(!ALLOWED_PAN_TYPES.includes(panType)) continue;
1638
- if(points===0||r.is_free_for_user===true) freeItems.push({...r,panType,link});
1639
- else paidItems.push({...r,panType,link});
1640
- }
1641
- let allItems=[...freeItems,...(showPaid?paidItems:[])];
1642
- if(allItems.length===0){document.getElementById('resource-list').innerHTML='<div class="empty">暂无115、百度、磁力资源</div>';return;}
1643
- let html=`<div style="margin-bottom:10px;"><strong>🎁 资源列表</strong> ${data.from_cache?'<span class="cache-badge">📦 缓存</span>':''}</div>`;
1644
- for(let r of allItems){
1645
- let points=r.unlock_points||0;
1646
- let isFree=points===0||r.is_free_for_user===true;
1647
- let link=r.link;
1648
- if(!link&&r.slug) link=localStorage.getItem(`unlock_${r.slug}`);
1649
- let panBadge=getPanBadgeHtml(r.panType);
1650
- if(link){
1651
- html+=`<div class="resource-card free"><div class="resource-title">${escapeHtml(r.title||r.name||'未命名')} ${panBadge}</div><div class="resource-meta">${r.share_size?`📦 ${r.share_size}`:''} ${r.video_resolution?`🎬 ${r.video_resolution}`:''}${!isFree?`<span>💰 ${points} 积分</span>`:''}</div><div class="resource-link">📥 <a href="${link}" target="_blank">${link}</a><button class="copy-btn" onclick="copyToClipboard('${link.replace(/'/g,"\\'")}',this)">📋 复制</button></div></div>`;
1652
- }else{
1653
- html+=`<div class="resource-card ${isFree?'free':'paid'}"><div class="resource-title">${escapeHtml(r.title||r.name||'未命名')} ${panBadge}</div><div class="resource-meta">${r.share_size?`📦 ${r.share_size}`:''} ${r.video_resolution?`🎬 ${r.video_resolution}`:''}${!isFree?`<span>💰 ${points} 积分</span>`:''}</div><button class="unlock-btn" onclick="handleUnlockClick('${r.slug}',${points})">🔓 获取链接${!isFree?'(消耗积分)':''}</button></div>`;
1654
- }
1655
- }
1656
- document.getElementById('resource-list').innerHTML=html;
1657
- let rightPanel=document.querySelector('.right-panel');
1658
- if(rightPanel) rightPanel.scrollIntoView({behavior:'smooth',block:'start'});
1659
- }catch(e){document.getElementById('resource-list').innerHTML=`<div class="empty">❌ 获取失败: ${e.message}</div>`; console.error(e);}
1660
- }
1661
-
1662
- // ========== 修改后的解锁逻辑 ==========
1663
- // 左侧资源列表的解锁处理
1664
- function handleUnlockClick(slug, points) {
1665
- // 直接尝试解锁(先查缓存),不再预先判断 points
1666
- doUnlockResource(slug, points, null);
1667
- }
1668
-
1669
- async function doUnlockResource(slug, points, password = null) {
1670
- try {
1671
- let body = {slug: slug, allow_points: true};
1672
- if (password) {
1673
- body.password = password;
1674
- }
1675
- let resp = await fetch('/api/cache/unlock', {
1676
- method: 'POST',
1677
- headers: {'Content-Type': 'application/json'},
1678
- body: JSON.stringify(body)
1679
- });
1680
- let data = await resp.json();
1681
- if (data.need_password) {
1682
- showPasswordModal({type:'unlock', slug, points});
1683
- return;
1684
- }
1685
- if (data.from_cache === true) alert('📦 来自缓存,不消耗积分');
1686
- if (data.code === 'OPENAPI_COOLDOWN') {
1687
- let wait = data.retry_after_seconds || 60;
1688
- alert(`请等待 ${wait} 秒`);
1689
- setTimeout(() => { if(selectedMovie) selectMovie(selectedMovie.id, selectedMovie.media_type); }, wait * 1000);
1690
- return;
1691
- }
1692
- let link = data.link || data.data?.full_url || data.data?.url || data.url;
1693
- if (link) {
1694
- localStorage.setItem(`unlock_${slug}`, link);
1695
- alert('✅ 解锁成功!');
1696
- } else {
1697
- alert('❌ 解锁失败');
1698
- }
1699
- if (selectedMovie) selectMovie(selectedMovie.id, selectedMovie.media_type);
1700
- } catch(e) {
1701
- alert(`错误: ${e.message}`);
1702
- if (selectedMovie) selectMovie(selectedMovie.id, selectedMovie.media_type);
1703
- }
1704
- }
1705
-
1706
- // 快捷解锁
1707
- function extractSlugFromUrl(input) {
1708
- let trimmed = input.trim();
1709
- if (/^[a-f0-9]{32}$/i.test(trimmed)) return trimmed;
1710
- let match = trimmed.match(/\/resource\/(?:[^\/]+\/)?([a-f0-9]{32})/i);
1711
- if (match) return match[1];
1712
- match = trimmed.match(/([a-f0-9]{32})/i);
1713
- return match ? match[1] : null;
1714
- }
1715
-
1716
- async function quickUnlock() {
1717
- let raw = document.getElementById('quickSlugInput').value;
1718
- if (!raw.trim()) {
1719
- alert('请输入资源链接或 slug');
1720
- return;
1721
- }
1722
- let slug = extractSlugFromUrl(raw);
1723
- if (!slug) {
1724
- alert('未能提取到有效的 slug (32位十六进制)');
1725
- return;
1726
- }
1727
- let resultDiv = document.getElementById('quickUnlockResult');
1728
- resultDiv.style.display = 'block';
1729
- resultDiv.innerHTML = '<div style="color:#aaa;">🔓 解锁中...</div>';
1730
- // 直接调用解锁(不带密码),后端会先查缓存,再根据是否需要密码返回 need_password
1731
- doQuickUnlock(slug, null);
1732
- }
1733
-
1734
- async function doQuickUnlock(slug, password) {
1735
- let resultDiv = document.getElementById('quickUnlockResult');
1736
- resultDiv.innerHTML = '<div style="color:#aaa;">🔓 解锁中...</div>';
1737
- try {
1738
- let body = {slug: slug, allow_points: true};
1739
- if (password) {
1740
- body.password = password;
1741
- }
1742
- let resp = await fetch('/api/cache/unlock', {
1743
- method: 'POST',
1744
- headers: {'Content-Type': 'application/json'},
1745
- body: JSON.stringify(body)
1746
- });
1747
- let data = await resp.json();
1748
- if (data.need_password) {
1749
- showPasswordModal({type:'quickUnlock', slug: slug});
1750
- return;
1751
- }
1752
- if (data.code === 'OPENAPI_COOLDOWN') {
1753
- let wait = data.retry_after_seconds || 60;
1754
- resultDiv.innerHTML = `<div style="color:#f39c12;">⏰ API 冷却中,请等待 ${wait} 秒后重试。</div>`;
1755
- alert(`冷却中,请等待 ${wait} 秒`);
1756
- return;
1757
- }
1758
- let link = data.link || data.data?.full_url || data.data?.url || data.url;
1759
- if (link) {
1760
- localStorage.setItem(`unlock_${slug}`, link);
1761
- let cacheInfo = '';
1762
- if (data.from_cache === true) {
1763
- alert('📦 来自缓存,不消耗积分');
1764
- cacheInfo = '<span style="color:#2ecc71;">📦 来自缓存,不消耗积分</span>';
1765
- }
1766
- let resourceTitle = data.data?.title || data.data?.name || data.data?.resource?.title || data.data?.resource?.name;
1767
- if (!resourceTitle) {
1768
- resourceTitle = `资源 ${slug.substring(0,8)}...`;
1769
- }
1770
- let panType = data.data?.pan_type || data.data?.resource?.pan_type || 'unknown';
1771
- if (panType === 'unknown' && link) {
1772
- panType = detectPanTypeFromLink(link);
1773
- }
1774
- let panBadge = getPanBadgeHtml(panType);
1775
- resultDiv.innerHTML = `
1776
- <div class="resource-detail">
1777
- <strong>📽️ ${escapeHtml(resourceTitle)}</strong> ${panBadge}
1778
- </div>
1779
- <div><strong>✅ 解锁成功</strong> ${cacheInfo}</div>
1780
- <div style="margin-top:8px; word-break:break-all;">
1781
- <a href="${link}" target="_blank">${link}</a>
1782
- <button class="copy-result-btn" onclick="copyToClipboard('${link.replace(/'/g,"\\'")}', this)">📋 复制</button>
1783
- </div>
1784
- `;
1785
- alert('✅ 解锁成功!');
1786
- } else {
1787
- resultDiv.innerHTML = `<div style="color:#e74c3c;">❌ 解锁失败:${data.message || '无返回链接'}</div>`;
1788
- alert('解锁失败');
1789
- }
1790
- } catch(e) {
1791
- let resultDiv = document.getElementById('quickUnlockResult');
1792
- resultDiv.innerHTML = `<div style="color:#e74c3c;">❌ 请求异常: ${e.message}</div>`;
1793
- alert(`错误: ${e.message}`);
1794
- }
1795
- }
1796
-
1797
- function toggleShowPaid(){ showPaid=document.getElementById('show-paid').checked; if(selectedMovie) selectMovie(selectedMovie.id,selectedMovie.media_type); }
1798
- function renderPagination(){
1799
- if(totalPages<=1){document.getElementById('pagination').innerHTML='';return;}
1800
- let html='';
1801
- if(currentPage>1) html+=`<button class="page-btn" onclick="goToPage(${currentPage-1})">◀</button>`;
1802
- for(let i=Math.max(1,currentPage-2);i<=Math.min(totalPages,currentPage+2);i++) html+=`<button class="page-btn ${i===currentPage?'active':''}" onclick="goToPage(${i})">${i}</button>`;
1803
- if(currentPage<totalPages) html+=`<button class="page-btn" onclick="goToPage(${currentPage+1})">▶</button>`;
1804
- document.getElementById('pagination').innerHTML=html;
1805
- }
1806
- function goToPage(page){currentPage=page;loadMovies();window.scrollTo({top:0});}
1807
- function escapeHtml(str){if(!str)return '';return str.replace(/[&<>]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;'})[m]);}
1808
- async function copyToClipboard(text,btn){
1809
- try { await navigator.clipboard.writeText(text); let o=btn.innerText; btn.innerText='✅ 已复制'; setTimeout(()=>btn.innerText=o,1500); }
1810
- catch(e){ let ta=document.createElement('textarea'); ta.value=text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); let o=btn.innerText; btn.innerText='✅ 已复制'; setTimeout(()=>btn.innerText=o,1500); }
1811
- }
1812
- async function manualBackup(){
1813
- let btn=event.target; let msg=document.getElementById('backupMsg');
1814
- btn.disabled=true; msg.innerText='备份中...';
1815
- try{let data=await fetch('/api/backup',{method:'POST'}).then(r=>r.json());msg.innerText=data.message;setTimeout(()=>msg.innerText='',3000);}catch(e){msg.innerText='失败';}finally{btn.disabled=false;}
1816
- }
1817
- async function updateAllStats(){
1818
- try { let r=await fetch('/api/cache/stats'); let s=await r.json(); document.getElementById('stat-movies').innerText=s.movies||0; document.getElementById('stat-unlocks').innerText=s.unlocks||0; } catch(e){}
1819
- try { let r=await fetch('/api/auto-unlock-stats'); let d=await r.json(); document.getElementById('stat-auto-unlocked').innerText=d.unlocked_resources||0; } catch(e){}
1820
- }
1821
- async function updateCountdown(){
1822
- try { let r=await fetch('/api/next-unlock-time'); let d=await r.json(); let rem=d.remaining||0; if(rem>0){ let m=Math.floor(rem/60); let s=rem%60; document.getElementById('next-unlock-countdown').innerText=`${m}:${s.toString().padStart(2,'0')}`; } else document.getElementById('next-unlock-countdown').innerText='0:00'; } catch(e){}
1823
- }
1824
- // 初始化
1825
- (async function init() {
1826
- await Promise.all([loadMovieGenres(), loadTVGenres()]);
1827
- document.getElementById('movie-presets').querySelectorAll('.preset-btn').forEach(btn => {
1828
- btn.addEventListener('click', () => selectPreset(btn.dataset.preset));
1829
- });
1830
- document.getElementById('tv-presets').querySelectorAll('.preset-btn').forEach(btn => {
1831
- btn.addEventListener('click', () => selectPreset(btn.dataset.preset));
1832
- });
1833
- await switchType('movie');
1834
- setInterval(updateAllStats, 5000);
1835
- setInterval(updateCountdown, 1000);
1836
- updateAllStats();
1837
- updateCountdown();
1838
- })();
1839
- </script>
1840
- </body>
1841
- </html>
1842
- """
1843
-
1844
- if __name__ == '__main__':
1845
- app.run(host='0.0.0.0', port=7860)
1846
-
1847
- # SQLite WAL 优化
1848
- try:
1849
- conn = sqlite3.connect(DB_PATH)
1850
- conn.execute("PRAGMA journal_mode=WAL")
1851
- conn.execute("PRAGMA synchronous=NORMAL")
1852
- conn.close()
1853
- print("✅ SQLite WAL 已启用")
1854
- except Exception as e:
1855
- print("WAL 初始化失败:", e)