530 lines
18 KiB
Python
530 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
# 开心影院 - TVbox Python 正式版
|
|
|
|
import re
|
|
import sys
|
|
import json
|
|
import urllib.parse
|
|
from pyquery import PyQuery as pq
|
|
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
|
|
|
|
class Spider(Spider):
|
|
|
|
def init(self, extend=""):
|
|
self.extend = extend
|
|
|
|
def getName(self):
|
|
return "开心影院"
|
|
|
|
def isVideoFormat(self, url):
|
|
return False
|
|
|
|
def manualVideoCheck(self):
|
|
return False
|
|
|
|
def destroy(self):
|
|
pass
|
|
|
|
host = 'https://www.kxyy2.cc'
|
|
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Mobile) '
|
|
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
'Chrome/114.0.0.0 Mobile Safari/537.36',
|
|
'Referer': host,
|
|
}
|
|
|
|
# 分类配置
|
|
classes_config = [
|
|
("电影", "1"),
|
|
("电视剧", "2"),
|
|
("综艺", "3"),
|
|
("动漫", "4"),
|
|
("短剧", "26"),
|
|
("纪录片", "24"),
|
|
]
|
|
|
|
# ==================== 首页 ====================
|
|
def homeContent(self, filter):
|
|
result = {}
|
|
classes = []
|
|
vlist = []
|
|
|
|
for name, tid in self.classes_config:
|
|
classes.append({
|
|
"type_name": name,
|
|
"type_id": tid
|
|
})
|
|
|
|
# ====== 筛选配置 ======
|
|
filters = {}
|
|
|
|
# 电影 (1)
|
|
filters["1"] = [
|
|
{"key": "class", "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": "动画"},
|
|
]},
|
|
{"key": "area", "name": "地区", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "中国大陆", "v": "中国大陆"},
|
|
{"n": "中国香港", "v": "中国香港"},
|
|
{"n": "美国", "v": "美国"},
|
|
{"n": "日本", "v": "日本"},
|
|
{"n": "韩国", "v": "韩国"},
|
|
{"n": "英国", "v": "英国"},
|
|
]},
|
|
{"key": "year", "name": "年份", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "2026", "v": "2026"},
|
|
{"n": "2025", "v": "2025"},
|
|
{"n": "2024", "v": "2024"},
|
|
{"n": "2023", "v": "2023"},
|
|
{"n": "2022", "v": "2022"},
|
|
{"n": "2021", "v": "2021"},
|
|
{"n": "2020", "v": "2020"},
|
|
]},
|
|
]
|
|
|
|
# 电视剧 (2)
|
|
filters["2"] = [
|
|
{"key": "class", "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": "穿越"},
|
|
]},
|
|
{"key": "area", "name": "地区", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "中国大陆", "v": "中国大陆"},
|
|
{"n": "中国香港", "v": "中国香港"},
|
|
{"n": "中国台湾", "v": "中国台湾"},
|
|
{"n": "美国", "v": "美国"},
|
|
{"n": "日本", "v": "日本"},
|
|
{"n": "韩国", "v": "韩国"},
|
|
{"n": "泰国", "v": "泰国"},
|
|
{"n": "英国", "v": "英国"},
|
|
]},
|
|
{"key": "year", "name": "年份", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "2026", "v": "2026"},
|
|
{"n": "2025", "v": "2025"},
|
|
{"n": "2024", "v": "2024"},
|
|
{"n": "2023", "v": "2023"},
|
|
{"n": "2022", "v": "2022"},
|
|
{"n": "2021", "v": "2021"},
|
|
{"n": "2020", "v": "2020"},
|
|
]},
|
|
]
|
|
|
|
# 综艺 (3)
|
|
filters["3"] = [
|
|
{"key": "class", "name": "类型", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "大陆综艺", "v": "大陆综艺"},
|
|
{"n": "港台综艺", "v": "港台综艺"},
|
|
{"n": "日韩综艺", "v": "日韩综艺"},
|
|
{"n": "欧美综艺", "v": "欧美综艺"},
|
|
]},
|
|
{"key": "year", "name": "年份", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "2026", "v": "2026"},
|
|
{"n": "2025", "v": "2025"},
|
|
{"n": "2024", "v": "2024"},
|
|
]},
|
|
]
|
|
|
|
# 动漫 (4)
|
|
filters["4"] = [
|
|
{"key": "class", "name": "类型", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "国产动漫", "v": "国产动漫"},
|
|
{"n": "日本动漫", "v": "日本动漫"},
|
|
{"n": "欧美动漫", "v": "欧美动漫"},
|
|
]},
|
|
{"key": "year", "name": "年份", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "2026", "v": "2026"},
|
|
{"n": "2025", "v": "2025"},
|
|
{"n": "2024", "v": "2024"},
|
|
{"n": "2023", "v": "2023"},
|
|
]},
|
|
]
|
|
|
|
# 短剧 (26)
|
|
filters["26"] = [
|
|
{"key": "year", "name": "年份", "value": [
|
|
{"n": "全部", "v": ""},
|
|
{"n": "2026", "v": "2026"},
|
|
{"n": "2025", "v": "2025"},
|
|
{"n": "2024", "v": "2024"},
|
|
]},
|
|
]
|
|
|
|
result['filters'] = filters
|
|
|
|
# 获取首页
|
|
rsp = self.fetch(self.host, headers=self.headers)
|
|
data = pq(rsp.text)
|
|
|
|
vlist.extend(self.getlist(data('.col-4.rows-md-7 .card-sm')))
|
|
|
|
result['class'] = classes
|
|
result['list'] = vlist[:40]
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
return {}
|
|
|
|
# ==================== 分类 ====================
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
page = int(pg) if pg else 1
|
|
|
|
area = extend.get('area', '') if extend else ''
|
|
|
|
if area:
|
|
url = f'{self.host}/vodshow/{tid}-{area}-------{page}---.html'
|
|
else:
|
|
if page > 1:
|
|
url = f'{self.host}/vodshow/{tid}-----------{page}---.html'
|
|
else:
|
|
url = f'{self.host}/vodshow/{tid}-----------.html'
|
|
|
|
rsp = self.fetch(url, headers=self.headers)
|
|
data = pq(rsp.text)
|
|
|
|
videos = self.getlist(data('.col-lg-8.col-4 .card-sm'))
|
|
if not videos:
|
|
videos = self.getlist(data('.card-sm'))
|
|
|
|
result = {
|
|
'list': videos,
|
|
'page': page,
|
|
'pagecount': 9999,
|
|
'limit': 90,
|
|
'total': 999999
|
|
}
|
|
return result
|
|
|
|
# ==================== 详情 ====================
|
|
def detailContent(self, ids):
|
|
vid = ids[0]
|
|
if not vid.startswith('http'):
|
|
if vid.startswith('/'):
|
|
url = self.host.rstrip('/') + vid
|
|
else:
|
|
url = self.host.rstrip('/') + '/' + vid
|
|
else:
|
|
url = vid
|
|
|
|
rsp = self.fetch(url, headers=self.headers)
|
|
if not rsp:
|
|
return {'list': []}
|
|
|
|
html = rsp.text
|
|
|
|
# ===== 清洗HTML,移除非法字符 =====
|
|
html = html.replace('\x00', '')
|
|
html = re.sub(r'[\x01-\x08\x0b\x0c\x0e-\x1f]', '', html)
|
|
|
|
try:
|
|
data = pq(html, parser='html')
|
|
except:
|
|
html = re.sub(r'[^\x09\x0a\x0d\x20-\x7e\x80-\xff]', '', html)
|
|
data = pq(html, parser='html')
|
|
|
|
# ===== 标题 =====
|
|
name = data('h1').text().strip()
|
|
if not name:
|
|
name = data('h2.d-none').text().strip()
|
|
if not name:
|
|
name = data('h2').text().strip()
|
|
|
|
# ===== 封面 =====
|
|
pic = data('.cover-lg-max-25 img').attr('src') or ''
|
|
if not pic:
|
|
pic = data('.col-md-auto img').attr('src') or ''
|
|
if pic and ('logo' in pic.lower() or 'favicon' in pic.lower()):
|
|
pic = ''
|
|
if pic and not pic.startswith('http'):
|
|
if pic.startswith('/'):
|
|
pic = self.host.rstrip('/') + pic
|
|
else:
|
|
pic = self.host.rstrip('/') + '/' + pic
|
|
|
|
# ===== 简介 =====
|
|
content = data('#synopsis .card-body p').text().strip()
|
|
if not content:
|
|
content = data('.card-body .text-secondary p').text().strip()
|
|
|
|
# ===== 信息提取 =====
|
|
director = ''
|
|
actor = ''
|
|
year = ''
|
|
area = ''
|
|
type_name = ''
|
|
|
|
for info in data('.text-truncate-sm').items():
|
|
text = info.text().strip()
|
|
if '导演' in text:
|
|
directors = [a.text().strip() for a in info('a').items()]
|
|
director = ','.join(directors) if directors else ''
|
|
elif '主演' in text:
|
|
actors = [a.text().strip() for a in info('a').items()]
|
|
actor = ','.join(actors) if actors else ''
|
|
elif '类型' in text:
|
|
types = [a.text().strip() for a in info('a').items()]
|
|
type_name = ','.join(types) if types else ''
|
|
elif '制片国家/地区' in text:
|
|
area_match = re.search(r'\[([^\]]+)\]', text)
|
|
if area_match:
|
|
area = area_match.group(1)
|
|
elif '首播' in text:
|
|
year_match = re.search(r'(\d{4})', text)
|
|
if year_match:
|
|
year = year_match.group(1)
|
|
|
|
if not year:
|
|
year_match = re.search(r'(\d{4})', name)
|
|
if year_match:
|
|
year = year_match.group(1)
|
|
|
|
# ===== 备注 =====
|
|
remarks = data('.text-orange').text().strip()
|
|
if not remarks:
|
|
remarks = data('.badge.bg-pink').text().strip()
|
|
if not remarks:
|
|
remarks = '更新中'
|
|
|
|
vod = {
|
|
'vod_id': vid,
|
|
'vod_name': name,
|
|
'vod_pic': pic,
|
|
'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': '',
|
|
'vod_play_url': ''
|
|
}
|
|
|
|
# ====== 播放列表 ======
|
|
# 线路名称 (正则提取, 最稳定)
|
|
tabs = []
|
|
nav_match = re.search(r'<ul[^>]*class="[^"]*nav-tabs[^"]*"[^>]*>(.*?)</ul>', html, re.DOTALL)
|
|
if nav_match:
|
|
nav_content = nav_match.group(1)
|
|
a_pattern = r'<a[^>]*>(.*?)</a>'
|
|
a_matches = re.findall(a_pattern, nav_content, re.DOTALL)
|
|
for a_match in a_matches:
|
|
text = re.sub(r'<[^>]+>', '', a_match).strip()
|
|
text = text.replace('\xa0', '').replace(' ', '').strip()
|
|
text = re.sub(r'\s*\d+$', '', text).strip()
|
|
if text and '源' in text and text not in tabs:
|
|
tabs.append(text)
|
|
|
|
# 备用: pyquery
|
|
if not tabs:
|
|
for tab in data('.nav-tabs li .nav-link').items():
|
|
tab_name = tab.text().strip()
|
|
tab_name = re.sub(r'\s*\d+$', '', tab_name).strip()
|
|
tab_name = tab_name.replace('\xa0', '').replace(' ', '').strip()
|
|
if tab_name and '源' in tab_name:
|
|
tabs.append(tab_name)
|
|
|
|
# 播放链接
|
|
play_lists = []
|
|
|
|
# 方法1: 从 tab-pane 提取
|
|
panes = re.findall(r'<div[^>]*class="[^"]*tab-pane[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
|
|
for pane in panes:
|
|
items = []
|
|
a_matches = re.findall(r'<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', pane, re.DOTALL)
|
|
for href, title in a_matches:
|
|
title = re.sub(r'<[^>]+>', '', title).strip()
|
|
if title.isdigit():
|
|
title = f'第{title}集'
|
|
if href and not href.startswith('http'):
|
|
if href.startswith('/'):
|
|
href = self.host.rstrip('/') + href
|
|
else:
|
|
href = self.host.rstrip('/') + '/' + href
|
|
if title and href and title not in ['播放', '立即播放']:
|
|
items.append(f'{title}${href}')
|
|
if items:
|
|
play_lists.append('#'.join(items))
|
|
|
|
# 方法2: 从 .btn-group 提取
|
|
if not play_lists:
|
|
btn_groups = re.findall(r'<div[^>]*class="[^"]*btn-group[^"]*"[^>]*>(.*?)</div>', html, re.DOTALL)
|
|
for group in btn_groups:
|
|
items = []
|
|
a_matches = re.findall(r'<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', group, re.DOTALL)
|
|
for href, title in a_matches:
|
|
title = re.sub(r'<[^>]+>', '', title).strip()
|
|
if href and not href.startswith('http'):
|
|
if href.startswith('/'):
|
|
href = self.host.rstrip('/') + href
|
|
else:
|
|
href = self.host.rstrip('/') + '/' + href
|
|
if title and href and title not in ['播放', '立即播放']:
|
|
items.append(f'{title}${href}')
|
|
if items:
|
|
play_lists.append('#'.join(items))
|
|
|
|
vod['vod_play_from'] = '$$$'.join(tabs) if tabs else '开心影院'
|
|
vod['vod_play_url'] = '$$$'.join(play_lists) if play_lists else ''
|
|
|
|
return {'list': [vod]}
|
|
|
|
# ==================== 搜索 ====================
|
|
def searchContent(self, key, quick, pg="1"):
|
|
return {
|
|
'list': [],
|
|
'page': pg,
|
|
'pagecount': 1,
|
|
'limit': 0,
|
|
'total': 0
|
|
}
|
|
|
|
# ==================== 播放 ====================
|
|
def playerContent(self, flag, id, vipFlags):
|
|
if id.startswith('http'):
|
|
play_url = id
|
|
else:
|
|
if id.startswith('/'):
|
|
play_url = self.host.rstrip('/') + id
|
|
else:
|
|
play_url = self.host.rstrip('/') + '/' + id
|
|
|
|
if '.m3u8' in play_url:
|
|
return {'parse': 0, 'url': play_url, 'header': self.headers}
|
|
|
|
rsp = self.fetch(play_url, headers=self.headers)
|
|
if not rsp:
|
|
return {'parse': 1, 'url': play_url, 'header': self.headers}
|
|
|
|
html = rsp.text
|
|
|
|
# 方法1: player_data
|
|
match = re.search(r'var\s+player_data\s*=\s*({[^;]+});', html, re.DOTALL)
|
|
if match:
|
|
try:
|
|
json_str = match.group(1).strip()
|
|
json_str = json_str.replace('\\/', '/')
|
|
json_str = re.sub(r',\s*}', '}', json_str)
|
|
data = json.loads(json_str)
|
|
real = data.get('url', '')
|
|
if real:
|
|
real = real.replace('\\/', '/')
|
|
if real.startswith('//'):
|
|
real = 'https:' + real
|
|
return {'parse': 0, 'url': real, 'header': self.headers}
|
|
except:
|
|
pass
|
|
|
|
# 方法2: 直接匹配 m3u8
|
|
m = re.search(r'"url"\s*:\s*"([^"]+\.m3u8[^"]*)"', html)
|
|
if not m:
|
|
m = re.search(r'https?://[^\s<>"\']+\.m3u8[^\s<>"\']*', html)
|
|
if m:
|
|
real = m.group(1) if m.group(1) else m.group(0)
|
|
real = real.replace('\\/', '/')
|
|
if real.startswith('//'):
|
|
real = 'https:' + real
|
|
return {'parse': 0, 'url': real, 'header': self.headers}
|
|
|
|
# 方法3: iframe
|
|
iframe = re.search(r'<iframe[^>]*src=["\']([^"\']+)["\']', html, re.IGNORECASE)
|
|
if iframe:
|
|
src = iframe.group(1)
|
|
if src.startswith('//'):
|
|
src = 'https:' + src
|
|
elif src.startswith('/'):
|
|
src = self.host.rstrip('/') + src
|
|
if src and src != 'about:blank':
|
|
try:
|
|
rsp2 = self.fetch(src, headers=self.headers)
|
|
if rsp2:
|
|
m2 = re.search(r'https?://[^\s<>"\']+\.m3u8[^\s<>"\']*', rsp2.text)
|
|
if m2:
|
|
return {'parse': 0, 'url': m2.group(0), 'header': self.headers}
|
|
except:
|
|
pass
|
|
|
|
return {'parse': 1, 'url': play_url, 'header': self.headers}
|
|
|
|
def localProxy(self, param):
|
|
return None
|
|
|
|
# ==================== getlist ====================
|
|
def getlist(self, data):
|
|
vlist = []
|
|
for item in data.items():
|
|
name = item('.card-title').text().strip()
|
|
if not name:
|
|
continue
|
|
|
|
pic = item('img').attr('data-src') or item('img').attr('src') or ''
|
|
if pic and not pic.startswith('http'):
|
|
if pic.startswith('/'):
|
|
pic = self.host.rstrip('/') + pic
|
|
else:
|
|
pic = self.host.rstrip('/') + '/' + pic
|
|
|
|
remark = item('.badge.bg-pink').text().strip() or ''
|
|
if not remark:
|
|
remark = item('.badge').text().strip() or ''
|
|
if not remark:
|
|
remark = item('.text-muted').text().strip() or ''
|
|
|
|
href = item('a.cover2').attr('href') or ''
|
|
if not href:
|
|
href = item('a.cover').attr('href') or ''
|
|
if not href:
|
|
href = item('a.d-block').attr('href') or ''
|
|
if not href:
|
|
href = item('a').attr('href') or ''
|
|
if href and not href.startswith('http'):
|
|
if href.startswith('/'):
|
|
href = self.host.rstrip('/') + href
|
|
else:
|
|
href = self.host.rstrip('/') + '/' + href
|
|
|
|
if name:
|
|
vlist.append({
|
|
'vod_id': href,
|
|
'vod_name': name,
|
|
'vod_pic': pic,
|
|
'vod_remarks': remark or '更新中'
|
|
})
|
|
|
|
return vlist |