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

391 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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))