Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-12 13:22:33 +00:00
parent 0a48c4d588
commit 9f2a0af1e2
25 changed files with 8412 additions and 6641 deletions
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-APP APTV
#EXT-X-APTV-TYPE blank
#EXTINF:-1 group-title="美亞🎬",美亚电影
http://173.208.234.146/live/mymovie.m3u8
#EXTINF:-1 group-title="星影🍿爆谷🎬",NOW星影台
http://173.208.234.146/live/nowxy.m3u8
#EXTINF:-1 group-title="星影🍿爆谷🎬",NOW爆谷台
http://173.208.234.146/live/nowbg.m3u8
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+111
View File
@@ -0,0 +1,111 @@
/*
@header({
searchable: 2,
filterable: 1,
quickSearch: 0,
title: '可可影视[优]',
'类型': '影视',
lang: 'dr2'
})
*/
var rule = {
title: '可可影视[优]',
host: 'https://www.keke1.app',
//host: 'https://www.kkys01.com',
url: '/show/fyclass-----2-fypage.html',
//url: '/show/fyclass-fyfilter-fypage.html',
searchUrl: '/search?t=lw%2FzDeVGGBRTbdH2HVvs7Q%3D%3D&k=**&page=fypage',
searchable: 2,
quickSearch: 0,
filterable: 1,
headers: {
'User-Agent': 'MOBILE_UA',
},
class_parse: '#nav-swiper&&.nav-swiper-slide;a&&Text;a&&href;/(\\w+).html',
cate_exclude: 'Netflix|今日更新|专题列表|排行榜',
tab_exclude:'可可影视提供',
tab_order: ['超清', '蓝光', '极速蓝光'],
tab_remove:['4K(高峰不卡)'],
play_parse: true,
lazy: $js.toString(() => {
input = {
parse: 1,
url: input,
js: 'document.querySelector("#my-video video").click()',
}
}),
limit: 20,
推荐: '.section-box:eq(2)&&.module-box-inner&&.module-item;*;*;*;*',
double: false,
一级: '.module-box-inner&&.module-item;.v-item-title:eq(1)&&Text;img:last-of-type&&data-original;.v-item-bottom&&span&&Text;a&&href',
二级: {
title: '.detail-pic&&img&&alt;.detail-tags&&a&&Text',
img: '.detail-pic&&img&&data-original',
desc: '.detail-info-row-main:eq(-2)&&Text;.detail-tags&&a&&Text;.detail-tags&&a:eq(1)&&Text;.detail-info-row-main:eq(1)&&Text;.detail-info-row-main&&Text',
content: '.detail-desc&&Text',
tabs: '.source-item-label',
//tabs: 'body&&.source-item-label[id]',
lists: '.episode-list:eq(#id) a',
},
搜索: '.search-result-list&&a;.title:eq(0)&&Text;.search-result-item-pic&&img&&data-original;.search-result-item-header&&Text;a&&href;.desc&&Text',
图片替换:'https://www.keke1.app=>https://vres.zclmjc.com',
预处理: $js.toString(() => {
function extractHashFromResponse(ruleHost) {
try {
let response = request(ruleHost);
const regex = /a0_0x2a54\s*=\s*\['([^']+)'/;
let match = response.match(regex);
return match ? match[1] : '';
} catch (error) {
console.error('请求失败:', error);
return '';
}
}
function sha1ToUint8ArrayLatin1(input) {
let hash = CryptoJS.SHA1(input);
let latin1String = hash.toString(CryptoJS.enc.Latin1);
let uint8Array = new Uint8Array(latin1String.length);
for (let i = 0; i < latin1String.length; i++) {
uint8Array[i] = latin1String.charCodeAt(i);
}
return uint8Array;
}
function run(c, n1) {
let i = 0;
while (i < 1000000) {
let input = c + i;
let hash = sha1ToUint8ArrayLatin1(input);
if (hash[n1] === 0xb0 && hash[n1 + 1] === 0x0b) {
let myck = 'cdndefend_js_cookie=' + c + i;
console.log('找到 myck:', myck);
rule.headers['cookie'] = myck;
setItem('mycookie', myck);
setItem('myhash', c);
break;
}
i++;
}
console.log('未找到符合条件的 i');
}
let hash = extractHashFromResponse(rule.host);
if (hash != '' && hash != getItem('myhash')) {
setItem('mycookie', '');
setItem('myhash', '');
run(hash, parseInt('0x' + hash[0], 16));
}
if (getItem('mycookie')) {
rule.headers['cookie'] = getItem('mycookie');
}
let html = fetch(HOST, {headers: rule.headers});
const regex2 = /<input type="hidden" name="t" value="([^"]+)"/;
let match2 = html.match(regex2);
rule.searchUrl = rule.searchUrl.replace("lw%2FzDeVGGBRTbdH2HVvs7Q%3D%3D", match2 ? encodeURIComponent(match2[1]) : '');
}),
}
+183
View File
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
import json
import re
import sys
from base.spider import Spider
from pyquery import PyQuery as pq
class Spider(Spider):
def init(self, extend=""):
self.host = "https://qswyt4444.com"
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}/',
'Accept-Language': 'zh-CN,zh;q=0.9',
}
def getName(self):
return "QS五月天"
def isVideoFormat(self, url):
return url.lower().endswith('.m3u8')
def manualVideoCheck(self):
return True
def destroy(self):
pass
def homeContent(self, filter):
result = {}
classes = []
seen_ids = set()
try:
res = self.fetch(self.host, headers=self.headers)
d = pq(res.text)
items = d('a[href^="/movie/block/"]')
for item in items.items():
href = item.attr('href')
match = re.search(r'/movie/block/(\d+)', href)
if match:
tid = match.group(1)
name = item.text().strip() or item.find('span').text().strip()
if tid and name and tid not in seen_ids:
classes.append({'type_name': name, 'type_id': tid})
seen_ids.add(tid)
except Exception:
pass
if not classes:
cateManual = {
"最新": "newest",
"国产": "50",
"淫荡少妇": "21",
"人妻诱惑": "22",
"大奶萝莉": "23",
"丝袜制服": "24",
"强奸": "45",
"群P": "46"
}
for k, v in cateManual.items():
classes.append({'type_name': k, 'type_id': v})
result['class'] = classes
result['filters'] = {}
return result
def homeVideoContent(self):
try:
res = self.fetch(self.host, headers=self.headers)
return {'list': self.parse_list(res.text)}
except Exception:
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
result = {}
if tid.isdigit():
url = f'{self.host}/movie/block/{tid}?page={pg}'
else:
url = f'{self.host}/movie/{tid}?page={pg}'
try:
res = self.fetch(url, headers=self.headers)
result['list'] = self.parse_list(res.text)
result['page'] = int(pg)
result['pagecount'] = 999
result['limit'] = 20
result['total'] = 9999
except Exception:
result['list'] = []
return result
def detailContent(self, ids):
tid = ids[0]
url = tid if tid.startswith('http') else (f'{self.host}{tid}' if tid.startswith('/') else f'{self.host}/movie/detail/{tid}')
res = self.fetch(url, headers=self.headers)
content = res.text
title = ""
pic = ""
title_match = re.search(r'<meta property="og:title" content="(.*?)">', content)
if title_match:
title = title_match.group(1).split(' - ')[0]
pic_match = re.search(r'<meta property="og:image" content="(.*?)">', content)
if pic_match:
pic = pic_match.group(1)
vod_play_from_list = []
vod_play_url_list = []
pat = r'(/api/m3u8/p/[a-zA-Z0-9]+\.m3u8)'
matches = re.findall(pat, content)
unique_urls = []
seen = set()
for m in matches:
full_url = m if m.startswith('http') else f"{self.host}{m}"
if full_url not in seen:
seen.add(full_url)
unique_urls.append(full_url)
for index, u in enumerate(unique_urls):
vod_play_from_list.append(f"线路{index + 1}")
vod_play_url_list.append(u)
vod = {
'vod_id': tid,
'vod_name': title,
'vod_pic': pic,
'type_name': '',
'vod_year': '',
'vod_area': '',
'vod_remarks': '',
'vod_actor': '',
'vod_director': '',
'vod_content': '',
'vod_play_from': '$$$'.join(vod_play_from_list),
'vod_play_url': '$$$'.join(vod_play_url_list)
}
return {'list': [vod]}
def searchContent(self, key, quick, pg="1"):
url = f'{self.host}/search/{key}?page={pg}'
try:
res = self.fetch(url, headers=self.headers)
return {'list': self.parse_list(res.text)}
except:
return {'list': []}
def playerContent(self, flag, id, vipFlags):
return {
'parse': 0,
'url': id,
'header': {
'User-Agent': self.headers['User-Agent']
}
}
def localProxy(self, param):
pass
def parse_list(self, html):
videos = []
d = pq(html)
items = d('a[href^="/movie/detail/"]')
seen_ids = set()
for item in items.items():
href = item.attr('href')
if href in seen_ids:
continue
title = item.attr('title')
if not title:
title = item.find('span').text() or item.text()
img_tag = item.find('img')
pic = img_tag.attr('data-src') or img_tag.attr('src') or ""
remarks = item.find('.duration').text()
if href and title:
seen_ids.add(href)
videos.append({
'vod_id': href,
'vod_name': title.strip(),
'vod_pic': pic,
'vod_remarks': remarks
})
return videos
+2 -2
View File
@@ -8,7 +8,7 @@ from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def init(self, extend=""):
self.host = "https://maihaolian.com"
self.host = "https://www.ht10010.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
@@ -23,11 +23,11 @@ class Spider(BaseSpider):
{'type_id': "/label/qq", 'type_name': "腾讯VIP精选"},
{'type_id': "/label/bli", 'type_name': "B站VIP精选"},
{'type_id': "/label/youku", 'type_name': "优酷VIP精选"},
{"type_id": "5", "type_name": "红果短剧"},
{"type_id": "2", "type_name": "电视剧"},
{"type_id": "1", "type_name": "电影"},
{"type_id": "4", "type_name": "动漫"},
{"type_id": "3", "type_name": "综艺"},
{"type_id": "5", "type_name": "热门短剧"},
], "filters": self._build_filters()}
def _build_filters(self):
+304
View File
@@ -0,0 +1,304 @@
# -*- coding: utf-8 -*-
import sys
import urllib.parse
import re
from lxml import etree
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "禁片天堂"
def init(self, extend):
pass
def homeContent(self, filter):
cateManual = {
"中文": "278",
"巨乳": "15",
"熟女": "95",
"騎乘位": "74",
"口交": "34",
"癡女": "75",
"潮吹": "32",
"企劃片": "84",
"美尻": "156",
"打手槍": "98",
"戲劇、連續劇": "58",
"制服": "19",
"美腿": "157",
"舔鮑": "122",
"美乳": "166",
"搭訕": "12",
"妄想族": "184",
"第一人稱視點": "167",
"媽媽系": "193",
"人妻・主婦": "26",
"多種職業": "84",
"羞辱": "163",
"女教師": "131",
"淫語": "151",
"肉感": "136",
"愛美臀": "111",
"背後位": "178",
"調教": "395",
"處男": "23",
"護士": "283",
"修長": "147",
"露內褲": "169",
"絲襪": "115",
"愛巨乳": "200",
"眼鏡": "290",
"超乳": "211",
"顏面騎乘": "263",
"惡作劇": "145",
"義母": "144",
"淫亂・過激系": "63",
"愛美腿": "11",
"爆乳": "483",
"女上司": "137",
"正太": "415",
"穿衣幹砲": "179",
"緊身皮衣": "304",
"學園": "421",
"空姐": "132",
"粉絲感謝祭": "190",
"背面騎乗位": "646",
"秘書": "363",
"女主播": "106",
"反向搭訕": "305",
"健身教練": "233",
"部下・同僚": "150",
"舞蹈": "130",
"緊身衣激凸": "321",
"3D影片": "508",
"早洩": "403"
}
result = {'class': [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]}
return result
def homeVideoContent(self):
return {}
def categoryContent(self, tid, pg, filter, extend):
result = {}
url = f'https://jptt.tv/tag_list?tid={tid}&idx={pg}'
try:
rsp = self.fetch(url)
root = etree.HTML(rsp.text)
videos = root.xpath('//div[contains(@class,"oneVideo")]')
vodList = []
for video in videos:
try:
name_elements = video.xpath('.//h3/text()')
if not name_elements:
continue
name = name_elements[0].strip()
img_elements = video.xpath('.//img/@src')
if not img_elements:
continue
img = img_elements[0]
if not img.startswith('http'):
img = 'https://jptt.tv' + img
desc_elements = video.xpath('.//p[contains(@class,"p_duration")]/text()')
desc = desc_elements[0].strip() if desc_elements else ''
link_elements = video.xpath('.//a/@href')
if not link_elements:
continue
link = link_elements[0]
vodList.append({
"vod_name": name,
"vod_pic": img,
"vod_remarks": desc,
"vod_id": link
})
except Exception as e:
print(f"[categoryContent video parse error]: {e}")
continue
result['list'] = vodList
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
except Exception as e:
print(f"[categoryContent fetch error]: {e}")
result['list'] = []
result['page'] = pg
result['pagecount'] = 0
result['limit'] = 0
result['total'] = 0
return result
def detailContent(self, array):
tid = array[0]
url = tid if tid.startswith('http') else f'https://jptt.tv{tid}'
try:
rsp = self.fetch(url)
root = etree.HTML(rsp.text)
title_elements = root.xpath('//h1[@class="h1_title"]/text()')
title = title_elements[0].strip() if title_elements else "未知标题"
pic_elements = root.xpath('//video/@poster')
pic = pic_elements[0] if pic_elements else ""
if pic and not pic.startswith('http'):
pic = 'https://jptt.tv' + pic
desc_elements = root.xpath('//div[contains(@class,"info_original")]//p/text()')
desc = desc_elements[0].strip() if desc_elements else title
play_url = self.extractVideoUrl(rsp.text)
vod = {
"vod_id": tid,
"vod_name": title,
"vod_pic": pic,
"vod_content": desc,
"vod_play_from": "注意身体",
"vod_play_url": "多看少打卡$" + play_url
}
return {'list': [vod]}
except Exception as e:
print(f"[detailContent error]: {e}")
return {'list': []}
def extractVideoUrl(self, html):
try:
source_match = re.search(r'<source\s+src="([^"]+)"', html)
if source_match:
video_url = source_match.group(1)
if video_url.startswith('//'):
video_url = 'https:' + video_url
return video_url
hls_patterns = [
r'//cdn-[^"\']+\.m3u8[^"\']*',
r'https?://[^"\']+\.m3u8[^"\']*',
r'/hlsredirect/[^"\']+\.m3u8'
]
for pattern in hls_patterns:
matches = re.findall(pattern, html)
if matches:
for match in matches:
if match.startswith('//'):
return 'https:' + match
elif match.startswith('http'):
return match
else:
return 'https://jptt.tv' + match
js_patterns = [
r'src\s*:\s*["\']([^"\']+\.m3u8[^"\']*)["\']',
r'url\s*:\s*["\']([^"\']+\.m3u8[^"\']*)["\']',
r'file\s*:\s*["\']([^"\']+\.m3u8[^"\']*)["\']'
]
for pattern in js_patterns:
match = re.search(pattern, html)
if match:
video_url = match.group(1)
if video_url.startswith('//'):
return 'https:' + video_url
elif video_url.startswith('http'):
return video_url
else:
return 'https://jptt.tv' + video_url
all_m3u8 = re.findall(r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']', html)
if all_m3u8:
return all_m3u8[0]
except Exception as e:
print(f"[extractVideoUrl error]: {e}")
return "https://cdn-mso2.jptt1.cc/hlsredirect/EXBrcBO4G9RhgaUlZQhY1w/1760457600/hls/video/1/99-22-00164.3gp/index.m3u8"
def searchContent(self, key, quick, pg="1"):
result = {}
url = f'https://jptt.tv/search?kw={urllib.parse.quote(key)}'
try:
rsp = self.fetch(url)
root = etree.HTML(rsp.text)
videos = root.xpath('//div[contains(@class,"oneVideo")]')
vodList = []
for video in videos:
try:
name_elements = video.xpath('.//h3/text()')
if not name_elements:
continue
name = name_elements[0].strip()
img_elements = video.xpath('.//img/@src')
if not img_elements:
continue
img = img_elements[0]
if not img.startswith('http'):
img = 'https://jptt.tv' + img
desc_elements = video.xpath('.//p[contains(@class,"p_duration")]/text()')
desc = desc_elements[0].strip() if desc_elements else ''
link_elements = video.xpath('.//a/@href')
if not link_elements:
continue
link = link_elements[0]
vodList.append({
"vod_name": name,
"vod_pic": img,
"vod_remarks": desc,
"vod_id": link
})
except Exception as e:
print(f"[searchContent video parse error]: {e}")
continue
result['list'] = vodList
except Exception as e:
print(f"[searchContent fetch error]: {e}")
result['list'] = []
return result
def playerContent(self, flag, id, vipFlags):
result = {}
if flag == "注意身体":
try:
if id.startswith('http') and '.m3u8' in id:
result["parse"] = 0
result["playUrl"] = ''
result["url"] = id
else:
url = id if id.startswith('http') else f'https://jptt.tv{id}'
rsp = self.fetch(url)
play_url = self.extractVideoUrl(rsp.text)
result["parse"] = 0
result["playUrl"] = ''
result["url"] = play_url
except Exception as e:
print(f"[playerContent error]: {e}")
result["parse"] = 0
result["playUrl"] = ''
result["url"] = "https://cdn-mso2.jptt1.cc/hlsredirect/EXBrcBO4G9RhgaUlZQhY1w/1760457600/hls/video/1/99-22-00164.3gp/index.m3u8"
result["header"] = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.54 Safari/537.36",
"Referer": "https://jptt.tv/",
"Origin": "https://jptt.tv"
}
return result
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def localProxy(self, param):
pass
+218
View File
@@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
import re
import sys
from urllib.parse import urlparse
import base64
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
self.host=self.gethost()
self.headers.update({'referer': f'{self.host}/'})
pass
def getName(self):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
headers = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
'sec-ch-ua-platform': '"Android"',
'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
}
def homeContent(self, filter):
data=self.getpq()
result = {}
classes = []
filters = {"1": {"name": "类型","key": "tid","value": [{"n": "喜剧","v": 6},{"n": "爱情","v": 7},{"n": "恐怖","v": 8},{"n": "动作","v": 9},{"n": "科幻","v": 10},{"n": "战争","v": 11},{"n": "犯罪","v": 12},{"n": "动画","v": 13},{"n": "奇幻","v": 14},{"n": "剧情","v": 15},{"n": "冒险","v": 16},{"n": "悬疑","v": 17},{"n": "惊悚","v": 18},{"n": "其它","v": 19}]},"2": {"name": "类型","key": "tid","value": [{"n": "大陆剧","v": 20},{"n": "港剧","v": 21},{"n": "韩剧","v": 22},{"n": "美剧","v": 23},{"n": "日剧","v": 24},{"n": "英剧","v": 25},{"n": "台剧","v": 26},{"n": "其它","v": 27}]}}
for k in data('.top_bar.clearfix a').items():
j = k.attr('href')
if j and 'list' in j:
id = re.search(r'\d+', j).group(0)
classes.append({
'type_name': k.text(),
'type_id': id
})
result['class'] = classes
result['filters'] = filters
result['list'] = self.getlist(data('.grid_box ul li'))
return result
def homeVideoContent(self):
pass
def categoryContent(self, tid, pg, filter, extend):
data=self.getpq(f"/list/{extend.get('tid',tid)}-{pg}.html")
result = {}
result['list'] = self.getlist(data('.grid_box ul li'))
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
data=self.getpq(ids[0])
vod = {
'vod_remarks': data('.grid_box.v_info_box p').text(),
'vod_content': data('.p_txt.show_part').text().split('\n')[0],
}
n=list(data('.play_from ul li').items())
p=list(data('ul.play_list li').items())
ns,ps=[],[]
for i,j in enumerate(n):
ns.append(j.text())
ps.append('#'.join([f"{k.text()}${k.attr('href')}" for k in list(p[i]('a').items())[::-1]]))
vod['vod_play_from']='$$$'.join(ns)
vod['vod_play_url']='$$$'.join(ps)
return {'list':[vod]}
def searchContent(self, key, quick, pg="1"):
pass
def playerContent(self, flag, id, vipFlags):
data=self.getpq(id)
try:
surl=data('section[style*="padding-top"] iframe').eq(0).attr('src')
sd=pq(self.fetch(surl,headers=self.headers).text)('body script').html()
jdata=self.extract_values(sd)
jdata['key']=self.hhh(jdata['key'])
parsed_url = urlparse(surl)
durl = parsed_url.scheme + "://" + parsed_url.netloc
headers = {
'accept': 'application/json, text/javascript, */*; q=0.01',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'dnt': '1',
'origin': durl,
'pragma': 'no-cache',
'priority': 'u=1, i',
'referer': f'{surl}',
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
'sec-ch-ua-mobile': '?1',
'sec-ch-ua-platform': '"Android"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'sec-fetch-storage-access': 'active',
'user-agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'x-requested-with': 'XMLHttpRequest',
}
jjb=self.post(f"{durl}/api.php",headers=headers,data=jdata).json()
url,p=jjb['url'],0
except Exception as e:
self.log(f"失败: {e}")
url,p=f'{self.host}{id}',1
phd={
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'sec-ch-ua-platform': '"Android"',
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="130", "Google Chrome";v="130"',
'sec-fetch-dest': 'video',
'referer': f'{self.host}/',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
return {'parse': p, 'url': url, 'header': phd}
def localProxy(self, param):
pass
def liveContent(self, url):
pass
def gethost(self):
data=pq(self.fetch("http://shapp.us",headers=self.headers).text)
for i in data('.content-top ul li').items():
h=i('a').attr('href')
if h:
data = self.fetch(h, headers=self.headers, timeout=5)
if data.status_code == 200:
return h
def extract_values(self, text):
url_match = re.search(r'var url = "([^"]+)"', text)
url = url_match.group(1) if url_match else None
t_match = re.search(r'var t = "([^"]+)"', text)
t = t_match.group(1) if t_match else None
key_match = re.search(r'var key = hhh\("([^"]+)"\)', text)
key_param = key_match.group(1) if key_match else None
act_match = re.search(r'var act = "([^"]+)"', text)
act = act_match.group(1) if act_match else None
play_match = re.search(r'var play = "([^"]+)"', text)
play = play_match.group(1) if play_match else None
return {
"url": url,
"t": t,
"key": key_param,
"act": act,
"play": play
}
def getlist(self,data):
videos = []
for i in data.items():
videos.append({
'vod_id': i('a').attr('href'),
'vod_name': i('a').attr('title'),
'vod_pic': i('a img').attr('data-original'),
'vod_remarks': i('.v_note').text()
})
return videos
def getpq(self, path=''):
data=self.fetch(f"{self.host}{path}",headers=self.headers).text
try:
return pq(data)
except Exception as e:
print(f"{str(e)}")
return pq(data.encode('utf-8'))
def hhh(self, t):
ee = {
"0Oo0o0O0": "a", "1O0bO001": "b", "2OoCcO2": "c", "3O0dO0O3": "d",
"4OoEeO4": "e", "5O0fO0O5": "f", "6OoGgO6": "g", "7O0hO0O7": "h",
"8OoIiO8": "i", "9O0jO0O9": "j", "0OoKkO0": "k", "1O0lO0O1": "l",
"2OoMmO2": "m", "3O0nO0O3": "n", "4OoOoO4": "o", "5O0pO0O5": "p",
"6OoQqO6": "q", "7O0rO0O7": "r", "8OoSsO8": "s", "9O0tO0O9": "t",
"0OoUuO0": "u", "1O0vO0O1": "v", "2OoWwO2": "w", "3O0xO0O3": "x",
"4OoYyO4": "y", "5O0zO0O5": "z", "0OoAAO0": "A", "1O0BBO1": "B",
"2OoCCO2": "C", "3O0DDO3": "D", "4OoEEO4": "E", "5O0FFO5": "F",
"6OoGGO6": "G", "7O0HHO7": "H", "8OoIIO8": "I", "9O0JJO9": "J",
"0OoKKO0": "K", "1O0LLO1": "L", "2OoMMO2": "M", "3O0NNO3": "N",
"4OoOOO4": "O", "5O0PPO5": "P", "6OoQQO6": "Q", "7O0RRO7": "R",
"8OoSSO8": "S", "9O0TTO9": "T", "0OoUO0": "U", "1O0VVO1": "V",
"2OoWWO2": "W", "3O0XXO3": "X", "4OoYYO4": "Y", "5O0ZZO5": "Z"
}
n = ""
o = base64.b64decode(t).decode('utf-8', errors='replace')
i = 0
while i < len(o):
l = o[i]
found = False
for key, value in ee.items():
if o[i:i + len(key)] == key:
l = value
i += len(key) - 1
found = True
break
if not found:
pass
n += l
i += 1
return n
+14
View File
@@ -0,0 +1,14 @@
哈TV18+,#genre#
彩虹R,http://58.99.33.2:1935/liveedge2/TnIsFYAs_408_1/chunklist_w114642605.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
松視1台,http://58.99.33.2:1935/liveedge2/TnIsFYAs_405_1/chunklist_w2097694717.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&=&dr=123&dt=&cust_type=NE
松視2台,http://58.99.33.2:1935/liveedge2/TnIsFYAs_406_1/chunklist_w1350191092.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
松視3台,http://58.99.33.2:1935/liveedge2/TnIsFYAs_407_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
松視4台,http://58.99.33.2:1935/liveedge2/TnIsFYAs_404_1/chunklist_w1221017520.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
玩家,http://58.99.33.2:1935/liveedge2/TnIsFYAs_401_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
happy,http://58.99.33.2:1935/liveedge2/TnIsFYAs_402_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
HOT,http://58.99.33.2:1935/liveedge2/TnIsFYAs_403_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
乐活,http://58.99.33.2:1935/liveedge2/TnIsFYAs_410_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
潘多拉完美,http://58.99.33.2:1935/liveedge2/TnIsFYAs_411_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
潘多拉粉红,http://58.99.33.2:1935/liveedge2/TnIsFYAs_412_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
惊艳电影台,http://58.99.33.2:1935/liveedge2/TnIsFYAs_413_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
香蕉台,http://58.99.33.2:1935/liveedge2/TnIsFYAs_414_1/chunklist_w452892769.m3u8?checkCode=37050688asdfsdfsadf&aa=60999&as=abcdefg&mmmm=&dr=123&dt=&cust_type=NE
+231
View File
@@ -0,0 +1,231 @@
# coding=utf-8
#!/usr/bin/env python3
import re
import sys
import urllib.parse
import requests
from bs4 import BeautifulSoup
# 禁用SSL证书验证警告
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "永乐视频"
def init(self, extend=""):
self.host = "https://www.ylys.tv/"
self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': self.host}
self.session = requests.Session()
self.session.verify = False
self.session.headers.update(self.headers)
def fetch(self, url, timeout=30):
try:
response = self.session.get(url, timeout=timeout, verify=False)
response.encoding = response.encoding if response.encoding != 'ISO-8859-1' else 'UTF-8'
return response
except:
return None
def homeContent(self, filter):
result = {
"class": [{'type_id': str(i), 'type_name': t} for i, t in enumerate(['电影', '剧集', '综艺', '动漫'], 1)],
"filters": self._get_filters(),
"list": []
}
rsp = self.fetch(self.host)
if rsp and rsp.status_code == 200:
result['list'] = self._extract_videos(rsp.text, 20)
return result
def categoryContent(self, tid, pg, filter, extend):
result = {"list": [], "page": int(pg), "pagecount": 99, "limit": 20, "total": 1980}
url = f"{self.host}/vodtype/{tid}/page/{pg}/" if int(pg) > 1 else f"{self.host}/vodtype/{tid}/"
rsp = self.fetch(url)
if rsp and rsp.status_code == 200:
result['list'] = self._extract_videos(rsp.text)
return result
def searchContent(self, key, quick, pg=1):
result = {"list": []}
search_key = urllib.parse.quote(key)
url = f"{self.host}/vodsearch/{search_key}-------------/page/{pg}/" if int(pg) > 1 else f"{self.host}/vodsearch/{search_key}-------------/"
rsp = self.fetch(url)
if rsp and rsp.status_code == 200:
result['list'] = self._extract_search_results(rsp.text)
return result
def detailContent(self, ids):
result = {"list": []}
vid = ids[0]
rsp = self.fetch(f"{self.host}/voddetail/{vid}/")
if not rsp or rsp.status_code != 200:
return result
html = rsp.text
play_from, play_url = self._extract_play_info(html, vid)
if play_from:
result['list'] = [{
'vod_id': vid,
'vod_name': self._extract_title(html),
'vod_pic': self._extract_pic(html),
'vod_content': self._extract_desc(html),
'vod_remarks': self._extract_remarks(html),
'vod_play_from': "$$$".join(play_from),
'vod_play_url': "$$$".join(play_url)
}]
return result
def playerContent(self, flag, id, vipFlags):
result = {"parse": 1, "playUrl": "", "url": ""}
if "-" not in id:
return result
rsp = self.fetch(f"{self.host}/play/{id}/")
if not rsp or rsp.status_code != 200:
return result
real_url_match = re.search(r'var player_aaaa=.*?"url":"([^"]+\.m3u8)"', rsp.text, re.S | re.I)
if real_url_match:
real_url = real_url_match.group(1).replace(r'\u002F', '/').replace(r'\/', '/')
result["parse"] = 0
result["url"] = real_url
else:
result["url"] = f"{self.host}/play/{id}/"
return result
def _get_filters(self):
return {
"1": [{"key": "class", "name": "类型", "value": [
{"n": "全部", "v": ""}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
{"n": "爱情片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "恐怖片", "v": "11"}
]}],
"2": [{"key": "class", "name": "类型", "value": [
{"n": "全部", "v": ""}, {"n": "国产剧", "v": "13"}, {"n": "港台剧", "v": "14"},
{"n": "日剧", "v": "15"}, {"n": "韩剧", "v": "33"}, {"n": "欧美剧", "v": "16"}
]}],
"3": [{"key": "class", "name": "类型", "value": [
{"n": "全部", "v": ""}, {"n": "内地综艺", "v": "27"}, {"n": "港台综艺", "v": "28"},
{"n": "日本综艺", "v": "29"}, {"n": "韩国综艺", "v": "36"}
]}],
"4": [{"key": "class", "name": "类型", "value": [
{"n": "全部", "v": ""}, {"n": "国产动漫", "v": "31"}, {"n": "日本动漫", "v": "32"},
{"n": "欧美动漫", "v": "42"}, {"n": "其他动漫", "v": "43"}
]}]
}
def _extract_videos(self, html, limit=0):
videos = []
pattern = r'<a href="/voddetail/(\d+)/".*?title="([^"]+)".*?<div class="module-item-note">([^<]+)</div>.*?data-original="([^"]+)"'
for vid, title, remark, pic in re.findall(pattern, html, re.S | re.I):
videos.append({
'vod_id': vid.strip(),
'vod_name': title.strip(),
'vod_pic': (self.host + pic if pic.startswith('/') else pic).strip(),
'vod_remarks': remark.strip()
})
return videos[:limit] if limit and len(videos) > limit else videos
def _extract_search_results(self, html):
videos = []
soup = BeautifulSoup(html, 'html.parser')
for item in soup.select('.module-card-item'):
link = item.select_one('a[href^="/voddetail/"]')
if not link:
continue
href = link.get('href', '')
vid_match = re.search(r'/voddetail/(\d+)/', href)
if not vid_match:
continue
vid = vid_match.group(1)
title_elem = item.select_one('.module-card-item-title strong')
img_elem = item.select_one('img')
pic = (img_elem.get('data-original') or img_elem.get('src')) if img_elem else ""
note_elem = item.select_one('.module-item-note')
videos.append({
'vod_id': vid,
'vod_name': title_elem.get_text(strip=True) if title_elem else "",
'vod_pic': self.host + pic if pic.startswith('/') else pic,
'vod_remarks': note_elem.get_text(strip=True) if note_elem else ""
})
return videos
def _extract_play_info(self, html, vid):
play_from, play_url = [], []
line_pattern = r'<(?:div|a)[^>]*class="[^"]*module-tab-item[^"]*"[^>]*>(?:.*?<span>([^<]+)</span>.*?<small>(\d+)</small>|.*?<span>([^<]+)</span>.*?<small class="no">(\d+)</small>)</(?:div|a)>'
for match in re.findall(line_pattern, html, re.S | re.I):
line_name = match[0] or match[2]
if line_name in play_from:
continue
play_from.append(line_name)
line_id = self._get_line_id(html, vid, line_name)
ep_matches = re.findall(rf'<a class="module-play-list-link" href="/play/{vid}-{line_id}-(\d+)/"[^>]*>.*?<span>([^<]+)</span></a>', html, re.S | re.I)
eps = [f"{ep_name.strip()}${vid}-{line_id}-{ep_num.strip()}" for ep_num, ep_name in ep_matches]
play_url.append("#".join(eps))
return play_from, play_url
def _get_line_id(self, html, vid, line_name):
line_id_match = re.search(rf'<a[^>]*href="/play/{vid}-(\d+)-1/"[^>]*>.*?<span>{re.escape(line_name)}</span>', html, re.S | re.I)
if line_id_match:
return line_id_match.group(1)
line_id_map = {"全球3线": "3", "大陆0线": "1", "大陆3线": "4", "大陆5线": "2", "大陆6线": "3"}
return line_id_map.get(line_name, "1")
def _extract_title(self, html):
title_match = re.search(r'<meta property="og:title" content="([^"]+)-[^-]+$"', html, re.S | re.I)
return title_match.group(1).strip() if title_match else ""
def _extract_pic(self, html):
pic_match = re.search(r'<meta property="og:image" content="([^"]+)"', html, re.S | re.I)
pic = pic_match.group(1).strip() if pic_match else ""
return self.host + pic if pic and pic.startswith('/') else pic
def _extract_desc(self, html):
desc_match = re.search(r'<meta property="og:description" content="([^"]+)"', html, re.S | re.I)
return desc_match.group(1).strip() if desc_match else "暂无简介"
def _extract_remarks(self, html):
year_match = re.search(r'<a title="(\d+)" href="/vodshow/\d+-----------\1/">', html, re.S | re.I)
year = year_match.group(1) if year_match else "未知年份"
area_match = re.search(r'<a title="([^"]+)" href="/vodshow/\d+-%E5%A2%A8%E8%A5%BF%E5%93%A5----------/">', html, re.S | re.I)
area = area_match.group(1) if area_match else "未知产地"
type_match = re.search(r'vod_class":"([^"]+)"', html, re.S | re.I)
type_str = type_match.group(1).replace(",", "/") if type_match else "未知类型"
return f"{year} | {area} | {type_str}"
# 本地测试
if __name__ == "__main__":
spider = Spider()
spider.init()
# 测试详情页解析
detail_result = spider.detailContent(["86027"])
if detail_result['list']:
detail = detail_result['list'][0]
print(f"视频名称: {detail.get('vod_name', '未知')}")
# 测试搜索功能
search_result = spider.searchContent("仙逆", False, 1)
print(f"搜索结果数量: {len(search_result['list'])}")
# 测试播放功能
play_result = spider.playerContent("", "86027-5-1", {})
print(f"播放URL: {play_result.get('url', '')}")
+739
View File
@@ -0,0 +1,739 @@
# -*- coding: utf-8 -*-
import re
import sys
import json
import time
import random
from urllib.parse import quote, unquote, urljoin
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
"""初始化 适配配置"""
# 修复:删除末尾空格
self.host = "https://www.mtyy1.com"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 11; MI 11 Build/RKQ1.201022.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/92.0.4515.159 Mobile Safari/537.36 TVBox/1.0',
'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, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Referer': self.host
}
self.source_map = {"NBY": "高清NB源", "1080zyk": "超清YZ源", "ffm3u8": "极速FF源", "lzm3u8": "稳定LZ源", "yzzy": "YZ源"}
self.ua_list = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1'
]
def getName(self):
return "麦田影院"
def isVideoFormat(self, url):
"""判断是否为直接播放格式"""
video_exts = ['.mp4', '.m3u8', '.flv', '.avi', '.mov', '.rmvb', '.m3u8']
return any(ext in url.lower() for ext in video_exts)
def manualVideoCheck(self):
return False
def destroy(self):
pass
def fix_encoding(self, text):
"""加强版编码修复,适配中文显示"""
if not text:
return ""
try:
# 处理可能的乱码情况
if isinstance(text, bytes):
try:
text = text.decode('utf-8')
except UnicodeDecodeError:
text = text.decode('gbk', errors='ignore')
# 处理Unicode转义序列
if '\\u' in text:
try:
text = text.encode('utf-8').decode('unicode_escape')
except:
pass
garbled_replace = {
'\u00e4\u00b8\u00ad': '', '\u00e6\u0096\u0087': '',
'\u00e5\u00bd\u00b1': '', '\u00e8\u00a7\u0086': '',
'\u00e9\u00a2\u0091': '', '\u00c3\u00a4': 'ä', '\u00c3\u00b6': 'ö'
}
for garbled, correct in garbled_replace.items():
text = text.replace(garbled, correct)
# 清理不可见字符
text = re.sub(r'[\x00-\x1f\x7f]', '', text).strip()
return text
except Exception as e:
self.log(f"编码修复异常: {str(e)}")
return str(text) if text else ""
def fetch_with_encoding(self, url, **kwargs):
"""带编码处理的请求方法,确保正确获取页面内容"""
try:
# 随机延迟,避免被识别为爬虫
time.sleep(random.uniform(0.5, 1.5))
# 随机选择User-Agent
headers = kwargs.get('headers', self.headers.copy())
headers['User-Agent'] = random.choice(self.ua_list)
kwargs['headers'] = headers
response = self.fetch(
url,
timeout=15,
allow_redirects=True,
**kwargs
)
response.encoding = 'utf-8'
if response.status_code != 200 or len(response.text) < 1000:
self.log(f"请求失败,状态码: {response.status_code},内容长度: {len(response.text)}")
raise Exception(f"页面内容无效或获取失败")
return response
except Exception as e:
self.log(f"请求 {url} 出错: {str(e)}")
raise
def getpq(self, text):
"""安全的pyquery解析,处理可能的解析错误"""
try:
return pq(text)
except Exception as e:
self.log(f"PyQuery 解析失败: {str(e)}")
clean_text = re.sub(r'[^\x20-\x7e\u4e00-\u9fff]', '', text)
return pq(clean_text) if clean_text else pq('')
def homeContent(self, filter):
"""获取首页内容和分类"""
try:
response = self.fetch_with_encoding(self.host)
doc = self.getpq(response.text)
result = {}
classes = []
nav_items = doc('div.head-nav a[href*="/vodtype/"], .this-wap a[href*="/vodtype/"]').items()
seen_cate = set()
for item in nav_items:
cate_text = self.fix_encoding(item.text().strip())
cate_href = item.attr('href')
if not cate_text or not cate_href or '/vodtype/' not in cate_href:
continue
cate_id = re.search(r'/vodtype/(\d+)\.html', cate_href)
if not cate_id or cate_id.group(1) in seen_cate:
continue
cate_id = cate_id.group(1)
seen_cate.add(cate_id)
classes.append({
'type_name': cate_text,
'type_id': cate_id
})
videos = []
seen_ids = set()
# 修复:精准选择器,确保抓取首页影片
video_boxes = doc('.public-list-box.public-pic-b, .wap-diy-vod-a .public-list-box').items()
for box in video_boxes:
link = box.find('a.public-list-exp')
if not link:
continue
vod_href = link.attr('href')
vod_id = re.search(r'/voddetail/(\d+)\.html', vod_href)
if not vod_id or vod_id.group(1) in seen_ids:
continue
vod_id = vod_id.group(1)
seen_ids.add(vod_id)
img = link.find('img')
# --- 修复标题:优先使用 a 标签的 title 属性或文本,避免使用 img.alt ---
vod_title = self.fix_encoding(
link.attr('title') or
link.text().strip() or
img.attr('alt') or ""
)
if not vod_title:
continue
# --- 修复封面图:优先使用 data-src ---
vod_pic = img.attr('data-src') or img.attr('src') or ""
vod_pic = urljoin(self.host, vod_pic) if vod_pic else ""
vod_remarks = self.fix_encoding(
box.find('.public-prt, .episode, .public-list-prb').text().strip() or
(re.search(r'\d+[集期]|更新至|完结|HD|超清', box.text()).group() if re.search(r'\d+[集期]|更新至|完结|HD|超清', box.text()) else "")
)
videos.append({
'vod_id': vod_id,
'vod_name': vod_title,
'vod_pic': vod_pic,
'vod_year': '',
'vod_remarks': vod_remarks
})
result['class'] = classes
result['list'] = videos
return result
except Exception as e:
self.log(f"获取首页内容时出错: {e}")
return {'class': [], 'list': []}
def homeVideoContent(self):
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
"""获取分类内容"""
try:
url = f"{self.host}/vodtype/{tid}.html"
if int(pg) > 1:
url = f"{self.host}/vodtype/{tid}-{pg}.html"
response = self.fetch_with_encoding(url)
doc = self.getpq(response.text)
videos = []
seen_ids = set()
# 修复:精准选择器
video_boxes = doc('.public-list-box.public-pic-b').items()
for box in video_boxes:
link = box.find('a.public-list-exp')
if not link:
continue
vod_href = link.attr('href')
vod_id = re.search(r'/voddetail/(\d+)\.html', vod_href)
if not vod_id or vod_id.group(1) in seen_ids:
continue
vod_id = vod_id.group(1)
seen_ids.add(vod_id)
img = link.find('img')
# --- 修复标题:优先使用 a 标签的 title 属性或文本 ---
vod_title = self.fix_encoding(
link.attr('title') or
link.text().strip() or
img.attr('alt') or ""
)
# --- 修复封面图:优先使用 data-src ---
vod_pic = urljoin(self.host, img.attr('data-src') or img.attr('src') or "")
vod_remarks = self.fix_encoding(box.find('.public-prt, .public-list-prb').text().strip() or "")
if vod_title:
videos.append({
'vod_id': vod_id,
'vod_name': vod_title,
'vod_pic': vod_pic,
'vod_year': '',
'vod_remarks': vod_remarks
})
result = {
'list': videos,
'page': pg,
'pagecount': 9999,
'limit': 80,
'total': 999999
}
return result
except Exception as e:
self.log(f"获取分类内容时出错: {e}")
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 80, 'total': 0}
def detailContent(self, ids):
"""获取视频详情,修复播放源显示问题"""
result = {"list": []}
if not ids or len(ids) == 0:
return result
vod_id = ids[0]
try:
# 先访问详情页获取基本信息
detail_url = f"{self.host}/voddetail/{vod_id}.html"
response = self.fetch_with_encoding(detail_url)
doc = self.getpq(response.text)
vod_info = {
"vod_id": vod_id,
"vod_name": self.fix_encoding(doc('h1.player-title-link').text().strip()),
# --- 修复封面图:优先使用 data-src ---
"vod_pic": urljoin(self.host, doc('.role-card img').attr('data-src') or doc('.role-card img').attr('src') or ""),
"vod_year": self.fix_encoding(doc('.player-details a[href*="2025"]').text().strip() or doc('.player-details li:contains("年份")').text().split('')[-1].strip()),
"vod_remarks": self.fix_encoding(doc('.co3').text().strip() or "7.7分"),
"vod_actor": self.fix_encoding(doc('.vod_data').attr('vod_actor') if doc('.vod_data') else ""),
"vod_director": self.fix_encoding(doc('.vod_data').attr('vod_director') if doc('.vod_data') else ""),
"vod_content": self.fix_encoding(doc('.card-text').text().strip()),
"vod_play_from": "",
"vod_play_url": ""
}
# 尝试获取播放页URL
play_page_url = None
play_link = doc('.anthology-list-play a:first').attr('href')
if play_link:
play_page_url = urljoin(self.host, play_link)
# 如果无法获取播放页URL,使用默认URL
if not play_page_url:
play_page_url = f"{self.host}/vodplay/{vod_id}-1-1.html"
# 访问播放页获取播放源信息
play_response = self.fetch_with_encoding(play_page_url)
play_doc = self.getpq(play_response.text)
# 存储所有线路信息
all_sources_data = {} # {source_name: [episode1$url1, episode2$url2], ...}
# --- 解析所有播放源 ---
# 查找播放源选项卡
tab_items = play_doc('a.vod-playerUrl[data-form]').items()
for tab in tab_items:
data_form = tab.attr('data-form')
source_name = self.fix_encoding(tab.text().replace('', '').strip())
# 标准化线路名称
display_source_name = self.source_map.get(data_form, source_name)
# 查找对应的播放列表
episodes = []
# 获取当前选项卡的索引
tab_index = list(play_doc('a.vod-playerUrl[data-form]')).index(tab[0])
# 找到对应的剧集列表
episode_boxes = play_doc('.anthology-list-box').eq(tab_index)
episode_links = episode_boxes.find('a').items()
for ep in episode_links:
ep_title = self.fix_encoding(ep.text().strip())
ep_href = ep.attr('href')
if ep_title and ep_href:
# 为每个播放源生成不同的URL(根据data-form参数)
full_href = urljoin(self.host, ep_href)
# 添加播放源标识到URL
if '?' in full_href:
full_href += f'&source={data_form}'
else:
full_href += f'?source={data_form}'
episodes.append(f"{ep_title}${full_href}")
if episodes:
all_sources_data[display_source_name] = '#'.join(episodes)
# --- 排序和优先使用YZ源 ---
final_play_from = []
final_play_url = []
yz_source_name = "超清YZ源"
if yz_source_name in all_sources_data:
final_play_from.append(yz_source_name)
final_play_url.append(all_sources_data.pop(yz_source_name))
for src_name, src_urls in all_sources_data.items():
final_play_from.append(src_name)
final_play_url.append(src_urls)
if not final_play_from:
default_ep = play_doc('.anthology-list-play a:first').attr('href')
if default_ep:
final_play_from.append("默认源")
final_play_url.append(f"正片${urljoin(self.host, default_ep)}")
vod_info["vod_play_from"] = "$$$".join(final_play_from)
vod_info["vod_play_url"] = "$$$".join(final_play_url)
result["list"].append(vod_info)
return result
except Exception as e:
self.log(f"获取视频详情时出错: {e}")
return {'list': []}
def searchContent(self, key, quick, pg="1"):
"""搜索功能 - 带验证码处理"""
result = {"list": [], "page": int(pg)}
try:
# 编码搜索关键词
encoded_key = quote(key)
search_url = f"{self.host}/vodsearch/-------------.html?wd={encoded_key}"
if int(pg) > 1:
search_url = f"{self.host}/vodsearch/-------------.html?wd={encoded_key}&page={pg}"
# 添加随机延迟,避免被识别为爬虫
time.sleep(random.uniform(1.0, 2.5))
# 使用更真实的浏览器头
headers = self.headers.copy()
headers.update({
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Cache-Control': 'max-age=0',
'TE': 'Trailers'
})
response = self.fetch(search_url, headers=headers, timeout=15)
# 检查是否被重定向到验证码页面
if response.url and "captcha" in response.url.lower():
self.log("检测到验证码页面,尝试绕过...")
# 尝试使用不同的方法绕过验证码
return self.handle_captcha_search(key, pg)
# 检查响应内容是否包含验证码提示
html_content = response.text
if "验证码" in html_content or "captcha" in html_content.lower():
self.log("页面包含验证码提示,尝试绕过...")
return self.handle_captcha_search(key, pg)
# 正常解析搜索结果
doc = self.getpq(html_content)
# 多种选择器尝试获取结果
selectors = [
'.public-list-box',
'.public-pic-b',
'.public-list-div',
'.vod-item',
'.search-result-item',
'[class*="vod"]'
]
videos = []
seen_ids = set()
for selector in selectors:
items = doc(selector)
if items.length > 0:
for item in items.items():
try:
link = item.find('a[href*="/voddetail/"]').first()
if not link:
continue
vod_href = link.attr('href')
if not vod_href:
continue
vod_id_match = re.search(r'/voddetail/(\d+)\.html', vod_href)
if not vod_id_match:
continue
vod_id = vod_id_match.group(1)
if vod_id in seen_ids:
continue
seen_ids.add(vod_id)
# 提取标题
title_selectors = [
'.time-title', '.vod-name', '.title',
'img[alt]', '[title]', 'h3', 'h4'
]
vod_title = ""
for title_sel in title_selectors:
title_elem = item.find(title_sel).first()
if title_elem:
title_text = title_elem.attr('title') or title_elem.attr('alt') or title_elem.text()
if title_text and len(title_text.strip()) > 0:
vod_title = self.fix_encoding(title_text.strip())
break
if not vod_title:
continue
# 提取封面
img = item.find('img').first()
vod_pic = img.attr('data-src') or img.attr('src') or ""
if vod_pic and not vod_pic.startswith(('http://', 'https://')):
vod_pic = urljoin(self.host, vod_pic)
# 提取备注
remark_selectors = ['.public-prt', '.public-list-prb', '.remark', '.episode']
vod_remarks = ""
for remark_sel in remark_selectors:
remark_elem = item.find(remark_sel).first()
if remark_elem:
remark_text = remark_elem.text()
if remark_text and len(remark_text.strip()) > 0:
vod_remarks = self.fix_encoding(remark_text.strip())
break
videos.append({
"vod_id": vod_id,
"vod_name": vod_title,
"vod_pic": vod_pic,
"vod_year": "",
"vod_remarks": vod_remarks
})
except:
continue
if videos:
break
result["list"] = videos
self.log(f"搜索 '{key}' 找到 {len(videos)} 个结果")
except Exception as e:
self.log(f"搜索失败: {str(e)}")
# 尝试备用搜索方法
try:
return self.backupSearch(key, pg)
except Exception as backup_error:
self.log(f"备用搜索也失败: {str(backup_error)}")
return result
def handle_captcha_search(self, key, pg):
"""处理验证码的搜索方法"""
result = {"list": [], "page": int(pg)}
try:
# 方法1: 尝试使用不同的User-Agent
encoded_key = quote(key)
search_url = f"{self.host}/vodsearch/-------------.html?wd={encoded_key}"
if int(pg) > 1:
search_url = f"{self.host}/vodsearch/-------------.html?wd={encoded_key}&page={pg}"
# 尝试不同的User-Agent
for ua in self.ua_list:
try:
headers = {
'User-Agent': ua,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Cache-Control': 'max-age=0'
}
time.sleep(random.uniform(2.0, 4.0))
response = self.fetch(search_url, headers=headers, timeout=15)
# 检查是否仍然有验证码
html_content = response.text
if "验证码" not in html_content and "captcha" not in html_content.lower():
# 解析搜索结果
doc = self.getpq(html_content)
videos = []
seen_ids = set()
# 简化选择器
items = doc('.public-list-box, .public-pic-b, .public-list-div')
for item in items.items():
link = item.find('a[href*="/voddetail/"]').first()
if not link:
continue
vod_href = link.attr('href')
vod_id_match = re.search(r'/voddetail/(\d+)\.html', vod_href)
if not vod_id_match:
continue
vod_id = vod_id_match.group(1)
if vod_id in seen_ids:
continue
seen_ids.add(vod_id)
# 提取标题
vod_title = self.fix_encoding(
link.attr('title') or
item.find('.time-title').text() or
item.find('img').attr('alt') or
link.text() or ""
).strip()
if not vod_title:
continue
# 提取封面
img = item.find('img').first()
vod_pic = img.attr('data-src') or img.attr('src') or ""
if vod_pic and not vod_pic.startswith(('http://', 'https://')):
vod_pic = urljoin(self.host, vod_pic)
videos.append({
"vod_id": vod_id,
"vod_name": vod_title,
"vod_pic": vod_pic,
"vod_year": "",
"vod_remarks": ""
})
result["list"] = videos
self.log(f"使用备用UA成功搜索 '{key}',找到 {len(videos)} 个结果")
return result
except Exception as e:
self.log(f"使用UA {ua[:30]}... 搜索失败: {str(e)}")
continue
# 如果所有方法都失败,返回空结果
self.log("所有绕过验证码的方法都失败了")
except Exception as e:
self.log(f"验证码处理失败: {str(e)}")
return result
def backupSearch(self, key, pg):
"""备用搜索方法 - 使用更简单的选择器"""
result = {"list": [], "page": int(pg)}
try:
encoded_key = quote(key)
search_url = f"{self.host}/vodsearch/-------------.html?wd={encoded_key}"
response = self.fetch_with_encoding(search_url)
html_content = response.text
# 使用正则表达式提取搜索结果
pattern = r'<a\s+href="(/voddetail/\d+\.html)"[^>]*>(.*?)</a>'
matches = re.findall(pattern, html_content, re.DOTALL)
seen_ids = set()
for href, content in matches:
vod_id_match = re.search(r'/voddetail/(\d+)\.html', href)
if not vod_id_match:
continue
vod_id = vod_id_match.group(1)
if vod_id in seen_ids:
continue
seen_ids.add(vod_id)
# 提取标题
title_match = re.search(r'<img[^>]*alt="([^"]*)"', content)
if not title_match:
title_match = re.search(r'title="([^"]*)"', content)
if not title_match:
# 尝试从链接内容提取文本
text_match = re.search(r'>([^<]+)<', content)
if text_match:
vod_title = text_match.group(1).strip()
else:
continue
else:
vod_title = title_match.group(1)
vod_title = self.fix_encoding(vod_title)
if not vod_title:
continue
# 提取封面图
img_match = re.search(r'<img[^>]*src="([^"]*)"', content)
vod_pic = img_match.group(1) if img_match else ""
if vod_pic and not vod_pic.startswith(('http://', 'https://')):
vod_pic = urljoin(self.host, vod_pic)
result["list"].append({
"vod_id": vod_id,
"vod_name": vod_title,
"vod_pic": vod_pic,
"vod_year": "",
"vod_remarks": ""
})
except Exception as e:
self.log(f"备用搜索失败: {str(e)}")
return result
def playerContent(self, flag, id, vipFlags):
"""播放地址解析 - 终极修复版"""
try:
play_page_url = urljoin(self.host, id)
if not play_page_url.startswith(('http://', 'https://')):
self.log(f"无效播放地址: {id}")
return {"parse": 1, "url": "", "header": self.headers}
response = self.fetch_with_encoding(play_page_url)
html_content = response.text
# --- 核心:提取播放器配置 ---
player_match = re.search(r'var\s+player_aaaa\s*=\s*({[^}]+?url\s*:\s*["\'][^"\']+["\'][^}]*})', html_content, re.DOTALL)
if not player_match:
self.log("未找到播放器配置脚本 player_aaaa")
return {"parse": 1, "url": play_page_url, "header": self.headers}
player_json_str = player_match.group(1)
# 修复可能的JSON格式问题(尾部逗号)
player_json_str = re.sub(r',\s*([}\]])', r'\1', player_json_str)
try:
player_data = json.loads(player_json_str)
except json.JSONDecodeError as e:
self.log(f"解析播放器 JSON 失败: {str(e)}")
return {"parse": 1, "url": play_page_url, "header": self.headers}
# --- 提取主播放地址和备用地址 ---
main_url = player_data.get("url", "").strip()
backup_url = player_data.get("url_next", "").strip()
# URL解码
if '%' in main_url:
main_url = unquote(main_url)
if '%' in backup_url:
backup_url = unquote(backup_url)
self.log(f"[修复版] 提取的主播放地址: {main_url}")
self.log(f"[修复版] 提取的备用播放地址: {backup_url}")
# --- 极简播放逻辑:谁有效播谁 ---
# 优先使用主地址
if main_url and self.isVideoFormat(main_url):
return {
"parse": 0, # 0 = 直接播放
"url": main_url,
"header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Referer": play_page_url, # ⚠️ 关键!必须添加 Referer
"Origin": self.host.rstrip('/')
}
}
# 主地址无效,尝试备用地址
elif backup_url and self.isVideoFormat(backup_url):
return {
"parse": 0,
"url": backup_url,
"header": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36",
"Referer": play_page_url, # ⚠️ 关键!必须添加 Referer
"Origin": self.host.rstrip('/')
}
}
# --- 万不得已的兜底方案 ---
self.log("未能提取有效视频地址,返回播放页")
return {
"parse": 1,
"url": play_page_url,
"header": self.headers
}
except Exception as e:
self.log(f"播放地址解析异常: {str(e)}")
return {
"parse": 1,
"url": urljoin(self.host, id),
"header": self.headers
}
def localProxy(self, param):
pass