Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-21 01:01:50 +00:00
parent bcc4ce09d6
commit 0f4975d89d
18 changed files with 2099 additions and 814 deletions
+591
View File
@@ -0,0 +1,591 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ztzssz.com 爬虫 - TVBox/影视仓 Spider 插件
支持分类浏览、筛选(类型/地区/语言/年份/字母)、搜索、详情获取、播放链接解析
选集正序排列,海报封面补充
"""
import re
import json
import logging
import urllib.parse
import os
import sys
import requests
from bs4 import BeautifulSoup
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
try:
from base.spider import Spider as BaseSpider
except ImportError:
BaseSpider = object
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class Spider(BaseSpider):
"""ztzssz.com 爬虫"""
BASE_URL = "https://www.ztzssz.com"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Linux; Android 12; SM-S908U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://www.ztzssz.com/",
}
# 分类:键为网站分类ID,与 /vodtype/{id}.html 对应
CATEGORY_MAP = {
"1": {"name": "电影", "url": "/vodtype/1.html"},
"2": {"name": "电视剧", "url": "/vodtype/2.html"},
"3": {"name": "综艺", "url": "/vodtype/3.html"},
"4": {"name": "动漫", "url": "/vodtype/4.html"},
"20": {"name": "短剧", "url": "/vodtype/20.html"},
"35": {"name": "动画片", "url": "/vodtype/35.html"},
"36": {"name": "4K电影", "url": "/vodtype/36.html"},
"37": {"name": "Netflix作品", "url": "/vodtype/37.html"},
}
def __init__(self):
try:
super().__init__()
except Exception:
pass
self.session = requests.Session()
self.session.verify = False
self.session.headers.update(self.HEADERS)
def init(self, extend):
pass
def getName(self):
return "ztzssz影视"
def _parse_ext(self, ext):
"""解析ext参数,兼容dict和JSON字符串"""
if not ext:
return {}
if isinstance(ext, dict):
return ext
if isinstance(ext, str):
try:
return json.loads(ext)
except Exception:
return {}
return {}
def _get(self, url):
try:
resp = self.session.get(url, timeout=30)
resp.encoding = "utf-8"
return resp
except Exception as e:
logger.error(f"请求失败 {url}: {e}")
return None
# ==================== 首页 ====================
def homeContent(self, filter=False):
try:
url = f"{self.BASE_URL}/"
resp = self._get(url)
if not resp:
return {}
classes = [{"type_id": cid, "type_name": info["name"]}
for cid, info in self.CATEGORY_MAP.items()]
home_list = self._parse_video_list(resp.text)
return {
"class": classes,
"filters": self._get_filters(),
"list": home_list,
}
except Exception as e:
logger.error(f"获取首页失败: {e}")
return {}
def homeVideoContent(self):
home = self.homeContent()
return {"list": home.get("list", [])}
# ==================== 筛选 ====================
def _get_filters(self):
"""筛选配置:类型/地区/语言/年份/字母
网站筛选URL为中文值(如 喜剧/大陆/国语),故筛选value直接用中文。
"""
filters = {}
# 类型:按分类区分(电影/电视剧/动漫等类型不同),这里取较通用的集合
type_values_common = [
{"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": "运动"},
{"n": "农村", "v": "农村"}, {"n": "儿童", "v": "儿童"},
{"n": "网络电影", "v": "网络电影"},
]
# 电视剧/综艺/动漫常用类型
type_values_series = [
{"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": "其他"},
]
area_values = [
{"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": "其他"},
]
lang_values = [
{"n": "全部", "v": ""},
{"n": "国语", "v": "国语"}, {"n": "英语", "v": "英语"},
{"n": "粤语", "v": "粤语"}, {"n": "闽南语", "v": "闽南语"},
{"n": "韩语", "v": "韩语"}, {"n": "日语", "v": "日语"},
{"n": "法语", "v": "法语"}, {"n": "德语", "v": "德语"},
{"n": "其它", "v": "其它"},
]
year_values = [{"n": "全部", "v": ""}]
for y in range(2026, 1999, -1):
year_values.append({"n": str(y), "v": str(y)})
letter_values = [{"n": "全部", "v": ""}]
for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
letter_values.append({"n": letter, "v": letter})
letter_values.append({"n": "其他", "v": "0"})
for cate_id in self.CATEGORY_MAP:
if cate_id in ("2", "3", "4", "20"):
tv = type_values_series
else:
tv = type_values_common
filters[cate_id] = [
{"key": "type", "name": "类型", "value": tv},
{"key": "area", "name": "地区", "value": area_values},
{"key": "lang", "name": "语言", "value": lang_values},
{"key": "year", "name": "年份", "value": year_values},
{"key": "letter", "name": "字母", "value": letter_values},
]
return filters
# ==================== 分类 ====================
def categoryContent(self, tid, pg, filter, ext):
try:
page = int(pg) if pg else 1
type_id = str(tid)
cate_info = self.CATEGORY_MAP.get(type_id)
if not cate_info:
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
ext_dict = self._parse_ext(ext)
type_filter = ext_dict.get('type', '')
lang_filter = ext_dict.get('lang', '')
year_filter = ext_dict.get('year', '')
letter_filter = ext_dict.get('letter', '')
area_filter = ext_dict.get('area', '')
has_filter = any([type_filter, lang_filter, year_filter, letter_filter, area_filter])
if has_filter:
# 筛选URL: /vodshow/{cate_id}-{area}--{type}-{lang}-{letter}---{page}---{year}.html
# 共12段: [cate_id, area, '', type, lang, letter, '', '', page, '', '', year]
seg_page = str(page) if page > 1 else ''
segs = [
type_id, area_filter, '', type_filter, lang_filter,
letter_filter, '', '', seg_page, '', '', year_filter
]
url = f"{self.BASE_URL}/vodshow/{'-'.join(segs)}.html"
else:
# 无筛选: /vodtype/{id}.html,分页 /vodtype/{id}-{page}.html
url = self.BASE_URL + cate_info["url"]
if page > 1:
url = url.replace('.html', f'-{page}.html')
resp = self._get(url)
if not resp:
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
videos = self._parse_video_list(resp.text)
pagecount = self._parse_total_pages(resp.text)
return {
"list": videos,
"page": page,
"pagecount": pagecount,
"limit": 20,
"total": len(videos) * pagecount if pagecount else len(videos),
}
except Exception as e:
logger.error(f"获取分类内容失败: {e}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 20, "total": 0}
def _parse_total_pages(self, html):
"""解析总页数,格式: <span class="num">1/1213</span>"""
patterns = [
r'class="num"[^>]*>\s*(\d+)\s*/\s*(\d+)',
r'(\d+)\s*/\s*(\d+)\s*</span>',
r'/vodtype/\d+-(\d+)\.html["\'][^>]*>尾页',
r'/vodshow/[^"\'-]+-(\d+)---\.html["\'][^>]*>尾页',
]
for pattern in patterns:
match = re.search(pattern, html)
if match:
return int(match.group(2) if match.lastindex and match.lastindex >= 2 else match.group(1))
return 1
def _parse_video_list(self, html):
"""解析视频列表(首页/分类/筛选/搜索通用)
卡片: a.ewave-vodlist__thumb (含 data-original, title, href) 或 div.ewave-vodlist__thumb > a.thumb-link
"""
videos = []
soup = BeautifulSoup(html, 'html.parser')
# 兼容两种结构:a.vodlist__thumb 自带链接,或 div.vodlist__thumb 内含 a.thumb-link
items = soup.find_all('a', class_=re.compile(r'vodlist__thumb'))
if not items:
items = soup.find_all('div', class_=re.compile(r'vodlist__thumb'))
seen_ids = set()
for item in items:
href = item.get('href', '')
if not href:
a_inner = item.find('a', href=True)
href = a_inner.get('href', '') if a_inner else ''
vid_match = re.search(r'/voddetail/(\d+)\.html', href)
if not vid_match:
continue
vid_id = vid_match.group(1)
if vid_id in seen_ids:
continue
seen_ids.add(vid_id)
title = item.get('title', '')
poster = item.get('data-original', '')
if not poster:
img = item.find('img')
if img:
poster = img.get('data-original', '') or img.get('src', '')
if not title:
img = item.find('img')
if img:
title = img.get('alt', '') or item.get('title', '')
remark_tag = item.find(class_=re.compile(r'pic-text|pic_tag'))
remarks = remark_tag.get_text(strip=True) if remark_tag else ''
if title:
videos.append({
"vod_id": vid_id,
"vod_name": title,
"vod_pic": poster,
"vod_remarks": remarks,
})
return videos
# ==================== 详情 ====================
def detailContent(self, ids):
try:
vod_id = ids[0] if isinstance(ids, list) else str(ids)
url = f"{self.BASE_URL}/voddetail/{vod_id}.html"
resp = self._get(url)
if not resp:
return {"list": []}
html = resp.text
soup = BeautifulSoup(html, 'html.parser')
# 标题: h1.title 内第一个 span(去除评分)
title = ''
h1 = soup.find('h1', class_=re.compile(r'title'))
if h1:
span = h1.find('span')
title = span.get_text(strip=True) if span else h1.get_text(strip=True)
if not title:
h1 = soup.find('h1')
if h1:
title = h1.get_text(strip=True)
# 海报: div.ewave-content__thumb 内 img[data-original]
poster = ''
thumb_box = soup.find(class_=re.compile(r'content__thumb|vodlist__thumb'))
if thumb_box:
img = thumb_box.find('img')
if img:
poster = img.get('data-original', '') or img.get('src', '')
# 信息: p.data 列表,一个p内可能含多个 label(类型/地区/年份用 span.text-muted 分隔)
year = area = type_name = actor = director = content = remarks = ''
data_ps = soup.find_all('p', class_=re.compile(r'data'))
for p in data_ps:
# 收集该p下所有 label span 及其后续文本,按 label 分组
labels = p.find_all('span', class_=re.compile(r'text-muted|left'))
for idx, label_tag in enumerate(labels):
label = label_tag.get_text(strip=True)
# 该label之后、下一个label之前的所有文本
nxt = labels[idx + 1] if idx + 1 < len(labels) else None
val = ''
for sib in label_tag.next_siblings:
if sib is nxt:
break
if hasattr(sib, 'get_text'):
t = sib.get_text(strip=True)
else:
t = str(sib).strip()
if t:
val += t
val = val.strip()
if label.startswith('类型'):
type_name = val
elif label.startswith('地区'):
area = val
elif label.startswith('年份'):
year_match = re.search(r'(\d{4})', val)
year = year_match.group(1) if year_match else val
elif label.startswith('主演'):
actor = val
elif label.startswith('导演'):
director = val
elif label.startswith('更新'):
remarks = val
# 简介: p.desc
desc_tag = soup.find('p', class_=re.compile(r'desc|content__desc'))
if desc_tag:
content = desc_tag.get_text(strip=True)
content = re.sub(r'详情\s*$', '', content).strip()
# 播放源和集数(正序)
play_from_list, play_url_list = self._parse_play_sources(html, vod_id)
vod_item = {
"vod_id": vod_id,
"vod_name": title,
"vod_pic": poster,
"type_name": type_name,
"vod_year": year,
"vod_area": area,
"vod_remarks": remarks,
"vod_actor": actor,
"vod_director": director,
"vod_content": content,
"vod_play_from": '$$$'.join(play_from_list),
"vod_play_url": '$$$'.join(play_url_list),
}
return {"list": [vod_item]}
except Exception as e:
logger.error(f"获取详情失败: {e}")
return {"list": []}
def _parse_play_sources(self, html, vod_id):
"""解析播放源和集数 - 正序排列
结构: ul.nav-tabs > li > a[href="#playlist{sid}"] (源名)
div.tab-pane#playlist{sid} > ul > a[href="/vodplay/{vid}-{sid}-{nid}.html"] (集数)
"""
play_from_list = []
play_url_list = []
soup = BeautifulSoup(html, 'html.parser')
# 源名映射: {sid: 源名}
source_names = {}
nav = soup.find('ul', class_=re.compile(r'nav-tabs'))
if nav:
for a in nav.find_all('a', href=True):
m = re.search(r'#playlist(\w+)', a.get('href', ''))
if m:
source_names[m.group(1)] = a.get_text(strip=True)
# 每个 tab-pane 为一个源
panes = soup.find_all('div', class_=re.compile(r'tab-pane'))
if not panes:
# 兜底:直接按 play 链接分组
return self._parse_play_sources_fallback(html, vod_id)
for pane in panes:
pane_id = pane.get('id', '')
sid_match = re.search(r'playlist(\w+)', pane_id)
if not sid_match:
continue
sid = sid_match.group(1)
from_name = source_names.get(sid, f"线路{sid}")
ul = pane.find('ul')
if not ul:
continue
episodes = []
seen_nid = set()
for a in ul.find_all('a', href=re.compile(r'/vodplay/')):
href = a.get('href', '')
m = re.search(r'/vodplay/(\d+)-(\w+)-(\d+)\.html', href)
if not m:
continue
nid = int(m.group(3))
if nid in seen_nid:
continue
seen_nid.add(nid)
ep_name = a.get_text(strip=True) or f"{nid}"
full_url = self.BASE_URL + href
episodes.append((nid, ep_name, full_url))
if not episodes:
continue
# 正序排列
episodes = sorted(episodes, key=lambda x: x[0])
play_from_list.append(from_name)
urls = [f"{name}${u}" for _, name, u in episodes]
play_url_list.append('#'.join(urls))
if not play_from_list:
return self._parse_play_sources_fallback(html, vod_id)
return play_from_list, play_url_list
def _parse_play_sources_fallback(self, html, vod_id):
"""兜底:从所有 play 链接按 sid 分组"""
play_from_list = []
play_url_list = []
links = re.findall(r'/vodplay/\d+-\w+-\d+\.html', html)
sources = {}
for link in links:
m = re.match(r'/vodplay/(\d+)-(\w+)-(\d+)\.html', link)
if not m:
continue
sid = m.group(2)
nid = int(m.group(3))
sources.setdefault(sid, {})[nid] = link
for sid in sorted(sources.keys()):
eps = sources[sid]
episodes = sorted(eps.items())
play_from_list.append(f"线路{sid}")
urls = [f"{nid}集${self.BASE_URL}{link}" for nid, link in episodes]
play_url_list.append('#'.join(urls))
return play_from_list, play_url_list
# ==================== 播放 ====================
def playerContent(self, flag, id, vipFlags):
"""解析播放页 m3u8 链接"""
try:
url = id
# 兼容相对路径
if url.startswith('/'):
url = self.BASE_URL + url
elif not url.startswith('http'):
url = self.BASE_URL + '/vodplay/' + url
resp = self._get(url)
if not resp:
return {}
m3u8_url = self._extract_m3u8(resp.text)
if not m3u8_url:
return {}
return {
"parse": 0,
"playUrl": "",
"url": m3u8_url,
"header": "",
}
except Exception as e:
logger.error(f"解析播放失败: {e}")
return {}
def _extract_m3u8(self, html):
"""从播放页提取 m3u8 链接"""
match = re.search(r'player_aaaa\s*=\s*({[^<]+})', html)
if match:
try:
data = json.loads(match.group(1))
m3u8_url = data.get('url', '')
if m3u8_url:
return m3u8_url
except Exception:
pass
# 兜底:直接匹配 m3u8
m = re.search(r'(https?://[^"\'\\s]+\.m3u8[^"\'\\s]*)', html)
return m.group(1) if m else ''
# ==================== 搜索 ====================
def searchContent(self, key, quick, pg="1"):
"""搜索内容 - TVBox标准接口(key, quick, pg)
优先使用AJAX接口(JSON格式,速度快),失败则回退到HTML搜索页
"""
try:
page = int(pg) if pg else 1
encoded_key = urllib.parse.quote(key)
# 优先尝试 AJAX 建议接口(返回JSON,数据干净)
ajax_url = f"{self.BASE_URL}/index.php/ajax/suggest?mid=1&wd={encoded_key}"
resp = self._get(ajax_url)
if resp and resp.headers.get('content-type', '').find('json') >= 0:
try:
data = resp.json()
if data.get('code') == 1 and data.get('list'):
videos = []
for item in data['list']:
videos.append({
"vod_id": str(item.get('id', '')),
"vod_name": item.get('name', ''),
"vod_pic": item.get('pic', ''),
"vod_remarks": item.get('note', ''),
})
return {
"list": videos,
"page": data.get('page', page),
"pagecount": data.get('pagecount', 1),
"limit": data.get('limit', 20),
"total": data.get('total', len(videos)),
}
except Exception:
pass
# 回退到HTML搜索页
url = f"{self.BASE_URL}/vodsearch/-------------.html?wd={encoded_key}"
if page > 1:
url += f"&page={page}"
resp = self._get(url)
if not resp:
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
if any(k in resp.text for k in ['验证码', '人机验证', '安全验证', 'just_a_test']):
logger.warning("搜索被安全验证拦截")
return {"list": [], "page": page, "pagecount": 1, "limit": 20, "total": 0}
videos = self._parse_video_list(resp.text)
pagecount = self._parse_total_pages(resp.text)
return {
"list": videos,
"page": page,
"pagecount": pagecount if pagecount > 1 else 1,
"limit": 20,
"total": len(videos) * pagecount if pagecount > 1 else len(videos),
}
except Exception as e:
logger.error(f"搜索失败: {e}")
return {"list": [], "page": 1, "pagecount": 1, "limit": 20, "total": 0}
+259
View File
@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
import sys
import re
import json
from urllib.parse import urljoin, quote
sys.path.append('..')
try:
from base.spider import Spider
except ImportError:
class Spider:
def fetch(self, url, headers=None, **kw):
import requests as rq
kw.pop('timeout', None)
r = rq.get(url, headers=headers, timeout=15, **kw)
r.encoding = 'utf-8'
return r
HOST = "https://gqc.ink"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
CATEGORIES = {
"dianying": "电影", "lianxuju": "连续剧", "zongyi": "综艺",
"dongman": "动漫", "duanju": "短剧",
}
class Spider(Spider):
def init(self, extend=""):
global HOST
try:
r = self.fetch(HOST, headers={"User-Agent": UA}, timeout=15000)
if hasattr(r, 'url') and r.url and r.url != HOST.rstrip("/"):
HOST = r.url.rstrip("/")
except:
pass
def homeContent(self, filter=False):
r = {"class": [], "list": []}
for k, v in CATEGORIES.items():
r["class"].append({"type_id": k, "type_name": v})
return r
def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg=1, filter=False, extend=""):
pn = 1
try: pn = max(int(str(pg)), 1)
except: pass
cat = str(tid)
if cat not in CATEGORIES:
cat = "dianying"
slug = cat
try:
if pn > 1:
url = f"{HOST}/vodshow/{slug}--------{pn}---.html"
else:
url = f"{HOST}/vodshow/{slug}--------1---.html"
r = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
html = r.text if hasattr(r, 'text') else str(r)
items = self._items(html)
return {"page": pn, "pagecount": self._pagecount(html), "limit": 50, "total": len(items), "list": items}
except:
return {"page": pn, "pagecount": 1, "limit": 50, "total": 0, "list": []}
def detailContent(self, ids):
if isinstance(ids, list):
vid = ids[0] if ids else ""
else:
vid = str(ids) if ids else ""
m = re.search(r'(\d+)', str(vid))
vid = m.group(1) if m else ""
if not vid:
return {"list": []}
try:
r = self.fetch(f"{HOST}/neirong/{vid}.html", headers={"User-Agent": UA}, timeout=30000)
h = r.text if hasattr(r, 'text') else str(r)
except:
return {"list": []}
d = {"vod_id": vid, "vod_name": "", "vod_pic": "", "vod_year": "",
"vod_area": "", "vod_class": "", "vod_director": "", "vod_actor": "",
"vod_content": "", "vod_remarks": "", "vod_play_from": "", "vod_play_url": ""}
t1 = re.search(r'<h1[^>]*>(.*?)</h1>', h, re.S)
if t1:
clean = re.sub(r'<[^>]+>', ' ', t1.group(1)).strip()
d["vod_name"] = clean.split('\n')[0].strip()
if not d["vod_name"]:
t2 = re.search(r'<title>(.*?)</title>', h)
if t2: d["vod_name"] = t2.group(1).split("-")[0].strip()
p = re.search(r'data-original="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if not p:
p = re.search(r'data-background="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if not p:
p = re.search(r'<img[^>]*src="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if p: d["vod_pic"] = p.group(1)
desc_m = re.search(r'class="fed-part-esan[^"]*"[\s\S]*?<span[^>]*>简介[:]?</span>\s*&nbsp;.*?(?:<a[^>]*>[\s\S]*?</a>)\s*([^<]+)', h)
if not desc_m:
desc_m = re.search(r'class="fed-part-esan[^"]*"[\s\S]*?<span[^>]*>简介[:]?</span>\s*&nbsp;\s*([^<]+)', h)
if desc_m:
desc = re.sub(r'\s+', ' ', desc_m.group(1)).strip()[:500]
d["vod_content"] = desc
for mi in re.finditer(r'class="fed-deta-info[^"]*"[\s\S]*?>(.*?)</li>', h, re.S):
t = re.sub(r'<[^>]+>', '', mi.group(1)).strip()
if "导演" in t: d["vod_director"] = t.split("")[-1].split(":")[-1].strip()
elif "主演" in t: d["vod_actor"] = t.split("")[-1].split(":")[-1].strip()
elif "年份" in t or "上映" in t:
ym = re.search(r'(\d{4})', t)
if ym: d["vod_year"] = ym.group(1)
elif "备注" in t or "更新" in t: d["vod_remarks"] = t
try:
pf, pu = [], []
tops = re.search(r'fed-drop-tops(.*?)fed-drop-btms', h, re.S)
route_buttons = []
if tops:
for bm in re.finditer(r'href="/bofang/\d+-(\d+)-1\.html"[\s\S]*?>(.*?)<span[^>]*>(\d+)<', tops.group(1), re.S):
route = bm.group(1)
name = re.sub(r'<[^>]+>', '', bm.group(2)).strip()
count = int(bm.group(3))
if name and count > 0:
route_buttons.append((route, name, count))
vis = re.search(r'fed-play-item fed-drop-item fed-visible(.*?)</ul>\s*</div>', h, re.S)
default_eps = []
default_route = None
if vis:
for em in re.finditer(r'href="/bofang/\d+-(\d+)-(\d+)\.html"[\s\S]*?>([^<]+)<', vis.group(1)):
r2, nid, name = em.group(1), em.group(2), em.group(3).strip()
if not default_route:
default_route = r2
default_eps.append((int(nid), name))
if not route_buttons:
items = re.findall(r'href="(/bofang/\d+-\d+-\d+\.html)"[\s\S]*?>([^<]+)<', h)
routes = {}
for href, name in items:
name = name.strip()
if not name or "报错" in name or "剧情" in name or "简介" in name or "立即播放" in name:
continue
rm = re.search(r'/bofang/\d+-(\d+)-(\d+)\.html', href)
if rm:
rt, nid = rm.group(1), rm.group(2)
if rt not in routes:
routes[rt] = []
routes[rt].append(f"{name}${urljoin(HOST, href)}")
for rt in sorted(routes):
pf.append(f"{len(pf)+1}")
pu.append("#".join(routes[rt]))
else:
for route, name, count in route_buttons:
eps_list = []
if default_route == route and default_eps:
for nid, ename in default_eps:
eps_list.append(f"{ename}${urljoin(HOST, f'/bofang/{vid}-{route}-{nid}.html')}")
else:
if default_eps:
for nid, ename in default_eps:
if nid <= count:
eps_list.append(f"{ename}${urljoin(HOST, f'/bofang/{vid}-{route}-{nid}.html')}")
else:
for nid in range(1, count + 1):
eps_list.append(f"{nid}集${urljoin(HOST, f'/bofang/{vid}-{route}-{nid}.html')}")
if eps_list:
pf.append(name)
pu.append("#".join(eps_list))
if pf:
d["vod_play_from"] = "$$$".join(pf)
d["vod_play_url"] = "$$$".join(pu)
except:
pass
return {"list": [d]}
def searchContent(self, key, quick=False, pg="1"):
return {"list": []}
def playerContent(self, flag, id, vipFlags=None):
a, b = str(flag), str(id) if id else ""
if a.startswith("http") or "/bofang/" in a:
url = a
elif b.startswith("http") or "/bofang/" in b:
url = b
elif a.startswith("/"):
url = urljoin(HOST, a)
elif b.startswith("/"):
url = urljoin(HOST, b)
else:
url = a
try:
r = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
h = r.text if hasattr(r, 'text') else str(r)
except:
return {"url": ""}
dm = re.search(r'"dmId"\s*:\s*"([^"]+)"', h)
if dm:
u = dm.group(1)
if u.startswith("http"):
if ".m3u8" in u:
return {"url": u}
return {"url": u}
m3u8 = re.search(r'(https?://[^\s"\'<>]+\.m3u8)', h)
if m3u8:
return {"url": m3u8.group(1)}
return {"url": ""}
def localProxy(self, param):
pass
def _pagecount(self, html):
pc = 1
# 优先用 data-total 属性 (fed模板分页跳转按钮)
dt = re.findall(r'data-total="(\d+)"', html)
for t in dt:
try: pc = max(pc, int(t))
except: pass
# /vodshow/ 格式分页: href="/vodshow/dianying--------2---.html"
pages_vod = re.findall(r'href="/vodshow/[^"]*-(\d+)-*--*\.html"', html)
for p in pages_vod:
try:
n = int(p)
if n > 1 and n < 10000:
pc = max(pc, n)
except: pass
# /fenlei/ 格式分页 (兼容旧格式)
pages_fen = re.findall(r'href="/fenlei/[^"]*-?(\d+)\.html"', html)
for p in pages_fen:
try:
n = int(p)
if n > 1 and n < 10000:
pc = max(pc, n)
except: pass
return pc
def _items(self, html):
items, seen = [], set()
# 只取 data-original 的列表项,不取 data-background 的轮播项
# 轮播项标题和封面不在同一个结构里,混入会导致错位
cover_map = {}
for m in re.finditer(r'data-original="(https?://[^"]+)"', html, re.I):
# 找到 data-original 前面最近的 href (取最后一个,不是第一个)
before = html[max(0, m.start()-500):m.start()]
hms = re.findall(r'href="(/neirong/(\d+)\.html)"', before)
if hms:
hm = hms[-1]
vid = hm[1]
if vid not in cover_map:
cover_map[vid] = (hm[0], m.group(1))
# 标题: fed-list-title
titles = {}
for tm in re.finditer(r'class="[^"]*fed-list-title[^"]*"[\s\S]*?href="/neirong/(\d+)\.html"[\s\S]*?>\s*(.*?)\s*</a>', html, re.S):
name = re.sub(r'<[^>]+>', '', tm.group(2)).strip()
if name:
titles[tm.group(1)] = name[:50]
# 合并: 只用列表项的封面
for vid, (href, pic) in cover_map.items():
if vid in seen:
continue
seen.add(vid)
items.append({
"vod_id": vid, "vod_name": titles.get(vid, ""),
"vod_pic": pic, "vod_remarks": "", "vod_url": urljoin(HOST, href),
})
return items
+215
View File
@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
import requests, re, json, base64
from urllib.parse import urljoin, quote, unquote
HOST = "https://www.moxy.top"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
class Spider:
def init(self, extend=""):
global HOST
try:
resp = requests.get(HOST, headers={"User-Agent": UA}, timeout=15, allow_redirects=True)
if resp.url and "moxy" not in resp.url:
HOST = resp.url.rstrip("/")
except:
pass
def homeContent(self, filter=False):
r = {"class": [], "list": [], "filter": {}}
for k, v in {"1":"电影","2":"连续剧","3":"综艺","4":"动漫","5":"短剧"}.items():
r["class"].append({"type_id": k, "type_name": v})
try:
resp = requests.get(HOST, headers={"User-Agent": UA}, timeout=30)
resp.encoding = "utf-8"
r["list"] = self._items(resp.text)[:60]
except:
pass
return r
def homeVideoContent(self):
return {"list": self.homeContent().get("list", [])}
def categoryContent(self, tid, pg=1, filter=False, extend=""):
pn = 1
try: pn = max(int(str(pg)), 1)
except: pass
cid = str(tid) if str(tid) in "12345" else "1"
try:
# 网站分页URL格式: /vodshow/{cid}--------{page}---.html
if pn > 1:
url = f"{HOST}/vodshow/{cid}--------{pn}---.html"
else:
url = f"{HOST}/vodshow/{cid}-----------.html"
resp = requests.get(url, headers={"User-Agent": UA}, timeout=30)
resp.encoding = "utf-8"
items = self._items(resp.text)
# 提取总页数
pagecount = self._pagecount(resp.text)
return {"page": pn, "pagecount": pagecount, "limit": 50, "total": len(items), "list": items}
except:
return {"page": pn, "pagecount": 1, "limit": 50, "total": 0, "list": []}
def detailContent(self, ids):
ids = str(ids) if ids else ""
m = re.search(r'(\d+)', ids)
vid = m.group(1) if m else ""
if not vid: return {"list": []}
try:
resp = requests.get(f"{HOST}/voddetail{vid}.html", headers={"User-Agent": UA}, timeout=30)
resp.encoding = "utf-8"
except:
return {"list": []}
h = resp.text
d = {"vod_id": vid, "vod_name": "", "vod_pic": "", "vod_year": "",
"vod_area": "", "vod_class": "", "vod_director": "", "vod_actor": "",
"vod_content": "", "vod_remarks": "", "vod_play_from": "", "vod_play_url": ""}
t1 = re.search(r'<h1[^>]*>(.*?)</h1>', h)
if t1: d["vod_name"] = t1.group(1).strip()
if not d["vod_name"]:
t2 = re.search(r'<title>(.*?)</title>', h)
if t2: d["vod_name"] = t2.group(1).split("-")[0].strip()
p = re.search(r'data-original="([^"]+)"', h)
if p: d["vod_pic"] = p.group(1)
for t in re.findall(r'<a[^>]*title="(\d{4})"', h):
d["vod_year"] = t
for t in re.findall(r'<a[^>]*title="([^"]*)"', h):
if t in ("中国大陆","中国","香港","台湾","美国","日本","韩国","英国","法国","泰国","印度"):
d["vod_area"] = t
desc = re.search(r'<div[^>]*class="[^"]*module-info-introduction-content[^"]*"[^>]*>\s*<p>(.*?)</p>', h, re.S)
if desc: d["vod_content"] = re.sub(r'<[^>]+>', '', desc.group(1)).strip()[:500]
for m in re.finditer(r'<div[^>]*class="[^"]*module-info-item[^"]*"[^>]*>(.*?)</div>', h, re.S):
t = re.sub(r'<[^>]+>', '', m.group(1)).strip()
if "导演" in t: d["vod_director"] = t.replace("导演:","").replace("导演:","").strip()
elif "主演" in t: d["vod_actor"] = t.replace("主演:","").replace("主演:","").strip()
elif "备注" in t: d["vod_remarks"] = t.replace("备注:","").replace("备注:","").strip()
try:
sources = re.findall(r'data-dropdown-value="([^"]+)"', h) or ["默认"]
blocks = re.findall(r'<div[^>]*class="[^"]*module-play-list[^"]*"[^>]*>(.*?)</div>\s*</div>\s*</div>', h, re.S)
if not blocks:
blocks = re.findall(r'<div[^>]*class="[^"]*module-play-list-content[^"]*"[^>]*>(.*?)</div>', h, re.S)
pf, pu = [], []
for i, blk in enumerate(blocks):
eps = re.findall(r'<a[^>]*href="(/vodplay/[^"]+)"[^>]*>(?:<[^>]+>)*([^<]{1,20})(?:</[^>]+>)*</a>', blk, re.S)
if not eps:
eps = re.findall(r'<a[^>]*href="(/vodplay/[^"]+)"[^>]*>.*?<span>(.*?)</span>', blk, re.S)
if eps:
src = sources[i] if i < len(sources) else f"{i+1}"
el = [f"{n.strip()}${urljoin(HOST, u)}" for u, n in eps if n.strip()]
if el:
pf.append(src)
pu.append("#".join(el))
if pf:
d["vod_play_from"] = "$$$".join(pf)
d["vod_play_url"] = "$$$".join(pu)
except:
pass
return {"list": [d]}
def searchContent(self, key, quick=False, pg="1"):
try:
resp = requests.get(f"{HOST}/vodsearch/{quote(str(key))}-------------.html", headers={"User-Agent": UA}, timeout=15)
resp.encoding = "utf-8"
if len(resp.text) > 200:
return {"list": self._items(resp.text)[:30]}
except:
pass
return {"list": []}
def playerContent(self, flag, id, vipFlags=None):
"""
兼容两种壳子参数顺序:
- FongMi/TV: playerContent(flag, id, vipFlags)
- PeekPro/CatVod: playerContent(id, flag, vipFlags)
自动检测哪个是播放页URL,哪个是flag
"""
a, b = str(flag), str(id) if id else ""
# 自动判断:包含http或/vodplay的是URL,另一个是flag
if a.startswith("http") or "/vodplay/" in a:
url, flag_val = a, b
elif b.startswith("http") or "/vodplay/" in b:
url, flag_val = b, a
elif a.startswith("/"):
url, flag_val = urljoin(HOST, a), b
elif b.startswith("/"):
url, flag_val = urljoin(HOST, b), a
else:
url, flag_val = a, b
try:
resp = requests.get(url, headers={"User-Agent": UA}, timeout=30)
resp.encoding = "utf-8"
except:
return {"url": ""}
pd = re.search(r'player_data\s*=\s*(\{.*?\})', resp.text, re.S)
if pd:
try:
data = json.loads(pd.group(1))
u = data.get("url", "")
if u:
try:
real_url = unquote(base64.b64decode(u).decode("utf-8"))
except:
real_url = u
if real_url.startswith("http"):
return {"url": real_url}
except:
pass
return {"url": ""}
def localProxy(self, param):
return []
def _pagecount(self, html):
"""从页面提取总页数"""
pc = 1
# 方法1: 尾页链接
last = re.search(r'<a[^>]*href="[^"]*vodshow/\d+[^"]*(\d+)---\.html"[^>]*>尾页', html, re.S)
if last:
pc = max(pc, int(last.group(1)))
# 方法2: page-link page-next 链接中的最大页码
page_links = re.findall(r'<a[^>]*href="[^"]*vodshow/\d+-(\d+)', html)
for p in page_links:
try:
n = int(p)
if n > 100: # 年份过滤掉
continue
pc = max(pc, n)
except:
pass
# 方法3: page-current 附近的页码
all_nums = re.findall(r'class="[^"]*page-number[^"]*"[^>]*>\s*(\d+)\s*<', html)
for n in all_nums:
try:
pc = max(pc, int(n))
except:
pass
return pc
def _items(self, html):
items, seen = [], set()
def ext(href, block):
v = re.search(r'/voddetail(\d+)\.html', href)
if not v or v.group(1) in seen: return None
seen.add(v.group(1))
t = (re.search(r'title="([^"]*)"', block) or re.search(r'alt="([^"]*)"', block))
if not t: return None
p = re.search(r'data-original="([^"]+)"', block)
n = re.search(r'<div[^>]*class="[^"]*module-item-note[^"]*"[^>]*>([^<]+)</div>', block)
return {"vod_id": v.group(1), "vod_name": t.group(1),
"vod_pic": p.group(1) if p else "",
"vod_remarks": n.group(1).strip() if n else "",
"vod_url": urljoin(HOST, href)}
for m in re.finditer(
r'<a[^>]*href="(/voddetail\d+\.html)"[^>]*title="([^"]*)"[^>]*class="[^"]*module-poster-item[^"]*"[^>]*>.*?</a>',
html, re.S
):
item = ext(m.group(1), m.group(0))
if item: items.append(item)
for m in re.finditer(
r'<a[^>]*href="(/voddetail\d+\.html)"[^>]*class="[^"]*module-card-item-poster[^"]*"[^>]*>.*?</a>',
html, re.S
):
item = ext(m.group(1), m.group(0))
if item: items.append(item)
return items
+244
View File
@@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
import sys
import re
import json
import base64
from urllib.parse import urljoin, quote, unquote
sys.path.append('..')
try:
from base.spider import Spider
except ImportError:
class Spider:
def fetch(self, url, headers=None, **kw):
import requests as rq
kw.pop('timeout', None)
r = rq.get(url, headers=headers, timeout=15, **kw)
r.encoding = 'utf-8'
return r
HOST = "https://www.qlys.cc"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
CATEGORIES = {
"1": "电影", "2": "电视剧", "3": "短剧", "4": "动漫", "5": "综艺",
}
class Spider(Spider):
def init(self, extend=""):
global HOST
try:
resp = self.fetch(HOST, headers={"User-Agent": UA}, timeout=15000)
if hasattr(resp, 'url') and resp.url and resp.url != HOST.rstrip("/"):
HOST = resp.url.rstrip("/")
except:
pass
def homeContent(self, filter=False):
r = {"class": [], "list": [], "filter": {}}
for k, v in CATEGORIES.items():
r["class"].append({"type_id": k, "type_name": v})
try:
resp = self.fetch(HOST, headers={"User-Agent": UA}, timeout=30000)
html = resp.text if hasattr(resp, 'text') else str(resp)
items = self._items(html)
r["list"] = [it for it in items if it["vod_pic"]][:30]
except:
pass
return r
def homeVideoContent(self):
return {"list": []}
def categoryContent(self, tid, pg=1, filter=False, extend=""):
pn = 1
try: pn = max(int(str(pg)), 1)
except: pass
cid = str(tid)
try:
if pn > 1:
url = f"{HOST}/index.php/vod/type/id/{cid}/page/{pn}.html"
else:
url = f"{HOST}/index.php/vod/type/id/{cid}.html"
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
html = resp.text if hasattr(resp, 'text') else str(resp)
items = self._items(html)
pagecount = self._pagecount(html)
return {"page": pn, "pagecount": pagecount, "limit": 50, "total": len(items), "list": items}
except:
return {"page": pn, "pagecount": 1, "limit": 50, "total": 0, "list": []}
def detailContent(self, ids):
if isinstance(ids, list):
vid = ids[0] if ids else ""
else:
vid = str(ids) if ids else ""
m = re.search(r'(\d+)', str(vid))
vid = m.group(1) if m else ""
if not vid: return {"list": []}
try:
resp = self.fetch(f"{HOST}/index.php/vod/detail/id/{vid}.html", headers={"User-Agent": UA}, timeout=30000)
h = resp.text if hasattr(resp, 'text') else str(resp)
except:
return {"list": []}
d = {"vod_id": vid, "vod_name": "", "vod_pic": "", "vod_year": "",
"vod_area": "", "vod_class": "", "vod_director": "", "vod_actor": "",
"vod_content": "", "vod_remarks": "", "vod_play_from": "", "vod_play_url": ""}
t1 = re.search(r'<h1[^>]*>(.*?)</h1>', h, re.S)
if t1:
clean = re.sub(r'<[^>]+>', ' ', t1.group(1)).strip()
parts = re.split(r'\s{2,}|\n', clean)
for part in parts:
p = part.strip()
if p and len(p) > 1:
d["vod_name"] = p
break
if not d["vod_name"]:
t2 = re.search(r'<title>(.*?)</title>', h)
if t2: d["vod_name"] = t2.group(1).split("-")[0].strip()
p = re.search(r'data-original="([^"]+)"', h)
if not p:
p = re.search(r'data-background="([^"]+)"', h)
if not p:
p = re.search(r'<img[^>]*src="([^"]+\.(?:jpg|jpeg|png|webp))"', h, re.I)
if p: d["vod_pic"] = p.group(1)
for t in re.findall(r'<a[^>]*title="(\d{4})"', h):
d["vod_year"] = t
desc = re.search(r'<div\s+id="content"[^>]*>(.*?)</div>\s*</div>', h, re.S)
if desc: d["vod_content"] = re.sub(r'<[^>]+>', '', desc.group(1)).strip()[:500]
if not d["vod_content"]:
desc2 = re.search(r'class="vod_content[^"]*"[^>]*>(.*?)</div>', h, re.S)
if desc2: d["vod_content"] = re.sub(r'<[^>]+>', '', desc2.group(1)).strip()[:500]
for mi in re.finditer(r'class="slide_info[^"]*"[^>]*>(.*?)</div>', h, re.S):
t = re.sub(r'<[^>]+>', '', mi.group(1)).strip()
if "导演" in t: d["vod_director"] = t.split("")[-1].split(":")[-1].strip()
elif "主演" in t: d["vod_actor"] = t.split("")[-1].split(":")[-1].strip()
elif "备注" in t or "更新" in t or "集数" in t: d["vod_remarks"] = t
try:
default_block = re.search(r'id="tab_con_playlist_(\d+)"[^>]*(?:style="display:block"|style="[^"]*"[^>]*>)(.*?)(?=<div\s+class="tab-content"|<div\s+id="tab_con_playlist_)', h, re.S)
if not default_block:
default_block = re.search(r'id="tab_con_playlist_(\d+)"[^>]*>(.*?)(?=<div\s+class="tab-content"|<div\s+id="tab_con_playlist_)', h, re.S)
default_pid = default_block.group(1) if default_block else "1"
default_content = default_block.group(2) if default_block else ""
tab_names = re.findall(r'<li[^>]*class="tab-switch[^"]*"[^>]*switch="tab_con_playlist_(\d+)"[^>]*>.*?<a[^>]*>\s*(.*?)\s*</a>', h, re.S)
default_name = "默认"
for pid, name in tab_names:
if pid == default_pid:
default_name = name.strip()
break
eps = re.findall(r'href="(/index\.php/vod/play/id/[^"]+/sid/\d+/nid/\d+\.html)"[^>]*>([^<]*)<', default_content)
el = []
for u, n in eps:
n = n.strip()
if n and "报错" not in n:
el.append(f"{n}${urljoin(HOST, u)}")
if not el:
play_eps = re.findall(r'href="(/index\.php/vod/play/id/\d+/sid/(\d+)/nid/\d+\.html)"[^>]*>([^<]*)<', h)
sources = {}
for href, sid, name in play_eps:
name = name.strip()
if not name or "报错" in name: continue
if sid not in sources:
sources[sid] = []
sources[sid].append(f"{name}${urljoin(HOST, href)}")
if default_pid in sources:
el = sources[default_pid]
if el:
d["vod_play_from"] = default_name
d["vod_play_url"] = "#".join(el)
except:
pass
return {"list": [d]}
def searchContent(self, key, quick=False, pg="1"):
try:
resp = self.fetch(f"{HOST}/index.php/vod/search.html", params={"wd": str(key)}, headers={"User-Agent": UA}, timeout=15000)
html = resp.text if hasattr(resp, 'text') else str(resp)
if len(html) > 200:
return {"list": self._items(html)[:30]}
except:
pass
return {"list": []}
def playerContent(self, flag, id, vipFlags=None):
a, b = str(flag), str(id) if id else ""
if a.startswith("http") or "/vod/play/" in a or "/index.php/vod/play/" in a:
url = a
elif b.startswith("http") or "/vod/play/" in b or "/index.php/vod/play/" in b:
url = b
elif a.startswith("/"):
url = urljoin(HOST, a)
elif b.startswith("/"):
url = urljoin(HOST, b)
else:
url = a
try:
resp = self.fetch(url, headers={"User-Agent": UA}, timeout=30000)
h = resp.text if hasattr(resp, 'text') else str(resp)
except:
return {"url": ""}
data = None
for tag in ['player_aaaa', 'player_data']:
idx = h.find(tag)
if idx < 0: continue
brace = h.find('{', idx)
if brace < 0: continue
depth, end = 0, brace
for i in range(brace, min(brace + 10000, len(h))):
if h[i] == '{': depth += 1
elif h[i] == '}':
depth -= 1
if depth == 0:
end = i + 1
break
try:
data = json.loads(h[brace:end])
except:
data = None
if data:
break
if data:
u = data.get("url", "")
if u:
if data.get("encrypt", 0) == 1:
try: u = unquote(base64.b64decode(u).decode("utf-8"))
except: pass
if u.startswith("http"):
return {"url": u}
return {"url": ""}
def localProxy(self, param):
pass
def _pagecount(self, html):
pc = 1
last = re.search(r'<a[^>]*href="[^"]*page/(\d+)\.html"[^>]*>尾页', html)
if last:
pc = max(pc, int(last.group(1)))
pages = re.findall(r'href="[^"]*page/(\d+)\.html"', html)
for p in pages:
try: pc = max(pc, int(p))
except: pass
return pc
def _items(self, html):
items, seen = [], set()
for m in re.finditer(r'<a[^>]*href="(/index\.php/vod/detail/id/(\d+)\.html)"[^>]*title="([^"]*)"', html):
vid, title = m.group(2), m.group(3)
if vid in seen: continue
seen.add(vid)
a_end = html.find('</a>', m.start())
if a_end < 0: continue
a_block = html[m.start():min(a_end + 4, m.start() + 2000)]
pic = re.search(r'data-original="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', a_block, re.I)
if not pic:
pic = re.search(r'data-background="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', a_block, re.I)
if not pic:
pic = re.search(r'<img[^>]*src="(https?://[^"]+\.(?:jpg|jpeg|png|webp))"', a_block, re.I)
items.append({
"vod_id": vid, "vod_name": title,
"vod_pic": pic.group(1) if pic else "",
"vod_remarks": "",
"vod_url": urljoin(HOST, m.group(1)),
})
return items