Files
hktk/开心影院.py
2026-08-05 06:46:45 +02:00

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('&nbsp;', '').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('&nbsp;', '').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