上传文件至「xbpq」
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys, re, base64, json, requests, time
|
||||
from base.spider import Spider
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote, urljoin
|
||||
from urllib3.util.retry import Retry
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend="{}"):
|
||||
origin = 'https://zh.stripchat.com'
|
||||
self.host = origin
|
||||
self.Doppiocdn = "doppiocdn.org"
|
||||
#domains = [
|
||||
# "doppiocdn.com", # cf cdn只能图片用,播放不了,可能触发验证码风控
|
||||
# "doppiocdn.org", # 靠谱云cdn,国内有节点
|
||||
# "doppiocdn.net" # cft cdn
|
||||
#]
|
||||
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0"
|
||||
self.headers = {'Origin': origin, 'Referer': f"{origin}/", 'User-Agent': user_agent, "Accept-Language": "zh,en;q=0.5"}
|
||||
self.stripchat_preferredVideoCodec = "H265"
|
||||
self.stripchat_key = "YzWScuyQRGAGcxx1KIJmiQ7BY9Vi35ftwLqUOVO8uoo="
|
||||
self.stripchat_pkey = "Fq6m2TO2ZeBkRPm9"
|
||||
self.stripchat_play='0 0'
|
||||
self.create_session_with_retry()
|
||||
|
||||
def getName(self): return "StripChat"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def normalize_username_for_hdstream(self, username):
|
||||
return username.replace('-', '_').lower()
|
||||
|
||||
def homeContent(self, filter):
|
||||
CLASSES = [{'type_name': '女主播g', 'type_id': 'girls'}, {'type_name': '情侣c', 'type_id': 'couples'}, {'type_name': '男主播m', 'type_id': 'men'}, {'type_name': '跨性别t', 'type_id': 'trans'}]
|
||||
VALUE = [{'n': '中国', 'v': 'tagLanguageChinese'}, {'n': '亚洲', 'v': 'ethnicityAsian'}, {'n': '白人', 'v': 'ethnicityWhite'}, {'n': '拉丁', 'v': 'ethnicityLatino'}, {'n': '混血', 'v': 'ethnicityMultiracial'}, {'n': '印度', 'v': 'ethnicityIndian'}, {'n': '阿拉伯', 'v': 'ethnicityMiddleEastern'}, {'n': '黑人', 'v': 'ethnicityEbony'}]
|
||||
VALUE_MEN = [{'n': '情侣', 'v': 'sexGayCouples'}, {'n': '直男', 'v': 'orientationStraight'}]
|
||||
TIDS = ('girls', 'couples', 'men', 'trans')
|
||||
filters = {tid: [{'key': 'tag', 'value': VALUE_MEN + VALUE if tid == 'men' else VALUE}] for tid in TIDS}
|
||||
return {'class': CLASSES, 'filters': filters}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
# 🔥 修复:明确定义limit变量
|
||||
limit = 60
|
||||
offset = limit * (int(pg) - 1)
|
||||
url = f"{self.host}/api/front/models?improveTs=false&removeShows=false&limit={limit}&offset={offset}&primaryTag={tid}&sortBy=stripRanking&rcmGrp=A&rbCnGr=true&prxCnGr=false&nic=false"
|
||||
if 'tag' in extend: url += f'&filterGroupTags=[["{extend["tag"]}"]]'
|
||||
rsp = self.session_get(url).json()
|
||||
videos = [{"vod_id": str(v['username']), "vod_name": f"{self.country_code_to_flag(str(v['country']))}{v['username']}", "vod_pic": f"https://img.{self.Doppiocdn}/snapshot/{v['id']}/{v['snapshotTimestamp']}", "vod_remarks": "" if v.get('status') == "public" else "🎫"} for v in rsp.get('models', [])]
|
||||
total = int(rsp.get('filteredCount', 0))
|
||||
return {"list": videos, "page": pg, "pagecount": (total + limit - 1) // limit, "limit": limit, "total": total}
|
||||
|
||||
def detailContent(self, array):
|
||||
username = array[0]
|
||||
|
||||
try:
|
||||
rsp = self.session_get(f"{self.host}/api/front/v2/models/username/{username}/cam").json()
|
||||
info, user = rsp['cam'], rsp['user']['user']
|
||||
uid, isLive = str(user['id']), user['isLive']
|
||||
oldName = self.stripchat_play.rsplit(' ', 1)[-1]
|
||||
if username != oldName:
|
||||
timestp = int(time.time())
|
||||
self.stripchat_play = f"0 {timestp} {username}"
|
||||
flag = self.country_code_to_flag(str(user['country']).strip())
|
||||
remark = "🔴 直播中" if isLive else "⚫ 已下播"
|
||||
show = info.get('show') or info.get('groupShowAnnouncement')
|
||||
if show:
|
||||
startAt = show.get('createdAt') or show.get('startAt')
|
||||
if startAt: remark = f"🎫 始于 {(datetime.strptime(startAt, '%Y-%m-%dT%H:%M:%SZ') + timedelta(hours=8)).strftime('%m月%d日 %H:%M')}"
|
||||
director = f"{flag}{username}"
|
||||
return {'list': [{"vod_id": username, "vod_name": str(info['topic'])[:80], "vod_pic": str(user['avatarUrl']), "vod_director": director, "vod_remarks": remark, 'vod_play_from': 'StripChat$$$LemonCams', 'vod_play_url': f"{uid}${uid}$$${uid}$lemon_{uid}"}]}
|
||||
except: return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
if int(pg) > 1: return {}
|
||||
tags = {'G': 'girls', 'C': 'couples', 'M': 'men', 'T': 'trans'}
|
||||
parts = key.split(maxsplit=1)
|
||||
tag, key = (tags.get(parts[0].upper()), parts[1].strip()) if len(parts) > 1 and parts[0].upper() in tags else ('girls', key.strip())
|
||||
rsp = self.session_get(f"{self.host}/api/front/v4/models/search/group/username?query={key}&limit=900&primaryTag={tag}").json()
|
||||
return {'list': [{"vod_id": str(u['username']), "vod_name": f"{self.country_code_to_flag(str(u['country']))}{u['username']}", "vod_pic": f"https://img.{self.Doppiocdn}/snapshot/{u['id']}/{u['snapshotTimestamp']}", "vod_remarks": "" if u['status'] == "public" else "🎫"} for u in rsp.get('models', []) if u['isLive']]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if id.startswith('lemon'):
|
||||
id = id.split('_')[1]
|
||||
rsp = self.session_get(f"https://edge-hls.growcdnssedge.com/hls/{id}/master/{id}_auto.m3u8?playlistType=lowLatency").text
|
||||
lines = rsp.strip().split('\n')
|
||||
urls = []
|
||||
for i, line in enumerate(lines):
|
||||
if '#EXT-X-STREAM-INF' in line:
|
||||
qn_start = line.find('NAME="')+6
|
||||
qn = line[qn_start:line.find('"', qn_start)]
|
||||
url = lines[i + 1]
|
||||
urls.extend([qn, url])
|
||||
lemon_headers = {
|
||||
'User-Agent': self.headers.get('User-Agent'),
|
||||
'Origin': 'https://www.lemoncams.com',
|
||||
'Referer': 'https://www.lemoncams.com/'
|
||||
}
|
||||
return {"url": urls, "parse": '0', "header": lemon_headers}
|
||||
|
||||
try:
|
||||
rsp = self.session_get(f"https://edge-hls.{self.Doppiocdn}/hls/{id}/master/{id}_auto.m3u8?playlistType=lowLatency").text
|
||||
lines = rsp.strip().split('\n')
|
||||
psch, pkey, urls, processed = 'v2', self.stripchat_pkey, [], False
|
||||
for i, line in enumerate(lines):
|
||||
#if line.startswith('#EXT-X-MOUFLON:') and not processed:
|
||||
# if len(parts := line.split(':')) >= 4: psch, pkey, processed = parts[2], parts[3], True
|
||||
if '#EXT-X-STREAM-INF' in line:
|
||||
qn_start = line.find('NAME="')+6
|
||||
qn = line[qn_start:line.find('"', qn_start)]
|
||||
full_url = f"{lines[i+1]}&psch={psch}&pkey={pkey}&preferredVideoCodec={self.stripchat_preferredVideoCodec}"
|
||||
urls.extend([qn, f"{self.getProxyUrl()}&url={quote(full_url)}"])
|
||||
headers = self.headers.copy()
|
||||
headers.pop('Accept-Language', None)
|
||||
return {"url": urls, "parse": '0', "header": headers}
|
||||
except: return {"url": [], "parse": 0}
|
||||
|
||||
def update_vod(self, username):
|
||||
content_data = self.detailContent([username]).get('list')[0]
|
||||
#content_data.pop('vod_id')
|
||||
payload = {"json": json.dumps(content_data)}
|
||||
self.post("http://127.0.0.1:9978/action?do=refresh&type=vod", data=payload)
|
||||
|
||||
def localProxy(self, param):
|
||||
url, type = param['url'], param.get('type', '')
|
||||
if type == 'rec_img':
|
||||
data = self.session_get(url, self.search_headers)
|
||||
return [200, 'application/octet-stream', data.content]
|
||||
rsp = self.session_get(url)
|
||||
oldCode, oldtmp, username = self.stripchat_play.rsplit(' ')
|
||||
timestp = int(time.time())
|
||||
is_time_up = (timestp - 10) > int(oldtmp)
|
||||
is_code_changed = (int(oldCode) != 0 and rsp.status_code != int(oldCode))
|
||||
if is_time_up or is_code_changed:
|
||||
self.stripchat_play = f"{rsp.status_code} {timestp} {username}"
|
||||
self.log('计划更新')
|
||||
self.update_vod(username)
|
||||
if is_code_changed:
|
||||
self.log('code变更')
|
||||
self.post("http://127.0.0.1:9978/action?do=refresh&type=player")
|
||||
return [404, "text/plain", ""]
|
||||
if rsp.status_code == 403: rsp = self.session_get(re.sub(r'(_\d+p\d*)?\.m3u8', '_160p_blurred.m3u8', url))
|
||||
if rsp.status_code != 200: return [404, "text/plain", ""]
|
||||
data = self.process_m3u8(rsp.text) if "#EXT-X-MOUFLON:URI:" in rsp.text else rsp.text
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
URL_PATTERN = re.compile(r'https://media-hls\.doppiocdn\.\w+/b-hls-\d+/media\.mp4')
|
||||
def process_m3u8(self, content):
|
||||
lines = content.strip().split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('#EXT-X-MOUFLON:URI:') and 'media.mp4' in lines[i+1]:
|
||||
mouflon = line.split(':', 2)[2].strip()
|
||||
encrypted = re.sub(r'(_part\d+)?\.mp4$', '', mouflon).rsplit('_', 2)[1]
|
||||
lines[i+1] = self.URL_PATTERN.sub(mouflon.replace(encrypted, self._decode(encrypted[::-1], self.stripchat_key)), lines[i+1])
|
||||
return '\n'.join(lines)
|
||||
|
||||
def country_code_to_flag(self, code):
|
||||
return ''.join(chr(ord(c.upper()) - ord('A') + 0x1F1E6) for c in code) if len(code) == 2 and code.isalpha() else code
|
||||
|
||||
def _decode(self, encrypted_b64: str, key_b64: str) -> str:
|
||||
# 补齐Base64填充,避免Incorrect padding错误
|
||||
missing_padding = len(encrypted_b64) % 4
|
||||
if missing_padding:
|
||||
encrypted_b64 += '=' * (4 - missing_padding)
|
||||
key_bytes = base64.b64decode(key_b64)
|
||||
encrypted = base64.b64decode(encrypted_b64)
|
||||
decrypted = bytearray(len(encrypted))
|
||||
for i in range(len(encrypted)):
|
||||
decrypted[i] = encrypted[i] ^ (key_bytes[i % len(key_bytes)] & 0xFF)
|
||||
return decrypted.decode('utf-8')
|
||||
|
||||
def create_session_with_retry(self):
|
||||
self.session = requests.Session()
|
||||
retry = Retry(total=5, backoff_factor=0.3, status_forcelist=[429, 500, 502, 503, 504], raise_on_status=False)
|
||||
adapter = requests.adapters.HTTPAdapter(max_retries=retry, pool_connections=100, pool_maxsize=100, pool_block=False)
|
||||
self.session.mount('http://', adapter)
|
||||
self.session.mount('https://', adapter)
|
||||
|
||||
def session_get(self, url, headers=None, stream=False): return self.session.get(url, headers = self.headers if headers is None else headers, timeout=5, stream=stream, allow_redirects = True)
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"作者": "网络",
|
||||
"站名": "51吃瓜擦边短剧",
|
||||
"编码": "UTF-8",
|
||||
"请求头": "User-Agent$MOBILE_UA",
|
||||
"主页url": "https://hwm6z4.ryhvlsd.com/category/cbdj/",
|
||||
"首页": "200",
|
||||
"起始页": "0",
|
||||
"分类url": "https://hwm6z4.ryhvlsd.com/category/{cateId}/{catePg}/[https://hwm6z4.ryhvlsd.com/category/{cateId}/];;mrcRAz",
|
||||
"分类": "擦边短剧$cbdj",
|
||||
"数组": "url mainEntityOfPage\"&&</a[不包含:旗士]",
|
||||
"图片": "https://raw.giteeusercontent.com/xianluyuan/WP/raw/master/image_download_1767716901851.jpg?metadata=eyJyIjoibWFzdGVyIiwiZnAiOiJpbWFnZV9kb3dubG9hZF8xNzY3NzE2OTAxODUxLmpwZyIsInVpZCI6MTYwMTU2NzEsInBpZCI6NDMxOTI0MDgsInN0byI6ImdpdC1zaGFyZGluZy1zdG8tMTB0LTA0MSIsInJwIjoicmVwb3MvYWEvZWEvYWFlYWJmM2IxODE0YjJjNjc0OTcyODM3ZGU2YThjZDkxYjc1MjYxZjUxNWJhNTg5OGIyOGI3YmZjZjA0NTgzMy5naXQiLCJpc3AiOnRydWUsImV4cGlyZV9hdCI6MTc4MDc1NjgwMH0&signature=cEOlUwSkvyXHiEVgFLk9cgEauGonjpnvdQALP9qkPsc",
|
||||
//"图片": "loadBannerDirect('&&'",
|
||||
"标题": "itemprop=\"headline\">&&</h2>",
|
||||
"副标题": "💗悠着点欣赏💗",
|
||||
"链接": "<a href=\"&&\"",
|
||||
"简介": "请不要相信任何广告!!!祝您观影愉快!",
|
||||
"跳转播放链接": "video\":{\"url\":\"&&\"",
|
||||
"搜索url": "https://hwm6z4.ryhvlsd.com/category/id/{pg}/?wd={wd}",
|
||||
"搜索模式": "1"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"作者": "集",
|
||||
"站名": "51吃瓜擦边短剧",
|
||||
"编码": "UTF-8",
|
||||
"请求头": "User-Agent$MOBILE_UA",
|
||||
"主页url": "https://hwm6z4.ryhvlsd.com/category/cbdj/",
|
||||
"首页": "200",
|
||||
"起始页": "0",
|
||||
"分类url": "https://hwm6z4.ryhvlsd.com/category/{cateId}/{catePg}/[https://hwm6z4.ryhvlsd.com/category/{cateId}/];;mrcRAz",
|
||||
"分类": "擦边短剧$cbdj",
|
||||
"数组": "url mainEntityOfPage\"&&</a[不包含:旗士]",
|
||||
"图片": "https://raw.giteeusercontent.com/xianluyuan/WP/raw/master/image_download_1767716901851.jpg?metadata=eyJyIjoibWFzdGVyIiwiZnAiOiJpbWFnZV9kb3dubG9hZF8xNzY3NzE2OTAxODUxLmpwZyIsInVpZCI6MTYwMTU2NzEsInBpZCI6NDMxOTI0MDgsInN0byI6ImdpdC1zaGFyZGluZy1zdG8tMTB0LTA0MSIsInJwIjoicmVwb3MvYWEvZWEvYWFlYWJmM2IxODE0YjJjNjc0OTcyODM3ZGU2YThjZDkxYjc1MjYxZjUxNWJhNTg5OGIyOGI3YmZjZjA0NTgzMy5naXQiLCJpc3AiOnRydWUsImV4cGlyZV9hdCI6MTc4MDc1NjgwMH0&signature=cEOlUwSkvyXHiEVgFLk9cgEauGonjpnvdQALP9qkPsc",
|
||||
"标题": "《&&》",
|
||||
"副标题": "<span itemprop=\"datePublished*>&&<",
|
||||
"链接": "<a href=\"&&\"",
|
||||
"简介": "请不要相信任何广告!!!祝您观影愉快!集为您介绍剧情👉-+/blockquote><p>&&</p><p><div class=\"dplayer",
|
||||
"跳转播放链接": "video\":{\"url\":\"&&\"",
|
||||
"搜索url": "https://hwm6z4.ryhvlsd.com/category/id/{pg}/?wd={wd}",
|
||||
"搜索模式": "1"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"主页url": "https://www.555k7.com/vodtype/126.html",
|
||||
"分类url": "https://www.555k7.com/vodshow/{cateId}-{area}-{by}-{class}-{lang}----{catePg}---{year}.html",
|
||||
"分类": "擦边短剧$126#",
|
||||
"剧情": "古代&现代&穿越&玄幻&霸总&英雄救美&未婚妻&师姐&绝美&逆袭&幻想&美女&爱情&甜宠&虐恋&爽剧&搞笑&情感&动漫&萌宝&抖音&快手&都市&言情&重生&乡村&神医",
|
||||
"数组": "p:div[class*=\"module-poster-items-base\"] a",
|
||||
"标题": "p:a->title",
|
||||
"副标题": "p:div[class*=\"module-item-cover\"]->text+p:div[class*=\"module-item-cover\"]->text[序号:2]",
|
||||
"图片": "p:img->data-original",
|
||||
"链接": "p:a->href",
|
||||
"影片状态": "更新:&&</div>",
|
||||
"影片年代": "p:div[class*=\"module-info-tag-link\"]->text",
|
||||
"影片类型": "p:div[class*=\"module-info-tag-link\"]->text[含序号:3]",
|
||||
"影片地区": "p:div[class*=\"module-info-tag-link\"]->text[含序号:2]",
|
||||
"导演": "p:div[class*=\"module-info-item\"] span->text[含序号:3][替换:导演:>>空]",
|
||||
"主演": "p:div[class*=\"module-info-item\"] span->text[含序号:4][替换:主演:>>空]",
|
||||
"简介": "p:div[class*=\"module-info-heading\"]->text",
|
||||
"线路数组": "p:div[class*=\"module-tab-items-box\"] div",
|
||||
"线路标题": "p:span->text+(+p:small->text+)",
|
||||
"播放数组": "p:div[class*=\"module-play-list\"]",
|
||||
"播放列表": "p:a",
|
||||
"播放标题": "p:span->text",
|
||||
"播放链接": "p:a->href"
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import gzip
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import requests
|
||||
|
||||
try:
|
||||
from base.spider import Spider as BaseSpider
|
||||
except Exception:
|
||||
class BaseSpider:
|
||||
pass
|
||||
|
||||
class _AESCBC:
|
||||
@staticmethod
|
||||
def encrypt(data, key, iv):
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
return AES.new(key, AES.MODE_CBC, iv).encrypt(_AESCBC.pad(data))
|
||||
except Exception:
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
enc = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()).encryptor()
|
||||
return enc.update(_AESCBC.pad(data)) + enc.finalize()
|
||||
|
||||
@staticmethod
|
||||
def decrypt(data, key, iv):
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
plain = AES.new(key, AES.MODE_CBC, iv).decrypt(data)
|
||||
except Exception:
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
dec = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()).decryptor()
|
||||
plain = dec.update(data) + dec.finalize()
|
||||
return _AESCBC.unpad(plain)
|
||||
|
||||
@staticmethod
|
||||
def pad(data):
|
||||
n = 16 - len(data) % 16
|
||||
return data + bytes([n]) * n
|
||||
|
||||
@staticmethod
|
||||
def unpad(data):
|
||||
n = data[-1] if data else 0
|
||||
return data[:-n] if 1 <= n <= 16 else data
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
self.host = "https://xqjzvcvt.top"
|
||||
self.api = self.host + "/api"
|
||||
self.name = "黄豆短剧"
|
||||
self.platform_key = "7961beb44246e3012ce228d6b5ced05a"
|
||||
self.version = "2.0.0"
|
||||
self.device_type = "web"
|
||||
self.session_id = uuid.uuid4().hex
|
||||
self.device_id = self.session_id
|
||||
self.token = ""
|
||||
self.headers = {"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", "Accept": "*/*", "Origin": self.host, "Referer": self.host + "/home", "Content-Type": "application/octet-stream"}
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.class_cache = None
|
||||
self.filter_cache = {}
|
||||
|
||||
def init(self, extend=""):
|
||||
if extend:
|
||||
try:
|
||||
cfg = json.loads(extend)
|
||||
self.host = (cfg.get("site") or cfg.get("base_url") or self.host).rstrip("/")
|
||||
self.api = self.host + "/api"
|
||||
self.token = cfg.get("token", self.token)
|
||||
self.headers["Origin"] = self.host
|
||||
self.headers["Referer"] = self.host + "/home"
|
||||
self.session.headers.update(self.headers)
|
||||
except Exception:
|
||||
None
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self._api("/drama/list", {"page": "1", "page_size": "18"})
|
||||
classes = self._classes()
|
||||
return {"class": classes, "filters": self._filters(classes), "list": [self._vod(x) for x in self._list(data)], "parse": 0, "jx": 0}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
extend = extend or {}
|
||||
if tid == "yuandou":
|
||||
data = self._api("/drama/navBlock", {"code": "yuandou", "tab": "recommend", "page": str(pg)})
|
||||
items = self._nav_items(data)
|
||||
else:
|
||||
req = {"page": str(pg), "page_size": "18"}
|
||||
if tid and tid not in ("all", "recommend"):
|
||||
tabs = self._nav_filter(tid)
|
||||
idx = self._int(extend.get("sub"), 0)
|
||||
sub = tabs[idx] if tabs and 0 <= idx < len(tabs) else {}
|
||||
flt = sub.get("filter", {}) if isinstance(sub, dict) else {}
|
||||
req["cat_id"] = flt.get("cat_id", "")
|
||||
if flt.get("tag_id"):
|
||||
req["tag_id"] = flt.get("tag_id", "")
|
||||
req["order"] = flt.get("order", "") or extend.get("order", "")
|
||||
elif extend.get("order"):
|
||||
req["order"] = extend.get("order")
|
||||
if extend.get("update_status"):
|
||||
req["update_status"] = extend.get("update_status")
|
||||
data = self._api("/drama/list", req)
|
||||
items = self._list(data)
|
||||
return {"page": int(pg), "pagecount": int(pg) if len(items) < 18 else int(pg) + 1, "limit": 18, "total": 99999, "list": [self._vod(x) for x in items], "parse": 0, "jx": 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = str(ids[0]).replace("rp_", "")
|
||||
obj = self._api("/drama/detail", {"id": vid})
|
||||
data = obj.get("data", obj) if isinstance(obj, dict) else {}
|
||||
if not isinstance(data, dict):
|
||||
return {"list": []}
|
||||
data = self._unlock(data)
|
||||
vod_id = self._sid(data.get("id") or data.get("drama_id") or vid)
|
||||
name = data.get("name") or data.get("title") or data.get("t") or vod_id
|
||||
eps = data.get("episodes") if isinstance(data.get("episodes"), list) else []
|
||||
count = self._int(data.get("episode_count") or data.get("free_episodes"), len(eps) or 1)
|
||||
play = []
|
||||
if eps:
|
||||
for i, ep in enumerate(eps, 1):
|
||||
seq = ep.get("seq") or ep.get("episode") or ep.get("ep") or i
|
||||
play.append("%s$%s|%s" % (ep.get("name") or ep.get("title") or "第%s集" % seq, vod_id, seq))
|
||||
else:
|
||||
play = ["第%s集$%s|%s" % (i, vod_id, i) for i in range(1, count + 1)]
|
||||
vod = {"vod_id": vod_id, "vod_name": name, "vod_pic": self._pic(data), "type_name": data.get("category") or data.get("type") or "", "vod_year": "", "vod_area": "", "vod_remarks": data.get("update_label") or "全%s集" % count, "vod_actor": "", "vod_director": "", "vod_content": data.get("description") or data.get("summary") or name, "vod_play_from": self.name, "vod_play_url": "#".join(play)}
|
||||
return {"list": [vod], "parse": 0, "jx": 0}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self._api("/drama/list", {"page": str(pg), "page_size": "18", "keywords": str(key)})
|
||||
items = self._list(data)
|
||||
return {"page": int(pg), "pagecount": int(pg) if len(items) < 18 else int(pg) + 1, "limit": 18, "total": 99999, "list": [self._vod(x) for x in items], "parse": 0, "jx": 0}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
vid, seq = self._split(id)
|
||||
obj = self._api("/drama/play", {"id": vid, "seq": str(seq)}, True)
|
||||
data = obj.get("data", {}) if isinstance(obj, dict) else {}
|
||||
url = data.get("m3u8") or data.get("url") or self._hls(vid, seq)
|
||||
return {"parse": 0, "playUrl": "", "url": url, "jx": 0, "header": {"User-Agent": self.headers["User-Agent"], "Referer": self.host + "/home", "Origin": self.host}}
|
||||
|
||||
def _api(self, path, data=None, silent=False):
|
||||
path = "/" + path.lstrip("/")
|
||||
rid = str(uuid.uuid4())
|
||||
key = self._key(rid)
|
||||
iv = os.urandom(16)
|
||||
raw = json.dumps({"token": self.token or "", "deviceId": self.device_id, "data": data or {}}, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
body = iv + _AESCBC.encrypt(gzip.compress(raw), key, iv)
|
||||
ts = int(time.time())
|
||||
sign = hashlib.sha256(("Dart|%s|%s|%s|%s" % (self.session_id, rid, ts, path)).encode("utf-8")).hexdigest() + "-" + str(ts)
|
||||
h = dict(self.headers)
|
||||
h.update({"version": self.version, "deviceType": self.device_type, "time": str(ts), "sign": sign, "requestId": rid, "sessionId": self.session_id, "deviceBrand": "", "deviceModel": "", "systemName": "", "systemVersion": ""})
|
||||
try:
|
||||
r = self.session.post(self.api + path, data=body, headers=h, timeout=20, verify=False)
|
||||
r.raise_for_status()
|
||||
return self._decode(r.content, rid)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _key(self, rid):
|
||||
return hmac.new(self.platform_key.encode("utf-8"), bytes.fromhex(str(rid).replace("-", "")), hashlib.sha256).digest()
|
||||
|
||||
def _decode(self, blob, rid):
|
||||
if not blob or len(blob) < 32 or (len(blob) - 16) % 16 != 0:
|
||||
try:
|
||||
return json.loads(blob.decode("utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
plain = _AESCBC.decrypt(blob[16:], self._key(rid), blob[:16])
|
||||
if plain[:2] == b"\x1f\x8b":
|
||||
plain = gzip.decompress(plain)
|
||||
return json.loads(plain.decode("utf-8"))
|
||||
|
||||
def _classes(self):
|
||||
if self.class_cache:
|
||||
return self.class_cache
|
||||
arr = [{"type_id": "all", "type_name": "全部短剧"}]
|
||||
data = self._api("/drama/navList", {})
|
||||
for item in self._list(data.get("data", data) if isinstance(data, dict) else data):
|
||||
tid = str(item.get("code") or item.get("id") or item.get("cat_id") or "")
|
||||
name = item.get("name") or item.get("title") or tid
|
||||
if tid and name:
|
||||
arr.append({"type_id": tid, "type_name": name})
|
||||
self.class_cache = arr
|
||||
return arr
|
||||
|
||||
def _filters(self, classes):
|
||||
common = [{"key": "order", "name": "排序", "value": [{"n": "默认", "v": ""}, {"n": "最新", "v": "new"}, {"n": "最热", "v": "hot"}]}, {"key": "update_status", "name": "状态", "value": [{"n": "全部", "v": ""}, {"n": "连载", "v": "0"}, {"n": "完结", "v": "1"}]}]
|
||||
fs = {}
|
||||
for c in classes:
|
||||
tid = c["type_id"]
|
||||
tabs = self._nav_filter(tid) if tid not in ("all", "yuandou") else []
|
||||
fs[tid] = ([{"key": "sub", "name": "子分类", "value": [{"n": t.get("name", "默认"), "v": str(i)} for i, t in enumerate(tabs)]}] if tabs else []) + common
|
||||
return fs
|
||||
|
||||
def _nav_filter(self, code):
|
||||
if code not in self.filter_cache:
|
||||
data = self._api("/drama/navFilter", {"code": str(code)})
|
||||
self.filter_cache[code] = self._list(data.get("data", data) if isinstance(data, dict) else data)
|
||||
return self.filter_cache.get(code, [])
|
||||
|
||||
def _list(self, data):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
if not isinstance(data, dict):
|
||||
return []
|
||||
if isinstance(data.get("list"), list):
|
||||
return data["list"]
|
||||
if isinstance(data.get("items"), list):
|
||||
return data["items"]
|
||||
if isinstance(data.get("data"), list):
|
||||
return data["data"]
|
||||
if isinstance(data.get("data"), dict):
|
||||
return self._list(data["data"])
|
||||
return []
|
||||
|
||||
def _nav_items(self, data):
|
||||
blocks = self._list(data.get("data", data) if isinstance(data, dict) else data)
|
||||
items = []
|
||||
for b in blocks:
|
||||
if isinstance(b, dict) and isinstance(b.get("items"), list):
|
||||
items += b.get("items")
|
||||
elif isinstance(b, dict) and (b.get("id") or b.get("drama_id")):
|
||||
items.append(b)
|
||||
return items
|
||||
|
||||
def _vod(self, item):
|
||||
item = item or {}
|
||||
vid = self._sid(item.get("id") or item.get("drama_id") or "")
|
||||
remarks = item.get("update_label") or item.get("corner") or ("全%s集" % item.get("episode_count") if item.get("episode_count") else "")
|
||||
return {"vod_id": vid, "vod_name": item.get("name") or item.get("title") or item.get("t") or vid, "vod_pic": self._pic(item), "vod_remarks": remarks}
|
||||
|
||||
def _pic(self, item):
|
||||
return item.get("img_y") or item.get("img_x") or item.get("img") or item.get("cover") or item.get("pic") or ""
|
||||
|
||||
def _unlock(self, d):
|
||||
eps = d.get("episodes")
|
||||
if isinstance(eps, list):
|
||||
for ep in eps:
|
||||
if isinstance(ep, dict):
|
||||
ep["is_buy"] = True
|
||||
ep["type"] = "free"
|
||||
ep["price"] = 0
|
||||
ep["methods"] = []
|
||||
d.update({"pay_type": "free", "money": 0, "episode_price": 0, "points_price": 0, "can_vip_watch": True, "is_buy_whole": True, "vip_episodes": [], "coin_episodes": [], "points_episodes": []})
|
||||
return d
|
||||
|
||||
def _sid(self, x):
|
||||
return str(x or "").replace("rp_", "")
|
||||
|
||||
def _split(self, x):
|
||||
p = str(x).split("|", 1)
|
||||
return self._sid(p[0]), p[1] if len(p) > 1 and p[1] else "1"
|
||||
|
||||
def _hls(self, vid, seq):
|
||||
return "%s/api/drama/hls/%s/%s/play.m3u8?line=free" % (self.host, self._sid(vid), seq)
|
||||
|
||||
def _int(self, x, d=0):
|
||||
try:
|
||||
return int(x)
|
||||
except Exception:
|
||||
return d
|
||||
Reference in New Issue
Block a user