Files
tvbox_collect/recha18/py/py_sefan.py
T
2026-07-11 04:45:19 +00:00

391 line
20 KiB
Python

# -*- coding: utf-8 -*-
# @Author : Adapted for 華視頻
# @Time : 2025/04/05
import sys
import requests
from lxml import etree
import re
import json
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def __init__(self):
self.home_url = 'https://hlove.tv'
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
"Referer": "https://hlove.tv/",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive"
}
self.default_pic = 'https://hlove.tv/api/images/default'
# 配置帶重試的會話
self.session = requests.Session()
retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
self.session.mount('https://', HTTPAdapter(max_retries=retries))
def init(self, extend):
pass
def getName(self):
return "華視界"
def getDependence(self):
return []
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
categories = "电影$movie#电视剧$drama#动漫$animation#综艺$variety#儿童$children"
class_list = [{'type_id': v.split('$')[1], 'type_name': v.split('$')[0]} for v in categories.split('#')]
# 電影篩選條件
movie_classes = "全部$all#剧情$juqing#喜剧$xiju#动作$dongzuo#惊悚$jingsong#爱情$aiqing#恐怖$kongbu#犯罪$fanzui#冒险$maoxian#奇幻$qihuan#悬疑$xuanyi#科幻$kehuan#家庭$jiating#动画$donghua#历史$lishi#战争$zhanzheng#音乐$yinle#动漫$dongman#电视电影$dianshidianying#西部$xibu#网络电影$wangluodianying#纪录$jilu#同性$tongxing#歌舞$gewu#灾难$zainan#动作冒险$dongzuomaoxian#战争政治$zhanzhengzhengzhi"
movie_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others"
movie_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970"
# 電視劇篩選條件
drama_classes = "全部$all#国产剧$guocanju#韩剧$hanju#欧美剧$oumeiju#港台剧$gangtaiju#英剧$yingju#新马泰$xinmata#剧情$juqing#喜剧$xiju#悬疑$xuanyi#犯罪$fanzui#科幻&奇幻$kehuanqihuan#动作冒险$dongzuomaoxian#动作&冒险$dongzuojiemaoxian#家庭$jiating#战争&政治$zhanzhengzhengzhi#爱情$aiqing#肥皂剧$feizaoju#短剧$duanju#同性$tongxing#西部$xibu#儿童$ertong#真人秀$zhenrenxiu#动画$donghua#惊悚$jingsong#脱口秀$tuokouxiu#动作$dongzuo#罪案$zuian#古装$guzhuang#都市$dushi#奇幻$qihuan#科幻$kehuan#历史$lishi#青春$qinchun#新闻$xinwen#穿越$chuanyue#军旅$junlv#歌舞$gewu#玄幻$xuanhuan#纪录$jilu#言情$yanqing#警匪$jingfei#音乐剧$yinleju#商战$shangzhan#武侠$wuxia#电视电影$dianshidianying"
drama_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#其他地区$others"
drama_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970"
# 綜藝篩選條件
variety_classes = "全部$all#真人秀$zhenrenxiu#喜剧$xiju#脱口秀$tuokouxiu#家庭$jiating#剧情$juqing#动作冒险$dongzuomaoxian#悬疑$xuanyi#动作&冒险$dongzuojiemaoxian#犯罪$fanzui#儿童$ertong#晚会$wanhui#音乐$yinle#动画$donghua#纪录$jilu#纪录片$jilupian"
variety_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others"
variety_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970"
# 動漫篩選條件
animation_classes = "全部$all#动画$donghua#喜剧$xiju#科幻&奇幻$kehuanqihuan#动作冒险$dongzuomaoxian#动作&冒险$dongzuojiemaoxian#剧情$juqing#悬疑$xuanyi#家庭$jiating#魔幻$mohuan#热血$rexue#犯罪$fanzui#战争&政治$zhanzhengzhengzhi#冒险$maoxian#剧场版$juchangban#其它$qita#恋爱$lianai#科幻$kehuan#爆笑$baoxiao#儿童$ertong#校园$xiaoyuan#竞技$jingji#少女$shaonv#爱情$aiqing#泡面$paomian#西部$xibu#穿越$chuanyue#格斗$gedou#治愈$zhiyu#机战$jizhan#推理$tuili#耽美$danmei#肥皂剧$feizaoju"
animation_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others"
animation_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970"
# 兒童篩選條件
children_classes = "全部$all#儿童$ertong#动画$donghua#喜剧$xiju#动作冒险$dongzuomaoxian#科幻&奇幻$kehuanqihuan#家庭$jiating#动作&冒险$dongzuojiemaoxian#剧情$juqing#悬疑$xuanyi#犯罪$fanzui#冒险$maoxian#科幻$kehuan#动作$dongzuo#动漫$dongman#历史$lishi#奇幻$qihuan"
children_areas = "全部$all#中国大陆$cn#美国$us#韩国$kr#香港$hk#台湾$tw#日本$jp#英国$gb#泰国$th#西班牙$sp#加拿大$ca#法国$fr#印度$in#澳大利亚$au#其他地区$others"
children_years = "全部$all#2025$2025#2024$2024#2023$2023#2022$2022#2021$2021#2020$2020#2019-2015$2015#2014-2010$2010#2009-2000$2000#90年代$1990#80年代$1980#更早$1970"
filters = {
'movie': [
{'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in movie_classes.split('#')]},
{'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in movie_areas.split('#')]},
{'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in movie_years.split('#')]}
],
'drama': [
{'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in drama_classes.split('#')]},
{'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in drama_areas.split('#')]},
{'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in drama_years.split('#')]}
],
'animation': [
{'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in animation_classes.split('#')]},
{'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in animation_areas.split('#')]},
{'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in animation_years.split('#')]}
],
'variety': [
{'name': '分类', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in variety_classes.split('#')]},
{'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in variety_areas.split('#')]},
{'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in variety_years.split('#')]}
],
'children': [
{'name': '类型', 'key': 'class', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in children_classes.split('#')]},
{'name': '地区', 'key': 'area', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in children_areas.split('#')]},
{'name': '年份', 'key': 'year', 'value': [{'n': v.split('$')[0], 'v': v.split('$')[1]} for v in children_years.split('#')]}
]
}
return {'class': class_list, 'filters': filters}
def homeVideoContent(self):
d = []
try:
# 增加超時時間並使用帶重試的會話
res = self.session.get(self.home_url, headers=self.headers, timeout=20)
res.encoding = 'utf-8'
html_text = res.text
next_data = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_text)
if not next_data:
print("未找到 __NEXT_DATA__")
return {'list': [], 'parse': 0, 'jx': 0}
next_json = json.loads(next_data.group(1))
cards = next_json['props']['pageProps'].get('cards', [])
print(f"找到 {len(cards)} 個分類區塊")
for section in cards:
section_title = section.get('name', '未知分類')
section_cards = section.get('cards', [])
for card in section_cards:
vod_id = card.get('id', '')
vod_name = card.get('name', '')
vod_pic = card.get('img', '')
vod_remarks = card.get('countStr', section_title)
if not vod_id or not vod_name:
continue
vod_path = f"/vod/detail/{vod_id}"
if not vod_pic or vod_pic == '/api/images/init':
vod_pic = self.default_pic
d.append({
'vod_id': vod_path,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': vod_remarks
})
# 簡化去重,直接返回列表
print(f"最終返回 {len(d)} 個影片")
return {'list': d, 'parse': 0, 'jx': 0}
except Exception as e:
print(f"Error in homeVideoContent: {e}")
return {'list': [], 'parse': 0, 'jx': 0}
def infer_category(self, section_title):
category_mapping = {
'電影': 'movie',
'电视剧': 'drama',
'动漫': 'animation',
'综艺': 'variety',
'儿童': 'children'
}
for key, value in category_mapping.items():
if key in section_title:
return value
return 'movie'
def categoryContent(self, cid, page, filter, ext):
_year = ext.get('year', 'all')
_class = ext.get('class', 'all')
_area = ext.get('area', 'all')
url = f"{self.home_url}/{cid}/{_year}/{_class}/{_area}"
if page != '1':
url += f"?page={page}"
d = []
try:
res = self.session.get(url, headers=self.headers, timeout=20)
res.encoding = 'utf-8'
root = etree.HTML(res.text)
data_list = root.xpath('//div[contains(@class, "h-film-listall_cardList___IXsY")]/a')
next_data = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', res.text)
total = 0
init_cards = []
if next_data:
next_json = json.loads(next_data.group(1))
init_cards = next_json['props']['pageProps'].get('initCard', [])
total = next_json['props']['pageProps'].get('total', len(data_list))
for i, card in enumerate(data_list):
vod_name = card.xpath('.//div[contains(@class, "h-film-listall_name__Gyb9x")]/text()')[0].strip()
vod_id = card.get('href', '')
vod_pic_list = card.xpath('.//img[contains(@class, "h-film-listall_img__jiamS")]/@src')
vod_pic = vod_pic_list[0] if vod_pic_list else None
if not vod_pic or vod_pic == '/api/images/init':
vod_pic = init_cards[i]['img'] if i < len(init_cards) and 'img' in init_cards[i] else self.default_pic
vod_remarks = init_cards[i]['countStr'] if i < len(init_cards) and 'countStr' in init_cards[i] else ''
d.append({
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': vod_remarks
})
pagecount = (total + 23) // 24 if total > 0 else 999
return {'list': d, 'page': int(page), 'pagecount': pagecount, 'limit': 24, 'total': total}
except Exception as e:
print(f"Error in categoryContent: {e}")
return {'list': d, 'page': int(page), 'pagecount': 999, 'limit': 24, 'total': 0}
def detailContent(self, did):
ids = did[0]
video_list = []
if not ids.startswith('/vod/detail/'):
ids = f"/vod/detail/{ids.lstrip('/')}"
detail_url = f"{self.home_url}{ids}"
print(f"請求的 detail_url: {detail_url}")
try:
res = self.session.get(detail_url, headers=self.headers, timeout=20)
print(f"HTTP 狀態碼: {res.status_code}")
if res.status_code != 200:
print(f"頁面不存在,URL: {detail_url}")
return {'list': [], 'msg': f'頁面不存在 (狀態碼: {res.status_code})'}
res.encoding = 'utf-8'
next_data = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', res.text)
if not next_data:
print(f"未找到 __NEXT_DATA__,URL: {detail_url}, 響應片段: {res.text[:200]}")
return {'list': [], 'msg': '未找到影片數據'}
next_json = json.loads(next_data.group(1))
page_props = next_json.get('props', {}).get('pageProps', {})
if 'collectionInfo' not in page_props:
print(f"collectionInfo 未找到,URL: {detail_url}, pageProps: {json.dumps(page_props, ensure_ascii=False)}")
return {'list': [], 'msg': '影片數據缺少 collectionInfo'}
collection_info = page_props['collectionInfo']
vod_name = collection_info.get('name', '')
vod_year = collection_info.get('time', '')
vod_area = collection_info.get('country', '')
vod_content = collection_info.get('desc', '')
vod_remarks = collection_info.get('countStr', '')
vod_actor = ', '.join([actor['name'] for actor in collection_info.get('actor', [])])
vod_director = ', '.join([director['name'] for director in collection_info.get('director', [])])
vod_pic = collection_info.get('imgUrl', self.default_pic)
is_movie = collection_info.get('isMovie', False)
play_from = []
play_url = []
for group in collection_info.get('videosGroup', []):
if not group.get('videos'):
continue
line_name = group.get('name', '线路1')
if is_movie:
video = group['videos'][0]
play_from.append(line_name)
play_url.append(f"{vod_name}${video['purl']}")
else:
episodes = []
for video in group['videos']:
ep_name = f"第{video['eporder']}集"
ep_url = video['purl']
episodes.append(f"{ep_name}${ep_url}")
play_from.append(line_name)
play_url.append('#'.join(episodes))
video_list.append({
'vod_id': ids,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': vod_remarks,
'vod_year': vod_year,
'vod_area': vod_area,
'vod_actor': vod_actor,
'vod_director': vod_director,
'vod_content': vod_content,
'vod_play_from': '$$$'.join(play_from),
'vod_play_url': '$$$'.join(play_url)
})
print(f"成功解析影片: {vod_name}, URL: {detail_url}")
return {"list": video_list}
except Exception as e:
print(f"Error in detailContent: {str(e)}, URL: {detail_url}")
return {'list': [], 'msg': f'解析錯誤: {str(e)}'}
def searchContent(self, key, quick):
try:
search_url = f"{self.home_url}/search?q={key}"
res = self.session.get(search_url, headers=self.headers, timeout=20)
res.encoding = 'utf-8'
root = etree.HTML(res.text)
data_list = root.xpath('//div[contains(@class, "h-film-listall_cardList___IXsY")]/a')
result = []
for item in data_list:
vod_name = item.xpath('.//div[contains(@class, "h-film-listall_name__Gyb9x")]/text()')[0].strip()
vod_id = item.get('href', '')
vod_pic = item.xpath('.//img[contains(@class, "h-film-listall_img__jiamS")]/@src')[0] if item.xpath('.//img[contains(@class, "h-film-listall_img__jiamS")]/@src') else self.default_pic
if vod_pic == '/api/images/init':
vod_pic = self.default_pic
result.append({
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_remarks': ''
})
return {'list': result}
except Exception as e:
print(f"Error in searchContent: {e}")
return {'list': []}
def playerContent(self, flag, pid, vipFlags):
try:
play_url = pid
headers = self.headers.copy()
headers['Referer'] = 'https://hlove.tv/' # 添加 Referer 以確保播放鏈接有效
return {
'url': play_url,
'header': json.dumps(headers),
'parse': 0,
'jx': 0
}
except Exception as e:
print(f"Error in playerContent: {e}")
return {'url': '', 'parse': 0, 'jx': 0}
def generate_children_html(self, vod_id):
detail = self.detailContent([vod_id])
if not detail['list']:
return "<h1>無法加載內容</h1>"
vod = detail['list'][0]
vod_name = vod['vod_name']
play_from = vod['vod_play_from'].split('$$$')
play_url = vod['vod_play_url'].split('$$$')
lines = list(zip(play_from, play_url))
sorted_lines = sorted(lines, key=lambda x: x[0] != 'heimuer')
selected_play_url = sorted_lines[0][1].split('#')[0].split('$')[1] if sorted_lines else ''
html = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{vod_name} - 兒童播放</title>
<style>
body {{ font-family: Arial, sans-serif; background-color: #f0f8ff; text-align: center; }}
h1 {{ color: #ff4500; }}
video {{ width: 100%; max-width: 600px; margin: 20px auto; }}
</style>
</head>
<body>
<h1>{vod_name}</h1>
<video controls controlsList="nodownload" oncontextmenu="return false;">
<source src="{selected_play_url}" type="application/x-mpegURL">
您的瀏覽器不支持視頻播放。
</video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
var video = document.querySelector('video');
var videoSrc = '{selected_play_url}';
if (Hls.isSupported()) {{
var hls = new Hls();
hls.loadSource(videoSrc);
hls.attachMedia(video);
}} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{
video.src = videoSrc;
}}
</script>
</body>
</html>
"""
return html
def localProxy(self, params):
pass
def destroy(self):
return '正在Destroy'
if __name__ == "__main__":
spider = Spider()
# 測試主頁
result = spider.homeVideoContent()
print(json.dumps(result, ensure_ascii=False, indent=2))
# 測試詳情頁
result = spider.detailContent(["/vod/detail/se4pnjL1IF6D"])
print(json.dumps(result, ensure_ascii=False, indent=2))