Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-21 16:35:25 +00:00
parent 18c881627d
commit ee7a20c91a
55 changed files with 7121 additions and 3967 deletions
+315
View File
@@ -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 段 jointid-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-onlyp.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()
-379
View File
@@ -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)
+396
View File
@@ -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('&nbsp;', ' ')
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
+421
View File
@@ -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 URIts 用原始绝对地址)"""
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]}...')