Sync all projects
This commit is contained in:
@@ -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
|
||||
}];
|
||||
}
|
||||
@@ -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('&', '&')
|
||||
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}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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("&", "&")
|
||||
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
|
||||
}
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 ""
|
||||
@@ -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("&","&") 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":""}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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_id,area_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}'}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user