Sync all projects
This commit is contained in:
+333
@@ -0,0 +1,333 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author : 星河
|
||||
# @Time : 2026.03.17 18:34
|
||||
# @file : symx.py
|
||||
# 山有木兮 (symx.club) 爬虫 - OK影视/TVBox Python版本
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import hmac
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
from base.spider import Spider as BaseSpider
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "山有木兮"
|
||||
self.host = "https://film.symx.club"
|
||||
# XOR解密密钥(用于解密system/config中的安全参数)
|
||||
self.xor_key = "0x1A2B3C4D5E6F7A8B9C"
|
||||
# 签名消息前缀
|
||||
self.sign_prefix = "symx_"
|
||||
# 签名参数(从config解密得到,默认值)
|
||||
self.sign_header_name = "X-Sign"
|
||||
self.sign_order = "pts" # 默认顺序: path+timestamp+secret
|
||||
self.sign_key = "symx"
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend='{}'):
|
||||
try:
|
||||
ext = json.loads(extend)
|
||||
if 'host' in ext:
|
||||
self.host = ext['host'].rstrip('/')
|
||||
except:
|
||||
pass
|
||||
# 获取安全配置
|
||||
self.fetch_security_config()
|
||||
|
||||
|
||||
|
||||
def xor_decode(self, hex_str):
|
||||
"""XOR解密hex编码的字符串"""
|
||||
if not hex_str:
|
||||
return ""
|
||||
result = []
|
||||
for i in range(0, len(hex_str), 2):
|
||||
val = int(hex_str[i:i + 2], 16)
|
||||
val ^= ord(self.xor_key[(i // 2) % len(self.xor_key)])
|
||||
result.append(chr(val))
|
||||
return ''.join(result)
|
||||
|
||||
def generate_timestamp(self):
|
||||
"""生成带校验位的时间戳"""
|
||||
ts = str(int(time.time() * 1000))
|
||||
prefix = ts[:-1]
|
||||
total = sum(int(c) for c in prefix)
|
||||
return prefix + str(total % 10)
|
||||
|
||||
def generate_sign(self, api_path, timestamp):
|
||||
"""生成HMAC-SHA256签名 - 核心算法"""
|
||||
try:
|
||||
secret = self.sign_prefix + self.sign_key
|
||||
parts = {
|
||||
'p': api_path,
|
||||
't': timestamp,
|
||||
's': secret
|
||||
}
|
||||
# 按sign_order顺序拼接
|
||||
raw = ''.join(parts.get(c, '') for c in self.sign_order)
|
||||
# 字符替换: 1->i, 0->o, 5->s
|
||||
msg = raw.replace('1', 'i').replace('0', 'o').replace('5', 's')
|
||||
# HMAC-SHA256
|
||||
mac = hmac.new(self.sign_key.encode('utf-8'), msg.encode('utf-8'), hashlib.sha256)
|
||||
return mac.hexdigest()
|
||||
except:
|
||||
return ""
|
||||
|
||||
def get_headers(self):
|
||||
"""获取基础请求头"""
|
||||
return {
|
||||
"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": self.host + "/",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"X-Platform": "web"
|
||||
}
|
||||
|
||||
def get_signed_headers(self, api_path):
|
||||
"""获取带签名的请求头(用于需要鉴权的API)"""
|
||||
headers = self.get_headers()
|
||||
ts = self.generate_timestamp()
|
||||
headers["X-Timestamp"] = ts
|
||||
headers[self.sign_header_name] = self.generate_sign(api_path, ts)
|
||||
return headers
|
||||
|
||||
def fetch_security_config(self):
|
||||
"""从/api/system/config获取并解密安全参数"""
|
||||
try:
|
||||
import requests
|
||||
resp = requests.get(self.host + "/api/system/config", headers=self.get_headers(), timeout=10)
|
||||
data = resp.json()
|
||||
if 'data' in data:
|
||||
cfg = data['data']
|
||||
raw_report_id = cfg.get('reportId', '')
|
||||
raw_trace_id = cfg.get('traceId', '')
|
||||
raw_session = cfg.get('session', '')
|
||||
if raw_report_id:
|
||||
self.sign_header_name = self.xor_decode(raw_report_id)
|
||||
if raw_trace_id:
|
||||
self.sign_order = self.xor_decode(raw_trace_id)
|
||||
if raw_session:
|
||||
self.sign_key = self.xor_decode(raw_session)
|
||||
|
||||
requests.get(self.host + "/api/stats/init", headers= self.get_signed_headers("/stats/init"), timeout=10)
|
||||
except Exception as e:
|
||||
print(f"[Symx] 获取安全配置失败: {e}")
|
||||
|
||||
def homeContent(self, filter):
|
||||
import requests
|
||||
result = {
|
||||
'class': [],
|
||||
'filters': {},
|
||||
'list': [],
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
try:
|
||||
# 获取分类
|
||||
cat_url = self.host + "/api/category/top"
|
||||
|
||||
# print(self.get_headers())
|
||||
|
||||
cat_resp = requests.get(cat_url, headers=self.get_headers(), timeout=10).json()
|
||||
|
||||
if 'data' in cat_resp:
|
||||
for item in cat_resp['data']:
|
||||
result['class'].append({
|
||||
'type_id': str(item['id']),
|
||||
'type_name': item['name']
|
||||
})
|
||||
|
||||
# 获取首页推荐
|
||||
film_url = self.host + "/api/film/category"
|
||||
film_resp = requests.get(film_url, headers=self.get_headers(), timeout=10).json()
|
||||
|
||||
if 'data' in film_resp:
|
||||
count = 0
|
||||
for category in film_resp['data']:
|
||||
if count >= 30:
|
||||
break
|
||||
if 'filmList' in category:
|
||||
for film in category['filmList']:
|
||||
if count >= 30:
|
||||
break
|
||||
result['list'].append({
|
||||
'vod_id': str(film['id']),
|
||||
'vod_name': film.get('name', ''),
|
||||
'vod_pic': film.get('cover', ''),
|
||||
'vod_remarks': film.get('doubanScore', '')
|
||||
})
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f"[Symx] homeContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
import requests
|
||||
result = {
|
||||
'list': [],
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
try:
|
||||
url = f"{self.host}/api/film/category/list?area=&categoryId={cid}&language=&pageNum={page}&pageSize=15&sort=updateTime&year="
|
||||
resp = requests.get(url, headers=self.get_headers(), timeout=10).json()
|
||||
|
||||
total = 0
|
||||
if 'data' in resp:
|
||||
data = resp['data']
|
||||
total = data.get('total', 0)
|
||||
if 'list' in data:
|
||||
for item in data['list']:
|
||||
result['list'].append({
|
||||
'vod_id': str(item['id']),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('cover', ''),
|
||||
'vod_remarks': item.get('updateStatus', '')
|
||||
})
|
||||
|
||||
page_count = (total + 14) // 15
|
||||
result['page'] = page
|
||||
result['pagecount'] = page_count
|
||||
result['limit'] = 15
|
||||
result['total'] = total
|
||||
except Exception as e:
|
||||
print(f"[Symx] categoryContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def detailContent(self, did):
|
||||
import requests
|
||||
result = {
|
||||
'list': [],
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
if not did:
|
||||
return result
|
||||
|
||||
vod_id = did[0]
|
||||
|
||||
try:
|
||||
url = self.host + "/api/film/detail?id=" + urllib.parse.quote(vod_id)
|
||||
resp = requests.get(url, headers=self.get_signed_headers("/film/detail"), timeout=10).json()
|
||||
|
||||
if 'data' not in resp:
|
||||
return result
|
||||
|
||||
info = resp['data']
|
||||
vod_name = info.get('name', '')
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': info.get('cover', ''),
|
||||
'vod_year': info.get('year', ''),
|
||||
'vod_area': info.get('other', ''),
|
||||
'vod_actor': info.get('actor', ''),
|
||||
'vod_director': info.get('director', ''),
|
||||
'vod_content': info.get('blurb', ''),
|
||||
'vod_remarks': f"豆瓣: {info.get('doubanScore', '')}" if info.get('doubanScore') else '',
|
||||
'vod_play_from': '',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
|
||||
# 解析播放列表
|
||||
play_froms = []
|
||||
play_urls = []
|
||||
|
||||
if 'playLineList' in info:
|
||||
for line in info['playLineList']:
|
||||
player_name = line.get('playerName', '')
|
||||
play_froms.append(player_name)
|
||||
|
||||
episodes = []
|
||||
if 'lines' in line:
|
||||
for ep in line['lines']:
|
||||
ep_name = ep.get('name', '')
|
||||
ep_id = str(ep['id'])
|
||||
episodes.append(f"{ep_name}${ep_id}")
|
||||
play_urls.append('#'.join(episodes))
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(play_froms)
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls)
|
||||
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
print(f"[Symx] detailContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
import requests
|
||||
result = {
|
||||
'parse': 0,
|
||||
'jx': 0,
|
||||
'url': '',
|
||||
'header': self.get_headers()
|
||||
}
|
||||
|
||||
try:
|
||||
url = self.host + "/api/line/play/parse?lineId=" + urllib.parse.quote(pid)
|
||||
resp = requests.get(url, headers=self.get_signed_headers("/line/play/parse"), timeout=10).json()
|
||||
|
||||
play_url = resp.get('data', '')
|
||||
if play_url:
|
||||
result['url'] = play_url
|
||||
else:
|
||||
result['parse'] = 1
|
||||
result['jx'] = 1
|
||||
except Exception as e:
|
||||
print(f"[Symx] playerContent error: {e}")
|
||||
result['parse'] = 1
|
||||
result['jx'] = 1
|
||||
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
import requests
|
||||
result = {
|
||||
'list': [],
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
try:
|
||||
url = f"{self.host}/api/film/search?keyword={urllib.parse.quote(key)}&pageNum={page}&pageSize=10"
|
||||
resp = requests.get(url, headers=self.get_signed_headers("/film/search"), timeout=10).json()
|
||||
|
||||
if 'data' in resp:
|
||||
data = resp['data']
|
||||
if 'list' in data:
|
||||
for item in data['list']:
|
||||
result['list'].append({
|
||||
'vod_id': str(item['id']),
|
||||
'vod_name': item.get('name', ''),
|
||||
'vod_pic': item.get('cover', ''),
|
||||
'vod_remarks': item.get('updateStatus', ''),
|
||||
'vod_year': item.get('year', '')
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[Symx] searchContent error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
spider = Spider()
|
||||
spider.init('{}')
|
||||
print(json.dumps(spider.homeContent(True), ensure_ascii=False, indent=2))
|
||||
Reference in New Issue
Block a user