Sync all projects

This commit is contained in:
github-actions[bot]
2026-06-29 03:41:14 +00:00
parent 529cd7c625
commit 8b025e0766
11717 changed files with 6370622 additions and 0 deletions
+389
View File
@@ -0,0 +1,389 @@
import json
import re
import sys
import hashlib
from base64 import b64decode, b64encode
from urllib.parse import urlparse
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider as BaseSpider
img_cache = {}
class Spider(BaseSpider):
def init(self, extend=""):
try:
self.proxies = json.loads(extend)
except:
self.proxies = {}
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'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',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache',
}
self.host = self.get_working_host()
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
print(f"使用站点: {self.host}")
def getName(self):
return "🌈 91吃瓜中心|终极完美版"
def isVideoFormat(self, url):
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
def manualVideoCheck(self):
return False
def destroy(self):
global img_cache
img_cache.clear()
def get_working_host(self):
dynamic_urls = [
'https://but.vncchqw.cc/'
]
for url in dynamic_urls:
try:
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
if response.status_code == 200:
return url
except Exception:
continue
return dynamic_urls[0]
def homeContent(self, filter):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'class': [], 'list': []}
data = self.getpq(response.text)
classes = []
category_selectors = ['.category-list ul li', '.nav-menu li', '.menu li', 'nav ul li']
for selector in category_selectors:
for k in data(selector).items():
link = k('a')
href = (link.attr('href') or '').strip()
name = (link.text() or '').strip()
if not href or href == '#' or not name: continue
classes.append({'type_name': name, 'type_id': href})
if classes: break
if not classes:
classes = [{'type_name': '最新', 'type_id': '/latest/'}, {'type_name': '热门', 'type_id': '/hot/'}]
return {'class': classes, 'list': self.getlist(data('#index article, article'))}
except Exception as e:
return {'class': [], 'list': []}
def homeVideoContent(self):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'list': []}
data = self.getpq(response.text)
return {'list': self.getlist(data('#index article, article'))}
except Exception as e:
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
try:
if '@folder' in tid:
v = self.getfod(tid.replace('@folder', ''))
return {'list': v, 'page': 1, 'pagecount': 1, 'limit': 90, 'total': len(v)}
pg = int(pg) if pg else 1
if tid.startswith('http'):
base_url = tid.rstrip('/')
else:
path = tid if tid.startswith('/') else f"/{tid}"
base_url = f"{self.host}{path}".rstrip('/')
if pg == 1:
url = f"{base_url}/"
else:
url = f"{base_url}/{pg}/"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
data = self.getpq(response.text)
videos = self.getlist(data('#archive article, #index article, article'), tid)
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
except Exception as e:
return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
def detailContent(self, ids):
try:
url = ids[0] if ids[0].startswith('http') else f"{self.host}{ids[0]}"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
data = self.getpq(response.text)
plist = []
used_names = set()
if data('.dplayer'):
for c, k in enumerate(data('.dplayer').items(), start=1):
try:
config_attr = k.attr('data-config')
if config_attr:
config = json.loads(config_attr)
video_url = config.get('video', {}).get('url', '')
if video_url:
ep_name = ''
parent = k.parents().eq(0)
for _ in range(4):
if not parent: break
heading = parent.find('h2, h3, h4').eq(0).text().strip()
if heading:
ep_name = heading
break
parent = parent.parents().eq(0)
base_name = ep_name if ep_name else f"视频{c}"
name = base_name
count = 2
while name in used_names:
name = f"{base_name} {count}"
count += 1
used_names.add(name)
plist.append(f"{name}${video_url}")
except: continue
if not plist:
content_area = data('.post-content, article')
for i, link in enumerate(content_area('a').items(), start=1):
link_text = link.text().strip()
link_href = link.attr('href')
if link_href and any(kw in link_text for kw in ['点击观看', '观看', '播放', '视频', '第一弹', '第二弹', '第三弹', '第四弹', '第五弹', '第六弹', '第七弹', '第八弹', '第九弹', '第十弹']):
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
if not ep_name: ep_name = f"视频{i}"
if not link_href.startswith('http'):
link_href = f"{self.host}{link_href}" if link_href.startswith('/') else f"{self.host}/{link_href}"
plist.append(f"{ep_name}${link_href}")
play_url = '#'.join(plist) if plist else f"未找到视频源${url}"
vod_content = ''
try:
tags = []
seen_names = set()
seen_ids = set()
tag_links = data('.tags a, .keywords a, .post-tags a')
candidates = []
for k in tag_links.items():
title = k.text().strip()
href = k.attr('href')
if title and href:
candidates.append({'name': title, 'id': href})
candidates.sort(key=lambda x: len(x['name']), reverse=True)
for item in candidates:
name = item['name']
id_ = item['id']
if id_ in seen_ids: continue
is_duplicate = False
for seen in seen_names:
if name in seen:
is_duplicate = True
break
if not is_duplicate:
target = json.dumps({'id': id_, 'name': name})
tags.append(f'[a=cr:{target}/]{name}[/a]')
seen_names.add(name)
seen_ids.add(id_)
if tags:
vod_content = ' '.join(tags)
else:
vod_content = data('.post-title').text()
except Exception:
vod_content = '获取标签失败'
if not vod_content:
vod_content = data('h1').text() or '91吃瓜中心'
return {'list': [{'vod_play_from': '91吃瓜中心', 'vod_play_url': play_url, 'vod_content': vod_content}]}
except:
return {'list': [{'vod_play_from': '91吃瓜中心', 'vod_play_url': '获取失败'}]}
def searchContent(self, key, quick, pg="1"):
try:
pg = int(pg) if pg else 1
if pg == 1:
url = f"{self.host}/search/{key}/"
else:
url = f"{self.host}/search/{key}/{pg}/"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
return {'list': self.getlist(self.getpq(response.text)('article')), 'page': pg, 'pagecount': 9999}
except:
return {'list': [], 'page': pg, 'pagecount': 9999}
def playerContent(self, flag, id, vipFlags):
parse = 0 if self.isVideoFormat(id) else 1
url = self.proxy(id) if '.m3u8' in id else id
return {'parse': parse, 'url': url, 'header': self.headers}
def localProxy(self, param):
try:
type_ = param.get('type')
url = param.get('url')
if type_ == 'cache':
key = param.get('key')
if content := img_cache.get(key):
return [200, 'image/jpeg', content]
return [404, 'text/plain', b'Expired']
elif type_ == 'img':
real_url = self.d64(url) if not url.startswith('http') else url
res = requests.get(real_url, headers=self.headers, proxies=self.proxies, timeout=10)
content = self.aesimg(res.content)
return [200, 'image/jpeg', content]
elif type_ == 'm3u8':
return self.m3Proxy(url)
else:
return self.tsProxy(url)
except:
return [404, 'text/plain', b'']
def proxy(self, data, type='m3u8'):
if data and self.proxies: return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
return data
def m3Proxy(self, url):
url = self.d64(url)
res = requests.get(url, headers=self.headers, proxies=self.proxies)
data = res.text
base = res.url.rsplit('/', 1)[0]
lines = []
for line in data.split('\n'):
if '#EXT' not in line and line.strip():
if not line.startswith('http'):
line = f"{base}/{line}"
lines.append(self.proxy(line, 'ts'))
else:
lines.append(line)
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
def tsProxy(self, url):
return [200, 'video/mp2t', requests.get(self.d64(url), headers=self.headers, proxies=self.proxies).content]
def e64(self, text):
return b64encode(str(text).encode()).decode()
def d64(self, text):
return b64decode(str(text).encode()).decode()
def aesimg(self, data):
if len(data) < 16: return data
keys = [(b'f5d965df75336270', b'97b60394abc2fbe1'), (b'75336270f5d965df', b'abc2fbe197b60394')]
for k, v in keys:
try:
dec = unpad(AES.new(k, AES.MODE_CBC, v).decrypt(data), 16)
if dec.startswith(b'\xff\xd8') or dec.startswith(b'\x89PNG'): return dec
except: pass
try:
dec = unpad(AES.new(k, AES.MODE_ECB).decrypt(data), 16)
if dec.startswith(b'\xff\xd8'): return dec
except: pass
return data
def getlist(self, data, tid=''):
videos = []
is_folder = '/mrdg' in (tid or '')
for k in data.items():
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
a = k if k.is_('a') else k('a').eq(0)
href = a.attr('href')
title = k('h2').text() or k('.entry-title').text() or k('.post-title').text()
if not title and k.is_('a'): title = k.text()
if href and title:
img = self.getimg(k('script').text(), k, card_html)
videos.append({
'vod_id': f"{href}{'@folder' if is_folder else ''}",
'vod_name': title.strip(),
'vod_pic': img,
'vod_remarks': k('time').text() or '',
'vod_tag': 'folder' if is_folder else '',
'style': {"type": "rect", "ratio": 1.33}
})
return videos
def getfod(self, id):
url = f"{self.host}{id}"
data = self.getpq(requests.get(url, headers=self.headers, proxies=self.proxies).text)
videos = []
for i, h2 in enumerate(data('.post-content h2').items()):
p_txt = data('.post-content p').eq(i * 2)
p_img = data('.post-content p').eq(i * 2 + 1)
p_html = p_img.outer_html() if hasattr(p_img, 'outer_html') else str(p_img)
videos.append({
'vod_id': p_txt('a').attr('href'),
'vod_name': p_txt.text().strip(),
'vod_pic': self.getimg('', p_img, p_html),
'vod_remarks': h2.text().strip()
})
return videos
def getimg(self, text, elem=None, html_content=None):
if m := re.search(r"loadBannerDirect\('([^']+)'", text or ''):
return self._proc_url(m.group(1))
if html_content is None and elem is not None:
html_content = elem.outer_html() if hasattr(elem, 'outer_html') else str(elem)
if not html_content: return ''
html_content = html_content.replace('&quot;', '"').replace('&apos;', "'").replace('&amp;', '&')
if 'data:image' in html_content:
m = re.search(r'(data:image/[a-zA-Z0-9+/=;,]+)', html_content)
if m: return self._proc_url(m.group(1))
m = re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp))', html_content, re.I)
if m: return self._proc_url(m.group(1))
if 'url(' in html_content:
m = re.search(r'url\s*\(\s*[\'"]?([^"\'\)]+)[\'"]?\s*\)', html_content, re.I)
if m: return self._proc_url(m.group(1))
return ''
def _proc_url(self, url):
if not url: return ''
url = url.strip('\'" ')
if url.startswith('data:'):
try:
_, b64_str = url.split(',', 1)
raw = b64decode(b64_str)
if not (raw.startswith(b'\xff\xd8') or raw.startswith(b'\x89PNG') or raw.startswith(b'GIF8')):
raw = self.aesimg(raw)
key = hashlib.md5(raw).hexdigest()
img_cache[key] = raw
return f"{self.getProxyUrl()}&type=cache&key={key}"
except: return ""
if not url.startswith('http'):
url = f"{self.host}{url}" if url.startswith('/') else f"{self.host}/{url}"
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
def getpq(self, data):
try: return pq(data)
except: return pq(data.encode('utf-8'))
+259
View File
@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
# 123AV短视频 - Fongmi影视App适配爬虫
# 优化为短视频模式,支持滑动切换
import sys
import re
import json
import urllib.parse
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "123AV"
def init(self, extend=''):
self.home_url = 'https://123av.fun'
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
def getDependence(self):
return []
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def homeContent(self, filter):
return {
'class': [
{'type_id': 'publish-time/sort-desc', 'type_name': '最新发布'},
{'type_id': 'view-count/sort-desc', 'type_name': '最多播放'},
{'type_id': 'comment-count/sort-desc', 'type_name': '最多评论'},
{'type_id': 'favorite-count/sort-desc', 'type_name': '最多收藏'},
{'type_id': 'explore', 'type_name': '探索发现'},
{'type_id': 'list', 'type_name': '排行榜'},
],
'filters': {}
}
def homeVideoContent(self):
return self.categoryContent('publish-time/sort-desc', 1, {}, {})
def _fetch_html(self, url):
try:
rsp = self.fetch(url, headers={
"User-Agent": self.ua,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
}, timeout=15)
if rsp and hasattr(rsp, 'text') and rsp.text:
return rsp.text
except Exception as e:
print(f'fetch error: {e}')
return ''
def _extract_video_list(self, html):
videos = []
if not html:
return videos
# 匹配视频卡片
card_pattern = re.compile(
r'<a\s+([^>]*data-src="https://static\.123av\.fun/[^"]+\.m3u8"[^>]*)>(.*?)</a>',
re.S
)
cards = card_pattern.findall(html)
for attrs, content in cards:
try:
src_match = re.search(r'data-src="(https://static\.123av\.fun/[^"]+\.m3u8)"', attrs)
poster_match = re.search(r'data-poster="([^"]*)"', attrs)
id_match = re.search(r'data-id="(\d+)"', attrs)
dur_match = re.search(r'data-duration="(\d+)"', attrs)
title_match = re.search(r'<xwya-video[^>]*alt="([^"]*)"', content)
if src_match and id_match:
m3u8_url = src_match.group(1)
vid = id_match.group(1)
poster = poster_match.group(1) if poster_match else ''
duration = dur_match.group(1) if dur_match else '0'
title = title_match.group(1).strip() if title_match else f'视频{vid}'
dur = int(duration)
if dur >= 3600:
duration_str = f'{dur // 3600}:{(dur % 3600) // 60:02d}:{dur % 60:02d}'
else:
duration_str = f'{dur // 60:02d}:{dur % 60:02d}'
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': poster,
'vod_remarks': duration_str,
})
except Exception as e:
continue
return videos
def categoryContent(self, tid, page, filter, ext):
video_list = []
if tid in ('explore', 'list', 'subscribe'):
url = f'{self.home_url}/{tid}/page-{page}'
else:
url = f'{self.home_url}/{tid}/page-{page}'
html = self._fetch_html(url)
video_list = self._extract_video_list(html)
return {
'list': video_list,
'page': int(page),
'pagecount': 999,
'limit': 20,
'total': 999 * 20
}
def detailContent(self, did):
"""视频详情 - 关键修改:返回播放URL让playerContent处理"""
video_list = []
try:
vid = did[0]
detail_url = f'{self.home_url}/detail/{vid}'
html = self._fetch_html(detail_url)
if html:
src_match = re.search(r'data-src="(https://static\.123av\.fun/[^"]+\.m3u8)"', html)
poster_match = re.search(r'data-poster="([^"]*)"', html)
title_match = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
if not title_match:
title_match = re.search(r'property="og:title"\s+content="([^"]*)"', html)
if not title_match:
title_match = re.search(r'<xwya-video[^>]*alt="([^"]*)"', html)
desc_match = re.search(r'property="og:description"\s+content="([^"]*)"', html)
dur_match = re.search(r'data-duration="(\d+)"', html)
m3u8_url = src_match.group(1) if src_match else ''
vod_pic = poster_match.group(1) if poster_match else ''
vod_name = title_match.group(1).strip() if title_match else ''
vod_content = desc_match.group(1) if desc_match else ''
duration_str = ''
if dur_match:
dur = int(dur_match.group(1))
if dur >= 3600:
duration_str = f'{dur // 3600}:{(dur % 3600) // 60:02d}:{dur % 60:02d}'
else:
duration_str = f'{dur // 60:02d}:{dur % 60:02d}'
else:
m3u8_url = ''
vod_pic = ''
vod_name = ''
vod_content = ''
duration_str = ''
# 关键修改:如果直接有m3u8,放入播放URL
# 使用特殊格式让Fongmi识别为短视频
if m3u8_url:
# 格式: 集数名称$url#集数名称$url
vod_play_url = f'正片${m3u8_url}'
else:
vod_play_url = ''
video_list.append({
'vod_id': vid,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': duration_str,
'vod_content': vod_content,
'vod_play_from': '短视频', # 改为短视频,可能触发滑动模式
'vod_play_url': vod_play_url,
'type_name': '短视频',
'vod_year': '',
'vod_area': '',
'vod_director': '',
'vod_actor': '',
})
except Exception as e:
print(f'detailContent error: {e}')
return {
'list': video_list,
'parse': 0,
'jx': 0
}
def searchContent(self, key, quick, page='1'):
video_list = []
try:
encoded_key = urllib.parse.quote(key)
url = f'{self.home_url}/search/{encoded_key}/page-{page}'
html = self._fetch_html(url)
video_list = self._extract_video_list(html)
except Exception as e:
print(f'searchContent error: {e}')
return {
'list': video_list,
'page': int(page),
'pagecount': 99,
'limit': 20,
'total': 99 * 20
}
def playerContent(self, flag, pid, vipFlags):
"""播放器内容 - 关键修改"""
# 如果pid已经是m3u8地址,直接返回
if pid.startswith('http') and '.m3u8' in pid:
return {
'parse': 0, # 直接播放
'url': pid,
'header': {
'User-Agent': self.ua,
'Referer': self.home_url + '/'
}
}
# 如果是详情页URL,获取m3u8
if '/detail/' in pid:
html = self._fetch_html(pid)
if html:
src_match = re.search(r'data-src="(https://static\.123av\.fun/[^"]+\.m3u8)"', html)
if src_match:
return {
'parse': 0,
'url': src_match.group(1),
'header': {
'User-Agent': self.ua,
'Referer': self.home_url + '/'
}
}
# 默认返回,让外部解析
return {
'parse': 1,
'url': pid,
'header': {
'User-Agent': self.ua,
'Referer': self.home_url + '/'
}
}
def localProxy(self, params):
return {}
def destroy(self):
return '正在Destroy'
if __name__ == '__main__':
pass
+479
View File
@@ -0,0 +1,479 @@
# -*- coding: utf-8 -*-
# 🌈 Love
import json
import random
import re
import sys
import threading
import time
from base64 import b64decode, b64encode
from urllib.parse import urlparse, urljoin
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self, extend=""):
try:self.proxies = json.loads(extend)
except:self.proxies = {}
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
'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',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache',
}
# Use working dynamic URLs directly
self.host = self.get_working_host()
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
self.log(f"使用站点: {self.host}")
print(f"使用站点: {self.host}")
pass
def getName(self):
return "🌈 51吸瓜"
def isVideoFormat(self, url):
# Treat direct media formats as playable without parsing
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
def manualVideoCheck(self):
return False
def destroy(self):
pass
def homeContent(self, filter):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200:
return {'class': [], 'list': []}
data = self.getpq(response.text)
result = {}
classes = []
# Try to get categories from different possible locations
category_selectors = [
'.category-list ul li',
'.nav-menu li',
'.menu li',
'nav ul li'
]
for selector in category_selectors:
for k in data(selector).items():
link = k('a')
href = (link.attr('href') or '').strip()
name = (link.text() or '').strip()
# Skip placeholder or invalid entries
if not href or href == '#' or not name:
continue
classes.append({
'type_name': name,
'type_id': href
})
if classes:
break
# If no categories found, create some default ones
if not classes:
classes = [
{'type_name': '最新', 'type_id': '/latest/'},
{'type_name': '热门', 'type_id': '/hot/'}
]
result['class'] = classes
result['list'] = self.getlist(data('#index article a'))
return result
except Exception as e:
print(f"homeContent error: {e}")
return {'class': [], 'list': []}
def homeVideoContent(self):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200:
return {'list': []}
data = self.getpq(response.text)
return {'list': self.getlist(data('#index article a, #archive article a'))}
except Exception as e:
print(f"homeVideoContent error: {e}")
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
try:
if '@folder' in tid:
id = tid.replace('@folder', '')
videos = self.getfod(id)
else:
# 处理分页逻辑
page_num = int(pg) if pg and pg.isdigit() else 1
# 构建基础URL
if tid.startswith('/'):
base_url = f"{self.host}{tid}"
else:
base_url = f"{self.host}/{tid}"
# 移除可能存在的末尾斜杠,以便统一添加分页
base_url = base_url.rstrip('/')
# 根据页码构建URL - 使用你提供的分页格式: /category/name/页码/
if page_num > 1:
url = f"{base_url}/{page_num}/"
else:
# 第一页使用基础URL
url = f"{base_url}/"
self.log(f"分类请求URL: {url}")
print(f"分类请求URL: {url}")
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
data = self.getpq(response.text)
videos = self.getlist(data('#archive article a, #index article a'), tid)
# 尝试获取总页数
pagecount = 1
try:
# 查找分页元素 - 尝试多种可能的选择器
pagination_selectors = ['.pagination', '.page-nav', '.pager', '.nav-links', '.pages']
for selector in pagination_selectors:
pagination = data(selector)
if pagination:
page_links = pagination.find('a')
page_numbers = []
for link in page_links.items():
text = link.text().strip()
if text.isdigit():
page_numbers.append(int(text))
if page_numbers:
pagecount = max(page_numbers)
break
# 如果没有找到分页元素,尝试查找下一页按钮判断页数
if pagecount == 1:
next_buttons = data('a:contains("下一页"), a:contains("Next")')
if next_buttons:
pagecount = 999 # 有下一页按钮但无法确定具体页数,设一个较大值
except Exception as e:
self.log(f"获取总页数失败: {e}")
pagecount = 999 # 如果无法获取,设置一个较大的值
result = {}
result['list'] = videos
result['page'] = pg
result['pagecount'] = pagecount
result['limit'] = 90
result['total'] = 999999
return result
except Exception as e:
print(f"categoryContent error: {e}")
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
def detailContent(self, ids):
try:
url = f"{self.host}{ids[0]}" if not ids[0].startswith('http') else ids[0]
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200:
return {'list': [{'vod_play_from': '51吸瓜', 'vod_play_url': f'页面加载失败${url}'}]}
data = self.getpq(response.text)
vod = {'vod_play_from': '51吸瓜'}
# Get content/description
try:
clist = []
if data('.tags .keywords a'):
for k in data('.tags .keywords a').items():
title = k.text()
href = k.attr('href')
if title and href:
clist.append('[a=cr:' + json.dumps({'id': href, 'name': title}) + '/]' + title + '[/a]')
vod['vod_content'] = ' '.join(clist) if clist else data('.post-title').text()
except:
vod['vod_content'] = data('.post-title').text() or '51吸瓜视频'
# Get video URLs (build episode list when multiple players exist)
try:
plist = []
used_names = set()
if data('.dplayer'):
for c, k in enumerate(data('.dplayer').items(), start=1):
config_attr = k.attr('data-config')
if config_attr:
try:
config = json.loads(config_attr)
video_url = config.get('video', {}).get('url', '')
# Determine a readable episode name from nearby headings if present
ep_name = ''
try:
parent = k.parents().eq(0)
# search up to a few ancestors for a heading text
for _ in range(3):
if not parent: break
heading = parent.find('h2, h3, h4').eq(0).text() or ''
heading = heading.strip()
if heading:
ep_name = heading
break
parent = parent.parents().eq(0)
except Exception:
ep_name = ''
base_name = ep_name if ep_name else f"视频{c}"
name = base_name
count = 2
# Ensure the name is unique
while name in used_names:
name = f"{base_name} {count}"
count += 1
used_names.add(name)
if video_url:
self.log(f"解析到视频: {name} -> {video_url}")
print(f"解析到视频: {name} -> {video_url}")
plist.append(f"{name}${video_url}")
except:
continue
if plist:
self.log(f"拼装播放列表,共{len(plist)}")
print(f"拼装播放列表,共{len(plist)}")
vod['vod_play_url'] = '#'.join(plist)
else:
vod['vod_play_url'] = f"未找到视频源${url}"
except Exception as e:
vod['vod_play_url'] = f"视频解析失败${url}"
return {'list': [vod]}
except Exception as e:
print(f"detailContent error: {e}")
return {'list': [{'vod_play_from': '51吸瓜', 'vod_play_url': f'详情页加载失败${ids[0] if ids else ""}'}]}
def searchContent(self, key, quick, pg="1"):
try:
# 处理搜索分页
page_num = int(pg) if pg and pg.isdigit() else 1
# 使用统一的分页格式
if page_num > 1:
url = f"{self.host}/search/{key}/{page_num}/"
else:
url = f"{self.host}/search/{key}/"
self.log(f"搜索请求URL: {url}")
print(f"搜索请求URL: {url}")
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200:
return {'list': [], 'page': pg}
data = self.getpq(response.text)
videos = self.getlist(data('#archive article a, #index article a'))
# 尝试获取搜索结果的页数
pagecount = 1
try:
pagination_selectors = ['.pagination', '.page-nav', '.pager', '.nav-links', '.pages']
for selector in pagination_selectors:
pagination = data(selector)
if pagination:
page_links = pagination.find('a')
page_numbers = []
for link in page_links.items():
text = link.text().strip()
if text.isdigit():
page_numbers.append(int(text))
if page_numbers:
pagecount = max(page_numbers)
break
# 如果没有找到分页元素,尝试查找下一页按钮判断页数
if pagecount == 1:
next_buttons = data('a:contains("下一页"), a:contains("Next")')
if next_buttons:
pagecount = 999
except:
pagecount = 999
return {'list': videos, 'page': pg, 'pagecount': pagecount}
except Exception as e:
print(f"searchContent error: {e}")
return {'list': [], 'page': pg}
def playerContent(self, flag, id, vipFlags):
url = id
p = 1
if self.isVideoFormat(url):
# m3u8/mp4 direct play; when using proxy setting, wrap to proxy for m3u8
if '.m3u8' in url:
url = self.proxy(url)
p = 0
self.log(f"播放请求: parse={p}, url={url}")
print(f"播放请求: parse={p}, url={url}")
return {'parse': p, 'url': url, 'header': self.headers}
def localProxy(self, param):
if param.get('type') == 'img':
res=requests.get(param['url'], headers=self.headers, proxies=self.proxies, timeout=10)
return [200,res.headers.get('Content-Type'),self.aesimg(res.content)]
elif param.get('type') == 'm3u8':return self.m3Proxy(param['url'])
else:return self.tsProxy(param['url'])
def proxy(self, data, type='m3u8'):
if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
else:return data
def m3Proxy(self, url):
url=self.d64(url)
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
data = ydata.content.decode('utf-8')
if ydata.headers.get('Location'):
url = ydata.headers['Location']
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
lines = data.strip().split('\n')
last_r = url[:url.rfind('/')]
parsed_url = urlparse(url)
durl = parsed_url.scheme + "://" + parsed_url.netloc
iskey=True
for index, string in enumerate(lines):
if iskey and 'URI' in string:
pattern = r'URI="([^"]*)"'
match = re.search(pattern, string)
if match:
lines[index] = re.sub(pattern, f'URI="{self.proxy(match.group(1), "mkey")}"', string)
iskey=False
continue
if '#EXT' not in string:
if 'http' not in string:
domain = last_r if string.count('/') < 2 else durl
string = domain + ('' if string.startswith('/') else '/') + string
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
data = '\n'.join(lines)
return [200, "application/vnd.apple.mpegur", data]
def tsProxy(self, url):
url = self.d64(url)
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
return [200, data.headers['Content-Type'], data.content]
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 get_working_host(self):
"""Get working host from known dynamic URLs"""
# Known working URLs from the dynamic gateway
dynamic_urls = [
'https://artist.vgwtswi.xyz',
'https://ability.vgwtswi.xyz',
'https://am.vgwtswi.xyz'
]
# Test each URL to find a working one
for url in dynamic_urls:
try:
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
if response.status_code == 200:
# Verify it has the expected content structure
data = self.getpq(response.text)
articles = data('#index article a')
if len(articles) > 0:
self.log(f"选用可用站点: {url}")
print(f"选用可用站点: {url}")
return url
except Exception as e:
continue
# Fallback to first URL if none work (better than crashing)
self.log(f"未检测到可用站点,回退: {dynamic_urls[0]}")
print(f"未检测到可用站点,回退: {dynamic_urls[0]}")
return dynamic_urls[0]
def getlist(self, data, tid=''):
videos = []
l = '/mrdg' in tid
for k in data.items():
a = k.attr('href')
b = k('h2').text()
# Some pages might not include datePublished; use a fallback
c = k('span[itemprop="datePublished"]').text() or k('.post-meta, .entry-meta, time').text()
if a and b:
videos.append({
'vod_id': f"{a}{'@folder' if l else ''}",
'vod_name': b.replace('\n', ' '),
'vod_pic': self.getimg(k('script').text()),
'vod_remarks': c or '',
'vod_tag': 'folder' if l else '',
'style': {"type": "rect", "ratio": 1.33}
})
return videos
def getfod(self, id):
url = f"{self.host}{id}"
data = self.getpq(requests.get(url, headers=self.headers, proxies=self.proxies).text)
vdata=data('.post-content[itemprop="articleBody"]')
r=['.txt-apps','.line','blockquote','.tags','.content-tabs']
for i in r:vdata.remove(i)
p=vdata('p')
videos=[]
for i,x in enumerate(vdata('h2').items()):
c=i*2
videos.append({
'vod_id': p.eq(c)('a').attr('href'),
'vod_name': p.eq(c).text(),
'vod_pic': f"{self.getProxyUrl()}&url={p.eq(c+1)('img').attr('data-xkrkllgl')}&type=img",
'vod_remarks':x.text()
})
return videos
def getimg(self, text):
match = re.search(r"loadBannerDirect\('([^']+)'", text)
if match:
url = match.group(1)
return f"{self.getProxyUrl()}&url={url}&type=img"
else:
return ''
def aesimg(self, word):
key = b'f5d965df75336270'
iv = b'97b60394abc2fbe1'
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(cipher.decrypt(word), AES.block_size)
return decrypted
def getpq(self, data):
try:
return pq(data)
except Exception as e:
print(f"{str(e)}")
return pq(data.encode('utf-8'))
+390
View File
@@ -0,0 +1,390 @@
import json
import re
import sys
import hashlib
from base64 import b64decode, b64encode
from urllib.parse import urlparse
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider as BaseSpider
img_cache = {}
class Spider(BaseSpider):
def init(self, extend=""):
try:
self.proxies = json.loads(extend)
except:
self.proxies = {}
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'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',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache',
}
self.host = self.get_working_host()
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
print(f"使用站点: {self.host}")
def getName(self):
return "🌈 51大赛|终极完美版"
def isVideoFormat(self, url):
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
def manualVideoCheck(self):
return False
def destroy(self):
global img_cache
img_cache.clear()
def get_working_host(self):
dynamic_urls = [
'https://jbgcz2.dzebypd.xyz/',
'https://jbgcz3.dzebypd.xyz/'
]
for url in dynamic_urls:
try:
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
if response.status_code == 200:
return url
except Exception:
continue
return dynamic_urls[0]
def homeContent(self, filter):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'class': [], 'list': []}
data = self.getpq(response.text)
classes = []
category_selectors = ['.category-list ul li', '.nav-menu li', '.menu li', 'nav ul li']
for selector in category_selectors:
for k in data(selector).items():
link = k('a')
href = (link.attr('href') or '').strip()
name = (link.text() or '').strip()
if not href or href == '#' or not name: continue
classes.append({'type_name': name, 'type_id': href})
if classes: break
if not classes:
classes = [{'type_name': '最新', 'type_id': '/latest/'}, {'type_name': '热门', 'type_id': '/hot/'}]
return {'class': classes, 'list': self.getlist(data('#index article, article'))}
except Exception as e:
return {'class': [], 'list': []}
def homeVideoContent(self):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'list': []}
data = self.getpq(response.text)
return {'list': self.getlist(data('#index article, article'))}
except Exception as e:
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
try:
if '@folder' in tid:
v = self.getfod(tid.replace('@folder', ''))
return {'list': v, 'page': 1, 'pagecount': 1, 'limit': 90, 'total': len(v)}
pg = int(pg) if pg else 1
if tid.startswith('http'):
base_url = tid.rstrip('/')
else:
path = tid if tid.startswith('/') else f"/{tid}"
base_url = f"{self.host}{path}".rstrip('/')
if pg == 1:
url = f"{base_url}/"
else:
url = f"{base_url}/{pg}/"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
data = self.getpq(response.text)
videos = self.getlist(data('#archive article, #index article, article'), tid)
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
except Exception as e:
return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
def detailContent(self, ids):
try:
url = ids[0] if ids[0].startswith('http') else f"{self.host}{ids[0]}"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
data = self.getpq(response.text)
plist = []
used_names = set()
if data('.dplayer'):
for c, k in enumerate(data('.dplayer').items(), start=1):
try:
config_attr = k.attr('data-config')
if config_attr:
config = json.loads(config_attr)
video_url = config.get('video', {}).get('url', '')
if video_url:
ep_name = ''
parent = k.parents().eq(0)
for _ in range(4):
if not parent: break
heading = parent.find('h2, h3, h4').eq(0).text().strip()
if heading:
ep_name = heading
break
parent = parent.parents().eq(0)
base_name = ep_name if ep_name else f"视频{c}"
name = base_name
count = 2
while name in used_names:
name = f"{base_name} {count}"
count += 1
used_names.add(name)
plist.append(f"{name}${video_url}")
except: continue
if not plist:
content_area = data('.post-content, article')
for i, link in enumerate(content_area('a').items(), start=1):
link_text = link.text().strip()
link_href = link.attr('href')
if link_href and any(kw in link_text for kw in ['点击观看', '观看', '播放', '视频', '第一弹', '第二弹', '第三弹', '第四弹', '第五弹', '第六弹', '第七弹', '第八弹', '第九弹', '第十弹']):
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
if not ep_name: ep_name = f"视频{i}"
if not link_href.startswith('http'):
link_href = f"{self.host}{link_href}" if link_href.startswith('/') else f"{self.host}/{link_href}"
plist.append(f"{ep_name}${link_href}")
play_url = '#'.join(plist) if plist else f"未找到视频源${url}"
vod_content = ''
try:
tags = []
seen_names = set()
seen_ids = set()
tag_links = data('.tags a, .keywords a, .post-tags a')
candidates = []
for k in tag_links.items():
title = k.text().strip()
href = k.attr('href')
if title and href:
candidates.append({'name': title, 'id': href})
candidates.sort(key=lambda x: len(x['name']), reverse=True)
for item in candidates:
name = item['name']
id_ = item['id']
if id_ in seen_ids: continue
is_duplicate = False
for seen in seen_names:
if name in seen:
is_duplicate = True
break
if not is_duplicate:
target = json.dumps({'id': id_, 'name': name})
tags.append(f'[a=cr:{target}/]{name}[/a]')
seen_names.add(name)
seen_ids.add(id_)
if tags:
vod_content = ' '.join(tags)
else:
vod_content = data('.post-title').text()
except Exception:
vod_content = '获取标签失败'
if not vod_content:
vod_content = data('h1').text() or '51大赛'
return {'list': [{'vod_play_from': '51大赛', 'vod_play_url': play_url, 'vod_content': vod_content}]}
except:
return {'list': [{'vod_play_from': '51大赛', 'vod_play_url': '获取失败'}]}
def searchContent(self, key, quick, pg="1"):
try:
pg = int(pg) if pg else 1
if pg == 1:
url = f"{self.host}/search/{key}/"
else:
url = f"{self.host}/search/{key}/{pg}/"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
return {'list': self.getlist(self.getpq(response.text)('article')), 'page': pg, 'pagecount': 9999}
except:
return {'list': [], 'page': pg, 'pagecount': 9999}
def playerContent(self, flag, id, vipFlags):
parse = 0 if self.isVideoFormat(id) else 1
url = self.proxy(id) if '.m3u8' in id else id
return {'parse': parse, 'url': url, 'header': self.headers}
def localProxy(self, param):
try:
type_ = param.get('type')
url = param.get('url')
if type_ == 'cache':
key = param.get('key')
if content := img_cache.get(key):
return [200, 'image/jpeg', content]
return [404, 'text/plain', b'Expired']
elif type_ == 'img':
real_url = self.d64(url) if not url.startswith('http') else url
res = requests.get(real_url, headers=self.headers, proxies=self.proxies, timeout=10)
content = self.aesimg(res.content)
return [200, 'image/jpeg', content]
elif type_ == 'm3u8':
return self.m3Proxy(url)
else:
return self.tsProxy(url)
except:
return [404, 'text/plain', b'']
def proxy(self, data, type='m3u8'):
if data and self.proxies: return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
return data
def m3Proxy(self, url):
url = self.d64(url)
res = requests.get(url, headers=self.headers, proxies=self.proxies)
data = res.text
base = res.url.rsplit('/', 1)[0]
lines = []
for line in data.split('\n'):
if '#EXT' not in line and line.strip():
if not line.startswith('http'):
line = f"{base}/{line}"
lines.append(self.proxy(line, 'ts'))
else:
lines.append(line)
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
def tsProxy(self, url):
return [200, 'video/mp2t', requests.get(self.d64(url), headers=self.headers, proxies=self.proxies).content]
def e64(self, text):
return b64encode(str(text).encode()).decode()
def d64(self, text):
return b64decode(str(text).encode()).decode()
def aesimg(self, data):
if len(data) < 16: return data
keys = [(b'f5d965df75336270', b'97b60394abc2fbe1'), (b'75336270f5d965df', b'abc2fbe197b60394')]
for k, v in keys:
try:
dec = unpad(AES.new(k, AES.MODE_CBC, v).decrypt(data), 16)
if dec.startswith(b'\xff\xd8') or dec.startswith(b'\x89PNG'): return dec
except: pass
try:
dec = unpad(AES.new(k, AES.MODE_ECB).decrypt(data), 16)
if dec.startswith(b'\xff\xd8'): return dec
except: pass
return data
def getlist(self, data, tid=''):
videos = []
is_folder = '/mrdg' in (tid or '')
for k in data.items():
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
a = k if k.is_('a') else k('a').eq(0)
href = a.attr('href')
title = k('h2').text() or k('.entry-title').text() or k('.post-title').text()
if not title and k.is_('a'): title = k.text()
if href and title:
img = self.getimg(k('script').text(), k, card_html)
videos.append({
'vod_id': f"{href}{'@folder' if is_folder else ''}",
'vod_name': title.strip(),
'vod_pic': img,
'vod_remarks': k('time').text() or '',
'vod_tag': 'folder' if is_folder else '',
'style': {"type": "rect", "ratio": 1.33}
})
return videos
def getfod(self, id):
url = f"{self.host}{id}"
data = self.getpq(requests.get(url, headers=self.headers, proxies=self.proxies).text)
videos = []
for i, h2 in enumerate(data('.post-content h2').items()):
p_txt = data('.post-content p').eq(i * 2)
p_img = data('.post-content p').eq(i * 2 + 1)
p_html = p_img.outer_html() if hasattr(p_img, 'outer_html') else str(p_img)
videos.append({
'vod_id': p_txt('a').attr('href'),
'vod_name': p_txt.text().strip(),
'vod_pic': self.getimg('', p_img, p_html),
'vod_remarks': h2.text().strip()
})
return videos
def getimg(self, text, elem=None, html_content=None):
if m := re.search(r"loadBannerDirect\('([^']+)'", text or ''):
return self._proc_url(m.group(1))
if html_content is None and elem is not None:
html_content = elem.outer_html() if hasattr(elem, 'outer_html') else str(elem)
if not html_content: return ''
html_content = html_content.replace('&quot;', '"').replace('&apos;', "'").replace('&amp;', '&')
if 'data:image' in html_content:
m = re.search(r'(data:image/[a-zA-Z0-9+/=;,]+)', html_content)
if m: return self._proc_url(m.group(1))
m = re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp))', html_content, re.I)
if m: return self._proc_url(m.group(1))
if 'url(' in html_content:
m = re.search(r'url\s*\(\s*[\'"]?([^"\'\)]+)[\'"]?\s*\)', html_content, re.I)
if m: return self._proc_url(m.group(1))
return ''
def _proc_url(self, url):
if not url: return ''
url = url.strip('\'" ')
if url.startswith('data:'):
try:
_, b64_str = url.split(',', 1)
raw = b64decode(b64_str)
if not (raw.startswith(b'\xff\xd8') or raw.startswith(b'\x89PNG') or raw.startswith(b'GIF8')):
raw = self.aesimg(raw)
key = hashlib.md5(raw).hexdigest()
img_cache[key] = raw
return f"{self.getProxyUrl()}&type=cache&key={key}"
except: return ""
if not url.startswith('http'):
url = f"{self.host}{url}" if url.startswith('/') else f"{self.host}/{url}"
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
def getpq(self, data):
try: return pq(data)
except: return pq(data.encode('utf-8'))
+390
View File
@@ -0,0 +1,390 @@
import json
import re
import sys
import hashlib
from base64 import b64decode, b64encode
from urllib.parse import urlparse
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider as BaseSpider
img_cache = {}
class Spider(BaseSpider):
def init(self, extend=""):
try:
self.proxies = json.loads(extend)
except:
self.proxies = {}
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'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',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache',
}
self.host = self.get_working_host()
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
print(f"使用站点: {self.host}")
def getName(self):
return "🌈 51爆料|终极完美版"
def isVideoFormat(self, url):
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
def manualVideoCheck(self):
return False
def destroy(self):
global img_cache
img_cache.clear()
def get_working_host(self):
dynamic_urls = [
'https://carry.wlmrpodg.cc/',
'https://analyze.wlmrpodg.cc/'
]
for url in dynamic_urls:
try:
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
if response.status_code == 200:
return url
except Exception:
continue
return dynamic_urls[0]
def homeContent(self, filter):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'class': [], 'list': []}
data = self.getpq(response.text)
classes = []
category_selectors = ['.category-list ul li', '.nav-menu li', '.menu li', 'nav ul li']
for selector in category_selectors:
for k in data(selector).items():
link = k('a')
href = (link.attr('href') or '').strip()
name = (link.text() or '').strip()
if not href or href == '#' or not name: continue
classes.append({'type_name': name, 'type_id': href})
if classes: break
if not classes:
classes = [{'type_name': '最新', 'type_id': '/latest/'}, {'type_name': '热门', 'type_id': '/hot/'}]
return {'class': classes, 'list': self.getlist(data('#index article, article'))}
except Exception as e:
return {'class': [], 'list': []}
def homeVideoContent(self):
try:
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'list': []}
data = self.getpq(response.text)
return {'list': self.getlist(data('#index article, article'))}
except Exception as e:
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
try:
if '@folder' in tid:
v = self.getfod(tid.replace('@folder', ''))
return {'list': v, 'page': 1, 'pagecount': 1, 'limit': 90, 'total': len(v)}
pg = int(pg) if pg else 1
if tid.startswith('http'):
base_url = tid.rstrip('/')
else:
path = tid if tid.startswith('/') else f"/{tid}"
base_url = f"{self.host}{path}".rstrip('/')
if pg == 1:
url = f"{base_url}/"
else:
url = f"{base_url}/{pg}/"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
if response.status_code != 200: return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
data = self.getpq(response.text)
videos = self.getlist(data('#archive article, #index article, article'), tid)
return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
except Exception as e:
return {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 0}
def detailContent(self, ids):
try:
url = ids[0] if ids[0].startswith('http') else f"{self.host}{ids[0]}"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
data = self.getpq(response.text)
plist = []
used_names = set()
if data('.dplayer'):
for c, k in enumerate(data('.dplayer').items(), start=1):
try:
config_attr = k.attr('data-config')
if config_attr:
config = json.loads(config_attr)
video_url = config.get('video', {}).get('url', '')
if video_url:
ep_name = ''
parent = k.parents().eq(0)
for _ in range(4):
if not parent: break
heading = parent.find('h2, h3, h4').eq(0).text().strip()
if heading:
ep_name = heading
break
parent = parent.parents().eq(0)
base_name = ep_name if ep_name else f"视频{c}"
name = base_name
count = 2
while name in used_names:
name = f"{base_name} {count}"
count += 1
used_names.add(name)
plist.append(f"{name}${video_url}")
except: continue
if not plist:
content_area = data('.post-content, article')
for i, link in enumerate(content_area('a').items(), start=1):
link_text = link.text().strip()
link_href = link.attr('href')
if link_href and any(kw in link_text for kw in ['点击观看', '观看', '播放', '视频', '第一弹', '第二弹', '第三弹', '第四弹', '第五弹', '第六弹', '第七弹', '第八弹', '第九弹', '第十弹']):
ep_name = link_text.replace('点击观看:', '').replace('点击观看', '').strip()
if not ep_name: ep_name = f"视频{i}"
if not link_href.startswith('http'):
link_href = f"{self.host}{link_href}" if link_href.startswith('/') else f"{self.host}/{link_href}"
plist.append(f"{ep_name}${link_href}")
play_url = '#'.join(plist) if plist else f"未找到视频源${url}"
vod_content = ''
try:
tags = []
seen_names = set()
seen_ids = set()
tag_links = data('.tags a, .keywords a, .post-tags a')
candidates = []
for k in tag_links.items():
title = k.text().strip()
href = k.attr('href')
if title and href:
candidates.append({'name': title, 'id': href})
candidates.sort(key=lambda x: len(x['name']), reverse=True)
for item in candidates:
name = item['name']
id_ = item['id']
if id_ in seen_ids: continue
is_duplicate = False
for seen in seen_names:
if name in seen:
is_duplicate = True
break
if not is_duplicate:
target = json.dumps({'id': id_, 'name': name})
tags.append(f'[a=cr:{target}/]{name}[/a]')
seen_names.add(name)
seen_ids.add(id_)
if tags:
vod_content = ' '.join(tags)
else:
vod_content = data('.post-title').text()
except Exception:
vod_content = '获取标签失败'
if not vod_content:
vod_content = data('h1').text() or '51爆料'
return {'list': [{'vod_play_from': '51爆料', 'vod_play_url': play_url, 'vod_content': vod_content}]}
except:
return {'list': [{'vod_play_from': '51爆料', 'vod_play_url': '获取失败'}]}
def searchContent(self, key, quick, pg="1"):
try:
pg = int(pg) if pg else 1
if pg == 1:
url = f"{self.host}/search/{key}/"
else:
url = f"{self.host}/search/{key}/{pg}/"
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
return {'list': self.getlist(self.getpq(response.text)('article')), 'page': pg, 'pagecount': 9999}
except:
return {'list': [], 'page': pg, 'pagecount': 9999}
def playerContent(self, flag, id, vipFlags):
parse = 0 if self.isVideoFormat(id) else 1
url = self.proxy(id) if '.m3u8' in id else id
return {'parse': parse, 'url': url, 'header': self.headers}
def localProxy(self, param):
try:
type_ = param.get('type')
url = param.get('url')
if type_ == 'cache':
key = param.get('key')
if content := img_cache.get(key):
return [200, 'image/jpeg', content]
return [404, 'text/plain', b'Expired']
elif type_ == 'img':
real_url = self.d64(url) if not url.startswith('http') else url
res = requests.get(real_url, headers=self.headers, proxies=self.proxies, timeout=10)
content = self.aesimg(res.content)
return [200, 'image/jpeg', content]
elif type_ == 'm3u8':
return self.m3Proxy(url)
else:
return self.tsProxy(url)
except:
return [404, 'text/plain', b'']
def proxy(self, data, type='m3u8'):
if data and self.proxies: return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
return data
def m3Proxy(self, url):
url = self.d64(url)
res = requests.get(url, headers=self.headers, proxies=self.proxies)
data = res.text
base = res.url.rsplit('/', 1)[0]
lines = []
for line in data.split('\n'):
if '#EXT' not in line and line.strip():
if not line.startswith('http'):
line = f"{base}/{line}"
lines.append(self.proxy(line, 'ts'))
else:
lines.append(line)
return [200, "application/vnd.apple.mpegurl", '\n'.join(lines)]
def tsProxy(self, url):
return [200, 'video/mp2t', requests.get(self.d64(url), headers=self.headers, proxies=self.proxies).content]
def e64(self, text):
return b64encode(str(text).encode()).decode()
def d64(self, text):
return b64decode(str(text).encode()).decode()
def aesimg(self, data):
if len(data) < 16: return data
keys = [(b'f5d965df75336270', b'97b60394abc2fbe1'), (b'75336270f5d965df', b'abc2fbe197b60394')]
for k, v in keys:
try:
dec = unpad(AES.new(k, AES.MODE_CBC, v).decrypt(data), 16)
if dec.startswith(b'\xff\xd8') or dec.startswith(b'\x89PNG'): return dec
except: pass
try:
dec = unpad(AES.new(k, AES.MODE_ECB).decrypt(data), 16)
if dec.startswith(b'\xff\xd8'): return dec
except: pass
return data
def getlist(self, data, tid=''):
videos = []
is_folder = '/mrdg' in (tid or '')
for k in data.items():
card_html = k.outer_html() if hasattr(k, 'outer_html') else str(k)
a = k if k.is_('a') else k('a').eq(0)
href = a.attr('href')
title = k('h2').text() or k('.entry-title').text() or k('.post-title').text()
if not title and k.is_('a'): title = k.text()
if href and title:
img = self.getimg(k('script').text(), k, card_html)
videos.append({
'vod_id': f"{href}{'@folder' if is_folder else ''}",
'vod_name': title.strip(),
'vod_pic': img,
'vod_remarks': k('time').text() or '',
'vod_tag': 'folder' if is_folder else '',
'style': {"type": "rect", "ratio": 1.33}
})
return videos
def getfod(self, id):
url = f"{self.host}{id}"
data = self.getpq(requests.get(url, headers=self.headers, proxies=self.proxies).text)
videos = []
for i, h2 in enumerate(data('.post-content h2').items()):
p_txt = data('.post-content p').eq(i * 2)
p_img = data('.post-content p').eq(i * 2 + 1)
p_html = p_img.outer_html() if hasattr(p_img, 'outer_html') else str(p_img)
videos.append({
'vod_id': p_txt('a').attr('href'),
'vod_name': p_txt.text().strip(),
'vod_pic': self.getimg('', p_img, p_html),
'vod_remarks': h2.text().strip()
})
return videos
def getimg(self, text, elem=None, html_content=None):
if m := re.search(r"loadBannerDirect\('([^']+)'", text or ''):
return self._proc_url(m.group(1))
if html_content is None and elem is not None:
html_content = elem.outer_html() if hasattr(elem, 'outer_html') else str(elem)
if not html_content: return ''
html_content = html_content.replace('&quot;', '"').replace('&apos;', "'").replace('&amp;', '&')
if 'data:image' in html_content:
m = re.search(r'(data:image/[a-zA-Z0-9+/=;,]+)', html_content)
if m: return self._proc_url(m.group(1))
m = re.search(r'(https?://[^"\'\s)]+\.(?:jpg|png|jpeg|webp))', html_content, re.I)
if m: return self._proc_url(m.group(1))
if 'url(' in html_content:
m = re.search(r'url\s*\(\s*[\'"]?([^"\'\)]+)[\'"]?\s*\)', html_content, re.I)
if m: return self._proc_url(m.group(1))
return ''
def _proc_url(self, url):
if not url: return ''
url = url.strip('\'" ')
if url.startswith('data:'):
try:
_, b64_str = url.split(',', 1)
raw = b64decode(b64_str)
if not (raw.startswith(b'\xff\xd8') or raw.startswith(b'\x89PNG') or raw.startswith(b'GIF8')):
raw = self.aesimg(raw)
key = hashlib.md5(raw).hexdigest()
img_cache[key] = raw
return f"{self.getProxyUrl()}&type=cache&key={key}"
except: return ""
if not url.startswith('http'):
url = f"{self.host}{url}" if url.startswith('/') else f"{self.host}/{url}"
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
def getpq(self, data):
try: return pq(data)
except: return pq(data.encode('utf-8'))
+71
View File
@@ -0,0 +1,71 @@
import re
import requests
class Spider():
def getName(self):
return 'JAV36'
def init(self, extend=''):
pass
def getDependence(self):
return []
def homeContent(self, filter):
return {
'class': [
{'type_name': '最新更新', 'type_id': 'latest-updates/'},
{'type_name': '4K高清', 'type_id': 'tags/4k/'}
],
'list': []
}
def homeVideoContent(self):
return self.categoryContent('latest-updates/', 1, False, {})
def categoryContent(self, tid, pg, filter, extend):
url = f'https://jav36.com/{tid}'
if int(pg) > 1:
url = f'https://jav36.com/{tid}{pg}/'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'}
try:
res = requests.get(url, headers=headers, timeout=15)
return {'list': self.parse_list(res.text)}
except: return {'list': []}
def searchContent(self, keyword, quick, pg=1):
url = f'https://jav36.com/search/{keyword}/'
if int(pg) > 1:
url = f'https://jav36.com/search/{keyword}/{pg}/'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'}
try:
res = requests.get(url, headers=headers, timeout=15)
return {'list': self.parse_list(res.text)}
except: return {'list': []}
def parse_list(self, html):
vod_list = []
pattern = r'href=\"https://jav36\.com/videos/(?P<id_path>\d+/(?P<id>[^/]+)/)\" title=\"(?P<name>[^\"]+)\".*?data-original=\"(?P<pic>[^\"]+)\"'
for m in re.finditer(pattern, html, re.S):
vod_list.append({
'vod_id': m.group('id_path'),
'vod_name': m.group('name').strip(),
'vod_pic': m.group('pic'),
'vod_remarks': 'Full HD'
})
return vod_list
def detailContent(self, ids):
return {'list': [{'vod_id': ids[0], 'vod_name': 'Video', 'vod_play_from': 'Direct', 'vod_play_url': 'Play$'+ids[0]}]}
def playerContent(self, flag, id, vipFlags):
url = f'https://jav36.com/videos/{id}'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'}
try:
res = requests.get(url, headers=headers, timeout=10)
match = re.search(r'\"contentUrl\"\s*:\s*\"([^\"]+)\"', res.text)
if match:
return {'parse': 0, 'url': match.group(1), 'header': {'User-Agent': headers['User-Agent'], 'Referer': 'https://jav36.com/'}}
except: pass
return {'parse': 0, 'url': url}
+143
View File
@@ -0,0 +1,143 @@
import json
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "PigAV_Stable"
def init(self, extend=""):
self.base_url = "https://pigav.ws"
self.api_url = "https://pigav.ws/api/v1"
# 基础浏览器特征
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Origin": "https://pigav.ws",
"Referer": "https://pigav.ws/"
}
self.page_size = 24
def homeContent(self, filter):
result = {"class": []}
result["class"] = [
{"type_id": "publishedAt", "type_name": "最近更新"},
{"type_id": "hot", "type_name": "热门视频"},
{"type_id": "views", "type_name": "最多观看"}
]
result["list"] = self.get_videos(f"{self.api_url}/videos?sort=-publishedAt&count={self.page_size}&start=0")
return result
def categoryContent(self, tid, pg, filter, extend):
sort_map = {"publishedAt": "-publishedAt", "hot": "-hot", "views": "-views"}
sort = sort_map.get(tid, "-publishedAt")
p = int(pg)
start = (max(1, p) - 1) * self.page_size
url = f"{self.api_url}/videos?sort={sort}&count={self.page_size}&start={start}"
return {"list": self.get_videos(url)}
def detailContent(self, ids):
vid = ids[0] if isinstance(ids, list) else ids
url = f"{self.api_url}/videos/{vid}"
try:
res = self.fetch(url, headers=self.headers, timeout=10)
data = json.loads(res.text if hasattr(res, 'text') else res)
vod = {
"vod_id": vid,
"vod_name": data.get("name", ""),
"vod_pic": self.fix_url(data.get("thumbnailPath", "")),
"vod_remarks": self.format_time(data.get("duration", 0)),
"vod_actor": data.get("channel", {}).get("displayName", ""),
"vod_content": data.get("description", ""),
"vod_play_from": "PigAV",
"vod_play_url": f"播放正片${vid}"
}
return {"list": [vod]}
except:
return {"list": []}
def searchContent(self, key, quick, pg="1"):
p = int(pg)
start = (max(1, p) - 1) * self.page_size
url = f"{self.api_url}/search/videos?search={key}&count={self.page_size}&start={start}"
return {"list": self.get_videos(url)}
def playerContent(self, flag, id, vipFlags):
url = f"{self.api_url}/videos/{id}"
# 终极伪装 Header:模拟 Chrome 播放内核请求行为,防止 CDN 断流
play_headers = {
"User-Agent": self.headers["User-Agent"],
"Referer": f"https://pigav.ws/videos/{id}",
"Origin": "https://pigav.ws",
"Accept": "*/*",
"Range": "bytes=0-", # 核心:开启断点续传支持
"Connection": "keep-alive",
"Sec-Fetch-Dest": "video",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site"
}
try:
res = self.fetch(url, headers=self.headers, timeout=12)
data = json.loads(res.text if hasattr(res, 'text') else res)
play_url = ""
# 优先方案:HLS (m3u8) 分片加载,最不容易超时
streaming = data.get("streamingPlaylists", [])
if streaming:
# 策略:如果网络差,选列表中间的画质(通常 0 是低,最后是高)
# 这里我们尝试选倒数第二个,通常是 720P,兼顾清晰度与速度
idx = max(0, len(streaming) - 2)
play_url = streaming[idx].get("playlistUrl")
# 备选方案:MP4 直链
if not play_url:
files = data.get("files", [])
if files:
# 优先寻找高度 <= 720 的文件
suitable_files = [f for f in files if f.get("resolution", {}).get("height", 0) <= 720]
best_file = suitable_files[-1] if suitable_files else files[0]
play_url = best_file.get("fileUrl") or best_file.get("fileDownloadUrl")
if play_url:
return {
"parse": 0,
"url": self.fix_url(play_url),
"header": play_headers,
"timeout": 60 # 延长内核超时建议值
}
except:
pass
return {"parse": 0, "url": ""}
def get_videos(self, url):
videos = []
try:
res = self.fetch(url, headers=self.headers, timeout=10)
content = res.text if hasattr(res, 'text') else res
data = json.loads(content)
items = data.get("data", []) if isinstance(data, dict) else data
for item in items:
videos.append({
"vod_id": item.get("shortUUID") or item.get("uuid"),
"vod_name": item.get("name", ""),
"vod_pic": self.fix_url(item.get("thumbnailPath", "")),
"vod_remarks": self.format_time(item.get("duration", 0))
})
except:
pass
return videos
def fix_url(self, path):
if not path: return ""
if path.startswith("http"): return path
return self.base_url + path
def format_time(self, seconds):
try:
sec = int(seconds)
m, s = divmod(sec, 60)
h, m = divmod(m, 60)
return f"{h:02d}:{m:02d}:{s:02d}" if h > 0 else f"{m:02d}:{s:02d}"
except: return ""
+124
View File
@@ -0,0 +1,124 @@
import sys
import re
import json
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "TOPTV"
def init(self, extend=""):
super().init(extend)
self.site_url = "https://toptv15.cyou"
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": self.site_url
}
self.sess = requests.Session()
self.sess.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=1)))
def fetch(self, url, timeout=10):
try:
res = self.sess.get(url, headers=self.headers, timeout=timeout, verify=False)
res.encoding = "utf-8"
return res
except:
return None
def homeContent(self, filter):
cate_list = [
{"type_name": "国产自拍", "type_id": "1"},
{"type_name": "国产传媒", "type_id": "2"},
{"type_name": "探花系列", "type_id": "3"},
{"type_name": "人妻熟女", "type_id": "4"},
{"type_name": "日本无码", "type_id": "5"},
{"type_name": "美乳巨乳", "type_id": "6"},
{"type_name": "强制侵犯", "type_id": "7"},
{"type_name": "制服诱惑", "type_id": "8"},
{"type_name": "绝色佳人", "type_id": "9"},
{"type_name": "家庭乱伦", "type_id": "10"},
{"type_name": "绝顶潮吹", "type_id": "11"},
{"type_name": "网红主播", "type_id": "12"}
]
return {"class": cate_list}
def categoryContent(self, tid, pg, filter, extend):
if not hasattr(self, 'site_url'): self.init()
pg = int(pg) if str(pg).isdigit() else 1
list_url = f"{self.site_url}/index.php/vod/type/id/{tid}/page/{pg}.html"
res = self.fetch(list_url)
video_list = []
if res:
pattern = r'href="(/index.php/vod/detail/id/(\d+).html)".*?data-original="(.*?)".*?vod-name.*?>(.*?)<'
matches = re.findall(pattern, res.text, re.S)
for href, v_id, pic, name in matches:
video_list.append({
"vod_id": v_id,
"vod_name": name.strip(),
"vod_pic": pic if pic.startswith("http") else self.site_url + pic,
"vod_remarks": ""
})
return {'list': video_list, 'page': pg, 'pagecount': 999, 'limit': 20, 'total': 9999}
def detailContent(self, ids):
if not hasattr(self, 'site_url'): self.init()
vod_id = ids[0]
res = self.fetch(f"{self.site_url}/index.php/vod/detail/id/{vod_id}.html")
if not res: return {}
html = res.text
name_match = re.search(r'vod-name.*?>(.*?)<', html) or re.search(r'title-box.*?>(.*?)<', html)
pic_match = re.search(r'detail-pic.*?src="(.*?)"', html) or re.search(r'data-original="(.*?)"', html)
play_matches = re.findall(r'href="(/index.php/vod/play/id/(\d+)/sid/(\d+)/nid/(\d+).html)">(.*?)<', html)
play_urls = []
for m in play_matches:
play_urls.append(f"{m[4]}${m[1]}-{m[2]}-{m[3]}")
if not play_urls:
play_urls.append(f"立即播放${vod_id}-1-1")
vod = {
"vod_id": vod_id,
"vod_name": name_match.group(1).strip() if name_match else "视频详情",
"vod_pic": pic_match.group(1) if pic_match else "",
"vod_play_from": "TOP-TV",
"vod_play_url": "#".join(play_urls)
}
return {"list": [vod]}
def playerContent(self, flag, id, vipFlags):
if not hasattr(self, 'site_url'): self.init()
parts = id.split('-')
if len(parts) == 3:
v_id, s_id, n_id = parts
play_url = f"{self.site_url}/index.php/vod/play/id/{v_id}/sid/{s_id}/nid/{n_id}.html"
else:
play_url = f"{self.site_url}/index.php/vod/play/id/{id}.html"
res = self.fetch(play_url)
if res:
data_json = re.search(r'var player_aaaa=(.*?)</script>', res.text)
if data_json:
try:
url = json.loads(data_json.group(1)).get("url", "")
return {"parse": 0, "url": url, "header": self.headers}
except:
pass
return {"parse": 1, "url": play_url}
def searchContent(self, key, quick, pg=1):
if not hasattr(self, 'site_url'): self.init()
res = self.fetch(f"{self.site_url}/index.php/vod/search/page/{pg}/wd/{key}.html")
video_list = []
if res:
pattern = r'href="(/index.php/vod/detail/id/(\d+).html)".*?data-original="(.*?)".*?vod-name.*?>(.*?)<'
matches = re.findall(pattern, res.text, re.S)
for href, v_id, pic, name in matches:
video_list.append({
"vod_id": v_id,
"vod_name": name.strip(),
"vod_pic": pic if pic.startswith("http") else self.site_url + pic
})
return {"list": video_list}
+303
View File
@@ -0,0 +1,303 @@
# coding: utf-8
import json
import sys
import re
import urllib.request
import urllib.parse
import ssl
sys.path.append('..')
from base.spider import Spider
VERSION = '2.0.0'
SITE_URL = 'https://newxvideos.pages.dev'
API_URL = 'https://newxvideos.pages.dev/api'
CATEGORIES = [
{"type_id": "Arab-159", "type_name": "阿拉伯"},
{"type_id": "Mature-38", "type_name": "成熟"},
{"type_id": "Cuckold-237", "type_name": "出轨背叛"},
{"type_id": "Femdom-235", "type_name": "调教"},
{"type_id": "Anal-12", "type_name": "肛交"},
{"type_id": "Brunette-25", "type_name": "褐发"},
{"type_id": "Black_Woman-30", "type_name": "黑人"},
{"type_id": "Redhead-31", "type_name": "红发"},
{"type_id": "Fucked_Up_Family-81", "type_name": "家庭乱搞"},
{"type_id": "Blonde-20", "type_name": "金发"},
{"type_id": "Big_Cock-34", "type_name": "巨屌"},
{"type_id": "Big_Tits-23", "type_name": "巨乳"},
{"type_id": "Big_Ass-24", "type_name": "巨臀"},
{"type_id": "Blowjob-15", "type_name": "口交"},
{"type_id": "Latina-16", "type_name": "拉丁裔"},
{"type_id": "Milf-19", "type_name": "辣妈"},
{"type_id": "Gapes-167", "type_name": "裂开"},
{"type_id": "Ass-14", "type_name": "美臀"},
{"type_id": "Lesbian-26", "type_name": "女同"},
{"type_id": "bbw-51", "type_name": "胖女"},
{"type_id": "Squirting-56", "type_name": "喷出"},
{"type_id": "Fisting-165", "type_name": "拳交"},
{"type_id": "Gangbang-69", "type_name": "群交"},
{"type_id": "Teen-13", "type_name": "少女"},
{"type_id": "Cumshot-18", "type_name": "射颜"},
{"type_id": "Cam_Porn-58", "type_name": "摄像头"},
{"type_id": "Bi_Sexual-62", "type_name": "双性恋"},
{"type_id": "Stockings-28", "type_name": "丝袜"},
{"type_id": "Oiled-22", "type_name": "涂油"},
{"type_id": "Lingerie-83", "type_name": "性感内衣"},
{"type_id": "Asian_Woman-32", "type_name": "亚洲"},
{"type_id": "Amateur-65", "type_name": "业余"},
{"type_id": "Interracial-27", "type_name": "异族"},
{"type_id": "Indian-89", "type_name": "印度"},
{"type_id": "Creampie-40", "type_name": "中出"},
{"type_id": "Solo_and_Masturbation-33", "type_name": "自慰"},
{"type_id": "AI-239", "type_name": "AI"},
{"type_id": "ASMR-229", "type_name": "ASMR"},
]
class Spider(Spider):
def getName(self):
return "V-HUB[成人]"
def init(self, extend):
if extend:
self.host = extend.get('host', SITE_URL)
else:
self.host = SITE_URL
self.api_url = self.host.rstrip('/') + '/api'
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': self.host + '/',
'Origin': self.host
}
self._ssl_context = ssl.create_default_context()
self._ssl_context.check_hostname = False
self._ssl_context.verify_mode = ssl.CERT_NONE
def _xhttp(self, params):
"""使用标准库urllib发起HTTP GET请求"""
try:
qs = urllib.parse.urlencode(params)
full_url = self.api_url + '?' + qs
req = urllib.request.Request(full_url, headers=self.headers, method='GET')
resp = urllib.request.urlopen(req, context=self._ssl_context, timeout=15)
data = json.loads(resp.read().decode('utf-8'))
if isinstance(data, list):
return data
elif isinstance(data, dict) and 'data' in data:
return data['data']
return []
except Exception as e:
print('_xhttp error: %s' % str(e), file=sys.stderr)
return []
def _format_time_cn(self, time_str):
"""将英文时间格式转为中文,如 '11 min' -> '11分钟'"""
if not time_str:
return ''
m = re.match(r'^(\d+)\s*min\s*$', time_str.strip(), re.IGNORECASE)
if m:
return m.group(1) + '分钟'
m = re.match(r'^(\d+)\s*h(?:our)?s?\s*(\d+)?\s*min\s*$', time_str.strip(), re.IGNORECASE)
if m:
h = m.group(1)
mi = m.group(2)
if mi:
return h + '小时' + mi + '分钟'
return h + '小时'
return time_str
def _extract_xvid(self, url):
"""从视频URL的查询参数中提取xvid值"""
if not url:
return ''
parsed = urllib.parse.urlparse(url)
qs = urllib.parse.parse_qs(parsed.query)
if 'xvid' in qs:
return qs['xvid'][0]
return ''
def _build_vod_list(self, raw_data):
"""将API返回的原始数据构造为vod列表"""
videos = []
for item in raw_data:
title = item.get('title', '')
clean_title = re.sub(r'^AVOTC资源网[—-]+\s*', '', title).strip()
if not clean_title:
clean_title = title
url = item.get('url', '')
vod_id = self._extract_xvid(url)
if not vod_id:
vod_id = str(item.get('videoid', ''))
videos.append({
'vod_id': vod_id,
'vod_name': clean_title,
'vod_pic': item.get('img', ''),
'vod_remarks': self._format_time_cn(item.get('time', '')),
'vod_url': url
})
return videos
def homeContent(self, filter):
"""首页:返回分类列表 + 首页视频"""
classes = []
for cat in CATEGORIES:
classes.append({'type_id': cat['type_id'], 'type_name': cat['type_name']})
raw_data = self._xhttp({'play': 'list', 'page': 1})
videos = self._build_vod_list(raw_data)
return {'class': classes, 'list': videos}
def homeVideoContent(self):
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
"""分类内容"""
raw_data = self._xhttp({'play': 'class', 'c': tid, 'page': pg})
videos = self._build_vod_list(raw_data)
type_name = tid
for cat in CATEGORIES:
if cat['type_id'] == tid:
type_name = cat['type_name']
break
return {
'page': int(pg),
'pagecount': 9999,
'limit': 90,
'total': 9999,
'type_name': type_name,
'list': videos
}
def detailContent(self, array):
"""详情:通过xvid获取视频播放地址"""
result = {}
if not array or not array[0]:
return result
xvid = array[0]
vod = {
'vod_id': xvid,
'vod_name': '视频详情',
'vod_pic': '',
'vod_remarks': '',
'vod_play_from': 'newxvideos',
'vod_play_url': ''
}
try:
qs = urllib.parse.urlencode({'xvid': xvid})
full_url = self.api_url + '?' + qs
req = urllib.request.Request(full_url, headers=self.headers, method='GET')
resp = urllib.request.urlopen(req, context=self._ssl_context, timeout=15)
data = json.loads(resp.read().decode('utf-8'))
except Exception as e:
print('detailContent error: %s' % str(e), file=sys.stderr)
result['list'] = [vod]
return result
play_urls = []
if isinstance(data, dict):
item = data
if 'data' in data and isinstance(data['data'], dict):
item = data['data']
hls_url = item.get('hls') or item.get('m3u8') or ''
hight_url = item.get('hight') or item.get('high') or item.get('hd') or ''
low_url = item.get('low') or item.get('sd') or ''
if hls_url:
play_urls.append('高清HLS$' + hls_url)
if hight_url:
play_urls.append('高清MP4$' + hight_url)
if low_url:
play_urls.append('低清MP4$' + low_url)
title = item.get('title', '')
if title:
clean_title = re.sub(r'^AVOTC资源网[—-]+\s*', '', title).strip()
if clean_title:
vod['vod_name'] = clean_title
img = item.get('img', '')
if img:
vod['vod_pic'] = img
time_str = item.get('time', '')
if time_str:
vod['vod_remarks'] = self._format_time_cn(time_str)
elif isinstance(data, list):
for item in data:
hls_url = item.get('hls') or item.get('m3u8') or ''
hight_url = item.get('hight') or item.get('high') or item.get('hd') or ''
low_url = item.get('low') or item.get('sd') or ''
if hls_url:
play_urls.append('高清HLS$' + hls_url)
if hight_url:
play_urls.append('高清MP4$' + hight_url)
if low_url:
play_urls.append('低清MP4$' + low_url)
if vod['vod_name'] == '视频详情':
title = item.get('title', '')
if title:
clean_title = re.sub(r'^AVOTC资源网[—-]+\s*', '', title).strip()
if clean_title:
vod['vod_name'] = clean_title
img = item.get('img', '')
if img:
vod['vod_pic'] = img
time_str = item.get('time', '')
if time_str:
vod['vod_remarks'] = self._format_time_cn(time_str)
if play_urls:
vod['vod_play_url'] = '#'.join(play_urls)
result['list'] = [vod]
return result
def searchContent(self, key, quick, pg='1'):
"""搜索"""
raw_data = self._xhttp({'play': 'k', 'k': key, 'page': pg})
videos = self._build_vod_list(raw_data)
return {
'page': int(pg),
'pagecount': 9999,
'limit': 90,
'total': 9999,
'list': videos
}
def playerContent(self, flag, id, vipFlags):
"""播放地址解析 - 直接返回用户选择的清晰度地址"""
if id and (id.startswith('http://') or id.startswith('https://')):
return {
'parse': 0,
'playUrl': '',
'url': id,
'header': {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.host + '/'
}
}
return {'parse': 0, 'playUrl': '', 'url': '', 'header': {}}
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def localProxy(self, param):
return {}
+360
View File
@@ -0,0 +1,360 @@
# coding=utf-8
import sys
import os
import re
import json
import urllib.parse
from base.spider import Spider
from bs4 import BeautifulSoup
class Spider(Spider):
def getName(self):
return "\u4e45\u4e45\u7f51"
def init(self, extend=""):
self.host = "https://ww.jiujiu.one"
print(f"Initialized with host: {self.host}")
def header(self):
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': self.host,
'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',
}
def homeContent(self, filter):
"""\u8fd4\u56de\u5206\u7c7b\u5217\u8868"""
result = {}
classes = [
{"type_name": "亞洲無碼", "type_id": "68"},
{"type_name": "日本女優", "type_id": "67"},
{"type_name": "日本無碼", "type_id": "23"},
{"type_name": "中文字幕", "type_id": "9"},
{"type_name": "日本有碼", "type_id": "24"},
{"type_name": "日韓無碼", "type_id": "82"},
{"type_name": "無碼專區", "type_id": "113"},
{"type_name": "AV明星", "type_id": "78"},
{"type_name": "倫理影片", "type_id": "269"},
{"type_name": "日本片商", "type_id": "90"},
{"type_name": "國產自拍", "type_id": "80"},
{"type_name": "傳媒原創", "type_id": "231"},
{"type_name": "國產精品", "type_id": "63"},
{"type_name": "國產情色", "type_id": "77"},
{"type_name": "美女主播", "type_id": "105"},
{"type_name": "強姦亂倫", "type_id": "33"},
{"type_name": "國產主播", "type_id": "36"},
{"type_name": "亞洲有碼", "type_id": "66"},
{"type_name": "偷拍自拍", "type_id": "3"},
{"type_name": "抖陰視頻", "type_id": "91"},
{"type_name": "制服誘惑", "type_id": "31"},
{"type_name": "黑料不打烊", "type_id": "10"},
{"type_name": "歐美精品", "type_id": "25"},
]
result["class"] = classes
result["list"] = []
return result
def homeVideoContent(self):
"""\u9996\u9875\u63a8\u8350\u89c6\u9891"""
try:
print("Fetching home page...")
rsp = self.fetch(self.host, headers=self.header())
print(f"Response status: {rsp.status}")
# \u4f7f\u7528\u6b63\u5219\u8865\u6551\u65b9\u6848\uff0c\u76f4\u63a5\u4eceHTML\u4e2d\u63d0\u53d6\u89c6\u9891\u4fe1\u606f
html = rsp.text
# \u5339\u914d\u89c6\u9891\u9879\u6a21\u5f0f
# \u67e5\u627e\u6240\u6709 div.item
videos = []
# \u4f7f\u7528BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
# \u76f4\u63a5\u67e5\u627e\u6240\u6709\u5e26\u6709\u89c6\u9891\u7684\u5361\u7247
items = soup.find_all('div', class_=lambda c: c and 'item' in c.split())
print(f"Found {len(items)} items with class containing 'item'")
for item in items:
try:
# \u627e\u5230\u94fe\u63a5
links = item.find_all('a', href=True)
if len(links) < 2:
continue
# \u6807\u9898\u94fe\u63a5\u901a\u5e38\u662f\u7b2c\u4e8c\u4e2a
title_link = links[1] if len(links) > 1 else links[0]
href = title_link.get('href', '')
if not href:
continue
# \u6784\u5efa\u5b8c\u6574\u7684URL
if href.startswith('/'):
vod_id = self.host + href
else:
vod_id = href
# \u6807\u9898
vod_name = title_link.get_text(strip=True)
# \u5c01\u9762\u56fe
vod_pic = ''
img = item.find('img')
if img:
vod_pic = img.get('src') or img.get('data-src') or ''
if vod_pic and not vod_pic.startswith('http'):
if vod_pic.startswith('/'):
vod_pic = self.host + vod_pic
else:
vod_pic = 'https:' + vod_pic if vod_pic.startswith('//') else vod_pic
# \u5907\u6ce8
vod_remarks = ''
badge = item.find('span', class_='badge')
if badge:
vod_remarks = badge.get_text(strip=True)
if vod_name and vod_id:
videos.append({
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': vod_remarks
})
print(f"Added video: {vod_name}")
except Exception as e:
print(f"Error processing item: {e}")
continue
print(f"Total videos extracted: {len(videos)}")
return {'list': videos}
except Exception as e:
print(f"Error in homeVideoContent: {e}")
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
"""\u5206\u7c7b\u9875\u5185\u5bb9"""
try:
# \u6784\u5efa\u5206\u7c7b\u9875URL\uff0c\u652f\u6301\u5206\u9875
if pg == "1":
url = f"{self.host}/c/{tid}"
else:
url = f"{self.host}/c/{tid}?page={pg}"
print(f"Fetching category URL: {url}")
rsp = self.fetch(url, headers=self.header())
soup = BeautifulSoup(rsp.text, 'html.parser')
videos = []
items = soup.find_all('div', class_=lambda c: c and 'item' in c.split())
for item in items:
try:
links = item.find_all('a', href=True)
if len(links) < 2:
continue
title_link = links[1] if len(links) > 1 else links[0]
href = title_link.get('href', '')
if not href:
continue
if href.startswith('/'):
vod_id = self.host + href
else:
vod_id = href
vod_name = title_link.get_text(strip=True)
vod_pic = ''
img = item.find('img')
if img:
vod_pic = img.get('src') or img.get('data-src') or ''
if vod_pic and not vod_pic.startswith('http'):
if vod_pic.startswith('/'):
vod_pic = self.host + vod_pic
else:
vod_pic = 'https:' + vod_pic if vod_pic.startswith('//') else vod_pic
vod_remarks = ''
badge = item.find('span', class_='badge')
if badge:
vod_remarks = badge.get_text(strip=True)
if vod_name and vod_id:
videos.append({
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': vod_remarks
})
except Exception as e:
print(f"Error processing item: {e}")
continue
# \u83b7\u53d6\u603b\u9875\u6570
total_pages = 1
pagination = soup.find('ul', class_='pagination')
if pagination:
page_links = pagination.find_all('a')
for link in page_links:
text = link.get_text(strip=True)
if text.isdigit():
page_num = int(text)
if page_num > total_pages:
total_pages = page_num
return {
'list': videos,
'page': int(pg),
'pagecount': total_pages,
'limit': len(videos),
'total': total_pages * len(videos) if total_pages > 0 else len(videos)
}
except Exception as e:
print(f"Error in categoryContent: {e}")
return {'list': [], 'page': int(pg), 'pagecount': 1, 'limit': 0, 'total': 0}
def detailContent(self, ids):
"""\u8be6\u60c5\u9875\u5185\u5bb9"""
try:
vod_id = ids[0]
if not vod_id.startswith('http'):
vod_id = self.host + vod_id
print(f"Fetching detail page: {vod_id}")
rsp = self.fetch(vod_id, headers=self.header())
soup = BeautifulSoup(rsp.text, 'html.parser')
vod = {
'vod_id': vod_id,
'vod_name': '',
'vod_pic': '',
'vod_actor': '',
'vod_director': '',
'vod_content': '',
'vod_play_from': '\u4e45\u4e45\u7f51',
'vod_play_url': ''
}
# \u6807\u9898
title = soup.find('h1')
if title:
vod['vod_name'] = title.get_text(strip=True)
# \u5c01\u9762
img = soup.find('meta', property='og:image')
if img and img.get('content'):
vod['vod_pic'] = img.get('content')
else:
img = soup.find('img', class_='card-img-top')
if img:
vod['vod_pic'] = img.get('src') or ''
# \u64ad\u653e\u5730\u5740 - \u67e5\u627evideo\u6807\u7b64\u6216iframe
play_url = ''
video = soup.find('video')
if video:
source = video.find('source')
if source and source.get('src'):
play_url = source.get('src')
if not play_url:
iframe = soup.find('iframe')
if iframe and iframe.get('src'):
play_url = iframe.get('src')
if play_url:
vod['vod_play_url'] = f'\u6b63\u7247${play_url}'
return {'list': [vod]}
except Exception as e:
print(f"Error in detailContent: {e}")
return {'list': []}
def searchContent(self, key, quick, pg=1):
"""\u641c\u7d22\u529f\u80fd"""
try:
search_url = f"{self.host}/node/search?q={urllib.parse.quote(keyword)}"
print(f"Search URL: {search_url}")
rsp = self.fetch(search_url, headers=self.header())
soup = BeautifulSoup(rsp.text, 'html.parser')
videos = []
items = soup.find_all('div', class_=lambda c: c and 'item' in c.split())
for item in items:
try:
links = item.find_all('a', href=True)
if len(links) < 2:
continue
title_link = links[1] if len(links) > 1 else links[0]
href = title_link.get('href', '')
if not href:
continue
if href.startswith('/'):
vod_id = self.host + href
else:
vod_id = href
vod_name = title_link.get_text(strip=True)
vod_pic = ''
img = item.find('img')
if img:
vod_pic = img.get('src') or img.get('data-src') or ''
if vod_pic and not vod_pic.startswith('http'):
if vod_pic.startswith('/'):
vod_pic = self.host + vod_pic
else:
vod_pic = 'https:' + vod_pic if vod_pic.startswith('//') else vod_pic
vod_remarks = ''
badge = item.find('span', class_='badge')
if badge:
vod_remarks = badge.get_text(strip=True)
if vod_name and vod_id:
videos.append({
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': vod_remarks
})
except Exception as e:
print(f"Error processing search item: {e}")
continue
return {'list': videos}
except Exception as e:
print(f"Error in searchContent: {e}")
return {'list': []}
def playerContent(self, flag, id, vipFlags):
"""\u8fd4\u56de\u64ad\u653e\u5730\u5740"""
return {
'parse': 0,
'playUrl': '',
'url': id
}
def isVideoFormat(self, url):
"""\u5224\u65ad\u662f\u5426\u4e3a\u89c6\u9891\u683c\u5f0f"""
video_extensions = ['.mp4', '.m3u8', '.flv', '.avi', '.mkv', '.wmv', '.mov']
lower_url = url.lower()
for ext in video_extensions:
if ext in lower_url:
return True
return False
def localProxy(self, param):
return None
def destroy(self):
pass
+375
View File
@@ -0,0 +1,375 @@
# coding=utf-8
#!/usr/bin/env python3
# @name 咖啡直播
# @author 转自 OmniBox JS
# @description 体育赛事录像回放 + 直播(足球/篮球/NBA)
# @version 2.0.0
import json
import requests
class Spider:
def getName(self):
return "咖啡直播"
def getDependence(self):
return []
def init(self, extend=""):
self.host = "https://kafeizhibo.com"
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Referer": "https://kafeizhibo.com/live/all"
}
def log(self, msg):
print("[咖啡直播] " + str(msg))
# =========================================================
# 工具
# =========================================================
def _normalize_url(self, path):
if not path:
return ""
if path.startswith("http"):
return path
if path.startswith("//"):
return "https:" + path
return self.host + ("" if path.startswith("/") else "/") + path
def _get(self, path, params=None, referer=None):
h = dict(self.headers)
if referer:
h["Referer"] = referer
resp = requests.get(self.host + path, headers=h, params=params, timeout=10)
return resp.json()
# =========================================================
# 直播部分
# =========================================================
def _fetch_live_all(self):
"""GET /api/v1/archor — 返回所有正在直播的频道"""
return self._get("/api/v1/archor", referer=self.host + "/live/all")
def _parse_live_list(self, items, category_filter=None):
"""
category: 1=足球, 2=篮球, None=全部
每个 archor 代表一个独立直播频道(同一场球可能有多个频道)
合并同 match_id 的频道到一个 vod,多线路在 detail 里处理
"""
# 按 room_id 去重(同一 room_id 只取第一个,避免重复)
seen_rooms = set()
result = []
for item in items:
if category_filter and item.get("category") != category_filter:
continue
room_id = str(item.get("room_id", ""))
if room_id in seen_rooms:
continue
seen_rooms.add(room_id)
home = item.get("home_team", "")
away = item.get("away_team", "")
league = item.get("league_name", "")
h_score = item.get("home_score", 0)
a_score = item.get("away_score", 0)
title = "{} vs {} ({})".format(home, away, league)
pic = self._normalize_url(item.get("screenshot", ""))
if not pic or "default" in pic:
mi = item.get("match_info") or {}
pic = mi.get("home_team_logo", "")
result.append({
"vod_id": "live_{}".format(room_id),
"vod_name": "🔴 " + title,
"vod_pic": pic,
"vod_remarks": "{} - {} | {}".format(h_score, a_score, item.get("name", "")),
})
return result
def _detail_live(self, room_id):
"""GET /api/v1/room/{room_id} — 获取直播间多线路"""
try:
data = self._get(
"/api/v1/room/{}".format(room_id),
referer=self.host + "/room/{}".format(room_id)
)
if data.get("code") != 200 or not data.get("data"):
return {"list": []}
d = data["data"]
room_info = d.get("room_info", {})
signals = d.get("signals", [])
home = room_info.get("home_team", "")
away = room_info.get("away_team", "")
league = room_info.get("league", "")
h_score = room_info.get("home_score", 0)
a_score = room_info.get("away_score", 0)
title = "{} vs {} ({})".format(home, away, league)
teams = d.get("teams", {})
pic = (teams.get("home") or {}).get("logo", "")
# 每条 signal 是一个线路(官方直播/原声直播)
episodes = []
for sig in signals:
url = sig.get("stream_url", "")
if url:
name = sig.get("name", "线路")
episodes.append("{}${}".format(name, url))
# 如果 signals 为空,fallback 到 archor
if not episodes:
archor = d.get("archor", {})
url = archor.get("stream_url", "")
if url:
episodes.append("{}${}".format(archor.get("name", "直播"), url))
vod = {
"vod_id": "live_{}".format(room_id),
"vod_name": "🔴 " + title,
"vod_pic": pic,
"vod_content": "{} {} vs {},比分 {} - {}".format(
league, home, away, h_score, a_score
),
"vod_play_from": "直播线路",
"vod_play_url": "#".join(episodes),
}
return {"list": [vod]}
except Exception as e:
self.log("直播详情失败: " + str(e))
return {"list": []}
# =========================================================
# 录像部分
# =========================================================
def _fetch_recordings(self, page=1, size=30, league=None, type_id=None):
params = {"page": page, "size": size}
if league:
params["league"] = league
elif type_id and type_id not in ("all", "nba", "live_all", "live_1", "live_2"):
params["type"] = type_id
h = dict(self.headers)
h["Referer"] = self.host + "/pc/replay"
resp = requests.get(self.host + "/api/v1/recordings", headers=h, params=params, timeout=10)
return resp.json()
def _parse_video_list(self, items):
result = []
for item in items:
title = "{} vs {} ({})".format(
item["home_team"], item["away_team"], item["league_name"]
)
score = "{} - {}".format(item["home_score"], item["away_score"])
pic = item.get("cover_image", "")
if pic and not pic.startswith("http"):
pic = self._normalize_url(pic)
if not pic or "default_cover" in pic:
pic = item.get("home_team_logo", "")
remarks = "{} | {} | {}个录像".format(
score, item["start_time"], item.get("recording_count", 0)
)
result.append({
"vod_id": str(item["match_id"]),
"vod_name": title,
"vod_pic": pic,
"vod_remarks": remarks,
})
return result
def _detail_recording(self, vid):
try:
h = dict(self.headers)
h["Referer"] = self.host + "/pc/replay"
resp = requests.get(
"{}/api/v1/match/{}/recordings".format(self.host, vid),
headers=h,
timeout=10
)
data = resp.json()
if data.get("code") != 200 or not data.get("data"):
return {"list": []}
match = data["data"]["match"]
replays = data["data"].get("replays", [])
highlights = data["data"].get("highlights", [])
title = "{} vs {} ({})".format(
match["home_team"], match["away_team"], match["league_name"]
)
pic = match.get("home_team_logo") or match.get("away_team_logo") or ""
episodes = []
for idx, rec in enumerate(replays):
if rec.get("video_url"):
name = rec.get("title") or "录像{}".format(idx + 1)
episodes.append("{}${}".format(name, rec["video_url"]))
for idx, rec in enumerate(highlights):
if rec.get("video_url"):
name = rec.get("title") or "集锦{}".format(idx + 1)
episodes.append("{}${}".format(name, rec["video_url"]))
vod = {
"vod_id": str(vid),
"vod_name": title,
"vod_pic": pic,
"vod_content": "{} {} {} vs {},比分 {} - {},比赛时间:{}".format(
match["league_name"], match.get("match_round", ""),
match["home_team"], match["away_team"],
match["home_score"], match["away_score"],
match["start_time"]
),
"vod_play_from": "录像源",
"vod_play_url": "#".join(episodes),
}
return {"list": [vod]}
except Exception as e:
self.log("录像详情失败: " + str(e))
return {"list": []}
# =========================================================
# FongMi 接口
# =========================================================
def homeContent(self, filter):
categories = [
# 直播分类
{"type_id": "live_all", "type_name": "🔴 直播全部"},
{"type_id": "live_1", "type_name": "🔴 直播足球"},
{"type_id": "live_2", "type_name": "🔴 直播篮球"},
# 录像分类
{"type_id": "all", "type_name": "录像全部"},
{"type_id": "1", "type_name": "录像足球"},
{"type_id": "2", "type_name": "录像篮球"},
{"type_id": "nba", "type_name": "录像NBA"},
]
# 首页展示直播列表
try:
data = self._fetch_live_all()
vod_list = self._parse_live_list(data.get("data", [])) if data.get("code") == 200 else []
except Exception as e:
self.log("首页失败: " + str(e))
vod_list = []
return {"class": categories, "list": vod_list}
def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg, filter, extend):
pg = int(pg) if pg else 1
# ---- 直播分类 ----
if tid in ("live_all", "live_1", "live_2"):
try:
data = self._fetch_live_all()
if data.get("code") == 200:
cat = None if tid == "live_all" else int(tid.split("_")[1])
vod_list = self._parse_live_list(data.get("data", []), category_filter=cat)
else:
vod_list = []
except Exception as e:
self.log("直播分类失败: " + str(e))
vod_list = []
return {"list": vod_list, "page": 1, "pagecount": 1, "limit": 100, "total": len(vod_list)}
# ---- 录像分类 ----
try:
if tid == "nba":
data = self._fetch_recordings(pg, 20, league="NBA")
size = 20
elif tid == "all":
data = self._fetch_recordings(pg, 30)
size = 30
else:
data = self._fetch_recordings(pg, 30, type_id=tid)
size = 30
vod_list = []
pagecount = 1
if data.get("code") == 200 and data.get("data"):
vod_list = self._parse_video_list(data["data"])
pagecount = pg + 1 if len(data["data"]) == size else pg
except Exception as e:
self.log("录像分类失败: " + str(e))
vod_list = []
pagecount = 1
return {"list": vod_list, "page": pg, "pagecount": pagecount, "limit": 30, "total": len(vod_list)}
def detailContent(self, ids):
vid = ids[0] if isinstance(ids, list) and ids else str(ids)
if vid.startswith("live_"):
room_id = vid[5:] # 去掉 "live_" 前缀
return self._detail_live(room_id)
else:
return self._detail_recording(vid)
def searchContent(self, key, quick, pg=1):
if not key:
return {"list": []}
keyword = key.lower()
result = []
# 搜索直播
try:
data = self._fetch_live_all()
if data.get("code") == 200:
for item in data.get("data", []):
if (keyword in item.get("home_team", "").lower()
or keyword in item.get("away_team", "").lower()
or keyword in item.get("league_name", "").lower()
or keyword in item.get("title", "").lower()):
room_id = str(item.get("room_id", ""))
home = item.get("home_team", "")
away = item.get("away_team", "")
league = item.get("league_name", "")
result.append({
"vod_id": "live_{}".format(room_id),
"vod_name": "🔴 {} vs {} ({})".format(home, away, league),
"vod_pic": "",
"vod_remarks": "直播中",
})
except Exception as e:
self.log("搜索直播失败: " + str(e))
# 搜索录像
try:
data = self._fetch_recordings(1, 100)
if data.get("code") == 200:
for item in data["data"]:
if (keyword in item["home_team"].lower()
or keyword in item["away_team"].lower()
or keyword in item["league_name"].lower()):
title = "{} vs {} ({})".format(
item["home_team"], item["away_team"], item["league_name"]
)
result.append({
"vod_id": str(item["match_id"]),
"vod_name": title,
"vod_pic": "",
"vod_remarks": "{} - {}".format(item["home_score"], item["away_score"]),
})
except Exception as e:
self.log("搜索录像失败: " + str(e))
return {"list": result, "page": 1, "pagecount": 1}
def playerContent(self, flag, id, vipFlags):
return {
"parse": 0,
"playUrl": "",
"url": id,
"header": json.dumps({
"User-Agent": self.headers["User-Agent"],
"Referer": self.host,
"Origin": self.host,
})
}
+343
View File
@@ -0,0 +1,343 @@
# coding = utf-8
# !/usr/bin/python
"""
"""
from Crypto.Util.Padding import unpad
from Crypto.Util.Padding import pad
from urllib.parse import unquote
from Crypto.Cipher import ARC4
from urllib.parse import quote
from base.spider import Spider
from Crypto.Cipher import AES
from bs4 import BeautifulSoup
from base64 import b64decode
import urllib.request
import urllib.parse
import binascii
import requests
import base64
import json
import time
import sys
import re
import os
sys.path.append('..')
xurl = "https://app.whjzjx.cn"
headers = {
'User-Agent': 'Linux; Android 12; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.101 Mobile Safari/537.36'
}
headerf = {
"platform": "1",
"user_agent": "Mozilla/5.0 (Linux; Android 9; V1938T Build/PQ3A.190705.08211809; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36",
"content-type": "application/json; charset=utf-8"
}
times = int(time.time() * 1000)
data = {
"device": "2a50580e69d38388c94c93605241fb306",
"package_name": "com.jz.xydj",
"android_id": "ec1280db12795506",
"install_first_open": True,
"first_install_time": 1752505243345,
"last_update_time": 1752505243345,
"report_link_url": "",
"authorization": "",
"timestamp": times
}
plain_text = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
key = "B@ecf920Od8A4df7"
key_bytes = key.encode('utf-8')
plain_bytes = plain_text.encode('utf-8')
cipher = AES.new(key_bytes, AES.MODE_ECB)
padded_data = pad(plain_bytes, AES.block_size)
ciphertext = cipher.encrypt(padded_data)
encrypted = base64.b64encode(ciphertext).decode('utf-8')
response = requests.post("https://u.shytkjgs.com/user/v3/account/login", headers=headerf, data=encrypted)
response_data = response.json()
Authorization = response_data['data']['token']
headerx = {
'authorization': Authorization,
'platform': '1',
'version_name': '3.8.3.1'
}
class Spider(Spider):
global xurl
global headerx
global headers
def getName(self):
return "首页"
def init(self, extend):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
if pl == 3:
plx = []
while True:
start_index = text.find(start_str)
if start_index == -1:
break
end_index = text.find(end_str, start_index + len(start_str))
if end_index == -1:
break
middle_text = text[start_index + len(start_str):end_index]
plx.append(middle_text)
text = text.replace(start_str + middle_text + end_str, '')
if len(plx) > 0:
purl = ''
for i in range(len(plx)):
matches = re.findall(start_index1, plx[i])
output = ""
for match in matches:
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
if match3:
number = match3.group(1)
else:
number = 0
if 'http' not in match[0]:
output += f"#{match[1]}${number}{xurl}{match[0]}"
else:
output += f"#{match[1]}${number}{match[0]}"
output = output[1:]
purl = purl + output + "$$$"
purl = purl[:-3]
return purl
else:
return ""
else:
start_index = text.find(start_str)
if start_index == -1:
return ""
end_index = text.find(end_str, start_index + len(start_str))
if end_index == -1:
return ""
if pl == 0:
middle_text = text[start_index + len(start_str):end_index]
return middle_text.replace("\\", "")
if pl == 1:
middle_text = text[start_index + len(start_str):end_index]
matches = re.findall(start_index1, middle_text)
if matches:
jg = ' '.join(matches)
return jg
if pl == 2:
middle_text = text[start_index + len(start_str):end_index]
matches = re.findall(start_index1, middle_text)
if matches:
new_list = [f'{item}' for item in matches]
jg = '$$$'.join(new_list)
return jg
def homeContent(self, filter):
result = {}
result = {"class": [{"type_id": "1", "type_name": "剧场"},
{"type_id": "3", "type_name": "新剧"},
{"type_id": "2", "type_name": "热播"},
{"type_id": "7", "type_name": "星选"},
{"type_id": "5", "type_name": "阳光"}],
}
return result
def homeVideoContent(self):
videos = []
url= f'{xurl}/v1/theater/home_page?theater_class_id=1&class2_id=4&page_num=1&page_size=24'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
if detail.status_code == 200:
data = detail.json()
for vod in data['data']['list']:
name = vod['theater']['title']
id = vod['theater']['id']
pic = vod['theater']['cover_url']
remark = vod['theater']['play_amount_str']
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result = {'list': videos}
return result
def categoryContent(self, cid, pg, filter, ext):
result = {}
videos = []
url = f'{xurl}/v1/theater/home_page?theater_class_id={cid}&page_num={pg}&page_size=24'
detail = requests.get(url=url,headers=headerx)
detail.encoding = "utf-8"
if detail.status_code == 200:
data = detail.json()
for vod in data['data']['list']:
name = vod['theater']['title']
id = vod['theater']['id']
pic = vod['theater']['cover_url']
remark = vod['theater']['theme']
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result = {'list': videos}
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
did = ids[0]
result = {}
videos = []
xianlu = ''
bofang = ''
url = f'{xurl}/v2/theater_parent/detail?theater_parent_id={did}'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
if detail.status_code == 200:
data = detail.json()
url = 'https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/1732707176882/jiduo.txt'
response = requests.get(url)
response.encoding = 'utf-8'
code = response.text
name = self.extract_middle_text(code, "s1='", "'", 0)
Jumps = self.extract_middle_text(code, "s2='", "'", 0)
content = '剧情:' + data['data']['introduction']
area = data['data']['desc_tags'][0]
remarks = data['data']['filing']
# 修复剧集只有一集的问题 - 检查theaters数据是否存在且不为空
if 'theaters' in data['data'] and data['data']['theaters']:
for sou in data['data']['theaters']:
id = sou['son_video_url']
name = sou['num']
bofang = bofang + str(name) + '$' + id + '#'
bofang = bofang[:-1] if bofang.endswith('#') else bofang
xianlu = '星芽'
else:
# 如果没有theaters数据,检查是否有单个视频URL
if 'video_url' in data['data'] and data['data']['video_url']:
bofang = '1$' + data['data']['video_url']
xianlu = '星芽'
else:
bofang = Jumps
xianlu = '1'
videos.append({
"vod_id": did,
"vod_content": content,
"vod_remarks": remarks,
"vod_area": area,
"vod_play_from": xianlu,
"vod_play_url": bofang
})
result['list'] = videos
return result
def playerContent(self, flag, id, vipFlags):
result = {}
result["parse"] = 0
result["playUrl"] = ''
result["url"] = id
result["header"] = headers
return result
def searchContentPage(self, key, quick, page):
result = {}
videos = []
payload = {
"text": key
}
url = f"{xurl}/v3/search"
detail = requests.post(url=url, headers=headerx, json=payload)
if detail.status_code == 200:
detail.encoding = "utf-8"
data = detail.json()
for vod in data['data']['theater']['search_data']:
name = vod['title']
id = vod['id']
pic = vod['cover_url']
remark = vod['score_str']
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result['list'] = videos
result['page'] = page
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def searchContent(self, key, quick, pg="1"):
return self.searchContentPage(key, quick, '1')
def localProxy(self, params):
if params['type'] == "m3u8":
return self.proxyM3u8(params)
elif params['type'] == "media":
return self.proxyMedia(params)
elif params['type'] == "ts":
return self.proxyTs(params)
return None
+392
View File
@@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
import re, urllib.parse
import json
from bs4 import BeautifulSoup
import requests
from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def init(self, extend=""):
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",
"Accept-Language": "zh-CN,zh;q=0.9",
}
def getName(self):
return '枫叶影院'
def homeContent(self, filter):
return {"class": [
{'type_id': "/label/qq", 'type_name': "腾讯VIP精选"},
{'type_id': "/label/bli", 'type_name': "B站VIP精选"},
{'type_id': "/label/youku", 'type_name': "优酷VIP精选"},
{"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):
area = [{"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": "其它"}]
year = [{"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": "2010", "v": "2010"},
{"n": "2009", "v": "2009"}, {"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"},
{"n": "2006", "v": "2006"}, {"n": "2005", "v": "2005"}, {"n": "2004", "v": "2004"}]
lang = [{"n": "全部", "v": ""}, {"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"}, {"n": "韩语", "v": "韩语"},
{"n": "日语", "v": "日语"}, {"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
{"n": "其它", "v": "其它"}]
sort = [{"n": "时间", "v": "time"}, {"n": "人气", "v": "hits"}, {"n": "评分", "v": "score"}]
letter = [{"n": "全部", "v": ""}, {"n": "A", "v": "A"}, {"n": "B", "v": "B"}, {"n": "C", "v": "C"},
{"n": "D", "v": "D"}, {"n": "E", "v": "E"}, {"n": "F", "v": "F"}, {"n": "G", "v": "G"},
{"n": "H", "v": "H"}, {"n": "I", "v": "I"}, {"n": "J", "v": "J"}, {"n": "K", "v": "K"},
{"n": "L", "v": "L"}, {"n": "M", "v": "M"}, {"n": "N", "v": "N"}, {"n": "O", "v": "O"},
{"n": "P", "v": "P"}, {"n": "Q", "v": "Q"}, {"n": "R", "v": "R"}, {"n": "S", "v": "S"},
{"n": "T", "v": "T"}, {"n": "U", "v": "U"}, {"n": "V", "v": "V"}, {"n": "W", "v": "W"},
{"n": "X", "v": "X"}, {"n": "Y", "v": "Y"}, {"n": "Z", "v": "Z"}, {"n": "0-9", "v": "0-9"}]
return {
"2": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "日韩剧", "v": "15"},
{"n": "海外剧", "v": "16"}]},
{"key": "area", "name": "地区", "value": area},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("古装", "古装"), ("战争", "战争"),
("青春偶像", "青春偶像"), ("喜剧", "喜剧"),
("家庭", "家庭"), ("犯罪", "犯罪"), ("动作", "动作"),
("奇幻", "奇幻"), ("剧情", "剧情"), ("历史", "历史"),
("经典", "经典"), ("乡村", "乡村"), ("情景", "情景"),
("商战", "商战"), ("网剧", "网剧"), ("其他", "其他")]]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"1": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
{"n": "恐怖片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "爱情片", "v": "10"},
{"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}, {"n": "纪录片", "v": "20"}]},
{"key": "area", "name": "地区", "value": area},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("喜剧", "喜剧"), ("爱情", "爱情"),
("恐怖", "恐怖"), ("动作", "动作"), ("科幻", "科幻"),
("剧情", "剧情"), ("战争", "战争"), ("警匪", "警匪"),
("犯罪", "犯罪"), ("动画", "动画"), ("奇幻", "奇幻"),
("武侠", "武侠"), ("冒险", "冒险"), ("枪战", "枪战"),
("悬疑", "悬疑"), ("惊悚", "惊悚"), ("经典", "经典"),
("青春", "青春"), ("文艺", "文艺"), ("微电影", "微电影"),
("古装", "古装"), ("历史", "历史"), ("运动", "运动"),
("农村", "农村"), ("儿童", "儿童"),
("网络电影", "网络电影")]]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"4": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "4"}, {"n": "国产动漫", "v": "25"}, {"n": "日韩动漫", "v": "26"}]},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("情感", "情感"), ("科幻", "科幻"),
("热血", "热血"), ("推理", "推理"), ("搞笑", "搞笑"),
("冒险", "冒险"), ("奇幻", "奇幻"), ("战斗", "战斗"),
("校园", "校园"), ("萝莉", "萝莉"), ("治愈", "治愈"),
("原创", "原创"), ("亲子", "亲子"), ("益智", "益智"),
("励志", "励志"), ("其他", "其他")]]},
{"key": "area", "name": "地区",
"value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
{"n": "日本", "v": "日本"}, {"n": "法国", "v": "法国"}, {"n": "英国", "v": "英国"},
{"n": "其它", "v": "其它"}]},
{"key": "year", "name": "年份", "value": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
"3": [
{"key": "class", "name": "类型",
"value": [{"n": "全部", "v": "3"}, {"n": "大陆综艺", "v": "21"}, {"n": "日韩综艺", "v": "22"}]},
{"key": "genre", "name": "剧情", "value": [{"n": v[0], "v": v[1]} for v in
[("全部", ""), ("选秀", "选秀"), ("情感", "情感"),
("访谈", "访谈"), ("播报", "播报"), ("音乐", "音乐"),
("美食", "美食"), ("旅游", "旅游"), ("搞笑", "搞笑"),
("游戏", "游戏"), ("亲子", "亲子"), ("其它", "其它")]]},
{"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": year},
{"key": "lang", "name": "语言", "value": lang},
{"key": "letter", "name": "字母", "value": letter},
{"key": "sort", "name": "排序", "value": sort},
],
}
def homeVideoContent(self):
html = self._fetch('/')
return {"list": self._parse_video_list(html)}
def categoryContent(self, tid, pg, filter, extend):
# 构建筛选参数:参照歪比巴卜,直接取extend里的值,fallback到filter
if tid.startswith('/label'):
url = f'{tid}/page/{pg}.html'
html = self._fetch(url)
items = self._parse_video_list(html)
page = int(pg)
page_count = page if len(items) < 24 else page + 2
return {"list": items, "page": page, "pagecount": page_count, "limit": 24, "total": page_count * 24}
args = {}
if extend and isinstance(extend, dict):
for k, v in extend.items():
if v:
args[k] = str(v)
if isinstance(filter, dict):
for k, v in filter.items():
if v and k not in args:
args[k] = str(v)
route_tid = args.get('class', args.get('tid', str(tid)))
area = args.get('area', '')
genre = args.get('genre', '')
year = args.get('year', '')
lang = args.get('lang', '')
letter = args.get('letter', '')
sort = args.get('sort', '')
# 无筛选走正常分页
if not area and not genre and not year and not lang and not letter and not sort:
url = f'/cupfox-list/{route_tid}--------{pg}---.html'
html = self._fetch(url)
items = self._parse_video_list(html)
page = int(pg)
soup = BeautifulSoup(html, 'html.parser')
pagecount = page
for a in soup.select('a.page-link'):
if a.text == '尾页':
m = re.search(r'---(\d+)---', a.get('href', ''))
if m:
pagecount = int(m.group(1))
break
if not items:
pagecount = 0
return {"list": items, "page": page, "pagecount": pagecount, "limit": 36, "total": 9999}
# 有筛选:{tid}-{area}-{sort}-{genre}-{lang}-{letter}------{year}.html
segs = [route_tid, area, sort, genre, lang, letter, '', '', year]
url = '/cupfox-list/' + '-'.join(segs) + '.html'
html = self._fetch(url)
items = self._parse_video_list(html)
return {"list": items, "page": 1, "pagecount": 1, "limit": 36, "total": 9999}
def detailContent(self, ids):
result = {"list": []}
vid = ids[0].split(',')[0].strip()
try:
html = self._fetch(f'/detail/{vid}.html')
if not html: return result
soup = BeautifulSoup(html, 'html.parser')
vod_name = soup.select_one('h3.slide-info-title')
vod_name = vod_name.text.strip() if vod_name else ''
vod_pic = soup.select_one('img.lazy')
vod_pic = self._fix_pic(vod_pic.get('data-src', '')) if vod_pic else ''
vod_director = ''
vod_actor = ''
for el in soup.select('.slide-info'):
text = el.get_text(' ').strip()
if text.startswith('导演:'):
vod_director = text.replace('导演:', '').strip()
elif text.startswith('演员:'):
vod_actor = text.replace('演员:', '').strip()
vod_content = soup.select_one('#height_limit')
vod_content = vod_content.get_text(' ', strip=True) if vod_content else ''
play_from, play_url = [], []
for tab in soup.select('.anthology-tab a.swiper-slide'):
src_name = re.sub(r'<[^>]+>', '', str(tab)).strip() or tab.get_text(' ', strip=True).strip()
if src_name:
play_from.append(src_name)
tab_blocks = soup.select('.anthology-list-box')
for i, block in enumerate(tab_blocks):
ep_list = []
for a in block.select('li a'):
href = a.get('href', '')
m = re.search(r'/play/(.*?)\.html', href)
if m:
ep_list.append(f'{a.text.strip()}${vid}-{m.group(1)}')
ep_list.reverse()
if ep_list and i < len(play_from):
play_url.append('#'.join(ep_list))
valid_from = [pf for i, pf in enumerate(play_from) if i < len(play_url)]
result["list"].append({
"vod_id": vid, "vod_name": vod_name, "vod_pic": vod_pic,
"vod_director": vod_director, "vod_actor": vod_actor,
"vod_content": vod_content,
"vod_play_from": "$$$".join(valid_from),
"vod_play_url": "$$$".join(play_url),
})
except:
pass
return result
def searchContent(self, key, quick, pg="1"):
try:
decoded = urllib.parse.unquote(key)
except:
decoded = key
html = self._fetch(f'/cupfox-search/{urllib.parse.quote(decoded)}----------{pg}---.html')
items = self._parse_search_list(html)
return {"list": items, "page": int(pg), "pagecount": 1, "limit": 36, "total": len(items)}
def playerContent(self, flag, id, vipFlags):
url = ''
try:
url = id if id.startswith('http') else f'{self.host}/play/{id}.html'
html = self._fetch(url)
if html:
m = re.search(r'player_aaaa=(.*?)</script>', html, re.S)
if m:
try:
pd = json.loads(m.group(1))
except Exception as e:
print(e)
pd = {}
# print('pd:', pd)
play_url = pd.get('url')
play_id = pd.get('from')
api_map = {
'YYNB': 'https://zzrs.mfdyvip.com/player/mplayer.php',
'JD4K': 'https://fgsrg.hzqingshan.com/player/mplayer.php',
}
if not play_url:
return {"parse": 0, "url": 'https://php.doube.eu.org/error.m3u8',
"header": {'User-Agent': 'Mozilla/5.0'}}
if play_url.startswith('http') and (play_url.endswith('.m3u8') or play_url.endswith('.mp4')):
return {"parse": 0, "url": play_url, "header": {'User-Agent': 'Mozilla/5.0'}}
else:
headers = {
'User-Agent': "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
'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",
'cache-control': "no-cache",
'pragma': "no-cache",
'priority': "u=0, i",
'referer': "https://www.ht10010.com/",
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.get(f"https://fgsrg.hzqingshan.com/player/?url={play_url}", headers=headers)
token = re.search(r'data-te="(.*?)"', response.text)
if token:
token = token.group(1)
payload = {
'url': play_url,
'token': token
}
# print('payload', payload)
try:
response = self.post(api_map[play_id], data=payload, headers=headers)
response.raise_for_status()
result = response.json()
# print('result:', result)
if result['code'] == 200 and 'url' in result:
play_url = result['url']
return {"parse": 0, "url": play_url, "header": {
'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'}}
except Exception as e:
print(e)
except Exception as e:
print(e)
return {"parse": 1, "url": url}
def localProxy(self, param=''):
return {}
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def _fetch(self, url):
try:
if not url.startswith('http'):
url = self.host + url
rsp = self.fetch(url, headers=self.headers)
return rsp.text if rsp else ''
except:
return ''
def _fix_pic(self, u):
if not u: return ''
if u.startswith('//'): return 'https:' + u
return u.replace('&amp;', '&')
def _parse_video_list(self, html):
videos, seen = [], set()
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('a.public-list-exp')
for a in cards:
href = a.get('href', '')
m = re.search(r'/detail/(\d+)\.html', href)
if not m: continue
vod_id = m.group(1)
if vod_id in seen: continue
seen.add(vod_id)
span = ','.join([span.text for span in a.select('span.public-prt')])
# print('span', span)
vod_name = a.get('title', '') or (a.select_one('img') and a.select_one('img').get('alt', '')) or ''
pic_el = a.select_one('img')
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
remark_el = a.select_one('.ft2') or a.select_one('.public-list-prb')
vod_remarks = remark_el.text.strip() if remark_el else ''
videos.append(
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks, "vod_year": span})
return videos
def _parse_search_list(self, html):
videos, seen = [], set()
soup = BeautifulSoup(html, 'html.parser')
cards = soup.select('a.public-list-exp')
for a in cards:
href = a.get('href', '')
m = re.search(r'/detail/(\d+)\.html', href)
if not m: continue
vod_id = m.group(1)
if vod_id in seen: continue
seen.add(vod_id)
pic_el = a.select_one('img')
vod_pic = self._fix_pic(pic_el.get('data-src', '')) if pic_el else ''
title_el = soup.select_one(f'a.thumb-txt[href="/detail/{vod_id}.html"]')
if title_el:
vod_name = title_el.text.strip()
else:
vod_name = a.select_one('img') and a.select_one('img').get('alt', '') or ''
remark_el = a.select_one('.public-list-prb') or a.select_one('.ft2')
vod_remarks = remark_el.text.strip() if remark_el else ''
videos.append(
{"vod_id": vod_id, "vod_name": vod_name.strip(), "vod_pic": vod_pic, "vod_remarks": vod_remarks})
return videos
if __name__ == '__main__':
sp = Spider()
sp.init()
# 20067-5-189
print(sp.categoryContent('/label/qq','1',True, {}))
# print(sp.playerContent('', '20067-6-189', []))
# print(sp.playerContent('', '20067-5-189', []))
pass
+380
View File
@@ -0,0 +1,380 @@
# -*- coding: utf-8 -*-
import requests
import re
import json
import traceback
import sys
from urllib.parse import quote
sys.path.append('../../')
try:
from base.spider import Spider
except ImportError:
# 定义一个基础接口类,用于本地测试
class Spider:
def init(self, extend=""):
pass
class Spider(Spider):
def __init__(self):
self.siteUrl = "https://www.kuaikaw.cn"
self.cateManual = {
"甜宠": "462",
"古装仙侠": "1102",
"现代言情": "1145",
"青春": "1170",
"豪门恩怨": "585",
"逆袭": "417-464",
"重生": "439-465",
"系统": "1159",
"总裁": "1147",
"职场商战": "943"
}
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 Edg/120.0.0.0",
"Referer": self.siteUrl,
"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"
}
def getName(self):
return "河马短剧"
def init(self, extend=""):
return
def fetch(self, url, headers=None, retry=2):
"""统一的网络请求接口"""
if headers is None:
headers = self.headers
for i in range(retry + 1):
try:
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
response.raise_for_status()
return response
except Exception as e:
if i == retry:
print(f"请求异常: {url}, 错误: {str(e)}")
return None
continue
def isVideoFormat(self, url):
video_formats = ['.mp4', '.mkv', '.avi', '.wmv', '.m3u8', '.flv', '.rmvb']
return any(format in url.lower() for format in video_formats)
def manualVideoCheck(self):
return False
def homeContent(self, filter):
result = {}
classes = [{'type_name': k, 'type_id': v} for k, v in self.cateManual.items()]
result['class'] = classes
try:
result['list'] = self.homeVideoContent()['list']
except:
result['list'] = []
return result
def homeVideoContent(self):
videos = []
try:
response = self.fetch(self.siteUrl)
if not response:
return {'list': []}
html_content = response.text
next_data_pattern = r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>'
next_data_match = re.search(next_data_pattern, html_content, re.DOTALL)
if not next_data_match:
return {'list': []}
next_data_json = json.loads(next_data_match.group(1))
page_props = next_data_json.get("props", {}).get("pageProps", {})
# 处理轮播图数据
if "bannerList" in page_props:
for banner in page_props["bannerList"]:
if banner.get("bookId"):
videos.append({
"vod_id": f"/drama/{banner['bookId']}",
"vod_name": banner.get("bookName", ""),
"vod_pic": banner.get("coverWap", ""),
"vod_remarks": f"{banner.get('statusDesc', '')} {banner.get('totalChapterNum', '')}".strip()
})
# 处理SEO分类推荐
if "seoColumnVos" in page_props:
for column in page_props["seoColumnVos"]:
for book in column.get("bookInfos", []):
if book.get("bookId"):
videos.append({
"vod_id": f"/drama/{book['bookId']}",
"vod_name": book.get("bookName", ""),
"vod_pic": book.get("coverWap", ""),
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}".strip()
})
# 去重处理
seen = set()
unique_videos = []
for video in videos:
key = (video["vod_id"], video["vod_name"])
if key not in seen:
seen.add(key)
unique_videos.append(video)
except Exception as e:
print(f"获取首页推荐内容出错: {e}")
unique_videos = []
return {'list': unique_videos}
def categoryContent(self, tid, pg, filter, extend):
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
url = f"{self.siteUrl}/browse/{tid}/{pg}"
response = self.fetch(url)
if not response:
return result
html_content = response.text
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
if not next_data_match:
return result
try:
next_data_json = json.loads(next_data_match.group(1))
page_props = next_data_json.get("props", {}).get("pageProps", {})
current_page = page_props.get("page", 1)
total_pages = page_props.get("pages", 1)
book_list = page_props.get("bookList", [])
videos = []
for book in book_list:
if book.get("bookId"):
videos.append({
"vod_id": f"/drama/{book['bookId']}",
"vod_name": book.get("bookName", ""),
"vod_pic": book.get("coverWap", ""),
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}".strip()
})
result.update({
'list': videos,
'page': int(current_page),
'pagecount': total_pages,
'limit': len(videos),
'total': len(videos) * total_pages if videos else 0
})
except Exception as e:
print(f"分类内容获取出错: {e}")
return result
def searchContent(self, key, quick, pg=1):
return self.searchContentPage(key, quick, pg)
def searchContentPage(self, key, quick, pg=1):
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
search_url = f"{self.siteUrl}/search?searchValue={quote(key)}&page={pg}"
response = self.fetch(search_url)
if not response:
return result
html_content = response.text
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
if not next_data_match:
return result
try:
next_data_json = json.loads(next_data_match.group(1))
page_props = next_data_json.get("props", {}).get("pageProps", {})
total_pages = page_props.get("pages", 1)
book_list = page_props.get("bookList", [])
videos = []
for book in book_list:
if book.get("bookId"):
videos.append({
"vod_id": f"/drama/{book['bookId']}",
"vod_name": book.get("bookName", ""),
"vod_pic": book.get("coverWap", ""),
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}".strip()
})
result.update({
'list': videos,
'pagecount': total_pages,
'total': len(videos) * total_pages if videos else 0
})
except Exception as e:
print(f"搜索内容出错: {e}")
return result
def detailContent(self, ids):
result = {'list': []}
if not ids:
return result
vod_id = ids[0]
if not vod_id.startswith('/drama/'):
vod_id = f'/drama/{vod_id}'
drama_url = f"{self.siteUrl}{vod_id}"
response = self.fetch(drama_url)
if not response:
return result
html = response.text
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.DOTALL)
if not next_data_match:
return result
try:
next_data = json.loads(next_data_match.group(1))
page_props = next_data.get("props", {}).get("pageProps", {})
book_info = page_props.get("bookInfoVo", {})
chapter_list = page_props.get("chapterList", [])
if not book_info.get("bookId"):
return result
# 基本信息
categories = [c.get("name", "") for c in book_info.get("categoryList", [])]
performers = [p.get("name", "") for p in book_info.get("performerList", [])]
vod = {
"vod_id": vod_id,
"vod_name": book_info.get("title", ""),
"vod_pic": book_info.get("coverWap", ""),
"type_name": ",".join(categories),
"vod_year": "",
"vod_area": book_info.get("countryName", ""),
"vod_remarks": f"{book_info.get('statusDesc', '')} {book_info.get('totalChapterNum', '')}".strip(),
"vod_actor": ", ".join(performers),
"vod_director": "",
"vod_content": book_info.get("introduction", "")
}
# 处理剧集
play_urls = self.processEpisodes(vod_id, chapter_list)
if play_urls:
vod['vod_play_from'] = '河马剧场'
vod['vod_play_url'] = '$$$'.join(play_urls)
result['list'] = [vod]
except Exception as e:
print(f"详情页解析出错: {e}")
traceback.print_exc()
return result
def processEpisodes(self, vod_id, chapter_list):
play_urls = []
episodes = []
for chapter in chapter_list:
chapter_id = chapter.get("chapterId", "")
chapter_name = chapter.get("chapterName", "")
if not chapter_id or not chapter_name:
continue
# 尝试获取直接视频链接
video_url = self.getDirectVideoUrl(chapter)
if video_url:
episodes.append(f"{chapter_name}${video_url}")
continue
# 回退方案
episodes.append(f"{chapter_name}${vod_id}${chapter_id}${chapter_name}")
if episodes:
play_urls.append("#".join(episodes))
return play_urls
def getDirectVideoUrl(self, chapter):
if "chapterVideoVo" not in chapter or not chapter["chapterVideoVo"]:
return None
video_info = chapter["chapterVideoVo"]
for key in ["mp4", "mp4720p", "vodMp4Url"]:
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
return video_info[key]
return None
def playerContent(self, flag, id, vipFlags):
result = {
"parse": 0,
"url": id,
"header": json.dumps(self.headers)
}
# 如果已经是视频链接直接返回
if 'http' in id and ('.mp4' in id or '.m3u8' in id):
return result
# 解析参数
parts = id.split('$')
if len(parts) < 2:
return result
drama_id = parts[0].replace('/drama/', '')
chapter_id = parts[1]
# 尝试获取视频链接
video_url = self.getEpisodeVideoUrl(drama_id, chapter_id)
if video_url:
result["url"] = video_url
return result
def getEpisodeVideoUrl(self, drama_id, chapter_id):
episode_url = f"{self.siteUrl}/episode/{drama_id}/{chapter_id}"
response = self.fetch(episode_url)
if not response:
return None
html = response.text
# 方法1: 从NEXT_DATA提取
next_data_match = re.search(r'<script id="__NEXT_DATA__".*?>(.*?)</script>', html, re.DOTALL)
if next_data_match:
try:
next_data = json.loads(next_data_match.group(1))
page_props = next_data.get("props", {}).get("pageProps", {})
chapter_info = page_props.get("chapterInfo", {})
if chapter_info and "chapterVideoVo" in chapter_info:
video_info = chapter_info["chapterVideoVo"]
for key in ["mp4", "mp4720p", "vodMp4Url"]:
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
return video_info[key]
except:
pass
# 方法2: 直接从HTML提取
mp4_matches = re.findall(r'(https?://[^"\']+\.mp4)', html)
if mp4_matches:
for url in mp4_matches:
if chapter_id in url or drama_id in url:
return url
return mp4_matches[0]
return None
def localProxy(self, param):
return [200, "video/MP2T", {}, param]
def destroy(self):
pass
+199
View File
@@ -0,0 +1,199 @@
#Kyele
import sys
import json
import time
import re
import requests
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def __init__(self):
super().__init__()
self.base = 'https://www.pandalive.co.kr'; self.api = 'https://api.pandalive.co.kr'; self.session = requests.Session()
self.ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
self.common_headers = {'User-Agent': self.ua, 'Accept': 'application/json, text/plain, */*', 'Origin': self.base, 'Referer': self.base + '/'}
self.x_device_info = {"t": "webPc", "v": "1.0", "ui": "0", "ck": {"sessKeyAsp": ""}}; self.extra_cookie = ''
def init(self, extend=""):
try:
if extend:
cfg = extend if isinstance(extend, dict) else json.loads(extend)
self.x_device_info = cfg.get('x_device_info', self.x_device_info); self.extra_cookie = cfg.get('cookie', '')
except Exception: pass
try:
self.session.headers.update(self.common_headers)
if self.extra_cookie: self.session.headers['Cookie'] = self.extra_cookie
self.session.get(self.base, timeout=8); self._app_token()
except Exception: pass
return self
def getName(self): return 'PandaLive'
def isVideoFormat(self, url): return url.endswith('.m3u8') or url.endswith('.mp4')
def manualVideoCheck(self): return False
def destroy(self):
try: self.session.close()
except Exception: pass
def _app_token(self):
headers = self._with_x_device_info(dict(self.session.headers))
return self.session.get(f'{self.api}/v1/member/app_token', headers=headers, timeout=8)
def _list_live(self, page=None, page_size=None, order_by='user', only_new='N'):
if page_size is None: page_size = 60
limit = page_size; offset = 0 if page is None else max(0, (page - 1) * limit)
headers = self._with_x_device_info(dict(self.session.headers))
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
data = {'orderBy': order_by, 'onlyNewBj': only_new, 'limit': str(limit), 'offset': str(offset)}
try:
r = self.session.post(f'{self.api}/v1/live', data=data, headers=headers, timeout=8)
j = {}
try: j = r.json()
except Exception: pass
if isinstance(j, dict) and isinstance(j.get('list'), list) and len(j['list']) > 0: return j
except Exception: pass
try: return self.session.get(f'{self.api}/v1/live', timeout=8).json()
except Exception: return {}
def _list_live_page(self, page, page_size, order_by='user', only_new='N'):
j = self._list_live(page=page, page_size=page_size, order_by=order_by, only_new=only_new)
return j.get('list', []) if isinstance(j, dict) else []
def _list_live_aggregate(self, max_pages=5, page_size=60, min_expect=60, order_by='user', only_new='N'):
try:
cache_key = f'pandalive_agg_v2_{order_by}_{only_new}'; cached = self.getCache(cache_key)
if isinstance(cached, dict) and isinstance(cached.get('list'), list): return cached['list']
except Exception: pass
seen = set(); result = []
first = self._list_live(page=None, page_size=page_size, order_by=order_by, only_new=only_new)
items = first.get('list', []) if isinstance(first, dict) else []
for it in items:
code = it.get('code') or it.get('userId')
if code and code not in seen: seen.add(code); result.append(it)
p = 1
while len(result) < min_expect and p <= max_pages:
page_items = self._list_live_page(page=p, page_size=page_size, order_by=order_by, only_new=only_new)
added = 0
for it in page_items:
code = it.get('code') or it.get('userId')
if code and code not in seen: seen.add(code); result.append(it); added += 1
if added == 0 and p > 1: break
p += 1
try:
payload = {"expiresAt": int(time.time()) + 20, "list": result}
self.setCache(f'pandalive_agg_v2_{order_by}_{only_new}', payload)
except Exception: pass
return result
def _live_play(self, play_id):
body = {'play_id': play_id, 'device': 'webPc', 'player': 'ivs'}
headers = self._with_x_device_info(dict(self.session.headers))
headers['Content-Type'] = 'application/json'; headers['Referer'] = f'{self.base}/play/{play_id.split("_")[0]}'
r = self.session.post(f'{self.api}/v1/live/play', headers=headers, data=json.dumps(body), timeout=8)
if r.status_code != 200:
try: self._app_token()
except Exception: pass
r = self.session.post(f'{self.api}/v1/live/play', headers=headers, data=json.dumps(body), timeout=8)
try: return r.json()
except Exception: return {'result': False, 'status': r.status_code, 'text': r.text}
def _with_x_device_info(self, headers):
try: headers['x-device-info'] = json.dumps(self.x_device_info, separators=(',', ':'))
except Exception: headers['x-device-info'] = '{"t":"webPc","v":"1.0","ui":"0","ck":{"sessKeyAsp":""}}'
return headers
def homeContent(self, filter):
classes = [{"type_name": "LIVE", "type_id": "live"}]
if filter:
filters = {"live": [{"key": "sort", "value": [
{"n": "观看次数", "v": "user-N"}, {"n": "热门", "v": "hot-N"},
{"n": "最新", "v": "new-N"}, {"n": "新人", "v": "user-Y"}
]}]}
else: filters = {}
return {"class": classes, "filters": filters}
def homeVideoContent(self):
items = self._list_live_aggregate(max_pages=3, page_size=60, min_expect=48, order_by='user', only_new='N')
if not items:
items = (self._list_live().get('list', []))
return {"list": [self._to_vod(it) for it in items[:48]]}
def categoryContent(self, tid, pg, filter, extend):
page_size = 24; p = 1
try: p = int(pg)
except Exception: pass
order_by, only_new = 'user', 'N'
try:
if isinstance(extend, str):
try: extend = json.loads(extend) if extend.strip().startswith('{') else {}
except Exception: extend = {}
if isinstance(extend, dict):
s = extend.get('sort')
if isinstance(s, str) and '-' in s:
ab = s.split('-', 1)
if len(ab) == 2: order_by, only_new = (ab[0] or 'user'), (ab[1] or 'N')
if (order_by, only_new) == ('user', 'N') and (tid or '').lower() != 'live':
order_by, only_new = self._tid_to_sort(tid)
except Exception: pass
server_items = self._list_live_page(page=p, page_size=page_size, order_by=order_by, only_new=only_new)
if len(server_items) >= page_size:
return {"page": p, "pagecount": 99999, "limit": page_size, "total": 999999, "list": [self._to_vod(it) for it in server_items]}
all_items = self._list_live_aggregate(max_pages=12, page_size=100, min_expect=120, order_by=order_by, only_new=only_new)
total = len(all_items)
if total <= 0:
base_list = self._list_live().get('list', [])
total = len(base_list)
if total <= 0: return {"page": p, "pagecount": 1, "limit": page_size, "total": 0, "list": []}
start = (p - 1) * page_size; end = start + page_size
part = base_list[start:end]
videos = [self._to_vod(it) for it in part]
return {"page": p, "pagecount": (total + page_size - 1)//page_size, "limit": page_size, "total": total, "list": videos}
start = ((p - 1) * page_size) % total; part = []
for i in range(page_size): part.append(all_items[(start + i) % total])
return {"page": p, "pagecount": 99999, "limit": page_size, "total": 999999, "list": [self._to_vod(it) for it in part]}
def _tid_to_sort(self, tid):
t = (tid or '').lower()
if t == 'live_newbj': return 'user', 'Y'
if t == 'live_hot': return 'hot', 'N'
if t == 'live_new': return 'new', 'N'
return 'user', 'N'
def detailContent(self, ids):
vid = ids[0]; parts = vid.split('|'); play_id = parts[0]
user_id = parts[1] if len(parts) > 1 else play_id.split('_')[0]
title = parts[2] if len(parts) > 2 else user_id
vod = {"vod_id": vid, "vod_name": title, "vod_pic": "", "type_name": "LIVE", "vod_year": "", "vod_area": "", "vod_remarks": "PandaLive", "vod_actor": "", "vod_director": "", "vod_content": title, "vod_play_from": "IVS", "vod_play_url": f"直播${vid}"}
return {"list": [vod]}
def searchContent(self, key, quick, pg="1"):
items = self._list_live().get('list', [])
key_l = key.lower(); result = []
for it in items:
title = str(it.get('title', '')); user_id = str(it.get('userId', '')); user_nick = str(it.get('userNick', ''))
if key_l in title.lower() or key_l in user_id.lower() or key_l in user_nick.lower():
result.append(self._to_vod(it))
return {"list": result, "page": 1}
def playerContent(self, flag, id, vipFlags):
try:
vid = id; parts = vid.split('|'); play_id = parts[0]
j = self._live_play(play_id); m3u8 = self._find_first_m3u8(j) if isinstance(j, dict) else ''
if not m3u8: return {"parse": 1, "playUrl": "", "url": f"{self.base}/play/{play_id.split('_')[0]}", "header": self._play_headers()}
return {"parse": 0, "playUrl": "", "url": m3u8, "header": self._play_headers()}
except Exception: return {"parse": 1, "playUrl": "", "url": f"{self.base}", "header": self._play_headers()}
def liveContent(self, url):
try:
play_id = url; j = self._live_play(play_id)
m3u8 = self._find_first_m3u8(j)
if m3u8: return {"parse": 0, "url": m3u8, "header": self._play_headers()}
except Exception: pass
return {"parse": 1, "url": f"{self.base}/play/{url}", "header": self._play_headers()}
def localProxy(self, param):
action = param.get('action') if isinstance(param, dict) else None
if action == 'play':
play_id = param.get('play_id', ''); j = self._live_play(play_id)
m3u8 = self._find_first_m3u8(j)
if m3u8: return self._redirect(m3u8)
return None
def _redirect(self, url):
return {"code": 302, "headers": {"Location": url}}
def _find_first_m3u8(self, obj):
try:
text = json.dumps(obj, ensure_ascii=False)
m = re.search(r'https?://[^\s"\\]+\.m3u8[^\s"\\]*', text)
if m: return m.group(0)
except Exception: pass
return ''
def _play_headers(self):
return {'User-Agent': self.ua, 'Referer': self.base + '/', 'Origin': self.base}
def _to_vod(self, it):
title = it.get('title') or it.get('userNick') or it.get('userId') or 'LIVE'
pic = it.get('thumbUrl') or it.get('ivsThumbnail') or ''
user_id = it.get('userId', ''); play_id = it.get('code', user_id); vod_id = f"{play_id}|{user_id}|{title}"
remarks = f"观众 {it.get('user', 0)} | 点赞 {it.get('likeCnt', 0)}"
return {'vod_id': vod_id, 'vod_name': title, 'vod_pic': pic, 'vod_remarks': remarks}
+72
View File
@@ -0,0 +1,72 @@
#coding=utf-8
#!/usr/bin/python
import sys
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def init(self,extend=""):
self.base_url='http://api.hclyz.com:81/mf'
def homeContent(self,filter):
classes = [{"type_name": "色播聚合","type_id":"/json.txt"}]
result = {"class": classes}
return result
def categoryContent(self,tid,pg,filter,extend):
home = self.fetch(f'{self.base_url}/json.txt').json()
data = home.get("pingtai")[1:]
videos = [
{
"vod_id": "/" + item['address'],
"vod_name": item['title'],
"vod_pic": item['xinimg'].replace("http://cdn.gcufbd.top/img/",
"https://slink.ltd/https://raw.githubusercontent.com/fish2018/lib/refs/heads/main/imgs/"),
"vod_remarks": item['Number'],
"style": {"type": "rect", "ratio": 1.33}
} for item in sorted(data, key=lambda x: int(x['Number']), reverse=True)
]
result = {
"page": pg,
"pagecount": 1,
"limit": len(videos),
"total": len(videos),
"list": videos
}
return result
def detailContent(self,array):
id = array[0]
data = self.fetch(f'{self.base_url}/{id}').json()
zhubo = data['zhubo']
playUrls = '#'.join([f"{vod['title']}${vod['address']}" for vod in zhubo])
vod = [{
"vod_play_from": 'sebo',
"vod_play_url": playUrls,
"vod_content": 'https://github.com/fish2018',
}]
result = {"list": vod}
return result
def playerContent(self,flag,id,vipFlags):
result = {
'parse': 0,
'url': id
}
return result
def getName(self):
return '色播聚合'
def homeVideoContent(self):
pass
def isVideoFormat(self,url):
pass
def manualVideoCheck(self):
pass
def searchContent(self,key,quick):
pass
def destroy(self):
pass
def localProxy(self, param):
pass
+350
View File
@@ -0,0 +1,350 @@
# coding=utf-8
# 文件名: test.py
# 描述: 萝莉AV爬虫 - 修正版
from base.spider import Spider
import re
import json
import urllib.parse
class Spider(Spider):
def getName(self):
return "萝莉AV"
def init(self, extend=""):
self.host = "https://212602.luoliav.cc"
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': self.host,
'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',
}
# 分类列表 - 根据页面导航栏
self.classes = [
{"type_name": "国产精选", "type_id": "1"},
{"type_name": "日韩AV", "type_id": "2"},
{"type_name": "蓝光超清", "type_id": "3"},
{"type_name": "欧美精品", "type_id": "4"},
{"type_name": "异族风情", "type_id": "5"},
{"type_name": "动漫专区", "type_id": "6"},
]
def homeContent(self, filter):
"""返回分类列表"""
result = {}
result["class"] = self.classes
return result
def homeVideoContent(self):
"""首页推荐视频"""
try:
url = f"{self.host}/index.html"
rsp = self.fetch(url, headers=self.headers)
html = rsp.text
videos = self._parse_video_list(html)
print(f"首页解析到 {len(videos)} 个视频")
return {"list": videos}
except Exception as e:
print(f"首页视频解析错误: {e}")
return {"list": []}
def categoryContent(self, tid, pg, filter, extend):
"""分类页面内容"""
try:
# 构建分类URL
url = f"{self.host}/list.php?cid={tid}&page={pg}"
print(f"分类页面URL: {url}")
rsp = self.fetch(url, headers=self.headers)
html = rsp.text
# 解析视频列表
videos = self._parse_video_list(html)
# 解析分页信息
pagecount = int(pg)
# 查找分页链接
pagination_pattern = r'<a href="list\.php\?cid=' + tid + r'&amp;page=(\d+)">(\d+)</a>'
page_matches = re.findall(pagination_pattern, html)
if page_matches:
pages = [int(p[1]) for p in page_matches]
if pages:
pagecount = max(pages)
else:
# 如果找不到分页,假设有3页
pagecount = int(pg) + 2
print(f"分类解析完成: 第 {pg} 页,共 {pagecount} 页,找到 {len(videos)} 个视频")
return {
"list": videos,
"page": int(pg),
"pagecount": pagecount,
"limit": 30,
"total": len(videos) * pagecount
}
except Exception as e:
print(f"分类页面解析错误: {e}")
return {
"list": [],
"page": int(pg),
"pagecount": 1,
"limit": 30,
"total": 0
}
def _parse_video_list(self, html):
"""解析视频列表的通用方法"""
videos = []
# 找到所有视频项
# 模式:<div class="group ... item"> ... </div>
pattern = r'<div[^>]*class="[^"]*group[^"]*item[^"]*"[^>]*>(.*?)</div>\s*</div>\s*</div>'
items = re.findall(pattern, html, re.DOTALL)
if not items:
# 备选模式:更简单的匹配
pattern = r'<div class="group[^"]*item[^>]*>.*?<a[^>]*href="([^"]+)"[^>]*>.*?<img[^>]*data-original="([^"]+)"[^>]*>.*?<a[^>]*class="[^"]*font-bold[^"]*"[^>]*>([^<]+)</a>'
matches = re.findall(pattern, html, re.DOTALL)
for match in matches:
vod_id = match[0]
if not vod_id.startswith('http'):
if vod_id.startswith('/'):
vod_id = self.host + vod_id
else:
vod_id = self.host + '/' + vod_id
# 去掉URL中的时间戳参数
vod_id = re.sub(r'\?.*$', '', vod_id)
vod_pic = match[1]
if vod_pic and not vod_pic.startswith('http'):
if vod_pic.startswith('//'):
vod_pic = 'https:' + vod_pic
elif vod_pic.startswith('/'):
vod_pic = self.host + vod_pic
vod_name = match[2].strip()
videos.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": ""
})
return videos
# 逐个解析每个视频项
for item in items:
try:
# 提取视频链接
href_match = re.search(r'<a[^>]*href="([^"]+)"[^>]*>', item)
if not href_match:
continue
vod_id = href_match.group(1)
if not vod_id.startswith('http'):
if vod_id.startswith('/'):
vod_id = self.host + vod_id
else:
vod_id = self.host + '/' + vod_id
# 去掉URL中的时间戳参数
vod_id = re.sub(r'\?.*$', '', vod_id)
# 提取封面图
img_match = re.search(r'<img[^>]*data-original="([^"]+)"[^>]*>', item)
if not img_match:
img_match = re.search(r'<img[^>]*src="([^"]+)"[^>]*>', item)
if not img_match:
continue
vod_pic = img_match.group(1)
if vod_pic and not vod_pic.startswith('http'):
if vod_pic.startswith('//'):
vod_pic = 'https:' + vod_pic
elif vod_pic.startswith('/'):
vod_pic = self.host + vod_pic
# 提取标题
title_match = re.search(r'<a[^>]*class="[^"]*font-bold[^"]*"[^>]*>([^<]+)</a>', item)
if not title_match:
title_match = re.search(r'<a[^>]*>([^<]+)</a>\s*</div>\s*$', item)
if not title_match:
continue
vod_name = title_match.group(1).strip()
videos.append({
"vod_id": vod_id,
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_remarks": ""
})
except Exception as e:
print(f"解析单个视频时出错: {e}")
continue
return videos
def detailContent(self, ids):
"""视频详情页"""
try:
vod_id = ids[0]
# 确保URL完整
if not vod_id.startswith('http'):
if vod_id.startswith('/'):
url = self.host + vod_id
else:
url = self.host + '/' + vod_id
else:
url = vod_id
print(f"详情页URL: {url}")
rsp = self.fetch(url, headers=self.headers)
html = rsp.text
# 提取标题
vod_name = "未知标题"
title_match = re.search(r'<title>([^<]+)</title>', html)
if title_match:
vod_name = title_match.group(1)
# 清理标题
vod_name = re.sub(r'\s*-\s*少女AV.*', '', vod_name)
# 提取封面图
vod_pic = ""
pic_match = re.search(r'<img[^>]+src="([^"]+)"[^>]*class="[^"]*thumb[^"]*"', html)
if not pic_match:
pic_match = re.search(r'<meta property="og:image" content="([^"]+)"', html)
if not pic_match:
pic_match = re.search(r'<img[^>]+data-original="([^"]+)"', html)
if pic_match:
vod_pic = pic_match.group(1)
if vod_pic.startswith('//'):
vod_pic = 'https:' + vod_pic
elif vod_pic.startswith('/'):
vod_pic = self.host + vod_pic
# 提取播放地址 - 这个需要您提供详情页HTML才能准确解析
play_url = ""
# 尝试找m3u8
m3u8_match = re.search(r'(https?://[^"\']+\.m3u8[^"\']*)', html)
if m3u8_match:
play_url = m3u8_match.group(1)
else:
# 找mp4
mp4_match = re.search(r'(https?://[^"\']+\.mp4[^"\']*)', html)
if mp4_match:
play_url = mp4_match.group(1)
# 如果没找到,可能需要从JavaScript变量中提取
if not play_url:
# 查找类似 var video_url = '...' 的代码
js_match = re.search(r'var[^;]*video[^=]*=[^"\']*["\']([^"\']+\.m3u8[^"\']*)["\']', html, re.IGNORECASE)
if js_match:
play_url = js_match.group(1)
if not play_url:
play_url = url # 如果没找到,让TVBox去解析
video = {
"vod_id": ids[0],
"vod_name": vod_name,
"vod_pic": vod_pic,
"vod_content": "",
"vod_year": "",
"vod_actor": "",
"vod_director": "",
"vod_area": "",
"vod_play_from": "萝莉AV",
"vod_play_url": f"播放${play_url}"
}
return {"list": [video]}
except Exception as e:
print(f"详情页解析错误: {e}")
import traceback
traceback.print_exc()
video = {
"vod_id": ids[0],
"vod_name": "加载失败",
"vod_pic": "",
"vod_content": "",
"vod_play_from": "默认",
"vod_play_url": f"播放${ids[0]}"
}
return {"list": [video]}
def searchContent(self, key, quick, pg=1):
"""搜索功能"""
try:
# 构建搜索URL
encoded_key = urllib.parse.quote(key)
url = f"{self.host}/search.php?keyword={encoded_key}&page={pg}"
print(f"搜索URL: {url}")
rsp = self.fetch(url, headers=self.headers)
html = rsp.text
# 搜索结果页可能和分类页结构相同
videos = self._parse_video_list(html)
print(f"搜索到 {len(videos)} 个结果")
return {
"list": videos,
"page": int(pg),
"pagecount": 1,
"limit": 30,
"total": len(videos)
}
except Exception as e:
print(f"搜索解析错误: {e}")
return {"list": []}
def playerContent(self, flag, id, vipFlags):
"""解析播放地址"""
result = {}
try:
# 如果已经是视频链接,直接返回
if id.endswith(('.m3u8', '.mp4', '.flv', '.ts')):
result["parse"] = 0
result["url"] = id
result["header"] = self.headers
else:
# 否则需要二次解析
if not id.startswith('http'):
if id.startswith('/'):
play_url = self.host + id
else:
play_url = self.host + '/' + id
else:
play_url = id
# 去掉时间戳参数
play_url = re.sub(r'\?.*$', '', play_url)
result["parse"] = 1
result["url"] = play_url
result["header"] = self.headers
except Exception as e:
print(f"播放地址解析错误: {e}")
result["parse"] = 1
result["url"] = id
result["header"] = self.headers
return result
def isVideoFormat(self, url):
"""判断是否为视频格式"""
return url.endswith(('.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts'))
def localProxy(self, params):
"""本地代理"""
return [200, "video/MP2T", ""]
+347
View File
@@ -0,0 +1,347 @@
# -*- coding: utf-8 -*-
import re
import json
import urllib.parse
from urllib.parse import urljoin, quote
import sys
sys.path.append('..')
from base.spider import Spider
from bs4 import BeautifulSoup
class Spider(Spider):
def getName(self):
return "香肠派对"
def init(self, extend=""):
self.host = "https://xiang512.xiang.party/xcpd"
pass
def header(self):
return {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
'Referer': self.host
}
def homeContent(self, filter):
result = {"class": [], "filters": {}, "list": []}
# 分类列表
classes = [
{"type_id": "1", "type_name": "在线看片"},
{"type_id": "2", "type_name": "无需等待"},
{"type_id": "3", "type_name": "不用下载"},
{"type_id": "4", "type_name": "全部免费"}
]
result["class"] = classes
# 获取首页视频
url = f"{self.host}/"
rsp = self.fetch(url, headers=self.header())
if rsp.status_code != 200:
return result
root = BeautifulSoup(rsp.text, 'html.parser')
videos = []
# 查找视频列表
items = root.select('ul.thumbnail-group.clearfix li')
for item in items:
try:
a = item.select_one('a.thumbnail')
if not a:
continue
href = a.get('href', '')
vod_id = re.search(r'/vod(?:detail|play)/(\d+)', href)
if not vod_id:
continue
vod_id = vod_id.group(1)
img = a.select_one('img')
pic = img.get('src', '') if img else ''
info = item.select_one('.video-info')
if info:
h5 = info.select_one('h5 a')
name = h5.get('title', '') if h5 else ''
if not name:
name = h5.text.strip() if h5 else ''
p = info.select_one('p')
remarks = p.text.strip() if p else ''
else:
name = a.get('title', '')
remarks = ''
videos.append({
"vod_id": vod_id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
})
if len(videos) >= 20:
break
except:
continue
result["list"] = videos
return result
def homeVideoContent(self):
return self.homeContent(False)
def categoryContent(self, tid, pg, filter, extend):
p = int(pg)
# 修复分页URL格式
url = f"{self.host}/vodtype/{tid}-{p}.html"
rsp = self.fetch(url, headers=self.header())
if rsp.status_code != 200:
return {"list": [], "page": p, "pagecount": 1}
root = BeautifulSoup(rsp.text, 'html.parser')
videos = []
# 提取总页数(从“共XX条数据,当前X/913页”)
pagecount = p
page_info = root.find(string=re.compile(r'\d+条数据,当前\d+/(\d+)页'))
if page_info:
try:
pagecount = int(re.search(r'/(\d+)页', page_info).group(1))
except:
pass
# 提取视频列表
items = root.select('ul.thumbnail-group.clearfix li')
for item in items:
try:
a = item.select_one('a.thumbnail')
if not a:
continue
href = a.get('href', '')
vod_id = re.search(r'/vod(?:detail|play)/(\d+)', href)
if not vod_id:
continue
vod_id = vod_id.group(1)
img = a.select_one('img')
pic = img.get('src', '') if img else ''
info = item.select_one('.video-info')
if info:
h5 = info.select_one('h5 a')
name = h5.get('title', '') if h5 else ''
if not name:
name = h5.text.strip() if h5 else ''
p_elem = info.select_one('p')
remarks = p_elem.text.strip() if p_elem else ''
else:
name = a.get('title', '')
remarks = ''
videos.append({
"vod_id": vod_id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
})
except:
continue
return {
"list": videos,
"page": p,
"pagecount": pagecount
}
def detailContent(self, ids):
vid = ids[0] if isinstance(ids, list) else ids.split(",")[0]
url = f"{self.host}/voddetail/{vid}.html"
rsp = self.fetch(url, headers=self.header())
if rsp.status_code != 200:
return {"list": []}
root = BeautifulSoup(rsp.text, 'html.parser')
# 标题
title = ""
h1 = root.select_one('h1.appel-title')
if h1:
title = h1.text.strip()
if not title:
title_elem = root.select_one('title')
if title_elem:
title = title_elem.text.replace('视频介绍--香肠派对', '').strip()
# 图片
pic = ""
img = root.select_one('img.appel-img')
if img:
pic = img.get('src', '')
if not pic:
img = root.select_one('.detail-poster img')
if img:
pic = img.get('src', '')
# 描述
desc = ""
desc_elem = root.select_one('.detail-content')
if desc_elem:
desc = desc_elem.text.strip()
if not desc:
desc_elem = root.select_one('.appel-content')
if desc_elem:
desc = desc_elem.text.strip()
# 播放列表
play_from_list = []
play_url_list = []
# 查找线路
tabs = root.select('.detail-tab li a')
play_blocks = root.select('ul.detail-play-list')
for i, block in enumerate(play_blocks):
line_name = tabs[i].text.strip() if i < len(tabs) else f"线路{i+1}"
urls = []
for a in block.select('a'):
href = a.get('href', '')
if href:
full_url = urljoin(self.host, href)
name = a.text.strip() or f"{len(urls)+1}"
urls.append(f"{name}${full_url}")
if urls:
play_from_list.append(line_name)
play_url_list.append("#".join(urls))
# 如果没有找到,尝试其他选择器
if not play_from_list:
lines = root.select('.ff-playurl-tab li a')
for i, block in enumerate(root.select('.ff-playurl-tab-pane')):
line_name = lines[i].text.strip() if i < len(lines) else f"线路{i+1}"
urls = []
for a in block.select('a'):
href = a.get('href', '')
if href:
full_url = urljoin(self.host, href)
name = a.text.strip() or f"{len(urls)+1}"
urls.append(f"{name}${full_url}")
if urls:
play_from_list.append(line_name)
play_url_list.append("#".join(urls))
vod_play_from = "$$$".join(play_from_list) if play_from_list else ""
vod_play_url = "$$$".join(play_url_list) if play_url_list else ""
return {"list": [{
"vod_id": vid,
"vod_name": title,
"vod_pic": pic,
"vod_content": desc,
"vod_play_from": vod_play_from,
"vod_play_url": vod_play_url
}]}
def searchContent(self, key, quick, pg="1"):
p = int(pg)
url = f"{self.host}/vodsearch/-------------.html?wd={quote(key)}&page={p}"
rsp = self.fetch(url, headers=self.header())
if rsp.status_code != 200:
return {"list": []}
root = BeautifulSoup(rsp.text, 'html.parser')
videos = []
items = root.select('ul.thumbnail-group.clearfix li')
for item in items:
try:
a = item.select_one('a.thumbnail')
if not a:
continue
href = a.get('href', '')
vod_id = re.search(r'/vod(?:detail|play)/(\d+)', href)
if not vod_id:
continue
vod_id = vod_id.group(1)
img = a.select_one('img')
pic = img.get('src', '') if img else ''
info = item.select_one('.video-info')
if info:
h5 = info.select_one('h5 a')
name = h5.get('title', '') if h5 else ''
if not name:
name = h5.text.strip() if h5 else ''
p_elem = info.select_one('p')
remarks = p_elem.text.strip() if p_elem else ''
else:
name = a.get('title', '')
remarks = ''
videos.append({
"vod_id": vod_id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks
})
except:
continue
return {"list": videos, "page": p}
def playerContent(self, flag, id, vipFlags):
# 构建播放页URL
if id.startswith('http'):
play_url = id
else:
play_url = urljoin(self.host, id)
rsp = self.fetch(play_url, headers=self.header())
if rsp.status_code != 200:
return {"parse": 0, "playUrl": play_url}
html = rsp.text
# 方法1:从 player_aaaa 提取
match = re.search(r'var player_aaaa\s*=\s*({.*?});', html, re.DOTALL)
if match:
try:
js_str = match.group(1)
js_str = re.sub(r'(\w+):', r'"\1":', js_str)
player_data = json.loads(js_str)
if player_data.get('url'):
return {"parse": 0, "playUrl": player_data['url']}
except:
pass
# 方法2:从 iframe 提取(关键修复)
# 匹配 id="playleft" 的 td 中的 iframe
iframe_match = re.search(r'<td[^>]*id="playleft"[^>]*>.*?<iframe[^>]+src="([^"]+)"', html, re.DOTALL)
if iframe_match:
iframe_url = iframe_match.group(1)
# 提取 url 参数
m3u8_match = re.search(r'[?&]url=([^&]+)', iframe_url)
if m3u8_match:
m3u8_url = urllib.parse.unquote(m3u8_match.group(1))
return {"parse": 0, "playUrl": m3u8_url}
# 如果 iframe 本身就是 m3u8
if '.m3u8' in iframe_url:
return {"parse": 0, "playUrl": iframe_url}
# 方法3:直接查找 iframe
iframe_match2 = re.search(r'<iframe[^>]+src="([^"]+)"', html)
if iframe_match2:
iframe_url = iframe_match2.group(1)
m3u8_match = re.search(r'[?&]url=([^&]+)', iframe_url)
if m3u8_match:
m3u8_url = urllib.parse.unquote(m3u8_match.group(1))
return {"parse": 0, "playUrl": m3u8_url}
# 方法4:直接查找 m3u8
m3u8 = re.search(r'https?://[^"\']+\.m3u8[^"\']*', html)
if m3u8:
return {"parse": 0, "playUrl": m3u8.group(0)}
# 方法5:让系统解析
return {"parse": 1, "url": play_url}
def localProxy(self, params):
return [200, "video/MP2T", ""]
+265
View File
@@ -0,0 +1,265 @@
"""
@header({
searchable: 1,
filterable: 0,
quickSearch: 0,
title: '麻豆免费在线播放',
lang: 'hipy'
})
"""
import json, re, sys
from urllib import parse as urlparse
import requests as req
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider as BaseSpider
class Spider(BaseSpider):
def getName(self):
return self.name
def init(self, extend='{}'):
self.debug = False
self.name = '麻豆免费在线播放'
self.home_url = 'https://c-you.hair'
self.headers = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36'
}
self.extend = extend
def getDependence(self):
return []
def isVideoFormat(self, url):
return False
def manualVideoCheck(self):
return False
def liveContent(self, url):
return {'list': [], 'code': 0, 'error': 0}
# ---------- 首页分类 ----------
def homeContent(self, filterable=False):
result = {
'class': [
{'type_name': '国产精品', 'type_id': '6'},
{'type_name': '中文字幕', 'type_id': '7'},
{'type_name': '伦理影片', 'type_id': '8'},
{'type_name': '自拍偷拍', 'type_id': '9'},
{'type_name': '口交视频', 'type_id': '10'},
{'type_name': '日韩无码', 'type_id': '11'},
{'type_name': '制服诱惑', 'type_id': '12'},
{'type_name': '国产色情', 'type_id': '13'},
],
'filters': {},
'list': [],
'code': 0,
'error': 0
}
return result
# ---------- 首页推荐 ----------
def homeVideoContent(self):
result = {'list': [], 'code': 0, 'error': 0}
try:
r = req.get(self.home_url + '/', headers=self.headers, timeout=10)
r.encoding = 'utf-8'
doc = pq(r.text)
items = doc('.col-md-3.resent-grid.recommended-grid')
for item in items.items():
a = item('.resent-grid-img a')
href = a.attr('href') or ''
title = item('h5 a.title').text().strip()
img = item('.resent-grid-img img').attr('data-original') or ''
views = item('.views-info span').text().strip()
if href and title:
result['list'].append({
'vod_id': href,
'vod_name': title,
'vod_pic': img,
'vod_remarks': views + '观看' if views else '',
})
except Exception as e:
print(f'homeVideoContent error: {e}')
return result
# ---------- 一级分类列表 ----------
def categoryContent(self, tid, pg, filterable, extend):
result = {'list': [], 'page': pg, 'pagecount': pg, 'limit': 90, 'total': 999999, 'code': 0, 'error': 0}
try:
if int(pg) <= 1:
url = f'{self.home_url}/vodtype/{tid}.html'
else:
url = f'{self.home_url}/vodtype/{tid}-{pg}.html'
r = req.get(url, headers=self.headers, timeout=10)
r.encoding = 'utf-8'
doc = pq(r.text)
# 获取总页数
page_info = doc('.pager .active a').text().strip()
if '/' in page_info:
total_pages = page_info.split('/')[-1].strip()
result['pagecount'] = int(total_pages) if total_pages.isdigit() else pg
else:
result['pagecount'] = 9999
items = doc('.col-md-3.resent-grid.recommended-grid')
for item in items.items():
a = item('.resent-grid-img a')
href = a.attr('href') or ''
title = item('h5 a.title').text().strip()
img = item('.resent-grid-img img').attr('data-original') or ''
views = item('.views-info span').text().strip()
if href and title:
result['list'].append({
'vod_id': href,
'vod_name': title,
'vod_pic': img,
'vod_remarks': views + '观看' if views else '',
})
except Exception as e:
print(f'categoryContent error: {e}')
return result
# ---------- 二级详情 ----------
def detailContent(self, ids):
result = {'list': [], 'code': 0, 'error': 0}
try:
vod_id = ids[0]
url = self.home_url + vod_id
r = req.get(url, headers=self.headers, timeout=10)
r.encoding = 'utf-8'
doc = pq(r.text)
title = doc('.song-info h3').text().strip()
img = doc('.video-grid img').attr('src') or ''
# 影片介绍块
info_text = doc('#myList li p').text().strip()
# 提取发布时间和时长
pub_time = ''
duration = ''
pub_match = re.search(r'发布时间:([\d\-]+)', info_text)
if pub_match:
pub_time = pub_match.group(1)
dur_match = re.search(r'视频时长:(\S+)', info_text)
if dur_match:
duration = dur_match.group(1)
# 分类标签
category = ''
cat_match = re.search(r'影片分类:(\S+)', info_text)
if cat_match:
category = cat_match.group(1).strip()
# 简介内容
content = info_text.split('')[-1].strip() if '' in info_text else ''
if not content:
# 尝试取最后一行非标签内容
lines = [l.strip() for l in info_text.split('\n') if l.strip()]
content = lines[-1] if lines else ''
# 播放链接
play_url = doc('a[href*="/vodplay/"]').attr('href') or ''
play_from = '在线播放'
play_url_formatted = ''
if play_url:
play_from = '在线播放'
play_url_formatted = f'播放${play_url}'
result['list'].append({
'vod_id': vod_id,
'vod_name': title if title else '',
'vod_pic': img,
'type_name': category,
'vod_year': pub_time[:4] if len(pub_time) >= 4 else '',
'vod_area': '',
'vod_actor': '',
'vod_director': '',
'vod_remarks': duration,
'vod_content': content,
'vod_play_from': play_from,
'vod_play_url': play_url_formatted,
})
except Exception as e:
print(f'detailContent error: {e}')
return result
# ---------- 搜索 ----------
def searchContent(self, key, quick=False, pg=1):
result = {'list': [], 'code': 0, 'error': 0}
try:
if int(pg) <= 1:
url = f'{self.home_url}/vodsearch/{urlparse.quote(key)}-------------.html'
else:
url = f'{self.home_url}/vodsearch/{urlparse.quote(key)}-------------{pg}---.html'
r = req.get(url, headers=self.headers, timeout=10)
r.encoding = 'utf-8'
doc = pq(r.text)
items = doc('.col-md-3.resent-grid.recommended-grid')
for item in items.items():
a = item('.resent-grid-img a')
href = a.attr('href') or ''
title = item('h5 a.title').text().strip()
img = item('.resent-grid-img img').attr('data-original') or ''
views = item('.views-info span').text().strip()
if href and title:
result['list'].append({
'vod_id': href,
'vod_name': title,
'vod_pic': img,
'vod_remarks': views + '观看' if views else '',
})
except Exception as e:
print(f'searchContent error: {e}')
return result
# ---------- 播放解析 ----------
def playerContent(self, flag, pid, vipFlags):
result = {
'parse': 1,
'playUrl': '',
'url': '',
'header': {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'
}
}
try:
play_url = self.home_url + pid
r = req.get(play_url, headers=self.headers, timeout=10)
r.encoding = 'utf-8'
html = r.text
# 尝试提取 player_data 或其他已知变量
player_match = re.search(r'var\s+player_data\s*=\s*({.*?});', html, re.DOTALL)
if player_match:
player_json = json.loads(player_match.group(1))
real_url = player_json.get('url', '')
if real_url:
result['url'] = real_url
result['parse'] = 0
return result
# 尝试匹配常见 iframe
iframe_match = re.search(r'<iframe[^>]+src="([^"]+)"', html)
if iframe_match:
result['url'] = iframe_match.group(1)
result['parse'] = 1
return result
# 尝试匹配 video 标签
video_match = re.search(r'<video[^>]+src="([^"]+)"', html)
if video_match:
result['url'] = video_match.group(1)
result['parse'] = 0
return result
# 尝试匹配 source 标签
source_match = re.search(r'<source[^>]+src="([^"]+)"', html)
if source_match:
result['url'] = source_match.group(1)
result['parse'] = 0
return result
# 兜底:嗅探模式,把播放页URL交给壳子嗅探
result['url'] = play_url
result['parse'] = 1
except Exception as e:
print(f'playerContent error: {e}')
return result
def localProxy(self, params):
return [404, 'text/plain', 'Not Found']
def destroy(self):
return '正在Destroy'