Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-02 13:18:47 +00:00
parent a6be617861
commit bde490f53e
18 changed files with 15966 additions and 10331 deletions
+252
View File
@@ -0,0 +1,252 @@
/**
* 3Q影视 爬虫
* 作者:deepseek
* 版本:1.0
* 最后更新:2025-12-17
* 发布页 https://qqqys.com
*
* @config
// * debug: true
*/
const baseUrl = 'https://qqqys.com';
/**
* 初始化配置
*/
async function init(cfg) {
return {};
}
/**
* 首页分类
*/
async function homeContent(filter) {
const filterConfig = {
class: [
{ type_id: "电影", type_name: "电影" },
{ type_id: "剧集", type_name: "剧集" },
{ type_id: "动漫", type_name: "动漫" },
{ type_id: "综艺", type_name: "综艺" }
],
filters: {
"电影": [
{ key: "class", name: "类型", value: [ {n:"全部",v:""}, {n:"动作",v:"动作"}, {n:"喜剧",v:"喜剧"}, {n:"爱情",v:"爱情"}, {n:"科幻",v:"科幻"}, {n:"恐怖",v:"恐怖"}, {n:"悬疑",v:"悬疑"}, {n:"犯罪",v:"犯罪"}, {n:"战争",v:"战争"}, {n:"动画",v:"动画"}, {n:"冒险",v:"冒险"}, {n:"历史",v:"历史"}, {n:"灾难",v:"灾难"}, {n:"纪录",v:"纪录"}, {n:"剧情",v:"剧情"} ] },
{ key: "area", name: "地区", value: [ {n:"全部",v:""}, {n:"大陆",v:"大陆"}, {n:"香港",v:"香港"}, {n:"台湾",v:"台湾"}, {n:"美国",v:"美国"}, {n:"日本",v:"日本"}, {n:"韩国",v:"韩国"}, {n:"泰国",v:"泰国"}, {n:"印度",v:"印度"}, {n:"英国",v:"英国"}, {n:"法国",v:"法国"}, {n:"德国",v:"德国"}, {n:"加拿大",v:"加拿大"}, {n:"西班牙",v:"西班牙"}, {n:"意大利",v:"意大利"}, {n:"澳大利亚",v:"澳大利亚"} ] },
{ key: "year", name: "年份", value: [ {n:"全部",v:""}, {n:"2026",v:"2026"}, {n:"2025",v:"2025"}, {n:"2024",v:"2024"}, {n:"2023",v:"2023"}, {n:"2022",v:"2022"}, {n:"2021",v:"2021"}, {n:"2020",v:"2020"}, {n:"2019",v:"2019"}, {n:"2018",v:"2018"}, {n:"2017",v:"2017"}, {n:"2016",v:"2016"}, {n:"2015-2011",v:"2015-2011"}, {n:"2010-2000",v:"2010-2000"}, {n:"90年代",v:"90年代"}, {n:"80年代",v:"80年代"}, {n:"更早",v:"更早"} ] },
{ key: "sort", name: "排序", value: [ {n:"人气",v:"hits"}, {n:"最新",v:"time"}, {n:"评分",v:"score"}, {n:"年份",v:"year"} ] }
],
"剧集": [
{ key: "class", name: "类型", value: [ {n:"全部",v:""}, {n:"爱情",v:"爱情"}, {n:"古装",v:"古装"}, {n:"武侠",v:"武侠"}, {n:"历史",v:"历史"}, {n:"家庭",v:"家庭"}, {n:"喜剧",v:"喜剧"}, {n:"悬疑",v:"悬疑"}, {n:"犯罪",v:"犯罪"}, {n:"战争",v:"战争"}, {n:"奇幻",v:"奇幻"}, {n:"科幻",v:"科幻"}, {n:"恐怖",v:"恐怖"} ] },
{ key: "area", name: "地区", value: [ {n:"全部",v:""}, {n:"大陆",v:"大陆"}, {n:"香港",v:"香港"}, {n:"台湾",v:"台湾"}, {n:"美国",v:"美国"}, {n:"日本",v:"日本"}, {n:"韩国",v:"韩国"}, {n:"泰国",v:"泰国"}, {n:"英国",v:"英国"} ] },
{ key: "year", name: "年份", value: [ {n:"全部",v:""}, {n:"2026",v:"2026"}, {n:"2025",v:"2025"}, {n:"2024",v:"2024"}, {n:"2023",v:"2023"}, {n:"2022",v:"2022"}, {n:"2021",v:"2021"}, {n:"2020-2016",v:"2020-2016"}, {n:"2015-2011",v:"2015-2011"}, {n:"2010-2000",v:"2010-2000"}, {n:"更早",v:"更早"} ] },
{ key: "sort", name: "排序", value: [ {n:"人气",v:"hits"}, {n:"最新",v:"time"}, {n:"评分",v:"score"}, {n:"年份",v:"year"} ] }
],
"动漫": [
{ key: "class", name: "类型", value: [ {n:"全部",v:""}, {n:"冒险",v:"冒险"}, {n:"奇幻",v:"奇幻"}, {n:"科幻",v:"科幻"}, {n:"武侠",v:"武侠"}, {n:"悬疑",v:"悬疑"} ] },
{ key: "area", name: "地区", value: [ {n:"全部",v:""}, {n:"大陆",v:"大陆"}, {n:"日本",v:"日本"}, {n:"欧美",v:"欧美"} ] },
{ key: "year", name: "年份", value: [ {n:"全部",v:""}, {n:"2026",v:"2026"}, {n:"2025",v:"2025"}, {n:"2024",v:"2024"}, {n:"2023",v:"2023"}, {n:"2022",v:"2022"}, {n:"2021",v:"2021"}, {n:"2020",v:"2020"}, {n:"2019",v:"2019"}, {n:"2018",v:"2018"}, {n:"2017",v:"2017"}, {n:"2016",v:"2016"}, {n:"2015",v:"2015"}, {n:"2014",v:"2014"}, {n:"2013",v:"2013"}, {n:"2012",v:"2012"}, {n:"2011",v:"2011"}, {n:"更早",v:"更早"} ] },
{ key: "sort", name: "排序", value: [ {n:"人气",v:"hits"}, {n:"最新",v:"time"}, {n:"评分",v:"score"}, {n:"年份",v:"year"} ] }
],
"综艺": [
{ key: "class", name: "类型", value: [ {n:"全部",v:""}, {n:"真人秀",v:"真人秀"}, {n:"音乐",v:"音乐"}, {n:"脱口秀",v:"脱口秀"}, {n:"歌舞",v:"歌舞"}, {n:"爱情",v:"爱情"} ] },
{ key: "area", name: "地区", value: [ {n:"全部",v:""}, {n:"大陆",v:"大陆"}, {n:"香港",v:"香港"}, {n:"台湾",v:"台湾"}, {n:"美国",v:"美国"}, {n:"日本",v:"日本"}, {n:"韩国",v:"韩国"} ] },
{ key: "year", name: "年份", value: [ {n:"全部",v:""}, {n:"2026",v:"2026"}, {n:"2025",v:"2025"}, {n:"2024",v:"2024"}, {n:"2023",v:"2023"}, {n:"2022",v:"2022"}, {n:"2021",v:"2021"}, {n:"2020",v:"2020"}, {n:"2019",v:"2019"}, {n:"2018",v:"2018"}, {n:"2017",v:"2017"}, {n:"2016",v:"2016"}, {n:"2015",v:"2015"}, {n:"2014",v:"2014"}, {n:"2013",v:"2013"}, {n:"2012",v:"2012"}, {n:"2011",v:"2011"}, {n:"更早",v:"更早"} ] },
{ key: "sort", name: "排序", value: [ {n:"人气",v:"hits"}, {n:"最新",v:"time"}, {n:"评分",v:"score"}, {n:"年份",v:"year"} ] }
]
}
};
return filterConfig;
}
/**
* 首页推荐视频
*/
async function homeVideoContent() {
let res = Java.req(baseUrl);
if (res.error) return Result.error('获取数据失败:' + res.error);
const doc = res.doc;
const videos = parseVideoList(doc);
return { list: videos };
}
/**
* 分类内容
*/
async function categoryContent(tid, pg, filter, extend) {
const area = extend.area || '';
const year = extend.year || '';
const cat = extend.class || '';
const sort = extend.sort || '';
let res = Java.req(`${baseUrl}/api.php/filter/vod?type_name=${tid}&class=${cat}&year=${year}&area=${area}&sort=${sort}&page=${pg}&limit=24`);
if (res.error) return Result.error('获取数据失败:' + res.error);
const result = JSON.parse(res.body);
result.list = result.data;
delete result.data;
return result;
}
/**
* 详情页
*/
async function detailContent(ids) {
let res = Java.req(`${baseUrl}/vd/${ids[0]}.html`);
if (res.error) return Result.error('详情获取失败:' + res.error);
let j = Java.req(`${baseUrl}/api.php/internal/search_aggregate?vod_id=${ids[0]}`);
const jsonData = JSON.parse(j.body);
const vods = parseDetailPage(res.doc, jsonData, ids[0]);
return { code: 1, msg: "数据列表", page: 1, pagecount: 1, limit: 1, total: 1, list: vods };
}
/**
* 搜索
*/
async function searchContent(key, quick, pg) {
let res = await Java.req(`${baseUrl}/vodsearch/${key}--/page/${pg}.html`);
const vods = [];
const items = res.doc.querySelectorAll('div.p-2 > div');
items.forEach(item => {
const link = item.querySelector('a[href*="/vd/"]');
if (!link) return;
const href = link.getAttribute('href');
const vod_id = href.match(/\/vd\/(\d+)\.html/)?.[1] || '';
const nameEl = item.querySelector('.text-\\[16px\\] span strong') ||
item.querySelector('.text-\\[16px\\] strong') ||
item.querySelector('div.ml-\\[105px\\] strong');
const vod_name = nameEl?.textContent?.trim() || '';
const img = item.querySelector('img[data-original]');
const vod_pic = img?.getAttribute('data-original') || '';
const remarkDiv = item.querySelector('div[class*="bottom-0"][class*="right-0"]') ||
item.querySelector('div[style*="gradient"]');
const vod_remarks = remarkDiv?.textContent?.trim() || '';
if (vod_id && vod_name) {
vods.push({
vod_id: vod_id,
vod_name: vod_name,
vod_pic: vod_pic,
vod_remarks: vod_remarks
});
}
});
const totalText = document.querySelector('.mac_total')?.textContent || '0';
const total = parseInt(totalText) || vods.length;
const pagecount = Math.ceil(total / 15);
const pageMatch = window.location.href.match(/page\/(\d+)\.html/);
const page = pageMatch ? parseInt(pageMatch[1]) : 1;
return {
code: 1,
msg: "数据列表",
list: vods,
page: page,
pagecount: pagecount,
limit: 15,
total: total
};
}
/**
* 播放器
*/
async function playerContent(flag, id, vipFlags) {
return { url: id, parse: 0 };
}
/* ---------------- 工具函数 ---------------- */
/**
* 提取视频列表
*/
function parseVideoList(document) {
const vods = [];
const items = document.querySelectorAll('.grid-cols-3 > div');
items.forEach(item => {
const link = item.querySelector('a');
const img = item.querySelector('img');
const remarkDiv = item.querySelector('.absolute.right-0.bottom-0.left-0');
let vod_id = '';
const href = link?.getAttribute('href') || '';
if (href.includes('/vd/')) {
vod_id = href.match(/\/vd\/(\d+)\.html/)?.[1] || '';
} else if (href.includes('.html')) {
vod_id = href.match(/(\d+)\.html/)?.[1] || '';
}
const vod = {
vod_id: vod_id,
vod_name: link?.getAttribute('title') || '',
vod_pic: img?.getAttribute('data-original') || '',
vod_remarks: remarkDiv?.textContent?.trim() || '',
};
vods.push(vod);
});
return vods;
}
/**
* 解析详情页
*/
function parseDetailPage(doc, jsonData, vod_id) {
const vod_name = doc.querySelector('.module-info-heading h1')?.textContent?.trim() || '';
const vod_pic = doc.querySelector('img[data-original]')?.getAttribute('data-original') || '';
const vod_content = doc.querySelector('.module-info-content .line-clamp-5 p')?.textContent?.trim() || '';
const infoDivs = doc.querySelectorAll('.module-info-heading > div > div');
const vod_remark = Array.from(infoDivs).map(div =>
div.textContent.replace(/[\t\n]/g, '').trim()
).filter(text => text).join(' / ');
const vod_play_from = jsonData.data?.map(item => item.site_name).filter(Boolean).join('$$$') || '';
const vod_play_url = jsonData.data?.map(item =>
item.vod_play_url?.replace(/\t+/g, '').trim()
).filter(Boolean).join('$$$') || '';
const flexItems = doc.querySelectorAll('.module-info-content .flex');
let vod_director = '';
let vod_actor = '';
flexItems.forEach(item => {
const span = item.querySelector('span');
if (span?.textContent.includes('导演')) {
const directorLinks = item.querySelectorAll('a');
if (directorLinks.length > 0) {
const vod_director = Array.from(directorLinks).map(link => link.textContent.trim());
}
} else if (span?.textContent.includes('主演')) {
const actorLinks = item.querySelectorAll('a');
if (actorLinks.length > 0) {
const vod_actor = Array.from(actorLinks).map(link => link.textContent.trim());
}
}
});
return [{
vod_id: vod_id,
vod_name: vod_name,
vod_pic: vod_pic,
vod_director: vod_director,
vod_actor: vod_actor,
vod_remark: vod_remark,
vod_content: vod_content,
vod_play_from: vod_play_from,
vod_play_url: vod_play_url
}];
}
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
import json
import sys
import re
import html as html_parser
from urllib.parse import quote
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host = "https://down.nigx.cn/hanime1.me"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': f'{self.host}/',
}
def getName(self):
return "Hanime"
def homeContent(self, filter):
classes = [
{'type_name': '最新上市', 'type_id': 'latest_rank'},
{'type_name': '裏番', 'type_id': '裏番'},
{'type_name': '泡麵番', 'type_id': '泡麵番'},
{'type_name': 'Motion Anime', 'type_id': 'Motion Anime'},
{'type_name': '3DCG', 'type_id': '3DCG'},
{'type_name': '2D動畫', 'type_id': '2D動畫'},
{'type_name': 'AI生成', 'type_id': 'AI生成'},
{'type_name': 'MMD', 'type_id': 'MMD'},
{'type_name': 'Cosplay', 'type_id': 'Cosplay'},
{'type_name': '本日排行', 'type_id': 'daily_rank'},
{'type_name': '本週排行', 'type_id': 'weekly_rank'},
{'type_name': '本月排行', 'type_id': 'monthly_rank'}
]
sort_options = [
{"n": "最新上市", "v": "最新上市"},
{"n": "最新上傳", "v": "最新上傳"},
{"n": "本日排行", "v": "本日排行"},
{"n": "本週排行", "v": "本週排行"},
{"n": "本月排行", "v": "本月排行"},
{"n": "觀看次數", "v": "觀看次數"}
]
filters = {}
for item in classes:
filters[item['type_id']] = [{"key": "sort", "name": "排序", "value": sort_options}]
return {'class': classes, 'filters': filters}
def categoryContent(self, tid, pg, filter, extend):
page = int(pg)
sort = extend.get('sort', '最新上市')
if 'rank' in tid or tid == 'latest_rank':
rank_map = {'latest_rank': '最新上市', 'daily_rank': '本日排行', 'weekly_rank': '本週排行', 'monthly_rank': '本月排行'}
url = f"{self.host}/search?sort={quote(rank_map.get(tid, '最新上市'))}&page={page}"
else:
# 分类请求必须使用 genre 参数
url = f"{self.host}/search?genre={quote(tid)}&sort={quote(sort)}&page={page}"
try:
content = self.fetch(url, headers=self.headers).text
vods = self.parse_vod_list(content)
# 提取总页数
pc_match = re.search(r'\/ (\d+)', content)
pagecount = int(pc_match.group(1)) if pc_match else page + 1
return {'list': vods, 'page': page, 'pagecount': pagecount}
except:
return {'list': []}
def parse_vod_list(self, html):
vods = []
seen = set()
# 模式1:适配搜索结果/分类页卡片布局 (video-item-container)
p1 = re.compile(r'class="video-item-container".*?href="[^"]*v=(\d+)".*?src="([^"]+)".*?class="duration">(.*?)<.*?class="title">(.*?)<', re.S)
# 模式2:适配首页/行布局 (home-rows)
p2 = re.compile(r'href="[^"]*watch\?v=(\d+)".*?src="([^"]+)".*?class="home-rows-videos-title"[^>]*>(.*?)</div>', re.S)
# 优先匹配卡片布局
for vid, pic, dur, title in p1.findall(html):
if vid not in seen:
seen.add(vid)
vods.append({
"vod_id": vid,
"vod_name": html_parser.unescape(title).strip(),
"vod_pic": pic,
"vod_remarks": dur.strip()
})
# 补充匹配行布局
if not vods:
for vid, pic, title in p2.findall(html):
if vid not in seen:
seen.add(vid)
vods.append({
"vod_id": vid,
"vod_name": html_parser.unescape(title).strip(),
"vod_pic": pic,
"vod_remarks": ""
})
return vods
def detailContent(self, ids):
vid = ids[0]
url = f"{self.host}/watch?v={vid}"
try:
html = self.fetch(url, headers=self.headers).text
title = re.search(r'<meta property="og:title" content="(.*?)"', html).group(1)
pic = re.search(r'<meta property="og:image" content="(.*?)"', html).group(1)
# 解析播放源并识别清晰度
sources = re.findall(r'<source[^>]+src="([^"]+)"', html)
if not sources:
sources = re.findall(r'https?://[^\s"\'<>]+?\.mp4[^\s"\'<>]*', html)
play_parts = []
seen_urls = set()
for s_url in sources:
s_url = html_parser.unescape(s_url).replace('&amp;', '&')
if s_url in seen_urls: continue
seen_urls.add(s_url)
# 画质识别逻辑
if '1080' in s_url: tag = "1080P"
elif '720' in s_url: tag = "720P"
else: tag = "标清"
play_parts.append(f"{tag}${s_url}")
# 画质由高到低排序
play_parts.sort(key=lambda x: 0 if "1080" in x else (1 if "720" in x else 2))
return {'list': [{
"vod_id": vid,
"vod_name": title,
"vod_pic": pic,
"vod_play_from": "Hanime",
"vod_play_url": "#".join(play_parts)
}]}
except:
return {'list': []}
def searchContent(self, key, quick, pg="1", extend=None):
url = f"{self.host}/search?query={quote(key)}&page={pg}"
try:
html = self.fetch(url, headers=self.headers).text
return {'list': self.parse_vod_list(html), 'page': pg}
except:
return {'list': []}
def playerContent(self, flag, id, vipFlags):
# 修复播放问题的关键:伪造 Referer
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://hanime1.me/', # 必须指向原站
'Connection': 'keep-alive'
}
return {'parse': 0, 'url': id, 'header': headers}
+248
View File
@@ -0,0 +1,248 @@
import re
import sys
import threading
import requests
from Crypto.Hash import MD5
sys.path.append("..")
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from urllib.parse import quote, urlparse
from base64 import b64encode, b64decode
import json
import time
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host = self.gethost()
self.did=self.getdid()
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def action(self, action):
pass
def destroy(self):
pass
def homeContent(self, filter):
data = self.getdata("/api.php/getappapi.index/initV119")
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序",
"sort": "排序"}
filters = {}
classes = []
json_data = data["type_list"]
homedata = data["banner_list"][8:]
for item in json_data:
if item["type_name"] == "全部":
continue
has_non_empty_field = False
jsontype_extend = json.loads(item["type_extend"])
homedata.extend(item["recommend_list"])
jsontype_extend["sort"] = "最新,最热,最赞"
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(",")
value_array = [{"n": value.strip(), "v": value.strip()} for value in values if
value.strip() != ""]
filters[str(item["type_id"])].append({"key": dkey, "name": dy[dkey], "value": value_array})
result = {}
result["class"] = classes
result["filters"] = filters
result["list"] = homedata[1:]
return result
def homeVideoContent(self):
pass
def categoryContent(self, tid, pg, filter, extend):
body = {"area": extend.get('area', '全部'), "year": extend.get('year', '全部'), "type_id": tid, "page": pg,
"sort": extend.get('sort', '最新'), "lang": extend.get('lang', '全部'),
"class": extend.get('class', '全部')}
result = {}
data = self.getdata("/api.php/getappapi.index/typeFilterVodList", body)
result["list"] = data["recommend_list"]
result["page"] = pg
result["pagecount"] = 9999
result["limit"] = 90
result["total"] = 999999
return result
def detailContent(self, ids):
body = f"vod_id={ids[0]}"
data = self.getdata("/api.php/getappapi.index/vodDetail", body)
vod = data["vod"]
play = []
names = []
for itt in data["vod_play_list"]:
a = []
names.append(itt["player_info"]["show"])
for it in itt['urls']:
it['user_agent'] = itt["player_info"].get("user_agent")
it["parse"] = itt["player_info"].get("parse")
a.append(f"{it['name']}${self.e64(json.dumps(it))}")
play.append("#".join(a))
vod["vod_play_from"] = "$$$".join(names)
vod["vod_play_url"] = "$$$".join(play)
result = {"list": [vod]}
return result
def searchContent(self, key, quick, pg="1"):
body = f"keywords={key}&type_id=0&page={pg}"
data = self.getdata("/api.php/getappapi.index/searchList", body)
result = {"list": data["search_list"], "page": pg}
return result
def playerContent(self, flag, id, vipFlags):
ids = json.loads(self.d64(id))
h = {"User-Agent": (ids['user_agent'] or "okhttp/3.14.9")}
try:
if re.search(r'url=', ids['parse_api_url']):
data = self.fetch(ids['parse_api_url'], headers=h, timeout=10).json()
url = data.get('url') or data['data'].get('url')
else:
body = f"parse_api={ids.get('parse') or ids['parse_api_url'].replace(ids['url'], '')}&url={quote(self.aes(ids['url'], True))}&token={ids.get('token')}"
b = self.getdata("/api.php/getappapi.index/vodParse", body)['json']
url = json.loads(b)['url']
if 'error' in url: raise ValueError(f"解析失败: {url}")
p = 0
except Exception as e:
print('错误信息:', e)
url, p = ids['url'], 1
if re.search(r'\.jpg|\.png|\.jpeg', url):
url = self.Mproxy(url)
result = {}
result["parse"] = p
result["url"] = url
result["header"] = h
return result
def localProxy(self, param):
return self.Mlocal(param)
def gethost(self):
headers = {
'User-Agent': 'okhttp/3.14.9'
}
response = self.fetch('https://miget-1313189639.cos.ap-guangzhou.myqcloud.com/mifun.txt',headers=headers).text
return self.host_late(response.split('\n'))
def host_late(self, url_list):
if isinstance(url_list, str):
urls = [u.strip() for u in url_list.split(',')]
else:
urls = url_list
if len(urls) <= 1:
return urls[0] if urls else ''
results = {}
threads = []
def test_host(url):
try:
url = url.strip()
start_time = time.time()
response = requests.head(url, timeout=1.0, allow_redirects=False)
delay = (time.time() - start_time) * 1000
results[url] = delay
except Exception as e:
results[url] = float('inf')
for url in urls:
t = threading.Thread(target=test_host, args=(url,))
threads.append(t)
t.start()
for t in threads:
t.join()
return min(results.items(), key=lambda x: x[1])[0]
def getdid(self):
did=self.getCache('did')
if not did:
t = str(int(time.time()))
did = self.md5(t)
self.setCache('did', did)
return did
def aes(self, text, b=None):
key = b"GETMIFUNGEIMIFUN"
cipher = AES.new(key, AES.MODE_CBC, key)
if b:
ct_bytes = cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))
ct = b64encode(ct_bytes).decode("utf-8")
return ct
else:
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
return pt.decode("utf-8")
def header(self):
t = str(int(time.time()))
header = {"Referer": self.host,
"User-Agent": "okhttp/3.14.9", "app-version-code": "516", "app-ui-mode": "light",
"app-api-verify-time": t, "app-user-device-id": self.did,
"app-api-verify-sign": self.aes(t, True),
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
return header
def getdata(self, path, data=None):
vdata = self.post(f"{self.host}{path}", headers=self.header(), data=data, timeout=10).json()['data']
data1 = self.aes(vdata)
return json.loads(data1)
def Mproxy(self, url):
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=m3u8"
def Mlocal(self, param, header=None):
url = self.d64(param["url"])
ydata = self.fetch(url, headers=header, allow_redirects=False)
data = ydata.content.decode('utf-8')
if ydata.headers.get('Location'):
url = ydata.headers['Location']
data = self.fetch(url, headers=header).content.decode('utf-8')
parsed_url = urlparse(url)
durl = parsed_url.scheme + "://" + parsed_url.netloc
lines = data.strip().split('\n')
for index, string in enumerate(lines):
if '#EXT' not in string and 'http' not in string:
last_slash_index = string.rfind('/')
lpath = string[:last_slash_index + 1]
lines[index] = durl + ('' if lpath.startswith('/') else '/') + lpath
data = '\n'.join(lines)
return [200, "application/vnd.apple.mpegur", data]
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()
+248
View File
@@ -0,0 +1,248 @@
import re
import sys
import threading
import requests
from Crypto.Hash import MD5
sys.path.append("..")
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from urllib.parse import quote, urlparse
from base64 import b64encode, b64decode
import json
import time
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host = self.gethost()
self.did=self.getdid()
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def action(self, action):
pass
def destroy(self):
pass
def homeContent(self, filter):
data = self.getdata("/api.php/getappapi.index/initV119")
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序",
"sort": "排序"}
filters = {}
classes = []
json_data = data["type_list"]
homedata = data["banner_list"][8:]
for item in json_data:
if item["type_name"] == "全部":
continue
has_non_empty_field = False
jsontype_extend = json.loads(item["type_extend"])
homedata.extend(item["recommend_list"])
jsontype_extend["sort"] = "最新,最热,最赞"
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(",")
value_array = [{"n": value.strip(), "v": value.strip()} for value in values if
value.strip() != ""]
filters[str(item["type_id"])].append({"key": dkey, "name": dy[dkey], "value": value_array})
result = {}
result["class"] = classes
result["filters"] = filters
result["list"] = homedata[1:]
return result
def homeVideoContent(self):
pass
def categoryContent(self, tid, pg, filter, extend):
body = {"area": extend.get('area', '全部'), "year": extend.get('year', '全部'), "type_id": tid, "page": pg,
"sort": extend.get('sort', '最新'), "lang": extend.get('lang', '全部'),
"class": extend.get('class', '全部')}
result = {}
data = self.getdata("/api.php/getappapi.index/typeFilterVodList", body)
result["list"] = data["recommend_list"]
result["page"] = pg
result["pagecount"] = 9999
result["limit"] = 90
result["total"] = 999999
return result
def detailContent(self, ids):
body = f"vod_id={ids[0]}"
data = self.getdata("/api.php/getappapi.index/vodDetail", body)
vod = data["vod"]
play = []
names = []
for itt in data["vod_play_list"]:
a = []
names.append(itt["player_info"]["show"])
for it in itt['urls']:
it['user_agent'] = itt["player_info"].get("user_agent")
it["parse"] = itt["player_info"].get("parse")
a.append(f"{it['name']}${self.e64(json.dumps(it))}")
play.append("#".join(a))
vod["vod_play_from"] = "$$$".join(names)
vod["vod_play_url"] = "$$$".join(play)
result = {"list": [vod]}
return result
def searchContent(self, key, quick, pg="1"):
body = f"keywords={key}&type_id=0&page={pg}"
data = self.getdata("/api.php/getappapi.index/searchList", body)
result = {"list": data["search_list"], "page": pg}
return result
def playerContent(self, flag, id, vipFlags):
ids = json.loads(self.d64(id))
h = {"User-Agent": (ids['user_agent'] or "okhttp/3.14.9")}
try:
if re.search(r'url=', ids['parse_api_url']):
data = self.fetch(ids['parse_api_url'], headers=h, timeout=10).json()
url = data.get('url') or data['data'].get('url')
else:
body = f"parse_api={ids.get('parse') or ids['parse_api_url'].replace(ids['url'], '')}&url={quote(self.aes(ids['url'], True))}&token={ids.get('token')}"
b = self.getdata("/api.php/getappapi.index/vodParse", body)['json']
url = json.loads(b)['url']
if 'error' in url: raise ValueError(f"解析失败: {url}")
p = 0
except Exception as e:
print('错误信息:', e)
url, p = ids['url'], 1
if re.search(r'\.jpg|\.png|\.jpeg', url):
url = self.Mproxy(url)
result = {}
result["parse"] = p
result["url"] = url
result["header"] = h
return result
def localProxy(self, param):
return self.Mlocal(param)
def gethost(self):
headers = {
'User-Agent': 'okhttp/3.14.9'
}
response = self.fetch('https://miget-1313189639.cos.ap-guangzhou.myqcloud.com/mifun.txt',headers=headers).text
return self.host_late(response.split('\n'))
def host_late(self, url_list):
if isinstance(url_list, str):
urls = [u.strip() for u in url_list.split(',')]
else:
urls = url_list
if len(urls) <= 1:
return urls[0] if urls else ''
results = {}
threads = []
def test_host(url):
try:
url = url.strip()
start_time = time.time()
response = requests.head(url, timeout=1.0, allow_redirects=False)
delay = (time.time() - start_time) * 1000
results[url] = delay
except Exception as e:
results[url] = float('inf')
for url in urls:
t = threading.Thread(target=test_host, args=(url,))
threads.append(t)
t.start()
for t in threads:
t.join()
return min(results.items(), key=lambda x: x[1])[0]
def getdid(self):
did=self.getCache('did')
if not did:
t = str(int(time.time()))
did = self.md5(t)
self.setCache('did', did)
return did
def aes(self, text, b=None):
key = b"GETMIFUNGEIMIFUN"
cipher = AES.new(key, AES.MODE_CBC, key)
if b:
ct_bytes = cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))
ct = b64encode(ct_bytes).decode("utf-8")
return ct
else:
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
return pt.decode("utf-8")
def header(self):
t = str(int(time.time()))
header = {"Referer": self.host,
"User-Agent": "okhttp/3.14.9", "app-version-code": "516", "app-ui-mode": "light",
"app-api-verify-time": t, "app-user-device-id": self.did,
"app-api-verify-sign": self.aes(t, True),
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
return header
def getdata(self, path, data=None):
vdata = self.post(f"{self.host}{path}", headers=self.header(), data=data, timeout=10).json()['data']
data1 = self.aes(vdata)
return json.loads(data1)
def Mproxy(self, url):
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=m3u8"
def Mlocal(self, param, header=None):
url = self.d64(param["url"])
ydata = self.fetch(url, headers=header, allow_redirects=False)
data = ydata.content.decode('utf-8')
if ydata.headers.get('Location'):
url = ydata.headers['Location']
data = self.fetch(url, headers=header).content.decode('utf-8')
parsed_url = urlparse(url)
durl = parsed_url.scheme + "://" + parsed_url.netloc
lines = data.strip().split('\n')
for index, string in enumerate(lines):
if '#EXT' not in string and 'http' not in string:
last_slash_index = string.rfind('/')
lpath = string[:last_slash_index + 1]
lines[index] = durl + ('' if lpath.startswith('/') else '/') + lpath
data = '\n'.join(lines)
return [200, "application/vnd.apple.mpegur", data]
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()
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
# 本资源来源于互联网公开渠道,仅可用于个人学习及爬虫技术交流。
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
import re,sys,uuid
from base.spider import Spider
sys.path.append('..')
class Spider(Spider):
host,config,local_uuid,parsing_config = '','','',[]
# 头部添加token认证
headers = {
'User-Agent': "Dart/2.19 (dart:io)",
'Accept-Encoding': "gzip",
'appto-local-uuid': local_uuid,
'token': "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJfY2hlY2siOiI4ZTEyNDE1Y2UyOGQzMGM4MWE3MDBiNWYxMDgzZTU2OCIsInVzZXJfaWQiOjM0NTYsInVzZXJfbmFtZSI6IjEwMTAxMiJ9LCJleHAiOjE4MDQ3MzkyODAuNjA4MTA4MywiaWF0IjoxNzczMjAzMjgxLCJpc3MiOiJBcHBUbyIsImp0aSI6ImZmZDMyYjk4N2VkMTg1ZjNiNGQ5Zjc5NzU2YWRjNGQ5IiwibmJmIjoxNzczMjAzMjgxLCJzdWIiOiJBcHBUbyJ9.tDhURwWVzsPy0-yXvo_d3bgsmoq9Ri5n0Y4fQsvxKy0"
}
def init(self, extend=''):
try:
host = extend.strip()
if not host.startswith('http'):
return {}
if not re.match(r'^https?://[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?/?$', host):
host_=self.fetch(host).json()
self.host = host_['domain']
else:
self.host = host
self.local_uuid = str(uuid.uuid4())
# 动态更新headers中的uuid(避免初始化时uuid为空)
self.headers['appto-local-uuid'] = self.local_uuid
response = self.fetch(f'{self.host}/apptov5/v1/config/get?p=android&__platform=android', headers=self.headers).json()
config = response['data']
self.config = config
parsing_conf = config['get_parsing']['lists']
parsing_config = {}
for i in parsing_conf:
if len(i['config']) != 0:
label = []
for j in i['config']:
if j['type'] == 'json':
label.append(j['label'])
parsing_config.update({i['key']:label})
self.parsing_config = parsing_config
return None
except Exception as e:
print(f'初始化异常:{e}')
return {}
def detailContent(self, ids):
response = self.fetch(f"{self.host}/apptov5/v1/vod/getVod?id={ids[0]}",headers=self.headers).json()
data3 = response['data']
videos = []
vod_play_url = ''
vod_play_from = ''
for i in data3['vod_play_list']:
play_url = ''
for j in i['urls']:
play_url += f"{j['name']}${i['player_info']['from']}@{j['url']}#"
vod_play_from += i['player_info']['show'] + '$$$'
vod_play_url += play_url.rstrip('#') + '$$$'
vod_play_url = vod_play_url.rstrip('$$$')
vod_play_from = vod_play_from.rstrip('$$$')
videos.append({
'vod_id': data3.get('vod_id'),
'vod_name': data3.get('vod_name'),
'vod_content': data3.get('vod_content'),
'vod_remarks': data3.get('vod_remarks'),
'vod_director': data3.get('vod_director'),
'vod_actor': data3.get('vod_actor'),
'vod_year': data3.get('vod_year'),
'vod_area': data3.get('vod_area'),
'vod_play_from': vod_play_from,
'vod_play_url': vod_play_url
})
return {'list': videos}
def searchContent(self, key, quick, pg='1'):
url = f"{self.host}/apptov5/v1/search/lists?wd={key}&page={pg}&type=&__platform=android"
response = self.fetch(url, headers=self.headers).json()
data = response['data']['data']
for i in data:
if i.get('vod_pic').startswith('mac://'):
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
return {'list': data, 'page': pg, 'total': response['data']['total']}
def playerContent(self, flag, id, vipflags):
default_ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
parsing_config = self.parsing_config
parts = id.split('@')
if len(parts) != 2:
return {'parse': 0, 'url': id, 'header': {'User-Agent': default_ua}}
playfrom, rawurl = parts
label_list = parsing_config.get(playfrom)
if not label_list:
return {'parse': 0, 'url': rawurl, 'header': {'User-Agent': default_ua}}
result = {'parse': 1, 'url': rawurl, 'header': {'User-Agent': default_ua}}
for label in label_list:
payload = {
'play_url': rawurl,
'label': label,
'key': playfrom
}
try:
response = self.post(
f"{self.host}/apptov5/v1/parsing/proxy?__platform=android",
data=payload,
headers=self.headers
).json()
except Exception as e:
print(f"请求异常: {e}")
continue
if not isinstance(response, dict):
continue
if response.get('code') == 422:
continue
data = response.get('data')
if not isinstance(data, dict):
continue
url = data.get('url')
if not url:
continue
ua = data.get('UA') or data.get('UserAgent') or default_ua
result = {
'parse': 0,
'url': url,
'header': {'User-Agent': ua}
}
break
return result
def homeContent(self, filter):
config = self.config
if not config:
return {}
home_cate = config['get_home_cate']
classes = []
for i in home_cate:
if isinstance(i.get('extend', []),dict):
classes.append({'type_id': i['cate'], 'type_name': i['title']})
return {'class': classes}
def homeVideoContent(self):
response = self.fetch(f'{self.host}/apptov5/v1/home/data?id=1&mold=1&__platform=android',headers=self.headers).json()
data = response['data']
vod_list = []
for i in data['sections']:
for j in i['items']:
vod_pic = j.get('vod_pic')
if vod_pic.startswith('mac://'):
vod_pic = vod_pic.replace('mac://', 'http://', 1)
vod_list.append({
"vod_id": j.get('vod_id'),
"vod_name": j.get('vod_name'),
"vod_pic": vod_pic,
"vod_remarks": j.get('vod_remarks')
})
return {'list': vod_list}
def categoryContent(self, tid, pg, filter, extend):
response = self.fetch(f"{self.host}/apptov5/v1/vod/lists?area={extend.get('area','')}&lang={extend.get('lang','')}&year={extend.get('year','')}&order={extend.get('sort','time')}&type_id={tid}&type_name=&page={pg}&pageSize=21&__platform=android", headers=self.headers).json()
data = response['data']
data2 = data['data']
for i in data['data']:
if i.get('vod_pic','').startswith('mac://'):
i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1)
return {'list': data2, 'page': pg, 'total': data['total']}
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
def localProxy(self, param):
pass
+948
View File
@@ -0,0 +1,948 @@
# -*- coding: utf-8 -*-
import re
import ssl
import json
import html
import base64
import urllib3
import threading
import time
import sys
from urllib.parse import quote, unquote, urljoin, urlparse
import requests
from requests.adapters import HTTPAdapter
from pyquery import PyQuery as pq
sys.path.append("..")
from base.spider import Spider as BaseSpider
urllib3.disable_warnings()
class SSLAdapter(HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False, **kwargs):
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
kwargs["ssl_context"] = ctx
return super().init_poolmanager(connections, maxsize, block=block, **kwargs)
def proxy_manager_for(self, proxy, **kwargs):
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
kwargs["ssl_context"] = ctx
return super().proxy_manager_for(proxy, **kwargs)
class Spider(BaseSpider):
hosts = [
"https://www.qwmkv.com",
"https://www.qwnull.com",
"https://www.qwfilm.com",
"https://www.qnmp4.com",
"https://www.qn63.com"
]
host = hosts[0]
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 12; M2012K11AC) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1"
}
CATEGORY_IDS = {"电影": 1, "剧集": 2, "综艺": 3, "动漫": 4, "短剧": 30}
KEYWORDS = ["杜比", "dolby", "原盘", "高码", "remux", "蓝光", "hdr10+", "hdr10", "hdr", "4k", "2160p", "uhd"]
QUARK_CHECK_LIMIT = 100
CHECK_TIME_BUDGET = 12.0 # 检测最多12秒
def getName(self):
return "七味-最终稳定版(含大屏分组排序+防串位修复)"
def init(self, extend=""):
self.session = requests.Session()
adapter = SSLAdapter(max_retries=2)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
self.session.verify = False
self.session.headers.update(dict(self.headers))
self.last_vod_pic = ""
self.vod_pic_cache = {}
self.pan_115_cookie = ""
self.ack_mp4 = "https://vd2.bdstatic.com/mda-nj5kxa8kr7wgq6ie/sc/cae_h264_nowatermark/1653272065989267185/mda-nj5kxa8kr7wgq6ie.mp4"
if extend:
try:
ext = json.loads(extend)
self.pan_115_cookie = ext.get("pan_115_cookie", "")
self.ack_mp4 = ext.get("ack_mp4", self.ack_mp4)
except Exception as e:
print(f"init extend error: {e}")
self._probe_host()
def destroy(self):
try:
self.session.close()
except:
pass
# ---------------- utils ----------------
def _probe_host(self):
for h in self.hosts:
try:
r = self.session.get(h + "/", timeout=6, headers=self.headers, verify=False)
if r.status_code == 200:
self.host = h
return
except:
pass
def _full_url(self, path):
if not path:
return ""
path = html.unescape(str(path)).strip()
if path.startswith("//"):
return "https:" + path
if path.startswith(("http://", "https://", "magnet:?")):
return path
return urljoin(self.host + "/", path)
def _full_url_by_host(self, host, path):
if not path:
return ""
path = html.unescape(str(path)).strip()
if path.startswith("//"):
return "https:" + path
if path.startswith(("http://", "https://", "magnet:?")):
return path
return urljoin(host.rstrip("/") + "/", path.lstrip("/"))
def _fetch(self, url, timeout=10):
tries = [self._full_url(url)]
if isinstance(url, str) and not url.startswith(("http://", "https://", "magnet:?")):
for h in self.hosts:
u = urljoin(h + "/", url)
if u not in tries:
tries.append(u)
for u in tries:
try:
h = dict(self.headers)
h["Referer"] = self.host + "/"
r = self.session.get(u, timeout=timeout, headers=h, verify=False)
r.encoding = r.apparent_encoding or "utf-8"
if r.status_code == 200 and len(r.text or "") > 30:
for hh in self.hosts:
if u.startswith(hh):
self.host = hh
break
return r
except:
continue
return None
def _pq(self, url, timeout=10):
r = self._fetch(url, timeout=timeout)
return pq(r.text if r else "")
def _clean_text(self, s):
return re.sub(r"\s+", " ", html.unescape(s or "")).strip()
def _clean_name(self, s, max_len=120):
s = html.unescape(s or "").replace("#", "").replace("$", "")
s = re.sub(r"\s+", " ", s).strip()
return s[:max_len]
def _img_src(self, img):
return img.attr("data-src") or img.attr("data-original") or img.attr("src") or ""
def _is_pan(self, u):
u = (u or "").lower()
return any(k in u for k in [
"pan.quark.cn/s/", "pan.baidu.com/s/", "drive.uc.cn/s/",
"pan.xunlei.com/s/", "aliyundrive.com/s/", "alipan.com/s/",
"cloud.189.cn/", "caiyun.139.com/", "123pan.com/s/",
"115.com/s/", "lanzou", "lanzoui", "lanzoux", "lanzoub"
])
def _b64e(self, obj):
txt = json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
return base64.urlsafe_b64encode(txt.encode()).decode().rstrip("=")
def _b64d(self, s):
try:
s += "=" * (-len(s) % 4)
return json.loads(base64.urlsafe_b64decode(s.encode()).decode())
except:
return {}
def _get_mid(self, tid):
if str(tid).isdigit():
return int(tid)
m = re.search(r"/vt/(\d+)", str(tid))
if m:
return int(m.group(1))
return 1
def _score_name(self, name):
n = (name or "").lower()
score = 0
for i, kw in enumerate(self.KEYWORDS):
if kw.lower() in n:
score += (len(self.KEYWORDS) - i)
return score
def _extract_video_list(self, doc):
videos, seen = [], set()
selectors = ["ul.pic-list li", "ul.content-list li", ".pic-list li", ".content-list li"]
nodes = []
for sel in selectors:
n = list(doc(sel).items())
if n:
nodes = n
break
for li in nodes:
a = li("a[href]").eq(0)
href = a.attr("href")
if not href:
continue
vid = self._full_url(href)
if vid in seen:
continue
seen.add(vid)
img = li("img").eq(0)
pic = self._full_url(self._img_src(img))
title = a.attr("title") or img.attr("alt") or li("h3 b").text() or li("h3").text() or ""
remark = self._clean_text(li("span.s1").text() or li("span.s2").text() or li("p").text() or li(".tag").text())
if pic:
self.vod_pic_cache[vid] = pic
videos.append({
"vod_id": vid,
"vod_name": self._clean_name(title, 80),
"vod_pic": pic,
"vod_remarks": remark
})
return videos
def _is_bad_cover(self, u):
if not u:
return True
s = u.lower()
return ("logo.png" in s) or ("loading" in s) or ("/template/piankuwap/image/logo" in s)
def _normalize_magnet(self, href):
try:
if not href:
return ""
href = str(href).strip().replace("&amp;", "&")
if href.startswith("push://"):
href = href.replace("push://", "", 1).replace("#0agent", "")
if "%3A" in href or "%3F" in href or "%26" in href:
href = unquote(href)
href = re.sub(r"\s+", "", href)
if not href.startswith("magnet:") or "urn:btih:" not in href:
return ""
return href
except:
return ""
def _magnet_btih(self, magnet):
m = self._normalize_magnet(magnet)
if not m:
return ""
g = re.search(r"xt=urn:btih:([a-zA-Z0-9]+)", m, re.I)
return g.group(1).lower() if g else ""
def _is_verify_page(self, text):
t = (text or "").lower()
return (
("系统安全验证" in t) or
("verify_check" in t) or
("mac_verify_img" in t) or
("请输入验证码" in t)
)
def _mk_vod_id(self, h, raw_id, raw_url=""):
if raw_url:
u = self._full_url_by_host(h, raw_url)
if "/mv/" in u and u.endswith(".html"):
return u
if str(raw_id).isdigit():
return f"{h}/mv/{raw_id}.html"
m = re.search(r"/mv/(\d+)\.html", u)
if m:
return f"{h}/mv/{m.group(1)}.html"
return u
rid = str(raw_id or "").strip()
if rid.isdigit():
return f"{h}/mv/{rid}.html"
if rid.startswith(("http://", "https://", "/")):
return self._full_url_by_host(h, rid)
return f"{h}/mv/{rid}.html" if rid else ""
# ---------------- only check quark/115 ----------------
def _check_pan_valid(self, url, provider, timeout=3):
if not url:
return False
if provider not in ("quark", "115"):
return True
try:
h = dict(self.headers)
h["Referer"] = self.host + "/"
r = requests.get(url, headers=h, timeout=timeout, verify=False, allow_redirects=True)
if r.status_code >= 400:
return False
text = (r.text or "").lower()
if provider == "quark":
keys = ["分享已失效", "不存在", "已被取消", "取消", "删除", "已被删除", "来晚了", "违规", "无法访问"]
else:
keys = ["分享已失效", "不存在", "404", "已取消", "链接错误"]
return not any(k in text for k in keys)
except:
return False
# ---------------- home ----------------
def homeContent(self, filter):
classes = [
{"type_name": "大陆电影", "type_id": "https://www.qwmkv.com/ms/1-大陆-time---------.html"},
{"type_name": "大陆剧集", "type_id": "https://www.qwmkv.com/ms/2-大陆-time---------.html"},
{"type_name": "大陆综艺", "type_id": "https://www.qwmkv.com/ms/3-大陆-time---------.html"},
{"type_name": "大陆动漫", "type_id": "https://www.qwmkv.com/ms/4-大陆-time---------.html"},
{"type_name": "电影", "type_id": "/vt/1.html"},
{"type_name": "综艺", "type_id": "/vt/3.html"},
{"type_name": "剧集", "type_id": "/vt/2.html"},
{"type_name": "动漫", "type_id": "/vt/4.html"},
{"type_name": "短剧", "type_id": "/vt/30.html"},
]
return {"class": classes}
def homeVideoContent(self):
doc = self._pq(self.host + "/")
videos = self._extract_video_list(doc)
return {"list": videos, "page": 1, "pagecount": 1, "limit": len(videos), "total": len(videos)}
# ---------------- category ----------------
def _build_category_url(self, tid, pg, fdict):
if isinstance(tid, str) and tid.startswith(("http://", "https://")):
if pg <= 1:
return tid
if "---------.html" in tid:
return tid.replace("---------.html", f"------{pg}---.html")
return tid.replace(".html", f"-{pg}.html")
mid = self._get_mid(tid)
if not fdict:
return f"{self.host}/vt/{mid}.html" if pg <= 1 else f"{self.host}/vt/{mid}-{pg}.html"
area = quote(fdict.get("地区", ""), safe="")
sort = ""
sv = fdict.get("排序", "")
if sv == "按时间":
sort = "time"
elif sv == "按人气":
sort = "hits"
elif sv == "按评分":
sort = "score"
typ = quote(fdict.get("类型", ""), safe="")
lang = quote(fdict.get("语言", ""), safe="")
year = fdict.get("年代", "")
fields = [area, sort, typ, lang, "", "", "", "", year]
base = f"{self.host}/ms/{mid}-" + "-".join(fields)
return base + ".html" if pg <= 1 else base + f"-{pg}.html"
def categoryContent(self, tid, pg, filter, extend):
pg = int(pg) if str(pg).isdigit() else 1
fdict = extend if isinstance(extend, dict) else {}
url = self._build_category_url(tid, pg, fdict)
doc = self._pq(url)
if len(doc("ul.pic-list li")) == 0 and len(doc("ul.content-list li")) == 0 and pg > 1:
doc = self._pq(url.replace(".html", f".html?page={pg}"))
videos = self._extract_video_list(doc)
page_count = pg
for a in doc(".pages a").items():
t = (a.text() or "").strip()
href = a.attr("href") or ""
if t.isdigit():
page_count = max(page_count, int(t))
else:
m = re.search(r"-(\d+)\.html", href)
if m:
page_count = max(page_count, int(m.group(1)))
return {
"list": videos,
"page": pg,
"pagecount": max(page_count, pg),
"limit": 30,
"total": max(page_count, pg) * 30
}
# ---------------- detail ----------------
def detailContent(self, ids):
try:
vod_id = ids[0] if isinstance(ids, list) and ids else ids
vod_id = self._full_url(vod_id)
doc = self._pq(vod_id)
raw = doc.html() or ""
# 1. 抓取基本影视信息
title = self._clean_text(doc("h1").eq(0).text())
if not title:
tt = self._clean_text(doc("title").text())
title = tt.split("在线观看")[0] if tt else "七味资源"
cover = ""
og = self._full_url(doc('meta[property="og:image"]').attr("content") or "")
if og and not self._is_bad_cover(og):
cover = og
if not cover:
c1 = self._full_url(self._img_src(doc(".main-left .img img").eq(0)))
if c1 and not self._is_bad_cover(c1):
cover = c1
if not cover:
for im in doc("img").items():
src = self._full_url(self._img_src(im))
if src and not self._is_bad_cover(src):
cover = src
break
if not cover:
cover = self.vod_pic_cache.get(vod_id, "")
if not cover:
cover = self._full_url("/template/piankuwap/image/logo.png")
self.last_vod_pic = cover
content = self._clean_text(
doc(".movie-introduce .sqjj_a").text() or
doc(".movie-introduce .zkjj_a").text() or
doc(".content").text()
)
# =====【优化修复】2. 多线路合并遍历抓取在线资源(彻底杜绝选择器冲突覆盖) =====
online_routes = {} # 格式: {"在线线路1": ["第1集$payload", "第2集$payload"]}
player_uls = doc("div#url .bd ul.player, ul.player")
line_no = 1
for ul_node in player_uls.items():
links = list(ul_node("a[href]").items())
if not links:
continue
route_name = f"📺在线播放-线路{line_no}"
episodes = []
for a in links:
href = a.attr("href")
if not href:
continue
src_name = self._clean_name(self._clean_text(a.text()) or "播放", 50)
payload = self._b64e({"type": "py", "url": self._full_url(href), "pic": cover})
episodes.append(f"{src_name}${payload}")
if episodes:
online_routes[route_name] = episodes
line_no += 1
# ===== 3. 规整网盘与磁力资源链接 =====
pan_resources = []
magnet_raw = []
seen_pan = set()
for a in doc("a[href]").items():
u = html.unescape(a.attr("href") or "").strip()
if not u:
continue
txt = self._clean_text(a.text())
if u.lower().startswith("magnet:?"):
magnet_raw.append((u, self._clean_name(txt or "磁力资源", 60)))
continue
if self._is_pan(u):
low = u.lower()
pv = "other"
if "pan.quark" in low: pv = "quark"
elif "115.com" in low: pv = "115"
elif "pan.baidu" in low: pv = "baidu"
elif "drive.uc.cn" in low: pv = "uc"
elif "pan.xunlei" in low: pv = "xunlei"
elif "aliyundrive" in low or "alipan" in low: pv = "ali"
elif "cloud.189" in low: pv = "189"
elif "123pan" in low: pv = "pan123"
if u not in seen_pan:
seen_pan.add(u)
pan_resources.append({
"provider": pv,
"url": u,
"name": txt or "网盘资源",
"checked_valid": False
})
for m in re.finditer(r"magnet:\?[^\s\"'<>]+", raw, re.I):
magnet_raw.append((html.unescape(m.group(0)), "磁力资源"))
# 磁力资源精简去重并打分排序
magnet_unified = []
btih_seen = set()
for u, n in magnet_raw:
mu = self._normalize_magnet(u)
if not mu:
continue
btih = self._magnet_btih(mu)
key = btih if btih else mu.lower()
if key in btih_seen:
continue
btih_seen.add(key)
magnet_unified.append({
"url": mu,
"name": self._clean_name(n or "磁力资源", 60)
})
magnet_unified.sort(key=lambda x: -self._score_name(x.get("name", "")))
# ===== 4. 网盘有效性探针(保持原有超时/计数策略) =====
check_begin = time.monotonic()
quark_checked = 0
valid_pan = []
for p in pan_resources:
if time.monotonic() - check_begin >= self.CHECK_TIME_BUDGET:
valid_pan.append(p)
continue
pv = p["provider"]
if pv == "quark":
if quark_checked >= self.QUARK_CHECK_LIMIT:
valid_pan.append(p)
continue
quark_checked += 1
if self._check_pan_valid(p["url"], "quark"):
p["checked_valid"] = True
valid_pan.append(p)
elif pv == "115":
if self._check_pan_valid(p["url"], "115"):
p["checked_valid"] = True
valid_pan.append(p)
else:
valid_pan.append(p)
# =====【功能修复】5. 网盘资源渠道完全独立隔离,防止错乱混杂 =====
pan_routes = {
"quark": {"name": "🟢夸克网盘", "list": []},
"ali": {"name": "☁️阿里云盘", "list": []},
"115": {"name": "固定115网盘", "list": []},
"baidu": {"name": "📘百度网盘", "list": []},
"uc": {"name": "📱UC网盘", "list": []},
"xunlei": {"name": "⚡迅雷网盘", "list": []},
"189": {"name": "☎️天翼云盘", "list": []},
"pan123": {"name": "📦123网盘", "list": []},
"other": {"name": "📦其它网盘", "list": []}
}
for r in valid_pan:
prov = r["provider"]
if prov not in pan_routes:
prov = "other"
ep_name = self._clean_name(r.get('name', '网盘提取资源'), 60)
payload = self._b64e({"type": "pan", "url": r["url"], "pic": cover})
pan_routes[prov]["list"].append(f"{ep_name}${payload}")
# =====【核心修复】6. 按标准大屏壳子1:1规则完美有序组装,防串位 =====
play_from = []
play_url = []
# 分支 A:写入网盘分类线路(依照预设的网盘体验优先级高低呈现)
drive_order = ["quark", "ali", "115", "baidu", "uc", "xunlei", "189", "pan123", "other"]
for d_key in drive_order:
route_info = pan_routes[d_key]
if route_info["list"]:
play_from.append(route_info["name"])
play_url.append("#".join(route_info["list"]))
# 分支 B:写入磁力解析相关线路(保持互相隔离)
if magnet_unified:
lines_115 = []
lines_play = []
for i, m in enumerate(magnet_unified, start=1):
nm = self._clean_name(f"磁力源-{i:02d} {m['name']}", 60)
encoded_mag = base64.urlsafe_b64encode(m['url'].encode()).decode().rstrip("=")
p_payload = self._b64e({"type": "magnet", "url": m['url'], "pic": cover})
lines_115.append(f"{nm}${encoded_mag}")
lines_play.append(f"{nm}${p_payload}")
# 独立线路一:115离线专用线
play_from.append("📥115云下载")
play_url.append("#".join(lines_115))
# 独立一条空白ACK确认交互线
play_from.append("0")
play_url.append("已提交请到115离线任务查看$__ACK__")
# 独立线路二:自带流播或通过本地壳嗅探弹磁力
play_from.append("🧲磁力播放")
play_url.append("#".join(lines_play))
# 分支 C:写入在线直连/网页采集线路
for r_name, r_eps in online_routes.items():
play_from.append(r_name)
play_url.append("#".join(r_eps))
# ===== 当前站搜索入口 =====
try:
search_payload = self._b64e({
"type": "search",
"wd": title,
"pic": cover
})
play_from.insert(0, "🔍点击选择")
play_url.insert(0, f"当前站搜索${search_payload}")
except Exception as e:
print(f"search line add error: {e}")
# 兜底处理
if not play_from:
play_from.append("🌐原网页查看")
play_url.append(f"点击跳转原详情页${self._b64e({'type': 'web', 'url': vod_id, 'pic': cover})}")
# 7. 构建标准影视输出字典
vod = {
"vod_id": vod_id,
"vod_name": self._clean_name(title, 100),
"vod_pic": cover,
"vod_content": content,
"vod_play_from": "$$$".join(play_from),
"vod_play_url": "$$$".join(play_url)
}
return {"list": [vod]}
except Exception as e:
print(f"detailContent error: {e}")
return {"list": []}
# ---------------- player ----------------
def _parse_py_page(self, py_url):
r = self._fetch(py_url, timeout=10)
if not r:
return ""
txt = r.text or ""
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*<", txt, re.S)
if not m:
m = re.search(r"player_aaaa\s*=\s*(\{.*?\})\s*;", txt, re.S)
if not m:
return ""
js = m.group(1)
try:
js2 = re.sub(r"(\w+)\s*:", r'"\1":', js)
obj = json.loads(js2)
except:
try:
obj = json.loads(js)
except:
return ""
u = obj.get("url", "") or ""
enc = str(obj.get("encrypt", "0"))
if enc == "1":
u = unquote(u)
elif enc == "2":
try:
u = unquote(base64.b64decode(u).decode("utf-8", "ignore"))
except:
pass
if u.startswith("//"):
u = "https:" + u
elif u.startswith("/"):
u = self._full_url(u)
return u
def _return_ack_video(self):
ret = {
"parse": 0,
"playUrl": "",
"url": self.ack_mp4,
"header": {
"User-Agent": self.headers.get("User-Agent", ""),
"Referer": self.host + "/"
}
}
if self.last_vod_pic:
ret["pic"] = self.last_vod_pic
ret["poster"] = self.last_vod_pic
return ret
def _add_to_115(self, magnet):
if not self.pan_115_cookie:
print("115添加失败: 未配置 pan_115_cookie")
return
magnet = self._normalize_magnet(magnet)
if not magnet:
print("115添加失败: 非法磁力")
return
headers = {
"User-Agent": self.headers.get("User-Agent", ""),
"Cookie": self.pan_115_cookie,
"Origin": "https://115.com",
"Referer": "https://115.com/web/lixian/",
"Accept": "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest"
}
try:
pan_sess = requests.Session()
pan_sess.verify = False
pan_sess.mount("http://", SSLAdapter(max_retries=2))
pan_sess.mount("https://", SSLAdapter(max_retries=2))
space_resp = pan_sess.get("https://115.com/?ct=offline&ac=space", headers=headers, timeout=10)
try:
space_json = space_resp.json()
except:
print(f"115获取签名失败(非JSON): {space_resp.text[:200]}")
return
if not space_json.get("state"):
print(f"115获取签名失败(可能Cookie过期): {space_json}")
return
sign = space_json.get("sign", "")
req_time = space_json.get("time", "")
if not sign or not req_time:
print(f"115签名数据异常: {space_json}")
return
uid_match = re.search(r'UID=(\d+)', self.pan_115_cookie)
uid = uid_match.group(1) if uid_match else ""
add_url = "https://115.com/web/lixian/?ct=lixian&ac=add_task_url"
post_data = {"url": magnet, "uid": uid, "sign": sign, "time": req_time}
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
add_resp = pan_sess.post(add_url, data=post_data, headers=headers, timeout=10)
try:
add_json = add_resp.json()
except:
print(f"115添加失败(非JSON): {add_resp.text[:200]}")
return
if add_json.get("state") or add_json.get("errcode") == 0:
print(f"115离线添加成功: {magnet[:100]}...")
else:
err = add_json.get("error_msg") or add_json.get("msg") or add_json.get("error") or str(add_json)
print(f"115添加失败: {err}")
except Exception as e:
print(f"115离线网络异常: {e}")
def playerContent(self, flag, id, vipFlags):
if flag == "0" or id == "__ACK__":
return self._return_ack_video()
if flag == "📥115云下载":
try:
decoded = base64.urlsafe_b64decode(id.encode() + b"==").decode()
magnet = self._normalize_magnet(decoded)
if not magnet:
return self._return_ack_video()
if not self.pan_115_cookie:
print("115未配置Cookie")
return self._return_ack_video()
threading.Thread(target=self._add_to_115, args=(magnet,), daemon=True).start()
return self._return_ack_video()
except Exception as e:
print(f"115云下载处理异常: {e}")
return self._return_ack_video()
if flag == "🧲磁力播放":
data = self._b64d(id)
if data and data.get("type") == "magnet":
mu = self._normalize_magnet(data.get("url", ""))
if mu:
pic = data.get("pic") or self.last_vod_pic
return {"parse": 0, "url": "push://" + mu, "pic": pic, "poster": pic}
if isinstance(id, str) and id.startswith("push://"):
return {"parse": 0, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
mu = self._normalize_magnet(id)
if mu:
return {"parse": 0, "url": "push://" + mu, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
return {"parse": 1, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
data = self._b64d(id)
if not data:
if isinstance(id, str) and id.startswith("push://"):
return {"parse": 0, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
return {"parse": 1, "url": id, "pic": self.last_vod_pic, "poster": self.last_vod_pic}
typ = data.get("type", "")
url = data.get("url", "")
pic = data.get("pic") or self.last_vod_pic
if not url:
return {"parse": 1, "url": id, "pic": pic, "poster": pic}
if typ == "search":
wd = data.get("wd", "").strip()
if not wd:
return {
"parse": 1,
"url": self.host,
"pic": pic,
"poster": pic
}
search_url = f"{self.host}/vodsearch/{quote(wd)}----------1---.html"
return {
"parse": 0,
"url": "push://" + search_url,
"pic": pic,
"poster": pic
}
if typ == "pan":
return {"parse": 0, "url": "push://" + url, "pic": pic, "poster": pic}
if typ == "magnet":
mu = self._normalize_magnet(url)
if mu:
return {"parse": 0, "url": "push://" + mu, "pic": pic, "poster": pic}
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
if typ == "py":
real = self._parse_py_page(url)
if real:
return {"parse": 0, "url": real, "pic": pic, "poster": pic}
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
if typ == "web":
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
return {"parse": 1, "url": url, "pic": pic, "poster": pic}
# ---------------- search ----------------
def searchContent(self, key, quick, pg="1"):
pg = int(pg) if str(pg).isdigit() else 1
wd = quote(key)
for h in self.hosts:
suggest_api = f"{h}/index.php/ajax/suggest?mid=1&limit=20&wd={wd}"
try:
r = self.session.get(suggest_api, timeout=8, headers=self.headers, verify=False)
txt = r.text or ""
if not self._is_verify_page(txt) and r.status_code == 200:
data = r.json()
lst = data.get("list") or []
videos = []
for it in lst:
vid = it.get("id") or it.get("vod_id")
name = it.get("name") or it.get("vod_name") or ""
pic = self._full_url_by_host(h, it.get("pic") or it.get("vod_pic") or "")
remarks = self._clean_text(it.get("en") or it.get("remark") or "")
jump_url = it.get("url") or it.get("link") or ""
vid_url = self._mk_vod_id(h, vid, jump_url)
if not vid_url:
continue
if pic:
self.vod_pic_cache[vid_url] = pic
videos.append({
"vod_id": vid_url,
"vod_name": self._clean_name(name, 80),
"vod_pic": pic,
"vod_remarks": remarks
})
if videos:
self.host = h
return {
"list": videos,
"page": pg,
"pagecount": pg + 1 if len(videos) >= 20 else pg,
"limit": len(videos),
"total": len(videos)
}
except:
pass
api_list = [
f"{h}/api.php/provide/vod/?ac=detail&wd={wd}&pg={pg}",
f"{h}/api.php/provide/vod?ac=detail&wd={wd}&pg={pg}",
]
for api in api_list:
try:
r = self.session.get(api, timeout=8, headers=self.headers, verify=False)
txt = r.text or ""
if self._is_verify_page(txt):
continue
if r.status_code != 200:
continue
data = r.json()
lst = data.get("list") or data.get("data") or []
videos = []
for it in lst:
vid = it.get("vod_id") or it.get("id")
name = it.get("vod_name") or it.get("name") or ""
pic = self._full_url_by_host(h, it.get("vod_pic") or it.get("pic") or "")
remarks = self._clean_text(it.get("vod_remarks") or it.get("remarks") or "")
jump_url = it.get("vod_play_url") or it.get("url") or it.get("link") or ""
vid_url = self._mk_vod_id(h, vid, jump_url)
if not vid_url:
continue
if pic:
self.vod_pic_cache[vid_url] = pic
videos.append({
"vod_id": vid_url,
"vod_name": self._clean_name(name, 80),
"vod_pic": pic,
"vod_remarks": remarks
})
if videos:
self.host = h
return {
"list": videos,
"page": int(data.get("page", pg) or pg),
"pagecount": int(data.get("pagecount", 1) or 1),
"limit": len(videos),
"total": int(data.get("total", len(videos)) or len(videos))
}
except:
pass
return {
"list": [],
"page": pg,
"pagecount": pg,
"limit": 0,
"total": 0
}
+134
View File
@@ -0,0 +1,134 @@
import requests, os, time, random, sys
from threading import Thread, Lock
from urllib.parse import urlparse
SAVE_DIR = "/storage/emulated/0/爬取保存/妹子视频爬取"
os.makedirs(SAVE_DIR, exist_ok=True)
VIDEO_APIS = [
("高质量", "http://api.tinise.cn/api/xjjsp"),
("小姐姐(高质量)", "http://api.yujn.cn/api/zzxjj.php?type=video"),
("小姐姐2(高质量)", "https://api.dwo.cc/api/ksvideo"),
("小姐姐3(高质量)", "http://api.qemao.com/api/douyin/"),
("随机小姐姐聚合", "https://sucyan.top/api/video/?msg=jk"),
("狱卒系列", "http://api.yujn.cn/api/jpmt.php"),
("美腿玉足", "https://sbtxqq.com/api/yzxl.php"),
("黑丝系列", "http://api.yujn.cn/api/heisis.php?type=video"),
("黑白丝", "http://api.tinise.cn/api/baisi"),
("黑白丝2", "http://api.tinise.cn/api/heisi"),
("抖音小姐姐", "http://api.qemao.com/api/douyin/"),
("高质量美女", "http://www.wudada.online/Api/NewSp"),
("完美身材", "http://api.yujn.cn/api/wmsc.php?type=video"),
("快手变装", "http://api.yujn.cn/api/ksbianzhuang.php?type=video"),
("抖音变装", "http://api.yujn.cn/api/bianzhuang.php?"),
("白丝系列", "http://api.yujn.cn/api/baisis.php?type=video"),
("快手女大学生", "https://api.yujn.cn/api/nvda.php?type=video"),
("抖音瞳瞳", "https://api.yujn.cn/api/tongtong.php?type=video"),
("丝滑舞蹈", "http://api.yujn.cn/api/shwd.php?type=video"),
("鞠婧祎系列", "http://api.yujn.cn/api/jjy.php?type=video"),
("美女穿搭", "http://api.yujn.cn/api/chuanda.php?type=video"),
("章若楠", "http://api.yujn.cn/api/zrn.php?type=video"),
("古风类", "http://api.yujn.cn/api/hanfu.php?type=video"),
("慢摇系列", "http://api.yujn.cn/api/manyao.php?type=video"),
("吊带系列", "http://api.yujn.cn/api/diaodai.php?type=video"),
("清纯系列", "http://api.yujn.cn/api/qingchun.php?type=video"),
("COS系列", "http://api.yujn.cn/api/COS.php?type=video"),
("纯情女高", "http://api.yujn.cn/api/nvgao.php?type=video"),
("街拍系列", "http://api.yujn.cn/api/jiepai.php?type=video"),
("变装系列", "http://api.yujn.cn/api/ksbianzhuang.php?type=video"),
("萝莉系列", "http://api.yujn.cn/api/luoli.php?type=video"),
("甜妹系列", "http://api.yujn.cn/api/tianmei.php?type=video"),
("随机美女", "https://v2.api-m.com/api/meinv?return=302"),
("随机小姐姐1", "http://api.yujn.cn/api/xjj.php?type=video"),
("随机小姐姐2", "http://api.yujn.cn/api/ksxjjsp.php?"),
("随机小姐姐3", "https://img.8845.top/xjj"),
("随机小姐姐4", "https://api.mhimg.cn/api/Sj_girls_video"),
("随机小姐姐5", "http://api.yujn.cn/api/juhexjj.php?type=video"),
]
count_lock = Lock()
total_ok = 0
total_fail = 0
def download_one(name, url, dest_dir, retries=3):
global total_ok, total_fail
safe = "".join(c for c in name if c.isalnum() or c in " _-").strip()
d = os.path.join(dest_dir, safe)
os.makedirs(d, exist_ok=True)
for r in range(retries):
try:
hd = {
"User-Agent": "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36",
"Accept": "*/*",
"Referer": "https://gitee.com/"
}
resp = requests.get(url, headers=hd, timeout=30, stream=True, allow_redirects=True)
if resp.status_code != 200:
print(f" [{safe}] HTTP {resp.status_code}, retry {r+1}")
time.sleep(2)
continue
ts = int(time.time() * 1000)
fp = os.path.join(d, f"{ts}.mp4")
with open(fp, "wb") as f:
for chunk in resp.iter_content(65536):
if chunk:
f.write(chunk)
kb = os.path.getsize(fp) / 1024
if kb < 100:
os.remove(fp)
print(f" [{safe}] 文件太小({kb:.0f}KB), retry {r+1}")
continue
with count_lock:
total_ok += 1
print(f" OK [{safe}] {fp} ({kb:.0f}KB)")
return True
except Exception as e:
print(f" FAIL [{safe}] {e}")
time.sleep(2)
with count_lock:
total_fail += 1
return False
def worker(name, url, dest, n):
for i in range(n):
download_one(name, url, dest)
if __name__ == "__main__":
print("=" * 55)
print(" 妹子发电站 · 38源批量视频爬虫")
print(f" 保存路径: {SAVE_DIR}")
print("=" * 55)
try:
n = int(input("每个源下载几个? (默认3): ") or "1")
c = int(input("并发数? (默认3): ") or "1")
except:
n, c = 1, 1
print(f"\n开始爬取 {len(VIDEO_APIS)} 个源, 每个{n}个, 并发{c}\n")
start = time.time()
for i in range(0, len(VIDEO_APIS), c):
batch = VIDEO_APIS[i:i+c]
ts = []
for name, url in batch:
t = Thread(target=worker, args=(name, url, SAVE_DIR, n))
t.start()
ts.append(t)
time.sleep(0.3)
for t in ts:
t.join()
elapsed = time.time() - start
print(f"\n{'=' * 55}")
print(f" 完成! 成功={total_ok}, 失败={total_fail}, 耗时={elapsed:.0f}s")
print(f" 视频保存在: {SAVE_DIR}")
print(f"{'=' * 55}")
+360
View File
@@ -0,0 +1,360 @@
# -*- coding: utf-8 -*-
import sys
import json
import re
import time
import requests
import base64
from urllib.parse import quote
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
sys.path.append('..')
from base.spider import Spider
class DyuziPanSpider(Spider):
"""心跳4k剧场网盘资源搜索爬虫 - 终极修复流解析与新增动漫分组版"""
SITE_URL = "https://ppan.dyuzi.com"
WEB_SEARCH_API = f"{SITE_URL}/api/other/web_search"
HOME_API = f"{SITE_URL}/api/frontend/home"
RANKING_API = f"{SITE_URL}/api/frontend/ranking"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"Accept": "text/event-stream, application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"Referer": SITE_URL,
"X-Requested-With": "XMLHttpRequest"
}
REQUEST_TIMEOUT = 60
MAX_RETRIES = 3
BACKOFF_FACTOR = 0.5
REQUEST_DELAY = 0.5
IS_TYPE_MAP = {
0: 'quark', 1: 'uc', 2: 'baidu', 3: 'aliyun', 4: 'xunlei', 5: 'a189', 6: 'quark'
}
PAN_CONFIG = {
'quark': {'name': '夸克云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/quark.webp'},
'uc': {'name': 'UC网盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/uc.webp'},
'a189': {'name': '天翼云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/189.webp'},
'aliyun': {'name': '阿里云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/aliyun.webp'},
'baidu': {'name': '百度网盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/baidu.webp'},
'xunlei': {'name': '迅雷云盘', 'icon': 'https://ppan.dyuzi.com/views/index/template/btlm/disk-icons/xunlei.webp'},
'magnet': {'name': '磁力链接', 'icon': ''},
'other': {'name': '其他网盘', 'icon': ''}
}
_PSQ_GROUP_ORDER = ["quark", "uc", "aliyun", "a189", "baidu", "xunlei", "magnet", "other"]
def __init__(self):
super().__init__()
self.pan_priority = ''
self._last_request_time = 0
self.session = requests.Session()
self.session.headers.update(self.HEADERS)
retries = Retry(total=self.MAX_RETRIES, backoff_factor=self.BACKOFF_FACTOR, status_forcelist=[429, 500, 502, 503, 504], raise_on_status=False)
self.session.mount('http://', HTTPAdapter(max_retries=retries))
self.session.mount('https://', HTTPAdapter(max_retries=retries))
def init(self, extend):
try:
extend_dict = json.loads(extend) if extend else {}
self.pan_priority = extend_dict.get('pan_priority', 'quark,a189,uc')
except json.JSONDecodeError:
self.pan_priority = 'quark,a189,uc'
def getName(self): return "盘搜"
def isVideoFormat(self, url): return False
def manualVideoCheck(self): return False
# ======= 1. 分类栏增加动漫类别 =======
def homeContent(self, filter):
return {
'class': [
{"type_id": "1", "type_name": "电视剧"},
{"type_id": "2", "type_name": "电影"},
{"type_id": "3", "type_name": "动漫"} # 新增动漫分类
],
'filters': {},
'list': []
}
# ======= 2. 首页推荐位混合展现(电视剧+动漫) =======
def homeVideoContent(self):
vod_list = []
# 抓取电视剧推荐 (前12个)
try:
resp = self.session.get(self.RANKING_API, params={'channel': '电视剧', 'limit': 12}, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
data = resp.json()
if data.get('code') == 0 and data.get('data', {}).get('list'):
for item in data['data']['list']:
vod_list.append({
"vod_id": self._b64e({'title': item.get('title', ''), 'type': 'ranking'}),
"vod_name": item.get('title', ''),
"vod_pic": item.get('src', ''),
"vod_remarks": f"剧集|热度:{item.get('hot_score', '0')[:4]}"
})
except: pass
# 抓取动漫推荐 (追加12个)
try:
resp = self.session.get(self.RANKING_API, params={'channel': '动漫', 'limit': 12}, timeout=self.REQUEST_TIMEOUT)
if resp.status_code == 200:
data = resp.json()
if data.get('code') == 0 and data.get('data', {}).get('list'):
for item in data['data']['list']:
vod_list.append({
"vod_id": self._b64e({'title': item.get('title', ''), 'type': 'ranking'}),
"vod_name": item.get('title', ''),
"vod_pic": item.get('src', ''),
"vod_remarks": f"动漫|热度:{item.get('hot_score', '0')[:4]}"
})
except: pass
return {'list': vod_list}
# ======= 3. 分类点击切换逻辑(支持动漫频道) =======
def categoryContent(self, cid, page, filter, ext):
try:
channel_map = {'1': '电视剧', '2': '电影', '3': '动漫'}
channel = channel_map.get(str(cid), '电视剧')
resp = self.session.get(self.RANKING_API, params={'channel': channel, 'limit': 30}, timeout=self.REQUEST_TIMEOUT)
resp.raise_for_status()
data = resp.json()
vod_list = []
if data.get('code') == 0 and data.get('data', {}).get('list'):
for item in data['data']['list']:
vod_list.append({
"vod_id": self._b64e({'title': item.get('title', ''), 'type': 'ranking'}),
"vod_name": item.get('title', ''),
"vod_pic": item.get('src', ''),
"vod_remarks": f"评分:{item.get('score_avg', '0')}"
})
return {'list': vod_list, 'page': 1, 'pagecount': 1, 'limit': 30, 'total': len(vod_list)}
except: return {'list': []}
def _get_pan_type(self, is_type): return self.IS_TYPE_MAP.get(is_type, 'other')
def _b64e(self, obj):
text = json.dumps(obj, ensure_ascii=False, separators=(",", ":")) if not isinstance(obj, str) else obj
return base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
def _b64d(self, s):
try:
s += "=" * (-len(s) % 4)
decoded = base64.urlsafe_b64decode(s.encode()).decode()
try: return json.loads(decoded)
except: return decoded
except: return s
def _parse_sse_response(self, response_text):
results = []
if not response_text: return results
for line in response_text.strip().split('\n'):
line = line.strip()
if line.startswith('data:') and '[DONE]' not in line:
try:
data = json.loads(line[5:].strip())
if 'title' in data and 'url' in data: results.append(data)
except: continue
return results
def _psq_quality_score(self, title):
score = 0
t_upper = title.upper()
if "杜比" in title or "DOLBY" in t_upper or "DOVI" in t_upper: score += 120000
if "DV" in t_upper: score += 100000
if "高码" in title or "HQ" in t_upper: score += 90000
if "HDR10+" in t_upper: score += 85000
if "HDR10" in t_upper: score += 80000
if "HDR" in t_upper: score += 75000
if "4K" in t_upper or "2160P" in t_upper or "UHD" in t_upper: score += 65000
if "1080P" in t_upper or "FHD" in t_upper: score += 45000
if "蓝光" in title or "BLURAY" in t_upper: score += 40000
if "REMUX" in t_upper: score += 35000
return score
def _psq_extract_size_gb(self, title):
try:
match = re.search(r'([0-9]+(?:\.[0-9]+)?)\s*([mMgGtT])[bB]?', title)
if match:
val = float(match.group(1))
unit = match.group(2).lower()
if unit == 't': return val * 1024
if unit == 'g': return val
if unit == 'm': return val / 1024
except: pass
return 0.0
def _clean_resource_title(self, title):
t = re.sub(r'https?://\S+', '', title)
t = re.sub(r'\[夸克网盘\]|\[UC网盘\]|\[天翼云盘\]|微云|百度云', '', t)
t = re.sub(r'^\s*【.*?】|^\s*\[.*?\]', '', t)
t = t.split('')[0].split('')[0]
t = re.sub(r'\s+', ' ', t).strip()
return t if t else title
def _secure_fetch_items(self, keywords):
elapsed = time.time() - self._last_request_time
if elapsed < self.REQUEST_DELAY: time.sleep(self.REQUEST_DELAY - elapsed)
try:
params = {'title': keywords, 'is_type': 'all', 'is_show': '1', 'skip_check': '0', 'status': '1', 'max': '120'}
resp = self.session.get(self.WEB_SEARCH_API, params=params, timeout=self.REQUEST_TIMEOUT)
self._last_request_time = time.time()
if resp.status_code == 200:
return self._parse_sse_response(resp.text)
except Exception as e:
print(f"[DyuziPan] 安全拉取接口异常被拦截: {e}")
return []
def searchContent(self, key, quick, pg="1"): return self._perform_search(key, pg)
def searchContentPage(self, key, quick, page): return self._perform_search(key, page)
def _perform_search(self, keywords, page_str):
try: page = int(page_str)
except: page = 1
result = {'list': [], 'page': page, 'pagecount': 1, 'limit': 60, 'total': 0}
if not keywords or page > 1: return result
items = self._secure_fetch_items(keywords)
if not items: return result
merged_resources = {}
for item in items:
title, url, is_type = item.get('title', ''), item.get('url', ''), item.get('is_type', -1)
if not url or not title: continue
pan_type = self._get_pan_type(is_type)
clean_name = self._clean_resource_title(title)
group_key = f"{pan_type}_{clean_name}"
if group_key not in merged_resources:
merged_resources[group_key] = {
'clean_name': clean_name,
'pan_type': pan_type,
'score': self._psq_quality_score(title) + self._psq_extract_size_gb(title),
'links': []
}
merged_resources[group_key]['links'].append({'title': title, 'url': url})
sorted_resources = list(merged_resources.values())
sorted_resources.sort(key=lambda x: x['score'], reverse=True)
for res in sorted_resources:
pan_cfg = self.PAN_CONFIG.get(res['pan_type'], self.PAN_CONFIG['other'])
display_name = f"[{pan_cfg['name']}] {res['clean_name']}"
result['list'].append({
"vod_id": self._b64e({'is_group': True, 'resource_name': display_name, 'pan_type': res['pan_type'], 'links': res['links']}),
"vod_name": display_name,
"vod_pic": pan_cfg.get('icon', ''),
"vod_remarks": f"包含 {len(res['links'])} 个文件"
})
result['total'] = len(result['list'])
return result
def detailContent(self, ids):
result = {'list': []}
if not ids or not ids[0]: return result
try:
vod_data = self._b64d(ids[0])
if not isinstance(vod_data, dict): return result
# ─── 首页排行榜/推荐页点击 ───
if vod_data.get('type') == 'ranking':
search_title = vod_data.get('title', '')
if not search_title: return result
items = self._secure_fetch_items(search_title)
if not items:
result['list'].append({
"vod_id": ids[0], "vod_name": search_title, "vod_pic": "",
"vod_content": f"提示:未在当前接口中检索到该动漫/影视的网盘分享。",
"vod_play_from": "暂无资源", "vod_play_url": "点击刷新重试$push://https://ppan.dyuzi.com", "vod_remarks": "无资源"
})
return result
buckets = {k: [] for k in self._PSQ_GROUP_ORDER}
for item in items:
title, url, is_type = item.get('title', ''), item.get('url', ''), item.get('is_type', -1)
if not url or not title: continue
pt = self._get_pan_type(is_type)
if pt not in buckets: pt = 'other'
buckets[pt].append({'title': title, 'url': url})
play_from_list = []
play_url_list = []
for group_key in self._PSQ_GROUP_ORDER:
b_links = buckets.get(group_key, [])
if not b_links: continue
play_from_list.append(self.PAN_CONFIG.get(group_key, self.PAN_CONFIG['other'])['name'])
eps = []
for idx, item in enumerate(b_links):
clean_ep = item['title'].replace('$', '').replace('#', '').strip()
if len(clean_ep) > 60: clean_ep = f"进入云盘播放-{idx+1}"
eps.append(f"{clean_ep}${item['url']}")
play_url_list.append("#".join(eps))
if play_from_list:
result['list'].append({
"vod_id": ids[0],
"vod_name": search_title,
"vod_pic": "",
"vod_content": f"资源名称: {search_title}\n系统已为您全网智能检索相关网盘源。",
"vod_play_from": "$$$".join(play_from_list),
"vod_play_url": "$$$".join(play_url_list),
"vod_remarks": f"聚合 {len(play_from_list)} 个网盘线路"
})
return result
# ─── 搜索页点击 ───
resource_name = vod_data.get('resource_name', '网盘资源')
pan_type = vod_data.get('pan_type', 'other')
links = vod_data.get('links', [])
pan_cfg = self.PAN_CONFIG.get(pan_type, self.PAN_CONFIG['other'])
episode_strings = []
for idx, item in enumerate(links):
clean_ep_title = item['title'].replace('$', '').replace('#', '').strip()
if len(clean_ep_title) > 60: clean_ep_title = f"打开云盘-{idx+1}"
episode_strings.append(f"{clean_ep_title}${item['url']}")
result['list'].append({
"vod_id": ids[0],
"vod_name": resource_name,
"vod_pic": pan_cfg.get('icon', ''),
"vod_content": f"资源名称: {resource_name}\n专属线路: {pan_cfg['name']}",
"vod_play_from": pan_cfg['name'],
"vod_play_url": "#".join(episode_strings),
"vod_remarks": f"{len(links)} 个资源版本"
})
except Exception as e:
print("[DyuziPan] 详情页分栏异常:", e)
return result
def playerContent(self, flag, pid, vipFlags):
result = {"parse": 0, "jx": 0, "url": "", "header": self.HEADERS}
if not pid: return result
try:
url = pid.split('$', 1)[1] if '$' in pid else pid
url = url.strip().replace(' ', '')
if not url.startswith(('http://', 'https://', 'magnet:')):
url = 'https://' + url
if not url.startswith('push://'):
url = 'push://' + url
result['url'] = url
except: pass
return result
def localProxy(self, params): return None
Spider = DyuziPanSpider
+318
View File
@@ -0,0 +1,318 @@
# -*- coding: utf-8 -*-
# by @嗷呜
import base64
import sys
from pprint import pprint
import hmac
import hashlib
import secrets
import time
import uuid
import json
import random
import string
from urllib.parse import quote
import requests
from Crypto.Cipher import AES
from Crypto.Hash import MD5
from Crypto.Util.Padding import pad
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend='{}'):
self.session = requests.session()
pass
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
host='https://film.symx.club'
RSA_N = "c1e3934d1614465b33053e7f48ee4ec87b14b95ef88947713d25eecbff7e74c7977d02dc1d9451f79dd5d1c10c29acb6a9b4d6fb7d0a0279b6719e1772565f09af627715919221aef91899cae08c0d686d748b20a3603be2318ca6bc2b59706592a9219d0bf05c9f65023a21d2330807252ae0066d59ceefa5f2748ea80bab81"
RSA_E = 65537
STATIC_BASE = "https://static.geetest.com/"
VERIFY_HOST = 'https://gcaptcha4.geetest.com'
ClientId=MD5.new(str(int(time.time())).encode()).hexdigest();Token=''
def rsa_encrypt(self,random_key):
pub_key = RSA.construct((int(self.RSA_N, 16), self.RSA_E))
cipher = PKCS1_v1_5.new(pub_key)
return cipher.encrypt(random_key.encode('utf-8')).hex()
def aes_encrypt(self,plaintext, key):
iv = b"0000000000000000"
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv)
padded_data = pad(plaintext.encode('utf-8'), AES.block_size)
return cipher.encrypt(padded_data).hex()
def get_w(self, payload_dict):
random_key = "".join(random.choices(string.ascii_letters + string.digits, k=16))
json_str = json.dumps(payload_dict, separators=(',', ':'))
return self.aes_encrypt(json_str, random_key) + self.rsa_encrypt(random_key)
def get_dynamic_payload(self,lot_number, set_left, passtime, captcha_id, pow_detail):
key_name = lot_number[26:30] + lot_number[12:16]
sub_key = lot_number[16:24]
val = lot_number[6:10]
pow_msg = f"1|0|md5|{pow_detail['datetime']}|{captcha_id}|{lot_number}||{secrets.token_hex(8) }"
pow_sign = hashlib.md5(pow_msg.encode()).hexdigest()
payload = {
"setLeft": set_left,
"passtime": passtime,
"userresponse": set_left / 1.0059466666666665 +2,
"device_id": "",
"lot_number": lot_number,
"pow_msg": pow_msg,
"pow_sign": pow_sign,
"geetest": "captcha",
"lang": "zh",
"ep": "123",
"biht": "1426265548",
"yDWL": "hZGx",
key_name: {sub_key: val},
"em": {"ph": 0, "cp": 0, "ek": "11", "wd": 1, "nt": 0, "si": 0, "sc": 0}
}
return payload
def generate_checksum_timestamp(self):
r = str(int(time.time() * 1000))
prefix = r[:-1]
digit_sum = sum(int(d) for d in prefix)
check_digit = digit_sum % 10
return prefix + str(check_digit)
def get_site_headers(self,path, end=0):
secret_key = "lslx_sk"
timestamp = self.generate_checksum_timestamp()
raw_data = f"{timestamp}symx_{secret_key}{path}"
arranged = raw_data.replace("1", "i").replace("0", "o").replace("5", "s")
if end:
secret_key = ''
arranged = ''
signature = hmac.new(
secret_key.encode('utf-8'),
arranged.encode('utf-8'),
digestmod=hashlib.sha256
).hexdigest()
header = {
'User-Agent': 'SYMX_ANDROID',
'user-agent': 'Mozilla/5.0 (Linux; Android 13; M2012K10C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36 uni-app Html5Plus/1.0 (Immersed/30.545454)',
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json;charset=UTF-8',
'X-Platform': 'android',
'X-Timestamp': timestamp,
'X-Sign-X': signature,
'X-Client-Id':self.ClientId,
'Referer': 'https://film.symx.club/',
}
if self.Token: header['X-Verify-Token'] = self.Token
if end:
del header['X-Sign-X']
header['X-Report-Id'] = signature
return header
def run_verify(self,i=0):
if i>3: return
try:
config = self.session.get(f'{self.host}/api/auth/verify/config',
headers=self.get_site_headers('/auth/verify/config')).json()
captcha_id = config['data']['captchaId']
params = {
'callback': f'geetest_{int(time.time()* 1000)}',
'captcha_id': captcha_id,
'challenge': str(uuid.uuid4()),
'client_type': 'web',
'lang': 'zho',
}
load_res = self.fetch(f'{self.VERIFY_HOST}/load', params=params).text
data = json.loads(load_res[len(params['callback']) + 1:-1])['data']
t=str(int(time.time() * 1000))
heade={
'timestamp':t,
"sign":MD5.new(f'44344434tffrfeeffgdggdg{t}'.encode()).hexdigest()
}
body={
'type':'solve',
'bg':base64.b64encode(self.fetch(f"{self.STATIC_BASE}{data['bg']}").content).decode(),
'hb':base64.b64encode(self.fetch(f"{self.STATIC_BASE}{data['slice']}").content).decode(),
}
resp=self.post("http://mytv6688.xyz/aowuapp",json=body,headers=heade).json()
print("验证结果1", resp)
pass_time = random.randint(1200, 2200)
inner_payload = self.get_dynamic_payload(data['lot_number'], resp["result"], pass_time, captcha_id,
data['pow_detail'])
verify_params = {
"callback": f"geetest_{int(time.time() * 1000)}",
"captcha_id": captcha_id,
"client_type": "web",
"lot_number": data['lot_number'],
"payload": data['payload'],
"process_token": data['process_token'],
"payload_protocol": data['payload_protocol'],
"pt": data['pt'],
"w": self.get_w(inner_payload)
}
verify_res_raw = self.fetch(f'{self.VERIFY_HOST}/verify', params=verify_params).text
print("验证结果2", verify_res_raw)
verify_data = json.loads(verify_res_raw[len(verify_params['callback']) + 1:-1])
sc = verify_data['data']['seccode']
json_body = {
"captchaId": sc['captcha_id'],
"captchaOutput": sc['captcha_output'],
"genTime": int(sc['gen_time']),
"lotNumber": sc['lot_number'],
"passToken": sc['pass_token']
}
final_res = self.session.post(f'{self.host}/api/auth/verify', headers=self.get_site_headers("/auth/verify"),
json=json_body)
print("验证结果3", final_res.text)
self.Token = final_res.json()["data"]["token"]
except Exception as e:
print(e)
return self.run_verify(i+1)
def Req(self,path,params,i=0):
self.session.headers.update(self.get_site_headers(path.split("/api")[-1], i))
resp=self.session.get(f"{self.host}{path}",params=params)
if '完成验证' in resp.text:
self.run_verify()
self.session.headers.update(self.get_site_headers(path.split("/api")[-1], i))
resp = self.session.get(f"{self.host}{path}",params=params)
print(resp.status_code)
# print(resp.text)
return resp.json()
def homeContent(self, filter):
data=self.Req("/api/category/top",{},1)
result = {}
classes = []
for k in data['data']:
classes.append({
'type_name': k['name'],
'type_id': k['id']
})
# fil = []
# resp=self.Req("/api/film/category/filter",{'categoryId':k['id']},1)
# for i,v in resp['data'].items():
# if not isinstance(v,list) or len(v)==0 or i=='sortOptions':continue
# fil.append({
# 'key': i,
# 'name': i,
# 'value': [{'n':x,'v':x} for x in v]
# })
# fil.append(self.ddd)
# filters[k['id']] = fil
result['class'] = classes
result['filters'] = self.fetch("http://mytv6688.xyz/pyplugin/木兮筛选.json").json()
return result
def homeVideoContent(self):
data=self.Req("/api/poster/list",{},1)
vlist = []
for k in data['data']:
vlist.append({
'vod_id': k.get('filmId'),
'vod_name': k.get('filmName'),
'vod_pic': k.get('poster'),
})
return {'list':vlist}
def getList(self,data):
vlist = []
for k in data:
vlist.append({
'vod_id': k.get('id'),
'vod_name': k.get('name'),
'vod_pic': k.get('cover'),
'vod_remarks': k.get('updateStatus'),
})
return vlist
def categoryContent(self, tid, pg, filter, extend):
params={
"area": extend.get('areaOptions', ''),
"childCategoryId": "",
"categoryId": tid,
"language": extend.get('languageOptions', ''),
"pageNum": pg,
"pageSize": "10",
"sort": extend.get('sortOptions', ''),
"year": extend.get('yearOptions', '')
}
resp=self.Req("/api/film/category/list", params)
result = {}
result['list'] =self.getList(resp['data']['list'])
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
resp=self.Req("/api/film/detail/play/app",{'id': ids[0]})
v=resp['data']
n,p=[],[]
for i in v.get('playLineList'):
n.append(i['playerName'])
m=[f"{j['name']}${j['id']}" for j in i.get('lines')]
p.append('#'.join(m))
vod = {
'type_name': v.get('categoryName'),
'vod_year': v.get('year'),
'vod_area': v.get('area'),
'vod_remarks': v.get('updateStatus'),
'vod_actor': v.get('actor'),
'vod_director': '云霄仙子(困困版)',
'vod_content': v.get('blurb'),
'vod_play_from': '$$$'.join(n),
'vod_play_url': '$$$'.join(p)
}
return {'list':[vod]}
def searchContent(self, key, quick, pg="1"):
params={
"pageNum": pg,
"pageSize": "10",
"keyword": key
}
resp=self.Req('/api/film/search',params=params)
return {'list':self.getList( resp['data']['list']),'page':pg}
def playerContent(self, flag, id, vipFlags):
resp=self.Req("/api/line/play/parse", {"lineId": id})
return {'parse': 0, 'url': resp['data'], 'header': ''}
def localProxy(self, param):
pass
def liveContent(self, url):
pass
if __name__ == "__main__":
sp = Spider()
formatJo = sp.init()
formatJo = sp.homeContent(False) # 主页,等于真表示启用筛选
# formatJo = sp.homeVideoContent() # 主页视频
# formatJo = sp.searchContent("斗罗",False,'1') # 搜索{"area":"大陆","by":"hits","class":"国产","lg":"国语"}
# formatJo = sp.categoryContent('2', '1', False, {}) # 分类
# formatJo = sp.detailContent(['126634']) # 详情
# formatJo = sp.playerContent("","https://www.yingmeng.net/vodplay/140148-2-1.html",{}) # 播放
# formatJo = sp.localProxy({"":"https://www.yingmeng.net/vodplay/140148-2-1.html"}) # 播放
pprint(formatJo)
+184
View File
@@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
# by @嗷呜
import json
import sys
import time
import requests
from base64 import b64decode, b64encode
from Crypto.Hash import MD5
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
pass
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
host='http://v.rbotv.cn'
headers = {
'User-Agent': 'okhttp-okgo/jeasonlzy',
'Accept-Language': 'zh-CN,zh;q=0.8'
}
def homeContent(self, filter):
data=requests.post(f'{self.host}/v3/type/top_type',headers=self.headers,files=self.getfiles({'': (None, '')})).json()
result = {}
classes = []
filters = {}
for k in data['data']['list']:
classes.append({
'type_name': k['type_name'],
'type_id': k['type_id']
})
fts = []
for i,x in k.items():
if isinstance(x, list) and len(x)>2:
fts.append({
'name': i,
'key': i,
'value': [{'n': j, 'v': j} for j in x if j and j!= '全部']
})
if len(fts):filters[k['type_id']] = fts
result['class'] = classes
result['filters'] = filters
return result
def homeVideoContent(self):
data=requests.post(f'{self.host}/v3/type/tj_vod',headers=self.headers,files=self.getfiles({'': (None, '')})).json()
return {'list':self.getv(data['data']['cai']+data['data']['loop'])}
def categoryContent(self, tid, pg, filter, extend):
files = {
'type_id': (None, tid),
'limit': (None, '12'),
'page': (None, pg)
}
for k,v in extend.items():
if k=='extend':k='class'
files[k] = (None, v)
data=requests.post(f'{self.host}/v3/home/type_search',headers=self.headers,files=self.getfiles(files)).json()
result = {}
result['list'] = self.getv(data['data']['list'])
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
data=requests.post(f'{self.host}/v3/home/vod_details',headers=self.headers,files=self.getfiles({'vod_id': (None, ids[0])})).json()
v=data['data']
vod = {
'vod_name': v.get('vod_name'),
'type_name': v.get('type_name'),
'vod_year': v.get('vod_year'),
'vod_area': v.get('vod_area'),
'vod_remarks': v.get('vod_remarks'),
'vod_actor': v.get('vod_actor'),
'vod_director': v.get('vod_director'),
'vod_content': pq(pq(v.get('vod_content','') or '').text()).text()
}
n,p=[],[]
for o,i in enumerate(v['vod_play_list']):
n.append(f"线路{o+1}({i.get('flag')})")
c=[]
for j in i.get('urls'):
d={'url':j.get('url'),'p':i.get('parse_urls'),'r':i.get('referer'),'u':i.get('ua')}
c.append(f"{j.get('name')}${self.e64(json.dumps(d))}")
p.append('#'.join(c))
vod.update({'vod_play_from':'$$$'.join(n),'vod_play_url':'$$$'.join(p)})
return {'list':[vod]}
def searchContent(self, key, quick, pg="1"):
files = {
'limit': (None, '12'),
'page': (None, pg),
'keyword': (None, key),
}
data=requests.post(f'{self.host}/v3/home/search',headers=self.headers,files=self.getfiles(files)).json()
return {'list':self.getv(data['data']['list']),'page':pg}
def playerContent(self, flag, id, vipFlags):
ids=json.loads(self.d64(id))
url=ids['url']
if isinstance(ids['p'],list) and len(ids['p']):
url=[]
for i,x in enumerate(ids['p']):
up={'url':ids['url'],'p':x,'r':ids['r'],'u':ids['u']}
url.extend([f"解析{i+1}",f"{self.getProxyUrl()}&data={self.e64(json.dumps(up))}"])
h={}
if ids.get('r'):
h['Referer'] = ids['r']
if ids.get('u'):
h['User-Agent'] = ids['u']
return {'parse': 0, 'url': url, 'header': h}
def localProxy(self, param):
data=json.loads(self.d64(param['data']))
h = {}
if data.get('r'):
h['Referer'] = data['r']
if data.get('u'):
h['User-Agent'] = data['u']
res=self.fetch(f"{data['p']}{data['url']}",headers=h).json()
url=res.get('url') or res['data'].get('url')
return [302,'video/MP2T',None,{'Location':url}]
def liveContent(self, url):
pass
def getfiles(self, p=None):
if p is None:p = {}
t=str(int(time.time()))
h = MD5.new()
h.update(f"7gp0bnd2sr85ydii2j32pcypscoc4w6c7g5spl{t}".encode('utf-8'))
s = h.hexdigest()
files = {
'sign': (None, s),
'timestamp': (None, t)
}
p.update(files)
return p
def getv(self,data):
videos = []
for i in data:
if i.get('vod_id') and str(i['vod_id']) != '0':
videos.append({
'vod_id': i['vod_id'],
'vod_name': i.get('vod_name'),
'vod_pic': i.get('vod_pic') or i.get('vod_pic_thumb'),
'vod_year': i.get('tag'),
'vod_remarks': i.get('vod_remarks')
})
return videos
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:
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:
return ""
+155
View File
@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
import sys,requests,base64,json,time,re
sys.path.append('..')
from base.spider import Spider as BaseSpider
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
class Spider(BaseSpider):
def __init__(self):
super().__init__()
self.site="https://pinglian.lol"
self.api_list=self.site+"/api/get_videos.php"
self.api_pan=self.site+"/api/search_pan_links.php"
self.username=""
self.password=""
self.cookie=""
self.check_api=""
self.enable_check=False
self.ua="Mozilla/5.0 (Linux; Android 16; Pixel 9 Pro Build/BP1A.250305.019) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7743.101 Mobile Safari/537.36"
self.channels={"1":"电影","2":"电视剧","3":"综艺","4":"动漫"}
self.session=requests.Session()
self.session.verify=False
self.session.headers.update({"User-Agent":self.ua,"X-Requested-With":"XMLHttpRequest","Accept":"*/*","Referer":self.site+"/all-videos.php","Accept-Language":"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"})
def getName(self):return "盘链"
def init(self,extend=""):
if extend:
try:
cfg=json.loads(extend)
if isinstance(cfg,dict):
self.username=cfg.get("username",self.username)
self.password=cfg.get("password",self.password)
self.cookie=cfg.get("cookie",self.cookie)
self.check_api=cfg.get("check_api",self.check_api)
self.enable_check=bool(cfg.get("enable_check",self.enable_check)) and bool(self.check_api)
except Exception:0
self.session.cookies.set("announcement_dismissed","true",domain="pinglian.lol",path="/")
if self.cookie:self.session.headers.update({"Cookie":self.cookie})
elif self.username and self.password:self._login()
return self
def destroy(self):self.session.close()
def _login(self):
try:
self.session.get(self.site+"/pages/login.php",timeout=12)
return bool(self.session.post(self.site+"/api/login.php",data={"username":self.username,"password":self.password,"remember":"on"},timeout=12).json().get("success"))
except Exception:return False
def _b64e(self,obj):
text=obj if isinstance(obj,str) else json.dumps(obj,ensure_ascii=False,separators=(",",":"))
return base64.urlsafe_b64encode(text.encode()).decode().rstrip("=")
def _b64d(self,s):
try:
text=base64.urlsafe_b64decode((s+"="*(-len(s)%4)).encode()).decode()
try:return json.loads(text)
except Exception:return text
except Exception:return s
def _safe(self,t):return str(t or "").replace("#","").replace("$","")
def _fetch_list(self,t=None,wd=None,page=1):
p={"pg":page}
if wd:p["wd"]=wd
elif t:p["t"]=t
else:return {"list":[],"page":page,"pagecount":0,"total":0}
try:
d=self.session.get(self.api_list,params=p,timeout=15).json()
if d.get("code")==1:return {"list":d.get("list",[]),"page":d.get("page",page),"pagecount":d.get("pagecount",1),"total":d.get("total",0)}
except Exception:0
return {"list":[],"page":page,"pagecount":0,"total":0}
def _check_links(self,links,disk_type,batch_size=30):
if not links or not self.check_api:return links
ok=[]
for i in range(0,len(links),batch_size):
b=links[i:i+batch_size]
try:
d=requests.post(self.check_api,json={"items":[{"disk_type":disk_type,"url":u} for u in b]},headers={"User-Agent":self.ua,"Accept":"application/json, text/plain, */*"},timeout=15,verify=False).json()
ok.extend([x.get("url","") for x in d.get("results",[]) if x.get("state")=="ok" and x.get("url")])
except Exception:ok.extend(b)
return ok
def _pan_url(self,x):return x.get("url","") or (self.site+"/api/go.php?t="+x.get("token","") if x.get("token") else "")
def _process_disk(self,k,v):
links=v.get("links",[]) if isinstance(v,dict) else []
raw,seen=[],set()
for x in links:
u=self._pan_url(x)
if u and u not in seen:raw.append(u);seen.add(u)
if not raw:return None,None
valid=raw if not self.enable_check or k in {"others","guangya"} else self._check_links(raw,k)
if not valid:return None,None
s=set(valid);eps=[]
for x in links:
u=self._pan_url(x)
if u not in s:continue
pwd=x.get("password","")
if pwd and "pwd=" not in u and "password=" not in u:u+=("&" if "?" in u else "?")+"pwd="+str(pwd)
eps.append(self._safe(x.get("title") or v.get("name") or k)+"$"+self._b64e(u))
if not eps:return None,None
eps.insert(0,"点击选择$noop")
return v.get("name",k),"#".join(eps)
def _fetch_pan_links(self,name,vid):
try:
d=self.session.get(self.api_pan,params={"keyword":name,"vod_id":vid,"_t":int(time.time()*1000)},timeout=20).json()
if not d.get("success"):return "",""
pan=d.get("data",{})
order=["quark","uc","xunlei","aliyun","baidu","115","123","tianyi","others"]
items=[]
for k in order:
if k in pan:items.append((k,pan.pop(k)))
items.extend(pan.items())
fs,us=[],[]
for k,v in items:
f,u=self._process_disk(k,v)
if f and u:fs.append(f);us.append(u)
return "$$$".join(fs),"$$$".join(us)
except Exception:return "",""
def _vod(self,x):return {"vod_id":self._b64e(x),"vod_name":x.get("vod_name",""),"vod_pic":x.get("vod_pic",""),"vod_remarks":x.get("vod_remarks","") or x.get("type_name","")}
def homeContent(self,filter):return {"class":[{"type_name":v,"type_id":k} for k,v in self.channels.items()],"list":[],"filters":{}}
def homeVideoContent(self):
r=self._fetch_list(t="1",page=1)
return {"list":[self._vod(x) for x in r.get("list",[])[:12]]}
def categoryContent(self,tid,pg,filter,extend):
if tid not in self.channels:return {"list":[],"page":1,"pagecount":0,"limit":30,"total":0}
page=int(pg) if str(pg).isdigit() else 1
r=self._fetch_list(t=tid,page=page)
return {"list":[self._vod(x) for x in r.get("list",[])],"page":page,"pagecount":r.get("pagecount",0),"limit":30,"total":r.get("total",0)}
def searchContent(self,key,quick,pg="1"):
page=int(pg) if str(pg).isdigit() else 1
if not key:return {"list":[],"page":page,"pagecount":0,"limit":30,"total":0}
r=self._fetch_list(wd=key,page=page)
return {"list":[self._vod(x) for x in r.get("list",[])],"page":page,"pagecount":r.get("pagecount",0),"limit":30,"total":r.get("total",0)}
def detailContent(self,ids):
x=self._b64d(ids[0])
if not isinstance(x,dict):return {"list":[]}
name=x.get("vod_name","")
fs,us=[],[]
pf=x.get("vod_play_from","");pu=x.get("vod_play_url","")
if pf and pu:fs.append(pf);us.append(pu)
pan_f,pan_u=self._fetch_pan_links(name,x.get("vod_id")) if name and x.get("vod_id") is not None else ("","")
if pan_f and pan_u:fs.append(pan_f);us.append(pan_u)
if not fs:fs,us=["提示"],["需要有效登录或暂无资源$noop"]
return {"list":[{"vod_id":ids[0],"vod_name":name,"vod_pic":x.get("vod_pic",""),"vod_year":x.get("vod_year",""),"vod_area":x.get("vod_area",""),"vod_actor":x.get("vod_actor",""),"vod_director":x.get("vod_director",""),"vod_content":x.get("vod_content",""),"vod_remarks":(str(x.get("vod_remarks",""))+" "+str(x.get("vod_score",""))).strip(),"vod_play_from":"$$$".join(fs),"vod_play_url":"$$$".join(us)}]}
def _real_pan_url(self,u):
if not isinstance(u,str) or "api/go.php" not in u:return u
try:
r=self.session.get(u,timeout=12,allow_redirects=True)
m=re.search("https?://(?:pan\\.quark\\.cn|drive\\.uc\\.cn|pan\\.baidu\\.com|www\\.aliyundrive\\.com|www\\.alipan\\.com|alipan\\.com|cloud\\.189\\.cn|www\\.123pan\\.com|123pan\\.com|pan\\.xunlei\\.com|115\\.com)[^\\s\"\'<>]+",r.text)
return m.group(0).replace("&amp;","&") if m else u
except Exception:return u
def playerContent(self,flag,id,vipFlags):
if not id or id=="noop":return {"parse":0,"jx":0,"url":""}
u=self._b64d(id)
if isinstance(u,dict):u=u.get("url","")
if not isinstance(u,str):return {"parse":0,"jx":0,"url":""}
u=self._real_pan_url(u)
pans=["pan.quark.cn","drive.uc.cn","pan.baidu.com","aliyundrive.com","alipan.com","cloud.189.cn","123pan.com","pan.xunlei.com","115.com"]
if any(x in u for x in pans):return {"parse":0,"jx":0,"url":"push://"+u,"header":{"User-Agent":self.ua,"Referer":self.site+"/"}}
if u.startswith("magnet:"):return {"parse":0,"jx":0,"url":u}
if ".m3u8" in u or ".mp4" in u:return {"parse":0,"jx":0,"url":u,"header":{"User-Agent":self.ua}}
if u.startswith("http"):return {"parse":0,"jx":0,"url":"push://"+u,"header":{"User-Agent":self.ua,"Referer":self.site+"/"}}
return {"parse":0,"jx":0,"url":""}
+848
View File
@@ -0,0 +1,848 @@
# -*- coding: utf-8 -*-
# 短剧聚合 Spider - 支持七猫、星芽、西饭、围观、河马
import re
import json
import base64
import hashlib
import time
import random
import requests
from urllib.parse import quote, unquote
from base.spider import Spider
class Spider(Spider):
def __init__(self):
super().__init__()
self.keys = 'd3dGiJc651gSQ8w1'
self.char_map = {
'+': 'P', '/': 'X', '0': 'M', '1': 'U', '2': 'l', '3': 'E', '4': 'r', '5': 'Y', '6': 'W', '7': 'b', '8': 'd', '9': 'J',
'A': '9', 'B': 's', 'C': 'a', 'D': 'I', 'E': '0', 'F': 'o', 'G': 'y', 'H': '_', 'I': 'H', 'J': 'G', 'K': 'i', 'L': 't',
'M': 'g', 'N': 'N', 'O': 'A', 'P': '8', 'Q': 'F', 'R': 'k', 'S': '3', 'T': 'h', 'U': 'f', 'V': 'R', 'W': 'q', 'X': 'C',
'Y': '4', 'Z': 'p', 'a': 'm', 'b': 'B', 'c': 'O', 'd': 'u', 'e': 'c', 'f': '6', 'g': 'K', 'h': 'x', 'i': '5', 'j': 'T',
'k': '-', 'l': '2', 'm': 'z', 'n': 'S', 'o': 'Z', 'p': '1', 'q': 'V', 'r': 'v', 's': 'j', 't': 'Q', 'u': '7', 'v': 'D',
'w': 'w', 'x': 'n', 'y': 'L', 'z': 'e'
}
self.headers_default = {
'User-Agent': 'okhttp/3.12.11',
'content-type': 'application/json; charset=utf-8'
}
self.platform = {
'星芽': {
'host': 'https://app.whjzjx.cn',
'url1': '/cloud/v2/theater/home_page?theater_class_id',
'url2': '/v2/theater_parent/detail',
'search': '/v3/search',
'classes': '/cloud/v2/theater/classes',
'rankDetail': '/cloud/v1/first_level_ranking/detail',
'loginUrl': 'https://u.shytkjgs.com/user/v1/account/login'
},
'西饭': {
'host': 'https://xifan-api-cn.youlishipin.com',
'url1': '/xifan/drama/portalPage',
'url2': '/xifan/drama/getDuanjuInfo',
'search': '/xifan/search/getSearchList'
},
'七猫': {
'host': 'https://api-store.qmplaylet.com',
'url1': '/api/v1/playlet/index',
'url2': 'https://api-read.qmplaylet.com/player/api/v1/playlet/info',
'search': '/api/v1/playlet/search'
},
'围观': {
'host': 'https://api.drama.9ddm.com',
'url1': '/drama/home/shortVideoTags',
'url2': '/drama/home/shortVideoDetail',
'search': '/drama/home/search'
},
'河马': {
'host': 'https://www.kuaikaw.cn',
'search': '/seo/video/6007'
}
}
self.platform_list = [
{'name': '七猫短剧', 'id': '七猫'},
{'name': '星芽短剧', 'id': '星芽'},
{'name': '西饭短剧', 'id': '西饭'},
{'name': '围观短剧', 'id': '围观'},
{'name': '河马短剧', 'id': '河马'}
]
self.rule_filter_def = {
'星芽': {'area': '1', 'class2': '0', 'rank': '1'},
'西饭': {'area': '都市'},
'七猫': {'area': '0'},
'围观': {'area': ''},
'河马': {'area': '462'}
}
self.filter_options = {
'七猫': [{
'key': 'area',
'name': '分类',
'value': [
{'n': '全部', 'v': '0'},
{'n': '男频', 'v': '1'},
{'n': '新剧', 'v': '3'},
{'n': '现代言情', 'v': '21'},
{'n': '神豪', 'v': '37'},
{'n': '萌宝', 'v': '356'},
{'n': '穿越', 'v': '373'},
{'n': '战神', 'v': '527'},
{'n': '神医', 'v': '1269'},
{'n': '古装', 'v': '1272'}
]
}],
'星芽': [{
'key': 'area',
'name': '剧场',
'value': [
{'n': '剧场', 'v': '1'},
{'n': '热播短剧', 'v': '2'},
{'n': '会员专享', 'v': '8'},
{'n': '星选好剧', 'v': '7'},
{'n': '新剧', 'v': '3'},
{'n': '阳光剧场', 'v': '5'},
{'n': '排行榜', 'v': '9'}
]
}, {
'key': 'class2',
'name': '类型',
'value': [
{'n': '全部', 'v': '0'},
{'n': '都市', 'v': '4'},
{'n': '逆袭', 'v': '7'},
{'n': '古装', 'v': '5'},
{'n': '亲情', 'v': '41'},
{'n': '现代言情', 'v': '15'},
{'n': '重生', 'v': '6'},
{'n': '虐恋', 'v': '8'},
{'n': '玄幻', 'v': '35'},
{'n': '穿越', 'v': '17'},
{'n': '脑洞', 'v': '32'},
{'n': '甜宠', 'v': '33'},
{'n': '古代言情', 'v': '37'},
{'n': '战神', 'v': '24'},
{'n': '历史', 'v': '40'},
{'n': '赘婿', 'v': '26'},
{'n': '萌宝', 'v': '9'},
{'n': '神医', 'v': '25'}
]
}, {
'key': 'rank',
'name': '榜单',
'value': [
{'n': '实时热榜', 'v': '1'},
{'n': '热搜榜', 'v': '2'},
{'n': '新剧榜', 'v': '3'},
{'n': '剧单榜', 'v': '4'},
{'n': '口碑榜', 'v': '5'}
]
}],
'西饭': [{
'key': 'area',
'name': '分类',
'value': [
{'n': '都市', 'v': '都市'},
{'n': '甜宠', 'v': '甜宠'},
{'n': '逆袭', 'v': '逆袭'},
{'n': '战神', 'v': '战神'},
{'n': '古装', 'v': '古装'},
{'n': '穿越', 'v': '穿越'},
{'n': '萌宝', 'v': '萌宝'}
]
}],
'围观': [{
'key': 'area',
'name': '分类',
'value': [
{'n': '全部', 'v': ''},
{'n': '都市', 'v': '都市'},
{'n': '逆袭', 'v': '逆袭'},
{'n': '家庭', 'v': '家庭'},
{'n': '古装', 'v': '古装'},
{'n': '复仇', 'v': '复仇'},
{'n': '甜宠', 'v': '甜宠'},
{'n': '悬疑', 'v': '悬疑'},
{'n': '爱情', 'v': '爱情'},
{'n': '重生', 'v': '重生'},
{'n': '总裁', 'v': '总裁'},
{'n': '穿越', 'v': '穿越'},
{'n': '萌宝', 'v': '萌宝'},
{'n': '战神', 'v': '战神'},
{'n': '职场', 'v': '职场'},
{'n': '神豪', 'v': '神豪'},
{'n': '神医', 'v': '神医'},
{'n': '赘婿', 'v': '赘婿'}
]
}],
'河马': [{
'key': 'area',
'name': '分类',
'value': [
{'n': '甜宠', 'v': '462'},
{'n': '古装仙侠', 'v': '1102'},
{'n': '现代言情', 'v': '1145'},
{'n': '青春', 'v': '1170'},
{'n': '豪门恩怨', 'v': '585'},
{'n': '逆袭', 'v': '417-464'},
{'n': '重生', 'v': '439-465'},
{'n': '系统', 'v': '1159'},
{'n': '总裁', 'v': '1147'},
{'n': '职场商战', 'v': '943'}
]
}]
}
# 缓存
self.qm_header = {'value': None, 'timestamp': 0}
self.xingya_token = None
self.xingya_headers = self.headers_default.copy()
def init(self, extend=""):
self.extend = extend
return self
def getName(self):
return "短剧聚合"
def _md5(self, text):
return hashlib.md5(text.encode()).hexdigest().lower()
def _base64_encode(self, text):
return base64.b64encode(text.encode()).decode()
def _base64_decode(self, text):
try:
return base64.b64decode(text).decode()
except:
return text
def _get_qm_params_and_sign(self):
now = int(time.time() * 1000)
if self.qm_header['value'] and now - self.qm_header['timestamp'] < 300000:
return self.qm_header['value']
session_id = str(now)
data = {
"static_score": "0.8",
"uuid": "00000000-7fc7-08dc-0000-000000000000",
"device-id": "20250220125449b9b8cac84c2dd3d035c9052a2572f7dd0122edde3cc42a70",
"sourceuid": "aa7de295aad621a6",
"refresh-type": "0",
"model": "22021211RC",
"client-id": "aa7de295aad621a6",
"brand": "Redmi",
"sys-ver": "12",
"phone-level": "H",
"wlb-uid": "aa7de295aad621a6",
"session-id": session_id
}
json_str = json.dumps(data, separators=(',', ':'))
base64_str = self._base64_encode(json_str)
qm_params = ''
for char in base64_str:
qm_params += self.char_map.get(char, char)
params_str = f"AUTHORIZATION=app-version=10001application-id=com.duoduo.readchannel=unknownis-white=net-env=5platform=androidqm-params={qm_params}reg={self.keys}"
sign = self._md5(params_str)
self.qm_header['value'] = {'qmParams': qm_params, 'sign': sign}
self.qm_header['timestamp'] = now
return self.qm_header['value']
def _get_header_x(self):
qm = self._get_qm_params_and_sign()
return {
'net-env': '5',
'reg': '',
'channel': 'unknown',
'is-white': '',
'platform': 'android',
'application-id': 'com.duoduo.read',
'authorization': '',
'app-version': '10001',
'user-agent': 'webviewversion/0',
'qm-params': qm['qmParams'],
'sign': qm['sign']
}
def _ensure_xingya_auth(self):
if self.xingya_headers.get('authorization'):
return self.xingya_headers
try:
plat = self.platform['星芽']
res = requests.post(
plat['loginUrl'],
headers={'User-Agent': 'okhttp/4.10.0', 'platform': '1', 'Content-Type': 'application/json'},
json={'device': '24250683a3bdb3f118dff25ba4b1cba1a'},
timeout=10,
verify=False
)
data = res.json()
token = data.get('data', {}).get('token') or data.get('token')
if token:
self.xingya_headers = {**self.headers_default, 'authorization': token}
self.xingya_token = token
except Exception as e:
pass
return self.xingya_headers
def _request(self, url, method='GET', headers=None, data=None, timeout=5000):
try:
headers = {**self.headers_default, **(headers or {})}
if method.upper() == 'POST':
res = requests.post(url, headers=headers, json=data, timeout=timeout/1000, verify=False)
else:
res = requests.get(url, headers=headers, timeout=timeout/1000, verify=False)
return res.json()
except Exception as e:
return None
def homeContent(self, filter):
classes = [{'type_name': p['name'], 'type_id': p['id']} for p in self.platform_list]
filters = {}
for item in self.platform_list:
platform_id = item['id']
if platform_id in self.filter_options:
filters[platform_id] = self.filter_options[platform_id]
return {'class': classes, 'filters': filters}
def homeVideoContent(self):
return self.categoryContent('七猫', '1', False, {})
def categoryContent(self, tid, pg, filter, extend):
pg = int(pg) if pg else 1
plat = self.platform.get(tid)
area = extend.get('area') if extend.get('area') is not None else self.rule_filter_def.get(tid, {}).get('area', '')
videos = []
if not plat:
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
try:
if tid == '七猫':
if pg > 1:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
sign = self._md5(f"operation=1playlet_privacy=1tag_id={area}{self.keys}")
url = f"{plat['host']}{plat['url1']}?tag_id={area}&playlet_privacy=1&operation=1&sign={sign}"
header_x = self._get_header_x()
res = self._request(url, headers={**header_x, **self.headers_default}, timeout=3000)
if res and res.get('data', {}).get('list'):
for i in res['data']['list'][:6]:
videos.append({
'vod_id': f"七猫@{quote(str(i['playlet_id']))}",
'vod_name': i['title'],
'vod_pic': i['image_link'],
'vod_remarks': f"{i['total_episode_num']}",
'vod_content': f"七猫短剧 | {i['total_episode_num']}"
})
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
elif tid == '星芽':
headers = self._ensure_xingya_auth()
if area == '9':
rank = extend.get('rank') or extend.get('class2') or self.rule_filter_def['星芽'].get('rank', '1')
if pg > 1:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
res = self._request(f"{plat['host']}{plat['rankDetail']}?id={rank}", headers=headers, timeout=10000)
for item in res.get('data', {}).get('list', []):
i = item.get('theater') or item
if not i or not i.get('id'):
continue
videos.append({
'vod_id': f"星芽@{plat['host']}{plat['url2']}?theater_parent_id={i['id']}",
'vod_name': i['title'],
'vod_pic': i['cover_url'],
'vod_remarks': f"{i.get('total', '')}"
})
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
class2 = extend.get('class2') or self.rule_filter_def['星芽'].get('class2', '0')
url = f"{plat['host']}{plat['url1']}={area}&type=1&class2_ids={class2}&page_num={pg}&page_size=24"
res = self._request(url, headers=headers, timeout=10000)
data = res.get('data', {})
for i in data.get('list', []):
item = i.get('theater') or i
if not item or not item.get('id'):
continue
videos.append({
'vod_id': f"星芽@{plat['host']}{plat['url2']}?theater_parent_id={item['id']}",
'vod_name': item['title'],
'vod_pic': item['cover_url'],
'vod_remarks': f"{item.get('total', '')}"
})
total = int(data.get('total') or len(videos))
is_single_page = not videos or data.get('is_end') or total <= len(videos) or len(videos) > 24
if is_single_page:
return {'list': videos if pg == 1 else [], 'page': pg, 'pagecount': 1, 'limit': len(videos) if pg == 1 else 0, 'total': total}
pagecount = max(1, (total + 23) // 24)
return {'list': videos, 'page': pg, 'pagecount': pagecount, 'limit': 24, 'total': total}
elif tid == '西饭':
if pg > 1:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
search_url = f"{plat['host']}{plat['search']}?reqType=search&offset=0&keyword={quote(area or '')}&quickEngineVersion=-1&scene="
search_res = self._request(search_url, timeout=10000)
for block in search_res.get('result', {}).get('elements', []):
for item in block.get('contents', []):
dj = item.get('duanjuVo') or {}
if not dj.get('duanjuId'):
continue
categories = dj.get('categories', [])
if area and area not in categories:
continue
videos.append({
'vod_id': f"西饭@{dj['duanjuId']}#{dj['source']}",
'vod_name': dj['title'],
'vod_pic': dj['coverImageUrl'],
'vod_remarks': f"{dj.get('total', '')}"
})
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
elif tid == '围观':
device_name = 'Pixel 8 Pro'
device_firm = 'Google'
client_info = self._md5(str(int(time.time() * 1000))[-10:])
url = f"{plat['host']}{plat['search']}?version_code=1500&version_name=1.5.0&device_name={quote(device_name)}&device_type=phone&is_first_day=true&is_first_24h=true&app_launch_way=icon&default_homepage=homepage_interaction&device_owning_firm={quote(device_firm)}&font_scale=default&os_type=1&clientInfo={client_info}"
res = self._request(url, method='POST', headers={'User-Agent': 'okhttp/5.1.0', 'Content-Type': 'application/json; charset=utf-8'}, data={'audience': '全部', 'order': '最新', 'page': pg, 'pageSize': 30, 'searchWord': '', 'subject': area or ''}, timeout=10000)
for i in res.get('data', []):
videos.append({
'vod_id': f"围观@{i['oneId']}",
'vod_name': i['title'],
'vod_pic': i.get('horzPoster') or i.get('vertPoster'),
'vod_remarks': f"{i.get('episodeCount', '')}"
})
return {'list': videos, 'page': pg, 'pagecount': pg if len(videos) < 30 else pg + 1, 'limit': 30, 'total': (pg - 1) * 30 + len(videos)}
elif tid == '河马':
url = f"{plat['host']}/browse/{area or self.rule_filter_def['河马']['area']}/{pg}"
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
'Referer': url,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}
res = requests.get(url, headers=headers, timeout=10, verify=False)
html = res.text
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>', html)
if match:
json_data = json.loads(match.group(1))
page_props = json_data.get('props', {}).get('pageProps', {})
for book in page_props.get('bookList', []):
if not book.get('bookId'):
continue
videos.append({
'vod_id': f"河马@/drama/{book['bookId']}",
'vod_name': book['bookName'],
'vod_pic': book.get('coverWap'),
'vod_remarks': f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}".strip()
})
pages = int(page_props.get('pages') or pg)
return {'list': videos, 'page': pg, 'pagecount': pages, 'limit': len(videos), 'total': pages * len(videos)}
except Exception as e:
pass
except Exception as e:
pass
return {'list': videos, 'page': pg, 'pagecount': 1, 'limit': len(videos), 'total': len(videos)}
def detailContent(self, ids):
videos = []
for id in ids if isinstance(ids, list) else [ids]:
if not id:
continue
parts = id.split('@', 1)
if len(parts) < 2:
continue
plat_id, did = parts[0], parts[1]
plat = self.platform.get(plat_id)
if not plat:
videos.append({'vod_id': id, 'vod_name': '平台不支持', 'vod_play_url': ''})
continue
vod = {'vod_id': id, 'vod_name': '未知', 'vod_pic': '', 'vod_remarks': '', 'vod_content': '', 'vod_play_from': '', 'vod_play_url': ''}
try:
if plat_id == '七猫':
did_decoded = unquote(did)
sign = self._md5(f"playlet_id={did_decoded}{self.keys}")
url = f"{plat['url2']}?playlet_id={did_decoded}&sign={sign}"
header_x = self._get_header_x()
res = self._request(url, headers={**header_x, **self.headers_default})
if res and res.get('data'):
d = res['data']
play_list = d.get('play_list', [])
play_url = '#'.join([f"{i['sort']}${i['video_url']}" for i in play_list])
vod = {**vod, 'vod_name': d['title'], 'vod_pic': d['image_link'], 'vod_remarks': f"{d['total_episode_num']}", 'vod_content': d.get('intro', ''), 'vod_play_from': '七猫短剧', 'vod_play_url': play_url}
elif plat_id == '星芽':
headers = self._ensure_xingya_auth()
res = self._request(did, headers=headers, timeout=10000)
if res and res.get('data'):
d = res['data']
theaters = d.get('theaters', [])
play_url = '#'.join([f"{i['num']}${i['son_video_url']}" for i in theaters])
vod = {**vod, 'vod_name': d['title'], 'vod_pic': d['cover_url'], 'vod_remarks': str(d.get('desc_tags', '')), 'vod_play_from': '星芽短剧', 'vod_play_url': play_url}
elif plat_id == '西饭':
duanju_id, source = did.split('#', 1)
url = f"{plat['host']}{plat['url2']}?duanjuId={duanju_id}&source={source}"
res = self._request(url)
if res and res.get('result'):
d = res['result']
episode_list = d.get('episodeList', [])
play_url = '#'.join([f"{e['index']}${e['playUrl']}" for e in episode_list])
status = '已完结' if d.get('updateStatus') == 'over' else f"更新{d.get('total', '')}"
vod = {**vod, 'vod_name': d['title'], 'vod_pic': d['coverImageUrl'], 'vod_remarks': f"{d.get('total', '')}{status}", 'vod_play_from': '西饭短剧', 'vod_play_url': play_url}
elif plat_id == '围观':
device_name = 'Pixel 8 Pro'
device_firm = 'Google'
client_info = self._md5(str(int(time.time() * 1000))[-10:])
url = f"{plat['host']}{plat['url2']}?version_code=1500&version_name=1.5.0&device_name={quote(device_name)}&device_type=phone&is_first_day=true&is_first_24h=true&app_launch_way=icon&default_homepage=homepage_interaction&device_owning_firm={quote(device_firm)}&font_scale=default&os_type=1&clientInfo={client_info}&oneId={did}&page=1&pageSize=1000&userId=0&queryAll=true"
res = self._request(url, headers={'User-Agent': 'okhttp/5.1.0', 'Content-Type': 'application/json; charset=utf-8'}, timeout=10000)
episodes = res.get('data', [])
if episodes:
play_url = '#'.join([f"{e.get('playOrder') or e.get('title')}${self._base64_encode(json.dumps(e.get('videoClarityList', [])))}" for e in episodes])
vod = {**vod, 'vod_name': res.get('title') or episodes[0].get('title') or vod['vod_name'], 'vod_pic': res.get('vertPoster') or episodes[0].get('vertPoster') or '', 'vod_remarks': f"{len(episodes)}", 'vod_content': res.get('description', ''), 'vod_play_from': '围观短剧', 'vod_play_url': play_url}
elif plat_id == '河马':
did_path = did if did.startswith('/drama/') else f"/drama/{did}"
full_url = f"{plat['host']}{did_path}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
'Referer': full_url,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}
res = requests.get(full_url, headers=headers, timeout=10, verify=False)
html = res.text
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>', html)
if match:
json_data = json.loads(match.group(1))
page_props = json_data.get('props', {}).get('pageProps', {})
book_info = page_props.get('bookInfoVo', {})
chapter_list = page_props.get('chapterList', [])
play_urls = []
for chapter in chapter_list:
chapter_id = chapter.get('chapterId')
chapter_name = chapter.get('chapterName')
video_vo = chapter.get('chapterVideoVo', {})
direct_url = video_vo.get('mp4') or video_vo.get('mp4720p') or video_vo.get('vodMp4Url')
if direct_url and re.search(r'\.(mp4|m3u8)', direct_url, re.I):
play_urls.append(f"{chapter_name}${direct_url}")
else:
drama_id = did_path.replace('/drama/', '')
play_urls.append(f"{chapter_name}${drama_id}+{chapter_id}")
vod = {**vod, 'vod_name': book_info.get('title') or book_info.get('bookName') or vod['vod_name'], 'vod_pic': book_info.get('coverWap') or '', 'vod_remarks': f"{book_info.get('statusDesc', '')} {book_info.get('totalChapterNum', '')}".strip(), 'vod_content': book_info.get('introduction', ''), 'vod_play_from': '河马短剧', 'vod_play_url': '#'.join(play_urls)}
except Exception as e:
vod['vod_name'] = '加载失败'
videos.append(vod)
return {'list': videos}
def playerContent(self, flag, id, vipFlags):
if '七猫' in flag:
return {'parse': 0, 'url': id}
if '西饭' in flag:
try:
res = requests.get(id, headers={'User-Agent': 'Mozilla/5.0'}, timeout=10, verify=False, allow_redirects=True)
final_url = res.url
return {'parse': 0, 'url': final_url or id}
except:
return {'parse': 0, 'url': id}
if '围观' in flag:
try:
ps = json.loads(self._base64_decode(id))
urls = []
for item in ps or []:
if item.get('name') and item.get('url'):
urls.extend([item['name'], item['url']])
return {'parse': 0, 'url': urls if urls else id, 'headers': {'User-Agent': 'okhttp/5.1.0'}}
except:
return {'parse': 0, 'url': id}
if '河马' in flag:
if re.search(r'\.(mp4|m3u8)', id, re.I):
return {'parse': 0, 'url': id}
parts = id.split('+', 1)
if len(parts) >= 2:
drama_id, chapter_id = parts
episode_url = f"{self.platform['河马']['host']}/episode/{drama_id}/{chapter_id}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
'Referer': episode_url,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}
try:
res = requests.get(episode_url, headers=headers, timeout=10, verify=False)
html = res.text
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">([\s\S]*?)</script>', html)
if match:
json_data = json.loads(match.group(1))
video_info = json_data.get('props', {}).get('pageProps', {}).get('chapterInfo', {}).get('chapterVideoVo', {})
video_url = video_info.get('mp4') or video_info.get('mp4720p') or video_info.get('vodMp4Url')
if not video_url:
m = re.search(r'(https?://[^"\']+\.mp4[^"\']*)', html)
video_url = m.group(1) if m else ''
return {'parse': 0, 'url': video_url}
except:
pass
return {'parse': 0, 'url': id}
return {'parse': 0, 'url': id}
def searchContent(self, key, quick, pg="1"):
pg = int(pg) if pg else 1
if not key:
return {
'list': [],
'page': pg,
'pagecount': 1,
'limit': 0,
'total': 0
}
videos = []
seen = set()
def safe_push(item):
if not item:
return
vod_id = str(item.get('vod_id', '')).strip()
vod_name = str(item.get('vod_name', '')).strip()
if not vod_id or not vod_name:
return
if vod_id in seen:
return
item['vod_id'] = vod_id
item['vod_name'] = vod_name
seen.add(vod_id)
videos.append(item)
# 七猫搜索
try:
sign = self._md5(
f"operation=2playlet_privacy=1search_word={key}{self.keys}"
)
url = (
f"{self.platform['七猫']['host']}"
f"{self.platform['七猫']['search']}"
f"?search_word={quote(key)}"
f"&playlet_privacy=1"
f"&operation=2"
f"&sign={sign}"
)
header_x = self._get_header_x()
res = self._request(
url,
headers={**header_x, **self.headers_default},
timeout=6000
)
if res:
for i in res.get('data', {}).get('list', []):
safe_push({
'vod_id': f"七猫@{quote(str(i['playlet_id']))}",
'vod_name': i.get('title', ''),
'vod_pic': i.get('image_link', ''),
'vod_remarks': f"七猫短剧|{i.get('total_episode_num', '')}"
})
except Exception:
pass
# 星芽搜索
try:
plat = self.platform['星芽']
headers = self._ensure_xingya_auth()
res = self._request(
plat['host'] + plat['search'],
method='POST',
headers=headers,
data={'text': key},
timeout=10000
)
if res:
data = res.get('data', {})
search_list = (
data.get('theater', {}).get('search_data', [])
or data.get('search_data', [])
or data.get('list', [])
)
for i in search_list:
if not i.get('id'):
continue
safe_push({
'vod_id': f"星芽@{plat['host']}{plat['url2']}?theater_parent_id={i['id']}",
'vod_name': i.get('title', ''),
'vod_pic': i.get('cover_url', ''),
'vod_remarks': f"星芽短剧|{i.get('total', '')}"
})
except Exception:
pass
# 西饭搜索
try:
plat = self.platform['西饭']
url = (
f"{plat['host']}{plat['search']}"
f"?reqType=search"
f"&offset={(pg - 1) * 30}"
f"&keyword={quote(key)}"
f"&quickEngineVersion=-1"
f"&scene="
)
res = self._request(url)
if res:
elements = res.get('result', {}).get('elements', [])
for block in elements:
if block.get('duanjuVo'):
contents = [block]
else:
contents = block.get('contents', [])
for item in contents:
dj = item.get('duanjuVo') or {}
if not dj.get('duanjuId'):
continue
safe_push({
'vod_id': f"西饭@{dj['duanjuId']}#{dj['source']}",
'vod_name': dj.get('title', ''),
'vod_pic': dj.get('coverImageUrl', ''),
'vod_remarks': f"西饭短剧|{dj.get('total', '')}"
})
except Exception:
pass
# 围观搜索
try:
plat = self.platform['围观']
device_name = 'Pixel 8 Pro'
device_firm = 'Google'
client_info = self._md5(str(int(time.time() * 1000))[-10:])
url = (
f"{plat['host']}{plat['search']}"
f"?version_code=1500"
f"&version_name=1.5.0"
f"&device_name={quote(device_name)}"
f"&device_type=phone"
f"&is_first_day=true"
f"&is_first_24h=true"
f"&app_launch_way=icon"
f"&default_homepage=homepage_interaction"
f"&device_owning_firm={quote(device_firm)}"
f"&font_scale=default"
f"&os_type=1"
f"&clientInfo={client_info}"
)
res = self._request(
url,
method='POST',
headers={
'User-Agent': 'okhttp/5.1.0',
'Content-Type': 'application/json; charset=utf-8'
},
data={
'audience': '',
'order': '',
'page': pg,
'pageSize': 30,
'searchWord': key,
'subject': ''
},
timeout=10000
)
if res:
for i in res.get('data', []):
safe_push({
'vod_id': f"围观@{i['oneId']}",
'vod_name': i.get('title', ''),
'vod_pic': i.get('horzPoster') or i.get('vertPoster'),
'vod_remarks': f"围观短剧|{i.get('episodeCount', '')}"
})
except Exception:
pass
# 河马搜索
try:
plat = self.platform['河马']
tmpid = ''.join(random.choices('0123456789abcdefghijklmnopqrstuvwxyz', k=16))
headers = {
'User-Agent': 'Mozilla/5.0',
'Referer': f"{plat['host']}/search?searchValue={quote(key)}",
'Origin': 'https://www.kuaikaw.cn',
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
'pname': 'www.kuaikaw.cn',
'tmpid': tmpid
}
res = self._request(
f"{plat['host']}{plat['search']}",
method='POST',
headers=headers,
data={
'sourceType': 1,
'keyword': key,
'index': pg,
'page': pg
},
timeout=10000
)
if res:
for book in res.get('data', {}).get('bookList', []):
if not book.get('bookId'):
continue
safe_push({
'vod_id': f"河马@/drama/{book['bookId']}",
'vod_name': book.get('bookName', ''),
'vod_pic': book.get('coverWap', ''),
'vod_remarks': (
f"{book.get('statusDesc', '')} "
f"{book.get('totalChapterNum', '')}"
).strip()
})
except Exception:
pass
return {
'list': videos,
'page': pg,
'pagecount': 1,
'limit': len(videos),
'total': len(videos)
}
+1260
View File
@@ -0,0 +1,1260 @@
# -*- coding: utf-8 -*-
# by @
# 修复版本 - 参考最新三合一.js重构虎牙、斗鱼、B站直播逻辑
# 修复:虎牙清晰度选择,确保ratio参数正确传递码率值
# 修复:斗鱼切换分辨率只能播放1秒的问题(每次重新获取安全密钥和签名)
# 修复:B站使用特殊UA和WBI签名绕过-352风控 [^90^][^30^]
import json
import re
import sys
import time
import hashlib
import random
import urllib.parse
from base64 import b64decode, b64encode
from urllib.parse import parse_qs
import requests
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
from concurrent.futures import ThreadPoolExecutor
class Spider(Spider):
def init(self, extend=""):
# 初始化B站WBI密钥
self.bili_wbi_keys = None
self.bili_wbi_expire = 0
pass
def getName(self):
return "直播"
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
headers = [
{
# 特殊UA绕过B站风控 [^90^]
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0"
},
{
"User-Agent": "Dart/3.4 (dart:io)"
}
]
excepturl = 'https://www.baidu.com'
hosts = {
"huya": ["https://www.huya.com", "https://mp.huya.com"],
"douyu": "https://www.douyu.com",
"wangyi": "https://cc.163.com",
"bili": ["https://api.live.bilibili.com", "https://api.bilibili.com"]
}
referers = {
"huya": "https://live.cdn.huya.com",
"douyu": "https://m.douyu.com",
"bili": "https://live.bilibili.com"
}
playheaders = {
"wangyi": {
"User-Agent": "ExoPlayer",
"Connection": "Keep-Alive",
"Icy-MetaData": "1"
},
"bili": {
'Accept': '*/*',
'Icy-MetaData': '1',
'referer': 'https://live.bilibili.com',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
},
'huya': {
'User-Agent': 'ExoPlayer',
'Connection': 'Keep-Alive',
'Icy-MetaData': '1'
},
'douyu': {
'User-Agent': 'libmpv',
'Icy-MetaData': '1'
}
}
# WBI签名相关常量 [^30^]
MIXIN_KEY_ENC_TAB = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
]
def _get_bili_wbi_keys(self):
"""获取B站WBI密钥 [^30^]"""
try:
# 检查缓存
if self.bili_wbi_keys and time.time() < self.bili_wbi_expire:
return self.bili_wbi_keys
# 从导航接口获取 - 使用特殊UA [^90^]
resp = self.fetch(
'https://api.bilibili.com/x/web-interface/nav',
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Referer': 'https://www.bilibili.com/'
}
).json()
if resp.get('code') != 0:
return None
img_url = resp['data']['wbi_img']['img_url']
sub_url = resp['data']['wbi_img']['sub_url']
# 提取文件名作为key
img_key = img_url.rsplit('/', 1)[1].split('.')[0]
sub_key = sub_url.rsplit('/', 1)[1].split('.')[0]
self.bili_wbi_keys = (img_key, sub_key)
self.bili_wbi_expire = time.time() + 86400 # 24小时过期
return self.bili_wbi_keys
except Exception as e:
print(f"获取B站WBI密钥失败: {e}")
return None
def _get_mixin_key(self, orig: str):
"""生成mixin_key [^30^]"""
return ''.join([orig[i] for i in self.MIXIN_KEY_ENC_TAB])[:32]
def _enc_wbi(self, params: dict):
"""WBI签名 [^30^]"""
keys = self._get_bili_wbi_keys()
if not keys:
return params
img_key, sub_key = keys
mixin_key = self._get_mixin_key(img_key + sub_key)
# 添加时间戳
params['wts'] = round(time.time())
# 排序参数
params = dict(sorted(params.items()))
# 过滤特殊字符
params = {
k: ''.join(filter(lambda c: c not in "!'()*", str(v)))
for k, v in params.items()
}
# 计算签名
query = urllib.parse.urlencode(params)
w_rid = hashlib.md5((query + mixin_key).encode()).hexdigest()
params['w_rid'] = w_rid
return params
def process_bili(self):
"""获取B站分类列表 - 使用WBI签名 [^30^]"""
try:
# 尝试获取分类列表 - 使用特殊UA和WBI签名
params = {'need_entrance': 1, 'parent_id': 0}
signed_params = self._enc_wbi(params)
data = self.fetch(
f'{self.hosts["bili"][0]}/room/v1/Area/getList',
params=signed_params,
headers=self.headers[0]
).json()
if data.get('code') == 0 and data.get('data'):
# 保存分类数据供后续使用
self.bili_areas = data['data']
return ('bili', [{'key': 'cate', 'name': '分类',
'value': [{'n': i['name'], 'v': str(i['id'])}
for i in data['data']]}])
return 'bili', None
except Exception as e:
print(f"bili处理错误: {e}")
# 使用默认分类
return 'bili', [{'key': 'cate', 'name': '分类',
'value': [{'n': '网游', 'v': '2'}, {'n': '手游', 'v': '3'},
{'n': '单机', 'v': '6'}, {'n': '娱乐', 'v': '1'},
{'n': '电台', 'v': '5'}, {'n': '虚拟主播', 'v': '9'},
{'n': '生活', 'v': '10'}, {'n': '知识', 'v': '11'},
{'n': '赛事', 'v': '13'}]}]
def process_douyu(self):
try:
self.dyufdata = self.fetch(
f'{self.referers["douyu"]}/api/cate/list',
headers=self.headers[1]
).json()
return ('douyu', [{'key': 'cate', 'name': '分类',
'value': [{'n': i['cate1Name'], 'v': str(i['cate1Id'])}
for i in self.dyufdata['data']['cate1Info']]}])
except Exception as e:
print(f"douyu错误: {e}")
return 'douyu', None
def homeContent(self, filter):
result = {}
cateManual = {
"虎牙": "huya",
"斗鱼": "douyu",
"网易": "wangyi"
}
classes = []
filters = {
'huya': [{'key': 'cate', 'name': '分类',
'value': [{'n': '网游', 'v': '1'}, {'n': '单机', 'v': '2'},
{'n': '娱乐', 'v': '8'}, {'n': '手游', 'v': '3'}]}]
}
with ThreadPoolExecutor(max_workers=2) as executor:
futures = {
executor.submit(self.process_bili): 'bili',
executor.submit(self.process_douyu): 'douyu'
}
for future in futures:
platform, filter_data = future.result()
if filter_data:
filters[platform] = filter_data
for k in cateManual:
classes.append({
'type_name': k,
'type_id': cateManual[k]
})
result['class'] = classes
result['filters'] = filters
return result
def homeVideoContent(self):
pass
def categoryContent(self, tid, pg, filter, extend):
vdata = []
result = {}
pagecount = 9999
result['page'] = pg
result['limit'] = 90
result['total'] = 999999
if tid == 'wangyi':
vdata, pagecount = self.wyccContent(tid, pg, filter, extend, vdata)
elif 'bili' in tid:
vdata, pagecount = self.biliContent(tid, pg, filter, extend, vdata)
elif 'huya' in tid:
vdata, pagecount = self.huyaContent(tid, pg, filter, extend, vdata)
elif 'douyu' in tid:
vdata, pagecount = self.douyuContent(tid, pg, filter, extend, vdata)
result['list'] = vdata
result['pagecount'] = pagecount
return result
def wyccContent(self, tid, pg, filter, extend, vdata):
params = {
'format': 'json',
'start': (int(pg) - 1) * 20,
'size': '20',
}
response = self.fetch(f'{self.hosts[tid]}/api/category/live/', params=params, headers=self.headers[0]).json()
for i in response['lives']:
if i.get('cuteid'):
bvdata = self.buildvod(
vod_id=f"{tid}@@{i['cuteid']}",
vod_name=i.get('title'),
vod_pic=i.get('cover'),
vod_remarks=i.get('nickname'),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(bvdata)
return vdata, 9999
def biliContent(self, tid, pg, filter, extend, vdata):
"""B站分类内容 - 使用WBI签名绕过风控 [^30^][^90^]"""
try:
# 分类列表 - 显示子分类
if extend.get('cate') and pg == '1' and 'click' not in tid:
# 从已保存的分类数据中找到对应分类的子分类
if hasattr(self, 'bili_areas'):
for area in self.bili_areas:
if str(area['id']) == extend['cate']:
for sub_area in area.get('list', []):
v = self.buildvod(
vod_id=f"click_{tid}@@{extend['cate']}@@{sub_area['id']}",
vod_name=sub_area.get('name'),
vod_pic=sub_area.get('pic'),
vod_tag=1,
style={"type": "oval", "ratio": 1}
)
vdata.append(v)
return vdata, 1
# 如果没有找到子分类,直接返回空,让用户进入房间列表
return vdata, 1
# 房间列表 - 使用getList接口并添加WBI签名 [^30^]
if 'click' in tid:
# 子分类房间
ids = tid.split('_')[1].split('@@')
tid = ids[0]
parent_area_id = ids[1]
area_id = ids[2]
else:
# 默认使用分类ID作为parent_area_idarea_id为0表示该分类下所有
parent_area_id = extend.get('cate', '2') # 默认网游
area_id = 0
# 构建请求参数并添加WBI签名 [^30^]
params = {
'parent_area_id': parent_area_id,
'area_id': area_id,
'page': pg,
'platform': 'web',
'sort_type': 'online' # 按热度排序
}
signed_params = self._enc_wbi(params)
# 调用getList接口
api_url = f'{self.hosts[tid][0]}/xlive/web-interface/v1/second/getList'
data = self.fetch(api_url, params=signed_params, headers=self.headers[0]).json()
# 如果WBI签名失败,尝试不带签名
if data.get('code') == -352:
print("WBI签名失败,尝试无签名请求...")
params = {
'parent_area_id': parent_area_id,
'area_id': area_id,
'page': pg,
'platform': 'web',
'sort_type': 'online'
}
data = self.fetch(api_url, params=params, headers=self.headers[0]).json()
if data.get('code') == 0:
room_list = data.get('data', {}).get('list', [])
for room in room_list:
if room.get('roomid'):
# 处理在线人数显示
online = room.get('online', 0)
if online > 10000:
online_str = f"{online / 10000:.1f}"
else:
online_str = str(online)
v = self.buildvod(
f"{tid}@@{room['roomid']}",
room.get('title', '未知标题'),
room.get('cover') or room.get('system_cover'),
f"{online_str}",
0,
room.get('uname', ''),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(v)
# 检查是否有更多数据
has_more = data.get('data', {}).get('has_more', 0)
if not has_more:
pagecount = int(pg)
else:
pagecount = 9999
else:
print(f"B站API返回错误: {data.get('message', '未知错误')} (code: {data.get('code')})")
pagecount = 1
return vdata, pagecount
except Exception as e:
print(f"B站内容获取错误: {e}")
return vdata, 1
def huyaContent(self, tid, pg, filter, extend, vdata):
if extend.get('cate') and pg == '1' and 'click' not in tid:
id = extend.get('cate')
data = self.fetch(f'{self.referers[tid]}/liveconfig/game/bussLive?bussType={id}',
headers=self.headers[1]).json()
for i in data['data']:
v = self.buildvod(
vod_id=f"click_{tid}@@{int(i['gid'])}",
vod_name=i.get('gameFullName'),
vod_pic=f'https://huyaimg.msstatic.com/cdnimage/game/{int(i["gid"])}-MS.jpg',
vod_tag=1,
style={"type": "oval", "ratio": 1}
)
vdata.append(v)
return vdata, 1
else:
gid = ''
if 'click' in tid:
ids = tid.split('_')[1].split('@@')
tid = ids[0]
gid = f'&gameId={ids[1]}'
data = self.fetch(f'{self.hosts[tid][0]}/cache.php?m=LiveList&do=getLiveListByPage&tagAll=0{gid}&page={pg}',
headers=self.headers[1]).json()
for i in data['data']['datas']:
if i.get('profileRoom'):
v = self.buildvod(
f"{tid}@@{i['profileRoom']}",
i.get('introduction'),
i.get('screenshot'),
str(int(i.get('totalCount', '1')) / 10000) + '',
0,
i.get('nick'),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(v)
return vdata, 9999
def douyuContent(self, tid, pg, filter, extend, vdata):
if extend.get('cate') and pg == '1' and 'click' not in tid:
for i in self.dyufdata['data']['cate2Info']:
if str(i['cate1Id']) == extend['cate']:
v = self.buildvod(
vod_id=f"click_{tid}@@{i['cate2Id']}",
vod_name=i.get('cate2Name'),
vod_pic=i.get('icon'),
vod_remarks=i.get('count'),
vod_tag=1,
style={"type": "oval", "ratio": 1}
)
vdata.append(v)
return vdata, 1
else:
path = f'/japi/weblist/apinc/allpage/6/{pg}'
if 'click' in tid:
ids = tid.split('_')[1].split('@@')
tid = ids[0]
path = f'/gapi/rkc/directory/mixList/2_{ids[1]}/{pg}'
url = f'{self.hosts[tid]}{path}'
data = self.fetch(url, headers=self.headers[1]).json()
for i in data['data']['rl']:
v = self.buildvod(
vod_id=f"{tid}@@{i['rid']}",
vod_name=i.get('rn'),
vod_pic=i.get('rs16'),
vod_year=str(int(i.get('ol', 1)) / 10000) + '',
vod_remarks=i.get('nn'),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(v)
return vdata, 9999
def detailContent(self, ids):
ids = ids[0].split('@@')
if ids[0] == 'wangyi':
vod = self.wyccDetail(ids)
elif ids[0] == 'bili':
vod = self.biliDetail(ids)
elif ids[0] == 'huya':
vod = self.huyaDetail(ids)
elif ids[0] == 'douyu':
vod = self.douyuDetail(ids)
return {'list': [vod]}
def wyccDetail(self, ids):
try:
vdata = self.getpq(f'{self.hosts[ids[0]]}/{ids[1]}', self.headers[0])('script').eq(-1).text()
def get_quality_name(vbr):
if vbr <= 600:
return "标清"
elif vbr <= 1000:
return "高清"
elif vbr <= 2000:
return "超清"
else:
return "蓝光"
data = json.loads(vdata)['props']['pageProps']['roomInfoInitData']
name = data['live'].get('title', ids[0])
vod = self.buildvod(vod_name=data.get('keywords_suffix'), vod_remarks=data['live'].get('title'),
vod_content=data.get('description_suffix'))
resolution_data = data['live']['quickplay']['resolution']
all_streams = {}
sorted_qualities = sorted(resolution_data.items(),
key=lambda x: x[1]['vbr'],
reverse=True)
for quality, data in sorted_qualities:
vbr = data['vbr']
quality_name = get_quality_name(vbr)
for cdn_name, url in data['cdn'].items():
if cdn_name not in all_streams and type(url) == str and url.startswith('http'):
all_streams[cdn_name] = []
if isinstance(url, str) and url.startswith('http'):
all_streams[cdn_name].extend([quality_name, url])
plists = []
names = []
for i, (cdn_name, stream_list) in enumerate(all_streams.items(), 1):
names.append(f'线路{i}')
pstr = f"{name}${ids[0]}@@{self.e64(json.dumps(stream_list))}"
plists.append(pstr)
vod['vod_play_from'] = "$$$".join(names)
vod['vod_play_url'] = "$$$".join(plists)
return vod
except Exception as e:
return self.handle_exception(e)
def biliDetail(self, ids):
"""
B站直播详情 - 使用playUrl接口获取多清晰度
"""
try:
room_id = ids[1]
# 获取房间信息
info_res = self.fetch(
f'{self.hosts["bili"][0]}/room/v1/Room/get_info?room_id={room_id}',
headers=self.headers[0]
).json()
if info_res.get('code') != 0:
return self.handle_exception(Exception("获取房间信息失败"))
room_info = info_res['data']
title = room_info.get('title', 'B站直播')
vod = self.buildvod(
vod_name=title,
type_name=f"{room_info.get('parent_area_name', '')}/{room_info.get('area_name', '')}",
vod_director=room_info.get('uname', ''),
vod_remarks=f"在线{room_info.get('online', 0)}"
)
# 获取播放地址信息
play_res = self.fetch(
f'{self.hosts["bili"][0]}/room/v1/Room/playUrl?cid={room_id}&qn=10000&platform=web',
headers={
**self.headers[0],
'Referer': 'https://live.bilibili.com/',
'Origin': 'https://live.bilibili.com'
}
).json()
if play_res.get('code') != 0:
return self.handle_exception(Exception("获取播放地址失败"))
play_data = play_res['data']
accept_quality = play_data.get('accept_quality', ['10000', '400', '250', '150'])
quality_desc = {item['qn']: item['desc'] for item in play_data.get('quality_description', [])}
# 构建清晰度列表
qualities = []
for qn in sorted([int(q) for q in accept_quality], reverse=True):
desc = quality_desc.get(qn, f'清晰度{qn}')
qualities.append(f"{desc}$bili@@{room_id}@@{qn}")
vod['vod_play_from'] = 'B站直播'
vod['vod_play_url'] = '#'.join(qualities)
return vod
except Exception as e:
print(f"B站详情错误: {e}")
return self.handle_exception(e)
def huyaDetail(self, ids):
"""
虎牙播放详情 - 参考最新三合一.js重构
支持多线路多清晰度选择
核心算法:通过房间信息API获取uid、streamName和rateArray,为每个清晰度生成签名URL
清晰度说明:
- 蓝光8M/6M/4M/10M = 8000/6000/4000/10000 kbps = 1080P+
- 蓝光 = 3000 kbps = 1080P
- 超清 = 2000 kbps = 1080P (官方标准)
- 高清 = 1200 kbps = 720P
- 标清/流畅 = 500-800 kbps = 480P/540P
"""
try:
room_id = ids[1]
# 1. 获取房间信息
api_url = f'{self.hosts[ids[0]][1]}/cache.php?m=Live&do=profileRoom&roomid={room_id}'
res = self.fetch(api_url, headers=self.headers[0])
if res.status_code != 200:
return self.handle_exception(Exception(f"API请求失败: {res.status_code}"))
data = res.json()
if not data or not data.get('data'):
return self.handle_exception(Exception("房间数据为空"))
room_data = data['data']
# 2. 提取关键信息
uid = room_data.get('profileInfo', {}).get('uid')
stream_info = room_data.get('stream', {})
live_data = room_data.get('liveData', {})
if not uid:
return self.handle_exception(Exception("缺少uid"))
# 3. 获取streamName和码率信息
base_stream_list = stream_info.get('baseSteamInfoList', [])
if not base_stream_list:
return self.handle_exception(Exception("无直播流信息"))
# 获取第一个CDN的streamName作为基准
base_stream = base_stream_list[0]
stream_name = base_stream.get('sStreamName')
if not stream_name:
return self.handle_exception(Exception("无法获取streamName"))
# 4. 构建VOD对象
vod = self.buildvod(
vod_name=live_data.get('introduction', '虎牙直播'),
type_name=live_data.get('gameFullName', ''),
vod_director=live_data.get('nick', ''),
vod_remarks=live_data.get('contentIntro', ''),
)
# 5. 获取所有CDN线路
cdn_list = []
for stream in base_stream_list:
cdn_type = stream.get('sCdnType', 'AL')
flv_url = stream.get('sFlvUrl', '')
hls_url = stream.get('sHlsUrl', '')
stream_name_cdn = stream.get('sStreamName', stream_name)
if flv_url:
cdn_list.append({
'cdn': cdn_type,
'flv_base': flv_url,
'hls_base': hls_url,
'stream_name': stream_name_cdn,
'priority': stream.get('iWebPriorityRate', 0)
})
# 按优先级排序
cdn_list.sort(key=lambda x: x['priority'], reverse=True)
# 6. 获取清晰度列表 (rateArray)
rate_array = stream_info.get('rateArray', [])
# 如果没有rateArray,尝试从vMultiStreamInfo获取
if not rate_array and 'vMultiStreamInfo' in room_data:
rate_array = room_data['vMultiStreamInfo']
# 如果仍然没有,使用默认清晰度(按虎牙官方标准)
if not rate_array:
rate_array = [
{'sDisplayName': '蓝光4M', 'iBitRate': 4000},
{'sDisplayName': '蓝光', 'iBitRate': 3000},
{'sDisplayName': '超清', 'iBitRate': 2000}, # 2000kbps = 1080P
{'sDisplayName': '高清', 'iBitRate': 1200}, # 1200kbps = 720P
{'sDisplayName': '流畅', 'iBitRate': 500}
]
# 过滤和排序清晰度
# 虎牙的rateArray中,iBitRate就是码率值,sDisplayName是显示名称
# 需要确保:超清=2000kbps(1080P),高清=1200kbps(720P)
filtered_rates = []
seen_bitrates = set()
for rate in rate_array:
bit_rate = rate.get('iBitRate', 0)
name = rate.get('sDisplayName', '')
# 跳过重复的码率
if bit_rate in seen_bitrates:
continue
# 修正清晰度名称,确保符合虎牙标准
# 2000kbps应该是超清(1080P),不是高清
if bit_rate == 2000 and ('高清' in name or '720' in name):
name = '超清' # 强制修正为超清
elif bit_rate == 1200 and ('标清' in name or '480' in name):
name = '高清' # 1200kbps对应高清
elif bit_rate == 2000 and name == '原画':
name = '超清' # 修正原画为超清
seen_bitrates.add(bit_rate)
filtered_rates.append({
'sDisplayName': name,
'iBitRate': bit_rate
})
# 按码率从高到低排序
sorted_rates = sorted(filtered_rates, key=lambda x: x['iBitRate'], reverse=True)
# 7. 为每个CDN生成各清晰度的播放URL
play_lines = []
line_names = []
for cdn_idx, cdn in enumerate(cdn_list[:3]): # 最多取3个CDN
cdn_name = cdn['cdn']
line_names.append(f"线路{cdn_idx + 1}({cdn_name})")
qualities = []
for rate in sorted_rates:
quality_name = rate['sDisplayName']
bit_rate = rate['iBitRate']
# 生成该清晰度的URL
quality_url = self._generate_huya_play_url(
cdn, uid, stream_name, bit_rate
)
qualities.extend([quality_name, quality_url])
# 编码该线路的所有清晰度
encoded_qualities = self.e64(json.dumps(qualities))
play_lines.append(f"{live_data.get('introduction', '直播')}${ids[0]}@@{encoded_qualities}")
# 8. 构建播放数据
vod['vod_play_from'] = "$$$".join(line_names)
vod['vod_play_url'] = "$$$".join(play_lines)
return vod
except Exception as e:
return self.handle_exception(e)
def _generate_huya_play_url(self, cdn, uid, stream_name, bit_rate):
"""
生成虎牙播放URL,参考最新三合一.js算法
关键:ratio参数必须正确设置为iBitRate值(如2000、4000等)
"""
# 基础URL构建
flv_base = cdn['flv_base']
stream = cdn['stream_name']
# 生成时间戳和签名参数
timestamp = int(time.time())
seqid = f"{uid}{timestamp}"
ss = hashlib.md5(f"{seqid}|huya_adr|102".encode()).hexdigest()
ws_time = hex(timestamp + 21600)[2:] # 16进制,有效期6小时
# 计算wsSecret
ws_secret = hashlib.md5(
f"DWq8BcJ3h6DJt6TY_{uid}_{stream_name}_{ss}_{ws_time}".encode()
).hexdigest()
# 构建基础URL
base_url = f"{flv_base}/{stream}.flv"
# 关键修复:ratio参数直接使用iBitRate值
# 超清=2000,高清=1200,蓝光=3000/4000/6000/8000
if bit_rate > 0:
ratio_param = f"ratio={bit_rate}"
else:
# 原画/0码率时,使用默认2000或从URL推断
ratio_param = "ratio=2000"
# 构建完整URL
play_url = (
f"{base_url}?{ratio_param}&wsSecret={ws_secret}&wsTime={ws_time}"
f"&ctype=huya_adr&seqid={seqid}&uid={uid}"
f"&fs=bgct&ver=1&t=102"
)
return play_url
def douyuDetail(self, ids):
"""
斗鱼播放详情 - 参考最新三合一.js重构
核心算法:设备ID生成 -> 获取加密密钥 -> 计算签名 -> 获取播放地址
修复:切换分辨率只能播放1秒的问题
方案:存储房间号和码率信息,在playerContent中实时获取对应码率的URL
"""
try:
channel = ids[1]
headers = self.gethr(0, zr=f'{self.hosts[ids[0]]}/{channel}')
# 1. 初始化会话和设备ID (参考JS中的initialize和setupDeviceId)
session = {}
# 请求首页获取Cookie
try:
home_res = self.fetch(f'{self.hosts[ids[0]]}/{channel}', headers=headers)
if home_res.headers.get('Set-Cookie'):
cookie_str = home_res.headers.get('Set-Cookie')
# 解析dy_did
did_match = re.search(r'dy_did=([a-f0-9]{32})', cookie_str)
if did_match:
device_id = did_match.group(1)
else:
device_id = self._generate_random_hex(32)
else:
device_id = self._generate_random_hex(32)
except:
device_id = self._generate_random_hex(32)
session['dy_did'] = device_id
session['mantine-color-scheme-value'] = 'light'
# 2. 获取房间基本信息
betard_res = self.fetch(f'{self.hosts[ids[0]]}/betard/{channel}', headers=headers).json()
if not betard_res or not betard_res.get('room'):
return self.handle_exception(Exception("获取房间信息失败"))
room_info = betard_res['room']
vname = room_info.get('room_name', '斗鱼直播')
vod = self.buildvod(
vod_name=vname,
vod_remarks=room_info.get('second_lvl_name', ''),
vod_director=room_info.get('nickname', ''),
)
# 3. 获取安全密钥 (参考JS中的getSecurityKey)
sec_url = f"{self.hosts[ids[0]]}/wgapi/livenc/liveweb/websec/getEncryption?did={device_id}"
sec_res = self.fetch(sec_url, headers=headers).json()
if not sec_res or sec_res.get('error') != 0:
return self.handle_exception(Exception("获取加密密钥失败"))
security_data = sec_res['data']
secret_key = security_data.get('key')
random_str = security_data.get('rand_str')
enc_time = security_data.get('enc_time', 1)
enc_data = security_data.get('enc_data')
# 4. 计算签名 (参考JS中的computeSignature)
current_time = int(time.time())
# 迭代计算MD5
current = random_str
for _ in range(enc_time):
current = hashlib.md5(f"{current}{secret_key}".encode()).hexdigest()
signature = hashlib.md5(f"{current}{secret_key}{channel}{current_time}".encode()).hexdigest()
# 5. 请求播放地址 (参考JS中的requestStreamData)
play_payload = {
'enc_data': enc_data,
'tt': str(current_time),
'did': device_id,
'auth': signature,
'cdn': '',
'rate': '',
'hevc': '0',
'fa': '0',
'ive': '0'
}
play_api = f"{self.hosts[ids[0]]}/lapi/live/getH5PlayV1/{channel}"
# 构建请求头带Cookie
play_headers = headers.copy()
cookie_str = '; '.join([f"{k}={v}" for k, v in session.items()])
play_headers['Cookie'] = cookie_str
play_headers['Content-Type'] = 'application/x-www-form-urlencoded'
play_res = requests.post(play_api, data=play_payload, headers=play_headers, timeout=10).json()
if not play_res or play_res.get('error') != 0:
# 尝试旧版API
play_res = self._try_legacy_douyu_api(channel, device_id, signature, current_time, play_headers)
if not play_res:
return self.handle_exception(Exception("获取播放地址失败"))
stream_info = play_res.get('data', {})
# 6. 检查并更新设备ID (参考JS中的checkAndUpdateDeviceId)
rtmp_live = stream_info.get('rtmp_live', '')
if rtmp_live:
did_match = re.search(r'did=([a-f0-9]{32})', rtmp_live)
if did_match and did_match.group(1) != device_id:
device_id = did_match.group(1)
session['dy_did'] = device_id
# 重新请求
play_payload['did'] = device_id
play_res = requests.post(play_api, data=play_payload, headers=play_headers, timeout=10).json()
if play_res and play_res.get('error') == 0:
stream_info = play_res.get('data', {})
# 7. 提取播放URL和多码率信息
stream_url = None
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
stream_url = f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
elif stream_info.get('hls_url'):
stream_url = stream_info['hls_url']
if not stream_url:
return self.handle_exception(Exception("无法获取播放地址"))
# 8. 构建多码率选项
multirates = stream_info.get('multirates', [])
# 关键修复:存储房间号和码率信息,而不是直接存储URL
# 这样在切换清晰度时可以重新获取对应码率的签名URL
qualities = []
if multirates:
# 按码率排序
sorted_rates = sorted(multirates, key=lambda x: x.get('bit', 0), reverse=True)
for rate in sorted_rates:
bit_rate = rate.get('rate', -1)
name = rate.get('name', f"{bit_rate}P")
# 存储格式:码率值,用于playerContent中重新获取URL
# 使用特殊标记#来区分这是码率值而不是URL
qualities.extend([name, f"#{bit_rate}"])
else:
# 只有原画
qualities = ['原画', '#-1']
# 同时存储房间号和设备信息,用于重新获取URL
# 格式:房间号|设备ID|签名信息(base64编码)
session_info = {
'channel': channel,
'device_id': device_id,
'secret_key': secret_key,
'random_str': random_str,
'enc_time': enc_time,
'enc_data': enc_data
}
encoded_session = self.e64(json.dumps(session_info))
# 9. 构建播放数据
# vod_play_url格式:房间名$平台@@base64(清晰度列表)@@base64(会话信息)
encoded_qualities = self.e64(json.dumps(qualities))
vod['vod_play_from'] = '斗鱼直播'
vod['vod_play_url'] = f"{vname}${ids[0]}@@{encoded_qualities}@@{encoded_session}"
return vod
except Exception as e:
return self.handle_exception(e)
def _generate_random_hex(self, length):
"""生成随机十六进制字符串"""
hex_chars = '0123456789abcdef'
return ''.join(random.choice(hex_chars) for _ in range(length))
def _try_legacy_douyu_api(self, channel, device_id, signature, timestamp, headers):
"""尝试使用旧版API获取播放地址"""
try:
legacy_payload = {
'did': device_id,
'tt': str(timestamp),
'sign': signature,
'cdn': '',
'rate': '-1',
'ver': 'Douyu_223061205',
'iar': '1',
'ive': '1',
'hevc': '0',
'fa': '0'
}
legacy_api = f"https://www.douyu.com/lapi/live/getH5Play/{channel}"
res = requests.post(legacy_api, data=legacy_payload, headers=headers, timeout=10)
return res.json() if res.status_code == 200 else None
except:
return None
def _get_douyu_play_url(self, channel, device_id, secret_key, random_str, enc_time, enc_data, rate):
"""
获取斗鱼指定码率的播放URL(带签名)
用于切换清晰度时重新获取URL
"""
try:
current_time = int(time.time())
# 重新计算签名
current = random_str
for _ in range(enc_time):
current = hashlib.md5(f"{current}{secret_key}".encode()).hexdigest()
signature = hashlib.md5(f"{current}{secret_key}{channel}{current_time}".encode()).hexdigest()
# 构建请求
play_payload = {
'enc_data': enc_data,
'tt': str(current_time),
'did': device_id,
'auth': signature,
'cdn': '',
'rate': str(rate) if rate > 0 else '',
'hevc': '0',
'fa': '0',
'ive': '0'
}
play_api = f"https://www.douyu.com/lapi/live/getH5PlayV1/{channel}"
headers = {
'User-Agent': self.headers[0]['User-Agent'],
'Referer': f'https://www.douyu.com/{channel}',
'Origin': 'https://www.douyu.com',
'Cookie': f'dy_did={device_id}; mantine-color-scheme-value=light',
'Content-Type': 'application/x-www-form-urlencoded'
}
play_res = requests.post(play_api, data=play_payload, headers=headers, timeout=10).json()
if not play_res or play_res.get('error') != 0:
# 尝试旧版API
return self._get_douyu_play_url_legacy(channel, device_id, signature, current_time, rate)
stream_info = play_res.get('data', {})
# 检查设备ID是否匹配
if stream_info.get('rtmp_live'):
did_match = re.search(r'did=([a-f0-9]{32})', stream_info['rtmp_live'])
if did_match and did_match.group(1) != device_id:
# 设备ID不匹配,使用新设备ID重新获取
return self._get_douyu_play_url(channel, did_match.group(1), secret_key, random_str, enc_time, enc_data, rate)
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
return f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
elif stream_info.get('hls_url'):
return stream_info['hls_url']
return None
except Exception as e:
print(f"获取斗鱼播放URL失败: {e}")
return None
def _get_douyu_play_url_legacy(self, channel, device_id, signature, timestamp, rate):
"""使用旧版API获取斗鱼播放URL"""
try:
legacy_payload = {
'did': device_id,
'tt': str(timestamp),
'sign': signature,
'cdn': '',
'rate': str(rate) if rate > 0 else '-1',
'ver': 'Douyu_223061205',
'iar': '1',
'ive': '1',
'hevc': '0',
'fa': '0'
}
legacy_api = f"https://www.douyu.com/lapi/live/getH5Play/{channel}"
headers = {
'User-Agent': self.headers[0]['User-Agent'],
'Referer': f'https://www.douyu.com/{channel}',
'Cookie': f'dy_did={device_id}',
'Content-Type': 'application/x-www-form-urlencoded'
}
res = requests.post(legacy_api, data=legacy_payload, headers=headers, timeout=10)
if res.status_code == 200:
data = res.json()
if data.get('error') == 0:
stream_info = data.get('data', {})
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
return f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
return None
except:
return None
def searchContent(self, key, quick, pg="1"):
pass
def playerContent(self, flag, id, vipFlags):
try:
ids = id.split('@@')
p = 1
if ids[0] in ['wangyi']:
p, url = 0, json.loads(self.d64(ids[1]))
elif ids[0] == 'bili':
p, url = self.biliplay(ids)
elif ids[0] == 'huya':
p, url = self.huyaplay(ids)
elif ids[0] == 'douyu':
p, url = self.douyuplay(ids)
return {'parse': p, 'url': url, 'header': self.playheaders[ids[0]]}
except Exception as e:
return {'parse': 1, 'url': self.excepturl, 'header': self.headers[0]}
def biliplay(self, ids):
"""
B站播放解析 - 使用playUrl接口获取指定清晰度直播流
ids: [平台, 房间号, 清晰度qn]
支持多线路返回
"""
try:
room_id = ids[1]
qn = ids[2] if len(ids) > 2 else '10000'
# 使用playUrl接口获取直播流
play_url = f'{self.hosts["bili"][0]}/room/v1/Room/playUrl?cid={room_id}&qn={qn}&platform=web'
data = self.fetch(play_url, headers={
**self.headers[0],
'Referer': 'https://live.bilibili.com/',
'Origin': 'https://live.bilibili.com'
}).json()
if data.get('code') != 0:
return 1, self.excepturl
play_data = data['data']
durl_list = play_data.get('durl', [])
if not durl_list:
return 1, self.excepturl
# 构建多线路结果 [线路1, URL1, 线路2, URL2, ...]
urls = []
for idx, item in enumerate(durl_list, 1):
url = item.get('url')
if url:
urls.extend([f'线路{idx}', url])
# 如果只有一条线路,直接返回URL
if len(urls) == 2:
return 0, urls[1] # 直接返回URL字符串
return 0, urls
except Exception as e:
print(f"B站播放错误: {e}")
return 1, self.excepturl
def huyaplay(self, ids):
"""
虎牙播放解析 - 返回所有清晰度选项供用户选择
ids[1] 格式: base64编码的 [清晰度名称1, URL1, 清晰度名称2, URL2, ...]
"""
try:
# ids[1] 是编码后的播放地址列表 [名称1, URL1, 名称2, URL2, ...]
decoded = json.loads(self.d64(ids[1]))
# decoded 是一个列表,奇数索引是名称,偶数索引是URL
return 0, decoded
except Exception as e:
print(f"虎牙播放解析错误: {e}")
return 1, self.excepturl
def douyuplay(self, ids):
"""
斗鱼播放解析 - 实时获取对应码率的播放URL
ids格式: [平台, base64(清晰度列表), base64(会话信息)]
清晰度列表: [名称1, #码率1, 名称2, #码率2, ...]
#表示这是码率值,需要重新获取URL
"""
try:
if len(ids) < 3:
# 兼容旧格式
decoded = json.loads(self.d64(ids[1]))
return 0, decoded
# 解析清晰度列表和会话信息
qualities = json.loads(self.d64(ids[1]))
session_info = json.loads(self.d64(ids[2]))
channel = session_info['channel']
device_id = session_info['device_id']
secret_key = session_info['secret_key']
random_str = session_info['random_str']
enc_time = session_info['enc_time']
enc_data = session_info['enc_data']
# 为每个清晰度实时获取播放URL
result = []
for i in range(0, len(qualities), 2):
name = qualities[i]
rate_marker = qualities[i + 1]
# 解析码率值(去掉#前缀)
if rate_marker.startswith('#'):
rate = int(rate_marker[1:])
else:
rate = -1
# 实时获取对应码率的URL
play_url = self._get_douyu_play_url(
channel, device_id, secret_key, random_str,
enc_time, enc_data, rate
)
if play_url:
result.extend([name, play_url])
if not result:
return 1, self.excepturl
return 0, result
except Exception as e:
print(f"斗鱼播放解析错误: {e}")
return 1, self.excepturl
def localProxy(self, param):
pass
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 josn_to_params(self, params, skip_empty=False):
query = []
for k, v in params.items():
if skip_empty and not v:
continue
query.append(f"{k}={v}")
return "&".join(query)
def params_to_json(self, query_string):
parsed_data = parse_qs(query_string)
result = {key: value[0] for key, value in parsed_data.items()}
return result
def buildvod(self, vod_id='', vod_name='', vod_pic='', vod_year='', vod_tag='', vod_remarks='', style='',
type_name='', vod_area='', vod_actor='', vod_director='',
vod_content='', vod_play_from='', vod_play_url=''):
vod = {
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_year': vod_year,
'vod_tag': 'folder' if vod_tag else '',
'vod_remarks': vod_remarks,
'style': style,
'type_name': type_name,
'vod_area': vod_area,
'vod_actor': vod_actor,
'vod_director': vod_director,
'vod_content': vod_content,
'vod_play_from': vod_play_from,
'vod_play_url': vod_play_url
}
vod = {key: value for key, value in vod.items() if value}
return vod
def getpq(self, url, headers=None, cookies=None):
data = self.fetch(url, headers=headers, cookies=cookies).text
try:
return pq(data)
except Exception as e:
print(f"解析页面错误: {str(e)}")
return pq(data.encode('utf-8'))
def gethr(self, index, rf='', zr=''):
headers = self.headers[index]
if zr:
headers['referer'] = zr
else:
headers['referer'] = f"{self.referers[rf]}/"
return headers
def handle_exception(self, e):
print(f"报错: {str(e)}")
return {'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'翻车啦${self.excepturl}'}
+355
View File
@@ -0,0 +1,355 @@
import requests
from bs4 import BeautifulSoup
import sqlite3
import time
import random
import os
import json
import re
import threading
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
# ===================== 配置 =====================
SAVE_DIR = "/storage/emulated/0/爬取保存/酷爱爬取"
DB_PATH = os.path.join(SAVE_DIR, "coolinet.db")
JSON_PATH = os.path.join(SAVE_DIR, "coolinet.json")
M3U_PATH = os.path.join(SAVE_DIR, "coolinet.m3u")
MAX_WORKERS = 5
MAX_RETRIES = 50
CONNECT_TIMEOUT = 30 # 连接超时
READ_TIMEOUT = 60 # 读取超时
PAGE_SLEEP = (0.5, 1.5)
DETAIL_SLEEP = (0.3, 0.8)
db_lock = threading.Lock()
json_lock = threading.Lock()
m3u_lock = threading.Lock()
class CoolinetSpider:
def __init__(self):
self.site_url = "https://www.coolinet.net"
self.session = requests.Session()
# 加上这两行,让请求走本地代理
self.session.proxies = {
'http': 'http://127.0.0.1:7890',
'https': 'http://127.0.0.1:7890'
}
self.session.headers.update({
# ... 后面不用改
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile 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',
'Connection': 'keep-alive',
'Referer': self.site_url
})
self.categories = {
'chinese-subtitle': '中文字幕',
'asia-video': '亚洲视频',
'eu-us-movie': '欧美电影',
'%e4%ba%9e%e6%b4%b2%e8%87%aa%e6%8b%8d%e5%81%b7%e6%8b%8d': '亚洲自拍偷拍',
'eu-us-self': '欧美自拍'
}
os.makedirs(SAVE_DIR, exist_ok=True)
self._init_db()
self.all_videos = []
self.total_saved = 0
def _init_db(self):
conn = sqlite3.connect(DB_PATH)
conn.execute('''CREATE TABLE IF NOT EXISTS videos (
vod_id TEXT PRIMARY KEY,
vod_name TEXT,
vod_pic TEXT,
vod_actor TEXT,
vod_director TEXT,
vod_remarks TEXT,
vod_pubdate TEXT,
vod_area TEXT,
vod_year TEXT,
vod_tags TEXT,
vod_content TEXT,
vod_play_from TEXT,
vod_play_url TEXT,
type_name TEXT
)''')
conn.commit()
conn.close()
def fetch(self, url, retries=MAX_RETRIES, extra_headers=None):
headers = self.session.headers.copy()
if extra_headers:
headers.update(extra_headers)
for i in range(retries):
try:
res = self.session.get(url, headers=headers,
timeout=(CONNECT_TIMEOUT, READ_TIMEOUT))
res.encoding = 'utf-8'
if res.status_code == 200:
return res.text
elif res.status_code == 404:
return None
elif res.status_code == 429:
# 被限流,多等一会儿
print("请求过于频繁,等待 60 秒...")
time.sleep(60)
else:
print(f"请求状态码异常: {res.status_code},重试 {i+1}/{retries}")
time.sleep(10)
except requests.exceptions.ConnectTimeout:
print(f"连接超时 (重试 {i+1}/{retries})")
time.sleep(20)
except requests.exceptions.ReadTimeout:
print(f"读取超时 (重试 {i+1}/{retries})")
time.sleep(15)
except Exception as e:
print(f"请求失败 (重试 {i+1}/{retries})")
time.sleep(30)
return None
def _page_exists(self, cate_id, page_num):
if page_num == 1:
url = f"{self.site_url}/category/{cate_id}/"
else:
url = f"{self.site_url}/category/{cate_id}/page/{page_num}/"
html = self.fetch(url)
if html is None:
return False
return 'videoPost' in html
def get_max_page(self, cate_id):
if not self._page_exists(cate_id, 1):
return 0
high = 1
while high < 10000:
next_high = high * 2
if self._page_exists(cate_id, next_high):
high = next_high
else:
break
else:
return 10000
low = high
high = high * 2
while low + 1 < high:
mid = (low + high) // 2
if self._page_exists(cate_id, mid):
low = mid
else:
high = mid
return low
def extract_links_from_soup(self, soup, referer_url=''):
links = set()
for tag in soup.select('video source[src], video[src], source[type*="mpegurl"], source[type*="mp4"]'):
src = tag.get('src')
if src:
links.add(urljoin(referer_url, src))
for tag in soup.select('[data-url], [data-src], [data-link]'):
for attr in ['data-url', 'data-src', 'data-link', 'src']:
val = tag.get(attr)
if val and ('.m3u8' in val or '.mp4' in val):
links.add(urljoin(referer_url, val))
for s in soup.find_all('script'):
if s.string:
found = re.findall(r'(https?://[^\s"\'<>\]]+\.(?:m3u8|mp4)[^\s"\'<>\]]*)', s.string)
for u in found:
u = re.sub(r'["\'\s\\,;].*$', '', u)
links.add(u)
extra = re.findall(r'(https?://[^\s"\'<>\]]*yocoolnet\.in[^\s"\'<>\]]*\.(?:m3u8|mp4)[^\s"\'<>\]]*)', s.string)
for u in extra:
u = re.sub(r'["\'\s\\,;].*$', '', u)
links.add(u)
all_text = soup.get_text()
for u in re.findall(r'(https?://[^\s"\'<>\]]*yocoolnet\.in[^\s"\'<>\]]*\.(?:m3u8|mp4)[^\s"\'<>\]]*)', all_text):
u = u.strip("'\"")
links.add(u)
valid = set()
for link in links:
if link.startswith('http') and ('.m3u8' in link or '.mp4' in link):
valid.add(link)
return valid
def fetch_detail(self, detail_url, cate_name=''):
html = self.fetch(detail_url)
if not html:
return {}
soup = BeautifulSoup(html, 'html.parser')
info = {
'vod_actor': '', 'vod_director': '', 'vod_remarks': '',
'vod_pubdate': '', 'vod_area': '', 'vod_year': '',
'vod_tags': [], 'vod_content': '',
'vod_play_from': '', 'vod_play_url': '', 'type_name': '成人影片'
}
try:
actor_tag = soup.select_one('.actor, .starring, [itemprop="actor"]')
if actor_tag: info['vod_actor'] = actor_tag.get_text(strip=True)
director_tag = soup.select_one('.director, [itemprop="director"]')
if director_tag: info['vod_director'] = director_tag.get_text(strip=True)
remark_tag = soup.select_one('.remarks, .score, .quality')
if remark_tag: info['vod_remarks'] = remark_tag.get_text(strip=True)
date_tag = soup.select_one('.date, [itemprop="datePublished"]')
if date_tag:
date_text = date_tag.get_text(strip=True)
info['vod_pubdate'] = date_text
m = re.search(r'(\d{4})', date_text)
if m: info['vod_year'] = m.group(1)
area_tag = soup.select_one('.area, .region')
if area_tag: info['vod_area'] = area_tag.get_text(strip=True)
tag_elems = soup.select('.tags a, .keywords a, .video-tags a')
if tag_elems: info['vod_tags'] = [t.get_text(strip=True) for t in tag_elems]
content_tag = soup.select_one('.description, .content, [itemprop="description"]')
if content_tag: info['vod_content'] = str(content_tag)
play_from_tag = soup.select_one('.playfrom, [data-playfrom], .source-name')
if play_from_tag: info['vod_play_from'] = play_from_tag.get_text(strip=True)
all_links = self.extract_links_from_soup(soup, detail_url)
iframe_tags = soup.find_all('iframe')
for iframe in iframe_tags:
src = iframe.get('src')
if not src:
continue
full_url = urljoin(detail_url, src)
print(" 检测到嵌入播放器,正在解析...")
embed_html = self.fetch(full_url, extra_headers={'Referer': detail_url})
if embed_html:
embed_soup = BeautifulSoup(embed_html, 'html.parser')
embed_links = self.extract_links_from_soup(embed_soup, full_url)
all_links.update(embed_links)
time.sleep(0.5)
if all_links:
unique_links = list(dict.fromkeys(all_links))
play_from = info['vod_play_from'] if info['vod_play_from'] else '高清'
info['vod_play_url'] = f"{play_from}${unique_links[0]}"
for extra in unique_links[1:]:
info['vod_play_url'] += f"${extra}"
print(f" ✓ 成功提取 {len(unique_links)} 个视频链接")
else:
info['vod_play_url'] = ''
print(" ⚠ 未提取到视频链接")
except Exception as e:
print(f"解析详情页出错: {e}")
return info
def save_one(self, video):
with db_lock:
conn = sqlite3.connect(DB_PATH)
conn.execute('''INSERT OR IGNORE INTO videos
(vod_id, vod_name, vod_pic, vod_actor, vod_director, vod_remarks,
vod_pubdate, vod_area, vod_year, vod_tags, vod_content,
vod_play_from, vod_play_url, type_name)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
(video['vod_id'], video['vod_name'], video['vod_pic'],
video['vod_actor'], video['vod_director'], video['vod_remarks'],
video['vod_pubdate'], video['vod_area'], video['vod_year'],
json.dumps(video['vod_tags'], ensure_ascii=False),
video['vod_content'], video['vod_play_from'],
video['vod_play_url'], video['type_name']))
conn.commit()
conn.close()
with json_lock:
self.all_videos.append(video)
self.total_saved += 1
with open(JSON_PATH, 'w', encoding='utf-8') as f:
json.dump({"list": self.all_videos}, f, ensure_ascii=False, indent=2)
if video['vod_play_url']:
first_url = video['vod_play_url'].split('$')[0]
if '.m3u8' in first_url or '.mp4' in first_url:
with m3u_lock:
m3u_exists = os.path.exists(M3U_PATH)
with open(M3U_PATH, 'a', encoding='utf-8') as f:
if not m3u_exists:
f.write("#EXTM3U\n")
f.write(f"#EXTINF:-1, {video['vod_name']}\n")
f.write(f"{first_url}\n")
def process_page(self, cate_name, page_url):
html = self.fetch(page_url)
if not html:
return 0
soup = BeautifulSoup(html, 'html.parser')
items = soup.select('.videoPost')
if not items:
return 0
def process_one(node):
a = node.select_one('a.videoLink')
if not a: return
vod_id = a.get('href')
vod_name = a.get('title', '')
img = node.select_one('img')
vod_pic = img.get('src') if img else ""
views_span = node.select_one('.thumbViews')
remark_text = views_span.text.strip() if views_span else ""
print(f" [{cate_name}] 处理: {vod_name}")
detail_url = urljoin(page_url, vod_id)
detail = self.fetch_detail(detail_url, cate_name)
video = {
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_actor': detail.get('vod_actor', ''),
'vod_director': detail.get('vod_director', ''),
'vod_remarks': detail.get('vod_remarks', remark_text),
'vod_pubdate': detail.get('vod_pubdate', ''),
'vod_area': detail.get('vod_area', ''),
'vod_year': detail.get('vod_year', ''),
'vod_tags': detail.get('vod_tags', []),
'vod_content': detail.get('vod_content', ''),
'vod_play_from': detail.get('vod_play_from', ''),
'vod_play_url': detail.get('vod_play_url', ''),
'type_name': detail.get('type_name', '')
}
self.save_one(video)
time.sleep(random.uniform(*DETAIL_SLEEP))
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
futures = [executor.submit(process_one, node) for node in items]
for f in as_completed(futures):
f.result()
return len(items)
def scan_category(self, cate_id, cate_name):
print(f">>> 正在扫库分类: {cate_name}")
max_page = self.get_max_page(cate_id)
if max_page == 0:
print(f" 分类 {cate_name} 首页无法访问,跳过")
return
print(f" 检测到总页数: {max_page}")
page_urls = []
for pg in range(1, max_page + 1):
url = f"{self.site_url}/category/{cate_id}/" if pg == 1 else f"{self.site_url}/category/{cate_id}/page/{pg}/"
page_urls.append((pg, url))
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_page = {
executor.submit(self.process_page, cate_name, url): (pg, url)
for pg, url in page_urls
}
for future in as_completed(future_to_page):
pg, url = future_to_page[future]
try:
count = future.result()
print(f" 分类 {cate_name} 页面 {pg} 完成,获取 {count}" if count else f" 分类 {cate_name} 页面 {pg} 无数据")
except Exception as e:
print(f" 分类 {cate_name} 页面 {pg} 异常: {e}")
time.sleep(random.uniform(*PAGE_SLEEP))
def scan_all(self):
for cate_id, cate_name in self.categories.items():
self.scan_category(cate_id, cate_name)
print(f"\n===== 全站扫描完成,共 {self.total_saved} 条记录 =====")
if __name__ == '__main__':
spider = CoolinetSpider()
spider.scan_all()