上传文件至「py」
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import quote, urljoin
|
||||
|
||||
import requests
|
||||
from lxml import etree
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self): return "Gimy影视"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://gimytv.biz"
|
||||
self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": self.host + "/"}
|
||||
self.classes = [{"type_id": "2", "type_name": "电视剧"}, {"type_id": "4", "type_name": "动漫"}, {"type_id": "3", "type_name": "综艺"}, {"type_id": "1", "type_name": "电影"}, {"type_id": "25", "type_name": "短剧"}]
|
||||
subtypes = {
|
||||
"1": [("全部", "1"), ("动作片", "6"), ("喜剧片", "7"), ("爱情片", "8"), ("科幻片", "9"), ("恐怖片", "10"), ("剧情片", "11"), ("战争片", "12"), ("动画电影", "24")],
|
||||
"2": [("全部", "2"), ("陆剧", "13"), ("短剧", "25"), ("韩剧", "15"), ("美剧", "16"), ("日剧", "20"), ("台剧", "14"), ("海外剧", "21"), ("港剧", "22"), ("纪录片", "23")],
|
||||
"3": [("全部", "3")], "4": [("全部", "4")], "25": [("全部", "25")]
|
||||
}
|
||||
years = [{"n": "全部", "v": ""}] + [{"n": str(x), "v": str(x)} for x in range(2026, 2015, -1)]
|
||||
sorts = [{"n": "最新更新", "v": "time"}, {"n": "最新上架", "v": "time_add"}, {"n": "周人气", "v": "hits_week"}, {"n": "总人气", "v": "hits"}]
|
||||
self.filters = {tid: [{"key": "type", "name": "类型", "value": [{"n": n, "v": v} for n, v in values]}, {"key": "year", "name": "年份", "value": years}, {"key": "by", "name": "排序", "value": sorts}] for tid, values in subtypes.items()}
|
||||
|
||||
def _get(self, url, sid=""):
|
||||
try:
|
||||
headers = dict(self.headers)
|
||||
if sid: headers["Cookie"] = "sid=" + str(sid)
|
||||
response = requests.get(url, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
response.encoding = response.apparent_encoding or "utf-8"
|
||||
return response.text
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _fix(self, url): return urljoin(self.host + "/", url or "")
|
||||
|
||||
def _parse_list(self, html):
|
||||
if not html: return []
|
||||
tree, result, seen = etree.HTML(html), [], set()
|
||||
for node in tree.xpath('//a[contains(concat(" ",normalize-space(@class)," ")," video-pic ") and contains(@href,"/voddetail/")]'):
|
||||
match = re.search(r"/voddetail/(\d+)\.html", node.get("href", ""))
|
||||
if not match or match.group(1) in seen: continue
|
||||
seen.add(match.group(1))
|
||||
name = node.get("title") or "".join(node.xpath('.//img/@alt')).strip()
|
||||
pic = node.get("data-original") or node.get("data-src") or node.get("data-lazyload") or "".join(node.xpath('.//img/@data-original | .//img/@data-src | .//img/@src'))
|
||||
remark = "".join(node.xpath('.//*[contains(@class,"note")]//text()')).strip()
|
||||
result.append({"vod_id": match.group(1), "vod_name": name.strip(), "vod_pic": self._fix(pic), "vod_remarks": remark})
|
||||
return result
|
||||
|
||||
def _pagecount(self, tree, page):
|
||||
values = [int(x) for x in tree.xpath('//a[contains(@href,"/vodshow/")]/@href') for x in re.findall(r"-(\d+)---", x)]
|
||||
return max(values + [page])
|
||||
|
||||
def homeContent(self, filter): return {"class": self.classes, "list": self._parse_list(self._get(self.host + "/")), "filters": self.filters}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page, ext = max(1, int(pg or 1)), extend if isinstance(extend, dict) else {}
|
||||
selected, year, by = str(ext.get("type") or tid), str(ext.get("year") or ""), str(ext.get("by") or "time")
|
||||
url = f"{self.host}/vodshow/{selected}--{by}------{page}---{year}.html"
|
||||
html = self._get(url)
|
||||
tree = etree.HTML(html) if html else etree.HTML("<html/>")
|
||||
videos = self._parse_list(html)
|
||||
return {"page": page, "pagecount": self._pagecount(tree, page), "limit": len(videos), "total": self._pagecount(tree, page) * max(len(videos), 1), "list": videos}
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = []
|
||||
for vid in ids:
|
||||
html = self._get(f"{self.host}/voddetail/{vid}.html")
|
||||
if not html: continue
|
||||
tree = etree.HTML(html)
|
||||
name = "".join(tree.xpath('//h1/text()')).strip() or "".join(tree.xpath('//h2/text()')).strip()
|
||||
pic = "".join(tree.xpath('//meta[@property="og:image"]/@content | //div[contains(@class,"detail-pic")]//img/@data-original | //div[contains(@class,"detail-pic")]//img/@src'))
|
||||
content = " ".join(x.strip() for x in tree.xpath('//span[contains(@class,"detail-intro")]//text() | //div[contains(@class,"details-content-all")]//text()') if x.strip())
|
||||
sources, playlists = [], []
|
||||
panels = tree.xpath('//div[contains(concat(" ",normalize-space(@class)," ")," playlist-mobile ")]')
|
||||
for panel in panels:
|
||||
episodes = panel.xpath('.//ul//a[contains(@href,"/video/")]')
|
||||
if not episodes: continue
|
||||
source = "".join(panel.xpath('./li[1]//text()')).strip() or "".join(panel.xpath('./span[1]//text()')).strip() or f"线路{len(sources) + 1}"
|
||||
plays = []
|
||||
for episode in episodes:
|
||||
href = episode.get("href", "")
|
||||
sid_match = re.search(r"sid=(\d+)", href)
|
||||
onclick_match = re.search(r"changeSid\(['\"]?(\d+)", episode.get("onclick", ""))
|
||||
sid = (onclick_match or sid_match).group(1) if onclick_match or sid_match else "1"
|
||||
path = href.split("#", 1)[0]
|
||||
label = "".join(episode.xpath(".//text()")).strip() or str(len(plays) + 1)
|
||||
plays.append(f"{label}${path}@@{sid}")
|
||||
sources.append(source)
|
||||
playlists.append("#".join(plays))
|
||||
result.append({"vod_id": str(vid), "vod_name": name, "vod_pic": self._fix(pic), "vod_content": content, "vod_play_from": "$$$".join(sources), "vod_play_url": "$$$".join(playlists)})
|
||||
return {"list": result}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
page = max(1, int(pg or 1))
|
||||
url = f"{self.host}/vodsearch/-------------.html?wd={quote(key)}&page={page}"
|
||||
return {"page": page, "pagecount": page, "list": self._parse_list(self._get(url))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
path, sid = (id.rsplit("@@", 1) + ["1"])[:2] if "@@" in id else (id, "1")
|
||||
url = self._fix(path)
|
||||
html = self._get(url, sid)
|
||||
marker = "var player_aaaa="
|
||||
if marker in html:
|
||||
try:
|
||||
data = json.JSONDecoder().raw_decode(html.split(marker, 1)[1])[0]
|
||||
play_url = data.get("url", "")
|
||||
if int(data.get("encrypt", 0)) == 1: play_url = quote(play_url, safe=":/?&=%")
|
||||
if play_url and any(x in play_url.lower() for x in (".m3u8", ".mp4", ".flv")):
|
||||
return {"parse": 0, "url": play_url, "header": {"User-Agent": self.headers["User-Agent"], "Referer": url}}
|
||||
except Exception:
|
||||
pass
|
||||
return {"parse": 1, "url": url, "header": {**self.headers, "Cookie": "sid=" + sid}}
|
||||
@@ -0,0 +1,426 @@
|
||||
#coding=utf-8
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TVBox / 影视仓 Python源脚本
|
||||
站点: 可可影视 (103.51.147.112:51120)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
import urllib3
|
||||
from urllib.parse import quote
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
# 禁用SSL警告
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://103.51.147.112:51120'
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://103.51.147.112:51120/'
|
||||
})
|
||||
self.cateManual = {
|
||||
'电影': '1',
|
||||
'连续剧': '2',
|
||||
'动漫': '3',
|
||||
'综艺纪录': '4',
|
||||
'短剧': '6'
|
||||
}
|
||||
# 隐晦的站点标识
|
||||
self._mark = chr(26143) + chr(27827)
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "可可影视"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({
|
||||
'type_id': str(v),
|
||||
'type_name': k
|
||||
})
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
url = f'{self.site}/channel/1.html'
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
doc = pq(r.text)
|
||||
items = doc('.module-item')
|
||||
seen = set()
|
||||
for item in items.items():
|
||||
a = item.find('.v-item')
|
||||
href = a.attr('href') or ''
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
|
||||
# 标题
|
||||
titles = item.find('.v-item-title')
|
||||
title = ''
|
||||
for j in range(len(titles)):
|
||||
t = titles.eq(j).text().strip()
|
||||
if t and t != '可可影视-kekys.com':
|
||||
title = t
|
||||
break
|
||||
|
||||
# 图片
|
||||
pic = ''
|
||||
imgs = item.find('img')
|
||||
for j in range(len(imgs)):
|
||||
img = imgs.eq(j)
|
||||
src = img.attr('data-original') or ''
|
||||
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
|
||||
pic = src
|
||||
break
|
||||
if pic and pic.startswith('/'):
|
||||
pic = 'https://vres.zyxpedu.com' + pic
|
||||
|
||||
# 备注
|
||||
note = ''
|
||||
bottom = item.find('.v-item-bottom span')
|
||||
if bottom.length:
|
||||
note = bottom.text().strip()
|
||||
|
||||
if title:
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return {'list': videos[:24], 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
url = f'{self.site}/channel/{tid}.html?page={page}'
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
doc = pq(r.text)
|
||||
items = doc('.module-item')
|
||||
for item in items.items():
|
||||
a = item.find('.v-item')
|
||||
href = a.attr('href') or ''
|
||||
vid = self.getVid(href)
|
||||
if not vid:
|
||||
continue
|
||||
|
||||
# 标题
|
||||
titles = item.find('.v-item-title')
|
||||
title = ''
|
||||
for j in range(len(titles)):
|
||||
t = titles.eq(j).text().strip()
|
||||
if t and t != '可可影视-kekys.com':
|
||||
title = t
|
||||
break
|
||||
|
||||
# 图片
|
||||
pic = ''
|
||||
imgs = item.find('img')
|
||||
for j in range(len(imgs)):
|
||||
img = imgs.eq(j)
|
||||
src = img.attr('data-original') or ''
|
||||
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
|
||||
pic = src
|
||||
break
|
||||
if pic and pic.startswith('/'):
|
||||
pic = 'https://vres.zyxpedu.com' + pic
|
||||
|
||||
# 备注
|
||||
note = ''
|
||||
bottom = item.find('.v-item-bottom span')
|
||||
if bottom.length:
|
||||
note = bottom.text().strip()
|
||||
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
|
||||
result['page'] = page
|
||||
# 修复:满页24条说明还有下一页,否则已到尾页
|
||||
result['pagecount'] = page + 1 if len(result['list']) >= 24 else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ids[0] if ids else ''
|
||||
if not vid:
|
||||
return result
|
||||
try:
|
||||
url = f'{self.site}/detail/{vid}.html'
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
html = r.text
|
||||
|
||||
# 标题:从title标签提取,最可靠
|
||||
title = ''
|
||||
title_match = re.search(r'<title>(.+?)</title>', html)
|
||||
if title_match:
|
||||
title = title_match.group(1).split('-')[0].strip()
|
||||
# 去掉特殊字符水印
|
||||
title = re.sub(r'[𝕜𝕜𝕪𝕤𝟘𝟙𝕔𝕠𝕞.\s]+', ' ', title).strip()
|
||||
title = re.sub(r'\s+', ' ', title).strip()
|
||||
|
||||
# 图片:从og:image提取
|
||||
pic = ''
|
||||
og_img = re.search(r'<meta\s+property="og:image"\s+content="([^"]+)"', html)
|
||||
if og_img:
|
||||
pic = og_img.group(1)
|
||||
if pic.startswith('/'):
|
||||
pic = 'https://vres.zyxpedu.com' + pic
|
||||
|
||||
# 简介:从meta description提取
|
||||
desc = ''
|
||||
desc_match = re.search(r'<meta\s+name="description"\s+content="([^"]+)"', html)
|
||||
if desc_match:
|
||||
desc = desc_match.group(1).strip()
|
||||
|
||||
# 播放线路和集数
|
||||
play_from = []
|
||||
play_url = []
|
||||
episodes_by_sid = {}
|
||||
sids_in_order = []
|
||||
seen_sids = set()
|
||||
|
||||
# 纯正则提取所有播放链接
|
||||
all_play = re.findall(r'<a[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]+class="episode-item"[^>]*>(.*?)</a>', html, re.DOTALL)
|
||||
# 如果没匹配到,试试class在href前面的情况
|
||||
if not all_play:
|
||||
all_play = re.findall(r'<a[^>]+class="episode-item"[^>]+href="(/play/\d+-(\d+)-(\d+)\.html)"[^>]*>(.*?)</a>', html, re.DOTALL)
|
||||
for href, sid, nid, link_html in all_play:
|
||||
text = re.sub(r'<[^>]+>', '', link_html).strip()
|
||||
if not text:
|
||||
continue
|
||||
if sid not in episodes_by_sid:
|
||||
episodes_by_sid[sid] = []
|
||||
episodes_by_sid[sid].append(f'{text}${href}')
|
||||
if sid not in seen_sids:
|
||||
seen_sids.add(sid)
|
||||
sids_in_order.append(sid)
|
||||
|
||||
# 线路名称
|
||||
source_labels = []
|
||||
all_labels = re.findall(r'class="source-item-label"[^>]*>([^<]+)</', html)
|
||||
for label in all_labels:
|
||||
label = label.strip()
|
||||
if label:
|
||||
source_labels.append(label)
|
||||
|
||||
for i, sid in enumerate(sids_in_order):
|
||||
if sid in episodes_by_sid and episodes_by_sid[sid]:
|
||||
if i < len(source_labels):
|
||||
line_name = source_labels[i]
|
||||
else:
|
||||
line_name = f'线路{sid}'
|
||||
# 跳过4K线路(只有APP端能用)
|
||||
if line_name == '4K':
|
||||
continue
|
||||
play_from.append(line_name)
|
||||
play_url.append('#'.join(episodes_by_sid[sid]))
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': self._mark,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from) if play_from else '',
|
||||
'vod_play_url': '$$$'.join(play_url) if play_url else ''
|
||||
}
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
play_url = id
|
||||
if id and not id.startswith('http'):
|
||||
play_url = self.site + id
|
||||
|
||||
r = self.session.get(play_url, timeout=15, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
|
||||
video_url = ''
|
||||
|
||||
patterns = [
|
||||
r"src:\s*['\"]([^'\"]+\.(m3u8|mp4)[^'\"]*)['\"]",
|
||||
r'"url"\s*:\s*"([^"]+\.(m3u8|mp4)[^"]*)"',
|
||||
r"url\s*:\s*'([^']+\.(m3u8|mp4)[^']*)'",
|
||||
]
|
||||
|
||||
for pat in patterns:
|
||||
m = re.search(pat, r.text, re.DOTALL)
|
||||
if m:
|
||||
video_url = m.group(1)
|
||||
break
|
||||
|
||||
# 修复:使用非捕获组 (?:m3u8|mp4),避免 re.findall 只返回捕获组内容
|
||||
if not video_url:
|
||||
all_urls = re.findall(r'https?://[^\s"\'<>]+\.(?:m3u8|mp4)[^\s"\'<>]*', r.text)
|
||||
if all_urls:
|
||||
for u in all_urls:
|
||||
if 'index.m3u8' in u or 'video.m3u8' in u or '.mp4' in u:
|
||||
video_url = u
|
||||
break
|
||||
if not video_url:
|
||||
video_url = all_urls[0]
|
||||
|
||||
# 修复:处理相对路径视频URL
|
||||
if video_url and not video_url.startswith('http'):
|
||||
if video_url.startswith('/'):
|
||||
video_url = self.site + video_url
|
||||
else:
|
||||
video_url = self.site + '/' + video_url
|
||||
|
||||
if video_url:
|
||||
result['parse'] = 0
|
||||
result['url'] = video_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
else:
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
result['parse'] = 1
|
||||
result['url'] = id if id.startswith('http') else self.site + id
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
# 先访问搜索页获取token
|
||||
search_url = f'{self.site}/search?k={quote(key)}'
|
||||
r = self.session.get(search_url, timeout=15, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
t_match = re.search(r'name="t" value="([^"]+)"', r.text)
|
||||
t = t_match.group(1) if t_match else ''
|
||||
|
||||
if t:
|
||||
url = f'{self.site}/search?k={quote(key)}&t={quote(t)}'
|
||||
if page > 1:
|
||||
url += f'&page={page}'
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
r.encoding = 'utf-8'
|
||||
|
||||
doc = pq(r.text)
|
||||
items = doc('.search-result-item')
|
||||
for item in items.items():
|
||||
# 修复:兼容 href 在元素本身或子级 <a> 标签上的情况
|
||||
href = item.attr('href') or ''
|
||||
if not href:
|
||||
a_tag = item.find('a')
|
||||
if a_tag.length:
|
||||
href = a_tag.attr('href') or ''
|
||||
|
||||
vid = self.getVid(href)
|
||||
if not vid:
|
||||
continue
|
||||
|
||||
# 标题
|
||||
title = ''
|
||||
title_elem = item.find('.title')
|
||||
if title_elem.length:
|
||||
title = title_elem.text().strip()
|
||||
if not title:
|
||||
img = item.find('img')
|
||||
if img.length:
|
||||
title = img.attr('alt') or img.attr('title') or ''
|
||||
title = title.strip()
|
||||
|
||||
# 图片
|
||||
pic = ''
|
||||
imgs = item.find('img')
|
||||
for j in range(len(imgs)):
|
||||
img = imgs.eq(j)
|
||||
src = img.attr('data-original') or img.attr('src') or ''
|
||||
if src and 'placeholder' not in src and 'logo_placeholder' not in src:
|
||||
pic = src
|
||||
break
|
||||
if pic and pic.startswith('/'):
|
||||
pic = 'https://vres.zyxpedu.com' + pic
|
||||
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': ''
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
|
||||
def getVid(self, url):
|
||||
if not url:
|
||||
return ''
|
||||
m = re.search(r'/detail/(\d+)\.html', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.search(r'/play/(\d+)-', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ''
|
||||
+375
@@ -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.cc"
|
||||
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,
|
||||
})
|
||||
}
|
||||
+1260
@@ -0,0 +1,1260 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @
|
||||
# 修复版本 - 参考最新三合一.js重构虎牙、斗鱼、B站直播逻辑
|
||||
# 修复:虎牙清晰度选择,确保ratio参数正确传递码率值
|
||||
# 修复:斗鱼切换分辨率只能播放1秒的问题(每次重新获取安全密钥和签名)
|
||||
# 修复:B站使用特殊UA和WBI签名绕过-352风控 [^90^][^30^]
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import hashlib
|
||||
import random
|
||||
import urllib.parse
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import parse_qs
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
# 初始化B站WBI密钥
|
||||
self.bili_wbi_keys = None
|
||||
self.bili_wbi_expire = 0
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "直播"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = [
|
||||
{
|
||||
# 特殊UA绕过B站风控 [^90^]
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0"
|
||||
},
|
||||
{
|
||||
"User-Agent": "Dart/3.4 (dart:io)"
|
||||
}
|
||||
]
|
||||
|
||||
excepturl = 'https://www.baidu.com'
|
||||
|
||||
hosts = {
|
||||
"huya": ["https://www.huya.com", "https://mp.huya.com"],
|
||||
"douyu": "https://www.douyu.com",
|
||||
"wangyi": "https://cc.163.com",
|
||||
"bili": ["https://api.live.bilibili.com", "https://api.bilibili.com"]
|
||||
}
|
||||
|
||||
referers = {
|
||||
"huya": "https://live.cdn.huya.com",
|
||||
"douyu": "https://m.douyu.com",
|
||||
"bili": "https://live.bilibili.com"
|
||||
}
|
||||
|
||||
playheaders = {
|
||||
"wangyi": {
|
||||
"User-Agent": "ExoPlayer",
|
||||
"Connection": "Keep-Alive",
|
||||
"Icy-MetaData": "1"
|
||||
},
|
||||
"bili": {
|
||||
'Accept': '*/*',
|
||||
'Icy-MetaData': '1',
|
||||
'referer': 'https://live.bilibili.com',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
||||
},
|
||||
'huya': {
|
||||
'User-Agent': 'ExoPlayer',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Icy-MetaData': '1'
|
||||
},
|
||||
'douyu': {
|
||||
'User-Agent': 'libmpv',
|
||||
'Icy-MetaData': '1'
|
||||
}
|
||||
}
|
||||
|
||||
# WBI签名相关常量 [^30^]
|
||||
MIXIN_KEY_ENC_TAB = [
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
|
||||
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
|
||||
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
|
||||
36, 20, 34, 44, 52
|
||||
]
|
||||
|
||||
def _get_bili_wbi_keys(self):
|
||||
"""获取B站WBI密钥 [^30^]"""
|
||||
try:
|
||||
# 检查缓存
|
||||
if self.bili_wbi_keys and time.time() < self.bili_wbi_expire:
|
||||
return self.bili_wbi_keys
|
||||
|
||||
# 从导航接口获取 - 使用特殊UA [^90^]
|
||||
resp = self.fetch(
|
||||
'https://api.bilibili.com/x/web-interface/nav',
|
||||
headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
||||
'Referer': 'https://www.bilibili.com/'
|
||||
}
|
||||
).json()
|
||||
|
||||
if resp.get('code') != 0:
|
||||
return None
|
||||
|
||||
img_url = resp['data']['wbi_img']['img_url']
|
||||
sub_url = resp['data']['wbi_img']['sub_url']
|
||||
|
||||
# 提取文件名作为key
|
||||
img_key = img_url.rsplit('/', 1)[1].split('.')[0]
|
||||
sub_key = sub_url.rsplit('/', 1)[1].split('.')[0]
|
||||
|
||||
self.bili_wbi_keys = (img_key, sub_key)
|
||||
self.bili_wbi_expire = time.time() + 86400 # 24小时过期
|
||||
|
||||
return self.bili_wbi_keys
|
||||
except Exception as e:
|
||||
print(f"获取B站WBI密钥失败: {e}")
|
||||
return None
|
||||
|
||||
def _get_mixin_key(self, orig: str):
|
||||
"""生成mixin_key [^30^]"""
|
||||
return ''.join([orig[i] for i in self.MIXIN_KEY_ENC_TAB])[:32]
|
||||
|
||||
def _enc_wbi(self, params: dict):
|
||||
"""WBI签名 [^30^]"""
|
||||
keys = self._get_bili_wbi_keys()
|
||||
if not keys:
|
||||
return params
|
||||
|
||||
img_key, sub_key = keys
|
||||
mixin_key = self._get_mixin_key(img_key + sub_key)
|
||||
|
||||
# 添加时间戳
|
||||
params['wts'] = round(time.time())
|
||||
|
||||
# 排序参数
|
||||
params = dict(sorted(params.items()))
|
||||
|
||||
# 过滤特殊字符
|
||||
params = {
|
||||
k: ''.join(filter(lambda c: c not in "!'()*", str(v)))
|
||||
for k, v in params.items()
|
||||
}
|
||||
|
||||
# 计算签名
|
||||
query = urllib.parse.urlencode(params)
|
||||
w_rid = hashlib.md5((query + mixin_key).encode()).hexdigest()
|
||||
|
||||
params['w_rid'] = w_rid
|
||||
return params
|
||||
|
||||
def process_bili(self):
|
||||
"""获取B站分类列表 - 使用WBI签名 [^30^]"""
|
||||
try:
|
||||
# 尝试获取分类列表 - 使用特殊UA和WBI签名
|
||||
params = {'need_entrance': 1, 'parent_id': 0}
|
||||
signed_params = self._enc_wbi(params)
|
||||
|
||||
data = self.fetch(
|
||||
f'{self.hosts["bili"][0]}/room/v1/Area/getList',
|
||||
params=signed_params,
|
||||
headers=self.headers[0]
|
||||
).json()
|
||||
|
||||
if data.get('code') == 0 and data.get('data'):
|
||||
# 保存分类数据供后续使用
|
||||
self.bili_areas = data['data']
|
||||
return ('bili', [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': i['name'], 'v': str(i['id'])}
|
||||
for i in data['data']]}])
|
||||
return 'bili', None
|
||||
except Exception as e:
|
||||
print(f"bili处理错误: {e}")
|
||||
# 使用默认分类
|
||||
return 'bili', [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': '网游', 'v': '2'}, {'n': '手游', 'v': '3'},
|
||||
{'n': '单机', 'v': '6'}, {'n': '娱乐', 'v': '1'},
|
||||
{'n': '电台', 'v': '5'}, {'n': '虚拟主播', 'v': '9'},
|
||||
{'n': '生活', 'v': '10'}, {'n': '知识', 'v': '11'},
|
||||
{'n': '赛事', 'v': '13'}]}]
|
||||
|
||||
def process_douyu(self):
|
||||
try:
|
||||
self.dyufdata = self.fetch(
|
||||
f'{self.referers["douyu"]}/api/cate/list',
|
||||
headers=self.headers[1]
|
||||
).json()
|
||||
return ('douyu', [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': i['cate1Name'], 'v': str(i['cate1Id'])}
|
||||
for i in self.dyufdata['data']['cate1Info']]}])
|
||||
except Exception as e:
|
||||
print(f"douyu错误: {e}")
|
||||
return 'douyu', None
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"虎牙": "huya",
|
||||
"斗鱼": "douyu",
|
||||
"网易": "wangyi"
|
||||
|
||||
}
|
||||
classes = []
|
||||
filters = {
|
||||
'huya': [{'key': 'cate', 'name': '分类',
|
||||
'value': [{'n': '网游', 'v': '1'}, {'n': '单机', 'v': '2'},
|
||||
{'n': '娱乐', 'v': '8'}, {'n': '手游', 'v': '3'}]}]
|
||||
}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
futures = {
|
||||
executor.submit(self.process_bili): 'bili',
|
||||
executor.submit(self.process_douyu): 'douyu'
|
||||
}
|
||||
|
||||
for future in futures:
|
||||
platform, filter_data = future.result()
|
||||
if filter_data:
|
||||
filters[platform] = filter_data
|
||||
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
vdata = []
|
||||
result = {}
|
||||
pagecount = 9999
|
||||
result['page'] = pg
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
if tid == 'wangyi':
|
||||
vdata, pagecount = self.wyccContent(tid, pg, filter, extend, vdata)
|
||||
elif 'bili' in tid:
|
||||
vdata, pagecount = self.biliContent(tid, pg, filter, extend, vdata)
|
||||
elif 'huya' in tid:
|
||||
vdata, pagecount = self.huyaContent(tid, pg, filter, extend, vdata)
|
||||
elif 'douyu' in tid:
|
||||
vdata, pagecount = self.douyuContent(tid, pg, filter, extend, vdata)
|
||||
result['list'] = vdata
|
||||
result['pagecount'] = pagecount
|
||||
return result
|
||||
|
||||
def wyccContent(self, tid, pg, filter, extend, vdata):
|
||||
params = {
|
||||
'format': 'json',
|
||||
'start': (int(pg) - 1) * 20,
|
||||
'size': '20',
|
||||
}
|
||||
response = self.fetch(f'{self.hosts[tid]}/api/category/live/', params=params, headers=self.headers[0]).json()
|
||||
for i in response['lives']:
|
||||
if i.get('cuteid'):
|
||||
bvdata = self.buildvod(
|
||||
vod_id=f"{tid}@@{i['cuteid']}",
|
||||
vod_name=i.get('title'),
|
||||
vod_pic=i.get('cover'),
|
||||
vod_remarks=i.get('nickname'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(bvdata)
|
||||
return vdata, 9999
|
||||
|
||||
def biliContent(self, tid, pg, filter, extend, vdata):
|
||||
"""B站分类内容 - 使用WBI签名绕过风控 [^30^][^90^]"""
|
||||
try:
|
||||
# 分类列表 - 显示子分类
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
# 从已保存的分类数据中找到对应分类的子分类
|
||||
if hasattr(self, 'bili_areas'):
|
||||
for area in self.bili_areas:
|
||||
if str(area['id']) == extend['cate']:
|
||||
for sub_area in area.get('list', []):
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{extend['cate']}@@{sub_area['id']}",
|
||||
vod_name=sub_area.get('name'),
|
||||
vod_pic=sub_area.get('pic'),
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
# 如果没有找到子分类,直接返回空,让用户进入房间列表
|
||||
return vdata, 1
|
||||
|
||||
# 房间列表 - 使用getList接口并添加WBI签名 [^30^]
|
||||
if 'click' in tid:
|
||||
# 子分类房间
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
parent_area_id = ids[1]
|
||||
area_id = ids[2]
|
||||
else:
|
||||
# 默认使用分类ID作为parent_area_id,area_id为0表示该分类下所有
|
||||
parent_area_id = extend.get('cate', '2') # 默认网游
|
||||
area_id = 0
|
||||
|
||||
# 构建请求参数并添加WBI签名 [^30^]
|
||||
params = {
|
||||
'parent_area_id': parent_area_id,
|
||||
'area_id': area_id,
|
||||
'page': pg,
|
||||
'platform': 'web',
|
||||
'sort_type': 'online' # 按热度排序
|
||||
}
|
||||
signed_params = self._enc_wbi(params)
|
||||
|
||||
# 调用getList接口
|
||||
api_url = f'{self.hosts[tid][0]}/xlive/web-interface/v1/second/getList'
|
||||
data = self.fetch(api_url, params=signed_params, headers=self.headers[0]).json()
|
||||
|
||||
# 如果WBI签名失败,尝试不带签名
|
||||
if data.get('code') == -352:
|
||||
print("WBI签名失败,尝试无签名请求...")
|
||||
params = {
|
||||
'parent_area_id': parent_area_id,
|
||||
'area_id': area_id,
|
||||
'page': pg,
|
||||
'platform': 'web',
|
||||
'sort_type': 'online'
|
||||
}
|
||||
data = self.fetch(api_url, params=params, headers=self.headers[0]).json()
|
||||
|
||||
if data.get('code') == 0:
|
||||
room_list = data.get('data', {}).get('list', [])
|
||||
for room in room_list:
|
||||
if room.get('roomid'):
|
||||
# 处理在线人数显示
|
||||
online = room.get('online', 0)
|
||||
if online > 10000:
|
||||
online_str = f"{online / 10000:.1f}万"
|
||||
else:
|
||||
online_str = str(online)
|
||||
|
||||
v = self.buildvod(
|
||||
f"{tid}@@{room['roomid']}",
|
||||
room.get('title', '未知标题'),
|
||||
room.get('cover') or room.get('system_cover'),
|
||||
f"{online_str}人",
|
||||
0,
|
||||
room.get('uname', ''),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(v)
|
||||
|
||||
# 检查是否有更多数据
|
||||
has_more = data.get('data', {}).get('has_more', 0)
|
||||
if not has_more:
|
||||
pagecount = int(pg)
|
||||
else:
|
||||
pagecount = 9999
|
||||
else:
|
||||
print(f"B站API返回错误: {data.get('message', '未知错误')} (code: {data.get('code')})")
|
||||
pagecount = 1
|
||||
|
||||
return vdata, pagecount
|
||||
|
||||
except Exception as e:
|
||||
print(f"B站内容获取错误: {e}")
|
||||
return vdata, 1
|
||||
|
||||
def huyaContent(self, tid, pg, filter, extend, vdata):
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
id = extend.get('cate')
|
||||
data = self.fetch(f'{self.referers[tid]}/liveconfig/game/bussLive?bussType={id}',
|
||||
headers=self.headers[1]).json()
|
||||
for i in data['data']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{int(i['gid'])}",
|
||||
vod_name=i.get('gameFullName'),
|
||||
vod_pic=f'https://huyaimg.msstatic.com/cdnimage/game/{int(i["gid"])}-MS.jpg',
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
else:
|
||||
gid = ''
|
||||
if 'click' in tid:
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
gid = f'&gameId={ids[1]}'
|
||||
data = self.fetch(f'{self.hosts[tid][0]}/cache.php?m=LiveList&do=getLiveListByPage&tagAll=0{gid}&page={pg}',
|
||||
headers=self.headers[1]).json()
|
||||
for i in data['data']['datas']:
|
||||
if i.get('profileRoom'):
|
||||
v = self.buildvod(
|
||||
f"{tid}@@{i['profileRoom']}",
|
||||
i.get('introduction'),
|
||||
i.get('screenshot'),
|
||||
str(int(i.get('totalCount', '1')) / 10000) + '万',
|
||||
0,
|
||||
i.get('nick'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 9999
|
||||
|
||||
def douyuContent(self, tid, pg, filter, extend, vdata):
|
||||
if extend.get('cate') and pg == '1' and 'click' not in tid:
|
||||
for i in self.dyufdata['data']['cate2Info']:
|
||||
if str(i['cate1Id']) == extend['cate']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"click_{tid}@@{i['cate2Id']}",
|
||||
vod_name=i.get('cate2Name'),
|
||||
vod_pic=i.get('icon'),
|
||||
vod_remarks=i.get('count'),
|
||||
vod_tag=1,
|
||||
style={"type": "oval", "ratio": 1}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 1
|
||||
else:
|
||||
path = f'/japi/weblist/apinc/allpage/6/{pg}'
|
||||
if 'click' in tid:
|
||||
ids = tid.split('_')[1].split('@@')
|
||||
tid = ids[0]
|
||||
path = f'/gapi/rkc/directory/mixList/2_{ids[1]}/{pg}'
|
||||
url = f'{self.hosts[tid]}{path}'
|
||||
data = self.fetch(url, headers=self.headers[1]).json()
|
||||
for i in data['data']['rl']:
|
||||
v = self.buildvod(
|
||||
vod_id=f"{tid}@@{i['rid']}",
|
||||
vod_name=i.get('rn'),
|
||||
vod_pic=i.get('rs16'),
|
||||
vod_year=str(int(i.get('ol', 1)) / 10000) + '万',
|
||||
vod_remarks=i.get('nn'),
|
||||
style={"type": "rect", "ratio": 1.33}
|
||||
)
|
||||
vdata.append(v)
|
||||
return vdata, 9999
|
||||
|
||||
def detailContent(self, ids):
|
||||
ids = ids[0].split('@@')
|
||||
if ids[0] == 'wangyi':
|
||||
vod = self.wyccDetail(ids)
|
||||
elif ids[0] == 'bili':
|
||||
vod = self.biliDetail(ids)
|
||||
elif ids[0] == 'huya':
|
||||
vod = self.huyaDetail(ids)
|
||||
elif ids[0] == 'douyu':
|
||||
vod = self.douyuDetail(ids)
|
||||
return {'list': [vod]}
|
||||
|
||||
def wyccDetail(self, ids):
|
||||
try:
|
||||
vdata = self.getpq(f'{self.hosts[ids[0]]}/{ids[1]}', self.headers[0])('script').eq(-1).text()
|
||||
|
||||
def get_quality_name(vbr):
|
||||
if vbr <= 600:
|
||||
return "标清"
|
||||
elif vbr <= 1000:
|
||||
return "高清"
|
||||
elif vbr <= 2000:
|
||||
return "超清"
|
||||
else:
|
||||
return "蓝光"
|
||||
|
||||
data = json.loads(vdata)['props']['pageProps']['roomInfoInitData']
|
||||
name = data['live'].get('title', ids[0])
|
||||
vod = self.buildvod(vod_name=data.get('keywords_suffix'), vod_remarks=data['live'].get('title'),
|
||||
vod_content=data.get('description_suffix'))
|
||||
resolution_data = data['live']['quickplay']['resolution']
|
||||
all_streams = {}
|
||||
sorted_qualities = sorted(resolution_data.items(),
|
||||
key=lambda x: x[1]['vbr'],
|
||||
reverse=True)
|
||||
for quality, data in sorted_qualities:
|
||||
vbr = data['vbr']
|
||||
quality_name = get_quality_name(vbr)
|
||||
for cdn_name, url in data['cdn'].items():
|
||||
if cdn_name not in all_streams and type(url) == str and url.startswith('http'):
|
||||
all_streams[cdn_name] = []
|
||||
if isinstance(url, str) and url.startswith('http'):
|
||||
all_streams[cdn_name].extend([quality_name, url])
|
||||
plists = []
|
||||
names = []
|
||||
for i, (cdn_name, stream_list) in enumerate(all_streams.items(), 1):
|
||||
names.append(f'线路{i}')
|
||||
pstr = f"{name}${ids[0]}@@{self.e64(json.dumps(stream_list))}"
|
||||
plists.append(pstr)
|
||||
vod['vod_play_from'] = "$$$".join(names)
|
||||
vod['vod_play_url'] = "$$$".join(plists)
|
||||
return vod
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def biliDetail(self, ids):
|
||||
"""
|
||||
B站直播详情 - 使用playUrl接口获取多清晰度
|
||||
"""
|
||||
try:
|
||||
room_id = ids[1]
|
||||
|
||||
# 获取房间信息
|
||||
info_res = self.fetch(
|
||||
f'{self.hosts["bili"][0]}/room/v1/Room/get_info?room_id={room_id}',
|
||||
headers=self.headers[0]
|
||||
).json()
|
||||
|
||||
if info_res.get('code') != 0:
|
||||
return self.handle_exception(Exception("获取房间信息失败"))
|
||||
|
||||
room_info = info_res['data']
|
||||
title = room_info.get('title', 'B站直播')
|
||||
|
||||
vod = self.buildvod(
|
||||
vod_name=title,
|
||||
type_name=f"{room_info.get('parent_area_name', '')}/{room_info.get('area_name', '')}",
|
||||
vod_director=room_info.get('uname', ''),
|
||||
vod_remarks=f"在线{room_info.get('online', 0)}人"
|
||||
)
|
||||
|
||||
# 获取播放地址信息
|
||||
play_res = self.fetch(
|
||||
f'{self.hosts["bili"][0]}/room/v1/Room/playUrl?cid={room_id}&qn=10000&platform=web',
|
||||
headers={
|
||||
**self.headers[0],
|
||||
'Referer': 'https://live.bilibili.com/',
|
||||
'Origin': 'https://live.bilibili.com'
|
||||
}
|
||||
).json()
|
||||
|
||||
if play_res.get('code') != 0:
|
||||
return self.handle_exception(Exception("获取播放地址失败"))
|
||||
|
||||
play_data = play_res['data']
|
||||
accept_quality = play_data.get('accept_quality', ['10000', '400', '250', '150'])
|
||||
quality_desc = {item['qn']: item['desc'] for item in play_data.get('quality_description', [])}
|
||||
|
||||
# 构建清晰度列表
|
||||
qualities = []
|
||||
for qn in sorted([int(q) for q in accept_quality], reverse=True):
|
||||
desc = quality_desc.get(qn, f'清晰度{qn}')
|
||||
qualities.append(f"{desc}$bili@@{room_id}@@{qn}")
|
||||
|
||||
vod['vod_play_from'] = 'B站直播'
|
||||
vod['vod_play_url'] = '#'.join(qualities)
|
||||
return vod
|
||||
|
||||
except Exception as e:
|
||||
print(f"B站详情错误: {e}")
|
||||
return self.handle_exception(e)
|
||||
|
||||
def huyaDetail(self, ids):
|
||||
"""
|
||||
虎牙播放详情 - 参考最新三合一.js重构
|
||||
支持多线路多清晰度选择
|
||||
核心算法:通过房间信息API获取uid、streamName和rateArray,为每个清晰度生成签名URL
|
||||
清晰度说明:
|
||||
- 蓝光8M/6M/4M/10M = 8000/6000/4000/10000 kbps = 1080P+
|
||||
- 蓝光 = 3000 kbps = 1080P
|
||||
- 超清 = 2000 kbps = 1080P (官方标准)
|
||||
- 高清 = 1200 kbps = 720P
|
||||
- 标清/流畅 = 500-800 kbps = 480P/540P
|
||||
"""
|
||||
try:
|
||||
room_id = ids[1]
|
||||
|
||||
# 1. 获取房间信息
|
||||
api_url = f'{self.hosts[ids[0]][1]}/cache.php?m=Live&do=profileRoom&roomid={room_id}'
|
||||
res = self.fetch(api_url, headers=self.headers[0])
|
||||
|
||||
if res.status_code != 200:
|
||||
return self.handle_exception(Exception(f"API请求失败: {res.status_code}"))
|
||||
|
||||
data = res.json()
|
||||
if not data or not data.get('data'):
|
||||
return self.handle_exception(Exception("房间数据为空"))
|
||||
|
||||
room_data = data['data']
|
||||
|
||||
# 2. 提取关键信息
|
||||
uid = room_data.get('profileInfo', {}).get('uid')
|
||||
stream_info = room_data.get('stream', {})
|
||||
live_data = room_data.get('liveData', {})
|
||||
|
||||
if not uid:
|
||||
return self.handle_exception(Exception("缺少uid"))
|
||||
|
||||
# 3. 获取streamName和码率信息
|
||||
base_stream_list = stream_info.get('baseSteamInfoList', [])
|
||||
if not base_stream_list:
|
||||
return self.handle_exception(Exception("无直播流信息"))
|
||||
|
||||
# 获取第一个CDN的streamName作为基准
|
||||
base_stream = base_stream_list[0]
|
||||
stream_name = base_stream.get('sStreamName')
|
||||
if not stream_name:
|
||||
return self.handle_exception(Exception("无法获取streamName"))
|
||||
|
||||
# 4. 构建VOD对象
|
||||
vod = self.buildvod(
|
||||
vod_name=live_data.get('introduction', '虎牙直播'),
|
||||
type_name=live_data.get('gameFullName', ''),
|
||||
vod_director=live_data.get('nick', ''),
|
||||
vod_remarks=live_data.get('contentIntro', ''),
|
||||
)
|
||||
|
||||
# 5. 获取所有CDN线路
|
||||
cdn_list = []
|
||||
for stream in base_stream_list:
|
||||
cdn_type = stream.get('sCdnType', 'AL')
|
||||
flv_url = stream.get('sFlvUrl', '')
|
||||
hls_url = stream.get('sHlsUrl', '')
|
||||
stream_name_cdn = stream.get('sStreamName', stream_name)
|
||||
|
||||
if flv_url:
|
||||
cdn_list.append({
|
||||
'cdn': cdn_type,
|
||||
'flv_base': flv_url,
|
||||
'hls_base': hls_url,
|
||||
'stream_name': stream_name_cdn,
|
||||
'priority': stream.get('iWebPriorityRate', 0)
|
||||
})
|
||||
|
||||
# 按优先级排序
|
||||
cdn_list.sort(key=lambda x: x['priority'], reverse=True)
|
||||
|
||||
# 6. 获取清晰度列表 (rateArray)
|
||||
rate_array = stream_info.get('rateArray', [])
|
||||
|
||||
# 如果没有rateArray,尝试从vMultiStreamInfo获取
|
||||
if not rate_array and 'vMultiStreamInfo' in room_data:
|
||||
rate_array = room_data['vMultiStreamInfo']
|
||||
|
||||
# 如果仍然没有,使用默认清晰度(按虎牙官方标准)
|
||||
if not rate_array:
|
||||
rate_array = [
|
||||
{'sDisplayName': '蓝光4M', 'iBitRate': 4000},
|
||||
{'sDisplayName': '蓝光', 'iBitRate': 3000},
|
||||
{'sDisplayName': '超清', 'iBitRate': 2000}, # 2000kbps = 1080P
|
||||
{'sDisplayName': '高清', 'iBitRate': 1200}, # 1200kbps = 720P
|
||||
{'sDisplayName': '流畅', 'iBitRate': 500}
|
||||
]
|
||||
|
||||
# 过滤和排序清晰度
|
||||
# 虎牙的rateArray中,iBitRate就是码率值,sDisplayName是显示名称
|
||||
# 需要确保:超清=2000kbps(1080P),高清=1200kbps(720P)
|
||||
filtered_rates = []
|
||||
seen_bitrates = set()
|
||||
|
||||
for rate in rate_array:
|
||||
bit_rate = rate.get('iBitRate', 0)
|
||||
name = rate.get('sDisplayName', '')
|
||||
|
||||
# 跳过重复的码率
|
||||
if bit_rate in seen_bitrates:
|
||||
continue
|
||||
|
||||
# 修正清晰度名称,确保符合虎牙标准
|
||||
# 2000kbps应该是超清(1080P),不是高清
|
||||
if bit_rate == 2000 and ('高清' in name or '720' in name):
|
||||
name = '超清' # 强制修正为超清
|
||||
elif bit_rate == 1200 and ('标清' in name or '480' in name):
|
||||
name = '高清' # 1200kbps对应高清
|
||||
elif bit_rate == 2000 and name == '原画':
|
||||
name = '超清' # 修正原画为超清
|
||||
|
||||
seen_bitrates.add(bit_rate)
|
||||
filtered_rates.append({
|
||||
'sDisplayName': name,
|
||||
'iBitRate': bit_rate
|
||||
})
|
||||
|
||||
# 按码率从高到低排序
|
||||
sorted_rates = sorted(filtered_rates, key=lambda x: x['iBitRate'], reverse=True)
|
||||
|
||||
# 7. 为每个CDN生成各清晰度的播放URL
|
||||
play_lines = []
|
||||
line_names = []
|
||||
|
||||
for cdn_idx, cdn in enumerate(cdn_list[:3]): # 最多取3个CDN
|
||||
cdn_name = cdn['cdn']
|
||||
line_names.append(f"线路{cdn_idx + 1}({cdn_name})")
|
||||
|
||||
qualities = []
|
||||
for rate in sorted_rates:
|
||||
quality_name = rate['sDisplayName']
|
||||
bit_rate = rate['iBitRate']
|
||||
|
||||
# 生成该清晰度的URL
|
||||
quality_url = self._generate_huya_play_url(
|
||||
cdn, uid, stream_name, bit_rate
|
||||
)
|
||||
|
||||
qualities.extend([quality_name, quality_url])
|
||||
|
||||
# 编码该线路的所有清晰度
|
||||
encoded_qualities = self.e64(json.dumps(qualities))
|
||||
play_lines.append(f"{live_data.get('introduction', '直播')}${ids[0]}@@{encoded_qualities}")
|
||||
|
||||
# 8. 构建播放数据
|
||||
vod['vod_play_from'] = "$$$".join(line_names)
|
||||
vod['vod_play_url'] = "$$$".join(play_lines)
|
||||
|
||||
return vod
|
||||
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def _generate_huya_play_url(self, cdn, uid, stream_name, bit_rate):
|
||||
"""
|
||||
生成虎牙播放URL,参考最新三合一.js算法
|
||||
关键:ratio参数必须正确设置为iBitRate值(如2000、4000等)
|
||||
"""
|
||||
# 基础URL构建
|
||||
flv_base = cdn['flv_base']
|
||||
stream = cdn['stream_name']
|
||||
|
||||
# 生成时间戳和签名参数
|
||||
timestamp = int(time.time())
|
||||
seqid = f"{uid}{timestamp}"
|
||||
ss = hashlib.md5(f"{seqid}|huya_adr|102".encode()).hexdigest()
|
||||
ws_time = hex(timestamp + 21600)[2:] # 16进制,有效期6小时
|
||||
|
||||
# 计算wsSecret
|
||||
ws_secret = hashlib.md5(
|
||||
f"DWq8BcJ3h6DJt6TY_{uid}_{stream_name}_{ss}_{ws_time}".encode()
|
||||
).hexdigest()
|
||||
|
||||
# 构建基础URL
|
||||
base_url = f"{flv_base}/{stream}.flv"
|
||||
|
||||
# 关键修复:ratio参数直接使用iBitRate值
|
||||
# 超清=2000,高清=1200,蓝光=3000/4000/6000/8000
|
||||
if bit_rate > 0:
|
||||
ratio_param = f"ratio={bit_rate}"
|
||||
else:
|
||||
# 原画/0码率时,使用默认2000或从URL推断
|
||||
ratio_param = "ratio=2000"
|
||||
|
||||
# 构建完整URL
|
||||
play_url = (
|
||||
f"{base_url}?{ratio_param}&wsSecret={ws_secret}&wsTime={ws_time}"
|
||||
f"&ctype=huya_adr&seqid={seqid}&uid={uid}"
|
||||
f"&fs=bgct&ver=1&t=102"
|
||||
)
|
||||
|
||||
return play_url
|
||||
|
||||
def douyuDetail(self, ids):
|
||||
"""
|
||||
斗鱼播放详情 - 参考最新三合一.js重构
|
||||
核心算法:设备ID生成 -> 获取加密密钥 -> 计算签名 -> 获取播放地址
|
||||
修复:切换分辨率只能播放1秒的问题
|
||||
方案:存储房间号和码率信息,在playerContent中实时获取对应码率的URL
|
||||
"""
|
||||
try:
|
||||
channel = ids[1]
|
||||
headers = self.gethr(0, zr=f'{self.hosts[ids[0]]}/{channel}')
|
||||
|
||||
# 1. 初始化会话和设备ID (参考JS中的initialize和setupDeviceId)
|
||||
session = {}
|
||||
|
||||
# 请求首页获取Cookie
|
||||
try:
|
||||
home_res = self.fetch(f'{self.hosts[ids[0]]}/{channel}', headers=headers)
|
||||
if home_res.headers.get('Set-Cookie'):
|
||||
cookie_str = home_res.headers.get('Set-Cookie')
|
||||
# 解析dy_did
|
||||
did_match = re.search(r'dy_did=([a-f0-9]{32})', cookie_str)
|
||||
if did_match:
|
||||
device_id = did_match.group(1)
|
||||
else:
|
||||
device_id = self._generate_random_hex(32)
|
||||
else:
|
||||
device_id = self._generate_random_hex(32)
|
||||
except:
|
||||
device_id = self._generate_random_hex(32)
|
||||
|
||||
session['dy_did'] = device_id
|
||||
session['mantine-color-scheme-value'] = 'light'
|
||||
|
||||
# 2. 获取房间基本信息
|
||||
betard_res = self.fetch(f'{self.hosts[ids[0]]}/betard/{channel}', headers=headers).json()
|
||||
if not betard_res or not betard_res.get('room'):
|
||||
return self.handle_exception(Exception("获取房间信息失败"))
|
||||
|
||||
room_info = betard_res['room']
|
||||
vname = room_info.get('room_name', '斗鱼直播')
|
||||
|
||||
vod = self.buildvod(
|
||||
vod_name=vname,
|
||||
vod_remarks=room_info.get('second_lvl_name', ''),
|
||||
vod_director=room_info.get('nickname', ''),
|
||||
)
|
||||
|
||||
# 3. 获取安全密钥 (参考JS中的getSecurityKey)
|
||||
sec_url = f"{self.hosts[ids[0]]}/wgapi/livenc/liveweb/websec/getEncryption?did={device_id}"
|
||||
sec_res = self.fetch(sec_url, headers=headers).json()
|
||||
|
||||
if not sec_res or sec_res.get('error') != 0:
|
||||
return self.handle_exception(Exception("获取加密密钥失败"))
|
||||
|
||||
security_data = sec_res['data']
|
||||
secret_key = security_data.get('key')
|
||||
random_str = security_data.get('rand_str')
|
||||
enc_time = security_data.get('enc_time', 1)
|
||||
enc_data = security_data.get('enc_data')
|
||||
|
||||
# 4. 计算签名 (参考JS中的computeSignature)
|
||||
current_time = int(time.time())
|
||||
|
||||
# 迭代计算MD5
|
||||
current = random_str
|
||||
for _ in range(enc_time):
|
||||
current = hashlib.md5(f"{current}{secret_key}".encode()).hexdigest()
|
||||
|
||||
signature = hashlib.md5(f"{current}{secret_key}{channel}{current_time}".encode()).hexdigest()
|
||||
|
||||
# 5. 请求播放地址 (参考JS中的requestStreamData)
|
||||
play_payload = {
|
||||
'enc_data': enc_data,
|
||||
'tt': str(current_time),
|
||||
'did': device_id,
|
||||
'auth': signature,
|
||||
'cdn': '',
|
||||
'rate': '',
|
||||
'hevc': '0',
|
||||
'fa': '0',
|
||||
'ive': '0'
|
||||
}
|
||||
|
||||
play_api = f"{self.hosts[ids[0]]}/lapi/live/getH5PlayV1/{channel}"
|
||||
|
||||
# 构建请求头带Cookie
|
||||
play_headers = headers.copy()
|
||||
cookie_str = '; '.join([f"{k}={v}" for k, v in session.items()])
|
||||
play_headers['Cookie'] = cookie_str
|
||||
play_headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
|
||||
play_res = requests.post(play_api, data=play_payload, headers=play_headers, timeout=10).json()
|
||||
|
||||
if not play_res or play_res.get('error') != 0:
|
||||
# 尝试旧版API
|
||||
play_res = self._try_legacy_douyu_api(channel, device_id, signature, current_time, play_headers)
|
||||
if not play_res:
|
||||
return self.handle_exception(Exception("获取播放地址失败"))
|
||||
|
||||
stream_info = play_res.get('data', {})
|
||||
|
||||
# 6. 检查并更新设备ID (参考JS中的checkAndUpdateDeviceId)
|
||||
rtmp_live = stream_info.get('rtmp_live', '')
|
||||
if rtmp_live:
|
||||
did_match = re.search(r'did=([a-f0-9]{32})', rtmp_live)
|
||||
if did_match and did_match.group(1) != device_id:
|
||||
device_id = did_match.group(1)
|
||||
session['dy_did'] = device_id
|
||||
# 重新请求
|
||||
play_payload['did'] = device_id
|
||||
play_res = requests.post(play_api, data=play_payload, headers=play_headers, timeout=10).json()
|
||||
if play_res and play_res.get('error') == 0:
|
||||
stream_info = play_res.get('data', {})
|
||||
|
||||
# 7. 提取播放URL和多码率信息
|
||||
stream_url = None
|
||||
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
|
||||
stream_url = f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
|
||||
elif stream_info.get('hls_url'):
|
||||
stream_url = stream_info['hls_url']
|
||||
|
||||
if not stream_url:
|
||||
return self.handle_exception(Exception("无法获取播放地址"))
|
||||
|
||||
# 8. 构建多码率选项
|
||||
multirates = stream_info.get('multirates', [])
|
||||
|
||||
# 关键修复:存储房间号和码率信息,而不是直接存储URL
|
||||
# 这样在切换清晰度时可以重新获取对应码率的签名URL
|
||||
qualities = []
|
||||
|
||||
if multirates:
|
||||
# 按码率排序
|
||||
sorted_rates = sorted(multirates, key=lambda x: x.get('bit', 0), reverse=True)
|
||||
for rate in sorted_rates:
|
||||
bit_rate = rate.get('rate', -1)
|
||||
name = rate.get('name', f"{bit_rate}P")
|
||||
|
||||
# 存储格式:码率值,用于playerContent中重新获取URL
|
||||
# 使用特殊标记#来区分这是码率值而不是URL
|
||||
qualities.extend([name, f"#{bit_rate}"])
|
||||
else:
|
||||
# 只有原画
|
||||
qualities = ['原画', '#-1']
|
||||
|
||||
# 同时存储房间号和设备信息,用于重新获取URL
|
||||
# 格式:房间号|设备ID|签名信息(base64编码)
|
||||
session_info = {
|
||||
'channel': channel,
|
||||
'device_id': device_id,
|
||||
'secret_key': secret_key,
|
||||
'random_str': random_str,
|
||||
'enc_time': enc_time,
|
||||
'enc_data': enc_data
|
||||
}
|
||||
encoded_session = self.e64(json.dumps(session_info))
|
||||
|
||||
# 9. 构建播放数据
|
||||
# vod_play_url格式:房间名$平台@@base64(清晰度列表)@@base64(会话信息)
|
||||
encoded_qualities = self.e64(json.dumps(qualities))
|
||||
vod['vod_play_from'] = '斗鱼直播'
|
||||
vod['vod_play_url'] = f"{vname}${ids[0]}@@{encoded_qualities}@@{encoded_session}"
|
||||
|
||||
return vod
|
||||
|
||||
except Exception as e:
|
||||
return self.handle_exception(e)
|
||||
|
||||
def _generate_random_hex(self, length):
|
||||
"""生成随机十六进制字符串"""
|
||||
hex_chars = '0123456789abcdef'
|
||||
return ''.join(random.choice(hex_chars) for _ in range(length))
|
||||
|
||||
def _try_legacy_douyu_api(self, channel, device_id, signature, timestamp, headers):
|
||||
"""尝试使用旧版API获取播放地址"""
|
||||
try:
|
||||
legacy_payload = {
|
||||
'did': device_id,
|
||||
'tt': str(timestamp),
|
||||
'sign': signature,
|
||||
'cdn': '',
|
||||
'rate': '-1',
|
||||
'ver': 'Douyu_223061205',
|
||||
'iar': '1',
|
||||
'ive': '1',
|
||||
'hevc': '0',
|
||||
'fa': '0'
|
||||
}
|
||||
legacy_api = f"https://www.douyu.com/lapi/live/getH5Play/{channel}"
|
||||
res = requests.post(legacy_api, data=legacy_payload, headers=headers, timeout=10)
|
||||
return res.json() if res.status_code == 200 else None
|
||||
except:
|
||||
return None
|
||||
|
||||
def _get_douyu_play_url(self, channel, device_id, secret_key, random_str, enc_time, enc_data, rate):
|
||||
"""
|
||||
获取斗鱼指定码率的播放URL(带签名)
|
||||
用于切换清晰度时重新获取URL
|
||||
"""
|
||||
try:
|
||||
current_time = int(time.time())
|
||||
|
||||
# 重新计算签名
|
||||
current = random_str
|
||||
for _ in range(enc_time):
|
||||
current = hashlib.md5(f"{current}{secret_key}".encode()).hexdigest()
|
||||
|
||||
signature = hashlib.md5(f"{current}{secret_key}{channel}{current_time}".encode()).hexdigest()
|
||||
|
||||
# 构建请求
|
||||
play_payload = {
|
||||
'enc_data': enc_data,
|
||||
'tt': str(current_time),
|
||||
'did': device_id,
|
||||
'auth': signature,
|
||||
'cdn': '',
|
||||
'rate': str(rate) if rate > 0 else '',
|
||||
'hevc': '0',
|
||||
'fa': '0',
|
||||
'ive': '0'
|
||||
}
|
||||
|
||||
play_api = f"https://www.douyu.com/lapi/live/getH5PlayV1/{channel}"
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.headers[0]['User-Agent'],
|
||||
'Referer': f'https://www.douyu.com/{channel}',
|
||||
'Origin': 'https://www.douyu.com',
|
||||
'Cookie': f'dy_did={device_id}; mantine-color-scheme-value=light',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
play_res = requests.post(play_api, data=play_payload, headers=headers, timeout=10).json()
|
||||
|
||||
if not play_res or play_res.get('error') != 0:
|
||||
# 尝试旧版API
|
||||
return self._get_douyu_play_url_legacy(channel, device_id, signature, current_time, rate)
|
||||
|
||||
stream_info = play_res.get('data', {})
|
||||
|
||||
# 检查设备ID是否匹配
|
||||
if stream_info.get('rtmp_live'):
|
||||
did_match = re.search(r'did=([a-f0-9]{32})', stream_info['rtmp_live'])
|
||||
if did_match and did_match.group(1) != device_id:
|
||||
# 设备ID不匹配,使用新设备ID重新获取
|
||||
return self._get_douyu_play_url(channel, did_match.group(1), secret_key, random_str, enc_time, enc_data, rate)
|
||||
|
||||
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
|
||||
return f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
|
||||
elif stream_info.get('hls_url'):
|
||||
return stream_info['hls_url']
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"获取斗鱼播放URL失败: {e}")
|
||||
return None
|
||||
|
||||
def _get_douyu_play_url_legacy(self, channel, device_id, signature, timestamp, rate):
|
||||
"""使用旧版API获取斗鱼播放URL"""
|
||||
try:
|
||||
legacy_payload = {
|
||||
'did': device_id,
|
||||
'tt': str(timestamp),
|
||||
'sign': signature,
|
||||
'cdn': '',
|
||||
'rate': str(rate) if rate > 0 else '-1',
|
||||
'ver': 'Douyu_223061205',
|
||||
'iar': '1',
|
||||
'ive': '1',
|
||||
'hevc': '0',
|
||||
'fa': '0'
|
||||
}
|
||||
legacy_api = f"https://www.douyu.com/lapi/live/getH5Play/{channel}"
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.headers[0]['User-Agent'],
|
||||
'Referer': f'https://www.douyu.com/{channel}',
|
||||
'Cookie': f'dy_did={device_id}',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
res = requests.post(legacy_api, data=legacy_payload, headers=headers, timeout=10)
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
if data.get('error') == 0:
|
||||
stream_info = data.get('data', {})
|
||||
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
|
||||
return f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
pass
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
ids = id.split('@@')
|
||||
p = 1
|
||||
if ids[0] in ['wangyi']:
|
||||
p, url = 0, json.loads(self.d64(ids[1]))
|
||||
elif ids[0] == 'bili':
|
||||
p, url = self.biliplay(ids)
|
||||
elif ids[0] == 'huya':
|
||||
p, url = self.huyaplay(ids)
|
||||
elif ids[0] == 'douyu':
|
||||
p, url = self.douyuplay(ids)
|
||||
return {'parse': p, 'url': url, 'header': self.playheaders[ids[0]]}
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': self.excepturl, 'header': self.headers[0]}
|
||||
|
||||
def biliplay(self, ids):
|
||||
"""
|
||||
B站播放解析 - 使用playUrl接口获取指定清晰度直播流
|
||||
ids: [平台, 房间号, 清晰度qn]
|
||||
支持多线路返回
|
||||
"""
|
||||
try:
|
||||
room_id = ids[1]
|
||||
qn = ids[2] if len(ids) > 2 else '10000'
|
||||
|
||||
# 使用playUrl接口获取直播流
|
||||
play_url = f'{self.hosts["bili"][0]}/room/v1/Room/playUrl?cid={room_id}&qn={qn}&platform=web'
|
||||
data = self.fetch(play_url, headers={
|
||||
**self.headers[0],
|
||||
'Referer': 'https://live.bilibili.com/',
|
||||
'Origin': 'https://live.bilibili.com'
|
||||
}).json()
|
||||
|
||||
if data.get('code') != 0:
|
||||
return 1, self.excepturl
|
||||
|
||||
play_data = data['data']
|
||||
durl_list = play_data.get('durl', [])
|
||||
|
||||
if not durl_list:
|
||||
return 1, self.excepturl
|
||||
|
||||
# 构建多线路结果 [线路1, URL1, 线路2, URL2, ...]
|
||||
urls = []
|
||||
for idx, item in enumerate(durl_list, 1):
|
||||
url = item.get('url')
|
||||
if url:
|
||||
urls.extend([f'线路{idx}', url])
|
||||
|
||||
# 如果只有一条线路,直接返回URL
|
||||
if len(urls) == 2:
|
||||
return 0, urls[1] # 直接返回URL字符串
|
||||
|
||||
return 0, urls
|
||||
|
||||
except Exception as e:
|
||||
print(f"B站播放错误: {e}")
|
||||
return 1, self.excepturl
|
||||
|
||||
def huyaplay(self, ids):
|
||||
"""
|
||||
虎牙播放解析 - 返回所有清晰度选项供用户选择
|
||||
ids[1] 格式: base64编码的 [清晰度名称1, URL1, 清晰度名称2, URL2, ...]
|
||||
"""
|
||||
try:
|
||||
# ids[1] 是编码后的播放地址列表 [名称1, URL1, 名称2, URL2, ...]
|
||||
decoded = json.loads(self.d64(ids[1]))
|
||||
# decoded 是一个列表,奇数索引是名称,偶数索引是URL
|
||||
return 0, decoded
|
||||
except Exception as e:
|
||||
print(f"虎牙播放解析错误: {e}")
|
||||
return 1, self.excepturl
|
||||
|
||||
def douyuplay(self, ids):
|
||||
"""
|
||||
斗鱼播放解析 - 实时获取对应码率的播放URL
|
||||
ids格式: [平台, base64(清晰度列表), base64(会话信息)]
|
||||
清晰度列表: [名称1, #码率1, 名称2, #码率2, ...]
|
||||
#表示这是码率值,需要重新获取URL
|
||||
"""
|
||||
try:
|
||||
if len(ids) < 3:
|
||||
# 兼容旧格式
|
||||
decoded = json.loads(self.d64(ids[1]))
|
||||
return 0, decoded
|
||||
|
||||
# 解析清晰度列表和会话信息
|
||||
qualities = json.loads(self.d64(ids[1]))
|
||||
session_info = json.loads(self.d64(ids[2]))
|
||||
|
||||
channel = session_info['channel']
|
||||
device_id = session_info['device_id']
|
||||
secret_key = session_info['secret_key']
|
||||
random_str = session_info['random_str']
|
||||
enc_time = session_info['enc_time']
|
||||
enc_data = session_info['enc_data']
|
||||
|
||||
# 为每个清晰度实时获取播放URL
|
||||
result = []
|
||||
for i in range(0, len(qualities), 2):
|
||||
name = qualities[i]
|
||||
rate_marker = qualities[i + 1]
|
||||
|
||||
# 解析码率值(去掉#前缀)
|
||||
if rate_marker.startswith('#'):
|
||||
rate = int(rate_marker[1:])
|
||||
else:
|
||||
rate = -1
|
||||
|
||||
# 实时获取对应码率的URL
|
||||
play_url = self._get_douyu_play_url(
|
||||
channel, device_id, secret_key, random_str,
|
||||
enc_time, enc_data, rate
|
||||
)
|
||||
|
||||
if play_url:
|
||||
result.extend([name, play_url])
|
||||
|
||||
if not result:
|
||||
return 1, self.excepturl
|
||||
|
||||
return 0, result
|
||||
except Exception as e:
|
||||
print(f"斗鱼播放解析错误: {e}")
|
||||
return 1, self.excepturl
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def josn_to_params(self, params, skip_empty=False):
|
||||
query = []
|
||||
for k, v in params.items():
|
||||
if skip_empty and not v:
|
||||
continue
|
||||
query.append(f"{k}={v}")
|
||||
return "&".join(query)
|
||||
|
||||
def params_to_json(self, query_string):
|
||||
parsed_data = parse_qs(query_string)
|
||||
result = {key: value[0] for key, value in parsed_data.items()}
|
||||
return result
|
||||
|
||||
def buildvod(self, vod_id='', vod_name='', vod_pic='', vod_year='', vod_tag='', vod_remarks='', style='',
|
||||
type_name='', vod_area='', vod_actor='', vod_director='',
|
||||
vod_content='', vod_play_from='', vod_play_url=''):
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': vod_name,
|
||||
'vod_pic': vod_pic,
|
||||
'vod_year': vod_year,
|
||||
'vod_tag': 'folder' if vod_tag else '',
|
||||
'vod_remarks': vod_remarks,
|
||||
'style': style,
|
||||
'type_name': type_name,
|
||||
'vod_area': vod_area,
|
||||
'vod_actor': vod_actor,
|
||||
'vod_director': vod_director,
|
||||
'vod_content': vod_content,
|
||||
'vod_play_from': vod_play_from,
|
||||
'vod_play_url': vod_play_url
|
||||
}
|
||||
vod = {key: value for key, value in vod.items() if value}
|
||||
return vod
|
||||
|
||||
def getpq(self, url, headers=None, cookies=None):
|
||||
data = self.fetch(url, headers=headers, cookies=cookies).text
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"解析页面错误: {str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def gethr(self, index, rf='', zr=''):
|
||||
headers = self.headers[index]
|
||||
if zr:
|
||||
headers['referer'] = zr
|
||||
else:
|
||||
headers['referer'] = f"{self.referers[rf]}/"
|
||||
return headers
|
||||
|
||||
def handle_exception(self, e):
|
||||
print(f"报错: {str(e)}")
|
||||
return {'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'翻车啦${self.excepturl}'}
|
||||
@@ -0,0 +1,396 @@
|
||||
from base.spider import Spider
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import base64
|
||||
from urllib.parse import quote
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "小心儿悠悠"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateId = [
|
||||
{"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": "0"}
|
||||
]
|
||||
result['class'] = cateId
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = self.categoryContent("1", 1, False, {})
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
url = f"http://wapi.kuwo.cn/api/www/artist/artistInfo?category={tid}&prefix=&pn={pg}&rn=30"
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'http://www.kuwo.cn/'
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.get(url, headers=headers, timeout=10)
|
||||
data = r.json()
|
||||
videos = []
|
||||
if data.get('data') and data['data'].get('artistList'):
|
||||
for item in data['data']['artistList']:
|
||||
video = {
|
||||
"vod_id": str(item.get('id', '')),
|
||||
"vod_name": item.get('name', ''),
|
||||
"vod_pic": item.get('pic300') or item.get('pic') or item.get('pic120', ''),
|
||||
"vod_remarks": f""
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
|
||||
except Exception as e:
|
||||
result['list'] = []
|
||||
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
rid = ids[0]
|
||||
result = {}
|
||||
|
||||
info_url = f"http://wapi.kuwo.cn/api/www/artist/artist?artistid={rid}"
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'http://www.kuwo.cn/'
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.get(info_url, headers=headers, timeout=10)
|
||||
info_data = r.json().get('data', {})
|
||||
|
||||
artist_name = info_data.get('name', '')
|
||||
|
||||
all_songs = self._get_artist_songs(rid)
|
||||
|
||||
artist_info = info_data.get('info', '')
|
||||
artist_info = re.sub(r'<[^>]+>', '', artist_info)
|
||||
artist_info = artist_info.replace(' ', ' ')
|
||||
artist_info = artist_info.replace('\r\n', '\n').replace('\r', '\n')
|
||||
artist_info = artist_info.strip()
|
||||
|
||||
max_songs = 300
|
||||
if len(all_songs) > max_songs:
|
||||
all_songs = all_songs[:max_songs]
|
||||
|
||||
play_arr = []
|
||||
for i, song in enumerate(all_songs):
|
||||
name = re.sub(r'[$#]', '', song.get('name', '')).strip()
|
||||
song_id = song.get('rid', '')
|
||||
album = song.get('album', '')
|
||||
|
||||
if album:
|
||||
play_arr.append(f"{name} - {album}${song_id}")
|
||||
else:
|
||||
play_arr.append(f"{name}${song_id}")
|
||||
|
||||
vod = {
|
||||
"vod_id": rid,
|
||||
"vod_name": artist_name,
|
||||
"vod_pic": info_data.get('pic300') or info_data.get('pic', ''),
|
||||
"vod_content": artist_info if artist_info else "暂无歌手简介",
|
||||
"vod_remarks": f"歌曲 : {len(all_songs)}首",
|
||||
"vod_actor": artist_name,
|
||||
"vod_play_from": "酷我音乐",
|
||||
"vod_play_url": "#".join(play_arr)
|
||||
}
|
||||
|
||||
result['list'] = [vod]
|
||||
|
||||
except Exception as e:
|
||||
vod = {
|
||||
"vod_id": rid,
|
||||
"vod_name": "加载失败",
|
||||
"vod_content": f"加载歌手信息失败: {str(e)}",
|
||||
"vod_remarks": "加载失败",
|
||||
"vod_actor": "未知",
|
||||
"vod_play_from": "酷我音乐",
|
||||
"vod_play_url": ""
|
||||
}
|
||||
result['list'] = [vod]
|
||||
|
||||
return result
|
||||
|
||||
def _get_artist_songs(self, rid):
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'http://www.kuwo.cn/'
|
||||
}
|
||||
|
||||
songs = []
|
||||
max_pages = 10
|
||||
|
||||
for page in range(1, max_pages + 1):
|
||||
try:
|
||||
url = f"http://wapi.kuwo.cn/api/www/artist/artistMusic?artistid={rid}&pn={page}&rn=30"
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') == 200:
|
||||
music_data = data.get('data', {})
|
||||
song_list = music_data.get('list', [])
|
||||
|
||||
if not song_list:
|
||||
break
|
||||
|
||||
for song in song_list:
|
||||
song_name = song.get('name', '').strip()
|
||||
if song_name:
|
||||
songs.append({
|
||||
'name': song_name,
|
||||
'rid': song.get('rid', ''),
|
||||
'album': song.get('album', ''),
|
||||
'duration': song.get('duration', '')
|
||||
})
|
||||
|
||||
if len(songs) >= 300:
|
||||
songs = songs[:300]
|
||||
break
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return songs
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
rid = id
|
||||
|
||||
qualities = []
|
||||
|
||||
quality_list = [
|
||||
("无损FLAC", 2000, "flac"),
|
||||
("高品质320K", 320, "mp3"),
|
||||
("标准128K", 128, "mp3")
|
||||
]
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10)',
|
||||
'Referer': 'https://www.kuwo.cn/'
|
||||
}
|
||||
|
||||
for quality_name, bitrate, format_type in quality_list:
|
||||
try:
|
||||
api_url = f"https://nmobi.kuwo.cn/mobi.s?f=web&user=0&source=kwplayer_ar_4.4.2.7_B_nuoweida_vh.apk&type=convert_url_with_sign&rid={rid}&bitrate={bitrate}&format={format_type}"
|
||||
r = requests.get(api_url, headers=headers, timeout=5)
|
||||
data = r.json()
|
||||
if data.get('code') == 200 and data.get('data') and data['data'].get('url'):
|
||||
qualities.append((quality_name, data['data']['url']))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not qualities:
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ""
|
||||
result["url"] = ""
|
||||
result["header"] = {}
|
||||
return result
|
||||
|
||||
urls = []
|
||||
for quality_name, quality_url in qualities:
|
||||
urls.append(quality_name)
|
||||
urls.append(quality_url)
|
||||
|
||||
lrc = ""
|
||||
pic = ""
|
||||
|
||||
try:
|
||||
lrc_api = f"https://kuwo.cn/openapi/v1/www/lyric/getlyric?musicId={rid}"
|
||||
lr = requests.get(lrc_api, timeout=5)
|
||||
lj = lr.json()
|
||||
if lj.get('data') and lj['data'].get('lrclist'):
|
||||
lrc = "\n".join([f"[{self._format_time(float(item.get('time', 0)))}]{item.get('lineLyric', '')}"
|
||||
for item in lj['data']['lrclist']])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
pic_url = f"http://artistpicserver.kuwo.cn/pic.web?type=rid_pic&pictype=url&size=500&rid={rid}"
|
||||
pr = requests.get(pic_url, timeout=5)
|
||||
if pr.text.startswith('http'):
|
||||
pic = pr.text.strip()
|
||||
else:
|
||||
pic = pic_url
|
||||
except Exception:
|
||||
pic = f"http://artistpicserver.kuwo.cn/pic.web?type=rid_pic&pictype=url&size=500&rid={rid}"
|
||||
|
||||
if lrc:
|
||||
try:
|
||||
ssa_lrc = self._create_ssa_subtitle(lrc)
|
||||
ssa_base64 = base64.b64encode(ssa_lrc.encode('utf-8')).decode('utf-8')
|
||||
ssa_url = f"data:text/x-ssa;base64,{ssa_base64}"
|
||||
|
||||
result["subs"] = [{
|
||||
"name": "5行歌词",
|
||||
"url": ssa_url,
|
||||
"format": "text/x-ssa",
|
||||
"selected": True
|
||||
}]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ""
|
||||
result["url"] = urls
|
||||
result["header"] = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": "https://www.kuwo.cn/"
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def _format_time(self, seconds):
|
||||
m = int(seconds // 60)
|
||||
s = seconds % 60
|
||||
return f"{m:02d}:{s:05.2f}"
|
||||
|
||||
def _create_ssa_subtitle(self, lrc_text):
|
||||
lines = []
|
||||
pattern = r'\[(\d{2}):(\d{2})\.(\d{2})\](.*)'
|
||||
|
||||
for line in lrc_text.split('\n'):
|
||||
match = re.match(pattern, line)
|
||||
if match:
|
||||
minutes = int(match.group(1))
|
||||
seconds = int(match.group(2))
|
||||
hundredths = int(match.group(3))
|
||||
text = match.group(4).strip()
|
||||
|
||||
total_seconds = minutes * 60 + seconds + hundredths / 100.0
|
||||
if text:
|
||||
lines.append({
|
||||
'start': total_seconds,
|
||||
'text': text
|
||||
})
|
||||
|
||||
if not lines:
|
||||
return ""
|
||||
|
||||
ssa_header = """[Script Info]
|
||||
ScriptType: v4.00+
|
||||
Collisions: Normal
|
||||
PlayResX: 1280
|
||||
PlayResY: 720
|
||||
Timer: 100.0000
|
||||
WrapStyle: 0
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: WAITING_TOP2,Roboto,55,&H0000FFFF,&H00808080,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,1,2,0,0,180,1
|
||||
Style: WAITING_TOP1,Roboto,55,&H0000FFFF,&H00808080,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,1,2,0,0,260,1
|
||||
Style: PLAYING_CENTER,Roboto,60,&H0000FF00,&H00808080,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,2,2,2,0,0,340,1
|
||||
Style: PLAYED_BOTTOM1,Roboto,55,&H0000FFFF,&H00808080,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,1,2,0,0,420,1
|
||||
Style: PLAYED_BOTTOM2,Roboto,55,&H0000FFFF,&H00808080,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1,1,2,0,0,500,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
"""
|
||||
|
||||
def format_ssa_time(seconds):
|
||||
h = int(seconds // 3600)
|
||||
m = int((seconds % 3600) // 60)
|
||||
s = int(seconds % 60)
|
||||
cs = int((seconds * 100) % 100)
|
||||
return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
|
||||
|
||||
events = []
|
||||
|
||||
for i in range(len(lines)):
|
||||
current = lines[i]
|
||||
current_end = lines[i+1]['start'] if i+1 < len(lines) else current['start'] + 5.0
|
||||
|
||||
wait2 = lines[i+2] if i+2 < len(lines) else None
|
||||
wait1 = lines[i+1] if i+1 < len(lines) else None
|
||||
played1 = lines[i-1] if i-1 >= 0 else None
|
||||
played2 = lines[i-2] if i-2 >= 0 else None
|
||||
|
||||
start_str = format_ssa_time(current['start'])
|
||||
end_str = format_ssa_time(current_end)
|
||||
|
||||
if wait2:
|
||||
events.append(f"Dialogue: 1,{start_str},{end_str},WAITING_TOP2,,0,0,0,,{wait2['text']}")
|
||||
|
||||
if wait1:
|
||||
events.append(f"Dialogue: 2,{start_str},{end_str},WAITING_TOP1,,0,0,0,,{wait1['text']}")
|
||||
|
||||
events.append(f"Dialogue: 3,{start_str},{end_str},PLAYING_CENTER,,0,0,0,,{current['text']}")
|
||||
|
||||
if played1:
|
||||
events.append(f"Dialogue: 4,{start_str},{end_str},PLAYED_BOTTOM1,,0,0,0,,{played1['text']}")
|
||||
|
||||
if played2:
|
||||
events.append(f"Dialogue: 5,{start_str},{end_str},PLAYED_BOTTOM2,,0,0,0,,{played2['text']}")
|
||||
|
||||
return ssa_header + "\n".join(events)
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
result = {}
|
||||
wd = quote(key)
|
||||
page_num = (int(pg) - 1) * 30
|
||||
url = f"https://search.kuwo.cn/r.s?client=kt&pn={page_num}&rn=30&all={wd}&vipver=1&ft=artist&encoding=utf8&rformat=json&mobi=1"
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Referer': 'http://www.kuwo.cn/'
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.get(url, headers=headers, timeout=10)
|
||||
data = r.json()
|
||||
videos = []
|
||||
if data.get('abslist'):
|
||||
base_path = data.get('BASEPICPATH', 'http://img1.kuwo.cn/star/starheads/')
|
||||
for item in data['abslist']:
|
||||
aid = item.get('ARTISTID') or item.get('DC_TARGETID', '')
|
||||
pic = item.get('hts_PICPATH') or (base_path + item['PICPATH'] if item.get('PICPATH') else '')
|
||||
video = {
|
||||
"vod_id": str(aid),
|
||||
"vod_name": item.get('ARTIST', ''),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": f"歌曲 : {item.get('SONGNUM', 0)}首"
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 30
|
||||
result['total'] = 999999
|
||||
|
||||
except Exception as e:
|
||||
result['list'] = []
|
||||
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
return self.searchContent(key, quick, pg)
|
||||
|
||||
def localProxy(self, param):
|
||||
return None
|
||||
Reference in New Issue
Block a user