Sync all projects
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(' ', ' ').replace('&', '&').replace('\u3000', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15)
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
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('&', '&')
|
||||
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('&', '&')
|
||||
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('&', '&')
|
||||
|
||||
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('&', '&')
|
||||
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", {}, ""]
|
||||
@@ -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 ''
|
||||
Reference in New Issue
Block a user