Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-25 12:11:32 +00:00
parent 3aee5d0a60
commit f1cf300521
26 changed files with 8829 additions and 5867 deletions
+251
View File
@@ -0,0 +1,251 @@
import cheerio from 'assets://js/lib/cheerio.min.js';
const baseUrl = "https://tv.time1080.xyz";
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const parseAPiUrl = "https://svip.qlplayer.cyou/?url=";
const HEADERS = {
"User-Agent": UA,
};
function mylog(...args) {
console.log(`[拾光影视]`, ...args);
}
function safeJsonParse(json) {
try {
return typeof json === "string" ? JSON.parse(json) : json;
} catch (e) {
return null;
}
}
async function myFetch(url, options = {}, needJsonParse = true) {
try {
let res = await req(url, {
method: options?.method || "get",
...options
})
return needJsonParse ? safeJsonParse(res?.content) : res?.content
} catch (err) {
mylog("myfetch err ", err)
}
}
async function init(cfg) {
mylog("Spider Init Done");
}
async function home(filter) {
}
async function homeVod() {
// 1. 请求 API 接口获取数据
const res = await req('https://tv.time1080.xyz/api/proxy.php?action=home').content;
mylog(res)
// 兼容不同请求工具返回对象或字符串的情况
const data = typeof res === 'string' ? JSON.parse(res) : (res.data || res);
const vodList = [];
// 2. 遍历 categories 下的所有分类 (动作片、动漫、喜剧片等)
if (data && data.categories) {
for (const catName in data.categories) {
const items = data.categories[catName];
if (Array.isArray(items)) {
items.forEach(item => {
vodList.push({
vod_id: item.id ? String(item.id) : '', // 影片ID
vod_name: item.name || '', // 片名
vod_pic: item.pic || '', // 海报图
vod_remarks: item.remarks || '', // 更新状态/备注
vod_blurb: item.content || '', // 简介/摘要
vod_year: item.year || '', // 年份
vod_area: item.area || '', // 地区
type_name: item.type || catName // 分类名称
});
});
}
}
}
// 3. 返回符合猫影视/TVBox 规范的 JSON 结构
return JSON.stringify({
list: vodList
});
}
async function category(tid, pg, filter, extend) {
}
async function detail(id) {
try {
const detailUrl = 'https://tv.time1080.xyz/api/proxy.php?action=detail&source=qilin&id=' + id
mylog("detailUrl ", detailUrl)
const responseData = await myFetch(detailUrl)
if (!responseData) {
return JSON.stringify({ list: [] });
}
let item = responseData?.details?.[0];
if (!item) {
return JSON.stringify({ msg: "responseData 发生错误" })
}
// 从 episodes 中提取播放源名称列表 (例如 "youku$$$qiyi")
let playFrom = "";
if (item.episodes && item.episodes.length > 0) {
playFrom = item.episodes.map(e => e.group).join('$$$');
} else {
playFrom = "播放源1$$$播放源2"; // 备用名称
}
let vod = {
vod_id: item.id.toString(),
vod_name: item.name,
vod_pic: item.pic,
type_name: item.type,
vod_year: item.year,
vod_area: item.area,
vod_lang: item.lang,
vod_director: item.director,
vod_actor: item.actor,
vod_content: item.content,
vod_remarks: item.remarks,
vod_play_from: playFrom, // 播放源名称,如:"youku$$$qiyi"
vod_play_url: item.play_url // 直接复用现成的 play_url
};
return JSON.stringify({
list: [vod]
});
return JSON.stringify({
list: [vod]
});
}
catch (e) {
console.error("detail error: " + e.message);
return JSON.stringify({ list: [] });
}
}
function formatUrl(url) {
if (!url) return "";
return url.replace(/\\/g, "").replace(/^(https?:\/)((?!\/))/i, "$1/");
}
function extractConfig(html) {
const apiTokenMatch = html.match(/apiToken\s*:\s*["']([^"']+)["']/);
return {
apiToken: apiTokenMatch ? apiTokenMatch[1] : null
};
}
// 修复判定逻辑 Bug
function isDirectVideoUrl(url) {
return DIRECT_URL_REG.test(url);
}
async function parseVideoUrl(htmlUrl) {
try {
const resoleUrl = parseAPiUrl + htmlUrl;
mylog("解析地址", resoleUrl);
const html1 = (await req(resoleUrl)).content || "";
const { apiToken } = extractConfig(html1);
if (!apiToken) return "";
const parseTokenUrl = `https://svip.qlplayer.cyou/api/resolve.php?token=${encodeURIComponent(apiToken)}`;
mylog("parseTokenUrl", parseTokenUrl);
const res = await req(parseTokenUrl);
const data = JSON.parse(res.content);
mylog("data", data);
const finalUrl = formatUrl(data.url);
mylog("finalUrl", finalUrl);
return finalUrl;
} catch (e) {
mylog("视频解析失败:", e.message);
return "";
}
}
/**
* OK影视 search 函数
* @param {string} kw - 搜索关键字
* @param {boolean} quick - 是否快速搜索
* @param {string} pg - 页码
* @returns {string} JSON 格式的搜索结果列表
*/
async function search(kw, quick, pg) {
try {
const url = `https://tv.time1080.xyz/api/proxy.php?action=search&wd=${encodeURIComponent(kw)}&page=1&source=qilin,mj`
let responseData = await myFetch(url)
if (!responseData || !responseData.results || responseData.results.length === 0) {
return JSON.stringify({ list: [] });
}
// 将源数据映射转换为 OK 影视标准的 vod 简短列表格式
let vodList = responseData.results.map(item => {
return {
vod_id: item.id.toString(), // 影片ID
vod_name: item.name, // 影片名称
vod_pic: item.pic, // 封面图
vod_remarks: item.remarks, // 状态/备注 (例如: 更新至150集)
type_name: item.type // 类型 (例如: 动漫)
};
});
return JSON.stringify({
page: parseInt(pg) || 1,
pagecount: 1,
limit: vodList.length,
total: vodList.length,
list: vodList
});
} catch (e) {
console.error("search error: " + e.message);
return JSON.stringify({ list: [] });
}
}
async function play(flag, id, flags) {
mylog(`开始获取播放地址: ${id}`);
const finalUrl = await parseVideoUrl(id)
try {
return JSON.stringify({ parse: 0, url: finalUrl })
} catch (e) {
mylog(`网络请求失败: ${e.message}`);
}
return JSON.stringify({ parse: 0, url: "" });
}
export default {
init,
home,
homeVod,
category,
detail,
search,
play
};
+262
View File
@@ -0,0 +1,262 @@
# -*- coding: utf-8 -*-
import json
import sys
import time
import uuid
from base64 import b64decode, b64encode
from concurrent.futures import ThreadPoolExecutor, as_completed
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, MD5
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Util.Padding import unpad
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host, self.appKey, self.rsakey = self.userinfo()
pass
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
def homeContent(self, filter):
data = self.fetch(f"{self.host}/api.php/zjv6.vod/types", headers=self.getheader()).json()
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序", }
filters = {}
classes = []
json_data = data['data']['list']
for item in json_data:
has_non_empty_field = False
jsontype_extend = item["type_extend"]
jsontype_extend['by'] = '按更新,按播放,按评分,按收藏'
classes.append({"type_name": item["type_name"], "type_id": item["type_id"]})
for key in dy:
if key in jsontype_extend and jsontype_extend[key].strip() != "":
has_non_empty_field = True
break
if has_non_empty_field:
filters[str(item["type_id"])] = []
for dkey in jsontype_extend:
if dkey in dy and jsontype_extend[dkey].strip() != "":
values = jsontype_extend[dkey].split(",")
sl = {'按更新': 'time', '按播放': 'hits', '按评分': 'score', '按收藏': 'store_num'}
value_array = [
{"n": value.strip(), "v": sl[value.strip()] if dkey == "by" else value.strip()}
for value in values
if value.strip() != ""
]
filters[str(item["type_id"])].append(
{"key": dkey, "name": dy[dkey], "value": value_array}
)
result = {"class": classes, "filters": filters}
return result
def homeVideoContent(self):
data = self.fetch(f"{self.host}/api.php/zjv6.vod/vodPhbAll", headers=self.getheader()).json()
return {'list': data['data']['list'][0]['vod_list']}
def categoryContent(self, tid, pg, filter, extend):
params = {
"type": tid,
"class": extend.get('class', ''),
"lang": extend.get('lang', ''),
"area": extend.get('area', ''),
"year": extend.get('year', ''),
"by": extend.get('by', ''),
"page": pg,
"limit": "12"
}
data = self.fetch(f"{self.host}/api.php/zjv6.vod", headers=self.getheader(), params=params).json()
result = {}
result['list'] = data['data']['list']
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
data = self.fetch(f"{self.host}/api.php/zjv6.vod/detail?vod_id={ids[0]}&rel_limit=10",
headers=self.getheader()).json()
vod = data['data']
v, np = {'vod_play_from': [], 'vod_play_url': []}, {}
for i in vod['vod_play_list']:
n = i['player_info']['show']
np[n] = []
for j in i['urls']:
j['parse'] = i['player_info']['parse2']
nm = j.pop('name')
np[n].append(f"{nm}${self.e64(json.dumps(j))}")
for key, value in np.items():
v['vod_play_from'].append(key)
v['vod_play_url'].append('#'.join(value))
v['vod_play_from'] = '$$$'.join(v['vod_play_from'])
v['vod_play_url'] = '$$$'.join(v['vod_play_url'])
vod.update(v)
vod.pop('vod_play_list', None)
vod.pop('type', None)
return {'list': [vod]}
def searchContent(self, key, quick, pg="1"):
data = self.fetch(f"{self.host}/api.php/zjv6.vod?page={pg}&limit=20&wd={key}", headers=self.getheader()).json()
return {'list': data['data']['list'], 'page': pg}
def playerContent(self, flag, id, vipFlags):
ids = json.loads(self.d64(id))
target_url = ids['url']
try:
parse_str = ids.get('parse', '')
if parse_str:
parse_urls = parse_str.split(',')
result_url = self.try_all_parses(parse_urls, target_url)
if result_url:
return {
'parse': 0,
'url': result_url,
'header': {'User-Agent': 'dart:io'}
}
return {
'parse': 1,
'url': target_url,
'header': {'User-Agent': 'dart:io'}
}
except Exception as e:
print(e)
return {
'parse': 1,
'url': target_url,
'header': {'User-Agent': 'dart:io'}
}
def liveContent(self, url):
pass
def localProxy(self, param):
pass
def userinfo(self):
t = str(int(time.time() * 1000))
uid = self.generate_uid()
sign = self.md5(f"appKey=3bbf7348cf314874883a18d6b6fcf67a&uid={uid}&time={t}")
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36',
'Connection': 'Keep-Alive',
'appKey': '3bbf7348cf314874883a18d6b6fcf67a',
'uid': uid,
'time': t,
'sign': sign,
}
params = {
'access_token': '74d5879931b9774be10dee3d8c51008e',
}
response = self.fetch('https://gitee.com/api/v5/repos/aycapp/openapi/contents/wawaconf.txt', params=params,
headers=headers).json()
data = json.loads(self.decrypt(response['content']))
return data['baseUrl'], data['appKey'], data['appSecret']
def e64(self, text):
try:
text_bytes = text.encode('utf-8')
encoded_bytes = b64encode(text_bytes)
return encoded_bytes.decode('utf-8')
except Exception as e:
print(f"Base64编码错误: {str(e)}")
return ""
def d64(self, encoded_text):
try:
encoded_bytes = encoded_text.encode('utf-8')
decoded_bytes = b64decode(encoded_bytes)
return decoded_bytes.decode('utf-8')
except Exception as e:
print(f"Base64解码错误: {str(e)}")
return ""
def md5(self, text):
h = MD5.new()
h.update(text.encode('utf-8'))
return h.hexdigest()
def generate_uid(self):
return uuid.uuid4().hex
def getheader(self):
t = str(int(time.time() * 1000))
uid = self.generate_uid()
sign = self.sign_message(f"appKey={self.appKey}&time={t}&uid={uid}")
headers = {
'User-Agent': 'okhttp/4.9.3',
'Connection': 'Keep-Alive',
'uid': uid,
'time': t,
'appKey': self.appKey,
'sign': sign,
}
return headers
def decrypt(self, encrypted_data):
key = b64decode('Crm4FXWkk5JItpYirFDpqg==')
cipher = AES.new(key, AES.MODE_ECB)
encrypted = bytes.fromhex(self.d64(encrypted_data))
decrypted = cipher.decrypt(encrypted)
unpadded = unpad(decrypted, AES.block_size)
return unpadded.decode('utf-8')
def sign_message(self, message):
private_key_str = f"-----BEGIN PRIVATE KEY-----\n{self.rsakey}\n-----END PRIVATE KEY-----"
private_key = RSA.import_key(private_key_str)
message_hash = SHA256.new(message.encode('utf-8'))
signature = pkcs1_15.new(private_key).sign(message_hash)
signature_b64 = b64encode(signature).decode('utf-8')
return signature_b64
def fetch_url(self, parse_url, target_url):
try:
response = self.fetch(f"{parse_url.replace('..', '.')}{target_url}",
headers={"user-agent": "okhttp/4.1.0/luob.app"}, timeout=5)
if response.status_code == 200:
try:
data = response.json()
result_url = data.get('url') or data.get('data', {}).get('url')
if result_url:
return result_url
except:
pass
return None
except:
return None
def try_all_parses(self, parse_urls, target_url):
with ThreadPoolExecutor(max_workers=(len(parse_urls))) as executor:
future_to_url = {
executor.submit(self.fetch_url, parse_url.strip(), target_url): parse_url
for parse_url in parse_urls if parse_url.strip()
}
for future in as_completed(future_to_url):
try:
result = future.result()
if result:
return result
except:
continue
return None
+250
View File
@@ -0,0 +1,250 @@
import requests
from bs4 import BeautifulSoup
import re
from base.spider import Spider
import sys
import json
import base64
import urllib.parse
sys.path.append('..')
murl = "https://3642.7rnr.com/web/index.html"
headerx = {
'User-Agent': "Mozilla/5.0 (Linux; Android 13; M2102J2SC Build/TKQ1.221114.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.7499.3 Mobile Safari/537.36",
'Accept-Encoding': "gzip, deflate, br, zstd"
}
response = requests.get(murl, allow_redirects=True)
xurl = response.url
nurl = xurl + '/web/abcdefg.ashx'
pm = ''
class Spider(Spider):
global xurl
global headerx
def getName(self):
return "首页"
def init(self, extend):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
result = {}
result = {"class": [{"type_id": "1003", "type_name": "亚洲无码"},
{"type_id": "3022", "type_name": "欧美无码"},
{"type_id": "3026", "type_name": "中文字幕"},
{"type_id": "3025", "type_name": "经典三级"},
{"type_id": "5", "type_name": "国产主播"},
{"type_id": "134", "type_name": "韩国主播"},
{"type_id": "3137", "type_name": "ASMR"},
{"type_id": "3138", "type_name": "恐怖色情"},
{"type_id": "131", "type_name": "网红视频"},
{"type_id": "132", "type_name": "国产视频"},
{"type_id": "3023", "type_name": "人妖伪娘"},
{"type_id": "130", "type_name": "动漫卡通"},
{"type_id": "3088", "type_name": "华人原创"},
{"type_id": "3135", "type_name": "JVID"},
{"type_id": "3136", "type_name": "SWAG"},
{"type_id": "3134", "type_name": "明星换脸"}]}
return result
def homeVideoContent(self):
videos = []
try:
payload = {
'action': "getindexdata",
't': "1762753963537691",
's': "5ad87a586f5aae9c2ca4f913d45f8958"}
detail = requests.post(url=nurl, data=payload, headers=headerx)
detail.encoding = "utf-8"
res = detail.json()
video_list = res.get("videos", [])
for video in video_list:
name = video.get("title", "")
id = video.get("id", "")
pic = video.get("coverimg", "")
remarks = video.get("updatedate", "")
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
}
videos.append(video)
result = {'list': videos}
return result
except:
pass
def categoryContent(self, cid, pg, filter, ext):
result = {}
videos = []
if pg:
page = int(pg)
else:
page = 1
if page == '1':
payload1 = {
'action': "getvideos",
'vtype': {cid},
'pageindex': "1",
'pagesize': "12",
'tags': "全部",
'sortindex': "1",
't': "176275570014518",
's': "ff4218e4cafd552c4d0c93eb935c14f1"}
else:
payload1 = {
'action': "getvideos",
'vtype': {cid},
'pageindex': {str(page)},
'pagesize': "12",
'tags': "全部",
'sortindex': "1",
't': "176275570014518",
's': "ff4218e4cafd552c4d0c93eb935c14f1"}
try:
detail = requests.post(url=nurl, data=payload1, headers=headerx)
detail.encoding = "utf-8"
res = detail.json()
video_list = res.get("videos", [])
for video in video_list:
name = video.get("title", "")
id = video.get("id", "")
pic = video.get("coverimg", "")
remarks = video.get("updatedate", "")
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
}
videos.append(video)
except:
pass
result = {'list': videos}
result['page'] = pg
result['pagecount'] = 99
result['limit'] = 90
result['total'] = 99
return result
def detailContent(self, ids):
did = ids[0]
result = {}
videos = []
playurl = ''
payload2 = {
'action': "getvideo",
'vid': did,
't': "1762756475175265",
's': "656d5b40c2122f86bc35895dc58fd113"}
res1 = requests.post(url=nurl, data=payload2, headers=headerx)
res1.encoding = "utf-8"
res = res1.json()
node = res.get("data", {}).get("Table", [{}])[0]
vod_id = node.get("id", "")
vod_name = node.get("title", "")
vod_pic = node.get("coverimg", "")
vod_remarks = node.get("updatedate", "")
vod_content = node.get("title", "")
playFrom = []
playList = []
if node.get("vurl"):
base_url1 = res.get("xldata", {}).get("value", "")
base_url2 = res.get("xldata", {}).get("value1", "")
if base_url1:
full_vurl1 = base_url1 + node.get("vurl", "")
playFrom.append("播放源1")
playList.append(full_vurl1)
if base_url2:
full_vurl2 = base_url2 + node.get("vurl", "")
playFrom.append("播放源2")
playList.append(full_vurl2)
videos.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": vod_remarks,
"vod_content": vod_content,
"vod_play_from": "$$$".join(playFrom),
"vod_play_url": "$$$".join(playList)
})
result['list'] = videos
return result
def playerContent(self, flag, id, vipFlags):
parts = id.split("http")
xiutan = 1
if xiutan == 1:
if len(parts) > 1:
before_https, after_https = parts[0], 'http' + parts[1]
result = {}
result["parse"] = xiutan
result["playUrl"] = ''
result["url"] = after_https
result["header"] = headerx
return result
def searchContentPage(self, key, quick, page):
result = {}
videos = []
payload3 = {
'action': "search",
'p': {key},
'pageindex': {str(page)},
'pagesize': "12",
'channelid': "0",
't': "1762756927087982",
's': "2392bd117b4e6e35b5ec1fa9bc380b6f"}
detail = requests.post(url=nurl, data=payload3, headers=headerx)
detail.encoding = "utf-8"
res = detail.json()
video_list = res.get("data", [])
for video in video_list:
name = video.get("title", "")
id = video.get("id", "")
pic = video.get("imgurl", "")
remarks = video.get("updatedate", "")
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
}
videos.append(video)
result['list'] = videos
result['page'] = page
result['pagecount'] = 60
result['limit'] = 30
result['total'] = 999999
return result
def searchContent(self, key, quick):
return self.searchContentPage(key, quick, '1')
def localProxy(self, params):
if params['type'] == "m3u8":
return self.proxyM3u8(params)
elif params['type'] == "media":
return self.proxyMedia(params)
elif params['type'] == "ts":
return self.proxyTs(params)
return None
+321
View File
@@ -0,0 +1,321 @@
import sys
import json
import re
from urllib.parse import quote, unquote
# 依赖 requests 库处理网络请求
import requests
# 引入 TVBox Python 爬虫基础类
sys.path.append('..')
from base.spider import Spider
## 自营4k60帧 mujizybf08.com 不能直连
class Spider(Spider):
HOST = "https://4k01.pianku.online"
PARSE_API_URL = "https://svip.qlplayer.cyou/?url="
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
HEADERS = {
"User-Agent": UA,
"Referer": HOST
}
def getName(self):
return "片库网"
def mylog(self, *args):
"""日志打印封装,便于在日志面板快速定位"""
msg = " ".join([str(arg) for arg in args])
print(f"[{self.getName()}] {msg}")
def init(self, extend=""):
self.mylog("Spider Init Done")
def build_url(self, path):
if not path:
return ""
if path.startswith("http"):
return path
return self.HOST + ("" if path.startswith("/") else "/") + path
def fetch(self, url, headers=None):
"""网络请求封装"""
try:
req_headers = self.HEADERS.copy()
if headers:
req_headers.update(headers)
res = requests.get(url, headers=req_headers, timeout=10)
res.encoding = 'utf-8'
return res.text
except Exception as e:
self.mylog(f"请求失败 [{url}]: {str(e)}")
return ""
def get_vod_list(self, html):
"""从 HTML 中提取视频列表"""
if not html:
return []
vod_list = []
regex = r'<div class="vod-item">[\s\S]*?<a href="\/voddetail\/(\d+)\.html" title="(.*?)"[\s\S]*?<img src="(.*?)"[\s\S]*?<span class="remarks">(.*?)<\/span>'
matches = re.findall(regex, html)
for match in matches:
vod_list.append({
"vod_id": match[0],
"vod_name": match[1],
"vod_pic": self.build_url(match[2]),
"vod_remarks": match[3].strip()
})
self.mylog(f"共提取到 {len(vod_list)} 条数据")
return vod_list
def homeContent(self, filter):
self.mylog(f"开始加载首页,filter={filter}")
classes = [
{"type_id": "20", "type_name": "电影"},
{"type_id": "37", "type_name": "剧集"},
{"type_id": "43", "type_name": "动漫"},
{"type_id": "45", "type_name": "综艺"}
]
filters = {
"20": [{
"key": "tid",
"name": "分类",
"value": [
{"n": "全部", "v": "20"}, {"n": "动作片", "v": "21"}, {"n": "喜剧片", "v": "22"},
{"n": "爱情片", "v": "23"}, {"n": "科幻片", "v": "24"}, {"n": "恐怖片", "v": "25"},
{"n": "剧情片", "v": "26"}, {"n": "战争片", "v": "27"}, {"n": "惊悚片", "v": "28"},
{"n": "犯罪片", "v": "29"}, {"n": "冒险篇", "v": "30"}, {"n": "动画片", "v": "31"},
{"n": "悬疑片", "v": "32"}, {"n": "武侠片", "v": "33"}, {"n": "奇幻片", "v": "34"},
{"n": "纪录片", "v": "35"}, {"n": "其他片", "v": "36"}
]
}]
}
try:
html = self.fetch(self.HOST)
vod_list = self.get_vod_list(html)
result = {
"class": classes,
"list": vod_list
}
if filter:
result["filters"] = filters
return result
except Exception as e:
self.mylog(f"homeContent 异常: {str(e)}")
return {"class": [], "list": []}
def homeVideoContent(self):
"""首页推荐视频"""
self.mylog("获取首页推荐视频")
try:
html = self.fetch(self.HOST)
vod_list = self.get_vod_list(html)
return {"list": vod_list}
except Exception as e:
self.mylog(f"homeVideoContent 异常: {str(e)}")
return {"list": []}
def categoryContent(self, tid, pg, filter, extend):
real_tid = extend.get("tid", tid) if extend else tid
page = str(pg) if pg else "1"
url = f"{self.HOST}/vodtype/{real_tid}.html" if page == "1" else f"{self.HOST}/vodtype/{real_tid}-{page}.html"
self.mylog(f"请求分类 URL: {url}")
try:
html = self.fetch(url)
vod_list = self.get_vod_list(html)
pagecount = int(page) + 1
total = 0
page_match = re.search(r'尾页.*?href=".*?-(\d+)\.html"', html)
if page_match:
pagecount = int(page_match.group(1))
total = pagecount * 24
return {
"list": vod_list,
"page": int(page),
"pagecount": pagecount,
"limit": 24,
"total": total
}
except Exception as e:
self.mylog(f"categoryContent 异常: {str(e)}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 24, "total": 0}
def detailContent(self, array):
id = array[0]
url = f"{self.HOST}/voddetail/{id}.html"
self.mylog(f"获取详情页 URL: {url}")
try:
html = self.fetch(url)
if not html:
return {"list": []}
# 获取基础文本信息
title_match = re.search(r'<h1[^>]*class="detail-title"[^>]*>(.*?)(?:<span|</h1)', html, re.S)
title = title_match.group(1).strip() if title_match else ""
pic_match = re.search(r'class="detail-poster"[^>]*>[\s\S]*?<img src="(.*?)"', html)
pic = self.build_url(pic_match.group(1)) if pic_match else ""
remarks_match = re.search(r'class="detail-remarks"[^>]*>(.*?)<\/span>', html)
remarks = remarks_match.group(1).strip() if remarks_match else ""
content_match = re.search(r'class="detail-desc"[^>]*>[\s\S]*?<p>(.*?)</p>', html, re.S)
content = content_match.group(1).strip() if content_match else ""
# 提取导演、主演、地区、年份
director, actor, area, year = "", "", "", ""
meta_matches = re.findall(r'<(?:span|p|div)[^>]*>(?:导演|主演|地区|年份)[:](.*?)(?:<\/span>|<\/p>|<\/div>)', html)
meta_full = re.findall(r'<(?:span|p|div)[^>]*>((?:导演|主演|地区|年份))[:]', html)
for key, val in zip(meta_full, meta_matches):
val = val.strip()
if "导演" in key:
director = val
elif "主演" in key:
actor = val
elif "地区" in key:
area = val
elif "年份" in key:
year = val
# 提取播放线路
play_from_list = re.findall(r'class="source-tab-item[^"]*"[^>]*>(.*?)<\/span>', html)
# 过滤并替换线路名称(针对自营4k60帧添加直连提醒与特殊符号)
processed_play_from = []
for item in play_from_list:
name = item.strip()
if "自营4K60帧" in name:
processed_play_from.append(f"{name}(注意直连)")
else:
processed_play_from.append(name)
play_from_list = processed_play_from
# 提取播放剧集列表
play_url_list = []
pane_matches = re.findall(r'<div[^>]*class="source-pane[^"]*"[^>]*>([\s\S]*?)<\/div>\s*(?=<div[^>]*class="source-pane|<\/section|<\/div>)', html)
for pane_html in pane_matches:
episodes = []
ep_matches = re.findall(r'href="(\/vodplay\/[^"]+)"[^>]*>(.*?)<\/a>', pane_html)
for ep_url, ep_name in ep_matches:
clean_name = re.sub(r'<[^>]+>', '', ep_name).strip()
full_ep_url = self.build_url(ep_url)
episodes.append(f"{clean_name}${full_ep_url}")
if episodes:
play_url_list.append("#".join(episodes))
# 补全线路
if not play_from_list and play_url_list:
play_from_list = [f"线路 {i+1}" for i in range(len(play_url_list))]
vod = {
"vod_id": id,
"vod_name": title,
"vod_pic": pic,
"vod_type_name": "",
"vod_year": year,
"vod_area": area,
"vod_remarks": remarks,
"vod_actor": actor,
"vod_director": director,
"vod_content": content,
"vod_play_from": "$$$".join(play_from_list),
"vod_play_url": "$$$".join(play_url_list)
}
self.mylog(f"成功解析视频详情: {title}")
return {"list": [vod]}
except Exception as e:
self.mylog(f"detailContent 异常: {str(e)}")
return {"list": []}
def searchContent(self, key, quick, pg="1"):
encoded_key = quote(key)
url = f"{self.HOST}/vodsearch/-------------.html?wd={encoded_key}"
self.mylog(f"开始搜索关键词: {key} -> URL: {url}")
try:
html = self.fetch(url)
vod_list = self.get_vod_list(html)
return {
"list": vod_list,
"page": 1,
"pagecount": 1
}
except Exception as e:
self.mylog(f"searchContent 异常: {str(e)}")
return {"list": []}
def parse_video_url(self, url):
"""二次解析视频真实的播放地址"""
try:
resolve_url = self.PARSE_API_URL + url
self.mylog("解析地址", resolve_url)
html1 = self.fetch(resolve_url)
api_token_match = re.search(r'apiToken\s*:\s*["\']([^"\']+)["\']', html1)
api_token = api_token_match.group(1) if api_token_match else None
if not api_token:
return ""
parse_token_url = f"https://svip.qlplayer.cyou/api/resolve.php?token={quote(api_token)}"
self.mylog("parseTTokenUrl", parse_token_url)
data_str = self.fetch(parse_token_url)
data = json.loads(data_str)
self.mylog("data", data)
raw_url = data.get("url", "")
# 格式化 URL
final_url = raw_url.replace("\\", "")
final_url = re.sub(r'^(https?:/)((?!/))', r'\1/', final_url, flags=re.I)
self.mylog("finalUrl", final_url)
return final_url
except Exception as e:
self.mylog(f"视频解析失败: {str(e)}")
return ""
def playerContent(self, flag, id, vipFlags):
play_url = self.build_url(id)
self.mylog(f"开始获取播放地址: {play_url}")
try:
html = self.fetch(play_url)
# 匹配 player_aaaa 后面的 JSON 对象
match = re.search(r'player_aaaa\s*=\s*(\{[\s\S]*?\})', html)
if match:
json_string = match.group(1)
player_data = json.loads(json_string)
target_url = player_data.get("url", "")
final_play_url = self.parse_video_url(target_url)
return {
"parse": 0,
"url": final_play_url
}
else:
self.mylog("未在网页 HTML 中找到 player_aaaa 匹配项")
except Exception as e:
self.mylog(f"网络请求失败: {str(e)}")
return {"parse": 0, "url": ""}
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
+233
View File
@@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
"""
红果短剧 TVBox Python 源。
extend 可填写播放桥地址,例如:
http://192.168.1.4:9979
或 JSON
{"bridge":"http://192.168.1.4:9979"}
"""
import json
import re
import sys
import time
from urllib.parse import quote, urlencode
import requests
sys.path.append("../../")
try:
from base.spider import Spider
except ImportError:
class Spider:
pass
class Spider(Spider):
site = "https://hongguoduanju.com"
headers = {
"User-Agent": (
"Mozilla/5.0 (Linux; Android 12; TV) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0 Safari/537.36"
),
"Accept-Language": "zh-CN,zh;q=0.9",
}
category_map = {
"热门": "sort_type=1",
"最新": "sort_type=2",
"都市": "background=cate_1",
"现代": "background=cate_757",
"古代": "background=cate_758",
"乡村": "background=cate_11",
"职场": "background=cate_127",
"校园": "background=cate_4",
"悬疑": "topic=cate_165",
"喜剧": "topic=cate_303",
"重生": "setting=cate_36",
"穿越": "setting=cate_37",
}
def __init__(self):
self.bridge = "http://192.168.1.4:9979"
self._cache = {}
def getName(self):
return "红果短剧"
def init(self, extend=""):
if isinstance(extend, dict):
self.bridge = str(extend.get("bridge") or self.bridge).rstrip("/")
elif extend:
text = str(extend).strip()
try:
data = json.loads(text)
self.bridge = str(data.get("bridge") or self.bridge).rstrip("/")
except Exception:
if text.startswith("http"):
self.bridge = text.rstrip("/")
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def destroy(self):
return
def _get(self, url):
response = requests.get(url, headers=self.headers, timeout=25)
response.raise_for_status()
response.encoding = "utf-8"
return response.text
def _router_data(self, url):
cached = self._cache.get(url)
if cached and time.time() - cached[0] < 300:
return cached[1]
html = self._get(url)
match = re.search(
r"window\._ROUTER_DATA\s*=\s*(\{.*?\})\s*</script>",
html,
re.S,
)
if not match:
raise RuntimeError("页面数据格式已变化")
data = json.loads(match.group(1))
self._cache[url] = (time.time(), data)
return data
@staticmethod
def _vod(item):
tags = item.get("tags") or []
if isinstance(tags, list):
tags = " · ".join(str(x) for x in tags[:3])
count = item.get("episode_cnt") or len(item.get("vid_list") or [])
remark = ("%s" % count) if count else str(tags or "")
return {
"vod_id": str(item.get("series_id") or ""),
"vod_name": str(item.get("series_name") or ""),
"vod_pic": str(item.get("series_cover") or ""),
"vod_remarks": remark,
}
def _category_items(self, query):
url = self.site + "/category?" + query
data = self._router_data(url)
page = data.get("loaderData", {}).get("category_page", {})
items = page.get("recommendList") or []
if not items:
items = page.get("categoryData", {}).get("recommendList") or []
seen = set()
result = []
for item in items:
sid = str(item.get("series_id") or "")
if sid and sid not in seen:
seen.add(sid)
result.append(item)
return result
def homeContent(self, filter):
return {
"class": [
{"type_name": name, "type_id": query}
for name, query in self.category_map.items()
],
"list": self.homeVideoContent().get("list", []),
}
def homeVideoContent(self):
try:
items = self._category_items("sort_type=1")[:30]
return {"list": [self._vod(x) for x in items]}
except Exception as exc:
print("红果首页读取失败:", exc)
return {"list": []}
def categoryContent(self, tid, pg, filter, extend):
page = max(1, int(pg or 1))
per_page = 30
try:
items = self._category_items(str(tid))
start = (page - 1) * per_page
chunk = items[start:start + per_page]
page_count = max(1, (len(items) + per_page - 1) // per_page)
return {
"list": [self._vod(x) for x in chunk],
"page": page,
"pagecount": page_count,
"limit": per_page,
"total": len(items),
}
except Exception as exc:
print("红果分类读取失败:", exc)
return {"list": [], "page": page, "pagecount": page}
def detailContent(self, ids):
series_id = str(ids[0])
url = self.site + "/detail?series_id=" + quote(series_id)
try:
data = self._router_data(url)
detail = data.get("loaderData", {}).get("detail_page", {})
series = detail.get("seriesDetail") or {}
vids = series.get("vid_list") or []
episodes = [
"%d集$%s" % (index + 1, vid)
for index, vid in enumerate(vids)
if str(vid)
]
tags = series.get("tags") or []
if isinstance(tags, list):
tags = ",".join(str(x) for x in tags)
vod = {
"vod_id": series_id,
"vod_name": str(series.get("series_name") or "红果短剧"),
"vod_pic": str(series.get("series_cover") or ""),
"type_name": str(tags),
"vod_remarks": "%s" % (series.get("episode_cnt") or len(vids)),
"vod_content": str(series.get("series_intro") or ""),
"vod_play_from": "红果",
"vod_play_url": "#".join(episodes),
}
return {"list": [vod]}
except Exception as exc:
print("红果详情读取失败:", exc)
return {"list": []}
def searchContent(self, key, quick, pg=1):
page = max(1, int(pg or 1))
per_page = 30
try:
items = self._category_items("sort_type=1")
keyword = str(key).strip().lower()
matches = [
x for x in items
if keyword in str(x.get("series_name") or "").lower()
or keyword in str(x.get("series_intro") or "").lower()
]
start = (page - 1) * per_page
return {
"list": [self._vod(x) for x in matches[start:start + per_page]],
"page": page,
}
except Exception as exc:
print("红果搜索失败:", exc)
return {"list": [], "page": page}
def searchContentPage(self, key, quick, pg=1):
return self.searchContent(key, quick, pg)
def playerContent(self, flag, pid, vipFlags):
url = self.bridge + "/play?" + urlencode({"vid": str(pid)})
return {
"parse": 0,
"playUrl": "",
"url": url,
"header": {
"User-Agent": self.headers["User-Agent"],
"Referer": self.site + "/",
},
}
def localProxy(self, params):
return None
+295
View File
@@ -0,0 +1,295 @@
# -*- coding: utf-8 -*-
import sys
import re
import html as html_mod
import urllib.parse
import requests
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "美剧天堂"
def init(self, context, extend=""):
self.host = "https://www.meijutt.cc"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Referer': self.host,
}
self.session = requests.Session()
self.session.verify = False
self.session.headers.update(self.headers)
def destroy(self):
try:
self.session.close()
except:
pass
def _get(self, url, timeout=10):
if url.startswith('/'):
url = self.host + url
try:
r = self.session.get(url, timeout=timeout, verify=False, allow_redirects=True)
r.encoding = 'utf-8'
return r
except:
return None
def _post(self, url, data, timeout=10):
if url.startswith('/'):
url = self.host + url
try:
headers = dict(self.headers)
headers['Content-Type'] = 'application/x-www-form-urlencoded'
r = self.session.post(url, data=data, headers=headers, timeout=timeout, verify=False, allow_redirects=True)
r.encoding = 'utf-8'
return r
except:
return None
def _clean(self, text):
return re.sub(r'\s+', ' ', text).strip() if text else ''
def _extract_text(self, html_str):
text = re.sub(r'<[^>]+>', '', html_str).strip() if html_str else ''
return html_mod.unescape(text)
# ==================== 首页 ====================
def homeContent(self, filter):
classes = [
{"type_id": "1", "type_name": "魔幻科幻"},
{"type_id": "2", "type_name": "灵异惊悚"},
{"type_id": "3", "type_name": "都市情感"},
{"type_id": "4", "type_name": "犯罪历史"},
{"type_id": "5", "type_name": "选秀综艺"},
{"type_id": "6", "type_name": "动漫卡通"},
]
return {"class": classes, "filters": {}}
def homeVideoContent(self):
r = self._get('/')
if not r:
return {"list": []}
return self._parse_home_list(r.text)
def _parse_home_list(self, html):
videos = []
seen = set()
items = re.findall(
r'<a href="(/meijutt/(\d+)\.html)"[^>]*title="([^"]*)"',
html, re.S
)
for href, vid, title in items:
if vid in seen:
continue
seen.add(vid)
pic_match = re.search(
rf'<a href="{re.escape(href)}"[^>]*>.*?<img[^>]*src="(https?://[^"]*)"',
html, re.S
)
pic = pic_match.group(1) if pic_match else ''
videos.append({
"vod_id": href,
"vod_name": title,
"vod_pic": pic,
})
if len(videos) >= 20:
break
return {"list": videos}
# ==================== 分类 ====================
def categoryContent(self, tid, pg, filter, extend):
pg = int(pg) if str(pg).isdigit() else 1
if pg == 1:
url = f'/mjtt/{tid}.html'
else:
url = f'/mjtt/{tid}-{pg}.html'
r = self._get(url)
if not r:
return {"list": [], "page": pg, "pagecount": 1, "limit": 20, "total": 0}
return self._parse_category_list(r.text, pg)
def _parse_category_list(self, html, pg):
videos = []
items = re.findall(
r'<div class="bor_img3_right">\s*<a href="(/meijutt/\d+\.html)"[^>]*title="([^"]*)"[^>]*>'
r'<img[^>]*data-original="([^"]*)"[^>]*>.*?</a>\s*<em>([\d.]+)</em>',
html, re.S
)
for href, title, pic, score in items:
videos.append({
"vod_id": href,
"vod_name": title,
"vod_pic": pic,
"vod_remarks": f"评分 {score}",
})
page_match = re.search(r'href="[^"]*-(\d+)\.html"[^>]*>\s*末页', html)
if not page_match:
page_match = re.search(r'href="[^"]*-(\d+)\.html"[^>]*>\s*>', html)
if not page_match:
page_match = re.search(r'共(\d+)页', html)
pagecount = int(page_match.group(1)) if page_match else pg
return {
"list": videos,
"page": pg,
"pagecount": pagecount,
"limit": 20,
"total": pagecount * 20,
}
# ==================== 详情 ====================
def detailContent(self, ids):
try:
vod_id = ids[0] if isinstance(ids, list) else ids
if not vod_id.startswith('http'):
vod_id = self.host + vod_id
r = self._get(vod_id)
if not r:
return {"list": []}
return self._parse_detail(r.text, vod_id)
except Exception:
return {"list": []}
def _parse_detail(self, html, url):
vod = {}
m = re.search(r'<div class="info-title"><span>(【.*?】)</span><h1>([^<]+)</h1>\((\d{4})\)</div>', html)
if m:
vod['vod_name'] = m.group(2).strip()
vod['vod_year'] = m.group(3)
vod['vod_area'] = re.sub(r'[\[\]【】]', '', m.group(1)).strip()
else:
m2 = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
if m2:
vod['vod_name'] = m2.group(1).strip()
pic_match = re.search(r'<img[^>]*(?:data-src|data-original|src)="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', html)
if pic_match:
vod['vod_pic'] = pic_match.group(1)
li_items = re.findall(r'<li>(.*?)</li>', html, re.S)
for li in li_items:
clean = self._extract_text(li)
if clean.startswith('主演:'):
actors = clean.replace('主演:', '').replace('更多>>', '').strip()
if actors and actors not in ('内详', ''):
vod['vod_actor'] = actors
elif clean.startswith('小分类:'):
vod['vod_type'] = clean.replace('小分类:', '').strip()
elif clean.startswith('地区:'):
area_text = clean.replace('地区:', '').strip()
area_match = re.match(r'([^\s更新]+)', area_text)
if area_match:
vod['vod_area'] = area_match.group(1)
elif clean.startswith('状态:'):
vod['vod_remarks'] = clean.replace('状态:', '').strip()
elif '电视台:' in clean:
channel = clean.split('电视台:', 1)[-1].split('单集')[0].strip()
if channel:
remarks = vod.get('vod_remarks', '')
vod['vod_remarks'] = f"{channel} / {remarks}" if remarks else channel
desc_parts = re.findall(r'<p[^>]*>(.*?)</p>', html, re.S)
for part in desc_parts:
clean = self._extract_text(part)
if len(clean) > 30 and '下载' not in clean and 'magnet' not in clean and 'gurl' not in clean:
vod['vod_content'] = clean
break
sources = {}
tab_labels = re.findall(r'<label[^>]*>\s*([^<]+)\s*<em>\[(\d+)\]</em>', html)
tab_ids = re.findall(r'id="play_(\d+)"', html)
tab_splits = re.split(r'<div class="tabs-list[^"]*"\s*id="play_\d+"', html)
for i, play_id in enumerate(tab_ids):
tab_name = tab_labels[i][0].strip() if i < len(tab_labels) else f'线路{i+1}'
section = tab_splits[i + 1] if i + 1 < len(tab_splits) else ''
end = re.search(r'<div class="tabs-list|<div class="o_list_cn', section)
if end:
section = section[:end.start()]
episodes = re.findall(r'href="(/meijuplay/[^"]+)"[^>]*>([^<]+)', section)
if episodes:
ep_list = []
for ep_url, ep_name in episodes:
ep_list.append(f"{ep_name}${self.host}{ep_url}")
sources[tab_name] = ep_list
gvar_matches = re.findall(r'var\s+GvodUrls\d+\s*=\s*"([^"]*)"', html)
for gvar in gvar_matches:
parts = gvar.split('###')
for part in parts:
if '$' not in part:
continue
name_url = part.split('$', 1)
if len(name_url) != 2:
continue
name = self._clean(name_url[0]) or '下载链接'
link_url = name_url[1]
if 'pan.quark.cn' in link_url:
src = '夸克网盘'
elif 'pan.xunlei.com' in link_url:
src = '迅雷网盘'
elif 'pan.baidu.com' in link_url:
src = '百度网盘'
elif link_url.startswith('ed2k://'):
src = 'ed2k'
elif link_url.startswith('magnet:'):
src = '磁力'
else:
src = '其他'
if src not in sources:
sources[src] = []
ep = f"{name}${link_url}"
if ep not in sources[src]:
sources[src].append(ep)
source_names = []
source_urls = []
download_order = ['夸克网盘', '迅雷网盘', '百度网盘', 'ed2k', '磁力', '其他']
streaming = [k for k in sources if k not in download_order and sources[k]]
downloads = [k for k in download_order if k in sources and sources[k]]
for src_name in streaming + downloads:
source_names.append(src_name)
source_urls.append('#'.join(sources[src_name]))
vod['vod_play_from'] = '$$$'.join(source_names) if source_names else '美剧天堂'
vod['vod_play_url'] = '$$$'.join(source_urls) if source_urls else ''
return {"list": [vod]}
# ==================== 播放 ====================
def playerContent(self, flag, id, vipFlags):
if '/meijuplay/' in id:
m3u8 = self._get_m3u8(id)
if m3u8:
return {"parse": 0, "url": m3u8, "header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"Referer": "https://www.meijutt.cc/"
}}
return {"parse": 0, "url": id, "header": {}}
def _get_m3u8(self, play_url):
try:
r = self._get(play_url)
if not r:
return None
m = re.search(r'var\s+now\s*=\s*(?:unescape\()?["\']([^"\']+)', r.text)
if m:
url = m.group(1)
if '%' in url:
url = urllib.parse.unquote(url)
if '.m3u8' in url:
return url
m2 = re.search(r'(https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*)', r.text)
if m2:
return m2.group(1)
iframe = re.search(r'iframe[^>]*src="([^"]*dm\.html[^"]*)"', r.text)
if iframe:
r2 = self._get(iframe.group(1))
if r2:
m3 = re.search(r'var\s+now\s*=\s*(?:unescape\()?["\']([^"\']+)', r2.text)
if m3:
url = m3.group(1)
if '%' in url:
url = urllib.parse.unquote(url)
if '.m3u8' in url:
return url
except Exception:
pass
return None
+343
View File
@@ -0,0 +1,343 @@
#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.cd-zj.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.cd-zj.com/'
})
self.cateManual = {
'\u7535\u5f71': '1',
'\u7535\u89c6\u5267': '2',
'\u7efc\u827a': '3',
'\u52a8\u6f2b': '4',
'\u70ed\u95e8\u77ed\u5267': '5',
'\u817e\u8bafSVIP': 'label/qq',
'\u4f18\u9177SVIP': 'label/youku',
'B\u7ad9SVIP': 'label/bli',
}
def init(self, extend=""):
pass
def getName(self):
return "\u67ab\u53f64K\u5907\u7528"
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('&nbsp;', ' ').replace('&amp;', '&').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:
return ''
def getVid(self, url):
if not url:
return ''
m = re.search(r'/detail/(\d+)\.html', url)
if m:
return m.group(1)
m = re.search(r'/play/(\d+)-', url)
if m:
return m.group(1)
return ''
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)
seen = set()
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
href = m.group(1)
title = m.group(2)
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+500]
pic = ''
pm = re.search(r'data-src="([^"]+)"', snippet)
if pm:
pic = pm.group(1).replace('&amp;', '&')
note = ''
nm = re.search(r'ft2">([^<]+)<', snippet)
if nm:
note = nm.group(1)
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 str(tid).startswith('label/'):
if page == 1:
url = f'{self.site}/{tid}.html'
else:
url = f'{self.site}/{tid}-{page}.html'
else:
if page == 1:
url = f'{self.site}/type/{tid}.html'
else:
url = f'{self.site}/type/{tid}-{page}.html'
html = self._get(url)
seen = set()
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
href = m.group(1)
title = m.group(2)
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+500]
pic = ''
pm = re.search(r'data-src="([^"]+)"', snippet)
if pm:
pic = pm.group(1).replace('&amp;', '&')
note = ''
nm = re.search(r'ft2">([^<]+)<', snippet)
if nm:
note = nm.group(1)
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}/detail/{vid}.html')
title = ''
tm = re.search(r'<title>\u300a(.+?)\u300b', html)
if tm:
title = 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'lazy1[^>]*data-src="([^"]+)"', html)
if pm:
pic = pm.group(1).replace('&amp;', '&')
desc = ''
dm = re.search(r'<meta name="description" content="(.+?)"', html)
if dm:
desc = dm.group(1).replace('\u5267\u60c5\u4ecb\u7ecd\uff1a', '').strip()
actor = ''
director = ''
info = re.search(r'slide-info(.*?)(?:anthology|swiper)', html, re.DOTALL)
if info:
block = info.group(1)
am = re.search(r'\u4e3b\u6f14[:\uff1a]\s*([^\n<]+)', block)
if am:
actor = am.group(1).strip()
dm2 = re.search(r'\u5bfc\u6f14[:\uff1a]\s*([^\n<]+)', block)
if dm2:
director = dm2.group(1).strip()
play_from = []
play_url = []
# \u627e anthology-tab \u533a\u5757\u5185\u7684\u6240\u6709 <a class="swiper-slide">
tab_block = re.search(r'class="anthology-tab[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
if tab_block:
tabs = re.findall(r'<a[^>]*class="swiper-slide"[^>]*>(.*?)</a>', tab_block.group(1), re.DOTALL)
else:
tabs = []
# \u627e\u6240\u6709 anthology-list-box \u533a\u5757
panels = re.findall(r'class="anthology-list-box[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
for i, tab in enumerate(tabs):
tab_name = self._clean(tab) or f'\u7ebf\u8def{i+1}'
play_from.append(tab_name)
episodes = []
if i < len(panels):
for em in re.finditer(r'<a[^>]*href="([^"]+)"[^>]*>([^<]+)<', 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))
vod = {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': '',
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': actor,
'vod_director': director if director else bytes.fromhex('e6989fe6b2b3').decode('utf-8'),
'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 = {}
try:
play_url = id
if id and not id.startswith('http'):
play_url = self.site + id
html = self._get(play_url)
# \u4f18\u5148\u4ece player_aaaa JSON \u63d0\u53d6 m3u8 \u76f4\u94fe
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 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
return result
except:
pass
# \u5907\u7528: \u4ece\u9875\u9762\u4e2d\u627e m3u8 \u94fe\u63a5
m = re.search(r'url":\s*"(https?://[^"]*\.m3u8[^"]*)"', 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 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
return result
# \u5907\u7528: 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 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
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 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.site + '/'
}
except Exception as e:
print(f'playerContent error: {e}')
result['parse'] = 1
result['url'] = id
result['jx'] = 0
result['header'] = {}
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}/cupfox-search/-------------.html'
params = {'wd': key}
if page > 1:
params['page'] = page
html = self._get(url)
seen = set()
for m in re.finditer(r'href="(/detail/\d+\.html)"[^>]*title="([^"]*)"', html):
href = m.group(1)
title = m.group(2)
vid = self.getVid(href)
if not vid or vid in seen:
continue
seen.add(vid)
snippet = html[m.start():m.start()+500]
pic = ''
pm = re.search(r'data-src="([^"]+)"', snippet)
if pm:
pic = pm.group(1).replace('&amp;', '&')
note = ''
nm = re.search(r'ft2">([^<]+)<', snippet)
if nm:
note = nm.group(1)
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", {}, ""]
+413
View File
@@ -0,0 +1,413 @@
# coding=utf-8
#!/usr/bin/python
import sys
sys.path.append('..')
from base.spider import Spider
import json
import time
import urllib.parse
import re
import requests
from lxml import etree
class Spider(Spider):
def getName(self):
return "香蕉视频"
def init(self, extend=""):
self.host = "https://618013.xyz"
self.api_host = "https://h5.xxoo168.org"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Referer': self.host
}
self.log(f"香蕉视频爬虫初始化完成,主站: {self.host}")
def html(self, content):
"""将HTML内容转换为可查询的对象"""
try:
return etree.HTML(content)
except:
self.log("HTML解析失败")
return None
def regStr(self, pattern, string, index=1):
"""正则表达式提取字符串"""
try:
match = re.search(pattern, string, re.IGNORECASE)
if match and len(match.groups()) >= index:
return match.group(index)
except:
pass
return ""
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
"""获取首页内容和分类"""
result = {}
# 只保留指定的分类
classes = [
{'type_id': '618013.xyz_1', 'type_name': '全部视频'},
{'type_id': '618013.xyz_13', 'type_name': '香蕉精品'},
{'type_id': '618013.xyz_22', 'type_name': '制服诱惑'},
{'type_id': '618013.xyz_6', 'type_name': '国产视频'},
{'type_id': '618013.xyz_8', 'type_name': '清纯少女'},
{'type_id': '618013.xyz_9', 'type_name': '辣妹大奶'},
{'type_id': '618013.xyz_10', 'type_name': '女同专属'},
{'type_id': '618013.xyz_11', 'type_name': '素人出演'},
{'type_id': '618013.xyz_12', 'type_name': '角色扮演'},
{'type_id': '618013.xyz_20', 'type_name': '人妻熟女'},
{'type_id': '618013.xyz_23', 'type_name': '日韩剧情'},
{'type_id': '618013.xyz_21', 'type_name': '经典伦理'},
{'type_id': '618013.xyz_7', 'type_name': '成人动漫'},
{'type_id': '618013.xyz_14', 'type_name': '精品二区'},
{'type_id': '618013.xyz_40', 'type_name': '精品三区'},
{'type_id': '618013.xyz_53', 'type_name': '动漫中字'},
{'type_id': '618013.xyz_52', 'type_name': '日本无码'},
{'type_id': '618013.xyz_33', 'type_name': '中文字幕'},
{'type_id': '618013.xyz_44', 'type_name': '国产传媒'},
{'type_id': '618013.xyz_32', 'type_name': '国产自拍'}
]
result['class'] = classes
try:
rsp = self.fetch(self.host, headers=self.headers)
doc = self.html(rsp.text)
videos = self._get_videos(doc, limit=20)
result['list'] = videos
except Exception as e:
self.log(f"首页获取出错: {str(e)}")
result['list'] = []
return result
def homeVideoContent(self):
"""分类定义 - 兼容性方法"""
return {
'class': [
{'type_id': '618013.xyz_1', 'type_name': '全部视频'},
{'type_id': '618013.xyz_13', 'type_name': '香蕉精品'},
{'type_id': '618013.xyz_22', 'type_name': '制服诱惑'},
{'type_id': '618013.xyz_6', 'type_name': '国产视频'},
{'type_id': '618013.xyz_8', 'type_name': '清纯少女'},
{'type_id': '618013.xyz_9', 'type_name': '辣妹大奶'},
{'type_id': '618013.xyz_10', 'type_name': '女同专属'},
{'type_id': '618013.xyz_11', 'type_name': '素人出演'},
{'type_id': '618013.xyz_12', 'type_name': '角色扮演'},
{'type_id': '618013.xyz_20', 'type_name': '人妻熟女'},
{'type_id': '618013.xyz_23', 'type_name': '日韩剧情'},
{'type_id': '618013.xyz_21', 'type_name': '经典伦理'},
{'type_id': '618013.xyz_7', 'type_name': '成人动漫'},
{'type_id': '618013.xyz_14', 'type_name': '精品二区'},
{'type_id': '618013.xyz_40', 'type_name': '精品三区'},
{'type_id': '618013.xyz_53', 'type_name': '动漫中字'},
{'type_id': '618013.xyz_52', 'type_name': '日本无码'},
{'type_id': '618013.xyz_33', 'type_name': '中文字幕'},
{'type_id': '618013.xyz_44', 'type_name': '国产传媒'},
{'type_id': '618013.xyz_32', 'type_name': '国产自拍'}
]
}
def categoryContent(self, tid, pg, filter, extend):
"""分类内容 - 修改为使用固定页数设置"""
try:
domain, type_id = tid.split('_')
url = f"https://{domain}/index.php/vod/type/id/{type_id}.html"
if pg and pg != '1':
url = url.replace('.html', f'/page/{pg}.html')
self.log(f"访问分类URL: {url}")
rsp = self.fetch(url, headers=self.headers)
doc = self.html(rsp.text)
videos = self._get_videos(doc, limit=20)
# 使用固定页数设置,而不是尝试从页面解析
pagecount = 999
total = 19980
return {
'list': videos,
'page': int(pg),
'pagecount': pagecount,
'limit': 20,
'total': total
}
except Exception as e:
self.log(f"分类内容获取出错: {str(e)}")
return {'list': []}
def searchContent(self, key, quick, pg="1"):
"""搜索功能"""
try:
search_url = f"{self.host}/index.php/vod/search.html?wd={urllib.parse.quote(key)}&page={pg}"
self.log(f"搜索URL: {search_url}")
rsp = self.fetch(search_url, headers=self.headers)
if not rsp or rsp.status_code != 200:
return {'list': []}
doc = self.html(rsp.text)
videos = self._get_videos(doc)
return {'list': videos}
except Exception as e:
self.log(f"搜索出错: {str(e)}")
return {'list': []}
def detailContent(self, ids):
"""详情页面"""
try:
vid = ids[0]
if '_' in vid:
domain, video_id = vid.split('_')
detail_url = f"https://{domain}/index.php/vod/detail/id/{video_id}.html"
else:
detail_url = f"{self.host}/index.php/vod/detail/id/{vid}.html"
self.log(f"访问详情URL: {detail_url}")
rsp = self.fetch(detail_url, headers=self.headers)
doc = self.html(rsp.text)
video_info = self._get_detail(doc, vid)
return {'list': [video_info]} if video_info else {'list': []}
except Exception as e:
self.log(f"详情获取出错: {str(e)}")
return {'list': []}
def playerContent(self, flag, id, vipFlags):
"""播放链接 - 直接使用API获取视频地址"""
try:
self.log(f"获取播放链接: flag={flag}, id={id}")
# 提取视频ID
if '_' in id:
_, video_id = id.split('_')
else:
video_id = id
self.log(f"视频ID: {video_id}")
# 直接调用API获取视频地址
api_url = f"{self.api_host}/api/v2/vod/reqplay/{video_id}"
self.log(f"请求API获取视频地址: {api_url}")
api_headers = self.headers.copy()
api_headers.update({
'Referer': f"{self.host}/",
'Origin': self.host,
'X-Requested-With': 'XMLHttpRequest'
})
api_response = self.fetch(api_url, headers=api_headers)
if api_response and api_response.status_code == 200:
data = api_response.json()
self.log(f"API响应: {data}")
if data.get('retcode') == 3:
video_url = data.get('data', {}).get('httpurl_preview', '')
else:
video_url = data.get('data', {}).get('httpurl', '')
if video_url:
# 移除可能的参数
video_url = video_url.replace('?300', '')
self.log(f"从API获取到视频地址: {video_url}")
return {'parse': 0, 'playUrl': '', 'url': video_url}
else:
self.log("API响应中没有找到视频地址")
else:
self.log(f"API请求失败,状态码: {api_response.status_code if api_response else '无响应'}")
# 如果API请求失败,回退到原来的方法
if '_' in id:
domain, play_id = id.split('_')
play_url = f"https://{domain}/html/kkyd.html?m={play_id}"
else:
play_url = f"{self.host}/html/kkyd.html?m={id}"
self.log(f"回退到播放页面: {play_url}")
return {'parse': 1, 'playUrl': '', 'url': play_url}
except Exception as e:
self.log(f"播放链接获取出错: {str(e)}")
# 出错时也返回播放页面URL
if '_' in id:
domain, play_id = id.split('_')
play_url = f"https://{domain}/html/kkyd.html?m={play_id}"
else:
play_url = f"{self.host}/html/kkyd.html?m={id}"
return {'parse': 1, 'playUrl': '', 'url': play_url}
# ========== 辅助方法 ==========
def _get_videos(self, doc, limit=None):
"""获取影片列表 - 根据实际网站结构"""
try:
videos = []
elements = doc.xpath('//a[@class="vodbox"]')
self.log(f"找到 {len(elements)} 个vodbox元素")
for elem in elements:
video = self._extract_video(elem)
if video:
videos.append(video)
return videos[:limit] if limit and videos else videos
except Exception as e:
self.log(f"获取影片列表出错: {str(e)}")
return []
def _extract_video(self, element):
"""提取影片信息 - 修复标题乱码问题,正确读取km-script标签文本"""
try:
# 1. 提取影片链接(获取vod_id的来源)
link = element.xpath('./@href')[0] # 获取a标签的href属性
if link.startswith('/'):
link = self.host + link # 补全相对路径为完整URL
# 2. 提取vod_id(从URL的m参数获取,而非hash,更准确)
vod_id = self.regStr(r'm=(\d+)', link) # 匹配 ?m=123 中的数字
if not vod_id:
vod_id = str(hash(link) % 1000000) # 兜底:hash生成唯一ID
# 3. 提取标题(关键修复:读取<p class="km-script">内的文本并解密)
title_elem = element.xpath('./p[@class="km-script"]/text()') # 定位km-script标签
if not title_elem:
# 尝试其他可能的标题选择器
title_elem = element.xpath('.//p[contains(@class, "script")]/text()')
if not title_elem:
title_elem = element.xpath('.//p/text()')
if not title_elem:
title_elem = element.xpath('.//h3/text()')
if not title_elem:
title_elem = element.xpath('.//h4/text()')
if not title_elem:
self.log(f"未找到标题元素,跳过该视频")
return None
title_encrypted = title_elem[0].strip() # 获取加密的标题文本
# 4. 解密标题 - 使用网站的解密算法
title = self._decrypt_title(title_encrypted)
# 5. 提取封面图(逻辑不变,兼容data-original和src
pic_elem = element.xpath('.//img/@data-original') # 优先懒加载地址
if not pic_elem:
pic_elem = element.xpath('.//img/@src') # 兜底:直接src地址
pic = pic_elem[0] if pic_elem else ''
# 6. 补全图片URL(处理相对路径或无协议的情况)
if pic:
if pic.startswith('//'):
pic = 'https:' + pic # 补全https协议
elif pic.startswith('/'):
pic = self.host + pic # 补全主域名
# 7. 返回正确的视频信息
return {
'vod_id': f"618013.xyz_{vod_id}",
'vod_name': title, # 此时title已为正确文本
'vod_pic': pic,
'vod_remarks': '',
'vod_year': ''
}
except Exception as e:
self.log(f"提取影片信息出错: {str(e)}")
return None
def _decrypt_title(self, encrypted_text):
"""解密标题 - 使用网站的解密算法"""
try:
# 网站使用的解密算法:每个字符与128进行异或操作
decrypted_chars = []
for char in encrypted_text:
# 将字符转换为Unicode码点
code_point = ord(char)
# 与128进行异或操作
decrypted_code = code_point ^ 128
# 转换回字符
decrypted_char = chr(decrypted_code)
decrypted_chars.append(decrypted_char)
# 拼接解密后的字符
decrypted_text = ''.join(decrypted_chars)
return decrypted_text
except Exception as e:
self.log(f"标题解密失败: {str(e)}")
return encrypted_text # 如果解密失败,返回原文本
def _get_detail(self, doc, vid):
"""获取详情信息 (优化版) - 修复播放源提取问题"""
try:
title = self._get_text(doc, ['//h1/text()', '//title/text()'])
pic = self._get_text(doc, ['//div[@class="dyimg"]//img/@src', '//img[@class="poster"]/@src'])
if pic and pic.startswith('/'):
pic = self.host + pic
desc = self._get_text(doc, ['//div[@class="yp_context"]/text()', '//div[@class="introduction"]//text()'])
actor = self._get_text(doc, ['//span[contains(text(),"主演")]/following-sibling::*/text()'])
director = self._get_text(doc, ['//span[contains(text(),"导演")]/following-sibling::*/text()'])
play_from = []
play_urls = []
# 尝试查找播放源
play_links = doc.xpath('//a[contains(@href, "m=")]')
if play_links:
episodes = []
for link in play_links:
ep_title = link.xpath('./text()')
ep_href = link.xpath('./@href')[0]
if ep_title:
ep_title = ep_title[0].strip()
play_id = self.regStr(r'm=(\d+)', ep_href)
if play_id:
episodes.append(f"{ep_title}${play_id}")
if episodes:
play_from.append("默认播放源")
play_urls.append('#'.join(episodes))
if not play_from:
self.log("未找到播放源元素,无法定位播放源列表")
# 即使没有播放源,也返回基本信息
return {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': '',
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': actor,
'vod_director': director,
'vod_content': desc,
'vod_play_from': '默认播放源',
'vod_play_url': f"第1集${vid}"
}
return {
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'type_name': '',
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': actor,
'vod_director': director,
'vod_content': desc,
'vod_play_from': '$$$'.join(play_from),
'vod_play_url': '$$$'.join(play_urls)
}
except Exception as e:
self.log(f"获取详情出错: {str(e)}")
return None
def _get_text(self, doc, selectors):
"""通用文本提取"""
for selector in selectors:
texts = doc.xpath(selector)
for text in texts:
if text and text.strip():
return text.strip()
return ''