Sync all projects
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
# coding=utf-8
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
import random
|
||||
import hashlib
|
||||
import hmac
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.Hash import SHA1, SHA256
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.name = "花子动漫"
|
||||
self.base = 'https://www.huazidm.com'
|
||||
self.ua = 'okhttp/5.3.2'
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': self.ua
|
||||
})
|
||||
self.aes_key = None
|
||||
self.client_id = None
|
||||
self.session_id = None
|
||||
self.types = {}
|
||||
self._home_cache = None
|
||||
self._home_cache_time = 0
|
||||
# 不在__init__里握手,延迟到第一次请求时
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
# ==================== 加密通信 ====================
|
||||
def _b64u_encode(self, data):
|
||||
return base64.urlsafe_b64encode(data).decode().rstrip('=')
|
||||
|
||||
def _b64u_decode(self, s):
|
||||
padding = 4 - len(s) % 4
|
||||
if padding != 4:
|
||||
s += '=' * padding
|
||||
return base64.urlsafe_b64decode(s)
|
||||
|
||||
def _aes_gcm_encrypt(self, key, plaintext):
|
||||
iv = os.urandom(12)
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
|
||||
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode())
|
||||
return {
|
||||
'iv': self._b64u_encode(iv),
|
||||
'data': self._b64u_encode(ciphertext),
|
||||
'tag': self._b64u_encode(tag)
|
||||
}
|
||||
|
||||
def _aes_gcm_decrypt(self, key, data):
|
||||
iv = self._b64u_decode(data['iv'])
|
||||
ciphertext = self._b64u_decode(data['data'])
|
||||
tag = self._b64u_decode(data['tag'])
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
|
||||
return cipher.decrypt_and_verify(ciphertext, tag).decode()
|
||||
|
||||
def _init_api(self):
|
||||
try:
|
||||
r = self.session.get(self.base + '/api/public/config', timeout=15)
|
||||
config = r.json()['data']
|
||||
pub_key = config['public_key']
|
||||
|
||||
self.aes_key = os.urandom(32)
|
||||
self.client_id = os.urandom(16).hex()
|
||||
|
||||
rsa_key = RSA.import_key(pub_key)
|
||||
cipher_rsa = PKCS1_OAEP.new(rsa_key, hashAlgo=SHA1)
|
||||
encrypted_key = base64.b64encode(cipher_rsa.encrypt(self.aes_key)).decode()
|
||||
|
||||
handshake_data = {
|
||||
'client_id': self.client_id,
|
||||
'encrypted_key': encrypted_key,
|
||||
'fingerprint': self.session.headers.get('User-Agent', '')
|
||||
}
|
||||
r2 = self.session.post(
|
||||
self.base + '/api/public/handshake',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
json=handshake_data,
|
||||
timeout=15
|
||||
)
|
||||
result = r2.json()
|
||||
if result.get('code') == 1:
|
||||
self.session_id = result['data']['session_id']
|
||||
else:
|
||||
raise Exception('handshake failed: ' + result.get('msg', ''))
|
||||
except Exception as e:
|
||||
print(f'_init_api error: {e}')
|
||||
|
||||
def _req(self, path, data=None, retry=True):
|
||||
if data is None:
|
||||
data = {}
|
||||
if not self.aes_key or not self.session_id:
|
||||
self._init_api()
|
||||
if not self.aes_key or not self.session_id:
|
||||
return None
|
||||
try:
|
||||
payload = self._aes_gcm_encrypt(self.aes_key, json.dumps(data, ensure_ascii=False))
|
||||
ts = str(int(time.time()))
|
||||
nonce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=20))
|
||||
sign = hmac.new(
|
||||
self.aes_key,
|
||||
f'{payload["data"]}|{ts}|{nonce}'.encode(),
|
||||
SHA256
|
||||
).hexdigest()
|
||||
body = {
|
||||
'client_id': self.client_id,
|
||||
'session_id': self.session_id,
|
||||
'ts': ts,
|
||||
'nonce': nonce,
|
||||
'sign': sign,
|
||||
'payload': payload
|
||||
}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-Id': self.client_id,
|
||||
'X-Session-Id': self.session_id
|
||||
}
|
||||
r = self.session.post(self.base + path, headers=headers, json=body, timeout=15)
|
||||
rd = r.json()
|
||||
if rd.get('code') != 1:
|
||||
# session失效时重新握手
|
||||
if retry and ('失效' in str(rd.get('msg', '')) or '过期' in str(rd.get('msg', '')) or rd.get('code') == 401):
|
||||
self._init_api()
|
||||
return self._req(path, data, retry=False)
|
||||
return None
|
||||
pd = rd['data']['payload']
|
||||
decrypted = self._aes_gcm_decrypt(self.aes_key, pd)
|
||||
return json.loads(decrypted)
|
||||
except Exception as e:
|
||||
print(f'_req {path} error: {e}')
|
||||
return None
|
||||
|
||||
# ==================== TVBox 接口 ====================
|
||||
def _get_home(self):
|
||||
# 缓存home数据5分钟,避免重复请求
|
||||
now = time.time()
|
||||
if self._home_cache and now - self._home_cache_time < 300:
|
||||
return self._home_cache
|
||||
data = self._req('/api/site/home')
|
||||
if data:
|
||||
self._home_cache = data
|
||||
self._home_cache_time = now
|
||||
return data
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
try:
|
||||
data = self._get_home()
|
||||
if not data:
|
||||
return result
|
||||
types = data.get('types', [])
|
||||
for t in types:
|
||||
tid = str(t['type_id'])
|
||||
name = t['type_name']
|
||||
result['class'].append({'type_id': tid, 'type_name': name})
|
||||
self.types[tid] = t
|
||||
# 精简筛选 - 只保留排序,减少初始化时间
|
||||
result['filters'][tid] = [{
|
||||
'key': 'sort', 'name': '排序', 'value': [
|
||||
{'n': '最新', 'v': 'latest'},
|
||||
{'n': '最热', 'v': 'hits'},
|
||||
{'n': '评分', 'v': 'score'},
|
||||
]
|
||||
}]
|
||||
|
||||
# 首页推荐
|
||||
for item in data.get('recommend', []):
|
||||
result['list'].append({
|
||||
'vod_id': str(item['id']),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('poster', ''),
|
||||
'vod_remarks': item.get('remark', '')
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeContent error: {e}')
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
data = self._req('/api/site/home')
|
||||
if data:
|
||||
for item in data.get('latest', []):
|
||||
videos.append({
|
||||
'vod_id': str(item['id']),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('poster', ''),
|
||||
'vod_remarks': item.get('remark', '')
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return {'list': videos, 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
params = {'keyword': '', 'category_id': int(tid), 'page': page, 'limit': 20}
|
||||
if extend:
|
||||
if extend.get('class'):
|
||||
params['genre'] = extend['class']
|
||||
if extend.get('year'):
|
||||
params['year'] = extend['year']
|
||||
if extend.get('area'):
|
||||
params['area'] = extend['area']
|
||||
if extend.get('sort'):
|
||||
sort_map = {'latest': 'updated_at', 'hits': 'hits', 'score': 'score'}
|
||||
params['sort'] = sort_map.get(extend['sort'], extend['sort'])
|
||||
data = self._req('/api/site/search', params)
|
||||
if data:
|
||||
for item in data.get('list', []):
|
||||
result['list'].append({
|
||||
'vod_id': str(item['id']),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('poster', ''),
|
||||
'vod_remarks': item.get('remark', '')
|
||||
})
|
||||
total = int(data.get('total', 0))
|
||||
result['page'] = page
|
||||
result['pagecount'] = (total + 19) // 20 if total > 0 else page
|
||||
result['limit'] = 20
|
||||
result['total'] = total
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = str(ids[0]) if ids else ''
|
||||
if not vid:
|
||||
return result
|
||||
try:
|
||||
data = self._req('/api/site/detail', {'id': int(vid)})
|
||||
if not data or 'detail' not in data:
|
||||
return result
|
||||
detail = data['detail']
|
||||
play_sources = detail.get('play_sources', [])
|
||||
play_from = []
|
||||
play_urls = []
|
||||
for src in play_sources:
|
||||
src_code = src.get('code', '')
|
||||
src_name = src.get('name', '')
|
||||
if not src_code or not src_name:
|
||||
continue
|
||||
episodes = src.get('episodes', [])
|
||||
ep_list = []
|
||||
for idx, ep in enumerate(episodes):
|
||||
ep_name = ep.get('name', f'第{idx+1}集')
|
||||
# 用base64编码避免特殊字符问题
|
||||
play_data = f'{vid}|{src_code}|{idx}'
|
||||
play_id = base64.b64encode(play_data.encode()).decode()
|
||||
ep_list.append(f'{ep_name}${play_id}')
|
||||
if ep_list:
|
||||
play_from.append(src_name)
|
||||
play_urls.append('#'.join(ep_list))
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': detail.get('name', ''),
|
||||
'vod_pic': detail.get('poster', ''),
|
||||
'type_name': detail.get('genre', ''),
|
||||
'vod_year': str(detail.get('year', '')),
|
||||
'vod_area': detail.get('area', ''),
|
||||
'vod_remarks': detail.get('remark', ''),
|
||||
'vod_actor': detail.get('actor', ''),
|
||||
'vod_director': detail.get('director', ''),
|
||||
'vod_content': detail.get('content_text', ''),
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_urls)
|
||||
}
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
data = self._req('/api/site/search', {
|
||||
'keyword': key,
|
||||
'page': page,
|
||||
'limit': 20
|
||||
})
|
||||
if data:
|
||||
for item in data.get('list', []):
|
||||
result['list'].append({
|
||||
'vod_id': str(item['id']),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('poster', ''),
|
||||
'vod_remarks': item.get('remark', '')
|
||||
})
|
||||
total = int(data.get('total', 0))
|
||||
result['page'] = page
|
||||
result['pagecount'] = (total + 19) // 20 if total > 0 else page
|
||||
result['limit'] = 20
|
||||
result['total'] = total
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
# id 是 base64 编码的 vod_id|source_code|episode_index
|
||||
try:
|
||||
decoded = base64.b64decode(id).decode()
|
||||
except:
|
||||
decoded = id
|
||||
parts = decoded.split('|')
|
||||
if len(parts) < 3:
|
||||
return {'parse': 0, 'jx': 0, 'url': ''}
|
||||
vod_id = int(parts[0])
|
||||
source_code = parts[1]
|
||||
episode_index = int(parts[2])
|
||||
|
||||
data = self._req('/api/site/play-resolve', {
|
||||
'id': vod_id,
|
||||
'source': source_code,
|
||||
'episode': episode_index
|
||||
})
|
||||
if data and data.get('play_url'):
|
||||
url = data['play_url']
|
||||
headers = {'User-Agent': self.ua}
|
||||
ua = data.get('user_agent', '')
|
||||
if ua:
|
||||
headers['User-Agent'] = ua
|
||||
return {
|
||||
'parse': 0,
|
||||
'jx': 0,
|
||||
'url': url,
|
||||
'header': headers,
|
||||
'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
return {'parse': 0, 'jx': 0, 'url': ''}
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -0,0 +1,491 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.dazhongs.com'
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://www.dazhongs.com/'
|
||||
})
|
||||
self.cateManual = {
|
||||
'电影': '1',
|
||||
'电视剧': '2',
|
||||
'综艺': '3',
|
||||
'动漫': '4',
|
||||
'短剧': '30',
|
||||
}
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '大众影视'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = text.replace(' ', ' ').replace('&', '&').replace('\u3000', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15)
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except Exception as e:
|
||||
print(f'_get error: {e}')
|
||||
return ''
|
||||
|
||||
def _getVid(self, url):
|
||||
if not url:
|
||||
return ''
|
||||
m = re.search(r'/voddetail/(\d+)\.html', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.search(r'/vodplay/(\d+)-', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
def _fixUrl(self, url):
|
||||
if not url:
|
||||
return ''
|
||||
url = url.replace('&', '&')
|
||||
if url.startswith('//'):
|
||||
return 'https:' + url
|
||||
if url.startswith('/'):
|
||||
return self.site + url
|
||||
if not url.startswith('http'):
|
||||
return self.site + '/' + url
|
||||
return url
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
html = self._get(self.site)
|
||||
if not html:
|
||||
return {'list': videos, 'parse': 0, 'jx': 0}
|
||||
seen = set()
|
||||
# 首页列表项: <li class="vodlist_item">...</li>
|
||||
for m in re.finditer(r'<li\s+class="vodlist_item[^"]*"[^>]*>(.*?)</li>', html, re.DOTALL):
|
||||
block = m.group(1)
|
||||
# 标题和链接
|
||||
tm = re.search(r'href="(/voddetail/\d+\.html)"[^>]*title="([^"]*)"', block)
|
||||
if not tm:
|
||||
continue
|
||||
href = tm.group(1)
|
||||
title = tm.group(2)
|
||||
vid = self._getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
# 封面
|
||||
pic = ''
|
||||
pm = re.search(r'data-original="([^"]+)"', block)
|
||||
if pm:
|
||||
pic = self._fixUrl(pm.group(1))
|
||||
# 备注
|
||||
note = ''
|
||||
nm = re.search(r'class="pic_text[^"]*"[^>]*>([^<]*)<', block)
|
||||
if nm:
|
||||
note = nm.group(1).strip()
|
||||
if title:
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return {'list': videos, 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
if page == 1:
|
||||
url = f'{self.site}/vodtype/{tid}.html'
|
||||
else:
|
||||
url = f'{self.site}/vodtype/{tid}-{page}.html'
|
||||
|
||||
html = self._get(url)
|
||||
if not html:
|
||||
return result
|
||||
seen = set()
|
||||
for m in re.finditer(r'<li\s+class="vodlist_item[^"]*"[^>]*>(.*?)</li>', html, re.DOTALL):
|
||||
block = m.group(1)
|
||||
tm = re.search(r'href="(/voddetail/\d+\.html)"[^>]*title="([^"]*)"', block)
|
||||
if not tm:
|
||||
continue
|
||||
href = tm.group(1)
|
||||
title = tm.group(2)
|
||||
vid = self._getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
pic = ''
|
||||
pm = re.search(r'data-original="([^"]+)"', block)
|
||||
if pm:
|
||||
pic = self._fixUrl(pm.group(1))
|
||||
note = ''
|
||||
nm = re.search(r'class="pic_text[^"]*"[^>]*>([^<]*)<', block)
|
||||
if nm:
|
||||
note = nm.group(1).strip()
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ids[0] if ids else ''
|
||||
if not vid:
|
||||
return result
|
||||
try:
|
||||
html = self._get(f'{self.site}/voddetail/{vid}.html')
|
||||
if not html:
|
||||
return result
|
||||
|
||||
# 标题
|
||||
title = ''
|
||||
tm = re.search(r'<h2\s+class="title[^"]*"[^>]*>([^<]+)<', html)
|
||||
if tm:
|
||||
title = self._clean(tm.group(1))
|
||||
if not title:
|
||||
tm = re.search(r'<title>([^<]+)', html)
|
||||
if tm:
|
||||
title = self._clean(tm.group(1))
|
||||
|
||||
# 封面
|
||||
pic = ''
|
||||
pm = re.search(r'class="vodlist_thumb[^"]*"[^>]*data-original="([^"]+)"', html)
|
||||
if pm:
|
||||
pic = self._fixUrl(pm.group(1))
|
||||
|
||||
# 备注(状态)
|
||||
remarks = ''
|
||||
rm = re.search(r'class="data_style[^"]*"[^>]*>([^<]*)<', html)
|
||||
if rm:
|
||||
remarks = rm.group(1).strip()
|
||||
|
||||
# 简介
|
||||
desc = ''
|
||||
dm = re.search(r'class="desc[^"]*"[^>]*>(.*?)</li>', html, re.DOTALL)
|
||||
if dm:
|
||||
desc = self._clean(dm.group(1))
|
||||
desc = re.sub(r'^简介:\s*', '', desc).strip()
|
||||
if not desc:
|
||||
dm2 = re.search(r'<meta\s+name="description"\s+content="([^"]*)"', html)
|
||||
if dm2:
|
||||
desc = dm2.group(1).strip()
|
||||
|
||||
# 年份/地区/类型/主演/导演
|
||||
vod_year = ''
|
||||
vod_area = ''
|
||||
type_name = ''
|
||||
vod_actor = ''
|
||||
vod_director = ''
|
||||
|
||||
data_items = re.findall(r'<li\s+class="data[^"]*"[^>]*>(.*?)</li>', html, re.DOTALL)
|
||||
if len(data_items) >= 1:
|
||||
first = data_items[0]
|
||||
ym = re.search(r'年份:</span><a[^>]*>([^<]+)', first)
|
||||
if ym:
|
||||
vod_year = ym.group(1).strip()
|
||||
am = re.search(r'地区:</span><a[^>]*>([^<]+)', first)
|
||||
if am:
|
||||
vod_area = am.group(1).strip()
|
||||
tm2 = re.search(r'类型:</span><a[^>]*>([^<]+)', first)
|
||||
if tm2:
|
||||
type_name = tm2.group(1).strip()
|
||||
if len(data_items) >= 3:
|
||||
actor_block = data_items[2]
|
||||
actor_m = re.search(r'主演:(.*)', actor_block, re.DOTALL)
|
||||
if actor_m:
|
||||
vod_actor = self._clean(actor_m.group(1))
|
||||
if len(data_items) >= 4:
|
||||
dir_block = data_items[3]
|
||||
dir_m = re.search(r'导演:(.*)', dir_block, re.DOTALL)
|
||||
if dir_m:
|
||||
vod_director = self._clean(dir_m.group(1))
|
||||
|
||||
# 线路和播放列表
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# 线路名称: #NumTab 下的 <a alt="线路名">
|
||||
tab_block = re.search(r'id="NumTab"[^>]*>(.*?)</div>', html, re.DOTALL)
|
||||
tabs = []
|
||||
if tab_block:
|
||||
tabs = re.findall(r'<a[^>]*alt="([^"]*)"[^>]*>', tab_block.group(1))
|
||||
|
||||
# 播放列表: 每个 .play_list_box 内的 content_playlist
|
||||
panels = re.findall(r'class="play_list_box[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
|
||||
|
||||
for i, tab_name in enumerate(tabs):
|
||||
play_from.append(tab_name)
|
||||
episodes = []
|
||||
if i < len(panels):
|
||||
for em in re.finditer(r'<a[^>]*href="(/vodplay/[^"]+)"[^>]*>([^<]+)<', panels[i]):
|
||||
ep_href = em.group(1)
|
||||
ep_name = em.group(2).strip()
|
||||
if ep_name and ep_href:
|
||||
episodes.append(f'{ep_name}${ep_href}')
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
# 兜底: 如果没找到线路tab但有播放列表 (单线路情况)
|
||||
if not tabs and panels:
|
||||
play_from.append('默认线路')
|
||||
episodes = []
|
||||
for em in re.finditer(r'<a[^>]*href="(/vodplay/[^"]+)"[^>]*>([^<]+)<', panels[0]):
|
||||
ep_href = em.group(1)
|
||||
ep_name = em.group(2).strip()
|
||||
if ep_name and ep_href:
|
||||
episodes.append(f'{ep_name}${ep_href}')
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': type_name,
|
||||
'vod_year': vod_year,
|
||||
'vod_area': vod_area,
|
||||
'vod_remarks': remarks,
|
||||
'vod_actor': vod_actor,
|
||||
'vod_director': vod_director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from) if play_from else '',
|
||||
'vod_play_url': '$$$'.join(play_url) if play_url else ''
|
||||
}
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
danmaku = 'http://127.0.0.1:9978/proxy?do=diydanmu'
|
||||
try:
|
||||
play_url = id
|
||||
if id and not id.startswith('http'):
|
||||
play_url = self.site + id
|
||||
|
||||
html = self._get(play_url)
|
||||
|
||||
# 优先从 player_aaaa JSON 提取 m3u8 直链
|
||||
m = re.search(r'player_aaaa\s*=\s*(\{.+?\})\s*<', html)
|
||||
if m:
|
||||
try:
|
||||
data = json.loads(m.group(1))
|
||||
m3u8 = data.get('url', '')
|
||||
if m3u8 and '.m3u8' in m3u8:
|
||||
result['parse'] = 0
|
||||
result['url'] = m3u8
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
result['danmaku'] = danmaku
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
# 备用: 从页面找 m3u8 链接
|
||||
m = re.search(r'url["\s:]+(https?://[^"\s]*\.m3u8[^"\s]*)', html)
|
||||
if m:
|
||||
result['parse'] = 0
|
||||
result['url'] = m.group(1).replace('\\/', '/')
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
result['danmaku'] = danmaku
|
||||
return result
|
||||
|
||||
# 备用: iframe 嗅探
|
||||
m = re.search(r'<iframe[^>]+src="([^"]+)"', html)
|
||||
if m:
|
||||
result['parse'] = 1
|
||||
result['url'] = m.group(1)
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
result['danmaku'] = danmaku
|
||||
else:
|
||||
# 最终兜底: 嗅探模式
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
result['danmaku'] = danmaku
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
result['parse'] = 1
|
||||
result['url'] = id
|
||||
result['jx'] = 0
|
||||
result['header'] = {}
|
||||
result['danmaku'] = danmaku
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
url = f'{self.site}/vodsearch/-------------.html'
|
||||
params = {'wd': key}
|
||||
if page > 1:
|
||||
params['page'] = page
|
||||
|
||||
r = self.session.get(url, params=params, timeout=15)
|
||||
r.encoding = 'utf-8'
|
||||
html = r.text
|
||||
if not html:
|
||||
return result
|
||||
seen = set()
|
||||
# 搜索结果用 .searchlist_item
|
||||
for m in re.finditer(r'<li\s+class="searchlist_item[^"]*"[^>]*>(.*?)</li>', html, re.DOTALL):
|
||||
block = m.group(1)
|
||||
tm = re.search(r'href="(/voddetail/\d+\.html)"[^>]*title="([^"]*)"', block)
|
||||
if not tm:
|
||||
continue
|
||||
href = tm.group(1)
|
||||
title = tm.group(2)
|
||||
vid = self._getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
# 封面
|
||||
pic = ''
|
||||
pm = re.search(r'data-original="([^"]+)"', block)
|
||||
if pm:
|
||||
pic = self._fixUrl(pm.group(1))
|
||||
# 备注
|
||||
note = ''
|
||||
nm = re.search(r'class="pic_text[^"]*"[^>]*>([^<]*)<', block)
|
||||
if nm:
|
||||
note = nm.group(1).strip()
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
spider = Spider()
|
||||
|
||||
print('=' * 60)
|
||||
print('测试 homeContent ...')
|
||||
home = spider.homeContent(True)
|
||||
print(f"分类数量: {len(home['class'])}")
|
||||
for c in home['class']:
|
||||
print(f" {c['type_id']}: {c['type_name']}")
|
||||
|
||||
print()
|
||||
print('=' * 60)
|
||||
print('测试 homeVideoContent ...')
|
||||
hv = spider.homeVideoContent()
|
||||
print(f"首页列表数量: {len(hv['list'])}")
|
||||
for item in hv['list'][:5]:
|
||||
print(f" {item['vod_name']} | {item['vod_id']} | {item['vod_remarks']}")
|
||||
|
||||
print()
|
||||
print('=' * 60)
|
||||
print('测试 categoryContent (电影, 第1页) ...')
|
||||
cat = spider.categoryContent('1', '1', True, {})
|
||||
print(f"分类列表数量: {len(cat['list'])}")
|
||||
for item in cat['list'][:3]:
|
||||
print(f" {item['vod_name']} | {item['vod_id']}")
|
||||
|
||||
print()
|
||||
print('=' * 60)
|
||||
print('测试 searchContent (家业) ...')
|
||||
search = spider.searchContent('家业', False, '1')
|
||||
print(f"搜索结果数量: {len(search['list'])}")
|
||||
for item in search['list'][:3]:
|
||||
print(f" {item['vod_name']} | {item['vod_id']}")
|
||||
|
||||
print()
|
||||
print('=' * 60)
|
||||
test_id = '102577'
|
||||
print(f'测试 detailContent (vod_id={test_id}) ...')
|
||||
detail = spider.detailContent([test_id])
|
||||
if detail['list']:
|
||||
info = detail['list'][0]
|
||||
print(f" 标题: {info['vod_name']}")
|
||||
print(f" 年份: {info['vod_year']}")
|
||||
print(f" 地区: {info['vod_area']}")
|
||||
print(f" 类型: {info['type_name']}")
|
||||
print(f" 主演: {info['vod_actor']}")
|
||||
print(f" 导演: {info['vod_director']}")
|
||||
print(f" 备注: {info['vod_remarks']}")
|
||||
print(f" 封面: {info['vod_pic']}")
|
||||
print(f" 简介: {info['vod_content'][:80]}...")
|
||||
print(f" 线路: {info['vod_play_from']}")
|
||||
play_urls = info['vod_play_url'].split('$$$')
|
||||
for i, pu in enumerate(play_urls):
|
||||
eps = pu.split('#')
|
||||
print(f" 线路{i+1}: 共{len(eps)}集, 前3集名: {[e.split('$')[0] for e in eps[:3]]}")
|
||||
|
||||
print()
|
||||
print('=' * 60)
|
||||
print('测试 playerContent ...')
|
||||
player = spider.playerContent('稳定线路①', f'/vodplay/102577-1-1.html', '')
|
||||
print(f" parse: {player.get('parse')}")
|
||||
print(f" url: {player.get('url', '')[:100]}...")
|
||||
print(f" jx: {player.get('jx')}")
|
||||
|
||||
print()
|
||||
print('全部测试完成!')
|
||||
+16
-10
@@ -4,16 +4,22 @@
|
||||
"wallpaper": "https://gitlab.com/tenrecha/recha/-/raw/main/background.jpg",
|
||||
"notice": "115.07.18_Ver.2 最終階段測試",
|
||||
"sites": [
|
||||
{
|
||||
"key" : "威視.py",
|
||||
"name" : "待測|威視",
|
||||
"type" : 3,
|
||||
"api" : "./py/威视TV.py",
|
||||
"searchable" : 1,
|
||||
"quickSearch": 1,
|
||||
"filterable" : 0,
|
||||
"changeable" : 0
|
||||
},
|
||||
{
|
||||
"key": "大眾",
|
||||
"name": "大眾影視|1080P",
|
||||
"type": 3,
|
||||
"api": "./py/大众.py",
|
||||
"searchable": 1,
|
||||
"quickSearch": 1
|
||||
},
|
||||
{
|
||||
"key": "花子",
|
||||
"name": "花子動漫|1080P",
|
||||
"type": 3,
|
||||
"api": "./py/huazi.py",
|
||||
"searchable": 1,
|
||||
"quickSearch": 1
|
||||
},
|
||||
{
|
||||
"key": "豆瓣",
|
||||
"name": "🇹🇼|熱播排行榜|自動切換",
|
||||
|
||||
Reference in New Issue
Block a user