From 31616a21e08772d18955f8fba92203b6a594c111 Mon Sep 17 00:00:00 2001 From: gyjune <14752+gyjune@noreply.example.org> Date: Wed, 5 Aug 2026 06:40:35 +0200 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E3=80=8C/=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 开心影院.py | 521 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 开心影院.py diff --git a/开心影院.py b/开心影院.py new file mode 100644 index 0000000..570ae74 --- /dev/null +++ b/开心影院.py @@ -0,0 +1,521 @@ +# -*- 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 + data = pq(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']*class="[^"]*nav-tabs[^"]*"[^>]*>(.*?)', html, re.DOTALL) + if nav_match: + nav_content = nav_match.group(1) + a_pattern = r']*>(.*?)' + 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']*class="[^"]*tab-pane[^"]*"[^>]*>(.*?)\s*', html, re.DOTALL) + for pane in panes: + items = [] + a_matches = re.findall(r']*href=["\']([^"\']+)["\'][^>]*>(.*?)', 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']*class="[^"]*btn-group[^"]*"[^>]*>(.*?)', html, re.DOTALL) + for group in btn_groups: + items = [] + a_matches = re.findall(r']*href=["\']([^"\']+)["\'][^>]*>(.*?)', 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']*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 \ No newline at end of file