443 lines
17 KiB
Python
443 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# 少货影视 - TVbox标准版
|
|
|
|
import re
|
|
import sys
|
|
import json
|
|
import urllib.parse
|
|
import urllib.request
|
|
import ssl
|
|
import http.cookiejar
|
|
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
|
|
ssl._create_default_https_context = ssl._create_unverified_context
|
|
|
|
|
|
class Spider(Spider):
|
|
|
|
def init(self, extend=""):
|
|
self.extend = extend
|
|
self.host = 'https://shdy3.com'
|
|
self.cookie_jar = http.cookiejar.CookieJar()
|
|
self.cookie_handler = urllib.request.HTTPCookieProcessor(self.cookie_jar)
|
|
self.opener = urllib.request.build_opener(self.cookie_handler)
|
|
self.fetch(self.host)
|
|
|
|
def getName(self):
|
|
return "少货影视"
|
|
|
|
def isVideoFormat(self, url):
|
|
return False
|
|
|
|
def manualVideoCheck(self):
|
|
return False
|
|
|
|
def destroy(self):
|
|
pass
|
|
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Linux; Android 9; ALN-AL00 Build/PQ3B.190801.05281406; wv) AppleWebKit/537.36',
|
|
'accept-language': 'zh-CN,zh;q=0.9',
|
|
'Referer': 'https://shdy3.com/',
|
|
}
|
|
|
|
classes_config = [
|
|
("电影", "1"),
|
|
("电视剧", "2"),
|
|
("动漫", "4"),
|
|
]
|
|
|
|
def fetch(self, url):
|
|
try:
|
|
req = urllib.request.Request(url, headers=self.headers)
|
|
with self.opener.open(req, timeout=15) as response:
|
|
content = response.read()
|
|
try:
|
|
return content.decode('utf-8')
|
|
except:
|
|
return content.decode('gbk', errors='ignore')
|
|
except Exception as e:
|
|
print(f"请求失败: {e}")
|
|
return ''
|
|
|
|
def clean_html(self, text):
|
|
if not text:
|
|
return ''
|
|
text = re.sub(r'<[^>]+>', '', text)
|
|
text = re.sub(r'\s+', ' ', text)
|
|
return text.strip()
|
|
|
|
# ==================== 首页 ====================
|
|
def homeContent(self, filter):
|
|
result = {}
|
|
classes = []
|
|
for name, tid in self.classes_config:
|
|
classes.append({"type_name": name, "type_id": tid})
|
|
|
|
filters = {
|
|
"1": [{"key": "cateId", "name": "类型", "value": [
|
|
{"n": "全部", "v": "1"},
|
|
{"n": "喜剧", "v": "6"},
|
|
{"n": "爱情", "v": "7"},
|
|
{"n": "恐怖", "v": "8"},
|
|
{"n": "动作", "v": "9"},
|
|
{"n": "科幻", "v": "10"},
|
|
{"n": "战争", "v": "11"},
|
|
{"n": "犯罪", "v": "12"},
|
|
{"n": "动画", "v": "13"},
|
|
{"n": "奇幻", "v": "14"},
|
|
{"n": "剧情", "v": "15"},
|
|
{"n": "冒险", "v": "16"},
|
|
{"n": "悬疑", "v": "17"},
|
|
{"n": "惊悚", "v": "18"},
|
|
{"n": "其他", "v": "20"},
|
|
]}],
|
|
"2": [{"key": "cateId", "name": "类型", "value": [
|
|
{"n": "全部", "v": "2"},
|
|
{"n": "国产剧", "v": "20"},
|
|
{"n": "TVB", "v": "21"},
|
|
{"n": "韩剧", "v": "22"},
|
|
{"n": "美剧", "v": "23"},
|
|
{"n": "日剧", "v": "24"},
|
|
{"n": "英剧", "v": "25"},
|
|
{"n": "台剧", "v": "26"},
|
|
{"n": "其他", "v": "27"},
|
|
]}],
|
|
}
|
|
result['filters'] = filters
|
|
|
|
html = self.fetch(self.host)
|
|
vlist = []
|
|
if html:
|
|
items = re.findall(
|
|
r'<a[^>]*class="myui-vodlist__thumb[^"]*"[^>]*href="([^"]+)"[^>]*title="([^"]+)"[^>]*data-original="([^"]+)"[^>]*>.*?<span[^>]*class="pic-text[^"]*"[^>]*>(.*?)</span>',
|
|
html, re.DOTALL
|
|
)
|
|
for href, title, pic, remark in items[:12]:
|
|
if href and title:
|
|
if not href.startswith('http'):
|
|
href = self.host + href if href.startswith('/') else self.host + '/' + href
|
|
if pic and not pic.startswith('http'):
|
|
pic = self.host + pic if pic.startswith('/') else self.host + '/' + pic
|
|
if pic and pic.startswith('//'):
|
|
pic = 'https:' + pic
|
|
vlist.append({
|
|
'vod_id': href,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': self.clean_html(remark)
|
|
})
|
|
|
|
result['class'] = classes
|
|
result['list'] = vlist
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
return {}
|
|
|
|
# ==================== 分类 ====================
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
page = int(pg) if pg else 1
|
|
|
|
if extend and 'cateId' in extend:
|
|
cateId = extend.get('cateId')
|
|
else:
|
|
cateId = tid
|
|
|
|
if page <= 1:
|
|
url = f"{self.host}/list/{cateId}.html"
|
|
else:
|
|
url = f"{self.host}/list/{cateId}-{page}.html"
|
|
|
|
html = self.fetch(url)
|
|
if not html:
|
|
return {'list': [], 'page': page, 'pagecount': 1}
|
|
|
|
videos = []
|
|
|
|
items = re.findall(
|
|
r'<div class="v_img">.*?<a href="([^"]+)" title="([^"]+)">.*?<img[^>]*data-original="([^"]+)".*?<div class="v_note">(.*?)</div>',
|
|
html, re.DOTALL
|
|
)
|
|
|
|
for href, title, pic, remark in items:
|
|
if href and title:
|
|
if not href.startswith('http'):
|
|
href = self.host + href if href.startswith('/') else self.host + '/' + href
|
|
if pic and not pic.startswith('http'):
|
|
pic = self.host + pic if pic.startswith('/') else self.host + '/' + pic
|
|
if pic and pic.startswith('//'):
|
|
pic = 'https:' + pic
|
|
videos.append({
|
|
'vod_id': href,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': self.clean_html(remark)
|
|
})
|
|
|
|
if not videos:
|
|
items2 = re.findall(
|
|
r'<a href="(/movie/[^"]+)" title="([^"]+)">.*?<img[^>]*data-original="([^"]+)"',
|
|
html, re.DOTALL
|
|
)
|
|
for href, title, pic in items2:
|
|
if href and title:
|
|
href = self.host + href if href.startswith('/') else self.host + '/' + href
|
|
if pic and not pic.startswith('http'):
|
|
pic = self.host + pic if pic.startswith('/') else self.host + '/' + pic
|
|
if pic and pic.startswith('//'):
|
|
pic = 'https:' + pic
|
|
videos.append({
|
|
'vod_id': href,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': ''
|
|
})
|
|
|
|
pagecount = page
|
|
page_match = re.search(r'<span>\d+/(\d+)</span>', html)
|
|
if page_match:
|
|
pagecount = int(page_match.group(1))
|
|
else:
|
|
if re.search(r'<a[^>]*href="[^"]*-\d+\.html"[^>]*>下一页</a>', html):
|
|
pagecount = page + 1
|
|
else:
|
|
pagecount = page
|
|
|
|
return {
|
|
'list': videos,
|
|
'page': page,
|
|
'pagecount': pagecount,
|
|
'limit': len(videos),
|
|
'total': pagecount * len(videos) if videos else 999999
|
|
}
|
|
|
|
# ==================== 详情 ====================
|
|
def detailContent(self, ids):
|
|
vid = ids[0] if ids else ''
|
|
if not vid:
|
|
return {'list': []}
|
|
|
|
if not vid.startswith('http'):
|
|
vid = self.host + vid if vid.startswith('/') else self.host + '/' + vid
|
|
|
|
html = self.fetch(vid)
|
|
if not html:
|
|
return {'list': []}
|
|
|
|
info = {'vod_id': vid}
|
|
|
|
# ====== 标题 ======
|
|
title_match = re.search(r'<h1[^>]*class="v_title[^"]*"[^>]*>(.*?)</h1>', html, re.DOTALL)
|
|
info['vod_name'] = self.clean_html(title_match.group(1)) if title_match else '未知'
|
|
|
|
# ====== 海报 ======
|
|
pic_match = re.search(r'<img[^>]*class="lazyload"[^>]*data-original="([^"]+)"', html)
|
|
if pic_match:
|
|
pic = pic_match.group(1)
|
|
if pic and not pic.startswith('http'):
|
|
pic = self.host + pic if pic.startswith('/') else self.host + '/' + pic
|
|
if pic and pic.startswith('//'):
|
|
pic = 'https:' + pic
|
|
info['vod_pic'] = pic
|
|
else:
|
|
info['vod_pic'] = ''
|
|
|
|
# ====== 简介 ======
|
|
desc_match = re.search(r'<p[^>]*class="p_txt[^"]*"[^>]*>(.*?)</p>', html, re.DOTALL)
|
|
info['vod_content'] = self.clean_html(desc_match.group(1)) if desc_match else ''
|
|
|
|
# ====== 导演、演员、年份、类型、地区 ======
|
|
info['vod_director'] = ''
|
|
info['vod_actor'] = ''
|
|
info['vod_year'] = ''
|
|
info['type_name'] = ''
|
|
info['vod_area'] = ''
|
|
|
|
info_p = re.search(r'<p>(.*?)</p>', html, re.DOTALL)
|
|
if info_p:
|
|
text = info_p.group(1)
|
|
# 去除<a>标签
|
|
text = re.sub(r'<a[^>]*>.*?</a>', '', text)
|
|
text = self.clean_html(text)
|
|
|
|
# 用 / 分割
|
|
parts = text.split('/')
|
|
for part in parts:
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
|
|
# 导演
|
|
if '导演:' in part:
|
|
info['vod_director'] = part.replace('导演:', '').strip()
|
|
# 主演
|
|
elif '主演:' in part:
|
|
info['vod_actor'] = part.replace('主演:', '').strip()
|
|
# 如果只有纯文本且没有冒号
|
|
elif ':' not in part:
|
|
if re.match(r'^\d{4}$', part):
|
|
info['vod_year'] = part
|
|
elif part in ['大陆', '美国', '日本', '韩国', '英国', '法国', '德国', '意大利', '西班牙', '加拿大', '澳大利亚', '台湾', '香港', '泰国', '印度', '其他']:
|
|
info['vod_area'] = part
|
|
else:
|
|
info['type_name'] = part
|
|
|
|
# 如果没提取到年份,从其他地方匹配
|
|
if not info['vod_year']:
|
|
year_match = re.search(r'(\d{4})</a>', html)
|
|
if year_match:
|
|
info['vod_year'] = year_match.group(1)
|
|
|
|
# ====== 播放列表 ======
|
|
play_from = []
|
|
play_url = []
|
|
|
|
from_match = re.search(r'<ul[^>]*class="from_list"[^>]*>(.*?)</ul>', html, re.DOTALL)
|
|
link_match = re.search(r'<ul[^>]*class="play_list"[^>]*id="play_link"[^>]*>(.*?)</ul>', html, re.DOTALL)
|
|
|
|
if from_match and link_match:
|
|
names = re.findall(r'<li[^>]*>(.*?)</li>', from_match.group(1), re.DOTALL)
|
|
names = [self.clean_html(n) for n in names if n.strip()]
|
|
link_blocks = re.findall(r'<li[^>]*>(.*?)</li>', link_match.group(1), re.DOTALL)
|
|
|
|
if names and link_blocks and len(names) == len(link_blocks):
|
|
for idx, block in enumerate(link_blocks):
|
|
eps = re.findall(r'<a[^>]*href="([^"]+)"[^>]*>(.*?)</a>', block, re.DOTALL)
|
|
source_name = names[idx] if idx < len(names) else f"线路{idx+1}"
|
|
episodes = []
|
|
for ep_href, ep_name in eps:
|
|
ep_name = ep_name.strip()
|
|
if ep_name and ep_href:
|
|
if not ep_href.startswith('/'):
|
|
ep_href = '/' + ep_href
|
|
num = re.sub(r'[^0-9]', '', ep_name)
|
|
if num:
|
|
num = int(num)
|
|
else:
|
|
num = len(episodes) + 1
|
|
episodes.append((num, f'{ep_name}${ep_href}'))
|
|
if episodes:
|
|
episodes.sort(key=lambda x: x[0])
|
|
play_from.append(source_name)
|
|
play_url.append('#'.join([ep[1] for ep in episodes]))
|
|
|
|
if not play_from:
|
|
panels = re.findall(
|
|
r'<div[^>]*class="myui-panel_bd[^"]*"[^>]*>.*?<ul[^>]*class="myui-content__list[^"]*"[^>]*>(.*?)</ul>',
|
|
html, re.DOTALL
|
|
)
|
|
titles = re.findall(r'<h3[^>]*class="title"[^>]*>(.*?)</h3>', html)
|
|
if panels:
|
|
for idx, panel in enumerate(panels):
|
|
eps = re.findall(r'<a[^>]*href="([^"]+)"[^>]*>(.*?)</a>', panel, re.DOTALL)
|
|
source_name = titles[idx] if idx < len(titles) else f"线路{idx+1}"
|
|
episodes = []
|
|
for ep_href, ep_name in eps:
|
|
ep_name = ep_name.strip()
|
|
if ep_name and ep_href:
|
|
if not ep_href.startswith('/'):
|
|
ep_href = '/' + ep_href
|
|
num = re.sub(r'[^0-9]', '', ep_name)
|
|
if num:
|
|
num = int(num)
|
|
else:
|
|
num = len(episodes) + 1
|
|
episodes.append((num, f'{ep_name}${ep_href}'))
|
|
if episodes:
|
|
episodes.sort(key=lambda x: x[0])
|
|
play_from.append(source_name)
|
|
play_url.append('#'.join([ep[1] for ep in episodes]))
|
|
|
|
if not play_from:
|
|
all_eps = re.findall(r'<a[^>]*href="(/play/[^"]+)"[^>]*>([^<]+)</a>', html, re.DOTALL)
|
|
if all_eps:
|
|
episodes = []
|
|
for ep_href, ep_name in all_eps:
|
|
ep_name = ep_name.strip()
|
|
if ep_name and ep_href:
|
|
if not ep_href.startswith('/'):
|
|
ep_href = '/' + ep_href
|
|
num = re.sub(r'[^0-9]', '', ep_name)
|
|
if num:
|
|
num = int(num)
|
|
else:
|
|
num = len(episodes) + 1
|
|
episodes.append((num, f'{ep_name}${ep_href}'))
|
|
if episodes:
|
|
episodes.sort(key=lambda x: x[0])
|
|
play_from.append("骚火影视")
|
|
play_url.append('#'.join([ep[1] for ep in episodes]))
|
|
|
|
info["vod_play_from"] = '$$$'.join(play_from) if play_from else "骚火影视"
|
|
info["vod_play_url"] = '$$$'.join(play_url) if play_url else ""
|
|
|
|
return {'list': [info]}
|
|
|
|
# ==================== 搜索 ====================
|
|
def searchContent(self, key, quick, pg="1"):
|
|
page = int(pg) if pg else 1
|
|
encoded_key = urllib.parse.quote(key, safe='')
|
|
url = f"{self.host}/s----------.html?wd={encoded_key}"
|
|
|
|
html = self.fetch(url)
|
|
if not html:
|
|
return {'list': [], 'page': page, 'pagecount': 0, 'limit': 90, 'total': 0}
|
|
|
|
videos = []
|
|
li_pattern = r'<li[^>]*class="clearfix"[^>]*>(.*?)</li>'
|
|
items = re.findall(li_pattern, html, re.DOTALL)
|
|
|
|
for item in items:
|
|
pic = ''
|
|
pic_match = re.search(r'data-original="([^"]+)"', item)
|
|
if pic_match:
|
|
pic = pic_match.group(1)
|
|
|
|
title_match = re.search(r'<a[^>]*href="([^"]+)"[^>]*title="([^"]+)"', item)
|
|
if not title_match:
|
|
continue
|
|
|
|
href = title_match.group(1)
|
|
title = title_match.group(2).strip()
|
|
|
|
remark = ''
|
|
remark_match = re.search(r'<span[^>]*class="pic-text[^"]*"[^>]*>(.*?)</span>', item)
|
|
if remark_match:
|
|
remark = self.clean_html(remark_match.group(1))
|
|
|
|
if href and title:
|
|
if not href.startswith('http'):
|
|
href = self.host + href if href.startswith('/') else self.host + '/' + href
|
|
if pic and not pic.startswith('http'):
|
|
pic = self.host + pic if pic.startswith('/') else self.host + '/' + pic
|
|
if pic and pic.startswith('//'):
|
|
pic = 'https:' + pic
|
|
videos.append({
|
|
'vod_id': href,
|
|
'vod_name': title,
|
|
'vod_pic': pic,
|
|
'vod_remarks': remark
|
|
})
|
|
|
|
return {'list': videos, 'page': page, 'pagecount': 999, 'limit': 90, 'total': 999999}
|
|
|
|
# ==================== 播放 ====================
|
|
def playerContent(self, flag, id, vipFlags):
|
|
"""
|
|
播放接口 - 完全模仿JS版本的play函数
|
|
"""
|
|
if id.startswith('http'):
|
|
final_url = id
|
|
else:
|
|
if id.startswith('/'):
|
|
final_url = self.host.rstrip('/') + id
|
|
else:
|
|
final_url = self.host.rstrip('/') + '/' + id
|
|
|
|
return {"parse": 1, "url": final_url, "jx": 0}
|
|
|
|
def localProxy(self, param):
|
|
return None |