376 lines
15 KiB
Python
376 lines
15 KiB
Python
"""
|
|
@header({
|
|
searchable: 1,
|
|
filterable: 1,
|
|
quickSearch: 1,
|
|
title: '茶杯狐-TJTCDL',
|
|
lang: 'hipy',
|
|
})
|
|
"""
|
|
|
|
# -*- coding: utf-8 -*-
|
|
# !/usr/bin/python
|
|
import requests
|
|
import base64
|
|
import random
|
|
import re
|
|
import json
|
|
import sys
|
|
import urllib.parse
|
|
import ssl
|
|
import urllib3
|
|
import hashlib
|
|
from bs4 import BeautifulSoup
|
|
from requests.adapters import HTTPAdapter
|
|
from urllib3.util.ssl_ import create_urllib3_context
|
|
|
|
urllib3.disable_warnings()
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
|
|
|
|
class TLSAdapter(HTTPAdapter):
|
|
def init_poolmanager(self, *args, **kwargs):
|
|
ciphers = (
|
|
'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:'
|
|
'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:'
|
|
'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:'
|
|
'DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'
|
|
)
|
|
context = create_urllib3_context(ciphers=ciphers)
|
|
context.check_hostname = False
|
|
context.verify_mode = ssl.CERT_NONE
|
|
kwargs['ssl_context'] = context
|
|
return super(TLSAdapter, self).init_poolmanager(*args, **kwargs)
|
|
|
|
|
|
class Spider(Spider):
|
|
def __init__(self):
|
|
super(Spider, self).__init__()
|
|
self.session = requests.Session()
|
|
self.session.verify = False
|
|
self.session.mount('https://', TLSAdapter())
|
|
self.host = "https://www.tjtcdl.com"
|
|
self.headers = {
|
|
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
|
|
'Referer': f'{self.host}/',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
}
|
|
|
|
def getName(self):
|
|
return "茶杯狐-TJTCDL"
|
|
|
|
def init(self, extend):
|
|
pass
|
|
|
|
def homeContent(self, filter):
|
|
classes = [
|
|
{"type_id": "1", "type_name": "电影"},
|
|
{"type_id": "2", "type_name": "电视剧"},
|
|
{"type_id": "4", "type_name": "动漫"},
|
|
{"type_id": "3", "type_name": "综艺"},
|
|
{"type_id": "5", "type_name": "热门短剧"},
|
|
]
|
|
|
|
filter_dict = {}
|
|
years = [{"n": "全部", "v": ""}] + [{"n": str(y), "v": str(y)} for y in range(2026, 2003, -1)]
|
|
orders = [
|
|
{"n": "按最新", "v": "time"},
|
|
{"n": "按最热", "v": "hits"},
|
|
{"n": "按评分", "v": "score"}
|
|
]
|
|
|
|
movie_classes = ["动作", "喜剧", "爱情", "科幻", "恐怖", "剧情", "战争", "惊悚", "悬疑", "犯罪", "奇幻", "冒险",
|
|
"动画", "武侠"]
|
|
movie_areas = ["大陆", "香港", "台湾", "美国", "韩国", "日本", "泰国", "新加坡", "马来西亚", "印度", "英国",
|
|
"法国", "加拿大", "西班牙", "俄罗斯", "其它"]
|
|
|
|
tv_classes = ["古装", "战争", "青春偶像", "喜剧", "家庭", "犯罪", "动作", "奇幻", "剧情", "历史", "经典",
|
|
"乡村", "情景", "商战", "网剧", "其他"]
|
|
tv_areas = ["内地", "韩国", "香港", "台湾", "日本", "美国", "泰国", "英国", "新加坡", "其他"]
|
|
|
|
comic_classes = ["科幻", "热血", "推理", "搞笑", "冒险", "萝莉", "校园", "动作", "机战", "运动", "战争", "少年",
|
|
"少女"]
|
|
show_classes = ["脱口秀", "真人秀", "搞笑", "访谈", "生活", "晚会", "美食", "游戏", "亲子", "旅游", "音乐",
|
|
"舞蹈"]
|
|
|
|
def create_filter(classes_list, areas_list):
|
|
return [
|
|
{"key": "class", "name": "类型",
|
|
"value": [{"n": "全部", "v": ""}] + [{"n": c, "v": c} for c in classes_list]},
|
|
{"key": "area", "name": "地区",
|
|
"value": [{"n": "全部", "v": ""}] + [{"n": a, "v": a} for a in areas_list]},
|
|
{"key": "year", "name": "年份", "value": years},
|
|
{"key": "by", "name": "排序", "value": orders}
|
|
]
|
|
|
|
filter_dict["1"] = create_filter(movie_classes, movie_areas)
|
|
filter_dict["2"] = create_filter(tv_classes, tv_areas)
|
|
filter_dict["4"] = create_filter(comic_classes, tv_areas)
|
|
filter_dict["3"] = create_filter(show_classes, tv_areas)
|
|
filter_dict["5"] = create_filter(["女频", "男频", "复仇", "甜宠", "穿越", "逆袭", "战神", "脑洞"],
|
|
["内地", "其他"])
|
|
|
|
return {"class": classes, "filters": filter_dict}
|
|
|
|
def homeVideoContent(self):
|
|
return {'list': []}
|
|
|
|
def categoryContent(self, cid, pg, filter, ext):
|
|
page = int(pg)
|
|
ext = ext or {}
|
|
area = urllib.parse.quote(ext.get('area', '')) if ext.get('area') else ''
|
|
by = ext.get('by', '')
|
|
class_name = urllib.parse.quote(ext.get('class', '')) if ext.get('class') else ''
|
|
lang = urllib.parse.quote(ext.get('lang', '')) if ext.get('lang') else ''
|
|
letter = ext.get('letter', '')
|
|
year = ext.get('year', '')
|
|
|
|
url = f"{self.host}/cupfox-list/{cid}-{area}-{by}-{class_name}-{lang}-{letter}---{page}---{year}.html"
|
|
res = self.session.get(url, headers=self.headers)
|
|
soup = BeautifulSoup(res.text, 'html.parser')
|
|
|
|
videos = []
|
|
for item in soup.select('.public-list-box'):
|
|
link = item.select_one('.public-list-exp')
|
|
if not link: continue
|
|
vid_match = re.search(r'/chabeihu/(\d+)\.html', link.get('href', ''))
|
|
if not vid_match: continue
|
|
|
|
pic_img = link.select_one('img')
|
|
note_tag = item.select_one('.public-list-prb')
|
|
|
|
videos.append({
|
|
"vod_id": vid_match.group(1),
|
|
"vod_name": link.get('title', '').strip(),
|
|
"vod_pic": pic_img.get('data-src') or pic_img.get('src') or '' if pic_img else '',
|
|
"vod_remarks": note_tag.text.strip() if note_tag else ''
|
|
})
|
|
|
|
return {'list': videos, 'page': page, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
|
|
|
def detailContent(self, ids):
|
|
did = ids[0]
|
|
url = f"{self.host}/chabeihu/{did}.html"
|
|
res = self.session.get(url, headers=self.headers, timeout=10)
|
|
soup = BeautifulSoup(res.text, 'html.parser')
|
|
|
|
name, state, actor, director, year, content = "", "", "", "", "", ""
|
|
for li in soup.select('.info-parameter li'):
|
|
em = li.select_one('em')
|
|
if not em: continue
|
|
em_text = em.text.strip()
|
|
val = li.text.replace(em_text, '').replace('\xa0', ' ').strip()
|
|
|
|
if '片名' in em_text:
|
|
name = val
|
|
elif '状态' in em_text:
|
|
state = val
|
|
elif '主演' in em_text:
|
|
actor = val
|
|
elif '导演' in em_text:
|
|
director = val
|
|
elif '年份' in em_text:
|
|
year = val
|
|
elif '简介' in em_text:
|
|
content = val
|
|
|
|
if not name:
|
|
title_tag = soup.select_one('.this-desc-title')
|
|
name = title_tag.text.strip() if title_tag else ''
|
|
|
|
play_from, play_url = [], []
|
|
sources = [s.text.replace(s.select_one('.badge').text, '').strip() if s.select_one('.badge') else s.text.strip()
|
|
for s in soup.select('.anthology-tab .swiper-slide')]
|
|
|
|
for idx, box in enumerate(soup.select('.anthology-list-box')):
|
|
eps = [
|
|
f"{a.text.strip()}${self.host + a.get('href') if not a.get('href').startswith('http') else a.get('href')}"
|
|
for a in box.select('li a') if a.get('href')]
|
|
if eps:
|
|
eps.reverse()
|
|
play_from.append(sources[idx] if idx < len(sources) else f"线路{idx + 1}")
|
|
play_url.append('#'.join(eps))
|
|
|
|
return {'list': [{
|
|
"vod_id": did,
|
|
"vod_name": name,
|
|
"vod_actor": actor,
|
|
"vod_director": director,
|
|
"vod_content": content,
|
|
"vod_remarks": state,
|
|
"vod_year": year,
|
|
"vod_play_from": '$$$'.join(play_from),
|
|
"vod_play_url": '$$$'.join(play_url)
|
|
}]}
|
|
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
try:
|
|
|
|
res = self.session.get(id, headers=self.headers, timeout=5)
|
|
match = re.search(r'var player_aaaa=(.*?)</script>', res.text)
|
|
if not match:
|
|
return {'parse': 0, 'url': ''}
|
|
|
|
player_data = json.loads(match.group(1))
|
|
durl = player_data.get('url', '')
|
|
encrypt = player_data.get('encrypt', 0)
|
|
from_flag = player_data.get('from', '')
|
|
|
|
if encrypt == 1:
|
|
durl = urllib.parse.unquote(durl)
|
|
elif encrypt == 2:
|
|
durl = urllib.parse.unquote(durl)
|
|
durl = base64.b64decode(durl).decode('utf-8')
|
|
durl = urllib.parse.unquote(durl)
|
|
|
|
|
|
if durl.startswith('http') and ('.m3u8' in durl or '.mp4' in durl):
|
|
|
|
return {'parse': 0, 'url': durl}
|
|
|
|
|
|
|
|
|
|
config_url = f"{self.host}/static/js/playerconfig.js"
|
|
config_res = self.session.get(config_url, headers=self.headers, verify=False, timeout=5)
|
|
|
|
parse_api = ""
|
|
if from_flag:
|
|
m = re.search(f'"{from_flag}":\\{{[^}}]*"parse":"([^"]+)"', config_res.text)
|
|
if m: parse_api = m.group(1).replace('\\/', '/')
|
|
if not parse_api:
|
|
m = re.search(r'"parse":"(http[^"]+)"', config_res.text)
|
|
if m: parse_api = m.group(1).replace('\\/', '/')
|
|
if not parse_api:
|
|
parse_api = "https://fgsrg.hzqingshan.com/player/?url="
|
|
|
|
iframe_url = parse_api + durl
|
|
print(f"[*] 2. 锁定隐藏解析服务器(iframe): {iframe_url}")
|
|
|
|
|
|
iframe_headers = self.headers.copy()
|
|
iframe_headers['Referer'] = id
|
|
iframe_res = self.session.get(iframe_url, headers=iframe_headers, verify=False, timeout=5)
|
|
|
|
iframe_soup = BeautifulSoup(iframe_res.text, 'html.parser')
|
|
player_data_tag = iframe_soup.select_one('#player-data')
|
|
|
|
if not player_data_tag:
|
|
|
|
return {'parse': 1, 'url': iframe_url}
|
|
|
|
token = player_data_tag.get('data-te', '')
|
|
bt = player_data_tag.get('data-bt', '/player/')
|
|
|
|
|
|
|
|
api_base = urllib.parse.urlparse(parse_api)
|
|
api_host = f"{api_base.scheme}://{api_base.netloc}"
|
|
api_url = f"{api_host}{bt}mplayer.php"
|
|
|
|
|
|
post_data = {'url': durl, 'token': token}
|
|
|
|
api_headers = self.headers.copy()
|
|
api_headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
|
api_headers['X-Requested-With'] = 'XMLHttpRequest'
|
|
api_headers['Referer'] = iframe_url
|
|
api_headers['Origin'] = api_host
|
|
|
|
|
|
api_res = self.session.post(api_url, data=post_data, headers=api_headers, verify=False, timeout=10)
|
|
|
|
if api_res.status_code == 200:
|
|
api_json = api_res.json()
|
|
print(f"[*] 5. 接口解密成功: {api_json}")
|
|
|
|
real_url = api_json.get('url') or api_json.get('data', {}).get('url', '')
|
|
urlmode = str(api_json.get('urlmode') or api_json.get('data', {}).get('urlmode', ''))
|
|
|
|
if urlmode == '1':
|
|
real_url = self.js_decrypt1(real_url)
|
|
elif urlmode == '2':
|
|
real_url = self.js_decrypt2(real_url)
|
|
elif urlmode == '3':
|
|
real_url = self.js_decrypt3(real_url)
|
|
|
|
for _ in range(3):
|
|
if real_url.startswith('WyJ') and '/' in real_url:
|
|
real_url = self.js_decrypt3(real_url)
|
|
else:
|
|
break
|
|
|
|
if real_url:
|
|
print(f"[*] 6. 🎉斩获最终真实 M3U8: {real_url}")
|
|
return {'parse': 0 if ('.m3u8' in real_url or '.mp4' in real_url) else 1, 'url': real_url}
|
|
|
|
|
|
return {'parse': 1, 'url': iframe_url}
|
|
|
|
except Exception as e:
|
|
|
|
return {'parse': 1, 'url': id}
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
search_url = f'{self.host}/cupfox-search/-------------.html'
|
|
res = self.session.get(search_url, params={'wd': key}, headers=self.headers)
|
|
soup = BeautifulSoup(res.text, 'html.parser')
|
|
|
|
videos = []
|
|
for item in soup.select('.public-list-box'):
|
|
link = item.select_one('.public-list-exp')
|
|
if not link: continue
|
|
vid_match = re.search(r'/chabeihu/(\d+)\.html', link.get('href', ''))
|
|
if not vid_match: continue
|
|
|
|
pic_img = link.select_one('img')
|
|
note_tag = item.select_one('.public-list-prb')
|
|
|
|
videos.append({
|
|
"vod_id": vid_match.group(1),
|
|
"vod_name": link.get('title', '').strip(),
|
|
"vod_pic": pic_img.get('data-src') or pic_img.get('src') or '' if pic_img else '',
|
|
"vod_remarks": note_tag.text.strip() if note_tag else ''
|
|
})
|
|
|
|
return {'list': videos, 'page': int(pg), 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
|
|
|
|
|
|
def js_decrypt1(self, data):
|
|
try:
|
|
key = hashlib.md5(b'test').hexdigest()
|
|
dec1 = base64.b64decode(data)
|
|
code = bytearray([dec1[i] ^ ord(key[i % len(key)]) for i in range(len(dec1))])
|
|
return base64.b64decode(code).decode('utf-8')
|
|
except:
|
|
return data
|
|
|
|
def js_decrypt2(self, data):
|
|
staticchars = "PXhw7UT1B0a9kQDKZsjIASmOezxYG4CHo5Jyfg2b8FLpEvRr3WtVnlqMidu6cN"
|
|
try:
|
|
dec = base64.b64decode(data).decode('utf-8', errors='ignore')
|
|
return "".join(
|
|
[staticchars[(staticchars.find(dec[i]) + 59) % 62] if staticchars.find(dec[i]) != -1 else dec[i] for i
|
|
in range(1, len(dec), 3)])
|
|
except:
|
|
return data
|
|
|
|
def js_decrypt3(self, data):
|
|
def fix_b64(s):
|
|
return s + '=' * (4 - len(s) % 4) if len(s) % 4 else s
|
|
|
|
try:
|
|
parts = data.split('/')
|
|
if len(parts) >= 3:
|
|
arr1 = json.loads(base64.b64decode(fix_b64(parts[0])).decode('utf-8'))
|
|
arr2 = json.loads(base64.b64decode(fix_b64(parts[1])).decode('utf-8'))
|
|
cipher = base64.b64decode(fix_b64('/'.join(parts[2:]))).decode('utf-8', errors='ignore')
|
|
return "".join([arr1[arr2.index(c)] if c in arr2 else c for c in cipher])
|
|
except:
|
|
pass
|
|
return data
|
|
|