Sync all projects
This commit is contained in:
+14
-2
@@ -126,6 +126,12 @@
|
||||
"api":"csp_yt",
|
||||
"homePage":"https://fgblh.github.io/uhuj.github.io/youtube.html"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"name": "🐬嘀嗒影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/嘀嗒影视.py"
|
||||
},
|
||||
{
|
||||
"key": "kkvm",
|
||||
"name": "🐬可可影视.py[追剧]",
|
||||
@@ -163,7 +169,7 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/神马影视.py"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"key": "ddyy",
|
||||
"name": "🐬低端影视.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/低端影视.py"
|
||||
@@ -724,9 +730,15 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/短剧聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kwms",
|
||||
"name": "🐬酷我音乐.py[音乐]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/酷我音乐.py"
|
||||
},
|
||||
{
|
||||
"key": "捡的音乐",
|
||||
"name": "🐬爱听音乐",
|
||||
"name": "🐬爱听音乐[音乐]",
|
||||
"type": 3,
|
||||
"api": "csp_Music1",
|
||||
"searchable": 1,
|
||||
|
||||
+13
-1
@@ -71,6 +71,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/皮皮虾.py",
|
||||
"ext": "http://43.248.117.123:4680"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"name": "🐬嘀嗒影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/嘀嗒影视.py"
|
||||
},
|
||||
{
|
||||
"key": "kkvm",
|
||||
"name": "🐬可可影视.py[追剧]",
|
||||
@@ -108,7 +114,7 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/神马影视.py"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"key": "ddyy",
|
||||
"name": "🐬低端影视.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/低端影视.py"
|
||||
@@ -323,6 +329,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/短剧聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kwms",
|
||||
"name": "🐬酷我音乐.py[音乐]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/酷我音乐.py"
|
||||
},
|
||||
{
|
||||
"key": "shjb",
|
||||
"name": "🐬色播聚合.py|🔞[成人直播]",
|
||||
|
||||
+13
-1
@@ -126,6 +126,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/皮皮虾.py",
|
||||
"ext": "http://43.248.117.123:4680"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"name": "🐬嘀嗒影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/嘀嗒影视.py"
|
||||
},
|
||||
{
|
||||
"key": "kkvm",
|
||||
"name": "🐬可可影视.py",
|
||||
@@ -163,7 +169,7 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/神马影视.py"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"key": "ddyy",
|
||||
"name": "🐬低端影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/低端影视.py"
|
||||
@@ -678,6 +684,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/短剧聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kwms",
|
||||
"name": "🐬酷我音乐.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/酷我音乐.py"
|
||||
},
|
||||
{
|
||||
"key": "捡的音乐",
|
||||
"name": "🐬爱听音乐",
|
||||
|
||||
+24
-1
@@ -88,7 +88,7 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/油管.py"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"key": "ddyy",
|
||||
"name": "🐬低端影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/低端影视.py"
|
||||
@@ -106,6 +106,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/皮皮虾.py",
|
||||
"ext": "http://43.248.117.123:4680"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"name": "🐬嘀嗒影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/嘀嗒影视.py"
|
||||
},
|
||||
{
|
||||
"key": "kkvm",
|
||||
"name": "🐬可可影视.py",
|
||||
@@ -657,6 +663,23 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/短剧聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kwms",
|
||||
"name": "🐬酷我音乐.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/酷我音乐.py"
|
||||
},
|
||||
{
|
||||
"key": "捡的音乐",
|
||||
"name": "🐬爱听音乐",
|
||||
"type": 3,
|
||||
"api": "csp_Music1",
|
||||
"searchable": 1,
|
||||
"quickSearch": 1,
|
||||
"ext": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/jar_js/Music1.json",
|
||||
"jar": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/jar_js/so.jar",
|
||||
"filterable": 1
|
||||
},
|
||||
{
|
||||
"key": "shjb",
|
||||
"name": "🐬色播聚合.py|🔞",
|
||||
|
||||
+16
-4
@@ -52,10 +52,10 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/枫叶影院.py"
|
||||
},
|
||||
{
|
||||
"key": "dytt",
|
||||
"name": "🐬电影天堂.py",
|
||||
"key": "ddvm",
|
||||
"name": "🐬嘀嗒影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/电影天堂.py"
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/嘀嗒影视.py"
|
||||
},
|
||||
{
|
||||
"key": "kkvm",
|
||||
@@ -113,7 +113,7 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/布布追剧.py"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"key": "ddyy",
|
||||
"name": "🐬低端影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/低端影视.py"
|
||||
@@ -340,12 +340,24 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/河马剧场.py"
|
||||
},
|
||||
{
|
||||
"key": "wddj",
|
||||
"name": "🐬黄豆短剧.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/黄豆短剧.py"
|
||||
},
|
||||
{
|
||||
"key": "djjh",
|
||||
"name": "🐬短剧聚合.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/短剧聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kwms",
|
||||
"name": "🐬酷我音乐.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/酷我音乐.py"
|
||||
},
|
||||
{
|
||||
"key": "shjb",
|
||||
"name": "🐬色播聚合.py|🔞",
|
||||
|
||||
+13
-1
@@ -88,7 +88,7 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/油管.py"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"key": "ddyy",
|
||||
"name": "🐬低端影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/低端影视.py"
|
||||
@@ -106,6 +106,12 @@
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/皮皮虾.py",
|
||||
"ext": "http://43.248.117.123:4680"
|
||||
},
|
||||
{
|
||||
"key": "ddvm",
|
||||
"name": "🐬嘀嗒影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/嘀嗒影视.py"
|
||||
},
|
||||
{
|
||||
"key": "kkvm",
|
||||
"name": "🐬可可影视.py",
|
||||
@@ -618,6 +624,12 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/短剧聚合.py"
|
||||
},
|
||||
{
|
||||
"key": "kwms",
|
||||
"name": "🐬酷我音乐.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/酷我音乐.py"
|
||||
},
|
||||
{
|
||||
"key": "捡的音乐",
|
||||
"name": "🐬爱听音乐",
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# FongMi/TVBox Python Spider - 嘀嗒影视 didahd.xyz
|
||||
import re, json, html, base64, binascii, hashlib, time
|
||||
from urllib.parse import urljoin, quote, unquote
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
except Exception:
|
||||
AES = None
|
||||
def unpad(data, bs): return data
|
||||
|
||||
try:
|
||||
from base.spider import Spider as BaseSpider
|
||||
except Exception:
|
||||
class BaseSpider(object):
|
||||
def fetch(self, url, headers=None, timeout=15, **kwargs):
|
||||
import requests
|
||||
return requests.get(url, headers=headers, timeout=timeout, verify=False)
|
||||
def post(self, url, headers=None, data=None, timeout=15, **kwargs):
|
||||
import requests
|
||||
return requests.post(url, headers=headers, data=data, timeout=timeout, verify=False)
|
||||
|
||||
class Spider(BaseSpider):
|
||||
def __init__(self):
|
||||
self.host = 'https://www.didahd.xyz'
|
||||
self.headers = {
|
||||
'User-Agent':'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36',
|
||||
'Referer':self.host + '/',
|
||||
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||
}
|
||||
self.classes = [
|
||||
{'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':'综艺'}
|
||||
]
|
||||
|
||||
def getName(self): return '嘀嗒影视'
|
||||
def getDependence(self): return []
|
||||
def init(self, extend=''): pass
|
||||
def isVideoFormat(self, url): return bool(re.search(r'\.(m3u8|mp4|flv|mkv)(\?|$)', str(url), re.I))
|
||||
def manualVideoCheck(self): return True
|
||||
def action(self, action): return None
|
||||
def destroy(self): pass
|
||||
def liveContent(self, url): return {'list': []}
|
||||
def localProxy(self, param): return [404, 'text/plain', 'Not Found']
|
||||
|
||||
def log(self, msg):
|
||||
try: print('[嘀嗒影视] ' + str(msg))
|
||||
except Exception: pass
|
||||
|
||||
def getHtml(self, url, referer=None):
|
||||
if not url.startswith('http'): url = urljoin(self.host, url)
|
||||
h = dict(self.headers)
|
||||
if referer: h['Referer'] = referer
|
||||
try:
|
||||
r = self.fetch(url, headers=h, timeout=15)
|
||||
if hasattr(r, 'content'):
|
||||
enc = getattr(r, 'encoding', None) or 'utf-8'
|
||||
return r.content.decode(enc, 'ignore')
|
||||
return getattr(r, 'text', '') or ''
|
||||
except Exception as e:
|
||||
self.log('请求失败 %s %s' % (url, e)); return ''
|
||||
|
||||
def postHtml(self, url, data, referer=None):
|
||||
if not url.startswith('http'): url = urljoin(self.host, url)
|
||||
h = dict(self.headers)
|
||||
if referer: h['Referer'] = referer
|
||||
h['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
try:
|
||||
if hasattr(super(), 'post'):
|
||||
r = self.post(url, headers=h, data=data, timeout=15)
|
||||
else:
|
||||
raise Exception('no post')
|
||||
if hasattr(r, 'content'):
|
||||
enc = getattr(r, 'encoding', None) or 'utf-8'
|
||||
return r.content.decode(enc, 'ignore')
|
||||
return getattr(r, 'text', '') or ''
|
||||
except Exception as e:
|
||||
self.log('POST失败 %s %s' % (url, e)); return ''
|
||||
|
||||
def clean(self, s):
|
||||
s = html.unescape(str(s or ''))
|
||||
s = re.sub(r'<script[\s\S]*?</script>|<style[\s\S]*?</style>', ' ', s, flags=re.I)
|
||||
s = re.sub(r'<[^>]+>', ' ', s)
|
||||
return re.sub(r'\s+', ' ', s).strip()
|
||||
|
||||
def fix(self, u):
|
||||
if not u: return ''
|
||||
u = html.unescape(str(u)).replace('\\/', '/').strip()
|
||||
return urljoin(self.host, u)
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {'class': self.classes, 'filters': self.makeFilters() if filter else {}}
|
||||
|
||||
def makeFilters(self):
|
||||
years = [{'n':'全部','v':''}] + [{'n':str(y),'v':str(y)} for y in range(2026, 2009, -1)]
|
||||
areas = [{'n':'全部','v':''}] + [{'n':x,'v':x} for x in ['大陆','香港','台湾','美国','日本','韩国','英国','法国','德国','泰国','印度','其它']]
|
||||
langs = [{'n':'全部','v':''}] + [{'n':x,'v':x} for x in ['国语','英语','粤语','韩语','日语','泰语','其它']]
|
||||
bys = [{'n':'时间','v':'time'},{'n':'人气','v':'hits'},{'n':'评分','v':'score'}]
|
||||
letters = [{'n':'全部','v':''}] + [{'n':c,'v':c} for c in list('ABCDEFGHIJKLMNOPQRSTUVWXYZ')] + [{'n':'0-9','v':'0-9'}]
|
||||
fs = [{'key':'area','name':'地区','value':areas},{'key':'year','name':'年份','value':years},{'key':'lang','name':'语言','value':langs},{'key':'letter','name':'字母','value':letters},{'key':'by','name':'排序','value':bys}]
|
||||
return {c['type_id']:fs for c in self.classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {'list': self.parseList(self.getHtml(self.host + '/'))[:30]}
|
||||
|
||||
def buildCategoryUrl(self, tid, pg, extend):
|
||||
pg = str(pg or '1'); ext = extend or {}
|
||||
area = str(ext.get('area','') or '')
|
||||
by = str(ext.get('by','') or '')
|
||||
lang = str(ext.get('lang','') or '')
|
||||
letter = str(ext.get('letter','') or '')
|
||||
year = str(ext.get('year','') or '')
|
||||
if any([area, by, lang, letter, year]):
|
||||
# 真实 href 是 12 段 join:tid-area-by-lang-空-空-空-空-pg-空-空-year
|
||||
# 例:/show/1-----------2025.html -> /show/1--------2---2025.html
|
||||
p = '' if pg == '1' else pg
|
||||
fields = [str(tid), area, by, lang, '', '', '', '', p, '', '', year]
|
||||
return self.host + '/show/' + '-'.join(fields) + '.html'
|
||||
if pg == '1': return self.host + '/type/%s.html' % tid
|
||||
return self.host + '/type/%s-%s.html' % (tid, pg)
|
||||
|
||||
def isNoResultPage(self, txt):
|
||||
return bool(re.search(r'没有找到您想要的结果|没有找到.*?结果|搜索无结果|暂无数据', txt or '', re.I))
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
url = self.buildCategoryUrl(tid, pg, extend or {})
|
||||
txt = self.getHtml(url, self.host + '/')
|
||||
vods = [] if self.isNoResultPage(txt) else self.parseList(txt)
|
||||
return {'list':vods, 'page':int(pg or 1), 'pagecount':999999 if vods else int(pg or 1), 'limit':len(vods), 'total':999999 if vods else 0}
|
||||
|
||||
def parseList(self, txt):
|
||||
vods, seen = [], set()
|
||||
blocks = re.findall(r'(<a\b(?=[^>]*class=["\'][^"\']*myui-vodlist__thumb[^"\']*["\'])(?=[^>]*href=["\'][^"\']*/detail/\d+\.html["\'])[\s\S]*?</a>)', txt or '', re.I)
|
||||
if not blocks:
|
||||
blocks = re.findall(r'(<div\b[^>]*class=["\'][^"\']*myui-vodlist__box[^"\']*["\'][\s\S]*?</div>\s*</div>)', txt or '', re.I)
|
||||
if not blocks:
|
||||
blocks = re.findall(r'(<a\b[^>]+href=["\'][^"\']*/detail/\d+\.html["\'][\s\S]*?</a>)', txt or '', re.I)
|
||||
for b in blocks:
|
||||
try:
|
||||
hm = re.search(r'href=["\']([^"\']*/detail/(\d+)\.html)["\']', b, re.I)
|
||||
if not hm: continue
|
||||
vid = self.fix(hm.group(1))
|
||||
if vid in seen: continue
|
||||
seen.add(vid)
|
||||
tm = re.search(r'title=["\']([^"\']+)["\']', b, re.I) or re.search(r'alt=["\']([^"\']+)["\']', b, re.I) or re.search(r'<h4[^>]*>[\s\S]*?<a[^>]*>([\s\S]*?)</a>', b, re.I)
|
||||
title = self.clean(tm.group(1)) if tm else ''
|
||||
pm = re.search(r'(?:data-original|data-src)=["\']([^"\']+)["\']', b, re.I) or re.search(r'<img[^>]+src=["\']((?!/template/|/static/)[^"\']+)["\']', b, re.I)
|
||||
rm = re.search(r'<span[^>]*class=["\'][^"\']*pic-text[^"\']*["\'][^>]*>([\s\S]*?)</span>', b, re.I)
|
||||
if title:
|
||||
vods.append({'vod_id':vid,'vod_name':title,'vod_pic':self.fix(pm.group(1)) if pm else '', 'vod_remarks':self.clean(rm.group(1)) if rm else ''})
|
||||
except Exception as e:
|
||||
self.log('列表单条失败 %s' % e)
|
||||
return vods
|
||||
|
||||
def detailContent(self, ids):
|
||||
url = ids[0]
|
||||
txt = self.getHtml(url, self.host + '/')
|
||||
mt = re.search(r'<h1[^>]*class=["\'][^"\']*title[^"\']*["\'][^>]*>([\s\S]*?)</h1>', txt, re.I) or re.search(r'<title>(.*?)\s*-\s*嘀嗒影视', txt, re.S)
|
||||
title = self.clean(mt.group(1)) if mt else ''
|
||||
pic_block = re.search(r'<a[^>]*class=["\'][^"\']*myui-vodlist__thumb[^"\']*picture[^"\']*["\'][\s\S]*?</a>', txt, re.I)
|
||||
picm = None
|
||||
if pic_block:
|
||||
pb = pic_block.group(0)
|
||||
picm = re.search(r'(?:data-original|data-src)=["\']([^"\']+)["\']', pb, re.I) or re.search(r'<img[^>]+src=["\']((?!/template/|/static/)[^"\']+)["\']', pb, re.I)
|
||||
def info(name):
|
||||
m = re.search(r'<span[^>]*class=["\'][^"\']*text-muted[^"\']*["\'][^>]*>%s[::]</span>([\s\S]*?)(?:<span[^>]*class=["\'][^"\']*split-line|</p>)' % name, txt, re.I)
|
||||
return self.clean(m.group(1)) if m else ''
|
||||
cm = re.search(r'剧情简介[::]</span>[\s\S]*?<span>([\s\S]*?)</span>', txt, re.I) or re.search(r'剧情简介[::]</span>([\s\S]*?)<br', txt, re.I)
|
||||
content = self.clean(cm.group(1)) if cm else ''
|
||||
tab_area = re.search(r'<ul[^>]*class=["\'][^"\']*nav-tabs[^"\']*active[^"\']*["\'][^>]*>([\s\S]*?)</ul>', txt, re.I)
|
||||
names = [self.clean(x[1]) for x in re.findall(r'href=["\']#playlist(\d+)["\'][^>]*>([\s\S]*?)</a>', tab_area.group(1) if tab_area else '', re.I)]
|
||||
groups = []
|
||||
for m in re.finditer(r'<div[^>]*id=["\']playlist(\d+)["\'][^>]*>([\s\S]*?)(?=<div[^>]*id=["\']playlist\d+["\']|</div>\s*</div>\s*<!--|<!-- 下载地址|$)', txt, re.I):
|
||||
groups.append(m.group(2))
|
||||
play_from, play_url = [], []
|
||||
for i,g in enumerate(groups):
|
||||
eps, used = [], set()
|
||||
for h,n in re.findall(r'<a\b[^>]+href=["\']([^"\']*/play/\d+-\d+-\d+\.html)["\'][^>]*>([\s\S]*?)</a>', g, re.I):
|
||||
fu = self.fix(h)
|
||||
if fu in used: continue
|
||||
used.add(fu)
|
||||
name = self.clean(n) or ('第%d集' % (len(eps)+1))
|
||||
eps.append(name + '$' + fu)
|
||||
if eps:
|
||||
line = names[i] if i < len(names) and names[i] else '线路%d' % (i+1)
|
||||
if re.search(r'网盘|云盘|夸克|百度|UC|PikPak|阿里', line, re.I):
|
||||
continue
|
||||
play_from.append(line); play_url.append('#'.join(eps))
|
||||
if not play_url:
|
||||
eps=[]
|
||||
for h,n in re.findall(r'href=["\']([^"\']*/play/\d+-\d+-\d+\.html)["\'][^>]*>([\s\S]*?)</a>', txt, re.I):
|
||||
item=(self.clean(n) or '播放') + '$' + self.fix(h)
|
||||
if item not in eps: eps.append(item)
|
||||
if eps: play_from, play_url = ['默认'], ['#'.join(eps)]
|
||||
vod = {'vod_id':url,'vod_name':title,'vod_pic':self.fix(picm.group(1)) if picm else '', 'type_name':info('分类'), 'vod_year':info('年份')[:4], 'vod_area':info('地区'), 'vod_remarks':info('更新时间'), 'vod_actor':info('主演'), 'vod_director':info('导演'), 'vod_content':content, 'vod_play_from':'$$$'.join(play_from), 'vod_play_url':'$$$'.join(play_url)}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
url = self.host + '/search/%s-------------.html' % quote(key)
|
||||
txt = self.getHtml(url, self.host + '/')
|
||||
vods = [] if self.isNoResultPage(txt) else self.parseList(txt)
|
||||
return {'list':vods, 'page':int(pg or 1), 'pagecount':1, 'limit':len(vods), 'total':len(vods)}
|
||||
|
||||
def decodePlayerUrl(self, data):
|
||||
url = data.get('url','') if isinstance(data, dict) else ''
|
||||
enc = str(data.get('encrypt','0')) if isinstance(data, dict) else '0'
|
||||
try:
|
||||
if enc == '1': url = unquote(url)
|
||||
elif enc == '2': url = unquote(base64.b64decode(url).decode('utf-8','ignore'))
|
||||
elif enc == '3' and re.fullmatch(r'[0-9a-fA-F]+', url or ''):
|
||||
# didahd 的 artplayer 线路要求把 hex 原文作为 url 参数,解码值仅作备用
|
||||
return url
|
||||
except Exception as e:
|
||||
self.log('播放器URL解码失败 %s' % e)
|
||||
return url.replace('\\/', '/')
|
||||
|
||||
def decodeArtUrl(self, cipher_text, timestamp):
|
||||
if not AES or not cipher_text or not timestamp: return ''
|
||||
try:
|
||||
seed = str(timestamp) + 'RY7e48naFXPsLJC'
|
||||
md5 = hashlib.md5(seed.encode('utf-8')).hexdigest()
|
||||
key = md5[16:32].encode('utf-8')
|
||||
iv = md5[0:16].encode('utf-8')
|
||||
raw = cipher_text.replace('\\/', '/')
|
||||
dec = AES.new(key, AES.MODE_CBC, iv).decrypt(base64.b64decode(raw))
|
||||
return unpad(dec, 16).decode('utf-8', 'ignore')
|
||||
except Exception as e:
|
||||
self.log('artplayer AES解密失败 %s' % e); return ''
|
||||
def parseSmartPlay(self, txt, timestamp, referer):
|
||||
try:
|
||||
if 'isSmartPlay' not in txt or 'true' not in txt[:8000]: return ''
|
||||
vm = re.search(r'const\s+playPageUrl\s*=\s*["\']([^"\']+)', txt, re.I)
|
||||
cm = re.search(r'const\s+secretKeySeed\s*=\s*["\']([^"\']+)', txt, re.I)
|
||||
if not vm or not cm or not timestamp: return ''
|
||||
api = 'https://hd.ticktockwow.com/smartplay-cache/api/webvideo_ty.php'
|
||||
t = int(time.time())
|
||||
body = json.dumps({'vkey':vm.group(1), 'code':cm.group(1), 't':t, 'signature':hashlib.md5(str(t).encode('utf-8')).hexdigest()})
|
||||
h = dict(self.headers)
|
||||
h.update({'Referer':self.host + '/static/player/artplayer/', 'Origin':self.host, 'Content-Type':'application/json', 'Accept':'application/json,text/plain,*/*'})
|
||||
r = self.post(api, headers=h, data=body, timeout=15)
|
||||
text = r.content.decode(getattr(r, 'encoding', None) or 'utf-8', 'ignore') if hasattr(r, 'content') else (getattr(r, 'text', '') or '')
|
||||
js = json.loads(text)
|
||||
enc = (js or {}).get('url','')
|
||||
u = self.decodeArtUrl(enc, timestamp)
|
||||
u = u.replace('\\/', '/') if u else ''
|
||||
return u if self.isVideoFormat(u) else ''
|
||||
except Exception as e:
|
||||
self.log('smartplay解析失败 %s' % e); return ''
|
||||
|
||||
def makePlayHeader(self, url):
|
||||
# 播放端优先“空防盗链头”:不主动带 Referer/Origin,避免第三方 CDN 因来源不匹配而限速/卡顿。
|
||||
# 实测 didahd 的 didahd secure、天翼云、快手、超星、小红书分片均可用 UA-only;p.ananas 空 UA 可能 403,所以保留 UA。
|
||||
h = {'User-Agent':self.headers['User-Agent']}
|
||||
try:
|
||||
if re.search(r'\.m3u8(?:\?|$)|qd-tjwq-person\.tjtele\.com|ctyunxs\.cn|PERSONCLOUD|video_m3u8/secure\.php', url, re.I):
|
||||
r = self.fetch(url, headers=h, timeout=8)
|
||||
txt = r.content[:2048].decode('utf-8', 'ignore') if hasattr(r, 'content') else (getattr(r, 'text', '') or '')[:2048]
|
||||
# 只做健康探测,不再返回 Referer/Origin;减少 EXO 分片请求卡顿。
|
||||
if '#EXTM3U' not in txt and txt:
|
||||
self.log('m3u8探测异常片段 ' + txt[:60].replace('\n',' '))
|
||||
except Exception as e:
|
||||
self.log('播放头检测失败 %s' % e)
|
||||
return h
|
||||
|
||||
def parseArtPlayer(self, raw_url, referer, next_url=''):
|
||||
if not raw_url or re.match(r'https?://(?:pan\.quark|pan\.baidu|www\.aliyundrive|drive\.uc)', raw_url, re.I): return ''
|
||||
art = self.host + '/static/player/artplayer/?url=' + quote(raw_url, safe='')
|
||||
if next_url: art += '&next=' + quote(next_url, safe='')
|
||||
txt = self.getHtml(art, referer)
|
||||
ts = re.search(r'const\s+timestamp\s*=\s*["\']([^"\']+)', txt, re.I)
|
||||
if not ts: return ''
|
||||
sm = self.parseSmartPlay(txt, ts.group(1), referer)
|
||||
if sm: return sm
|
||||
qm = re.search(r'const\s+qualities\s*=\s*(\[[\s\S]*?\]);', txt, re.I)
|
||||
if not qm: return ''
|
||||
try:
|
||||
arr = json.loads(qm.group(1))
|
||||
for it in arr:
|
||||
u = self.decodeArtUrl(it.get('url',''), ts.group(1))
|
||||
if u:
|
||||
u = self.fix(u)
|
||||
if self.isVideoFormat(u): return u
|
||||
except Exception as e:
|
||||
self.log('artplayer qualities解析失败 %s' % e)
|
||||
return ''
|
||||
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if self.isVideoFormat(id): return {'parse':0, 'url':id, 'header':self.makePlayHeader(id)}
|
||||
txt = self.getHtml(id, self.host + '/')
|
||||
data = None
|
||||
m = re.search(r'var\s+player_[a-zA-Z0-9_]+\s*=\s*(\{[\s\S]*?\})\s*</script>', txt, re.I)
|
||||
if m:
|
||||
try: data = json.loads(m.group(1))
|
||||
except Exception as e: self.log('播放器JSON失败 %s' % e)
|
||||
url = self.decodePlayerUrl(data or {})
|
||||
if self.isVideoFormat(url):
|
||||
fu = self.fix(url)
|
||||
return {'parse':0, 'url':fu, 'header':self.makePlayHeader(fu)}
|
||||
final = self.parseArtPlayer(url, id, (data or {}).get('link_next',''))
|
||||
if final:
|
||||
return {'parse':0, 'url':final, 'header':self.makePlayHeader(final)}
|
||||
mm = re.search(r'(https?:\\?/\\?/[^"\']+?\.(?:m3u8|mp4)[^"\']*)', txt, re.I)
|
||||
if mm:
|
||||
u = self.fix(mm.group(1))
|
||||
return {'parse':0, 'url':u, 'header':self.makePlayHeader(u)}
|
||||
if url and re.match(r'https?://', url):
|
||||
return {'parse':1, 'url':url, 'header':self.headers}
|
||||
return {'parse':1, 'url':id, 'header':self.headers}
|
||||
|
||||
spider = Spider()
|
||||
@@ -1,379 +0,0 @@
|
||||
# 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 datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import xml.etree.ElementTree as ET
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import binascii
|
||||
import requests
|
||||
import random
|
||||
import base64
|
||||
import html
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://web.tt4747.com"
|
||||
headerx1 = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
leixing = {"key": "类型","name": "类型",
|
||||
"value": [{"n": "全部", "v": ""},{"n": "Netflix", "v": "Netflix"},{"n": "剧情", "v": "剧情"},{"n": "喜剧", "v": "喜剧"},{"n": "动作", "v": "动作"},
|
||||
{"n": "爱情", "v": "爱情"},{"n": "恐怖", "v": "恐怖"},{"n": "惊悚", "v": "惊悚"},{"n": "犯罪", "v": "犯罪"},{"n": "科幻", "v": "科幻"},
|
||||
{"n": "悬疑", "v": "悬疑"},{"n": "奇幻", "v": "奇幻"},{"n": "冒险", "v": "冒险"},{"n": "战争", "v": "战争"},{"n": "历史", "v": "历史"},
|
||||
{"n": "古装", "v": "古装"},{"n": "家庭", "v": "家庭"},{"n": "传记", "v": "传记"},{"n": "武侠", "v": "武侠"},{"n": "同性", "v": "同性"},
|
||||
{"n": "歌舞", "v": "歌舞"},{"n": "短片", "v": "短片"},{"n": "动画", "v": "动画"},{"n": "儿童", "v": "儿童"},{"n": "职场", "v": "职场"}]}
|
||||
zy_leixing = {"key": "类型","name": "类型",
|
||||
"value": [{"n": "全部", "v": ""},{"n": "纪录", "v": "纪录"},{"n": "真人秀", "v": "真人秀"},{"n": "记录", "v": "记录"},{"n": "脱口秀", "v": "脱口秀"},
|
||||
{"n": "剧情", "v": "剧情"},{"n": "历史", "v": "历史"},{"n": "喜剧", "v": "喜剧"},{"n": "传记", "v": "传记"},{"n": "相声", "v": "相声"},
|
||||
{"n": "节目", "v": "节目"},{"n": "歌舞", "v": "歌舞"},{"n": "冒险", "v": "冒险"},{"n": "运动", "v": "运动"},{"n": "Season", "v": "Season"},
|
||||
{"n": "犯罪", "v": "犯罪"},{"n": "短片", "v": "短片"},{"n": "搞笑", "v": "搞笑"},{"n": "晚会", "v": "晚会"}]}
|
||||
dm_leixing = {"key": "类型","name": "类型",
|
||||
"value": [{"n": "全部", "v": ""},{"n": "Netflix", "v": "Netflix"},{"n": "动态漫画", "v": "动态漫画"},{"n": "剧情", "v": "剧情"},{"n": "动画", "v": "动画"},
|
||||
{"n": "喜剧", "v": "喜剧"},{"n": "冒险", "v": "冒险"},{"n": "动作", "v": "动作"},{"n": "奇幻", "v": "奇幻"},{"n": "科幻", "v": "科幻"},
|
||||
{"n": "儿童", "v": "儿童"},{"n": "搞笑", "v": "搞笑"},{"n": "爱情", "v": "爱情"},{"n": "家庭", "v": "家庭"},{"n": "短片", "v": "短片"},
|
||||
{"n": "热血", "v": "热血"},{"n": "益智", "v": "益智"},{"n": "悬疑", "v": "悬疑"},{"n": "经典", "v": "经典"},{"n": "校园", "v": "校园"},
|
||||
{"n": "Anime", "v": "Anime"},{"n": "运动", "v": "运动"},{"n": "亲子", "v": "亲子"},{"n": "青春", "v": "青春"},{"n": "恋爱", "v": "恋爱"},
|
||||
{"n": "武侠", "v": "武侠"},{"n": "惊悚", "v": "惊悚"}]}
|
||||
diqu = {"key": "地区","name": "地区",
|
||||
"value": [{"n": "全部", "v": ""},{"n": "大陆", "v": "大陆"},{"n": "香港", "v": "香港"},{"n": "台湾", "v": "台湾"},{"n": "美国", "v": "美国"},
|
||||
{"n": "日本", "v": "日本"},{"n": "韩国", "v": "韩国"},{"n": "英国", "v": "英国"},{"n": "法国", "v": "法国"},{"n": "德国", "v": "德国"},
|
||||
{"n": "印度", "v": "印度"},{"n": "泰国", "v": "泰国"},{"n": "丹麦", "v": "丹麦"},{"n": "瑞典", "v": "瑞典"},{"n": "巴西", "v": "巴西"},
|
||||
{"n": "加拿大", "v": "加拿大"},{"n": "俄罗斯", "v": "俄罗斯"},{"n": "意大利", "v": "意大利"},{"n": "比利时", "v": "比利时"},{"n": "爱尔兰", "v": "爱尔兰"},
|
||||
{"n": "西班牙", "v": "西班牙"},{"n": "澳大利亚", "v": "澳大利亚"},{"n": "其他", "v": "其他"}]}
|
||||
yuyuan = {"key": "语言","name": "语言",
|
||||
"value": [{"n": "全部", "v": ""},{"n": "国语", "v": "国语"},{"n": "粤语", "v": "粤语"},{"n": "英语", "v": "英语"},{"n": "日语", "v": "日语"},
|
||||
{"n": "韩语", "v": "韩语"},{"n": "法语", "v": "法语"},{"n": "其他", "v": "其他"}]}
|
||||
nianfen = {"key": "年份","name": "年份",
|
||||
"value": [{"n": "全部", "v": ""},{"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"}]}
|
||||
paixu = {"key": "排序","name": "排序",
|
||||
"value": [{"n": "全部", "v": ""},{"n": "按时间", "v": "time"},{"n": "按人气", "v": "hits"},{"n": "按评分", "v": "score"}]}
|
||||
result = {"class": [{"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": "伦理"}],
|
||||
|
||||
"list": [],
|
||||
"filters": {"1": [leixing,diqu,yuyuan,nianfen,paixu],
|
||||
"2": [leixing,diqu,yuyuan,nianfen,paixu],
|
||||
"3": [zy_leixing,diqu,yuyuan,nianfen,paixu],
|
||||
"4": [dm_leixing,diqu,yuyuan,nianfen,paixu],
|
||||
"5": [paixu],
|
||||
"6": [paixu]}}
|
||||
|
||||
return result
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def fetch_image_response_info(self, url):
|
||||
try:
|
||||
response = requests.get(url, timeout=10, allow_redirects=True)
|
||||
return response.text
|
||||
except Exception as e:
|
||||
return ''
|
||||
|
||||
# def fetch_image_response_info(self, url):
|
||||
# try:
|
||||
# response = requests.get(url, timeout=10, allow_redirects=True)
|
||||
# content_type = response.headers.get('Content-Type', '')
|
||||
#
|
||||
# if 'text' in content_type or 'json' in content_type:
|
||||
# return {
|
||||
# 'status': 'text',
|
||||
# 'content': response.text
|
||||
# }
|
||||
# elif 'image' in content_type:
|
||||
# # img_base64 = base64.b64encode(response.content).decode('utf-8')
|
||||
# # print(img_base64)
|
||||
# return {
|
||||
# 'status': 'image',
|
||||
# 'content': '',
|
||||
# 'size': len(response.content)
|
||||
# }
|
||||
# else:
|
||||
# return {
|
||||
# 'status': 'unknown',
|
||||
# 'content': str(response.content)
|
||||
# }
|
||||
#
|
||||
# except Exception as e:
|
||||
# return {
|
||||
# 'status': 'error',
|
||||
# 'error': str(e)
|
||||
# }
|
||||
|
||||
def get_video_covers(self, videos, max_workers=8):
|
||||
vod_ids = []
|
||||
for video in videos:
|
||||
vod_id = video['vod_id']
|
||||
if '/voddetail/' in vod_id:
|
||||
vod_id = vod_id.split('/voddetail/')[1].strip('/')
|
||||
vod_ids.append(vod_id)
|
||||
|
||||
cover_map = {}
|
||||
lock = threading.Lock()
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
def fetch_cover(vid):
|
||||
nonlocal success_count, fail_count
|
||||
url = f"https://web.tt4747.com/voddetail/{vid}/"
|
||||
time.sleep(random.uniform(0.3, 1.0))
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': random.choice([
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
|
||||
'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',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0'
|
||||
]),
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://web.tt4747.com/'
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
pic_div = soup.find('div', class_='detail-pic')
|
||||
if pic_div:
|
||||
img = pic_div.find('img')
|
||||
if img:
|
||||
pic_urls = img.get('data-original')
|
||||
pic_urls = 'https://pics.xhsdns.cn/vod/252/252122.jpg'
|
||||
pic_info = self.fetch_image_response_info(pic_urls)
|
||||
pic_url = f'ddata:image/png;base64,{pic_info}'
|
||||
if pic_url:
|
||||
with lock:
|
||||
success_count += 1
|
||||
cover_map[vid] = pic_url
|
||||
return vid, pic_url
|
||||
pattern = r'data-original="([^"]+\.jpg)"'
|
||||
match = re.search(pattern, response.text)
|
||||
if match:
|
||||
pic_url = match.group(1)
|
||||
with lock:
|
||||
success_count += 1
|
||||
cover_map[vid] = pic_url
|
||||
return vid, pic_url
|
||||
|
||||
with lock:
|
||||
fail_count += 1
|
||||
return vid, None
|
||||
else:
|
||||
with lock:
|
||||
fail_count += 1
|
||||
return vid, None
|
||||
except Exception as e:
|
||||
with lock:
|
||||
fail_count += 1
|
||||
return vid, None
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = {executor.submit(fetch_cover, vid): vid for vid in vod_ids}
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
future.result(timeout=30)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
for video in videos:
|
||||
vod_id = video['vod_id']
|
||||
if '/voddetail/' in vod_id:
|
||||
vid = vod_id.split('/voddetail/')[1].strip('/')
|
||||
else:
|
||||
vid = vod_id
|
||||
video['vod_pic'] = cover_map.get(vid, '')
|
||||
return videos
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
LX = ext.get('类型', '')
|
||||
DQ = ext.get('地区', '')
|
||||
YY = ext.get('语言', '')
|
||||
NF = ext.get('年份', '')
|
||||
PX = ext.get('排序', '')
|
||||
if cid == '5' or cid == '6':
|
||||
url = f'{xurl}/rss/index.xml?mid=1&tid={cid}&page={str(page)}&limit=24&class=&year=&area=&lang=&by={PX}'
|
||||
else:
|
||||
url = f'{xurl}/rss/index.xml?mid=1&tid={cid}&page={str(page)}&limit=24&class={LX}&year={NF}&area={DQ}&lang={YY}&by={PX}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
try:
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(res)
|
||||
items = root.findall('.//item')
|
||||
for item in items:
|
||||
title_elem = item.find('title')
|
||||
if title_elem is not None and title_elem.text:
|
||||
title_text = title_elem.text
|
||||
if ' ' in title_text:
|
||||
first_space = title_text.find(' ')
|
||||
last_space = title_text.rfind(' ')
|
||||
if first_space == last_space:
|
||||
vod_name = title_text[:first_space]
|
||||
vod_remarks = title_text[last_space + 1:]
|
||||
else:
|
||||
vod_name = title_text[:first_space]
|
||||
vod_remarks = title_text[last_space + 1:]
|
||||
else:
|
||||
vod_name = title_text
|
||||
vod_remarks = ""
|
||||
else:
|
||||
vod_name = ""
|
||||
vod_remarks = ""
|
||||
link_elem = item.find('link')
|
||||
vod_ids = link_elem.text if link_elem is not None else ""
|
||||
vod_id = vod_ids.replace('http://web.dy1996.com/', 'https://web.tt4747.com/')
|
||||
pubdate_elem = item.find('pubDate')
|
||||
if pubdate_elem is not None and pubdate_elem.text:
|
||||
pubdate_text = pubdate_elem.text
|
||||
if ' ' in pubdate_text:
|
||||
vod_year = pubdate_text.split(' ')[0]
|
||||
else:
|
||||
vod_year = pubdate_text
|
||||
else:
|
||||
vod_year = ""
|
||||
|
||||
video = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": vod_name,
|
||||
"vod_year": vod_year,
|
||||
"vod_pic": '',
|
||||
"vod_remarks": vod_remarks
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
except ET.ParseError as e:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
if videos:
|
||||
try:
|
||||
videos = self.get_video_covers(videos, max_workers=8)
|
||||
except Exception as e:
|
||||
''
|
||||
|
||||
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 = '咖啡直播'
|
||||
if did.startswith('@@@'):
|
||||
fenge = did.split("@@@")
|
||||
url = f"{fenge[1]}"
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
data = json.loads(res)['data']['replays']
|
||||
ids = []
|
||||
for item in data:
|
||||
bf_name = item['title']
|
||||
bf_url = item['video_url']
|
||||
ids.append(f"{bf_name}${bf_url}")
|
||||
bofang = '#'.join(ids)
|
||||
else:
|
||||
bofang = did
|
||||
videos.append({
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
fenge = id.split("http")
|
||||
id = f"http{fenge[1]}"
|
||||
url = id
|
||||
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = url
|
||||
result["header"] = headerx
|
||||
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
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
spider_instance = Spider()
|
||||
|
||||
# res=spider_instance.homeContent('filter') # 分类🚨
|
||||
|
||||
# res = spider_instance.homeVideoContent() # 首页🚨
|
||||
|
||||
res=spider_instance.categoryContent('2', 1, 'filter', {}) # 分页🚨
|
||||
|
||||
# res = spider_instance.detailContent(['@@@https://kafeizhibo.cc/api/v1/match/21895/recordings']) # 详情页🚨
|
||||
|
||||
# res = spider_instance.playerContent('1', '直播21$01https://live.666666.zip/live/4528263.m3u8', 'vipFlags') # 播放页🚨
|
||||
|
||||
# res = spider_instance.searchContentPage('我', 'quick', '1') # 搜索页🚨
|
||||
|
||||
print(res)
|
||||
@@ -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
|
||||
@@ -0,0 +1,421 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
黄豆短剧爬虫
|
||||
站点: https://www.hdmgdj.com
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
from base.spider import Spider as BaseSpider
|
||||
except ImportError:
|
||||
class BaseSpider:
|
||||
pass
|
||||
|
||||
|
||||
class Spider(BaseSpider):
|
||||
"""黄豆短剧爬虫"""
|
||||
|
||||
BASE_URL = 'https://www.hdmgdj.com'
|
||||
API_BASE = 'https://hdmgdj.com/api'
|
||||
|
||||
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',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Referer': 'https://www.hdmgdj.com/',
|
||||
'Origin': 'https://www.hdmgdj.com',
|
||||
}
|
||||
|
||||
_filter_cache = {} # 分类筛选缓存
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = ""
|
||||
self.error_play_url = "https://kjjsaas-sh.oss-cn-shanghai.aliyuncs.com/u/3401405881/20240818-936952-fc31b16575e80a7562cdb1f81a39c6b0.mp4"
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.HEADERS)
|
||||
|
||||
# ==================== 标准接口 ====================
|
||||
|
||||
def init(self, extend="{}"):
|
||||
"""初始化"""
|
||||
if extend:
|
||||
try:
|
||||
self.extend = json.loads(extend)
|
||||
if 'name' in self.extend:
|
||||
self.name = self.extend['name']
|
||||
if 'base_url' in self.extend:
|
||||
self.BASE_URL = self.extend['base_url']
|
||||
self.API_BASE = self.extend['base_url'] + '/api'
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def getName(self):
|
||||
"""获取爬虫名称"""
|
||||
return "黄豆短剧"
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""首页"""
|
||||
result = {
|
||||
"class": [],
|
||||
"filters": {},
|
||||
"list": [],
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
# 获取分类
|
||||
genres_data = self._get('/genres')
|
||||
if genres_data and isinstance(genres_data, list):
|
||||
for g in genres_data:
|
||||
gid = str(g.get('id', ''))
|
||||
if not gid:
|
||||
continue
|
||||
result["class"].append({
|
||||
"type_id": gid,
|
||||
"type_name": g.get('name', ''),
|
||||
})
|
||||
|
||||
# 获取首页推荐
|
||||
home_data = self._get('/home')
|
||||
if home_data and isinstance(home_data, dict):
|
||||
# guess 猜你喜欢
|
||||
guess_list = home_data.get('guess', [])
|
||||
if isinstance(guess_list, list):
|
||||
for item in guess_list:
|
||||
result["list"].append(self._parse_vod(item))
|
||||
|
||||
# feature 精选
|
||||
feature_list = home_data.get('feature', [])
|
||||
if isinstance(feature_list, list):
|
||||
for item in feature_list:
|
||||
result["list"].append(self._parse_vod(item))
|
||||
|
||||
# 如果列表为空,用首页第一页数据
|
||||
if not result["list"]:
|
||||
dramas_data = self._get('/dramas?page=1&size=20')
|
||||
if dramas_data and isinstance(dramas_data, dict):
|
||||
for item in dramas_data.get('list', []):
|
||||
result["list"].append(self._parse_vod(item))
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类页"""
|
||||
result = {
|
||||
"page": pg,
|
||||
"pagecount": 999,
|
||||
"limit": 20,
|
||||
"total": 99999,
|
||||
"list": [],
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
# 分类ID是genre id
|
||||
data = self._get(f'/dramas?genreId={tid}&page={pg}&size=20')
|
||||
if data and isinstance(data, dict):
|
||||
lst = data.get('list', [])
|
||||
total = data.get('total', 0)
|
||||
result["total"] = total
|
||||
result["pagecount"] = (total + 19) // 20 if total else 999
|
||||
for item in lst:
|
||||
result["list"].append(self._parse_vod(item))
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情页"""
|
||||
result = {
|
||||
"list": [],
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
vid = ids[0]
|
||||
data = self._get(f'/dramas/{vid}')
|
||||
|
||||
if data and isinstance(data, dict):
|
||||
episodes = data.get('episodes', [])
|
||||
|
||||
# 组装播放地址
|
||||
play_url_parts = []
|
||||
for ep in episodes:
|
||||
ep_title = ep.get('title', f"第{ep.get('ep', 0)}集")
|
||||
play_url = ep.get('playUrl', '')
|
||||
if play_url:
|
||||
play_url_parts.append(f"{ep_title}${play_url}")
|
||||
|
||||
vod = {
|
||||
"vod_id": str(data['id']),
|
||||
"vod_name": data.get('t', ''),
|
||||
"vod_pic": data.get('cover', ''),
|
||||
"type_name": data.get('sub', ''),
|
||||
"vod_year": '',
|
||||
"vod_area": '',
|
||||
"vod_remarks": f"{data.get('serial', '')}·{data.get('plays', '')}播放",
|
||||
"vod_actor": '',
|
||||
"vod_director": '黄豆短剧',
|
||||
"vod_content": data.get('summary', '') or data.get('t', ''),
|
||||
"vod_play_from": '黄豆短剧',
|
||||
"vod_play_url": '#'.join(play_url_parts),
|
||||
}
|
||||
result["list"].append(vod)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索"""
|
||||
result = {
|
||||
"page": pg,
|
||||
"pagecount": 999,
|
||||
"limit": 20,
|
||||
"total": 99999,
|
||||
"list": [],
|
||||
"parse": 0,
|
||||
"jx": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
data = self._get(f'/search?kw={urllib.parse.quote(key)}&page={pg}&size=20')
|
||||
if data and isinstance(data, dict):
|
||||
lst = data.get('list', [])
|
||||
total = data.get('total', 0)
|
||||
result["total"] = total
|
||||
result["pagecount"] = (total + 19) // 20 if total else 0
|
||||
for item in lst:
|
||||
result["list"].append(self._parse_vod(item))
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放页 - 直接返回 m3u8 data URI"""
|
||||
result = {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": self.error_play_url,
|
||||
"jx": 0,
|
||||
"header": "",
|
||||
}
|
||||
|
||||
if id:
|
||||
# 直接在 playerContent 里生成解密后的 m3u8,用 data URI 返回
|
||||
# 这样播放地址就不是 127.0.0.1 代理了
|
||||
m3u8_content = self._build_m3u8_with_key(id)
|
||||
if m3u8_content:
|
||||
import base64
|
||||
m3u8_b64 = base64.b64encode(m3u8_content.encode('utf-8')).decode('ascii')
|
||||
result["url"] = "data:application/vnd.apple.mpegurl;base64," + m3u8_b64
|
||||
result["parse"] = 0
|
||||
|
||||
return result
|
||||
|
||||
def _build_m3u8_with_key(self, url):
|
||||
"""构建 m3u8 内容(key 内嵌为 base64 data URI,ts 用原始绝对地址)"""
|
||||
import hashlib
|
||||
import re
|
||||
import base64
|
||||
if not url:
|
||||
return None
|
||||
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
content = r.text
|
||||
|
||||
# 计算 key
|
||||
key_bytes = self._get_key_bytes(url)
|
||||
if key_bytes:
|
||||
key_b64 = base64.b64encode(key_bytes).decode('ascii')
|
||||
key_data_uri = "data:text/plain;base64," + key_b64
|
||||
content = re.sub(
|
||||
r'(#EXT-X-KEY:.*?URI=")[^"]*(")',
|
||||
r'\1' + key_data_uri + r'\2',
|
||||
content
|
||||
)
|
||||
|
||||
# 把相对路径的 ts 改成绝对路径
|
||||
base_url = url.rsplit('/', 1)[0] + '/'
|
||||
lines = content.split('\n')
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
if line.startswith('http'):
|
||||
new_lines.append(line)
|
||||
else:
|
||||
new_lines.append(base_url + line)
|
||||
else:
|
||||
new_lines.append(line)
|
||||
content = '\n'.join(new_lines)
|
||||
return content
|
||||
except Exception as e:
|
||||
print(f"_build_m3u8_with_key error: {e}")
|
||||
return None
|
||||
|
||||
def _get_key_bytes(self, url):
|
||||
"""从 m3u8 URL 计算解密 key"""
|
||||
import hashlib
|
||||
import re
|
||||
m = re.search(r'/hls/([0-9a-f]{64})/', url)
|
||||
if not m:
|
||||
return None
|
||||
video_id = m.group(1)
|
||||
ver_match = re.search(r'[?&]version=([^&#]+)', url)
|
||||
version = ver_match.group(1) if ver_match else 'v1'
|
||||
prefix = "xnaichanping"
|
||||
key_str = prefix + video_id + version
|
||||
return hashlib.md5(key_str.encode()).digest()
|
||||
|
||||
def localProxy(self, params):
|
||||
"""本地代理 - 处理海报图片解密"""
|
||||
try:
|
||||
do = params.get('do', '')
|
||||
if do == 'img':
|
||||
url = params.get('url', '')
|
||||
if not url:
|
||||
return 0
|
||||
return self._decrypt_image(url)
|
||||
except Exception as e:
|
||||
print(f"localProxy error: {e}")
|
||||
return 0
|
||||
|
||||
def _decrypt_image(self, url):
|
||||
"""解密加密的海报图片"""
|
||||
import hashlib
|
||||
import re
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
encrypted = r.content
|
||||
|
||||
# 提取 imageId (64位哈希)
|
||||
m = re.search(r'([0-9a-f]{64})', url)
|
||||
if not m:
|
||||
return [200, "image/png", {}, encrypted]
|
||||
|
||||
image_id = m.group(1)
|
||||
|
||||
# 提取 version
|
||||
ver_match = re.search(r'[?&]version=([^&#]+)', url)
|
||||
version = ver_match.group(1) if ver_match else 'v1'
|
||||
|
||||
# 计算解密 key
|
||||
prefix = "xnaichanping"
|
||||
key_str = prefix + image_id + version
|
||||
key_bytes = hashlib.md5(key_str.encode()).digest()
|
||||
|
||||
# AES-128-CBC 解密,IV=0
|
||||
from Crypto.Cipher import AES
|
||||
iv = bytes.fromhex('00000000000000000000000000000000')
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
|
||||
decrypted = cipher.decrypt(encrypted)
|
||||
|
||||
# 去掉 PKCS7 padding
|
||||
pad_len = decrypted[-1]
|
||||
if 1 <= pad_len <= 16:
|
||||
decrypted = decrypted[:-pad_len]
|
||||
|
||||
# 确定图片类型
|
||||
content_type = "image/png"
|
||||
if decrypted[:3] == b'\xff\xd8\xff':
|
||||
content_type = "image/jpeg"
|
||||
elif decrypted[:8] == b'\x89PNG\r\n\x1a\n':
|
||||
content_type = "image/png"
|
||||
elif decrypted[:6] == b'GIF87a' or decrypted[:6] == b'GIF89a':
|
||||
content_type = "image/gif"
|
||||
elif decrypted[:4] == b'RIFF' and decrypted[8:12] == b'WEBP':
|
||||
content_type = "image/webp"
|
||||
|
||||
return [200, content_type, {}, decrypted]
|
||||
except Exception as e:
|
||||
print(f"_decrypt_image error: {e}")
|
||||
return 0
|
||||
|
||||
# ==================== 内部方法 ====================
|
||||
|
||||
def _parse_vod(self, item):
|
||||
"""解析视频条目"""
|
||||
return {
|
||||
"vod_id": str(item['id']),
|
||||
"vod_name": item.get('t', ''),
|
||||
"vod_pic": item.get('cover', ''),
|
||||
"vod_remarks": f"{item.get('serial', '')}·{item.get('eps', 0)}集",
|
||||
}
|
||||
|
||||
def _get(self, path):
|
||||
"""发送 GET 请求"""
|
||||
url = self.API_BASE + path
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, verify=False)
|
||||
resp = r.json()
|
||||
if resp.get('code') == 0 and resp.get('data') is not None:
|
||||
return resp['data']
|
||||
return None
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
|
||||
# 调试用
|
||||
if __name__ == '__main__':
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
s = Spider()
|
||||
s.init()
|
||||
|
||||
print('=== 首页 ===')
|
||||
home = s.homeContent(True)
|
||||
print(f'分类: {len(home["class"])}个')
|
||||
for c in home['class']:
|
||||
print(f' {c["type_id"]}: {c["type_name"]}')
|
||||
print(f'推荐: {len(home["list"])}个')
|
||||
for v in home['list'][:5]:
|
||||
print(f' {v["vod_id"]}: {v["vod_name"]} - {v["vod_remarks"]}')
|
||||
print()
|
||||
|
||||
print('=== 分类1(都市)第1页 ===')
|
||||
cr = s.categoryContent('1', 1, True, {})
|
||||
print(f'总数: {cr["total"]}, 本页: {len(cr["list"])}个')
|
||||
for v in cr['list'][:5]:
|
||||
print(f' {v["vod_id"]}: {v["vod_name"]}')
|
||||
print()
|
||||
|
||||
print('=== 搜索 穿越 ===')
|
||||
sr = s.searchContent('穿越', False, '1')
|
||||
print(f'结果: {len(sr["list"])}个, 总数: {sr["total"]}')
|
||||
for v in sr['list'][:5]:
|
||||
print(f' {v["vod_id"]}: {v["vod_name"]}')
|
||||
print()
|
||||
|
||||
if sr['list']:
|
||||
vid = sr['list'][0]['vod_id']
|
||||
print(f'=== 详情 {vid} ===')
|
||||
dr = s.detailContent([vid])
|
||||
if dr['list']:
|
||||
v = dr['list'][0]
|
||||
print(f'标题: {v["vod_name"]}')
|
||||
print(f'分类: {v["type_name"]}')
|
||||
print(f'备注: {v["vod_remarks"]}')
|
||||
print(f'播放源: {v["vod_play_from"]}')
|
||||
play_urls = v["vod_play_url"].split('#')
|
||||
print(f'集数: {len(play_urls)}集')
|
||||
print(f'第一集: {play_urls[0][:80]}...')
|
||||
Reference in New Issue
Block a user