Add files via upload
This commit is contained in:
+432
@@ -0,0 +1,432 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 4K影视插件 - 优化版本
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "4k影视"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.4kvm.org'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-Ch-Ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
}
|
||||
|
||||
# 公共方法
|
||||
def _normalize_url(self, url):
|
||||
"""标准化URL处理"""
|
||||
if not url:
|
||||
return url
|
||||
if url.startswith('//'):
|
||||
return f"https:{url}"
|
||||
elif url.startswith('/'):
|
||||
return f"{self.host}{url}"
|
||||
return url
|
||||
|
||||
def _extract_video_basic(self, item):
|
||||
"""提取视频基本信息"""
|
||||
try:
|
||||
link = self._normalize_url(item('a').attr('href') or item('h3 a').attr('href') or item('.data h3 a').attr('href'))
|
||||
if not link:
|
||||
return None
|
||||
|
||||
title = (item('h3').text().strip() or item('.data h3').text().strip() or
|
||||
item('img').attr('alt') or item('a').attr('title') or '未知标题')
|
||||
|
||||
img = self._normalize_url(item('img').attr('src') or item('img').attr('data-src'))
|
||||
|
||||
# 简化备注提取
|
||||
remarks = (item('.rating, .imdb, .vote').text().strip() or
|
||||
item('.year, .date, span').text().strip() or
|
||||
item('.type, .genre, .tag').text().strip())
|
||||
|
||||
return {
|
||||
'vod_id': link,
|
||||
'vod_name': title,
|
||||
'vod_pic': img or '',
|
||||
'vod_remarks': remarks,
|
||||
'vod_year': ''
|
||||
}
|
||||
except:
|
||||
return None
|
||||
|
||||
def _get_episode_count(self, season_data, page_html):
|
||||
"""智能检测集数"""
|
||||
# 方法1: 精确容器检测
|
||||
episode_container = season_data('.jujiepisodios')
|
||||
if episode_container:
|
||||
episode_links = episode_container('a')
|
||||
episode_numbers = [int(link.text().strip()) for link in episode_links.items()
|
||||
if link.text().strip().isdigit() and 1 <= int(link.text().strip()) <= 200]
|
||||
if episode_numbers:
|
||||
return max(episode_numbers)
|
||||
|
||||
# 方法2: JavaScript数据提取
|
||||
video_matches = re.findall(r'video.*?=.*?\[(.*?)\]', page_html, re.IGNORECASE | re.DOTALL)
|
||||
for match in video_matches:
|
||||
if '"name":' in match:
|
||||
episode_names = re.findall(r'"name"\s*:\s*(\d+)', match)
|
||||
if len(episode_names) >= 5:
|
||||
episode_numbers = sorted(set(int(name) for name in episode_names))
|
||||
if episode_numbers[0] == 1 and episode_numbers[-1] - episode_numbers[0] == len(episode_numbers) - 1:
|
||||
return max(episode_numbers)
|
||||
|
||||
# 方法3: 文本模式匹配
|
||||
page_text = season_data.text()
|
||||
for pattern in [r'共(\d+)集', r'全(\d+)集', r'更新至(\d+)集', r'第(\d+)集']:
|
||||
matches = re.findall(pattern, page_text)
|
||||
if matches:
|
||||
return max(int(m) for m in matches if m.isdigit())
|
||||
|
||||
# 默认值
|
||||
return 24 if season_data('iframe, video, .player') else 1
|
||||
|
||||
def homeContent(self, filter):
|
||||
try:
|
||||
data = self.getpq(self.fetch(self.host, headers=self.headers).text)
|
||||
classes = []
|
||||
|
||||
# 简化分类提取
|
||||
nav_items = data('header .head-main-nav ul.main-header > li')
|
||||
for k in nav_items.items():
|
||||
main_link = k.children('a').eq(0)
|
||||
link = main_link.attr('href')
|
||||
name = main_link.text().strip()
|
||||
|
||||
if link and name and name not in ['首页', '影片下载']:
|
||||
link = self._normalize_url(link)
|
||||
class_info = {'type_name': name, 'type_id': link}
|
||||
if '电视剧' in name or 'tvshows' in link:
|
||||
class_info['filter_type'] = 'tvshows'
|
||||
classes.append(class_info)
|
||||
|
||||
# 子分类
|
||||
for sub_item in k('ul li').items():
|
||||
sub_link = self._normalize_url(sub_item('a').attr('href'))
|
||||
sub_name = sub_item('a').text().strip()
|
||||
if sub_link and sub_name:
|
||||
sub_class_info = {'type_name': f"{name}-{sub_name}", 'type_id': sub_link}
|
||||
if '电视剧' in name or 'tvshows' in sub_link:
|
||||
sub_class_info['filter_type'] = 'tvshows'
|
||||
classes.append(sub_class_info)
|
||||
|
||||
return {'class': classes, 'list': self.getHomeList(data)}
|
||||
except Exception as e:
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
try:
|
||||
url = tid if pg == '1' else f"{tid}/page/{pg}" if '?' not in tid else f"{tid}&page={pg}"
|
||||
data = self.getpq(self.fetch(url, headers=self.headers).text)
|
||||
|
||||
video_list = self.getVideoList(data)
|
||||
if '电视剧' in url or 'tvshows' in url:
|
||||
video_list = self.filterTVShowsOnly(video_list)
|
||||
|
||||
return {'list': video_list, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
|
||||
except Exception as e:
|
||||
return {'list': [], 'page': int(pg), 'pagecount': 1, 'limit': 30, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
first_id = next(iter(ids)) if hasattr(ids, '__iter__') and not isinstance(ids, str) else ids
|
||||
data = self.getpq(self.fetch(first_id, headers=self.headers).text)
|
||||
|
||||
# 基本信息提取
|
||||
vod = {
|
||||
'vod_id': first_id,
|
||||
'vod_name': data('.sheader h1, h1').text().strip(),
|
||||
'vod_pic': self._normalize_url(data('.sheader .poster img, .poster img').attr('src')),
|
||||
'vod_content': data('.sbox .wp-content, #info .wp-content').text().strip(),
|
||||
'vod_year': '', 'vod_area': '', 'vod_remarks': '', 'vod_actor': '', 'vod_director': ''
|
||||
}
|
||||
|
||||
# 提取分类
|
||||
genres = data('.sgeneros a')
|
||||
if genres:
|
||||
vod['type_name'] = ', '.join(g.text() for g in genres.items())
|
||||
|
||||
# 播放链接处理
|
||||
play_options = data('#playeroptions ul li, .dooplay_player_option')
|
||||
if play_options:
|
||||
play_links = self._extract_play_options(play_options, first_id)
|
||||
else:
|
||||
season_links = data('.seasons-list a, .season-item a, .se-c a, .se-a a, .seasons a')
|
||||
play_links = self.getSeasonEpisodes(season_links) if season_links else [f"播放${first_id}"]
|
||||
|
||||
vod['vod_play_from'] = '老僧酿酒'
|
||||
vod['vod_play_url'] = '#'.join(play_links)
|
||||
|
||||
return {'list': [vod]}
|
||||
except Exception as e:
|
||||
return {'list': []}
|
||||
|
||||
def _extract_play_options(self, play_options, first_id):
|
||||
"""提取播放选项"""
|
||||
play_links = []
|
||||
for option in play_options.items():
|
||||
title = option('.title, span.title').text().strip() or '播放'
|
||||
server = option('.server, span.server').text().strip()
|
||||
if server:
|
||||
title = f"{title}-{server}"
|
||||
|
||||
data_post = option.attr('data-post')
|
||||
data_nume = option.attr('data-nume')
|
||||
data_type = option.attr('data-type')
|
||||
|
||||
if data_post and data_nume:
|
||||
play_url = f"{first_id}?post={data_post}&nume={data_nume}&type={data_type}"
|
||||
play_links.append(f"{title}${play_url}")
|
||||
|
||||
return play_links
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
search_url = f"{self.host}/xssearch?s={key}"
|
||||
if pg != "1":
|
||||
search_url += f"&p={pg}"
|
||||
|
||||
data = self.getpq(self.fetch(search_url, headers=self.headers).text)
|
||||
raw_results = self.getVideoList(data)
|
||||
filtered_results = self.filterSearchResults(raw_results, key)
|
||||
|
||||
return {'list': filtered_results, 'page': int(pg)}
|
||||
except Exception as e:
|
||||
return {'list': [], 'page': int(pg)}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
# 解析播放参数
|
||||
data_post = data_nume = data_type = None
|
||||
if '?' in id:
|
||||
base_url, params = id.split('?', 1)
|
||||
param_dict = dict(param.split('=', 1) for param in params.split('&') if '=' in param)
|
||||
data_post = param_dict.get('post')
|
||||
data_nume = param_dict.get('nume')
|
||||
data_type = param_dict.get('type')
|
||||
|
||||
# API调用
|
||||
if data_post and data_nume:
|
||||
try:
|
||||
api_url = f"{self.host}/wp-json/dooplayer/v1/post/{data_post}"
|
||||
api_response = self.fetch(api_url, headers=self.headers,
|
||||
params={'type': data_type or 'movie', 'source': data_nume})
|
||||
if api_response.status_code == 200:
|
||||
api_data = api_response.json()
|
||||
if 'embed_url' in api_data:
|
||||
embed_url = api_data['embed_url']
|
||||
parse_flag = 0 if any(ext in embed_url.lower() for ext in ['.m3u8', '.mp4', '.flv', '.avi']) else 1
|
||||
return {'parse': parse_flag, 'url': embed_url, 'header': self.headers}
|
||||
except:
|
||||
pass
|
||||
|
||||
# 页面解析回退
|
||||
page_url = base_url if '?' in id else id
|
||||
data = self.getpq(self.fetch(page_url, headers=self.headers).text)
|
||||
|
||||
# 查找播放源
|
||||
iframe = data('iframe.metaframe, .dooplay_player iframe, .player iframe').attr('src')
|
||||
if iframe:
|
||||
iframe = self._normalize_url(iframe)
|
||||
parse_flag = 0 if any(ext in iframe.lower() for ext in ['.m3u8', '.mp4', '.flv']) else 1
|
||||
return {'parse': parse_flag, 'url': iframe, 'header': self.headers}
|
||||
|
||||
video_src = self._normalize_url(data('video source, video').attr('src'))
|
||||
if video_src:
|
||||
return {'parse': 0, 'url': video_src, 'header': self.headers}
|
||||
|
||||
return {'parse': 1, 'url': page_url, 'header': self.headers}
|
||||
|
||||
except Exception as e:
|
||||
return {'parse': 1, 'url': id, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def getHomeList(self, data):
|
||||
"""获取首页推荐列表"""
|
||||
videos = []
|
||||
items = data('article, .module .content .items .item, .movies-list article')
|
||||
for item in items.items():
|
||||
video_info = self._extract_video_basic(item)
|
||||
if video_info:
|
||||
videos.append(video_info)
|
||||
return videos
|
||||
|
||||
def getVideoList(self, data):
|
||||
"""获取视频列表"""
|
||||
videos = []
|
||||
items = data('article, .items article, .content article, .search-results article')
|
||||
for item in items.items():
|
||||
video_info = self._extract_video_basic(item)
|
||||
if video_info:
|
||||
videos.append(video_info)
|
||||
return videos
|
||||
|
||||
def extractVideoInfo(self, item):
|
||||
"""兼容性方法,已优化为_extract_video_basic"""
|
||||
return self._extract_video_basic(item)
|
||||
|
||||
def getpq(self, text):
|
||||
"""创建PyQuery对象"""
|
||||
try:
|
||||
return pq(text)
|
||||
except:
|
||||
try:
|
||||
return pq(text.encode('utf-8'))
|
||||
except:
|
||||
return pq('')
|
||||
|
||||
def filterSearchResults(self, results, search_key):
|
||||
"""过滤和排序搜索结果"""
|
||||
if not results or not search_key:
|
||||
return results
|
||||
|
||||
search_key_lower = search_key.lower().strip()
|
||||
search_words = search_key_lower.split()
|
||||
scored_results = []
|
||||
|
||||
for result in results:
|
||||
title = result.get('vod_name', '').lower()
|
||||
|
||||
# 计算相关性分数
|
||||
if search_key_lower == title:
|
||||
score = 100
|
||||
elif search_key_lower in title:
|
||||
score = 80
|
||||
elif title.startswith(search_key_lower):
|
||||
score = 70
|
||||
elif all(word in title for word in search_words):
|
||||
score = 60
|
||||
else:
|
||||
word_matches = sum(1 for word in search_words if word in title)
|
||||
if word_matches > 0:
|
||||
score = 30 + (word_matches * 10)
|
||||
else:
|
||||
continue
|
||||
|
||||
# 内容类型加分
|
||||
if '剧' in search_key_lower and 'tvshows' in result.get('vod_id', ''):
|
||||
score += 5
|
||||
elif '电影' in search_key_lower and 'movies' in result.get('vod_id', ''):
|
||||
score += 5
|
||||
|
||||
scored_results.append((score, result))
|
||||
|
||||
# 排序和过滤
|
||||
scored_results.sort(key=lambda x: x[0], reverse=True)
|
||||
min_score = 30 if len(search_words) > 1 else 40
|
||||
filtered = [result for score, result in scored_results if score >= min_score]
|
||||
|
||||
# 如果结果太少,放宽标准
|
||||
if len(filtered) < 3 and len(scored_results) > 3:
|
||||
filtered = [result for score, result in scored_results[:10]]
|
||||
|
||||
return filtered
|
||||
|
||||
def filterTVShowsOnly(self, video_list):
|
||||
"""过滤电视剧分类中的电影内容"""
|
||||
if not video_list:
|
||||
return video_list
|
||||
|
||||
filtered_videos = []
|
||||
movie_keywords = ['/movies/', '/movie/', 'Movie', '电影']
|
||||
tvshow_keywords = ['/tvshows/', '/tvshow/', '/seasons/', 'TV', '剧', '季', '集']
|
||||
|
||||
for video in video_list:
|
||||
vod_id = video.get('vod_id', '')
|
||||
vod_name = video.get('vod_name', '')
|
||||
vod_remarks = video.get('vod_remarks', '')
|
||||
|
||||
# 检查是否是电影
|
||||
is_movie = any(keyword in vod_id for keyword in movie_keywords[:3])
|
||||
if is_movie:
|
||||
continue
|
||||
|
||||
# 检查是否是电视剧
|
||||
is_tvshow = (any(keyword in vod_id for keyword in tvshow_keywords[:3]) or
|
||||
any(keyword in vod_name + vod_remarks for keyword in tvshow_keywords[3:]))
|
||||
|
||||
if is_tvshow or not is_movie:
|
||||
filtered_videos.append(video)
|
||||
|
||||
return filtered_videos
|
||||
|
||||
def getSeasonEpisodes(self, season_links):
|
||||
"""获取电视剧每个季的集数信息"""
|
||||
play_links = []
|
||||
|
||||
try:
|
||||
for season in season_links.items():
|
||||
season_title = season.text().strip() or '第1季'
|
||||
season_url = self._normalize_url(season.attr('href'))
|
||||
|
||||
if not season_url:
|
||||
continue
|
||||
|
||||
try:
|
||||
season_resp = self.fetch(season_url, headers=self.headers)
|
||||
if season_resp.status_code == 200:
|
||||
season_data = self.getpq(season_resp.text)
|
||||
episode_count = self._get_episode_count(season_data, season_resp.text)
|
||||
|
||||
# 限制集数范围(提高上限以支持长篇动漫)
|
||||
episode_count = min(max(episode_count, 1), 500)
|
||||
|
||||
# 生成播放链接
|
||||
if episode_count == 1:
|
||||
play_links.append(f"{season_title}${season_url}")
|
||||
else:
|
||||
clean_title = season_title.split('已完結')[0].split('更新')[0].strip()
|
||||
for ep_num in range(1, episode_count + 1):
|
||||
episode_title = f"{clean_title} 第{ep_num}集"
|
||||
episode_url = f"{season_url}?ep={ep_num}"
|
||||
play_links.append(f"{episode_title}${episode_url}")
|
||||
else:
|
||||
play_links.append(f"{season_title}${season_url}")
|
||||
|
||||
except Exception:
|
||||
play_links.append(f"{season_title}${season_url}")
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return play_links
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-fetch-dest': 'document',
|
||||
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
}
|
||||
|
||||
host = "https://4k-av.com"
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq()
|
||||
result = {}
|
||||
classes = []
|
||||
for k in list(data('#category ul li').items())[:-1]:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': k('a').attr('href')
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list'] = self.getlist(data('#MainContent_scrollul ul li'),'.poster span')
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=self.getpq(f"{tid}page-{pg}.html")
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('#MainContent_newestlist .virow .NTMitem'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data=self.getpq(ids[0])
|
||||
v=data('#videoinfo')
|
||||
vod = {
|
||||
'type_name': v('#MainContent_tags.tags a').text(),
|
||||
'vod_year': v('#MainContent_videodetail.videodetail a').text(),
|
||||
'vod_remarks': v('#MainContent_titleh12 h2').text(),
|
||||
'vod_content': v('p.cnline').text(),
|
||||
'vod_play_from': '4KAV',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
vlist=data('#rtlist li')
|
||||
if vlist:
|
||||
c=[f"{i('span').text()}${i('a').attr('href')}" for i in list(vlist.items())[1:]]
|
||||
c.insert(0,f"{vlist.eq(0)('span').text()}${ids[0]}")
|
||||
vod['vod_play_url'] = '#'.join(c)
|
||||
else:
|
||||
vod['vod_play_url'] = f"{data('#tophead h1').text()}${ids[0]}"
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.getpq(f"/s?k={key}")
|
||||
return {'list':self.getlist(data('#MainContent_newestlist .virow.search .NTMitem.Main'))}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
data=self.getpq(id)
|
||||
p,url=0,data('#MainContent_videowindow source').attr('src')
|
||||
if not url:raise Exception("未找到播放地址")
|
||||
except Exception as e:
|
||||
p,url=1,f"{self.host}{id}"
|
||||
headers = {
|
||||
'origin': self.host,
|
||||
'referer': f'{self.host}/',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
}
|
||||
return {'parse': p, 'url': url, 'header': headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def getlist(self,data,y='.resyear label[title="分辨率"]'):
|
||||
videos = []
|
||||
for i in data.items():
|
||||
ns = i('.title h2').text().split(' ')
|
||||
videos.append({
|
||||
'vod_id': i('.title a').attr('href'),
|
||||
'vod_name': ns[0],
|
||||
'vod_pic': i('.poster img').attr('src'),
|
||||
'vod_remarks': ns[-1] if len(ns) > 1 else '',
|
||||
'vod_year': i(y).text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, path=''):
|
||||
url=f"{self.host}{path}"
|
||||
data=self.fetch(url,headers=self.headers).text
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import time
|
||||
import uuid
|
||||
from base64 import b64decode, b64encode
|
||||
import json
|
||||
import sys
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import MD5
|
||||
from Crypto.Util.Padding import unpad, pad
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = "https://api.230110.xyz"
|
||||
|
||||
phost = "https://cdn.230110.xyz"
|
||||
|
||||
headers = {
|
||||
'origin': host,
|
||||
'referer': f'{host}/',
|
||||
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data='9XSPkyFMrOOG34JSg//ZosMof45cyBo9hwZMZ5rvI6Yz/ZZlXWIf8/644OzwW+FNIOdJ61R/Lxjy1tqN+ZzokxtiVzb8LjYAkh6GFudwAUXFt9yS1ZjAxC3tDKrQsJQLk3nym0s00DBBzLBntRBDFz7nbba+OOBuQOZpL3CESGL42l4opdoViQLhO/dIizY1kIOk2NxxpDC9Z751gPl1ctHWuLWhuLG/QWgNWi/iHScjKrMHJKcC9GQHst/4Q3dgZ03eQIIVB6jvoV1XXoBCz6fjM/jM3BXpzSttT4Stglwy93gWuNWuZiKypHK2Q0lO10oM0ceRW2a0fPGId+rNYMRO3cR/C0ZueD4cmTAVOuxVr9ZZSP8/nhD0bHyAPONXtchIDJb0O/kdFHk2KTJfQ5q4fHOyzezczc4iQDV/R0S8cGZKM14MF+wytA/iljfj43H0UYqq5pM+MCUGRTdYEtuxCp0+A+DiOhNZwY/Km/TgBoGZQWGbpljJ2LAVnWhxX+ickLH7zuR/FeIwP/R8zOuR+8C8UlT9eHTqtvfNzaGdFxt316atHy8TNjRO7J5a177mqsHs3ziG0toDDzLDCbhRUjFgVA3ktahhXiWaaCo/ZGSJAA8TDO5DYqnJ0JDaX0ILPj8QB5zxrHYmRE8PboIr3RBAjz1sREbaHfjrUjoh29ePhlolLV00EvgoxP5knaqt5Ws/sq5IG57qKCAPgqXzblPLHToJGBtukKhLp8jbGJrkb6PVn4/jysks0NGE'
|
||||
return {'class':self.aes(data,False)}
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data = {"q": "", "filter": [f"type_id = {tid}"], "offset": (int(pg)-1) * 24, "limit": 24, "sort": ["video_time:desc"],"lang": "zh-cn", "route": "/videos/search"}
|
||||
result = {}
|
||||
if 'skey_' in tid:return self.searchContent(tid.split('_')[-1], True, pg)
|
||||
result['list'] = self.getl(self.getdata(data))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data={"limit":1,"filter":[f"video_id = {ids[0]}"],"lang":"zh-cn","route":"/videos/search"}
|
||||
res = self.getdata(data)[0]
|
||||
purl=urlunparse(urlparse(self.phost)._replace(path=urlparse(res.get('video_url')).path))
|
||||
vod = {
|
||||
'vod_play_from': 'dsysav',
|
||||
'vod_play_url': f"{res.get('video_duration')}${purl}"
|
||||
}
|
||||
if res.get('video_tag'):
|
||||
clist = []
|
||||
tags=res['video_tag'].split(',')
|
||||
for k in tags:
|
||||
clist.append('[a=cr:' + json.dumps({'id': f'skey_{k}', 'name': k}) + '/]' + k + '[/a]')
|
||||
vod['vod_content'] = ' '.join(clist)
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data={"q":key,"filter":[],"offset":(int(pg)-1) * 24,"limit":24,"sort":["video_time:desc"],"lang":"zh-cn","route":"/videos/search"}
|
||||
return {'list':self.getl(self.getdata(data)),'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if id.endswith('.mpd'):
|
||||
id=f"{self.getProxyUrl()}&url={self.e64(id)}&type=mpd"
|
||||
return {'parse': 0, 'url': id, 'header':self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
if param.get('type') and param['type']=='mpd':
|
||||
url = self.d64(param.get('url'))
|
||||
ids=url.split('/')
|
||||
id=f"{ids[-3]}/{ids[-2]}/"
|
||||
xpu = f"{self.getProxyUrl()}&path=".replace('&', '&')
|
||||
data = self.fetch(url, headers=self.headers).text
|
||||
data = data.replace('initialization="', f'initialization="{xpu}{id}').replace('media="',f'media="{xpu}{id}')
|
||||
return [200,'application/octet-stream',data]
|
||||
else:
|
||||
hsign=self.md5(f"AjPuom638LmWfWyeM5YueKuJ9PuWLdRn/mpd/{param.get('path')}1767196800")
|
||||
bytes_data = bytes.fromhex(hsign)
|
||||
sign = b64encode(bytes_data).decode('utf-8').replace('=','').replace('+','-').replace('/','_')
|
||||
url=f"{self.phost}/mpd/{param.get('path')}?sign={sign}&expire=1767196800"
|
||||
return [302,'text/plain',None,{'Location':url}]
|
||||
|
||||
def liveContent(self, url):
|
||||
pass
|
||||
|
||||
def aes(self, text, operation=True):
|
||||
key = b'OPQT123412FRANME'
|
||||
iv = b'MRDCQP12QPM13412'
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
if operation:
|
||||
ct_bytes = cipher.encrypt(pad(json.dumps(text).encode("utf-8"), AES.block_size))
|
||||
ct = b64encode(ct_bytes).decode("utf-8")
|
||||
return ct
|
||||
else:
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
return json.loads(pt.decode("utf-8"))
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self,encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def getl(self,data):
|
||||
videos = []
|
||||
for i in data:
|
||||
img = i.get('video_cover')
|
||||
if img and 'http' in img:img = urlunparse(urlparse(self.phost)._replace(path=urlparse(img).path))
|
||||
videos.append({
|
||||
'vod_id': i.get('video_id'),
|
||||
'vod_name': i.get('video_title'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i.get('video_duration'),
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
return videos
|
||||
|
||||
def getdata(self,data):
|
||||
uid = str(uuid.uuid4())
|
||||
t = int(time.time())
|
||||
json_data = {
|
||||
'sign': self.md5(f"{self.e64(json.dumps(data))}{uid}{t}AjPuom638LmWfWyeM5YueKuJ9PuWLdRn"),
|
||||
'nonce': uid,
|
||||
'timestamp': t,
|
||||
'data': self.aes(data),
|
||||
}
|
||||
res = self.post(f"{self.host}/v1", json=json_data, headers=self.headers).json()
|
||||
res = self.aes(res['data'], False)
|
||||
return res
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
|
||||
sys.path.append('../../')
|
||||
try:
|
||||
from base.spider import Spider
|
||||
except ImportError:
|
||||
# 定义一个基础接口类,用于本地测试
|
||||
class Spider:
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.siteUrl = "https://www.kuaikaw.cn"
|
||||
self.cateManual = {
|
||||
"甜宠": "462",
|
||||
"古装仙侠": "1102",
|
||||
"现代言情": "1145",
|
||||
"青春": "1170",
|
||||
"豪门恩怨": "585",
|
||||
"逆袭": "417-464",
|
||||
"重生": "439-465",
|
||||
"系统": "1159",
|
||||
"总裁": "1147",
|
||||
"职场商战": "943"
|
||||
}
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
|
||||
"Referer": self.siteUrl,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
|
||||
}
|
||||
|
||||
def getName(self):
|
||||
return "河马短剧"
|
||||
|
||||
def init(self, extend=""):
|
||||
return
|
||||
|
||||
def fetch(self, url, headers=None, retry=2):
|
||||
"""统一的网络请求接口"""
|
||||
if headers is None:
|
||||
headers = self.headers
|
||||
|
||||
for i in range(retry + 1):
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except Exception as e:
|
||||
if i == retry:
|
||||
print(f"请求异常: {url}, 错误: {str(e)}")
|
||||
return None
|
||||
continue
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
video_formats = ['.mp4', '.mkv', '.avi', '.wmv', '.m3u8', '.flv', '.rmvb']
|
||||
return any(format in url.lower() for format in video_formats)
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in self.cateManual.items()]
|
||||
result['class'] = classes
|
||||
|
||||
try:
|
||||
result['list'] = self.homeVideoContent()['list']
|
||||
except:
|
||||
result['list'] = []
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
response = self.fetch(self.siteUrl)
|
||||
if not response:
|
||||
return {'list': []}
|
||||
|
||||
html_content = response.text
|
||||
next_data_pattern = r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>'
|
||||
next_data_match = re.search(next_data_pattern, html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return {'list': []}
|
||||
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
# 处理轮播图数据
|
||||
if "bannerList" in page_props:
|
||||
for banner in page_props["bannerList"]:
|
||||
if banner.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{banner['bookId']}",
|
||||
"vod_name": banner.get("bookName", ""),
|
||||
"vod_pic": banner.get("coverWap", ""),
|
||||
"vod_remarks": f"{banner.get('statusDesc', '')} {banner.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
# 处理SEO分类推荐
|
||||
if "seoColumnVos" in page_props:
|
||||
for column in page_props["seoColumnVos"]:
|
||||
for book in column.get("bookInfos", []):
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
# 去重处理
|
||||
seen = set()
|
||||
unique_videos = []
|
||||
for video in videos:
|
||||
key = (video["vod_id"], video["vod_name"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_videos.append(video)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取首页推荐内容出错: {e}")
|
||||
unique_videos = []
|
||||
|
||||
return {'list': unique_videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
url = f"{self.siteUrl}/browse/{tid}/{pg}"
|
||||
|
||||
response = self.fetch(url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html_content = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
current_page = page_props.get("page", 1)
|
||||
total_pages = page_props.get("pages", 1)
|
||||
book_list = page_props.get("bookList", [])
|
||||
|
||||
videos = []
|
||||
for book in book_list:
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
result.update({
|
||||
'list': videos,
|
||||
'page': int(current_page),
|
||||
'pagecount': total_pages,
|
||||
'limit': len(videos),
|
||||
'total': len(videos) * total_pages if videos else 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"分类内容获取出错: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
return self.searchContentPage(key, quick, pg)
|
||||
|
||||
def searchContentPage(self, key, quick, pg=1):
|
||||
result = {'list': [], 'page': pg, 'pagecount': 1, 'limit': 20, 'total': 0}
|
||||
search_url = f"{self.siteUrl}/search?searchValue={quote(key)}&page={pg}"
|
||||
|
||||
response = self.fetch(search_url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html_content = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html_content, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data_json = json.loads(next_data_match.group(1))
|
||||
page_props = next_data_json.get("props", {}).get("pageProps", {})
|
||||
|
||||
total_pages = page_props.get("pages", 1)
|
||||
book_list = page_props.get("bookList", [])
|
||||
|
||||
videos = []
|
||||
for book in book_list:
|
||||
if book.get("bookId"):
|
||||
videos.append({
|
||||
"vod_id": f"/drama/{book['bookId']}",
|
||||
"vod_name": book.get("bookName", ""),
|
||||
"vod_pic": book.get("coverWap", ""),
|
||||
"vod_remarks": f"{book.get('statusDesc', '')} {book.get('totalChapterNum', '')}集".strip()
|
||||
})
|
||||
|
||||
result.update({
|
||||
'list': videos,
|
||||
'pagecount': total_pages,
|
||||
'total': len(videos) * total_pages if videos else 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索内容出错: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': []}
|
||||
if not ids:
|
||||
return result
|
||||
|
||||
vod_id = ids[0]
|
||||
if not vod_id.startswith('/drama/'):
|
||||
vod_id = f'/drama/{vod_id}'
|
||||
|
||||
drama_url = f"{self.siteUrl}{vod_id}"
|
||||
response = self.fetch(drama_url)
|
||||
if not response:
|
||||
return result
|
||||
|
||||
html = response.text
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.DOTALL)
|
||||
if not next_data_match:
|
||||
return result
|
||||
|
||||
try:
|
||||
next_data = json.loads(next_data_match.group(1))
|
||||
page_props = next_data.get("props", {}).get("pageProps", {})
|
||||
book_info = page_props.get("bookInfoVo", {})
|
||||
chapter_list = page_props.get("chapterList", [])
|
||||
|
||||
if not book_info.get("bookId"):
|
||||
return result
|
||||
|
||||
# 基本信息
|
||||
categories = [c.get("name", "") for c in book_info.get("categoryList", [])]
|
||||
performers = [p.get("name", "") for p in book_info.get("performerList", [])]
|
||||
|
||||
vod = {
|
||||
"vod_id": vod_id,
|
||||
"vod_name": book_info.get("title", ""),
|
||||
"vod_pic": book_info.get("coverWap", ""),
|
||||
"type_name": ",".join(categories),
|
||||
"vod_year": "",
|
||||
"vod_area": book_info.get("countryName", ""),
|
||||
"vod_remarks": f"{book_info.get('statusDesc', '')} {book_info.get('totalChapterNum', '')}集".strip(),
|
||||
"vod_actor": ", ".join(performers),
|
||||
"vod_director": "",
|
||||
"vod_content": book_info.get("introduction", "")
|
||||
}
|
||||
|
||||
# 处理剧集
|
||||
play_urls = self.processEpisodes(vod_id, chapter_list)
|
||||
if play_urls:
|
||||
vod['vod_play_from'] = '河马剧场'
|
||||
vod['vod_play_url'] = '$$$'.join(play_urls)
|
||||
|
||||
result['list'] = [vod]
|
||||
|
||||
except Exception as e:
|
||||
print(f"详情页解析出错: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
return result
|
||||
|
||||
def processEpisodes(self, vod_id, chapter_list):
|
||||
play_urls = []
|
||||
episodes = []
|
||||
|
||||
for chapter in chapter_list:
|
||||
chapter_id = chapter.get("chapterId", "")
|
||||
chapter_name = chapter.get("chapterName", "")
|
||||
|
||||
if not chapter_id or not chapter_name:
|
||||
continue
|
||||
|
||||
# 尝试获取直接视频链接
|
||||
video_url = self.getDirectVideoUrl(chapter)
|
||||
if video_url:
|
||||
episodes.append(f"{chapter_name}${video_url}")
|
||||
continue
|
||||
|
||||
# 回退方案
|
||||
episodes.append(f"{chapter_name}${vod_id}${chapter_id}${chapter_name}")
|
||||
|
||||
if episodes:
|
||||
play_urls.append("#".join(episodes))
|
||||
|
||||
return play_urls
|
||||
|
||||
def getDirectVideoUrl(self, chapter):
|
||||
if "chapterVideoVo" not in chapter or not chapter["chapterVideoVo"]:
|
||||
return None
|
||||
|
||||
video_info = chapter["chapterVideoVo"]
|
||||
for key in ["mp4", "mp4720p", "vodMp4Url"]:
|
||||
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
|
||||
return video_info[key]
|
||||
return None
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {
|
||||
"parse": 0,
|
||||
"url": id,
|
||||
"header": json.dumps(self.headers)
|
||||
}
|
||||
|
||||
# 如果已经是视频链接直接返回
|
||||
if 'http' in id and ('.mp4' in id or '.m3u8' in id):
|
||||
return result
|
||||
|
||||
# 解析参数
|
||||
parts = id.split('$')
|
||||
if len(parts) < 2:
|
||||
return result
|
||||
|
||||
drama_id = parts[0].replace('/drama/', '')
|
||||
chapter_id = parts[1]
|
||||
|
||||
# 尝试获取视频链接
|
||||
video_url = self.getEpisodeVideoUrl(drama_id, chapter_id)
|
||||
if video_url:
|
||||
result["url"] = video_url
|
||||
|
||||
return result
|
||||
|
||||
def getEpisodeVideoUrl(self, drama_id, chapter_id):
|
||||
episode_url = f"{self.siteUrl}/episode/{drama_id}/{chapter_id}"
|
||||
response = self.fetch(episode_url)
|
||||
if not response:
|
||||
return None
|
||||
|
||||
html = response.text
|
||||
|
||||
# 方法1: 从NEXT_DATA提取
|
||||
next_data_match = re.search(r'<script id="__NEXT_DATA__".*?>(.*?)</script>', html, re.DOTALL)
|
||||
if next_data_match:
|
||||
try:
|
||||
next_data = json.loads(next_data_match.group(1))
|
||||
page_props = next_data.get("props", {}).get("pageProps", {})
|
||||
chapter_info = page_props.get("chapterInfo", {})
|
||||
|
||||
if chapter_info and "chapterVideoVo" in chapter_info:
|
||||
video_info = chapter_info["chapterVideoVo"]
|
||||
for key in ["mp4", "mp4720p", "vodMp4Url"]:
|
||||
if key in video_info and video_info[key] and ".mp4" in video_info[key].lower():
|
||||
return video_info[key]
|
||||
except:
|
||||
pass
|
||||
|
||||
# 方法2: 直接从HTML提取
|
||||
mp4_matches = re.findall(r'(https?://[^"\']+\.mp4)', html)
|
||||
if mp4_matches:
|
||||
for url in mp4_matches:
|
||||
if chapter_id in url or drama_id in url:
|
||||
return url
|
||||
return mp4_matches[0]
|
||||
|
||||
return None
|
||||
|
||||
def localProxy(self, param):
|
||||
return [200, "video/MP2T", {}, param]
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
@@ -0,0 +1,499 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import re
|
||||
try:
|
||||
import ujson
|
||||
except ImportError:
|
||||
ujson = json
|
||||
try:
|
||||
from pyquery import PyQuery as pq
|
||||
except ImportError:
|
||||
pq = None
|
||||
try:
|
||||
from cachetools import TTLCache
|
||||
except ImportError:
|
||||
class TTLCache:
|
||||
def __init__(self, maxsize=100, ttl=600):
|
||||
self.cache = {}
|
||||
self.maxsize = maxsize
|
||||
def __contains__(self, key):
|
||||
return key in self.cache
|
||||
def __getitem__(self, key):
|
||||
return self.cache[key]
|
||||
def __setitem__(self, key, value):
|
||||
if len(self.cache) >= self.maxsize:
|
||||
first_key = next(iter(self.cache))
|
||||
del self.cache[first_key]
|
||||
self.cache[key] = value
|
||||
def __len__(self):
|
||||
return len(self.cache)
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.cache = TTLCache(maxsize=100, ttl=600)
|
||||
def getName(self):
|
||||
return "Libvio"
|
||||
def init(self, extend=""):
|
||||
print("============{0}============".format(extend))
|
||||
if not hasattr(self, 'cache'):
|
||||
self.cache = TTLCache(maxsize=100, ttl=600)
|
||||
pass
|
||||
def _fetch_with_cache(self, url, headers=None):
|
||||
cache_key = f"{url}_{hash(str(headers))}"
|
||||
if cache_key in self.cache:
|
||||
return self.cache[cache_key]
|
||||
try:
|
||||
response = self.fetch(url, headers=headers or self.header)
|
||||
except Exception as e:
|
||||
print(f"Fetch failed for {url}: {e}")
|
||||
response = None # Fallback to None on error
|
||||
if response:
|
||||
self.cache[cache_key] = response
|
||||
return response
|
||||
def _parse_html_fast(self, html_text):
|
||||
if not html_text:
|
||||
return None
|
||||
if pq is not None:
|
||||
try:
|
||||
return pq(html_text)
|
||||
except:
|
||||
pass
|
||||
return self.html(self.cleanText(html_text))
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {"电影": "1", "电视剧": "2", "动漫": "4", "日韩剧": "15", "欧美剧": "16"}
|
||||
classes = []
|
||||
for k in cateManual:
|
||||
classes.append({'type_name': k, 'type_id': cateManual[k]})
|
||||
result['class'] = classes
|
||||
if (filter):
|
||||
result['filters'] = self._generate_filters()
|
||||
return result
|
||||
def homeVideoContent(self):
|
||||
rsp = self._fetch_with_cache("https://www.libvio.site")
|
||||
if not rsp:
|
||||
return {'list': []}
|
||||
doc = self._parse_html_fast(rsp.text)
|
||||
videos = []
|
||||
if pq is not None and hasattr(doc, '__call__'):
|
||||
try:
|
||||
thumb_links = doc('a.stui-vodlist__thumb.lazyload')
|
||||
for i in range(thumb_links.length):
|
||||
try:
|
||||
thumb = thumb_links.eq(i)
|
||||
href = thumb.attr('href')
|
||||
if not href: continue
|
||||
sid_match = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not sid_match: continue
|
||||
sid = sid_match.group(1)
|
||||
name = thumb.attr('title')
|
||||
if not name: continue
|
||||
pic = thumb.attr('data-original') or ""
|
||||
mark = thumb.text().strip()
|
||||
videos.append({"vod_id": sid, "vod_name": name.strip(), "vod_pic": pic, "vod_remarks": mark})
|
||||
except Exception as e: continue
|
||||
except: pass
|
||||
if not videos:
|
||||
try:
|
||||
thumb_links = doc.xpath("//a[@class='stui-vodlist__thumb lazyload']")
|
||||
for thumb in thumb_links:
|
||||
try:
|
||||
href = thumb.xpath("./@href")[0]
|
||||
sid_match = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not sid_match: continue
|
||||
sid = sid_match.group(1)
|
||||
name = thumb.xpath("./@title")[0].strip()
|
||||
if not name: continue
|
||||
pic_list = thumb.xpath("./@data-original")
|
||||
pic = pic_list[0] if pic_list else ""
|
||||
mark_list = thumb.xpath("./text()")
|
||||
mark = mark_list[0].strip() if mark_list else ""
|
||||
videos.append({"vod_id": sid, "vod_name": name, "vod_pic": pic, "vod_remarks": mark})
|
||||
except Exception as e: continue
|
||||
except Exception as e: print(f"Homepage parse failed: {e}")
|
||||
result = {'list': videos}
|
||||
return result
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
url = 'https://www.libvio.site/type/{0}-{1}.html'.format(tid, pg)
|
||||
print(url)
|
||||
rsp = self._fetch_with_cache(url)
|
||||
if not rsp:
|
||||
return result
|
||||
doc = self._parse_html_fast(rsp.text)
|
||||
videos = []
|
||||
if pq is not None and hasattr(doc, '__call__'):
|
||||
try:
|
||||
thumb_links = doc('a.stui-vodlist__thumb.lazyload')
|
||||
for i in range(thumb_links.length):
|
||||
try:
|
||||
thumb = thumb_links.eq(i)
|
||||
href = thumb.attr('href')
|
||||
if not href: continue
|
||||
sid_match = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not sid_match: continue
|
||||
sid = sid_match.group(1)
|
||||
name = thumb.attr('title')
|
||||
if not name: continue
|
||||
pic = thumb.attr('data-original') or ""
|
||||
mark = thumb.text().strip()
|
||||
videos.append({"vod_id": sid, "vod_name": name.strip(), "vod_pic": pic, "vod_remarks": mark})
|
||||
except Exception as e: continue
|
||||
except: pass
|
||||
if not videos:
|
||||
try:
|
||||
thumb_links = doc.xpath("//a[@class='stui-vodlist__thumb lazyload']")
|
||||
for thumb in thumb_links:
|
||||
try:
|
||||
href = thumb.xpath("./@href")[0]
|
||||
sid_match = re.search(r'/detail/(\d+)\.html', href)
|
||||
if not sid_match: continue
|
||||
sid = sid_match.group(1)
|
||||
name = thumb.xpath("./@title")[0].strip()
|
||||
if not name: continue
|
||||
pic_list = thumb.xpath("./@data-original")
|
||||
pic = pic_list[0] if pic_list else ""
|
||||
mark_list = thumb.xpath("./text()")
|
||||
mark = mark_list[0].strip() if mark_list else ""
|
||||
videos.append({"vod_id": sid, "vod_name": name, "vod_pic": pic, "vod_remarks": mark})
|
||||
except Exception as e: continue
|
||||
except Exception as e: print(f"Category parse failed: {e}")
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
def detailContent(self, array):
|
||||
tid = array[0]
|
||||
url = 'https://www.libvio.site/detail/{0}.html'.format(tid)
|
||||
rsp = self._fetch_with_cache(url)
|
||||
if not rsp:
|
||||
return {'list': []}
|
||||
doc = self._parse_html_fast(rsp.text)
|
||||
title = doc('h1').text().strip() or ""
|
||||
pic = doc('img').attr('data-original') or doc('img').attr('src') or ""
|
||||
detail = ""
|
||||
try:
|
||||
detail_content = doc('.detail-content').text().strip()
|
||||
if detail_content: detail = detail_content
|
||||
else:
|
||||
detail_text = doc('*:contains("简介:")').text()
|
||||
if detail_text and '简介:' in detail_text:
|
||||
detail_part = detail_text.split('简介:')[1]
|
||||
if '详情' in detail_part: detail_part = detail_part.replace('详情', '')
|
||||
detail = detail_part.strip()
|
||||
except: pass
|
||||
douban = "0.0"
|
||||
|
||||
score_text = doc('.detail-info *:contains("分")').text() or ""
|
||||
score_match = re.search(r'(\d+\.?\d*)\s*分', score_text)
|
||||
if score_match: douban = score_match.group(1)
|
||||
vod = {"vod_id": tid, "vod_name": title, "vod_pic": pic, "type_name": "", "vod_year": "", "vod_area": "", "vod_remarks": "", "vod_actor": "", "vod_director": "", "vod_douban_score": douban, "vod_content": detail}
|
||||
info_text = doc('p').text()
|
||||
if '类型:' in info_text:
|
||||
type_match = re.search(r'类型:([^/]+)', info_text)
|
||||
if type_match: vod['type_name'] = type_match.group(1).strip()
|
||||
if '主演:' in info_text:
|
||||
actor_match = re.search(r'主演:([^/]+)', info_text)
|
||||
if actor_match: vod['vod_actor'] = actor_match.group(1).strip()
|
||||
if '导演:' in info_text:
|
||||
director_match = re.search(r'导演:([^/]+)', info_text)
|
||||
if director_match: vod['vod_director'] = director_match.group(1).strip()
|
||||
|
||||
playFrom = []
|
||||
playList = []
|
||||
|
||||
# 改进的播放线路提取逻辑
|
||||
vodlist_heads = doc('.stui-vodlist__head')
|
||||
for i in range(vodlist_heads.length):
|
||||
head = vodlist_heads.eq(i)
|
||||
h3_elem = head.find('h3')
|
||||
if h3_elem.length == 0:
|
||||
continue
|
||||
|
||||
header_text = h3_elem.text().strip()
|
||||
if not any(keyword in header_text for keyword in ['播放', '下载', 'BD5', 'UC', '夸克']):
|
||||
continue
|
||||
|
||||
playFrom.append(header_text)
|
||||
vodItems = []
|
||||
|
||||
# 提取当前播放线路下的所有播放链接
|
||||
play_links = head.find('a[href*="/play/"]')
|
||||
for j in range(play_links.length):
|
||||
try:
|
||||
link = play_links.eq(j)
|
||||
href = link.attr('href')
|
||||
name = link.text().strip()
|
||||
if not href or not name:
|
||||
continue
|
||||
|
||||
tId_match = re.search(r'/play/([^.]+)\.html', href)
|
||||
if not tId_match:
|
||||
continue
|
||||
|
||||
tId = tId_match.group(1)
|
||||
vodItems.append(name + "$" + tId)
|
||||
except:
|
||||
continue
|
||||
|
||||
playList.append('#'.join(vodItems) if vodItems else "")
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(playFrom) if playFrom else ""
|
||||
vod['vod_play_url'] = '$$$'.join(playList) if playList else ""
|
||||
result = {'list': [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick):
|
||||
url = 'https://www.libvio.site/index.php/ajax/suggest?mid=1&wd={0}'.format(key)
|
||||
rsp = self._fetch_with_cache(url, headers=self.header)
|
||||
if not rsp:
|
||||
return {'list': []}
|
||||
try: jo = ujson.loads(rsp.text)
|
||||
except: jo = json.loads(rsp.text)
|
||||
result = {}
|
||||
jArray = []
|
||||
if jo.get('total', 0) > 0:
|
||||
for j in jo.get('list', []):
|
||||
jArray.append({"vod_id": j.get('id', ''), "vod_name": j.get('name', ''), "vod_pic": j.get('pic', ''), "vod_remarks": ""})
|
||||
result = {'list': jArray}
|
||||
return result
|
||||
def _generate_filters(self):
|
||||
|
||||
|
||||
years = [{"n": "全部", "v": ""}]
|
||||
for year in range(2025, 1999, -1):
|
||||
years.append({"n": str(year), "v": str(year)})
|
||||
|
||||
|
||||
movie_filters = [
|
||||
{
|
||||
"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": "惊悚"}, {"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": "韩国"},
|
||||
{"n": "德国", "v": "德国"}, {"n": "泰国", "v": "泰国"}, {"n": "印度", "v": "印度"},
|
||||
{"n": "意大利", "v": "意大利"}, {"n": "西班牙", "v": "西班牙"},
|
||||
{"n": "加拿大", "v": "加拿大"}, {"n": "其他", "v": "其他"}
|
||||
]
|
||||
},
|
||||
{"key": "year", "name": "年份", "value": years}
|
||||
]
|
||||
|
||||
|
||||
tv_filters = [
|
||||
{
|
||||
"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": "英国"},
|
||||
{"n": "新加坡", "v": "新加坡"}, {"n": "其他", "v": "其他"}
|
||||
]
|
||||
},
|
||||
{"key": "year", "name": "年份", "value": years}
|
||||
]
|
||||
|
||||
|
||||
anime_filters = [
|
||||
{
|
||||
"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": "原创"}, {"n": "亲子", "v": "亲子"}, {"n": "益智", "v": "益智"},
|
||||
{"n": "励志", "v": "励志"}, {"n": "其他", "v": "其他"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "area", "name": "地区",
|
||||
"value": [
|
||||
{"n": "全部", "v": ""}, {"n": "中国", "v": "中国"}, {"n": "日本", "v": "日本"},
|
||||
{"n": "欧美", "v": "欧美"}, {"n": "其他", "v": "其他"}
|
||||
]
|
||||
},
|
||||
{"key": "year", "name": "年份", "value": years}
|
||||
]
|
||||
|
||||
|
||||
asian_filters = [
|
||||
{
|
||||
"key": "class", "name": "剧情",
|
||||
"value": [
|
||||
{"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": "泰国"}
|
||||
]
|
||||
},
|
||||
{"key": "year", "name": "年份", "value": years[:25]}
|
||||
]
|
||||
|
||||
|
||||
western_filters = [
|
||||
{
|
||||
"key": "class", "name": "剧情",
|
||||
"value": [
|
||||
{"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": "其他"}
|
||||
]
|
||||
},
|
||||
{"key": "year", "name": "年份", "value": years[:25]}
|
||||
]
|
||||
|
||||
return {
|
||||
"1": movie_filters, # 电影
|
||||
"2": tv_filters, # 电视剧
|
||||
"4": anime_filters, # 动漫
|
||||
"15": asian_filters, # 日韩剧
|
||||
"16": western_filters # 欧美剧
|
||||
}
|
||||
header = {"Referer": "https://www.libvio.site", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"}
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
# 如果已经是push链接,直接返回
|
||||
if id.startswith('push://'):
|
||||
return {"parse": 0, "playUrl": "", "url": id, "header": ""}
|
||||
|
||||
result = {}
|
||||
url = 'https://www.libvio.site/play/{0}.html'.format(id)
|
||||
try:
|
||||
rsp = self._fetch_with_cache(url, headers=self.header)
|
||||
if not rsp:
|
||||
return {"parse": 1, "playUrl": "", "url": url, "header": ujson.dumps(self.header)}
|
||||
return self._handle_cloud_drive(url, rsp, id)
|
||||
except Exception as e:
|
||||
print(f"Player parse error: {e}")
|
||||
return {"parse": 1, "playUrl": "", "url": url, "header": ujson.dumps(self.header)}
|
||||
|
||||
def _handle_cloud_drive(self, url, rsp, id):
|
||||
try:
|
||||
page_text = rsp.text
|
||||
|
||||
# 首先尝试从JavaScript变量中提取网盘链接
|
||||
script_pattern = r'var player_[^=]*=\s*({[^}]+})'
|
||||
matches = re.findall(script_pattern, page_text)
|
||||
|
||||
for match in matches:
|
||||
try:
|
||||
player_data = ujson.loads(match)
|
||||
from_value = player_data.get('from', '')
|
||||
url_value = player_data.get('url', '')
|
||||
|
||||
if from_value == 'kuake' and url_value:
|
||||
# 夸克网盘
|
||||
drive_url = url_value.replace('\\/', '/')
|
||||
return {"parse": 0, "playUrl": "", "url": f"push://{drive_url}", "header": ""}
|
||||
elif from_value == 'uc' and url_value:
|
||||
# UC网盘
|
||||
drive_url = url_value.replace('\\/', '/')
|
||||
return {"parse": 0, "playUrl": "", "url": f"push://{drive_url}", "header": ""}
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Cloud drive parse error: {e}")
|
||||
|
||||
# 如果所有网盘解析都失败,尝试BD5播放源
|
||||
return self._handle_bd5_player(url, rsp, id)
|
||||
|
||||
def _handle_bd5_player(self, url, rsp, id):
|
||||
try:
|
||||
doc = self._parse_html_fast(rsp.text)
|
||||
page_text = rsp.text
|
||||
api_match = re.search(r'https://www\.libvio\.site/vid/plyr/vr2\.php\?url=([^&"\s]+)', page_text)
|
||||
if api_match:
|
||||
return {"parse": 0, "playUrl": "", "url": api_match.group(1), "header": ujson.dumps({"User-Agent": self.header["User-Agent"], "Referer": "https://www.libvio.site/"})}
|
||||
iframe_src = doc('iframe').attr('src')
|
||||
if iframe_src:
|
||||
try:
|
||||
iframe_content = self._fetch_with_cache(iframe_src, headers=self.header)
|
||||
if not iframe_content: raise Exception("Iframe fetch failed")
|
||||
video_match = re.search(r'https://[^"\s]+\.mp4', iframe_content.text)
|
||||
if video_match: return {"parse": 0, "playUrl": "", "url": video_match.group(0), "header": ujson.dumps({"User-Agent": self.header["User-Agent"], "Referer": "https://www.libvio.site/"})}
|
||||
except Exception as e: print(f"iframe视频解析失败: {e}")
|
||||
script_match = re.search(r'var player_[^=]*=\s*({[^}]+})', page_text)
|
||||
if script_match:
|
||||
try:
|
||||
jo = ujson.loads(script_match.group(1))
|
||||
if jo:
|
||||
nid = str(jo.get('nid', ''))
|
||||
player_from = jo.get('from', '')
|
||||
if player_from:
|
||||
scriptUrl = f'https://www.libvio.site/static/player/{player_from}.js'
|
||||
scriptRsp = self._fetch_with_cache(scriptUrl)
|
||||
if not scriptRsp: raise Exception("Script fetch failed")
|
||||
parse_match = re.search(r'src="([^"]+url=)', scriptRsp.text)
|
||||
if parse_match:
|
||||
parseUrl = parse_match.group(1)
|
||||
path = f"{jo.get('url', '')}&next={jo.get('link_next', '')}&id={jo.get('id', '')}&nid={nid}"
|
||||
parseRsp = self._fetch_with_cache(parseUrl + path, headers=self.header)
|
||||
if not parseRsp: raise Exception("Parse fetch failed")
|
||||
url_match = re.search(r"urls\s*=\s*'([^']+)'", parseRsp.text)
|
||||
if url_match: return {"parse": 0, "playUrl": "", "url": url_match.group(1), "header": ""}
|
||||
except Exception as e: print(f"JavaScript播放器解析失败: {e}")
|
||||
except Exception as e: print(f"BD5播放源解析错误: {e}")
|
||||
return {"parse": 1, "playUrl": "", "url": url, "header": ujson.dumps(self.header)}
|
||||
def isVideoFormat(self, url):
|
||||
|
||||
return False
|
||||
def manualVideoCheck(self):
|
||||
|
||||
pass
|
||||
def localProxy(self, param):
|
||||
|
||||
action = b''
|
||||
try:
|
||||
header_dict = json.loads(param.get('header', '{}')) if param.get('header') else {}
|
||||
resp = self.fetch(param['url'], headers=header_dict)
|
||||
action = resp.content
|
||||
except Exception as e:
|
||||
print(f"Local proxy error: {e}")
|
||||
return [200, "video/MP2T", action, param.get('header', '')]
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
# by嗷呜(finally)
|
||||
import sys
|
||||
import os
|
||||
sys.path.append("..")
|
||||
import re
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import string
|
||||
from Crypto.Util.Padding import unpad
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5, AES
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "电影猎手"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.device = self.device_id()
|
||||
self.host = self.gethost()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
t = str(int(time.time()))
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
filters = {}
|
||||
classes = []
|
||||
bba = self.url()
|
||||
data = self.fetch(f"{self.host}/api/v1/app/config?pack={bba[0]}&signature={bba[1]}", headers=self.header()).text
|
||||
data1 = self.aes(data)
|
||||
dy = {"class":"类型","area":"地区","lang":"语言","year":"年份","letter":"字母","by":"排序","sort":"排序"}
|
||||
data1['data']['movie_screen']['sort'].pop(0)
|
||||
for item in data1['data']['movie_screen']['sort']:
|
||||
item['n'] = item.pop('name')
|
||||
item['v'] = item.pop('value')
|
||||
for item in data1['data']['movie_screen']['filter']:
|
||||
has_non_empty_field = False
|
||||
classes.append({"type_name": item["name"], "type_id": str(item["id"])})
|
||||
for key in dy:
|
||||
if key in item and item[key]:
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["id"])] = []
|
||||
filters[str(item["id"])].append(
|
||||
{"key": 'sort', "name": '排序', "value": data1['data']['movie_screen']['sort']})
|
||||
for dkey in item:
|
||||
if dkey in dy and item[dkey]:
|
||||
item[dkey].pop(0)
|
||||
value_array = [
|
||||
{"n": value.strip(), "v": value.strip()}
|
||||
for value in item[dkey]
|
||||
if value.strip() != ""
|
||||
]
|
||||
filters[str(item["id"])].append(
|
||||
{"key": dkey, "name": dy[dkey], "value": value_array}
|
||||
)
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
bba = self.url()
|
||||
url = f'{self.host}/api/v1/movie/index_recommend?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()
|
||||
videos = []
|
||||
for item in data['data']:
|
||||
if len(item['list']) > 0:
|
||||
for it in item['list']:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"type_id": tid, "sort": extend.get("sort", "by_default"), "class": extend.get("class", "类型"),
|
||||
"area": extend.get("area", "地区"), "year": extend.get("year", "年份"), "page": str(pg),
|
||||
"pageSize": "21"}
|
||||
result = {}
|
||||
list = []
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/screen/list?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data']['list']
|
||||
for item in data:
|
||||
list.append(self.voides(item))
|
||||
result["list"] = list
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = {"id": ids[0]}
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie/detail?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
video = {'vod_name': data.get('name'),'type_name': data.get('type_name'),'vod_year': data.get('year'),'vod_area': data.get('area'),'vod_remarks': data.get('dynami'),'vod_content': data.get('content')}
|
||||
play = []
|
||||
names = []
|
||||
tasks = []
|
||||
for itt in data["play_from"]:
|
||||
name = itt["name"]
|
||||
a = []
|
||||
if len(itt["list"]) > 0:
|
||||
names.append(name)
|
||||
play.append(self.playeach(itt['list']))
|
||||
else:
|
||||
tasks.append({"movie_id": ids[0], "from_code": itt["code"]})
|
||||
names.append(name)
|
||||
if tasks:
|
||||
with ThreadPoolExecutor(max_workers=len(tasks)) as executor:
|
||||
results = executor.map(self.playlist, tasks)
|
||||
for result in results:
|
||||
if result:
|
||||
play.append(result)
|
||||
else:
|
||||
play.append("")
|
||||
video["vod_play_from"] = "$$$".join(names)
|
||||
video["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [video]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
body = {"keyword": key, "sort": "", "type_id": "0", "page": str(pg), "pageSize": "10",
|
||||
"res_type": "by_movie_name"}
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/search?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data'].get('list')
|
||||
videos = []
|
||||
for it in data:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos, "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id
|
||||
if "m3u8" not in url and "mp4" not in url:
|
||||
try:
|
||||
add = id.split('|||')
|
||||
data = {"from_code": add[0], "play_url": add[1], "episode_id": add[2], "type": "play"}
|
||||
bba = self.url(data)
|
||||
data2 = self.fetch(f"{self.host}/api/v1/movie_addr/parse_url?pack={bba[0]}&signature={bba[1]}",
|
||||
headers=self.header()).json()['data']
|
||||
url = data2.get('play_url') or data2.get('download_url')
|
||||
try:
|
||||
url1 = self.fetch(url, headers=self.header(), allow_redirects=False).headers['Location']
|
||||
if url1 and "http" in url1:
|
||||
url = url1
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
if '.jpg' in url or '.jpeg' in url or '.png' in url:
|
||||
url = self.getProxyUrl() + "&url=" + b64encode(url.encode('utf-8')).decode('utf-8') + "&type=m3u8"
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["url"] = url
|
||||
result["header"] = {'user-agent': 'okhttp/4.9.2'}
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
url = b64decode(param["url"]).decode('utf-8')
|
||||
durl = url[:url.rfind('/')]
|
||||
data = self.fetch(url, headers=self.header()).content.decode("utf-8")
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
# if 'URI="' in string and 'http' not in string:
|
||||
# lines[index] = index
|
||||
# 暂时预留,貌似用不到
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
lines[index] = durl + ('' if string.startswith('/') else '/') + string
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def device_id(self):
|
||||
characters = string.ascii_lowercase + string.digits
|
||||
random_string = ''.join(random.choices(characters, k=32))
|
||||
return random_string
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/4.9.2',
|
||||
'Connection': 'Keep-Alive',
|
||||
}
|
||||
response = self.fetch('https://app-site.ecoliving168.com/domain_v5.json', headers=headers).json()
|
||||
url = response['api_service'].replace('/api/', '')
|
||||
return url
|
||||
|
||||
def header(self):
|
||||
headers = {
|
||||
'User-Agent': 'Android',
|
||||
'Accept': 'application/prs.55App.v2+json',
|
||||
'timestamp': self.t,
|
||||
'x-client-setting': '{"pure-mode":1}',
|
||||
'x-client-uuid': '{"device_id":' + self.device + '}, "type":1,"brand":"Redmi", "model":"M2012K10C", "system_version":30, "sdk_version":"3.1.0.7"}',
|
||||
'x-client-version': '3096 '
|
||||
}
|
||||
return headers
|
||||
|
||||
def url(self, id=None):
|
||||
if not id:
|
||||
id = {}
|
||||
id["timestamp"] = self.t
|
||||
public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA02F/kPg5A2NX4qZ5JSns+bjhVMCC6JbTiTKpbgNgiXU+Kkorg6Dj76gS68gB8llhbUKCXjIdygnHPrxVHWfzmzisq9P9awmXBkCk74Skglx2LKHa/mNz9ivg6YzQ5pQFUEWS0DfomGBXVtqvBlOXMCRxp69oWaMsnfjnBV+0J7vHbXzUIkqBLdXSNfM9Ag5qdRDrJC3CqB65EJ3ARWVzZTTcXSdMW9i3qzEZPawPNPe5yPYbMZIoXLcrqvEZnRK1oak67/ihf7iwPJqdc+68ZYEmmdqwunOvRdjq89fQMVelmqcRD9RYe08v+xDxG9Co9z7hcXGTsUquMxkh29uNawIDAQAB'
|
||||
encrypted_text = json.dumps(id)
|
||||
public_key = RSA.import_key(b64decode(public_key))
|
||||
cipher = PKCS1_v1_5.new(public_key)
|
||||
encrypted_message = cipher.encrypt(encrypted_text.encode('utf-8'))
|
||||
encrypted_message_base64 = b64encode(encrypted_message).decode('utf-8')
|
||||
result = encrypted_message_base64.replace('+', '-').replace('/', '_').replace('=', '')
|
||||
key = '635a580fcb5dc6e60caa39c31a7bde48'
|
||||
sign = hmac.new(key.encode(), result.encode(), hashlib.md5).hexdigest()
|
||||
return result, sign
|
||||
|
||||
def playlist(self, body):
|
||||
try:
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie_addr/list?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
return self.playeach(data)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def playeach(self,data):
|
||||
play_urls = []
|
||||
for it in data:
|
||||
if re.search(r"mp4|m3u8", it["play_url"]):
|
||||
play_urls.append(f"{it['episode_name']}${it['play_url']}")
|
||||
else:
|
||||
play_urls.append(
|
||||
f"{it['episode_name']}${it['from_code']}|||{it['play_url']}|||{it['episode_id']}"
|
||||
)
|
||||
return '#'.join(play_urls)
|
||||
|
||||
def voides(self, item):
|
||||
if item['name'] or item['title']:
|
||||
voide = {
|
||||
"vod_id": item.get('id') or item.get('click'),
|
||||
'vod_name': item.get('name') or item.get('title'),
|
||||
'vod_pic': item.get('cover') or item.get('image'),
|
||||
'vod_year': item.get('year') or item.get('label'),
|
||||
'vod_remarks': item.get('dynamic') or item.get('sub_title')
|
||||
}
|
||||
return voide
|
||||
|
||||
def aes(self, text):
|
||||
text = text.replace('-', '+').replace('_', '/') + '=='
|
||||
key = b"e6d5de5fcc51f53d"
|
||||
iv = b"2f13eef7dfc6c613"
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size).decode("utf-8")
|
||||
return json.loads(pt)
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys,json,time,base64,random,string,hashlib
|
||||
from urllib.parse import urlencode,quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES,PKCS1_v1_5
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Util.Padding import pad,unpad
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.base_url = 'https://api-h5.uvod.tv'; self.web_url = 'https://m.uvod.tv'; self.token = ''; self._iv = b"abcdefghijklmnop"
|
||||
self._client_private = """-----BEGIN PRIVATE KEY-----
|
||||
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJ4FBai1Y6my4+fc
|
||||
8AD5tyYzxgN8Q7M/PuFv+8i1Xje8ElXYVwzvYd1y/cNxwgW4RX0tDy9ya562V33x
|
||||
6SyNr29DU6XytOeOlOkxt3gd5169K4iFaJ0l0wA4koMTcCAYVxC9B4+zzS5djYmF
|
||||
MuRGfYgKYNH99vfY7BZjdAY68ty5AgMBAAECgYB1rbvHJj5wVF7Rf4Hk2BMDCi9+
|
||||
zP4F8SW88Y6KrDbcPt1QvOonIea56jb9ZCxf4hkt3W6foRBwg86oZo2FtoZcpCJ+
|
||||
rFqUM2/wyV4CuzlL0+rNNSq7bga7d7UVld4hQYOCffSMifyF5rCFNH1py/4Dvswm
|
||||
pi5qljf+dPLSlxXl2QJBAMzPJ/QPAwcf5K5nngQtbZCD3nqDFpRixXH4aUAIZcDz
|
||||
S1RNsHrT61mEwZ/thQC2BUJTQNpGOfgh5Ecd1MnURwsCQQDFhAFfmvK7svkygoKX
|
||||
t55ARNZy9nmme0StMOfdb4Q2UdJjfw8+zQNtKFOM7VhB7ijHcfFuGsE7UeXBe20n
|
||||
g/XLAkEAv9SoT2hgJaQxxUk4MCF8pgddstJlq8Z3uTA7JMa4x+kZfXTm/6TOo6I8
|
||||
2VbXZLsYYe8op0lvsoHMFvBSBljV0QJBAKhxyoYRa98dZB5qZRskciaXTlge0WJk
|
||||
kA4vvh3/o757izRlQMgrKTfng1GVfIZFqKtnBiIDWTXQw2N9cnqXtH8CQAx+CD5t
|
||||
l1iT0cMdjvlMg2two3SnpOjpo7gALgumIDHAmsVWhocLtcrnJI032VQSUkNnLq9z
|
||||
EIfmHDz0TPTNHBQ=
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
self._client_public = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeBQWotWOpsuPn3PAA+bcmM8YD
|
||||
fEOzPz7hb/vItV43vBJV2FcM72Hdcv3DccIFuEV9LQ8vcmuetld98eksja9vQ1Ol
|
||||
8rTnjpTpMbd4HedevSuIhWidJdMAOJKDE3AgGFcQvQePs80uXY2JhTLkRn2ICmDR
|
||||
/fb32OwWY3QGOvLcuQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
self._server_public = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeBQWotWOpsuPn3PAA+bcmM8YD
|
||||
fEOzPz7hb/vItV43vBJV2FcM72Hdcv3DccIFuEV9LQ8vcmuetld98eksja9vQ1Ol
|
||||
8rTnjpTpMbd4HedevSuIhWidJdMAOJKDE3AgGFcQvQePs80uXY2JhTLkRn2ICmDR
|
||||
/fb32OwWY3QGOvLcuQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
def getName(self): return "UVOD"
|
||||
|
||||
def init(self, extend=""):
|
||||
try: cfg = json.loads(extend) if isinstance(extend, str) and extend.strip().startswith('{') else extend if isinstance(extend, dict) else {}
|
||||
except Exception: cfg = {}
|
||||
self.base_url = cfg.get('base_url', self.base_url); self.token = cfg.get('token', self.token)
|
||||
return self.homeContent(False)
|
||||
|
||||
def isVideoFormat(self, url): return any(x in url.lower() for x in ['.m3u8', '.mp4']) if url else False
|
||||
def manualVideoCheck(self): return False
|
||||
def destroy(self): pass
|
||||
|
||||
def _random_key(self, n=32):
|
||||
chars = string.ascii_letters + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(n))
|
||||
|
||||
def _encrypt(self, plain_text: str) -> str:
|
||||
aes_key = self._random_key(32).encode('utf-8')
|
||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv=self._iv)
|
||||
ct_b64 = base64.b64encode(cipher.encrypt(pad(plain_text.encode('utf-8'), AES.block_size))).decode('utf-8')
|
||||
rsa_pub = RSA.import_key(self._server_public); rsa_cipher = PKCS1_v1_5.new(rsa_pub)
|
||||
rsa_b64 = base64.b64encode(rsa_cipher.encrypt(aes_key)).decode('utf-8')
|
||||
return f"{ct_b64}.{rsa_b64}"
|
||||
|
||||
def _decrypt(self, enc_text: str) -> str:
|
||||
try:
|
||||
parts = enc_text.split('.'); ct_b64, rsa_b64 = parts
|
||||
rsa_priv = RSA.import_key(self._client_private)
|
||||
aes_key = PKCS1_v1_5.new(rsa_priv).decrypt(base64.b64decode(rsa_b64), None)
|
||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv=self._iv)
|
||||
pt = unpad(cipher.decrypt(base64.b64decode(ct_b64)), AES.block_size)
|
||||
return pt.decode('utf-8', 'ignore')
|
||||
except Exception: return enc_text
|
||||
|
||||
def _build_headers(self, path: str, payload: dict):
|
||||
ts = str(int(time.time() * 1000)); token = self.token or ''
|
||||
if path == '/video/latest':
|
||||
parent_id = payload.get('parent_category_id', 101); text = f"-parent_category_id={parent_id}-{ts}"
|
||||
elif path == '/video/list':
|
||||
keyword = payload.get('keyword')
|
||||
if keyword: keyword = quote(str(keyword), safe='').lower(); text = f"-keyword={keyword}&need_fragment=1&page=1&pagesize=42&sort_type=asc-{ts}"
|
||||
else: page = payload.get('page', 1); pagesize = payload.get('pagesize', 42); parent_id = payload.get('parent_category_id', ''); text = f"-page={page}&pagesize={pagesize}&parent_category_id={parent_id}&sort_type=asc-{ts}"
|
||||
elif path == '/video/info': text = f"-id={payload.get('id', '')}-{ts}"
|
||||
elif path == '/video/source': quality = payload.get('quality', ''); fragment_id = payload.get('video_fragment_id', ''); video_id = payload.get('video_id', ''); text = f"-quality={quality}&video_fragment_id={fragment_id}&video_id={video_id}-{ts}"
|
||||
else: filtered = {k: v for k, v in (payload or {}).items() if v not in (0, '0', '', False, None)}; query = urlencode(sorted(filtered.items()), doseq=True).lower(); text = f"{token}-{query}-{ts}"
|
||||
sig = hashlib.md5(text.encode('utf-8')).hexdigest()
|
||||
return {'Content-Type': 'application/json', 'X-TOKEN': token, 'X-TIMESTAMP': ts, 'X-SIGNATURE': sig, 'Origin': self.web_url, 'Referer': self.web_url + '/', 'Accept': '*/*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'}
|
||||
|
||||
def _post_api(self, path: str, payload: dict):
|
||||
url = self.base_url.rstrip('/') + path
|
||||
try:
|
||||
body = self._encrypt(json.dumps(payload, ensure_ascii=False)); headers = self._build_headers(path, payload)
|
||||
rsp = self.post(url, data=body, headers=headers, timeout=15)
|
||||
if rsp.status_code != 200 or not rsp.text: return None
|
||||
txt = rsp.text.strip(); obj = None
|
||||
try: dec = self._decrypt(txt); obj = json.loads(dec)
|
||||
except:
|
||||
try: obj = json.loads(txt)
|
||||
except: pass
|
||||
if isinstance(obj, dict) and obj.get('error') == 0: return obj.get('data')
|
||||
return None
|
||||
except Exception: return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self._post_api('/video/category', {}); lst = (data.get('list') or data.get('category') or []) if isinstance(data, dict) else (data or []); classes = []
|
||||
for it in lst:
|
||||
cid = it.get('id') or it.get('category_id') or it.get('value'); name = it.get('name') or it.get('label') or it.get('title')
|
||||
if cid and name: classes.append({'type_name': str(name), 'type_id': str(cid)})
|
||||
if not classes: classes = [{'type_name': '电影', 'type_id': '100'}, {'type_name': '电视剧', 'type_id': '101'}, {'type_name': '综艺', 'type_id': '102'}, {'type_name': '动漫', 'type_id': '103'}, {'type_name': '体育', 'type_id': '104'}, {'type_name': '纪录片', 'type_id': '105'}, {'type_name': '粤台专区', 'type_id': '106'}]
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self._post_api('/video/latest', {'parent_category_id': 101})
|
||||
if isinstance(data, dict): lst = data.get('video_latest_list') or data.get('list') or data.get('rows') or data.get('items') or []
|
||||
elif isinstance(data, list): lst = data
|
||||
else: lst = []
|
||||
videos = []
|
||||
for k in lst:
|
||||
vid = k.get('id') or k.get('video_id') or k.get('videoId')
|
||||
if vid: videos.append({'vod_id': str(vid), 'vod_name': k.get('title') or k.get('name') or '', 'vod_pic': k.get('poster') or k.get('cover') or k.get('pic') or '', 'vod_remarks': k.get('score') or k.get('remarks') or ''})
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
page = int(pg) if str(pg).isdigit() else 1
|
||||
payload = {'parent_category_id': str(tid), 'category_id': None, 'language': None, 'year': None, 'region': None, 'state': None, 'keyword': '', 'paid': None, 'page': page, 'pagesize': 42, 'sort_field': '', 'sort_type': 'asc'}
|
||||
if isinstance(extend, dict):
|
||||
for k in ['category_id', 'year', 'region', 'state', 'keyword']:
|
||||
if extend.get(k): payload[k] = extend[k]
|
||||
data = self._post_api('/video/list', payload)
|
||||
if isinstance(data, dict): lst = data.get('video_list') or data.get('list') or data.get('rows') or data.get('items') or []; total = data.get('total', 999999)
|
||||
elif isinstance(data, list): lst = data; total = 999999
|
||||
else: lst, total = [], 0
|
||||
videos = []
|
||||
for k in lst:
|
||||
vid = k.get('id') or k.get('video_id') or k.get('videoId')
|
||||
if vid: videos.append({'vod_id': str(vid), 'vod_name': k.get('title') or k.get('name') or '', 'vod_pic': k.get('poster') or k.get('cover') or k.get('pic') or '', 'vod_remarks': k.get('score') or ''})
|
||||
return {'list': videos, 'page': page, 'pagecount': 9999, 'limit': 24, 'total': total}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0]; data = self._post_api('/video/info', {'id': vid}) or {}; video_info = data.get('video', {}) if isinstance(data, dict) else {}; fragments = data.get('video_fragment_list', []) if isinstance(data, dict) else []; play_urls = []
|
||||
if fragments:
|
||||
for fragment in fragments:
|
||||
name = fragment.get('symbol', '播放'); fragment_id = fragment.get('id', ''); qualities = fragment.get('qualities', [])
|
||||
if fragment_id and qualities:
|
||||
|
||||
max_quality = max(qualities) if qualities else 4
|
||||
play_urls.append(f"{name}${vid}|{fragment_id}|[{max_quality}]")
|
||||
if not play_urls: play_urls.append(f"播放${vid}")
|
||||
vod = {'vod_id': str(vid), 'vod_name': video_info.get('title') or video_info.get('name') or '', 'vod_pic': video_info.get('poster') or video_info.get('cover') or video_info.get('pic') or '', 'vod_year': video_info.get('year') or '', 'vod_remarks': video_info.get('duration') or '', 'vod_content': video_info.get('description') or video_info.get('desc') or '', 'vod_play_from': '优汁🍑源', 'vod_play_url': '#'.join(play_urls) + '$$$'}
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
page = int(pg) if str(pg).isdigit() else 1
|
||||
payload = {'parent_category_id': None, 'category_id': None, 'language': None, 'year': None, 'region': None, 'state': None, 'keyword': key, 'paid': None, 'page': page, 'pagesize': 42, 'sort_field': '', 'sort_type': 'asc', 'need_fragment': 1}
|
||||
data = self._post_api('/video/list', payload)
|
||||
if isinstance(data, dict): lst = data.get('video_list') or data.get('list') or data.get('rows') or data.get('items') or []
|
||||
elif isinstance(data, list): lst = data
|
||||
else: lst = []
|
||||
videos = []
|
||||
for k in lst:
|
||||
vid = k.get('id') or k.get('video_id') or k.get('videoId')
|
||||
if vid: videos.append({'vod_id': str(vid), 'vod_name': k.get('title') or k.get('name') or '', 'vod_pic': k.get('poster') or k.get('cover') or k.get('pic') or '', 'vod_remarks': k.get('score') or ''})
|
||||
return {'list': videos}
|
||||
|
||||
def _extract_first_media(self, obj):
|
||||
if not obj: return None
|
||||
if isinstance(obj, str): s = obj.strip(); return s if self.isVideoFormat(s) else None
|
||||
if isinstance(obj, (dict, list)):
|
||||
for v in (obj.values() if isinstance(obj, dict) else obj):
|
||||
r = self._extract_first_media(v)
|
||||
if r: return r
|
||||
return None
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parts = id.split('|'); video_id = parts[0]
|
||||
if len(parts) >= 3:
|
||||
fragment_id = parts[1]; qualities_str = parts[2].strip('[]').replace(' ', ''); qualities = [q.strip() for q in qualities_str.split(',') if q.strip()]; quality = qualities[0] if qualities else '4'
|
||||
payload = {'video_id': video_id, 'video_fragment_id': int(fragment_id) if str(fragment_id).isdigit() else fragment_id, 'quality': int(quality) if str(quality).isdigit() else quality, 'seek': None}
|
||||
else: payload = {'video_id': video_id, 'video_fragment_id': 1, 'quality': 4, 'seek': None}
|
||||
data = self._post_api('/video/source', payload) or {}
|
||||
url = (data.get('video', {}).get('url', '') or data.get('url') or data.get('playUrl') or data.get('play_url') or self._extract_first_media(data) or '')
|
||||
if not url: return {'parse': 1, 'url': id}
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', 'Referer': self.web_url + '/', 'Origin': self.web_url}
|
||||
return {'parse': 0, 'url': url, 'header': headers}
|
||||
|
||||
def localProxy(self, param): return None
|
||||
@@ -0,0 +1,383 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
作者 丢丢喵 🚓 内容均从互联网收集而来 仅供交流学习使用 版权归原创者所有 如侵犯了您的权益 请通知作者 将及时删除侵权内容
|
||||
====================Diudiumiao====================
|
||||
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from Crypto.Util.Padding import pad
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://www.yymp3.com"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {"class": []}
|
||||
|
||||
detail = requests.get(url=xurl, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('div', id="nav_box")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('a')
|
||||
|
||||
for vod in vods:
|
||||
name = vod.text.strip()
|
||||
skip_keywords = ["首页", "动漫"]
|
||||
if any(keyword in name for keyword in skip_keywords):
|
||||
continue
|
||||
|
||||
id = vod['href']
|
||||
if 'http' not in id:
|
||||
id = xurl + id
|
||||
|
||||
result["class"].append({"type_id": id, "type_name": name})
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if '@' in cid:
|
||||
fenge = cid.split("@")
|
||||
|
||||
detail = requests.get(url=xurl + fenge[0], headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('dl', class_="albumlist c")
|
||||
|
||||
for vod in soups:
|
||||
names = vod.find('a', class_="A_name")
|
||||
name = names.text.strip()
|
||||
|
||||
ids = vod.find('dd', class_="A_details")
|
||||
id = ids.find('a')['href']
|
||||
|
||||
pic = vod.find('img')['src']
|
||||
|
||||
if 'http' not in pic:
|
||||
pic = "https:" + pic
|
||||
|
||||
remark = "推荐"
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
else:
|
||||
detail = requests.get(url=cid, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
soups = doc.find_all('ul', class_="Cate_slist c")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('a')
|
||||
|
||||
for vod in vods:
|
||||
|
||||
name = vod.text.strip()
|
||||
|
||||
id = vod['href']
|
||||
|
||||
pic = "https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/af3a1f95d591c34d/1755975256375.png"
|
||||
|
||||
remark = "推荐"
|
||||
|
||||
video = {
|
||||
"vod_id": id+'@'+name,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_tag": "folder",
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
xianlu = ''
|
||||
bofang = ''
|
||||
|
||||
if 'Play' in did:
|
||||
if 'http' not in did:
|
||||
bofang = xurl + did
|
||||
|
||||
xianlu = '搜索专线'
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
else:
|
||||
if 'http' not in did:
|
||||
did = xurl + did
|
||||
|
||||
res = requests.get(url=did, headers=headerx)
|
||||
res.encoding = "utf-8"
|
||||
res = res.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
content = self.extract_middle_text(res,'style="height:93px;">','</p>', 0)
|
||||
content = content.replace('<br>', '').replace('</br>', '').replace(' ', '')
|
||||
|
||||
director = self.extract_middle_text(res,'公司:','</', 0)
|
||||
|
||||
actor = self.extract_middle_text(res, '歌手:', '</li>',1,'href=.*?>(.*?)</a>')
|
||||
|
||||
year = self.extract_middle_text(res, '时间:', '<', 0)
|
||||
|
||||
area = self.extract_middle_text(res, '语种:', '<', 0)
|
||||
|
||||
soups = doc.find_all('ul', class_="A_list4")
|
||||
|
||||
for item in soups:
|
||||
vods = item.find_all('li')
|
||||
|
||||
for sou in vods:
|
||||
|
||||
ids = sou.find('div', class_="td1_l")
|
||||
id = ids.find('a')['href']
|
||||
|
||||
if 'http' not in id:
|
||||
id = xurl + id
|
||||
|
||||
names = sou.find('div', class_="td1_l")
|
||||
name = names.text.strip()
|
||||
|
||||
bofang = bofang + name + '$' + id + '#'
|
||||
|
||||
bofang = bofang[:-1]
|
||||
|
||||
xianlu = '音乐专线'
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_director": director,
|
||||
"vod_actor": actor,
|
||||
"vod_year": year,
|
||||
"vod_area": area,
|
||||
"vod_content": content,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
res = requests.get(url=id, headers=headerx)
|
||||
res.encoding = "utf-8"
|
||||
res = res.text
|
||||
|
||||
year = self.extract_middle_text(res, '$song_data[0]', ';', 0)
|
||||
fenge = year.split('|')
|
||||
|
||||
url = "https://ting8.yymp3.com/" + fenge[4]
|
||||
url = url.replace('wma', 'mp3')
|
||||
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = url
|
||||
result["header"] = headerx
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
url = f'{xurl}/search/?page={str(page)}&key={key}&tp=1'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('ul', class_="searchResult c")
|
||||
|
||||
for item in soups:
|
||||
vods = item.find_all('li')
|
||||
|
||||
for vod in vods[1:]:
|
||||
|
||||
names = vod.find('div', class_="p3")
|
||||
name1 = names.text.strip()
|
||||
|
||||
name2s = vod.find('div', class_="p2")
|
||||
name2 = name2s.text.strip()
|
||||
|
||||
name = name1 + ' ' + name2
|
||||
|
||||
id = names.find('a')['href']
|
||||
|
||||
pic = "https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/af3a1f95d591c34d/1755975256375.png"
|
||||
|
||||
remark = "推荐"
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
#coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import requests
|
||||
import threading
|
||||
from uuid import uuid4
|
||||
from urllib.parse import quote
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "EMBY"
|
||||
|
||||
def init(self, extend):
|
||||
try:
|
||||
extendDict = json.loads(extend)
|
||||
self.baseUrl = extendDict['server'].strip('/')
|
||||
self.username = extendDict['username']
|
||||
self.password = extendDict['password']
|
||||
self.proxy = extendDict['proxy']
|
||||
self.thread = extendDict['thread'] if 'thread' in extendDict else 0
|
||||
self.device_id = extendDict.get('device_id', str(uuid4()))
|
||||
self.client = extendDict.get('client', 'Hills Windows')
|
||||
self.device_name = extendDict.get('device_name', 'My Computer')
|
||||
self.client_version = extendDict.get('client_version', '0.2.2')
|
||||
except:
|
||||
self.baseUrl = ''
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
self.proxy = ''
|
||||
self.thread = 0
|
||||
self.device_id = str(uuid4())
|
||||
self.client = 'Hills Windows'
|
||||
self.device_name = 'My Computer'
|
||||
self.client_version = '0.2.2'
|
||||
|
||||
# 初始化header
|
||||
self.header = {
|
||||
"User-Agent": f"{self.client}/{self.client_version}".replace(' ', '-'), # 替换空格为连字符
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version
|
||||
}
|
||||
|
||||
# 初始化播放会话字典
|
||||
self.play_sessions = {}
|
||||
|
||||
def destroy(self):
|
||||
# 清理所有播放会话
|
||||
for session_id in list(self.play_sessions.keys()):
|
||||
self._record_playback_stop(session_id)
|
||||
self.play_sessions.clear()
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
try:
|
||||
embyInfos = self.getAccessToken()
|
||||
except:
|
||||
return {'msg': '获取Emby服务器信息出错'}
|
||||
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
url = f"{self.baseUrl}/emby/Users/{embyInfos['User']['Id']}/Views"
|
||||
params = {
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
}
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
typeInfos = r.json()["Items"]
|
||||
classList = []
|
||||
for typeInfo in typeInfos:
|
||||
if "播放列表" in typeInfo['Name'] or '相机' in typeInfo['Name']:
|
||||
continue
|
||||
classList.append({"type_name": typeInfo['Name'], "type_id": typeInfo['Id']})
|
||||
result = {'class': classList}
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
try:
|
||||
embyInfos = self.getAccessToken()
|
||||
except:
|
||||
return {'list': [], 'msg': '获取Emby服务器信息出错'}
|
||||
|
||||
result = {}
|
||||
page = int(page)
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
url = f"{self.baseUrl}/emby/Users/{embyInfos['User']['Id']}/Items"
|
||||
params = {
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken'],
|
||||
"SortBy": "DateLastContentAdded,SortName",
|
||||
"IncludeItemTypes": "Movie,Series",
|
||||
"SortOrder": "Descending",
|
||||
"ParentId": cid,
|
||||
"Recursive": "true",
|
||||
"Limit": "30",
|
||||
"ImageTypeLimit": 1,
|
||||
"StartIndex": str((page - 1) * 30),
|
||||
"EnableImageTypes": "Primary,Backdrop,Thumb,Banner",
|
||||
"Fields": "BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,CommunityRating,Status,CriticRating,EndDate,Path",
|
||||
"EnableUserData": "true"
|
||||
}
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
videoList = r.json()['Items']
|
||||
videos = []
|
||||
for video in videoList:
|
||||
name = self.cleanText(video['Name'])
|
||||
videos.append({
|
||||
"vod_id": video['Id'],
|
||||
"vod_name": name,
|
||||
"vod_pic": f"{self.baseUrl}/emby/Items/{video['Id']}/Images/Primary?maxWidth=400&tag={video['ImageTags']['Primary']}&quality=90" if 'Primary' in video['ImageTags'] else '',
|
||||
"vod_remarks": video['ProductionYear'] if 'ProductionYear' in video else ''
|
||||
})
|
||||
result['list'] = videos
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if page * 30 < int(r.json()['TotalRecordCount']) else page
|
||||
result['limit'] = len(videos)
|
||||
result['total'] = int(r.json()['TotalRecordCount']) if "TotalRecordCount" in r.json() else 0
|
||||
return result
|
||||
|
||||
def detailContent(self, did):
|
||||
try:
|
||||
embyInfos = self.getAccessToken()
|
||||
except:
|
||||
return {'list': [], 'msg': '获取Emby服务器信息出错'}
|
||||
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
url = f"{self.baseUrl}/emby/Users/{embyInfos['User']['Id']}/Items/{did[0]}"
|
||||
params = {
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
}
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
videoInfos = r.json()
|
||||
vod = {
|
||||
"vod_id": did[0],
|
||||
"vod_name": videoInfos['Name'],
|
||||
"vod_pic": f'{self.baseUrl}/emby/Items/{did[0]}/Images/Primary?maxWidth=400&tag={videoInfos["ImageTags"]["Primary"]}&quality=90' if 'Primary' in videoInfos['ImageTags'] else '',
|
||||
"type_name": videoInfos['Genres'][0] if len(videoInfos['Genres']) > 0 else '',
|
||||
"vod_year": videoInfos['ProductionYear'] if 'ProductionYear' in videoInfos else '',
|
||||
"vod_content": videoInfos['Overview'].replace('\xa0', ' ').replace('\n\n', '\n').strip() if 'Overview' in videoInfos else '',
|
||||
"vod_play_from": "EMBY"
|
||||
}
|
||||
playUrl = ''
|
||||
if not videoInfos['IsFolder']:
|
||||
playUrl += f"{videoInfos['Name'].strip()}${videoInfos['Id']}#"
|
||||
else:
|
||||
url = f"{self.baseUrl}/emby/Shows/{did[0]}/Seasons"
|
||||
params.update(
|
||||
{
|
||||
"UserId": embyInfos['User']['Id'],
|
||||
"EnableImages": "true",
|
||||
"Fields": "BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,CommunityRating",
|
||||
"EnableUserData": "true",
|
||||
"EnableTotalRecordCount": "false"
|
||||
}
|
||||
)
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
if r.status_code == 200:
|
||||
playInfos = r.json()['Items']
|
||||
for playInfo in playInfos:
|
||||
url = f"{self.baseUrl}/emby/Shows/{playInfo['Id']}/Episodes"
|
||||
params.update(
|
||||
{
|
||||
"SeasonId": playInfo['Id'],
|
||||
"Fields": "BasicSyncInfo,CanDelete,CommunityRating,PrimaryImageAspectRatio,ProductionYear,Overview"
|
||||
}
|
||||
)
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
videoList = r.json()['Items']
|
||||
for video in videoList:
|
||||
playUrl += f"{playInfo['Name'].replace('#', '-').replace('$', '|').strip()}|{video['Name'].strip()}${video['Id']}#"
|
||||
else:
|
||||
url = f"{self.baseUrl}/emby/Users/{embyInfos['User']['Id']}/Items"
|
||||
params = {
|
||||
"ParentId": did[0],
|
||||
"Fields": "BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,CommunityRating,CriticRating",
|
||||
"ImageTypeLimit": "1",
|
||||
"StartIndex": "0",
|
||||
"EnableUserData": "true",
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
}
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
videoList = r.json()['Items']
|
||||
for video in videoList:
|
||||
playUrl += f"{video['Name'].replace('#', '-').replace('$', '|').strip()}${video['Id']}#"
|
||||
vod['vod_play_url'] = playUrl.strip('#')
|
||||
result = {'list': [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, pg)
|
||||
|
||||
def searchContentPage(self, keywords, quick, page):
|
||||
try:
|
||||
embyInfos = self.getAccessToken()
|
||||
except:
|
||||
return {'list': [], 'msg': '获取Emby服务器信息出错'}
|
||||
page = int(page)
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
url = f"{self.baseUrl}/emby/Users/{embyInfos['User']['Id']}/Items"
|
||||
params = {
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken'],
|
||||
"SortBy": "SortName",
|
||||
"SortOrder": "Ascending",
|
||||
"Fields": "BasicSyncInfo,CanDelete,Container,PrimaryImageAspectRatio,ProductionYear,Status,EndDate",
|
||||
"StartIndex": str(((page-1)*50)),
|
||||
"EnableImageTypes": "Primary,Backdrop,Thumb",
|
||||
"ImageTypeLimit": "1",
|
||||
"Recursive": "true",
|
||||
"SearchTerm": keywords,
|
||||
"IncludeItemTypes": "Movie,Series,BoxSet",
|
||||
"GroupProgramsBySeries": "true",
|
||||
"Limit": "50",
|
||||
"EnableTotalRecordCount": "true"
|
||||
}
|
||||
r = requests.get(url, params=params, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
|
||||
videos = []
|
||||
vodList = r.json()['Items']
|
||||
for vod in vodList:
|
||||
sid = vod['Id']
|
||||
name = self.cleanText(vod['Name'])
|
||||
pic = f'{self.baseUrl}/emby/Items/{sid}/Images/Primary?maxWidth=400&tag={vod["ImageTags"]["Primary"]}&quality=90' if 'Primary' in vod["ImageTags"] else ''
|
||||
videos.append({
|
||||
"vod_id": sid,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": vod['ProductionYear'] if 'ProductionYear' in vod else ''
|
||||
})
|
||||
result = {'list': videos}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
try:
|
||||
embyInfos = self.getAccessToken()
|
||||
except:
|
||||
return {'list': [], 'msg': '获取Emby服务器信息出错'}
|
||||
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
|
||||
# 获取播放信息
|
||||
url = f"{self.baseUrl}/emby/Items/{pid}/PlaybackInfo"
|
||||
params = {
|
||||
"UserId": embyInfos['User']['Id'],
|
||||
"IsPlayback": "false",
|
||||
"AutoOpenLiveStream": "false",
|
||||
"StartTimeTicks": 0,
|
||||
"MaxStreamingBitrate": "2147483647",
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
}
|
||||
data = "{\"DeviceProfile\":{\"SubtitleProfiles\":[{\"Method\":\"Embed\",\"Format\":\"ass\"},{\"Format\":\"ssa\",\"Method\":\"Embed\"},{\"Format\":\"subrip\",\"Method\":\"Embed\"},{\"Format\":\"sub\",\"Method\":\"Embed\"},{\"Method\":\"Embed\",\"Format\":\"pgssub\"},{\"Format\":\"subrip\",\"Method\":\"External\"},{\"Method\":\"External\",\"Format\":\"sub\"},{\"Method\":\"External\",\"Format\":\"ass\"},{\"Format\":\"ssa\",\"Method\":\"External\"},{\"Method\":\"External\",\"Format\":\"vtt\"},{\"Method\":\"External\",\"Format\":\"ass\"},{\"Format\":\"ssa\",\"Method\":\"External\"}],\"CodecProfiles\":[{\"Codec\":\"h264\",\"Type\":\"Video\",\"ApplyConditions\":[{\"Property\":\"IsAnamorphic\",\"Value\":\"true\",\"Condition\":\"NotEquals\",\"IsRequired\":false},{\"IsRequired\":false,\"Value\":\"high|main|baseline|constrained baseline\",\"Condition\":\"EqualsAny\",\"Property\":\"VideoProfile\"},{\"IsRequired\":false,\"Value\":\"80\",\"Condition\":\"LessThanEqual\",\"Property\":\"VideoLevel\"},{\"IsRequired\":false,\"Value\":\"true\",\"Condition\":\"NotEquals\",\"Property\":\"IsInterlaced\"}]},{\"Codec\":\"hevc\",\"ApplyConditions\":[{\"Property\":\"IsAnamorphic\",\"Value\":\"true\",\"Condition\":\"NotEquals\",\"IsRequired\":false},{\"IsRequired\":false,\"Value\":\"high|main|main 10\",\"Condition\":\"EqualsAny\",\"Property\":\"VideoProfile\"},{\"Property\":\"VideoLevel\",\"Value\":\"175\",\"Condition\":\"LessThanEqual\",\"IsRequired\":false},{\"IsRequired\":false,\"Value\":\"true\",\"Condition\":\"NotEquals\",\"Property\":\"IsInterlaced\"}],\"Type\":\"Video\"}],\"MaxStreamingBitrate\":40000000,\"TranscodingProfiles\":[{\"Container\":\"ts\",\"AudioCodec\":\"aac,mp3,wav,ac3,eac3,flac,opus\",\"VideoCodec\":\"hevc,h264,mpeg4\",\"BreakOnNonKeyFrames\":true,\"Type\":\"Video\",\"MaxAudioChannels\":\"6\",\"Protocol\":\"hls\",\"Context\":\"Streaming\",\"MinSegments\":2}],\"DirectPlayProfiles\":[{\"Container\":\"mov,mp4,mkv,hls,webm\",\"Type\":\"Video\",\"VideoCodec\":\"h264,hevc,dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9\",\"AudioCodec\":\"aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus,pcm,pcm_s24le\"}],\"ResponseProfiles\":[{\"MimeType\":\"video/mp4\",\"Type\":\"Video\",\"Container\":\"m4v\"}],\"ContainerProfiles\":[],\"MusicStreamingTranscodingBitrate\":40000000,\"MaxStaticBitrate\":40000000}}"
|
||||
r = requests.post(url, params=params, data=data, headers=header, timeout=120, proxies={"http": self.proxy, "https": self.proxy})
|
||||
|
||||
# 获取播放URL
|
||||
media_sources = r.json()['MediaSources']
|
||||
if not media_sources:
|
||||
return {'list': [], 'msg': '没有可用的媒体源'}
|
||||
|
||||
# 使用第一个媒体源
|
||||
media_source = media_sources[0]
|
||||
direct_stream_url = media_source.get('DirectStreamUrl')
|
||||
|
||||
if not direct_stream_url:
|
||||
return {'list': [], 'msg': '无法获取播放URL'}
|
||||
|
||||
url = self.baseUrl + direct_stream_url
|
||||
|
||||
# 记录播放开始
|
||||
try:
|
||||
session_id = self._record_playback_start(embyInfos, pid, media_source)
|
||||
# 启动播放进度更新线程
|
||||
self._start_progress_updater(embyInfos, pid, media_source, session_id)
|
||||
except Exception as e:
|
||||
print(f"记录播放开始失败: {e}")
|
||||
|
||||
if int(self.thread) > 0:
|
||||
try:
|
||||
self.fetch('http://127.0.0.1:7777', timeout=120)
|
||||
except:
|
||||
self.fetch('http://127.0.0.1:9978/go')
|
||||
url = f'http://127.0.0.1:7777/?url={quote(url)}&thread={self.thread}'
|
||||
|
||||
result = {
|
||||
"url": url,
|
||||
"header": self.header,
|
||||
"parse": 0
|
||||
}
|
||||
return result
|
||||
|
||||
def _record_playback_start(self, embyInfos, item_id, media_source):
|
||||
"""记录播放开始"""
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
|
||||
# 添加认证头
|
||||
header.update({
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
})
|
||||
|
||||
# 生成唯一的会话ID
|
||||
session_id = f"session_{int(time.time())}_{random.randint(1000, 9999)}"
|
||||
|
||||
# 构建播放开始数据
|
||||
play_data = {
|
||||
"ItemId": item_id,
|
||||
"MediaSourceId": media_source.get('Id'),
|
||||
"CanSeek": True,
|
||||
"IsPaused": False,
|
||||
"IsMuted": False,
|
||||
"PositionTicks": 0,
|
||||
"PlayMethod": "DirectStream",
|
||||
"PlaySessionId": session_id,
|
||||
"LiveStreamId": None,
|
||||
"AudioStreamIndex": 1,
|
||||
"SubtitleStreamIndex": -1,
|
||||
"VolumeLevel": 100,
|
||||
"PlaybackStartTimeTicks": int(time.time() * 10000000)
|
||||
}
|
||||
|
||||
# 发送播放开始请求
|
||||
play_url = f"{self.baseUrl}/Sessions/Playing"
|
||||
try:
|
||||
response = requests.post(
|
||||
play_url,
|
||||
json=play_data,
|
||||
headers=header,
|
||||
timeout=5,
|
||||
proxies={"http": self.proxy, "https": self.proxy}
|
||||
)
|
||||
if response.status_code == 200 or response.status_code == 204:
|
||||
print(f"播放开始记录成功: {response.status_code}")
|
||||
# 保存会话信息
|
||||
self.play_sessions[session_id] = {
|
||||
'embyInfos': embyInfos,
|
||||
'item_id': item_id,
|
||||
'media_source': media_source,
|
||||
'start_time': time.time(),
|
||||
'last_update': time.time()
|
||||
}
|
||||
return session_id
|
||||
else:
|
||||
print(f"播放开始记录失败: {response.status_code}, {response.text}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"播放开始记录请求异常: {e}")
|
||||
return None
|
||||
|
||||
def _record_playback_progress(self, embyInfos, item_id, media_source, session_id, position_seconds):
|
||||
"""记录播放进度"""
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
|
||||
# 添加认证头
|
||||
header.update({
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
})
|
||||
|
||||
# 构建播放进度数据
|
||||
progress_data = {
|
||||
"ItemId": item_id,
|
||||
"MediaSourceId": media_source.get('Id'),
|
||||
"PositionTicks": int(position_seconds * 10000000), # 转换为ticks
|
||||
"IsPaused": False,
|
||||
"PlaySessionId": session_id,
|
||||
"EventName": "timeupdate"
|
||||
}
|
||||
|
||||
# 发送播放进度请求
|
||||
progress_url = f"{self.baseUrl}/Sessions/Playing/Progress"
|
||||
try:
|
||||
response = requests.post(
|
||||
progress_url,
|
||||
json=progress_data,
|
||||
headers=header,
|
||||
timeout=5,
|
||||
proxies={"http": self.proxy, "https": self.proxy}
|
||||
)
|
||||
if response.status_code == 200 or response.status_code == 204:
|
||||
print(f"播放进度更新成功: {position_seconds}秒")
|
||||
return True
|
||||
else:
|
||||
print(f"播放进度更新失败: {response.status_code}, {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"播放进度更新请求异常: {e}")
|
||||
return False
|
||||
|
||||
def _record_playback_stop(self, session_id):
|
||||
"""记录播放停止"""
|
||||
if session_id not in self.play_sessions:
|
||||
return False
|
||||
|
||||
session_info = self.play_sessions[session_id]
|
||||
embyInfos = session_info['embyInfos']
|
||||
item_id = session_info['item_id']
|
||||
media_source = session_info['media_source']
|
||||
total_duration = time.time() - session_info['start_time']
|
||||
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
|
||||
# 添加认证头
|
||||
header.update({
|
||||
"X-Emby-Client": self.client,
|
||||
"X-Emby-Device-Name": self.device_name,
|
||||
"X-Emby-Device-Id": self.device_id,
|
||||
"X-Emby-Client-Version": self.client_version,
|
||||
"X-Emby-Token": embyInfos['AccessToken']
|
||||
})
|
||||
|
||||
# 构建播放停止数据
|
||||
stop_data = {
|
||||
"ItemId": item_id,
|
||||
"MediaSourceId": media_source.get('Id'),
|
||||
"PositionTicks": int(total_duration * 10000000), # 转换为ticks
|
||||
"PlaySessionId": session_id
|
||||
}
|
||||
|
||||
# 发送播放停止请求
|
||||
stop_url = f"{self.baseUrl}/Sessions/Playing/Stopped"
|
||||
try:
|
||||
response = requests.post(
|
||||
stop_url,
|
||||
json=stop_data,
|
||||
headers=header,
|
||||
timeout=5,
|
||||
proxies={"http": self.proxy, "https": self.proxy}
|
||||
)
|
||||
if response.status_code == 200 or response.status_code == 204:
|
||||
print(f"播放停止记录成功: 总时长 {total_duration:.1f}秒")
|
||||
# 移除会话信息
|
||||
if session_id in self.play_sessions:
|
||||
del self.play_sessions[session_id]
|
||||
return True
|
||||
else:
|
||||
print(f"播放停止记录失败: {response.status_code}, {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"播放停止记录请求异常: {e}")
|
||||
return False
|
||||
|
||||
def _start_progress_updater(self, embyInfos, item_id, media_source, session_id):
|
||||
"""启动播放进度更新线程"""
|
||||
if not session_id:
|
||||
return
|
||||
|
||||
def progress_updater():
|
||||
try:
|
||||
start_time = time.time()
|
||||
last_update = start_time
|
||||
|
||||
# 每30秒更新一次播放进度
|
||||
while session_id in self.play_sessions:
|
||||
current_time = time.time()
|
||||
elapsed = current_time - start_time
|
||||
|
||||
# 每30秒更新一次进度
|
||||
if current_time - last_update >= 30:
|
||||
self._record_playback_progress(
|
||||
embyInfos, item_id, media_source, session_id, elapsed
|
||||
)
|
||||
last_update = current_time
|
||||
|
||||
# 检查是否超过最大持续时间(2小时)
|
||||
if elapsed >= 7200: # 2小时
|
||||
break
|
||||
|
||||
time.sleep(5) # 每5秒检查一次
|
||||
|
||||
# 播放结束,记录停止
|
||||
if session_id in self.play_sessions:
|
||||
self._record_playback_stop(session_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"播放进度更新线程异常: {e}")
|
||||
# 确保在异常情况下也尝试记录播放停止
|
||||
if session_id in self.play_sessions:
|
||||
self._record_playback_stop(session_id)
|
||||
|
||||
# 启动线程
|
||||
thread = threading.Thread(target=progress_updater, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def getAccessToken(self):
|
||||
key = f"emby_{self.baseUrl}_{self.username}_{self.password}"
|
||||
embyInfos = self.getCache(key)
|
||||
if embyInfos:
|
||||
return embyInfos
|
||||
|
||||
header = self.header.copy()
|
||||
header['Content-Type'] = "application/json; charset=UTF-8"
|
||||
|
||||
auth_data = {
|
||||
"Username": self.username,
|
||||
"Pw": self.password
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
f"{self.baseUrl}/emby/Users/AuthenticateByName",
|
||||
json=auth_data,
|
||||
headers=header,
|
||||
timeout=120,
|
||||
proxies={"http": self.proxy, "https": self.proxy}
|
||||
)
|
||||
embyInfos = r.json()
|
||||
self.setCache(key, embyInfos)
|
||||
return embyInfos
|
||||
|
||||
def cleanText(self, text):
|
||||
# 清理文本中的特殊字符
|
||||
if not text:
|
||||
return ""
|
||||
return text.replace("\n", " ").replace("\r", " ").replace("\t", " ").strip()
|
||||
+599
@@ -0,0 +1,599 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
import re
|
||||
import base64
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "两个BT"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.bttwoo.com"
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'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',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': self.host
|
||||
}
|
||||
self.log(f"两个BT爬虫初始化完成,主站: {self.host}")
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""首页内容 - TVBox标准实现"""
|
||||
result = {}
|
||||
|
||||
# 1. 定义分类 - 基于实际网站结构
|
||||
classes = [
|
||||
{'type_id': 'movie_bt_tags/xiju', 'type_name': '喜剧'},
|
||||
{'type_id': 'movie_bt_tags/aiqing', 'type_name': '爱情'},
|
||||
{'type_id': 'movie_bt_tags/adt', 'type_name': '冒险'},
|
||||
{'type_id': 'movie_bt_tags/at', 'type_name': '动作'},
|
||||
{'type_id': 'movie_bt_tags/donghua', 'type_name': '动画'},
|
||||
{'type_id': 'movie_bt_tags/qihuan', 'type_name': '奇幻'},
|
||||
{'type_id': 'movie_bt_tags/xuanni', 'type_name': '悬疑'},
|
||||
{'type_id': 'movie_bt_tags/kehuan', 'type_name': '科幻'},
|
||||
{'type_id': 'movie_bt_tags/juqing', 'type_name': '剧情'},
|
||||
{'type_id': 'movie_bt_tags/kongbu', 'type_name': '恐怖'},
|
||||
{'type_id': 'meiju', 'type_name': '美剧'},
|
||||
{'type_id': 'gf', 'type_name': '高分电影'}
|
||||
]
|
||||
result['class'] = classes
|
||||
|
||||
# 2. 添加筛选配置
|
||||
result['filters'] = self._get_filters()
|
||||
|
||||
# 3. 获取首页推荐内容
|
||||
try:
|
||||
rsp = self.fetch(self.host, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc, limit=50)
|
||||
result['list'] = videos
|
||||
except Exception as e:
|
||||
self.log(f"首页获取出错: {str(e)}")
|
||||
result['list'] = []
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""兼容性方法 - 提供分类定义"""
|
||||
return {
|
||||
'class': [
|
||||
{'type_id': 'movie_bt_tags/xiju', 'type_name': '喜剧'},
|
||||
{'type_id': 'movie_bt_tags/aiqing', 'type_name': '爱情'},
|
||||
{'type_id': 'movie_bt_tags/adt', 'type_name': '冒险'},
|
||||
{'type_id': 'movie_bt_tags/at', 'type_name': '动作'},
|
||||
{'type_id': 'movie_bt_tags/donghua', 'type_name': '动画'},
|
||||
{'type_id': 'movie_bt_tags/qihuan', 'type_name': '奇幻'},
|
||||
{'type_id': 'movie_bt_tags/xuanni', 'type_name': '悬疑'},
|
||||
{'type_id': 'movie_bt_tags/kehuan', 'type_name': '科幻'},
|
||||
{'type_id': 'movie_bt_tags/juqing', 'type_name': '剧情'},
|
||||
{'type_id': 'movie_bt_tags/kongbu', 'type_name': '恐怖'},
|
||||
{'type_id': 'meiju', 'type_name': '美剧'},
|
||||
{'type_id': 'gf', 'type_name': '高分电影'}
|
||||
],
|
||||
'filters': self._get_filters()
|
||||
}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""分类内容 - 支持筛选功能"""
|
||||
try:
|
||||
# 合并filter和extend参数
|
||||
if filter and isinstance(filter, dict):
|
||||
if not extend:
|
||||
extend = {}
|
||||
extend.update(filter)
|
||||
|
||||
self.log(f"分类请求: tid={tid}, pg={pg}, extend={extend}")
|
||||
|
||||
url = self._build_url(tid, pg, extend)
|
||||
if not url:
|
||||
return {'list': []}
|
||||
|
||||
rsp = self.fetch(url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
videos = self._get_videos(doc, limit=20)
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': int(pg),
|
||||
'pagecount': 999,
|
||||
'limit': 20,
|
||||
'total': 19980
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"分类内容获取出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索功能 - 智能过滤"""
|
||||
try:
|
||||
search_url = f"{self.host}/xssssearch?q={urllib.parse.quote(key)}"
|
||||
if pg and pg != "1":
|
||||
search_url += f"&p={pg}"
|
||||
|
||||
self.log(f"搜索URL: {search_url}")
|
||||
rsp = self.fetch(search_url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
|
||||
videos = []
|
||||
seen_ids = set()
|
||||
|
||||
# 搜索结果选择器
|
||||
elements = doc.xpath('//li[contains(@class,"") and .//a[contains(@href,"/movie/")]]')
|
||||
self.log(f"找到 {len(elements)} 个搜索结果元素")
|
||||
|
||||
for elem in elements:
|
||||
video = self._extract_video_info(elem, is_search=True)
|
||||
if video and video['vod_id'] not in seen_ids:
|
||||
# 添加相关性检查
|
||||
if self._is_relevant_search_result(video['vod_name'], key):
|
||||
videos.append(video)
|
||||
seen_ids.add(video['vod_id'])
|
||||
self.log(f"✅ 相关视频: {video['vod_name']} (ID: {video['vod_id']})")
|
||||
else:
|
||||
self.log(f"❌ 过滤无关: {video['vod_name']} (搜索: {key})")
|
||||
|
||||
self.log(f"最终搜索结果: {len(videos)} 个视频")
|
||||
return {'list': videos}
|
||||
except Exception as e:
|
||||
self.log(f"搜索出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""详情页面"""
|
||||
try:
|
||||
vid = ids[0]
|
||||
detail_url = f"{self.host}/movie/{vid}.html"
|
||||
rsp = self.fetch(detail_url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
|
||||
video_info = self._get_detail(doc, vid)
|
||||
return {'list': [video_info]} if video_info else {'list': []}
|
||||
except Exception as e:
|
||||
self.log(f"详情获取出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""播放链接"""
|
||||
try:
|
||||
self.log(f"获取播放链接: flag={flag}, id={id}")
|
||||
|
||||
# 解码Base64播放ID
|
||||
try:
|
||||
decoded_id = base64.b64decode(id).decode('utf-8')
|
||||
self.log(f"解码播放ID: {decoded_id}")
|
||||
except:
|
||||
decoded_id = id
|
||||
|
||||
play_url = f"{self.host}/v_play/{id}.html"
|
||||
|
||||
# 返回播放页面URL,让播放器处理
|
||||
return {'parse': 1, 'playUrl': '', 'url': play_url}
|
||||
except Exception as e:
|
||||
self.log(f"播放链接获取出错: {str(e)}")
|
||||
return {'parse': 1, 'playUrl': '', 'url': f"{self.host}/v_play/{id}.html"}
|
||||
|
||||
# ========== 辅助方法 ==========
|
||||
|
||||
def _get_filters(self):
|
||||
"""获取筛选配置 - TVBox兼容版"""
|
||||
base_filters = [
|
||||
{
|
||||
'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': '2025', 'v': '2025'},
|
||||
{'n': '2024', 'v': '2024'},
|
||||
{'n': '2023', 'v': '2023'},
|
||||
{'n': '2022', 'v': '2022'},
|
||||
{'n': '2021', 'v': '2021'},
|
||||
{'n': '2020', 'v': '2020'},
|
||||
{'n': '2019', 'v': '2019'},
|
||||
{'n': '2018', 'v': '2018'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# 为每个分类提供筛选配置
|
||||
filters = {}
|
||||
category_ids = [
|
||||
'movie_bt_tags/xiju', 'movie_bt_tags/aiqing', 'movie_bt_tags/adt',
|
||||
'movie_bt_tags/at', 'movie_bt_tags/donghua', 'movie_bt_tags/qihuan',
|
||||
'movie_bt_tags/xuanni', 'movie_bt_tags/kehuan', 'movie_bt_tags/juqing',
|
||||
'movie_bt_tags/kongbu', 'meiju', 'gf'
|
||||
]
|
||||
|
||||
for category_id in category_ids:
|
||||
filters[category_id] = base_filters
|
||||
|
||||
return filters
|
||||
|
||||
def _build_url(self, tid, pg, extend):
|
||||
"""构建URL - 支持筛选"""
|
||||
try:
|
||||
# 基础分类URL映射
|
||||
if tid.startswith('movie_bt_tags/'):
|
||||
url = f"{self.host}/{tid}"
|
||||
elif tid == 'meiju':
|
||||
url = f"{self.host}/meiju"
|
||||
elif tid == 'gf':
|
||||
url = f"{self.host}/gf"
|
||||
else:
|
||||
url = f"{self.host}/{tid}"
|
||||
|
||||
# 添加分页
|
||||
if pg and pg != '1':
|
||||
if '?' in url:
|
||||
url += f"&paged={pg}"
|
||||
else:
|
||||
url += f"?paged={pg}"
|
||||
|
||||
return url
|
||||
except Exception as e:
|
||||
self.log(f"构建URL出错: {str(e)}")
|
||||
return f"{self.host}/movie_bt_tags/xiju"
|
||||
|
||||
def _get_videos(self, doc, limit=None):
|
||||
"""获取视频列表"""
|
||||
try:
|
||||
videos = []
|
||||
seen_ids = set()
|
||||
|
||||
# 尝试多种选择器
|
||||
selectors = [
|
||||
'//li[.//a[contains(@href,"/movie/")]]',
|
||||
'//div[contains(@class,"item")]//li[.//a[contains(@href,"/movie/")]]'
|
||||
]
|
||||
|
||||
for selector in selectors:
|
||||
elements = doc.xpath(selector)
|
||||
if elements:
|
||||
for elem in elements:
|
||||
video = self._extract_video_info(elem)
|
||||
if video and video['vod_id'] not in seen_ids:
|
||||
videos.append(video)
|
||||
seen_ids.add(video['vod_id'])
|
||||
break
|
||||
|
||||
return videos[:limit] if limit and videos else videos
|
||||
except Exception as e:
|
||||
self.log(f"获取视频列表出错: {str(e)}")
|
||||
return []
|
||||
|
||||
def _extract_video_info(self, element, is_search=False):
|
||||
"""提取视频信息"""
|
||||
try:
|
||||
# 提取链接
|
||||
links = element.xpath('.//a[contains(@href,"/movie/")]/@href')
|
||||
if not links:
|
||||
return None
|
||||
|
||||
link = links[0]
|
||||
if link.startswith('/'):
|
||||
link = self.host + link
|
||||
|
||||
vod_id = self.regStr(r'/movie/(\d+)\.html', link)
|
||||
if not vod_id:
|
||||
return None
|
||||
|
||||
# 提取标题
|
||||
title_selectors = [
|
||||
'.//h3/a/text()',
|
||||
'.//h3/text()',
|
||||
'.//a/@title',
|
||||
'.//a/text()'
|
||||
]
|
||||
title = ''
|
||||
for selector in title_selectors:
|
||||
titles = element.xpath(selector)
|
||||
for t in titles:
|
||||
if t and t.strip() and len(t.strip()) > 1:
|
||||
title = t.strip()
|
||||
break
|
||||
if title:
|
||||
break
|
||||
|
||||
if not title:
|
||||
return None
|
||||
|
||||
# 提取图片
|
||||
pic = self._extract_image(element, is_search, vod_id)
|
||||
|
||||
# 提取备注
|
||||
remarks = self._extract_remarks(element)
|
||||
|
||||
return {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': remarks,
|
||||
'vod_year': ''
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"提取视频信息出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def _extract_image(self, element, is_search=False, vod_id=None):
|
||||
"""图片提取 - 处理懒加载"""
|
||||
pic_selectors = [
|
||||
'.//img/@data-original',
|
||||
'.//img/@data-src',
|
||||
'.//img/@src'
|
||||
]
|
||||
|
||||
for selector in pic_selectors:
|
||||
pics = element.xpath(selector)
|
||||
for p in pics:
|
||||
# 跳过懒加载占位符
|
||||
if (p and not p.endswith('blank.gif') and
|
||||
not p.startswith('data:image/') and 'base64' not in p):
|
||||
if p.startswith('//'):
|
||||
return 'https:' + p
|
||||
elif p.startswith('/'):
|
||||
return self.host + p
|
||||
elif p.startswith('http'):
|
||||
return p
|
||||
|
||||
# 搜索页面特殊处理:从详情页面获取
|
||||
if is_search and vod_id:
|
||||
return self._get_image_from_detail(vod_id)
|
||||
|
||||
return ''
|
||||
|
||||
def _extract_remarks(self, element):
|
||||
"""提取备注信息"""
|
||||
remarks_selectors = [
|
||||
'.//span[contains(@class,"rating")]/text()',
|
||||
'.//div[contains(@class,"rating")]/text()',
|
||||
'.//span[contains(@class,"status")]/text()',
|
||||
'.//div[contains(@class,"status")]/text()',
|
||||
'.//span[contains(text(),"集")]/text()',
|
||||
'.//span[contains(text(),"1080p")]/text()',
|
||||
'.//span[contains(text(),"HD")]/text()'
|
||||
]
|
||||
|
||||
for selector in remarks_selectors:
|
||||
remarks_list = element.xpath(selector)
|
||||
for r in remarks_list:
|
||||
if r and r.strip():
|
||||
return r.strip()
|
||||
|
||||
return ''
|
||||
|
||||
def _get_image_from_detail(self, vod_id):
|
||||
"""从详情页面获取图片"""
|
||||
try:
|
||||
detail_url = f"{self.host}/movie/{vod_id}.html"
|
||||
rsp = self.fetch(detail_url, headers=self.headers)
|
||||
doc = self.html(rsp.text)
|
||||
|
||||
# 详情页图片选择器
|
||||
pic_selectors = [
|
||||
'//img[contains(@class,"poster")]/@src',
|
||||
'//div[contains(@class,"poster")]//img/@src',
|
||||
'//img[contains(@alt,"")]/@src'
|
||||
]
|
||||
|
||||
for selector in pic_selectors:
|
||||
pics = doc.xpath(selector)
|
||||
for p in pics:
|
||||
if p and not p.endswith('blank.gif'):
|
||||
if p.startswith('//'):
|
||||
return 'https:' + p
|
||||
elif p.startswith('/'):
|
||||
return self.host + p
|
||||
elif p.startswith('http'):
|
||||
return p
|
||||
except:
|
||||
pass
|
||||
|
||||
return ''
|
||||
|
||||
def _is_relevant_search_result(self, title, search_key):
|
||||
"""检查搜索结果是否与搜索关键词相关"""
|
||||
if not title or not search_key:
|
||||
return False
|
||||
|
||||
title_lower = title.lower()
|
||||
search_key_lower = search_key.lower()
|
||||
|
||||
# 直接包含搜索关键词的肯定相关
|
||||
if search_key_lower in title_lower:
|
||||
return True
|
||||
|
||||
# 字符匹配
|
||||
search_chars = set(search_key_lower.replace(' ', ''))
|
||||
title_chars = set(title_lower.replace(' ', ''))
|
||||
|
||||
if len(search_chars) > 0:
|
||||
match_ratio = len(search_chars & title_chars) / len(search_chars)
|
||||
if match_ratio >= 0.6:
|
||||
return True
|
||||
|
||||
# 短搜索词要求严格匹配
|
||||
if len(search_key_lower) <= 2:
|
||||
return search_key_lower in title_lower
|
||||
|
||||
return False
|
||||
|
||||
def _get_detail(self, doc, vod_id):
|
||||
"""获取详情信息"""
|
||||
try:
|
||||
# 提取标题
|
||||
title_selectors = [
|
||||
'//h1/text()',
|
||||
'//h2/text()',
|
||||
'//title/text()'
|
||||
]
|
||||
title = ''
|
||||
for selector in title_selectors:
|
||||
titles = doc.xpath(selector)
|
||||
for t in titles:
|
||||
if t and t.strip():
|
||||
title = t.strip()
|
||||
break
|
||||
if title:
|
||||
break
|
||||
|
||||
# 提取图片
|
||||
pic_selectors = [
|
||||
'//img[contains(@class,"poster")]/@src',
|
||||
'//div[contains(@class,"poster")]//img/@src',
|
||||
'//img/@src'
|
||||
]
|
||||
pic = ''
|
||||
for selector in pic_selectors:
|
||||
pics = doc.xpath(selector)
|
||||
for p in pics:
|
||||
if p and not p.endswith('blank.gif'):
|
||||
if p.startswith('//'):
|
||||
pic = 'https:' + p
|
||||
elif p.startswith('/'):
|
||||
pic = self.host + p
|
||||
elif p.startswith('http'):
|
||||
pic = p
|
||||
break
|
||||
if pic:
|
||||
break
|
||||
|
||||
# 提取描述
|
||||
desc_selectors = [
|
||||
'//div[contains(@class,"intro")]//text()',
|
||||
'//div[contains(@class,"description")]//text()',
|
||||
'//p[contains(@class,"desc")]//text()'
|
||||
]
|
||||
desc = ''
|
||||
for selector in desc_selectors:
|
||||
descs = doc.xpath(selector)
|
||||
desc_parts = []
|
||||
for d in descs:
|
||||
if d and d.strip():
|
||||
desc_parts.append(d.strip())
|
||||
if desc_parts:
|
||||
desc = ' '.join(desc_parts)
|
||||
break
|
||||
|
||||
# 提取演员
|
||||
actor_selectors = [
|
||||
'//li[contains(text(),"主演")]/text()',
|
||||
'//span[contains(text(),"主演")]/following-sibling::text()',
|
||||
'//div[contains(@class,"actor")]//text()'
|
||||
]
|
||||
actor = ''
|
||||
for selector in actor_selectors:
|
||||
actors = doc.xpath(selector)
|
||||
for a in actors:
|
||||
if a and a.strip() and '主演' in a:
|
||||
actor = a.strip().replace('主演:', '').replace('主演', '')
|
||||
break
|
||||
if actor:
|
||||
break
|
||||
|
||||
# 提取导演
|
||||
director_selectors = [
|
||||
'//li[contains(text(),"导演")]/text()',
|
||||
'//span[contains(text(),"导演")]/following-sibling::text()',
|
||||
'//div[contains(@class,"director")]//text()'
|
||||
]
|
||||
director = ''
|
||||
for selector in director_selectors:
|
||||
directors = doc.xpath(selector)
|
||||
for d in directors:
|
||||
if d and d.strip() and '导演' in d:
|
||||
director = d.strip().replace('导演:', '').replace('导演', '')
|
||||
break
|
||||
if director:
|
||||
break
|
||||
|
||||
# 提取播放源
|
||||
play_sources = self._parse_play_sources(doc, vod_id)
|
||||
|
||||
return {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join([source['name'] for source in play_sources]),
|
||||
'vod_play_url': '$$$'.join([source['episodes'] for source in play_sources])
|
||||
}
|
||||
except Exception as e:
|
||||
self.log(f"获取详情出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def _parse_play_sources(self, doc, vod_id):
|
||||
"""解析播放源"""
|
||||
try:
|
||||
play_sources = []
|
||||
|
||||
# 查找播放链接
|
||||
episode_selectors = [
|
||||
'//a[contains(@href,"/v_play/")]',
|
||||
'//div[contains(@class,"play")]//a'
|
||||
]
|
||||
|
||||
episodes = []
|
||||
for selector in episode_selectors:
|
||||
episode_elements = doc.xpath(selector)
|
||||
if episode_elements:
|
||||
for ep in episode_elements:
|
||||
ep_title = ep.xpath('./text()')[0] if ep.xpath('./text()') else ''
|
||||
ep_url = ep.xpath('./@href')[0] if ep.xpath('./@href') else ''
|
||||
|
||||
if ep_title and ep_url:
|
||||
# 提取播放ID
|
||||
play_id = self.regStr(r'/v_play/([^.]+)\.html', ep_url)
|
||||
if play_id:
|
||||
episodes.append(f"{ep_title.strip()}${play_id}")
|
||||
break
|
||||
|
||||
if episodes:
|
||||
play_sources.append({
|
||||
'name': '默认播放',
|
||||
'episodes': '#'.join(episodes)
|
||||
})
|
||||
else:
|
||||
# 默认播放源
|
||||
play_sources.append({
|
||||
'name': '默认播放',
|
||||
'episodes': f'第1集$bXZfMTM0NTY4LW5tXzE='
|
||||
})
|
||||
|
||||
return play_sources
|
||||
except Exception as e:
|
||||
self.log(f"解析播放源出错: {str(e)}")
|
||||
return [{'name': '默认播放', 'episodes': f'第1集$bXZfMTM0NTY4LW5tXzE='}]
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import requests
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append("..")
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from urllib.parse import quote, urlparse
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = self.gethost()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.getdata("/api.php/getappapi.index/initV119")
|
||||
dy = {"class": "类型", "area": "地区", "lang": "语言", "year": "年份", "letter": "字母", "by": "排序",
|
||||
"sort": "排序"}
|
||||
filters = {}
|
||||
classes = []
|
||||
json_data = data["type_list"]
|
||||
homedata = data["banner_list"][8:]
|
||||
for item in json_data:
|
||||
if item["type_name"] == "全部":
|
||||
continue
|
||||
has_non_empty_field = False
|
||||
jsontype_extend = json.loads(item["type_extend"])
|
||||
homedata.extend(item["recommend_list"])
|
||||
jsontype_extend["sort"] = "最新,最热,最赞"
|
||||
classes.append({"type_name": item["type_name"], "type_id": item["type_id"]})
|
||||
for key in dy:
|
||||
if key in jsontype_extend and jsontype_extend[key].strip() != "":
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["type_id"])] = []
|
||||
for dkey in jsontype_extend:
|
||||
if dkey in dy and jsontype_extend[dkey].strip() != "":
|
||||
values = jsontype_extend[dkey].split(",")
|
||||
value_array = [{"n": value.strip(), "v": value.strip()} for value in values if
|
||||
value.strip() != ""]
|
||||
filters[str(item["type_id"])].append({"key": dkey, "name": dy[dkey], "value": value_array})
|
||||
result = {}
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
result["list"] = homedata[1:]
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"area": extend.get('area', '全部'), "year": extend.get('year', '全部'), "type_id": tid, "page": pg,
|
||||
"sort": extend.get('sort', '最新'), "lang": extend.get('lang', '全部'),
|
||||
"class": extend.get('class', '全部')}
|
||||
result = {}
|
||||
data = self.getdata("/api.php/getappapi.index/typeFilterVodList", body)
|
||||
result["list"] = data["recommend_list"]
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = f"vod_id={ids[0]}"
|
||||
data = self.getdata("/api.php/getappapi.index/vodDetail", body)
|
||||
vod = data["vod"]
|
||||
play = []
|
||||
names = []
|
||||
for itt in data["vod_play_list"]:
|
||||
a = []
|
||||
names.append(itt["player_info"]["show"])
|
||||
for it in itt['urls']:
|
||||
it['user_agent'] = itt["player_info"].get("user_agent")
|
||||
it["parse"] = itt["player_info"].get("parse")
|
||||
a.append(f"{it['name']}${self.e64(json.dumps(it))}")
|
||||
play.append("#".join(a))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
body = f"keywords={key}&type_id=0&page={pg}"
|
||||
data = self.getdata("/api.php/getappapi.index/searchList", body)
|
||||
result = {"list": data["search_list"], "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ids = json.loads(self.d64(id))
|
||||
h = {"User-Agent": (ids['user_agent'] or "okhttp/3.14.9")}
|
||||
try:
|
||||
if re.search(r'url=', ids['parse_api_url']):
|
||||
data = self.fetch(ids['parse_api_url'], headers=h, timeout=10).json()
|
||||
url = data.get('url') or data['data'].get('url')
|
||||
else:
|
||||
body = f"parse_api={ids.get('parse') or ids['parse_api_url'].replace(ids['url'], '')}&url={quote(self.aes(ids['url'], True))}&token={ids.get('token')}"
|
||||
b = self.getdata("/api.php/getappapi.index/vodParse", body)['json']
|
||||
url = json.loads(b)['url']
|
||||
if 'error' in url: raise ValueError(f"解析失败: {url}")
|
||||
p = 0
|
||||
except Exception as e:
|
||||
print('错误信息:', e)
|
||||
url, p = ids['url'], 1
|
||||
|
||||
if re.search(r'\.jpg|\.png|\.jpeg', url):
|
||||
url = self.Mproxy(url)
|
||||
result = {}
|
||||
result["parse"] = p
|
||||
result["url"] = url
|
||||
result["header"] = h
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
return self.Mlocal(param)
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/3.14.9'
|
||||
}
|
||||
response = self.fetch('https://ydysdynamicdomainname.68.gy:10678/c9m2js298x82h6/l9m8bx23j2o2p9q/dynamicdomainname.txt',
|
||||
headers=headers).text
|
||||
return self.host_late(response.split('\n'))
|
||||
|
||||
def host_late(self, url_list):
|
||||
if isinstance(url_list, str):
|
||||
urls = [u.strip() for u in url_list.split(',')]
|
||||
else:
|
||||
urls = url_list
|
||||
|
||||
if len(urls) <= 1:
|
||||
return urls[0] if urls else ''
|
||||
|
||||
results = {}
|
||||
threads = []
|
||||
|
||||
def test_host(url):
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.head(url,timeout=1.0, allow_redirects=False)
|
||||
delay = (time.time() - start_time) * 1000
|
||||
results[url] = delay
|
||||
except Exception as e:
|
||||
results[url] = float('inf')
|
||||
|
||||
for url in urls:
|
||||
t = threading.Thread(target=test_host, args=(url,))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
return min(results.items(), key=lambda x: x[1])[0]
|
||||
|
||||
def aes(self, text, b=None):
|
||||
key = b"k9o3p2c8b7m3z0o8"
|
||||
cipher = AES.new(key, AES.MODE_CBC, key)
|
||||
if b:
|
||||
ct_bytes = cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))
|
||||
ct = b64encode(ct_bytes).decode("utf-8")
|
||||
return ct
|
||||
else:
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
return pt.decode("utf-8")
|
||||
|
||||
def header(self):
|
||||
t = str(int(time.time()))
|
||||
header = {"Referer": self.host,
|
||||
"User-Agent": "okhttp/3.14.9", "app-version-code": "140", "app-ui-mode": "light",
|
||||
"app-api-verify-time": t, "app-user-device-id": self.md5(t),
|
||||
"app-api-verify-sign": self.aes(t, True),
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}
|
||||
return header
|
||||
|
||||
def getdata(self, path, data=None):
|
||||
vdata = self.post(f"{self.host}{path}", headers=self.header(), data=data, timeout=10).json()['data']
|
||||
data1 = self.aes(vdata)
|
||||
return json.loads(data1)
|
||||
|
||||
def Mproxy(self, url):
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=m3u8"
|
||||
|
||||
def Mlocal(self, param, header=None):
|
||||
url = self.d64(param["url"])
|
||||
ydata = self.fetch(url, headers=header, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = self.fetch(url, headers=header).content.decode('utf-8')
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
last_slash_index = string.rfind('/')
|
||||
lpath = string[:last_slash_index + 1]
|
||||
lines[index] = durl + ('' if lpath.startswith('/') else '/') + lpath
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
+716
@@ -0,0 +1,716 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 🌈 Love
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from base64 import b64decode, b64encode
|
||||
from urllib.parse import urlparse, quote
|
||||
|
||||
import requests
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
try:self.proxies = json.loads(extend)
|
||||
except:self.proxies = {}
|
||||
self.headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
}
|
||||
# Use working dynamic URLs directly
|
||||
self.host = self.get_working_host()
|
||||
self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"})
|
||||
self.log(f"使用站点: {self.host}")
|
||||
print(f"使用站点: {self.host}")
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "🌈 今日看料"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
# Treat direct media formats as playable without parsing
|
||||
return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
try:
|
||||
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
result = {}
|
||||
classes = []
|
||||
|
||||
# 优先从导航栏获取分类
|
||||
nav_selectors = [
|
||||
'#navbarCollapse .navbar-nav .nav-item .nav-link',
|
||||
'.navbar-nav .nav-item .nav-link',
|
||||
'#nav .menu-item a',
|
||||
'.menu .menu-item a'
|
||||
]
|
||||
|
||||
found_categories = False
|
||||
for selector in nav_selectors:
|
||||
for item in data(selector).items():
|
||||
href = item.attr('href') or ''
|
||||
name = item.text().strip()
|
||||
|
||||
# 过滤掉非分类链接
|
||||
if (not href or not name or
|
||||
href == '#' or
|
||||
href.startswith('http') or
|
||||
'about' in href.lower() or
|
||||
'contact' in href.lower() or
|
||||
'tags' in href.lower() or
|
||||
'top' in href.lower() or
|
||||
'start' in href.lower() or
|
||||
'time' in href.lower()):
|
||||
continue
|
||||
|
||||
# 确保是分类链接(包含category或明确的分类路径)
|
||||
if '/category/' in href or any(cat in href for cat in ['/dy/', '/ks/', '/douyu/', '/hy/', '/hj/', '/tt/', '/wh/', '/asmr/', '/xb/', '/xsp/', '/rdgz/']):
|
||||
# 处理相对路径
|
||||
if href.startswith('/'):
|
||||
type_id = href
|
||||
else:
|
||||
type_id = f'/{href}'
|
||||
|
||||
classes.append({
|
||||
'type_name': name,
|
||||
'type_id': type_id
|
||||
})
|
||||
found_categories = True
|
||||
|
||||
# 如果导航栏没找到,尝试从分类下拉菜单获取
|
||||
if not found_categories:
|
||||
category_selectors = [
|
||||
'.category-list a',
|
||||
'.slide-toggle + .category-list a',
|
||||
'.menu .category-list a'
|
||||
]
|
||||
for selector in category_selectors:
|
||||
for item in data(selector).items():
|
||||
href = item.attr('href') or ''
|
||||
name = item.text().strip()
|
||||
|
||||
if href and name and href != '#':
|
||||
if href.startswith('/'):
|
||||
type_id = href
|
||||
else:
|
||||
type_id = f'/{href}'
|
||||
|
||||
classes.append({
|
||||
'type_name': name,
|
||||
'type_id': type_id
|
||||
})
|
||||
found_categories = True
|
||||
|
||||
# 去重
|
||||
unique_classes = []
|
||||
seen_ids = set()
|
||||
for cls in classes:
|
||||
if cls['type_id'] not in seen_ids:
|
||||
unique_classes.append(cls)
|
||||
seen_ids.add(cls['type_id'])
|
||||
|
||||
# 如果没有找到分类,创建默认分类
|
||||
if not unique_classes:
|
||||
unique_classes = [
|
||||
{'type_name': '热点关注', 'type_id': '/category/rdgz/'},
|
||||
{'type_name': '抖音', 'type_id': '/category/dy/'},
|
||||
{'type_name': '快手', 'type_id': '/category/ks/'},
|
||||
{'type_name': '斗鱼', 'type_id': '/category/douyu/'},
|
||||
{'type_name': '虎牙', 'type_id': '/category/hy/'},
|
||||
{'type_name': '花椒', 'type_id': '/category/hj/'},
|
||||
{'type_name': '推特', 'type_id': '/category/tt/'},
|
||||
{'type_name': '网红', 'type_id': '/category/wh/'},
|
||||
{'type_name': 'ASMR', 'type_id': '/category/asmr/'},
|
||||
{'type_name': 'X播', 'type_id': '/category/xb/'},
|
||||
{'type_name': '小视频', 'type_id': '/category/xsp/'}
|
||||
]
|
||||
|
||||
result['class'] = unique_classes
|
||||
result['list'] = self.getlist(data('#index article a, #archive article a'))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"homeContent error: {e}")
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
return {'list': []}
|
||||
data = self.getpq(response.text)
|
||||
return {'list': self.getlist(data('#index article a, #archive article a'))}
|
||||
except Exception as e:
|
||||
print(f"homeVideoContent error: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
try:
|
||||
# 修复URL构建 - 去除多余的斜杠
|
||||
base_url = tid.lstrip('/').rstrip('/')
|
||||
if pg and pg != '1':
|
||||
url = f"{self.host}{base_url}/{pg}/"
|
||||
else:
|
||||
url = f"{self.host}{base_url}/"
|
||||
|
||||
print(f"分类页面URL: {url}")
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
print(f"分类页面请求失败: {response.status_code}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article a, #index article a, .post-card'), tid)
|
||||
|
||||
# 如果没有找到视频,尝试其他选择器
|
||||
if not videos:
|
||||
videos = self.getlist(data('article a, .post a, .entry-title a'), tid)
|
||||
|
||||
print(f"找到 {len(videos)} 个视频")
|
||||
|
||||
# 改进的页数检测逻辑
|
||||
pagecount = self.detect_page_count(data, pg)
|
||||
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = pagecount
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"categoryContent error: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
def tagContent(self, tid, pg, filter, extend):
|
||||
"""标签页面内容"""
|
||||
try:
|
||||
# 修复URL构建 - 去除多余的斜杠
|
||||
base_url = tid.lstrip('/').rstrip('/')
|
||||
if pg and pg != '1':
|
||||
url = f"{self.host}{base_url}/{pg}/"
|
||||
else:
|
||||
url = f"{self.host}{base_url}/"
|
||||
|
||||
print(f"标签页面URL: {url}")
|
||||
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
if response.status_code != 200:
|
||||
print(f"标签页面请求失败: {response.status_code}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article a, #index article a, .post-card'), tid)
|
||||
|
||||
# 如果没有找到视频,尝试其他选择器
|
||||
if not videos:
|
||||
videos = self.getlist(data('article a, .post a, .entry-title a'), tid)
|
||||
|
||||
print(f"找到 {len(videos)} 个标签相关视频")
|
||||
|
||||
# 页数检测
|
||||
pagecount = self.detect_page_count(data, pg)
|
||||
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = pagecount
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"tagContent error: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
def detect_page_count(self, data, current_page):
|
||||
"""改进的页数检测方法"""
|
||||
pagecount = 99999 # 默认大数字,允许无限翻页
|
||||
|
||||
# 方法1: 检查分页器中的所有页码链接
|
||||
page_numbers = []
|
||||
|
||||
# 查找所有可能的页码链接
|
||||
page_selectors = [
|
||||
'.page-navigator a',
|
||||
'.pagination a',
|
||||
'.pages a',
|
||||
'.page-numbers a'
|
||||
]
|
||||
|
||||
for selector in page_selectors:
|
||||
for page_link in data(selector).items():
|
||||
href = page_link.attr('href') or ''
|
||||
text = page_link.text().strip()
|
||||
|
||||
# 从href中提取页码
|
||||
if href:
|
||||
# 匹配 /category/dy/2/ 这种格式
|
||||
match = re.search(r'/(\d+)/?$', href.rstrip('/'))
|
||||
if match:
|
||||
page_num = int(match.group(1))
|
||||
if page_num not in page_numbers:
|
||||
page_numbers.append(page_num)
|
||||
|
||||
# 从文本中提取数字页码
|
||||
if text and text.isdigit():
|
||||
page_num = int(text)
|
||||
if page_num not in page_numbers:
|
||||
page_numbers.append(page_num)
|
||||
|
||||
# 如果有找到页码,取最大值
|
||||
if page_numbers:
|
||||
max_page = max(page_numbers)
|
||||
print(f"从分页器检测到最大页码: {max_page}")
|
||||
return max_page
|
||||
|
||||
# 方法2: 检查是否存在"下一页"按钮
|
||||
next_selectors = [
|
||||
'.page-navigator .next',
|
||||
'.pagination .next',
|
||||
'.next-page',
|
||||
'a:contains("下一页")'
|
||||
]
|
||||
|
||||
for selector in next_selectors:
|
||||
if data(selector):
|
||||
print("检测到下一页按钮,允许继续翻页")
|
||||
return 99999
|
||||
|
||||
# 方法3: 如果当前页视频数量很少,可能没有下一页
|
||||
if len(data('#archive article, #index article, .post-card')) < 5:
|
||||
print("当前页内容较少,可能没有下一页")
|
||||
return int(current_page)
|
||||
|
||||
print("使用默认页数: 99999")
|
||||
return 99999
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
url = f"{self.host}{ids[0]}" if not ids[0].startswith('http') else ids[0]
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'list': [{'vod_play_from': '今日看料', 'vod_play_url': f'页面加载失败${url}'}]}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
vod = {'vod_play_from': '今日看料'}
|
||||
|
||||
# 获取标题
|
||||
title_selectors = ['.post-title', 'h1.entry-title', 'h1', '.post-card-title']
|
||||
for selector in title_selectors:
|
||||
title_elem = data(selector)
|
||||
if title_elem:
|
||||
vod['vod_name'] = title_elem.text().strip()
|
||||
break
|
||||
|
||||
if 'vod_name' not in vod:
|
||||
vod['vod_name'] = '今日看料视频'
|
||||
|
||||
# 获取内容/描述
|
||||
try:
|
||||
clist = []
|
||||
if data('.tags .keywords a'):
|
||||
for k in data('.tags .keywords a').items():
|
||||
title = k.text()
|
||||
href = k.attr('href')
|
||||
if title and href:
|
||||
# 使href相对路径
|
||||
if href.startswith(self.host):
|
||||
href = href.replace(self.host, '')
|
||||
clist.append('[a=cr:' + json.dumps({'id': href, 'name': title}) + '/]' + title + '[/a]')
|
||||
vod['vod_content'] = ' '.join(clist) if clist else data('.post-content').text() or vod['vod_name']
|
||||
except:
|
||||
vod['vod_content'] = vod['vod_name']
|
||||
|
||||
# 获取视频URLs
|
||||
try:
|
||||
plist = []
|
||||
used_names = set()
|
||||
|
||||
# 查找DPlayer视频
|
||||
if data('.dplayer'):
|
||||
for c, k in enumerate(data('.dplayer').items(), start=1):
|
||||
config_attr = k.attr('data-config')
|
||||
if config_attr:
|
||||
try:
|
||||
config = json.loads(config_attr)
|
||||
video_url = config.get('video', {}).get('url', '')
|
||||
if video_url:
|
||||
name = f"视频{c}"
|
||||
count = 2
|
||||
while name in used_names:
|
||||
name = f"视频{c}_{count}"
|
||||
count += 1
|
||||
used_names.add(name)
|
||||
self.log(f"解析到视频: {name} -> {video_url}")
|
||||
print(f"解析到视频: {name} -> {video_url}")
|
||||
plist.append(f"{name}${video_url}")
|
||||
except:
|
||||
continue
|
||||
|
||||
# 查找视频标签
|
||||
if not plist:
|
||||
video_selectors = ['video source', 'video', 'iframe[src*="video"]', 'a[href*=".m3u8"]', 'a[href*=".mp4"]']
|
||||
for selector in video_selectors:
|
||||
for c, elem in enumerate(data(selector).items(), start=1):
|
||||
src = elem.attr('src') or elem.attr('href') or ''
|
||||
if src and any(ext in src for ext in ['.m3u8', '.mp4', 'video']):
|
||||
name = f"视频{c}"
|
||||
count = 2
|
||||
while name in used_names:
|
||||
name = f"视频{c}_{count}"
|
||||
count += 1
|
||||
used_names.add(name)
|
||||
plist.append(f"{name}${src}")
|
||||
|
||||
if plist:
|
||||
self.log(f"拼装播放列表,共{len(plist)}个")
|
||||
print(f"拼装播放列表,共{len(plist)}个")
|
||||
vod['vod_play_url'] = '#'.join(plist)
|
||||
else:
|
||||
vod['vod_play_url'] = f"正片${url}"
|
||||
|
||||
except Exception as e:
|
||||
print(f"视频解析错误: {e}")
|
||||
vod['vod_play_url'] = f"正片${url}"
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
except Exception as e:
|
||||
print(f"detailContent error: {e}")
|
||||
return {'list': [{'vod_play_from': '今日看料', 'vod_play_url': f'详情页加载失败${ids[0] if ids else ""}'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
try:
|
||||
# 优先使用标签搜索
|
||||
encoded_key = quote(key)
|
||||
url = f"{self.host}/tag/{encoded_key}/{pg}" if pg != "1" else f"{self.host}/tag/{encoded_key}/"
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
# 尝试搜索页面
|
||||
url = f"{self.host}/search/{encoded_key}/{pg}" if pg != "1" else f"{self.host}/search/{encoded_key}/"
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
videos = self.getlist(data('#archive article a, #index article a, .post-card'))
|
||||
|
||||
# 使用改进的页数检测方法
|
||||
pagecount = self.detect_page_count(data, pg)
|
||||
|
||||
return {'list': videos, 'page': pg, 'pagecount': pagecount}
|
||||
|
||||
except Exception as e:
|
||||
print(f"searchContent error: {e}")
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
def getTagsContent(self, pg="1"):
|
||||
"""获取标签页面内容"""
|
||||
try:
|
||||
url = f"{self.host}/tags.html"
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
data = self.getpq(response.text)
|
||||
tags = []
|
||||
|
||||
# 从标签页面提取所有标签 - 使用更宽松的选择器
|
||||
for tag_elem in data('a[href*="/tag/"]').items():
|
||||
tag_name = tag_elem.text().strip()
|
||||
tag_href = tag_elem.attr('href') or ''
|
||||
|
||||
if tag_name and tag_href and '/tag/' in tag_href and tag_name != '全部标签': # 排除标题链接
|
||||
# 处理为相对路径
|
||||
tag_id = tag_href.replace(self.host, '')
|
||||
if not tag_id.startswith('/'):
|
||||
tag_id = '/' + tag_id
|
||||
|
||||
tags.append({
|
||||
'vod_id': tag_id,
|
||||
'vod_name': f"🏷️ {tag_name}",
|
||||
'vod_pic': '',
|
||||
'vod_remarks': '标签',
|
||||
'vod_tag': 'tag',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
|
||||
print(f"找到 {len(tags)} 个标签")
|
||||
|
||||
# 分页处理 - 标签页面通常不需要分页
|
||||
result = {}
|
||||
result['list'] = tags
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 1 # 标签页面通常只有一页
|
||||
result['limit'] = 999
|
||||
result['total'] = len(tags)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"getTagsContent error: {e}")
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id
|
||||
p = 1
|
||||
if self.isVideoFormat(url):
|
||||
if '.m3u8' in url:
|
||||
url = self.proxy(url)
|
||||
p = 0
|
||||
self.log(f"播放请求: parse={p}, url={url}")
|
||||
print(f"播放请求: parse={p}, url={url}")
|
||||
return {'parse': p, 'url': url, 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
try:
|
||||
if param.get('type') == 'img':
|
||||
img_url = self.d64(param['url'])
|
||||
if not img_url.startswith(('http://', 'https://')):
|
||||
if img_url.startswith('/'):
|
||||
img_url = f"{self.host}{img_url}"
|
||||
else:
|
||||
img_url = f"{self.host}/{img_url}"
|
||||
|
||||
res = requests.get(img_url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
return [200, res.headers.get('Content-Type', 'image/jpeg'), res.content]
|
||||
elif param.get('type') == 'm3u8':
|
||||
return self.m3Proxy(param['url'])
|
||||
else:
|
||||
return self.tsProxy(param['url'])
|
||||
except Exception as e:
|
||||
print(f"localProxy error: {e}")
|
||||
return [500, "text/plain", f"Proxy error: {str(e)}".encode()]
|
||||
|
||||
def proxy(self, data, type='m3u8'):
|
||||
if data and len(self.proxies):
|
||||
return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}"
|
||||
else:
|
||||
return data
|
||||
|
||||
def m3Proxy(self, url):
|
||||
try:
|
||||
url = self.d64(url)
|
||||
ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False)
|
||||
data = ydata.content.decode('utf-8')
|
||||
if ydata.headers.get('Location'):
|
||||
url = ydata.headers['Location']
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8')
|
||||
lines = data.strip().split('\n')
|
||||
last_r = url[:url.rfind('/')]
|
||||
parsed_url = urlparse(url)
|
||||
durl = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
iskey = True
|
||||
for index, string in enumerate(lines):
|
||||
if iskey and 'URI' in string:
|
||||
pattern = r'URI="([^"]*)"'
|
||||
match = re.search(pattern, string)
|
||||
if match:
|
||||
lines[index] = re.sub(pattern, f'URI="{self.proxy(match.group(1), "mkey")}"', string)
|
||||
iskey = False
|
||||
continue
|
||||
if '#EXT' not in string:
|
||||
if 'http' not in string:
|
||||
domain = last_r if string.count('/') < 2 else durl
|
||||
string = domain + ('' if string.startswith('/') else '/') + string
|
||||
lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0])
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
except Exception as e:
|
||||
print(f"m3Proxy error: {e}")
|
||||
return [500, "text/plain", f"m3u8 proxy error: {str(e)}".encode()]
|
||||
|
||||
def tsProxy(self, url):
|
||||
try:
|
||||
url = self.d64(url)
|
||||
data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True)
|
||||
return [200, data.headers.get('Content-Type', 'video/mp2t'), data.content]
|
||||
except Exception as e:
|
||||
print(f"tsProxy error: {e}")
|
||||
return [500, "text/plain", f"ts proxy error: {str(e)}".encode()]
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64编码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解码错误: {str(e)}")
|
||||
return ""
|
||||
|
||||
def get_working_host(self):
|
||||
"""Get working host from known dynamic URLs"""
|
||||
dynamic_urls = [
|
||||
'https://kanliao25.com//',
|
||||
'https://kanliao7.org/',
|
||||
'https://kanliao7.net/',
|
||||
'https://kanliao14.com/'
|
||||
]
|
||||
|
||||
for url in dynamic_urls:
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10)
|
||||
if response.status_code == 200:
|
||||
data = self.getpq(response.text)
|
||||
articles = data('#index article a, #archive article a')
|
||||
if len(articles) > 0:
|
||||
self.log(f"选用可用站点: {url}")
|
||||
print(f"选用可用站点: {url}")
|
||||
return url
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
self.log(f"未检测到可用站点,回退: {dynamic_urls[0]}")
|
||||
print(f"未检测到可用站点,回退: {dynamic_urls[0]}")
|
||||
return dynamic_urls[0]
|
||||
|
||||
def getlist(self, data, tid=''):
|
||||
videos = []
|
||||
for k in data.items():
|
||||
a = k.attr('href')
|
||||
b = k('h2').text() or k('.post-card-title').text() or k('.entry-title').text() or k.text()
|
||||
c = k('span[itemprop="datePublished"]').text() or k('.post-meta, .entry-meta, time, .post-card-info').text()
|
||||
|
||||
# 过滤广告:检查是否包含"热搜HOT"标志
|
||||
if self.is_advertisement(k):
|
||||
print(f"过滤广告: {b}")
|
||||
continue
|
||||
|
||||
if a and b and b.strip():
|
||||
# 处理相对路径
|
||||
if not a.startswith('http'):
|
||||
if a.startswith('/'):
|
||||
vod_id = a
|
||||
else:
|
||||
vod_id = f'/{a}'
|
||||
else:
|
||||
vod_id = a
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': b.replace('\n', ' ').strip(),
|
||||
'vod_pic': self.get_article_img(k),
|
||||
'vod_remarks': c.strip() if c else '',
|
||||
'vod_tag': '',
|
||||
'style': {"type": "rect", "ratio": 1.33}
|
||||
})
|
||||
return videos
|
||||
|
||||
def is_advertisement(self, article_elem):
|
||||
"""判断是否为广告(包含热搜HOT标志)"""
|
||||
# 检查.wraps元素是否包含"热搜HOT"文本
|
||||
hot_elements = article_elem.find('.wraps')
|
||||
for elem in hot_elements.items():
|
||||
if '热搜HOT' in elem.text():
|
||||
return True
|
||||
|
||||
# 检查标题是否包含广告关键词
|
||||
title = article_elem('h2').text() or article_elem('.post-card-title').text() or ''
|
||||
ad_keywords = ['热搜HOT', '手机链接', 'DNS设置', '修改DNS', 'WIFI设置']
|
||||
if any(keyword in title for keyword in ad_keywords):
|
||||
return True
|
||||
|
||||
# 检查背景颜色是否为广告特有的渐变背景
|
||||
style = article_elem.attr('style') or ''
|
||||
if 'background:' in style and any(gradient in style for gradient in ['-webkit-linear-gradient', 'linear-gradient']):
|
||||
# 进一步检查是否包含特定的广告颜色组合
|
||||
ad_gradients = ['#ec008c,#fc6767', '#ffe259,#ffa751']
|
||||
if any(gradient in style for gradient in ad_gradients):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_article_img(self, article_elem):
|
||||
"""从文章元素中提取图片,多种方式尝试"""
|
||||
# 方式1: 从script标签中提取loadBannerDirect
|
||||
script_text = article_elem('script').text()
|
||||
if script_text:
|
||||
match = re.search(r"loadBannerDirect\('([^']+)'", script_text)
|
||||
if match:
|
||||
url = match.group(1)
|
||||
if not url.startswith(('http://', 'https://')):
|
||||
if url.startswith('/'):
|
||||
url = f"{self.host}{url}"
|
||||
else:
|
||||
url = f"{self.host}/{url}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img"
|
||||
|
||||
# 方式2: 从背景图片中提取
|
||||
bg_elem = article_elem.find('.blog-background')
|
||||
if bg_elem:
|
||||
style = bg_elem.attr('style') or ''
|
||||
bg_match = re.search(r'background-image:\s*url\(["\']?([^"\'\)]+)["\']?\)', style)
|
||||
if bg_match:
|
||||
img_url = bg_match.group(1)
|
||||
if img_url and not img_url.startswith('data:'):
|
||||
if not img_url.startswith(('http://', 'https://')):
|
||||
if img_url.startswith('/'):
|
||||
img_url = f"{self.host}{img_url}"
|
||||
else:
|
||||
img_url = f"{self.host}/{img_url}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(img_url)}&type=img"
|
||||
|
||||
# 方式3: 从图片标签中提取
|
||||
img_elem = article_elem.find('img')
|
||||
if img_elem:
|
||||
data_src = img_elem.attr('data-src')
|
||||
if data_src:
|
||||
if not data_src.startswith(('http://', 'https://')):
|
||||
if data_src.startswith('/'):
|
||||
data_src = f"{self.host}{data_src}"
|
||||
else:
|
||||
data_src = f"{self.host}/{data_src}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(data_src)}&type=img"
|
||||
|
||||
src = img_elem.attr('src')
|
||||
if src:
|
||||
if not src.startswith(('http://', 'https://')):
|
||||
if src.startswith('/'):
|
||||
src = f"{self.host}{src}"
|
||||
else:
|
||||
src = f"{self.host}/{src}"
|
||||
return f"{self.getProxyUrl()}&url={self.e64(src)}&type=img"
|
||||
|
||||
return ''
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from urllib.parse import quote
|
||||
from Crypto.Hash import MD5
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.cookies.update(self.cookie)
|
||||
self.get_ctoken()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.youku.com'
|
||||
|
||||
shost='https://search.youku.com'
|
||||
|
||||
h5host='https://acs.youku.com'
|
||||
|
||||
ihost='https://v.youku.com'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (; Windows 10.0.26100.3194_64 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Electron/14.2.0 Safari/537.36 Node/14.17.0 YoukuDesktop/9.2.60 UOSYouku (2.0.1)-Electron(UTDID ZYmGMAAAACkDAMU8hbiMmYdd;CHANNEL official;ZREAL 0;BTYPE TM2013;BRAND TIMI;BUILDVER 9.2.60.1001)',
|
||||
'Referer': f'{host}/'
|
||||
}
|
||||
|
||||
cookie={
|
||||
"__ysuid": "17416134165380iB",
|
||||
"__aysid": "1741613416541WbD",
|
||||
"xlly_s": "1",
|
||||
"isI18n": "false",
|
||||
"cna": "bNdVIKmmsHgCAXW9W6yrQ1/s",
|
||||
"__ayft": "1741672162330",
|
||||
"__arpvid": "1741672162331FBKgrn-1741672162342",
|
||||
"__ayscnt": "1",
|
||||
"__aypstp": "1",
|
||||
"__ayspstp": "3",
|
||||
"tfstk": "gZbiib4JpG-6DqW-B98_2rwPuFrd1fTXQt3vHEp4YpJIBA3OgrWcwOi90RTOo9XVQ5tAM5NcK_CP6Ep97K2ce1XDc59v3KXAgGFLyzC11ET2n8U8yoyib67M3xL25e8gS8pbyzC1_ET4e8URWTsSnHv2uh8VTeJBgEuN3d-ELQAWuKWV36PHGpJ2uEWVTxvicLX1ewyUXYSekxMf-CxMEqpnoqVvshvP_pABOwvXjL5wKqeulm52np_zpkfCDGW9Ot4uKFIRwZtP7vP9_gfAr3KEpDWXSIfWRay-DHIc_Z-hAzkD1i5Ooi5LZ0O5YO_1mUc476YMI3R6xzucUnRlNe_zemKdm172xMwr2L7CTgIkbvndhFAVh3_YFV9Ng__52U4SQKIdZZjc4diE4EUxlFrfKmiXbBOHeP72v7sAahuTtWm78hRB1yV3tmg9bBOEhWVnq5KwOBL5."
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
categories = ["电视剧", "电影", "综艺", "动漫", "少儿", "纪录片", "文化", "亲子", "教育", "搞笑", "生活",
|
||||
"体育", "音乐", "游戏"]
|
||||
classes = [{'type_name': category, 'type_id': category} for category in categories]
|
||||
filters = {}
|
||||
self.typeid = {}
|
||||
with ThreadPoolExecutor(max_workers=len(categories)) as executor:
|
||||
tasks = {
|
||||
executor.submit(self.cf, {'type': category}, True): category
|
||||
for category in categories
|
||||
}
|
||||
|
||||
for future in as_completed(tasks):
|
||||
try:
|
||||
category = tasks[future]
|
||||
session, ft = future.result()
|
||||
filters[category] = ft
|
||||
self.typeid[category] = session
|
||||
except Exception as e:
|
||||
print(f"处理分类 {tasks[future]} 时出错: {str(e)}")
|
||||
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
vlist = []
|
||||
params={"ms_codes":"2019061000","params":"{\"debug\":0,\"gray\":0,\"pageNo\":1,\"utdid\":\"ZYmGMAAAACkDAMU8hbiMmYdd\",\"userId\":\"\",\"bizKey\":\"YOUKU_WEB\",\"appPackageKey\":\"com.youku.YouKu\",\"showNodeList\":0,\"reqSubNode\":0,\"nodeKey\":\"WEBHOME\",\"bizContext\":\"{\\\"spmA\\\":\\\"a2hja\\\"}\"}","system_info":"{\"device\":\"pcweb\",\"os\":\"pcweb\",\"ver\":\"1.0.0.0\",\"userAgent\":\"Mozilla/5.0 (; Windows 10.0.26100.3194_64 ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Electron/14.2.0 Safari/537.36 Node/14.17.0 YoukuDesktop/9.2.60 UOSYouku (2.0.1)-Electron(UTDID ZYmGMAAAACkDAMU8hbiMmYdd;CHANNEL official;ZREAL 0;BTYPE TM2013;BRAND TIMI;BUILDVER 9.2.60.1001)\",\"guid\":\"1590141704165YXe\",\"appPackageKey\":\"com.youku.pcweb\",\"young\":0,\"brand\":\"\",\"network\":\"\",\"ouid\":\"\",\"idfa\":\"\",\"scale\":\"\",\"operator\":\"\",\"resolution\":\"\",\"pid\":\"\",\"childGender\":0,\"zx\":0}"}
|
||||
data=self.getdata(f'{self.h5host}/h5/mtop.youku.columbus.home.query/1.0/',params)
|
||||
okey=list(data['data'].keys())[0]
|
||||
for i in data['data'][okey]['data']['nodes'][0]['nodes'][-1]['nodes'][0]['nodes']:
|
||||
if i.get('nodes') and i['nodes'][0].get('data'):
|
||||
i=i['nodes'][0]['data']
|
||||
if i.get('assignId'):
|
||||
vlist.append({
|
||||
'vod_id': i['assignId'],
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': i.get('vImg') or i.get('img'),
|
||||
'vod_year': i.get('mark',{}).get('data',{}).get('text'),
|
||||
'vod_remarks': i.get('summary')
|
||||
})
|
||||
return {'list': vlist}
|
||||
except Exception as e:
|
||||
print(f"处理主页视频数据时出错: {str(e)}")
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
vlist = []
|
||||
result['page'] = pg
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
pagecount = 9999
|
||||
params = {'type': tid}
|
||||
id = self.typeid[tid]
|
||||
params.update(extend)
|
||||
if pg == '1':
|
||||
id=self.cf(params)
|
||||
data=self.session.get(f'{self.host}/category/data?session={id}¶ms={quote(json.dumps(params))}&pageNo={pg}').json()
|
||||
try:
|
||||
data=data['data']['filterData']
|
||||
for i in data['listData']:
|
||||
if i.get('videoLink') and 's=' in i['videoLink']:
|
||||
vlist.append({
|
||||
'vod_id': i.get('videoLink').split('s=')[-1],
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': i.get('img'),
|
||||
'vod_year': i.get('rightTagText'),
|
||||
'vod_remarks': i.get('summary')
|
||||
})
|
||||
self.typeid[tid]=quote(json.dumps(data['session']))
|
||||
except:
|
||||
pagecount=pg
|
||||
result['list'] = vlist
|
||||
result['pagecount'] = pagecount
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
try:
|
||||
data=self.session.get(f'{self.ihost}/v_getvideo_info/?showId={ids[0]}').json()
|
||||
v=data['data']
|
||||
vod = {
|
||||
'type_name': v.get('showVideotype'),
|
||||
'vod_year': v.get('lastUpdate'),
|
||||
'vod_remarks': v.get('rc_title'),
|
||||
'vod_actor': v.get('_personNameStr'),
|
||||
'vod_content': v.get('showdesc'),
|
||||
'vod_play_from': '优酷',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
params={"biz":"new_detail_web2","videoId":v.get('vid'),"scene":"web_page","componentVersion":"3","ip":data.get('ip'),"debug":0,"utdid":"ZYmGMAAAACkDAMU8hbiMmYdd","userId":0,"platform":"pc","nextSession":"","gray":0,"source":"pcNoPrev","showId":ids[0]}
|
||||
sdata,index=self.getinfo(params)
|
||||
pdata=sdata['nodes']
|
||||
if index > len(pdata):
|
||||
batch_size = len(pdata)
|
||||
total_batches = ((index + batch_size - 1) // batch_size) - 1
|
||||
ssj = json.loads(sdata['data']['session'])
|
||||
with ThreadPoolExecutor(max_workers=total_batches) as executor:
|
||||
futures = []
|
||||
for batch in range(total_batches):
|
||||
start = batch_size + 1 + (batch * batch_size)
|
||||
end = start + batch_size - 1
|
||||
next_session = ssj.copy()
|
||||
next_session.update({
|
||||
"itemStartStage": start,
|
||||
"itemEndStage": min(end, index)
|
||||
})
|
||||
current_params = params.copy()
|
||||
current_params['nextSession'] = json.dumps(next_session)
|
||||
futures.append((start, executor.submit(self.getvinfo, current_params)))
|
||||
futures.sort(key=lambda x: x[0])
|
||||
|
||||
for _, future in futures:
|
||||
try:
|
||||
result = future.result()
|
||||
pdata.extend(result['nodes'])
|
||||
except Exception as e:
|
||||
print(f"Error fetching data: {str(e)}")
|
||||
vod['vod_play_url'] = '#'.join([f"{i['data'].get('title')}${i['data']['action'].get('value')}" for i in pdata])
|
||||
return {'list': [vod]}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {'list': [{'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'呜呜呜${self.host}'}]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data=self.session.get(f'{self.shost}/api/search?pg={pg}&keyword={key}').json()
|
||||
vlist = []
|
||||
for i in data['pageComponentList']:
|
||||
if i.get('commonData') and (i['commonData'].get('showId') or i['commonData'].get('realShowId')):
|
||||
i=i['commonData']
|
||||
vlist.append({
|
||||
'vod_id': i.get('showId') or i.get('realShowId'),
|
||||
'vod_name': i['titleDTO'].get('displayName'),
|
||||
'vod_pic': i['posterDTO'].get('vThumbUrl'),
|
||||
'vod_year': i.get('feature'),
|
||||
'vod_remarks': i.get('updateNotice')
|
||||
})
|
||||
return {'list': vlist, 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'jx':1,'parse': 1, 'url': f"{self.ihost}/video?vid={id}", 'header': ''}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def cf(self,params,b=False):
|
||||
response = self.session.get(f'{self.host}/category/data?params={quote(json.dumps(params))}&optionRefresh=1&pageNo=1').json()
|
||||
data=response['data']['filterData']
|
||||
session=quote(json.dumps(data['session']))
|
||||
if b:
|
||||
return session,self.get_filter_data(data['filter']['filterData'][1:])
|
||||
return session
|
||||
|
||||
def process_key(self, key):
|
||||
if '_' not in key:
|
||||
return key
|
||||
parts = key.split('_')
|
||||
result = parts[0]
|
||||
for part in parts[1:]:
|
||||
if part:
|
||||
result += part[0].upper() + part[1:]
|
||||
return result
|
||||
|
||||
def get_filter_data(self, data):
|
||||
result = []
|
||||
try:
|
||||
for item in data:
|
||||
if not item.get('subFilter'):
|
||||
continue
|
||||
first_sub = item['subFilter'][0]
|
||||
if not first_sub.get('filterType'):
|
||||
continue
|
||||
filter_item = {
|
||||
'key': self.process_key(first_sub['filterType']),
|
||||
'name': first_sub['title'],
|
||||
'value': []
|
||||
}
|
||||
for sub in item['subFilter']:
|
||||
if 'value' in sub:
|
||||
filter_item['value'].append({
|
||||
'n': sub['title'],
|
||||
'v': sub['value']
|
||||
})
|
||||
if filter_item['value']:
|
||||
result.append(filter_item)
|
||||
|
||||
except Exception as e:
|
||||
print(f"处理筛选数据时出错: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
def get_ctoken(self):
|
||||
data=self.session.get(f'{self.h5host}/h5/mtop.ykrec.recommendservice.recommend/1.0/?jsv=2.6.1&appKey=24679788')
|
||||
|
||||
def md5(self,t,text):
|
||||
h = MD5.new()
|
||||
token=self.session.cookies.get('_m_h5_tk').split('_')[0]
|
||||
data=f"{token}&{t}&24679788&{text}"
|
||||
h.update(data.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def getdata(self, url, params, recursion_count=0, max_recursion=3):
|
||||
data = json.dumps(params)
|
||||
t = int(time.time() * 1000)
|
||||
jsdata = {
|
||||
'appKey': '24679788',
|
||||
't': t,
|
||||
'sign': self.md5(t, data),
|
||||
'data': data
|
||||
}
|
||||
response = self.session.get(url, params=jsdata)
|
||||
if '令牌过期' in response.text:
|
||||
if recursion_count >= max_recursion:
|
||||
raise Exception("达到最大递归次数,无法继续请求")
|
||||
self.get_ctoken()
|
||||
return self.getdata(url, params, recursion_count + 1, max_recursion)
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
def getvinfo(self,params):
|
||||
body = {
|
||||
"ms_codes": "2019030100",
|
||||
"params": json.dumps(params),
|
||||
"system_info": "{\"os\":\"iku\",\"device\":\"iku\",\"ver\":\"9.2.9\",\"appPackageKey\":\"com.youku.iku\",\"appPackageId\":\"pcweb\"}"
|
||||
}
|
||||
data = self.getdata(f'{self.h5host}/h5/mtop.youku.columbus.gateway.new.execute/1.0/', body)
|
||||
okey = list(data['data'].keys())[0]
|
||||
i = data['data'][okey]['data']
|
||||
return i
|
||||
|
||||
def getinfo(self,params):
|
||||
i = self.getvinfo(params)
|
||||
jdata=i['nodes'][0]['nodes'][3]
|
||||
info=i['data']['extra']['episodeTotal']
|
||||
if i['data']['extra']['showCategory'] in ['电影','游戏']:
|
||||
jdata = i['nodes'][0]['nodes'][4]
|
||||
return jdata,info
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author : Doubebly
|
||||
# @Time : 2025/11/16 22:12
|
||||
# @file : 兄弟影视.min.py
|
||||
#!/usr/bin/python3
|
||||
|
||||
R='vod_pic'
|
||||
Q='span.public-list-prb.hide.ft2'
|
||||
P='data-src'
|
||||
O=print
|
||||
L='vod_remarks'
|
||||
K='vod_name'
|
||||
J='vod_id'
|
||||
I='href'
|
||||
G='a'
|
||||
F='jx'
|
||||
E='parse'
|
||||
D='type_name'
|
||||
B='list'
|
||||
A=''
|
||||
import hashlib as Z,json,re,sys,time as M,requests as C
|
||||
from urllib import parse as N
|
||||
from pyquery import PyQuery as H
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider as S
|
||||
class Spider(S):
|
||||
def __init__(A):super().__init__();A.debug=False;A.name='兄弟影视';A.error_play_url='https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4';A.home_url='https://www.brovods.top';A.headers={'User-Agent':'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36'}
|
||||
def getName(A):return A.name
|
||||
def init(A,extend='{}'):A.extend=extend
|
||||
def getDependence(A):return[]
|
||||
def isVideoFormat(A,url):0
|
||||
def manualVideoCheck(A):0
|
||||
def liveContent(B,url):return A
|
||||
def homeContent(G,filter):A='type_id';C={'class':[{A:'Movies',D:'电影'},{A:'TV',D:'剧集'},{A:'Shows',D:'综艺'},{A:'Anime',D:'动漫'},{A:'Snaps',D:'短剧'},{A:'Documentaries',D:'纪录片'}],'filters':{},B:[],E:0,F:0};return C
|
||||
def homeVideoContent(C):A={B:[],E:0,F:0};return A
|
||||
def categoryContent(D,cid,page,filter,ext):
|
||||
M={B:[],E:0,F:0};N=D.home_url+f"/show/{cid}--------{page}---/";O=C.get(N,headers=D.headers);S=H(O.text.encode());T=S('div.public-list-box.public-pic-b div.public-list-div.public-list-bj')
|
||||
for A in T.items():U=A(G).attr(I);V=A(G).attr('title');W=A('img').attr(P);X=A(Q).text();M[B].append({J:U,K:V,R:W,L:X})
|
||||
return M
|
||||
def detailContent(N,did):
|
||||
U='$$$';O={B:[],E:0,F:0};P=did[0];M=N.home_url+P;V=C.get(M,headers=N.headers);Q=H(V.text.encode());W=[A.text()for A in Q('div.anthology-tab div.swiper-wrapper a').items()];X=Q('ul.anthology-list-play');R=[]
|
||||
for Y in X.items():
|
||||
S=[]
|
||||
for T in Y('li').items():Z=T(G).text();M=T(G).attr(I);S.append(f"{Z}${M}")
|
||||
R.append('#'.join(S))
|
||||
O[B].append({D:A,J:P,K:A,L:A,'vod_year':A,'vod_area':A,'vod_actor':A,'vod_director':A,'vod_content':A,'vod_play_from':U.join(W),'vod_play_url':U.join(R)});return O
|
||||
def searchContent(D,key,quick,page='1'):
|
||||
M='div.thumb-content div.thumb-txt.cor4.hide a';G={B:[],E:0,F:0};N=D.home_url+f"/ss/{key}----------{page}---/";O=C.get(N,headers=D.headers);S=H(O.text.encode());T=S('div.public-list-box.search-box')
|
||||
for A in T.items():U=A(M).attr(I);V=A(M).text();W=A('img.gen-movie-img').attr(P);X=A(Q).text();G[B].append({J:U,K:V,R:W,L:X})
|
||||
return G
|
||||
def playerContent(B,flag,pid,vipFlags):
|
||||
Y='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';X='https://play.brovods.top';W='user-agent';V='origin';U='header';G='url';D={G:B.error_play_url,E:0,F:0,U:{}};a=B.home_url+pid;H=C.get(a,headers=B.headers);J=re.search('var player_aaaa=(.*?)</script>',H.text)
|
||||
if J:
|
||||
K=json.loads(J.group(1));b=N.quote(K[G]);c=B.home_url+K['link_next'];d='https://play.brovods.top?url='+b+'&next='+N.quote(c,safe=A);I=C.get(d,headers=B.headers);L=re.search('"url":"(.*?)",',I.text);P=re.search('"pbgjz":"(.*?)",',I.text);Q=re.search('"dmkey":"(.*?)",',I.text)
|
||||
if L and P and Q:
|
||||
e=L.group(1);f=P.group(1);g=Q.group(1);h={'accept':'application/json, text/javascript, */*; q=0.01','accept-language':'zh-CN,zh;q=0.9','content-type':'application/x-www-form-urlencoded; charset=UTF-8',V:X,W:Y};R=M.time();S=M.localtime(R);i=S.tm_min*60+S.tm_sec;j=int(R-i);k=f"{j}cnmdhb";l={G:e,'pbgjz':f,'dmkey':g,'key':Z.sha256(k.encode('utf-8')).hexdigest()};H=C.post('https://play.brovods.top/JX',headers=h,json=l);T=H.json()
|
||||
if T['code']==200:D[G]=T['cnmdhb'];m={W:Y,V:X};D[U]=m
|
||||
O(D);return D
|
||||
def localProxy(A,params):0
|
||||
def destroy(A):return'正在Destroy'
|
||||
if __name__=='__main__':0
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
作者 丢丢喵推荐 🚓 内容均从互联网收集而来 仅供交流学习使用 版权归原创者所有 如侵犯了您的权益 请通知作者 将及时删除侵权内容
|
||||
====================Diudiumiao====================
|
||||
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from Crypto.Util.Padding import pad
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://djw1.com"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {"class": []}
|
||||
|
||||
detail = requests.get(url=xurl + "/all/", headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('section', class_="container items")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('li')
|
||||
|
||||
for vod in vods:
|
||||
|
||||
id = vod.find('a')['href']
|
||||
|
||||
name = vod.text.strip()
|
||||
|
||||
result["class"].append({"type_id": id, "type_name": "" + name})
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
url = f'{cid}page/{str(page)}/'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('section', class_="container items")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('li')
|
||||
|
||||
for vod in vods:
|
||||
|
||||
name = vod.find('img')['alt']
|
||||
|
||||
ids = vod.find('a', class_="image-line")
|
||||
id = ids['href']
|
||||
|
||||
pic = vod.find('img')['src']
|
||||
|
||||
remark = self.extract_middle_text(str(vod), 'class="remarks light">', '<', 0)
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": '▶️' + remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
xianlu = ''
|
||||
bofang = ''
|
||||
|
||||
if 'http' not in did:
|
||||
did = xurl + did
|
||||
|
||||
res = requests.get(url=did, headers=headerx)
|
||||
res.encoding = "utf-8"
|
||||
res = res.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
url = 'https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/1732707176882/jiduo.txt'
|
||||
response = requests.get(url)
|
||||
response.encoding = 'utf-8'
|
||||
code = response.text
|
||||
name = self.extract_middle_text(code, "s1='", "'", 0)
|
||||
Jumps = self.extract_middle_text(code, "s2='", "'", 0)
|
||||
|
||||
content = '集多为您介绍剧情📢' + self.extract_middle_text(res,'class="info-detail">','<', 0)
|
||||
|
||||
remarks = self.extract_middle_text(res, 'class="info-mark">', '<', 0)
|
||||
|
||||
year = self.extract_middle_text(res, 'class="info-addtime">', '<', 0)
|
||||
|
||||
if name not in content:
|
||||
bofang = Jumps
|
||||
xianlu = '1'
|
||||
else:
|
||||
soups = doc.find('div', class_="ep-list-items")
|
||||
|
||||
soup = soups.find_all('a')
|
||||
|
||||
for sou in soup:
|
||||
|
||||
id = sou['href']
|
||||
|
||||
name = sou.text.strip()
|
||||
|
||||
bofang = bofang + name + '$' + id + '#'
|
||||
|
||||
bofang = bofang[:-1]
|
||||
|
||||
xianlu = '专线'
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_remarks": remarks,
|
||||
"vod_year": year,
|
||||
"vod_content": content,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
res = requests.get(url=id, headers=headerx)
|
||||
res.encoding = "utf-8"
|
||||
res = res.text
|
||||
|
||||
url = self.extract_middle_text(res, '"wwm3u8":"', '"', 0).replace('\\', '')
|
||||
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = url
|
||||
result["header"] = headerx
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
url = f'{xurl}/search/{key}/page/{str(page)}/'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('section', class_="container items")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('li')
|
||||
|
||||
for vod in vods:
|
||||
|
||||
name = vod.find('img')['alt']
|
||||
|
||||
ids = vod.find('a', class_="image-line")
|
||||
id = ids['href']
|
||||
|
||||
pic = vod.find('img')['src']
|
||||
|
||||
remark = self.extract_middle_text(str(vod), 'class="remarks light">', '<', 0)
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": '▶️' + remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.name = "剧透社"
|
||||
self.host = "https://1.star2.cn"
|
||||
self.timeout = 5000
|
||||
self.limit = 20
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
}
|
||||
self.default_image = "https://images.gamedog.cn/gamedog/imgfile/20241205/05105843u5j9.png"
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend=""):
|
||||
print(f"============{extend}============")
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [
|
||||
{"type_name": "国剧", "type_id": "ju"},
|
||||
{"type_name": "电影", "type_id": "mv"},
|
||||
{"type_name": "动漫", "type_id": "dm"},
|
||||
{"type_name": "短剧", "type_id": "dj"},
|
||||
{"type_name": "综艺", "type_id": "zy"},
|
||||
{"type_name": "韩日", "type_id": "rh"},
|
||||
{"type_name": "英美", "type_id": "ym"},
|
||||
{"type_name": "外剧", "type_id": "wj"}
|
||||
]
|
||||
}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
url = f"{self.host}/{tid}/" if pg == 1 else f"{self.host}/{tid}/?page={pg}"
|
||||
|
||||
try:
|
||||
rsp = self.fetch(url, headers=self.headers, timeout=self.timeout)
|
||||
if rsp:
|
||||
videos = self._parse_video_list(rsp.text)
|
||||
result.update({
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': self.limit,
|
||||
'total': 999999
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Category parse error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _parse_video_list(self, html_text):
|
||||
videos = []
|
||||
|
||||
def build_full_url(href):
|
||||
if href.startswith("http"):
|
||||
return href
|
||||
return f"{self.host}{href}" if href.startswith("/") else f"{self.host}/{href}"
|
||||
|
||||
try:
|
||||
pattern = r'<li[^>]*>.*?<a[^>]*href="([^"]*)"[^>]*class="main"[^>]*>(.*?)</a>.*?</li>'
|
||||
for match in re.finditer(pattern, html_text, re.S):
|
||||
href = match.group(1)
|
||||
name = match.group(2).strip()
|
||||
|
||||
if href and name and href.startswith("/"):
|
||||
cleaned_name = re.sub(r'^【[^】]*】', '', name).strip()
|
||||
final_name = cleaned_name if cleaned_name else name
|
||||
videos.append({
|
||||
"vod_id": build_full_url(href),
|
||||
"vod_name": final_name,
|
||||
"vod_pic": self.default_image,
|
||||
"vod_remarks": "",
|
||||
"vod_content": final_name
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Parse video list error: {e}")
|
||||
|
||||
return videos
|
||||
|
||||
def detailContent(self, array):
|
||||
result = {'list': []}
|
||||
if array:
|
||||
try:
|
||||
vod_id = array[0]
|
||||
detail_url = vod_id if vod_id.startswith("http") else f"{self.host}{vod_id}"
|
||||
rsp = self.fetch(detail_url, headers=self.headers, timeout=self.timeout)
|
||||
if rsp:
|
||||
vod = self._parse_detail_page(rsp.text, detail_url)
|
||||
if vod:
|
||||
result['list'] = [vod]
|
||||
except Exception as e:
|
||||
print(f"Detail parse error: {e}")
|
||||
return result
|
||||
|
||||
def _parse_detail_page(self, html_text, detail_url):
|
||||
try:
|
||||
title_match = re.search(r'<h1[^>]*>(.*?)</h1>', html_text, re.S)
|
||||
title = title_match.group(1).strip() if title_match else "未知标题"
|
||||
title = re.sub(r'^【[^】]+】', '', title).strip() or "未知标题"
|
||||
baidu_links = []
|
||||
quark_links = []
|
||||
|
||||
link_pattern = r'<a[^>]*href="([^"]*)"[^>]*>.*?</a>'
|
||||
for match in re.finditer(link_pattern, html_text, re.S):
|
||||
href = match.group(1)
|
||||
if href:
|
||||
if "pan.baidu.com" in href:
|
||||
baidu_links.append(href)
|
||||
elif "pan.quark.cn" in href:
|
||||
quark_links.append(href)
|
||||
|
||||
play_links = baidu_links + quark_links
|
||||
play_from = "剧透社" if play_links else "无资源"
|
||||
|
||||
play_url_parts = []
|
||||
for link in play_links:
|
||||
if "pan.baidu.com" in link:
|
||||
play_url_parts.append(f"百度${link}")
|
||||
else:
|
||||
play_url_parts.append(f"夸克${link}")
|
||||
|
||||
play_url = "#".join(play_url_parts) or "暂无资源$#"
|
||||
|
||||
return {
|
||||
"vod_id": detail_url,
|
||||
"vod_name": title,
|
||||
"vod_pic": self.default_image,
|
||||
"vod_content": title,
|
||||
"vod_remarks": "",
|
||||
"vod_play_from": play_from,
|
||||
"vod_play_url": play_url
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Parse detail page error: {e}")
|
||||
return {
|
||||
"vod_id": detail_url,
|
||||
"vod_name": "未知标题",
|
||||
"vod_pic": self.default_image,
|
||||
"vod_content": f"加载详情页失败:{str(e)}",
|
||||
"vod_remarks": "",
|
||||
"vod_play_from": "无资源",
|
||||
"vod_play_url": "暂无资源$#"
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, pg):
|
||||
result = {'list': []}
|
||||
try:
|
||||
url = f"{self.host}/search/?keyword={key}"
|
||||
rsp = self.fetch(url, headers=self.headers, timeout=self.timeout)
|
||||
if rsp:
|
||||
result['list'] = self._parse_video_list(rsp.text)
|
||||
except Exception as e:
|
||||
print(f"Search error: {e}")
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if id.startswith("push://"):
|
||||
return {"parse": 0, "playUrl": "", "url": id, "header": ""}
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": f"push://{id}",
|
||||
"header": json.dumps(self.headers)
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {"list": []}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return False
|
||||
|
||||
def localProxy(self, url, param):
|
||||
return {"parse": 0, "playUrl": "", "url": url}
|
||||
|
||||
def manualVideoCheck(self, url):
|
||||
return {"parse": 0, "playUrl": "", "url": url}
|
||||
|
||||
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
作者 丢丢喵 🚓 内容均从互联网收集而来 仅供交流学习使用 版权归原创者所有 如侵犯了您的权益 请通知作者 将及时删除侵权内容
|
||||
====================Diudiumiao====================
|
||||
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from Crypto.Util.Padding import pad
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import datetime
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://nnyy.la"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def split_id_by_at(self, id):
|
||||
return id.split("@")
|
||||
|
||||
def split_first_part_by_comma(self, first_part):
|
||||
return first_part.split(",")
|
||||
|
||||
def parse_url_dictionary(self, response_text):
|
||||
pattern = r'urlDictionary\[(\d+)\]\[(\d+)\]\s*=\s*"([^"]+)"'
|
||||
matches = re.findall(pattern, response_text)
|
||||
url_dictionary = {}
|
||||
for key1, key2, value in matches:
|
||||
key1_int = int(key1)
|
||||
key2_int = int(key2)
|
||||
if key1_int not in url_dictionary:
|
||||
url_dictionary[key1_int] = {}
|
||||
url_dictionary[key1_int][key2_int] = value
|
||||
return url_dictionary
|
||||
|
||||
def get_url_from_dictionary(self, url_dictionary, indices):
|
||||
primary_index = int(indices[0].strip())
|
||||
secondary_index = int(indices[1].strip())
|
||||
return url_dictionary[primary_index][secondary_index]
|
||||
|
||||
def decrypt_url(self, encrypted_hex, key="i_love_you"):
|
||||
encrypted_bytes = bytes.fromhex(encrypted_hex)
|
||||
s = list(range(256))
|
||||
j = 0
|
||||
key_bytes = key.encode('utf-8')
|
||||
key_length = len(key_bytes)
|
||||
for i in range(256):
|
||||
j = (j + s[i] + key_bytes[i % key_length]) % 256
|
||||
s[i], s[j] = s[j], s[i]
|
||||
i = 0
|
||||
j = 0
|
||||
decrypted_bytes = bytearray(len(encrypted_bytes))
|
||||
for k in range(len(encrypted_bytes)):
|
||||
i = (i + 1) % 256
|
||||
j = (j + s[i]) % 256
|
||||
s[i], s[j] = s[j], s[i]
|
||||
keystream_byte = s[(s[i] + s[j]) % 256]
|
||||
decrypted_bytes[k] = encrypted_bytes[k] ^ keystream_byte
|
||||
return decrypted_bytes.decode('utf-8')
|
||||
|
||||
def _extract_play_sources(self, soups):
|
||||
xianlu = ''
|
||||
for item in soups:
|
||||
vods = item.find_all('dt')
|
||||
for sou in vods:
|
||||
name = sou.text.strip()
|
||||
xianlu = xianlu + name + '$$$'
|
||||
xianlu = xianlu[:-3]
|
||||
return xianlu
|
||||
|
||||
def _extract_play_urls(self, soups1, did):
|
||||
bofang = ''
|
||||
for item in soups1:
|
||||
vods1 = item.find_all('a')
|
||||
for sou1 in vods1:
|
||||
id1 = sou1['onclick']
|
||||
numbers = re.findall(r'\((.*?)\)', id1)[0] if re.findall(r'\((.*?)\)', id1) else ""
|
||||
id = f"{numbers}@{did}"
|
||||
name = sou1.text.strip()
|
||||
bofang = bofang + name + '$' + id + '#'
|
||||
bofang = bofang[:-1] + '$$$'
|
||||
bofang = bofang[:-3]
|
||||
return bofang
|
||||
|
||||
def _extract_content(self, res):
|
||||
content_raw = self.extract_middle_text(res, '剧情简介:<span>', '<', 0).replace('\n', '')
|
||||
return '😸丢丢为您介绍剧情📢' + (content_raw if content_raw is not None else "暂无剧情介绍")
|
||||
|
||||
def _extract_director(self, res):
|
||||
director_raw = self.extract_middle_text(res, '导演:', '</div>', 1, 'href=".*?">(.*?)</a>')
|
||||
return director_raw if director_raw is not None and director_raw.strip() != "" else "暂无导演介绍"
|
||||
|
||||
def _extract_actor(self, res):
|
||||
actor_raw = self.extract_middle_text(res, '主演:', '</div>', 1, 'href=".*?">(.*?)</a>')
|
||||
return actor_raw if actor_raw is not None and actor_raw.strip() != "" else "暂无主演介绍"
|
||||
|
||||
def _extract_remarks(self, res):
|
||||
remarks_raw = self.extract_middle_text(res, '类型:', '</div>', 1, 'href=".*?">(.*?)</a>')
|
||||
return remarks_raw if remarks_raw is not None and remarks_raw.strip() != "" else "暂无类型介绍"
|
||||
|
||||
def _extract_area(self, res):
|
||||
area_raw = self.extract_middle_text(res, '制片国家/地区:', '</div>', 1, 'href=".*?">(.*?)</a>')
|
||||
return area_raw if area_raw is not None and area_raw.strip() != "" else "暂无国家/地区介绍"
|
||||
|
||||
def _extract_year(self, doc):
|
||||
years = doc.find('h1', class_="product-title")
|
||||
year = years.text.strip() if years else '暂无年份介绍'
|
||||
return year.replace('\n', '打分:')
|
||||
|
||||
def _extract_video_items(self, vods):
|
||||
videos = []
|
||||
for vod in vods:
|
||||
name = vod.find('img')['alt']
|
||||
ids = vod.find('a', class_="thumbnail")
|
||||
id = ids['href']
|
||||
pic = vod.find('img')['data-src']
|
||||
remarks = vod.find('div', class_="note")
|
||||
remark = remarks.text.strip()
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
videos.append(video)
|
||||
return videos
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {"class": []}
|
||||
|
||||
detail = requests.get(url=xurl, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('div', class_="nav")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('a')
|
||||
|
||||
for vod in vods:
|
||||
|
||||
name = vod.text.strip()
|
||||
|
||||
skip_names = ["首页"]
|
||||
if name in skip_names:
|
||||
continue
|
||||
|
||||
id = vod['href'].replace('/', '')
|
||||
|
||||
result["class"].append({"type_id": id, "type_name": name})
|
||||
|
||||
result["class"].append({"type_id": "duanju", "type_name": "短剧"})
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
|
||||
detail = requests.get(url=xurl, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find('div', class_="bd")
|
||||
|
||||
vods = soups.find_all('li')
|
||||
|
||||
videos = self._extract_video_items(vods)
|
||||
|
||||
result = {'list': videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
url = f'{xurl}/{cid}/?page={str(page)}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('div', class_="lists-content")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('li')
|
||||
|
||||
videos = self._extract_video_items(vods)
|
||||
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if 'http' not in did:
|
||||
did = xurl + did
|
||||
|
||||
detail = requests.get(url=did, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
content = self._extract_content(res)
|
||||
|
||||
director = self._extract_director(res)
|
||||
|
||||
actor = self._extract_actor(res)
|
||||
|
||||
remarks = self._extract_remarks(res)
|
||||
|
||||
area = self._extract_area(res)
|
||||
|
||||
year = self._extract_year(doc)
|
||||
|
||||
soups = doc.find_all('div', class_="playlists")
|
||||
xianlu = self._extract_play_sources(soups)
|
||||
|
||||
soups1 = doc.find_all('ul', class_="sort-list")
|
||||
bofang = self._extract_play_urls(soups1, did)
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_director": director,
|
||||
"vod_actor": actor,
|
||||
"vod_remarks": remarks,
|
||||
"vod_year": year,
|
||||
"vod_area": area,
|
||||
"vod_content": content,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
fenge = self.split_id_by_at(id)
|
||||
|
||||
fenge1 = self.split_first_part_by_comma(fenge[0])
|
||||
|
||||
detail = requests.get(url=fenge[1], headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
|
||||
url_dictionary = self.parse_url_dictionary(res)
|
||||
|
||||
result = self.get_url_from_dictionary(url_dictionary, fenge1)
|
||||
|
||||
url = self.decrypt_url(result)
|
||||
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["playUrl"] = ''
|
||||
result["url"] = url
|
||||
result["header"] = headerx
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, pg):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
url = f'{xurl}/search?wd={key}'
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('div', class_="lists-content")
|
||||
|
||||
for item in soups:
|
||||
vods = item.find_all('li')
|
||||
|
||||
videos = self._extract_video_items(vods)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 1
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import json
|
||||
import urllib.parse
|
||||
import re
|
||||
from lxml import etree
|
||||
from urllib.parse import urljoin
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "奇优影院"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"电影": "1",
|
||||
"电视剧": "2",
|
||||
"动漫": "3",
|
||||
"综艺": "4",
|
||||
"午夜": "6"
|
||||
}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]
|
||||
result['class'] = classes
|
||||
|
||||
filters = {
|
||||
"1": [{"key": "by", "name": "排序", "value": [{"n": "按时间", "v": "time"}, {"n": "按人气", "v": "hit"}]}],
|
||||
"2": [{"key": "by", "name": "排序", "value": [{"n": "按时间", "v": "time"}, {"n": "按人气", "v": "hit"}]}],
|
||||
"3": [{"key": "by", "name": "排序", "value": [{"n": "按时间", "v": "time"}, {"n": "按人气", "v": "hit"}]}],
|
||||
"4": [{"key": "by", "name": "排序", "value": [{"n": "按时间", "v": "time"}, {"n": "按人气", "v": "hit"}]}],
|
||||
"6": [{"key": "by", "name": "排序", "value": [{"n": "按时间", "v": "time"}, {"n": "按人气", "v": "hit"}]}]
|
||||
}
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
rsp = self.fetch("http://qiyoudy5.com/")
|
||||
root = self.parse_html(rsp.content)
|
||||
if not root:
|
||||
return {'list': []}
|
||||
|
||||
videos = []
|
||||
# 轮播图
|
||||
for a in root.xpath("//div[contains(@class,'carousel')]//a[contains(@class,'stui-vodlist__thumb')]"):
|
||||
try:
|
||||
name = a.xpath(".//span[@class='pic-text text-center']/text()")[0].strip() if a.xpath(".//span[@class='pic-text text-center']/text()") else a.xpath("./@title")[0] if a.xpath("./@title") else "未知"
|
||||
style = a.xpath("./@style")[0] if a.xpath("./@style") else ""
|
||||
pic = re.search(r"background:\s*url\((.*?)\)", style).group(1) if re.search(r"background:\s*url\((.*?)\)", style) else ""
|
||||
sid = a.xpath("./@href")[0] if a.xpath("./@href") else ""
|
||||
videos.append({"vod_id": sid, "vod_name": name, "vod_pic": pic, "vod_remarks": "推荐"})
|
||||
except:
|
||||
continue
|
||||
|
||||
# 视频列表
|
||||
for a in root.xpath("//ul[contains(@class,'stui-vodlist')]//a[contains(@class,'stui-vodlist__thumb')]"):
|
||||
try:
|
||||
name = a.xpath("./@title")[0] if a.xpath("./@title") else "未知"
|
||||
pic = a.xpath("./@data-original")[0] if a.xpath("./@data-original") else ""
|
||||
sid = a.xpath("./@href")[0] if a.xpath("./@href") else ""
|
||||
remark = a.xpath(".//span[@class='pic-text text-right']/text()")[0] if a.xpath(".//span[@class='pic-text text-right']/text()") else ""
|
||||
videos.append({"vod_id": sid, "vod_name": name, "vod_pic": pic, "vod_remarks": remark})
|
||||
except:
|
||||
continue
|
||||
|
||||
return {'list': videos}
|
||||
except:
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
try:
|
||||
order = extend.get('by', 'time') if extend else 'time'
|
||||
url = f'http://qiyoudy5.com/list/{tid}_{pg}.html?order={order}'
|
||||
rsp = self.fetch(url)
|
||||
root = self.parse_html(rsp.content)
|
||||
|
||||
if not root:
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
videos = []
|
||||
for a in root.xpath("//a[contains(@class,'stui-vodlist__thumb')]"):
|
||||
try:
|
||||
name = a.xpath("./@title")[0] if a.xpath("./@title") else "未知"
|
||||
pic = a.xpath("./@data-original")[0] if a.xpath("./@data-original") else ""
|
||||
sid = a.xpath("./@href")[0] if a.xpath("./@href") else ""
|
||||
remark = a.xpath(".//span[@class='pic-text text-right']/text()")[0] if a.xpath(".//span[@class='pic-text text-right']/text()") else ""
|
||||
videos.append({"vod_id": sid, "vod_name": name, "vod_pic": pic, "vod_remarks": remark})
|
||||
except:
|
||||
continue
|
||||
|
||||
current_page = int(root.xpath("//ul[contains(@class,'stui-page')]//a[@class='active']/text()")[0]) if root.xpath("//ul[contains(@class,'stui-page')]//a[@class='active']/text()") else pg
|
||||
|
||||
page_numbers = []
|
||||
for link in root.xpath("//ul[contains(@class,'stui-page')]//a[contains(@href,'list')]/@href"):
|
||||
match = re.search(r'list/\d+_(\d+)\.html', link)
|
||||
if match:
|
||||
page_numbers.append(int(match.group(1)))
|
||||
total_page = max(page_numbers) if page_numbers else 1
|
||||
|
||||
return {
|
||||
'list': videos,
|
||||
'page': current_page,
|
||||
'pagecount': total_page if total_page > 0 else 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
except:
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0}
|
||||
|
||||
def detailContent(self, array):
|
||||
try:
|
||||
tid = array[0]
|
||||
url = f'http://qiyoudy5.com{tid}'
|
||||
rsp = self.fetch(url)
|
||||
root = self.parse_html(rsp.content)
|
||||
|
||||
if not root:
|
||||
return {'list': []}
|
||||
|
||||
# 基本信息
|
||||
detail_node = root.xpath("//div[contains(@class,'stui-content__detail')]") or root.xpath("//div[@class='stui-player__detail']")
|
||||
pic = title = area = director = actor = year = desc = ""
|
||||
|
||||
if detail_node:
|
||||
detail_node = detail_node[0]
|
||||
pic = self.get_first(root.xpath("//meta[@property='og:image']/@content") or detail_node.xpath(".//img/@data-original"))
|
||||
title = self.get_first(detail_node.xpath(".//h1//text()"))
|
||||
if not title:
|
||||
page_title = self.get_first(root.xpath("//title/text()"))
|
||||
title = re.search(r"《(.*?)》", page_title).group(1) if page_title and re.search(r"《(.*?)》", page_title) else ""
|
||||
|
||||
area = self.get_first(root.xpath("//meta[@property='og:video:area']/@content"))
|
||||
director = self.get_first(root.xpath("//meta[@property='og:video:director']/@content"))
|
||||
actor = self.get_first(root.xpath("//meta[@property='og:video:actor']/@content"))
|
||||
year_info = self.get_first(root.xpath("//p[@class='data']//text()[contains(.,'年份:')]"))
|
||||
year = re.search(r"年份:(\d{4})", year_info).group(1) if year_info and re.search(r"年份:(\d{4})", year_info) else ""
|
||||
desc = self.get_first(root.xpath("//meta[@property='og:description']/@content"))
|
||||
|
||||
# 播放列表
|
||||
playFrom, playUrl = [], []
|
||||
for tab in root.xpath("//ul[contains(@class,'nav-tabs')]/li"):
|
||||
tab_name = self.get_first(tab.xpath(".//a/text()"))
|
||||
tab_id = self.get_first(tab.xpath(".//a/@href")).replace("#", "") if tab.xpath(".//a/@href") else ""
|
||||
|
||||
if tab_name and tab_id:
|
||||
play_list = root.xpath(f"//div[@id='{tab_id}']//ul[contains(@class,'stui-content__playlist')]//a")
|
||||
if play_list:
|
||||
playFrom.append(tab_name)
|
||||
episodes = []
|
||||
for episode in play_list:
|
||||
ep_name = self.get_first(episode.xpath("./text()")) or "播放"
|
||||
ep_url = self.get_first(episode.xpath("./@href"))
|
||||
if ep_url:
|
||||
episodes.append(f"{ep_name}${ep_url}")
|
||||
if episodes:
|
||||
playUrl.append("#".join(episodes))
|
||||
|
||||
vod = {
|
||||
"vod_id": tid,
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"vod_year": year,
|
||||
"vod_area": area,
|
||||
"vod_actor": actor,
|
||||
"vod_director": director,
|
||||
"vod_content": desc
|
||||
}
|
||||
|
||||
if playFrom and playUrl:
|
||||
vod['vod_play_from'] = "$$$".join(playFrom)
|
||||
vod['vod_play_url'] = "$$$".join(playUrl)
|
||||
|
||||
return {'list': [vod]}
|
||||
except:
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
try:
|
||||
url = "http://qiyoudy5.com/search.php"
|
||||
# 修复:使用正确的参数名和变量
|
||||
post_data = {
|
||||
'searchword': key, # 改为变量key,而不是字符串'key'
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": "http://qiyoudy5.com/",
|
||||
"Origin": "http://qiyoudy5.com"
|
||||
}
|
||||
|
||||
# 修复:只发送一次POST请求,删除重复的请求
|
||||
rsp = self.post(url, data=post_data, headers=headers)
|
||||
|
||||
root = self.parse_html(rsp.content)
|
||||
|
||||
if not root:
|
||||
return {'list': []}
|
||||
|
||||
videos = []
|
||||
|
||||
# 多种选择器尝试获取搜索结果
|
||||
selectors = [
|
||||
"//ul[contains(@class,'stui-vodlist__media')]//li",
|
||||
"//ul[contains(@class,'stui-vodlist')]//li",
|
||||
"//a[contains(@class,'stui-vodlist__thumb')]"
|
||||
]
|
||||
|
||||
result_items = []
|
||||
for selector in selectors:
|
||||
result_items = root.xpath(selector)
|
||||
if result_items:
|
||||
break
|
||||
|
||||
for item in result_items:
|
||||
try:
|
||||
if item.tag == 'a': # 直接是a标签
|
||||
href = self.get_first(item.xpath("./@href"))
|
||||
title = self.get_first(item.xpath("./@title"))
|
||||
pic = self.get_first(item.xpath("./@data-original"))
|
||||
remark = self.get_first(item.xpath(".//span[contains(@class,'pic-text')]/text()"))
|
||||
else: # li标签
|
||||
link = item.xpath(".//a[contains(@class,'stui-vodlist__thumb')]") or item.xpath(".//a")
|
||||
if not link:
|
||||
continue
|
||||
link = link[0]
|
||||
href = self.get_first(link.xpath("./@href"))
|
||||
title = self.get_first(link.xpath("./@title"))
|
||||
pic = self.get_first(link.xpath("./@data-original"))
|
||||
if not pic:
|
||||
style = self.get_first(link.xpath("./@style"))
|
||||
if style and "background-image" in style:
|
||||
pic_match = re.search(r"background-image:\s*url\(['\"]?(.*?)['\"]?\)", style)
|
||||
if pic_match:
|
||||
pic = pic_match.group(1)
|
||||
remark = self.get_first(item.xpath(".//span[contains(@class,'pic-text')]/text()"))
|
||||
|
||||
if href and title:
|
||||
videos.append({
|
||||
"vod_id": href,
|
||||
"vod_name": title.strip(),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark or ""
|
||||
})
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# 备用解析方案
|
||||
if not videos:
|
||||
for a in root.xpath("//a[contains(@href,'/vod/')]"):
|
||||
try:
|
||||
href = self.get_first(a.xpath("./@href"))
|
||||
title = self.get_first(a.xpath("./@title")) or self.get_first(a.xpath(".//text()"))
|
||||
pic = self.get_first(a.xpath("./@data-original"))
|
||||
remark = self.get_first(a.xpath(".//span[contains(@class,'pic-text')]/text()"))
|
||||
|
||||
if href and title:
|
||||
videos.append({
|
||||
"vod_id": href,
|
||||
"vod_name": title.strip(),
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remark or ""
|
||||
})
|
||||
except:
|
||||
continue
|
||||
|
||||
# 去重
|
||||
seen = set()
|
||||
unique_videos = []
|
||||
for video in videos:
|
||||
identifier = (video["vod_id"], video["vod_name"])
|
||||
if identifier not in seen:
|
||||
seen.add(identifier)
|
||||
unique_videos.append(video)
|
||||
|
||||
return {'list': unique_videos}
|
||||
|
||||
except Exception as e:
|
||||
return {'list': []}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
url = f"http://qiyoudy5.com{id}"
|
||||
rsp = self.fetch(url)
|
||||
_, html_content = self.parse_html(rsp.content, return_content=True)
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": url,
|
||||
}
|
||||
|
||||
# 多种方式查找播放地址
|
||||
# 1. API链接
|
||||
for pattern in [r"http://api\.yongfan99\.com:81/content\.php\?[^'\"]+", r"content\.php\?vid=[^&]+&type=[^'\"]+"]:
|
||||
match = re.search(pattern, html_content)
|
||||
if match:
|
||||
api_url = match.group(0)
|
||||
if not api_url.startswith('http'):
|
||||
api_url = "http://api.yongfan99.com:81/" + api_url
|
||||
try:
|
||||
api_rsp = self.fetch(api_url, headers=headers)
|
||||
m3u8_match = re.search(r'http[s]?://[^\s"\']+\.m3u8[^\s"\']*', api_rsp.text)
|
||||
if m3u8_match:
|
||||
return {"parse": 0, "playUrl": "", "url": m3u8_match.group(0), "header": headers}
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2. iframe中的播放器
|
||||
for pattern in [r'<iframe[^>]*src=[\'"]([^\'"]+)[\'"][^>]*>', r'src\s*=\s*[\'"]((?:http[^\'"]*)?/play/[^\'"]*)[\'"]']:
|
||||
for iframe_src in re.findall(pattern, html_content):
|
||||
if not iframe_src.startswith('http'):
|
||||
iframe_src = urljoin(url, iframe_src)
|
||||
try:
|
||||
iframe_rsp = self.fetch(iframe_src, headers=headers)
|
||||
m3u8_match = re.search(r'http[s]?://[^\s"\']+\.m3u8[^\s"\']*', iframe_rsp.text)
|
||||
if m3u8_match:
|
||||
return {"parse": 0, "playUrl": "", "url": m3u8_match.group(0), "header": headers}
|
||||
except:
|
||||
continue
|
||||
|
||||
# 3. 直接搜索m3u8链接
|
||||
m3u8_match = re.search(r'http[s]?://[^\s"\']+\.m3u8[^\s"\']*', html_content)
|
||||
if m3u8_match:
|
||||
return {"parse": 0, "playUrl": "", "url": m3u8_match.group(0), "header": headers}
|
||||
|
||||
# 4. 返回原始URL进行外部解析
|
||||
return {"parse": 1, "playUrl": "", "url": url, "header": headers}
|
||||
|
||||
except:
|
||||
return {"parse": 1, "playUrl": "", "url": f"http://qiyoudy5.com{id}", "header": {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": "http://qiyoudy5.com/",
|
||||
}}
|
||||
|
||||
# 辅助函数
|
||||
def parse_html(self, content, return_content=False):
|
||||
encodings = ['utf-8', 'gbk', 'gb2312', 'iso-8859-1']
|
||||
html_content = None
|
||||
for encoding in encodings:
|
||||
try:
|
||||
html_content = content.decode(encoding)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if html_content is None:
|
||||
html_content = content.decode('utf-8', errors='replace')
|
||||
|
||||
html_content = self.clean_html(html_content)
|
||||
root = etree.HTML(html_content)
|
||||
|
||||
if return_content:
|
||||
return root, html_content
|
||||
return root
|
||||
|
||||
def get_first(self, array, default=""):
|
||||
return array[0] if array else default
|
||||
|
||||
def clean_html(self, html_content):
|
||||
html_content = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', html_content)
|
||||
replacements = {' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"'}
|
||||
for old, new in replacements.items():
|
||||
html_content = html_content.replace(old, new)
|
||||
return html_content
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return any(fmt in url for fmt in ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.webm'])
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return True
|
||||
|
||||
def localProxy(self, param):
|
||||
return {}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 1,
|
||||
quickSearch: 1,
|
||||
title: '小苹果',
|
||||
lang: 'hipy'
|
||||
})
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
import re
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'http://asp.xpgtv.com'
|
||||
|
||||
headers = {
|
||||
"User-Agent": "okhttp/3.12.11"
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data = self.fetch(f"{self.host}/api.php/v2.vod/androidtypes", headers=self.headers).json()
|
||||
dy = {
|
||||
"classes": "类型",
|
||||
"areas": "地区",
|
||||
"years": "年份",
|
||||
"sortby": "排序",
|
||||
}
|
||||
filters = {}
|
||||
classes = []
|
||||
for item in data['data']:
|
||||
# 简化字段检查逻辑
|
||||
has_non_empty_field = any(key in item and len(item[key]) > 1 for key in dy)
|
||||
item['soryby'] = ['updatetime', 'hits', 'score']
|
||||
demos = ['时间', '人气', '评分']
|
||||
classes.append({"type_name": item["type_name"], "type_id": str(item["type_id"])})
|
||||
|
||||
if has_non_empty_field:
|
||||
filters[str(item["type_id"])] = []
|
||||
for dkey in item:
|
||||
if dkey in dy and len(item[dkey]) > 1:
|
||||
values = item[dkey]
|
||||
value_array = [
|
||||
{"n": demos[idx] if dkey == "sortby" else value.strip(), "v": value.strip()}
|
||||
for idx, value in enumerate(values) if value.strip()
|
||||
]
|
||||
filters[str(item["type_id"])].append(
|
||||
{"key": dkey, "name": dy[dkey], "value": value_array}
|
||||
)
|
||||
|
||||
return {
|
||||
"class": classes,
|
||||
"filters": filters
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
rsp = self.fetch(f"{self.host}/api.php/v2.main/androidhome", headers=self.headers).json()
|
||||
videos = []
|
||||
for i in rsp['data']['list']:
|
||||
videos.extend(self.getlist(i['list']))
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params = {
|
||||
"page": pg,
|
||||
"type": tid,
|
||||
"area": extend.get('areaes', ''),
|
||||
"year": extend.get('yeares', ''),
|
||||
"sortby": extend.get('sortby', ''),
|
||||
"class": extend.get('classes', '')
|
||||
}
|
||||
# 简化参数过滤
|
||||
params = {k: v for k, v in params.items() if v}
|
||||
rsp = self.fetch(f'{self.host}/api.php/v2.vod/androidfilter10086', headers=self.headers, params=params).json()
|
||||
|
||||
return {
|
||||
'list': self.getlist(rsp['data']),
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 90,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
rsp = self.fetch(f'{self.host}/api.php/v3.vod/androiddetail2?vod_id={ids[0]}', headers=self.headers).json()
|
||||
v = rsp.get('data', {}) or {}
|
||||
urls = v.get('urls') or []
|
||||
play_items = []
|
||||
|
||||
# 预编译正则表达式提高性能
|
||||
patterns = [
|
||||
(r'^\d+$', None), # 纯数字
|
||||
(r'^\d+-\d+$', None), # 数字范围
|
||||
(r'^第\d+[集期话节]$', None), # 第X集/期/话/节
|
||||
(r'^第\d+季$', None), # 第X季
|
||||
(r'^[集期话]?\d+$', None), # 集X/期X/话X
|
||||
(r'^E[P]?\d+$', re.IGNORECASE), # EP1/E01
|
||||
(r'^\d+[PpKk]$', None), # 分辨率
|
||||
(r'^[Hh][Dd]$', None), # HD
|
||||
(r'^[Ff][Hh][Dd]$', None), # FHD
|
||||
(r'^[Uu][Hh][Dd]$', None) # UHD
|
||||
]
|
||||
|
||||
allowed_chinese_keywords = {
|
||||
'蓝光', '超清', '高清', '标清', '枪版', '全清',
|
||||
'全集', '全', '完整版', '正片', '预告', '花絮'
|
||||
}
|
||||
|
||||
for i in urls:
|
||||
key = (i.get('key') or i.get('name') or "").strip()
|
||||
url = (i.get('url') or "").strip()
|
||||
|
||||
if key and url:
|
||||
if key in allowed_chinese_keywords:
|
||||
play_items.append(f"{key}${url}")
|
||||
else:
|
||||
# 使用预编译的正则表达式检查
|
||||
matched = False
|
||||
for pattern, flags in patterns:
|
||||
if flags:
|
||||
if re.match(pattern, key, flags):
|
||||
matched = True
|
||||
break
|
||||
else:
|
||||
if re.match(pattern, key):
|
||||
matched = True
|
||||
break
|
||||
|
||||
if matched:
|
||||
play_items.append(f"{key}${url}")
|
||||
|
||||
play_url = "#".join(play_items)
|
||||
|
||||
vod = {
|
||||
'vod_id': v.get('id'),
|
||||
'vod_name': v.get('name'),
|
||||
'vod_pic': v.get('pic'),
|
||||
'vod_year': v.get('year'),
|
||||
'vod_area': v.get('area'),
|
||||
'vod_lang': v.get('lang'),
|
||||
'type_name': v.get('className'),
|
||||
'vod_actor': v.get('actor'),
|
||||
'vod_director': v.get('director'),
|
||||
'vod_content': v.get('content'),
|
||||
'vod_play_from': '小苹果',
|
||||
'vod_play_url': play_url
|
||||
}
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
rsp = self.fetch(f'{self.host}/api.php/v2.vod/androidsearch10086?page={pg}&wd={key}', headers=self.headers).json()
|
||||
return {'list': self.getlist(rsp['data']), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
header = {
|
||||
'user_id': 'XPGBOX',
|
||||
'token2': 'SnAXiSW8vScXE0Z9aDOnK5xffbO75w1+uPom3WjnYfVEA1oWtUdi2Ihy1N8=',
|
||||
'version': 'XPGBOX com.phoenix.tv1.5.7',
|
||||
'hash': 'd78a',
|
||||
'screenx': '2345',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
'token': 'ElEDlwCVgXcFHFhddiq2JKteHofExRBUrfNlmHrWetU3VVkxnzJAodl52N9EUFS+Dig2A/fBa/V9RuoOZRBjYvI+GW8kx3+xMlRecaZuECdb/3AdGkYpkjW3wCnpMQxf8vVeCz5zQLDr8l8bUChJiLLJLGsI+yiNskiJTZz9HiGBZhZuWh1mV1QgYah5CLTbSz8=',
|
||||
'timestamp': '1743060300',
|
||||
'screeny': '1065',
|
||||
}
|
||||
if 'http' not in id:
|
||||
id = f"http://c.xpgtv.net/m3u8/{id}.m3u8"
|
||||
return {"parse": 0, "url": id, "header": header}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for vod in data:
|
||||
# 简化备注生成逻辑
|
||||
remarks = f"更新至{vod.get('updateInfo')}" if vod.get('updateInfo') else vod.get('score', '')
|
||||
videos.append({
|
||||
"vod_id": vod['id'],
|
||||
"vod_name": vod['name'],
|
||||
"vod_pic": vod['pic'],
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
return videos
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import json
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from pyquery import PyQuery as pq
|
||||
from requests import Session
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = self.gethost()
|
||||
self.headers['referer'] = f'{self.host}/'
|
||||
self.session = Session()
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def getName(self):
|
||||
return "minijj"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-full-version': '"133.0.6943.98"',
|
||||
'sec-ch-ua-arch': '"x86"',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-ch-ua-platform-version': '"19.0.0"',
|
||||
'sec-ch-ua-model': '""',
|
||||
'sec-ch-ua-full-version-list': '"Not(A:Brand";v="99.0.0.0", "Google Chrome";v="133.0.6943.98", "Chromium";v="133.0.6943.98"',
|
||||
'dnt': '1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'sec-fetch-site': 'none',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-fetch-dest': 'document',
|
||||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'priority': 'u=0, i'
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"電影": "1",
|
||||
"電視劇": "2",
|
||||
"經典動漫": "3",
|
||||
"綜藝娛樂": "4",
|
||||
}
|
||||
classes = []
|
||||
filters = {}
|
||||
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
|
||||
filters = {
|
||||
'1': [{'key': 'type', 'name': '類型', 'value': [{'n': '全部', 'v': '1'}, {'n': '動作片', 'v': '8'}, {'n': '喜劇片', 'v': '9'}, {'n': '愛情片', 'v': '10'}, {'n': '科幻片', 'v': '11'}, {'n': '恐怖片', 'v': '12'}, {'n': '戰爭片', 'v': '13'}, {'n': '劇情片', 'v': '14'}]},
|
||||
{'key': 'area', 'name': '地區', 'value': [{'n': '全部', 'v': ''}, {'n': '大陸', 'v': 'dalu'}, {'n': '美國', 'v': 'meiguo'}, {'n': '香港', 'v': 'xianggang'}, {'n': '台灣', 'v': 'taiwan'}, {'n': '韓國', 'v': 'hanguo'}, {'n': '日本', 'v': 'riben'}, {'n': '泰國', 'v': 'taiguo'}, {'n': '新加坡', 'v': 'xinjiapo'}, {'n': '馬來西亞', 'v': 'malaixiya'}, {'n': '印度', 'v': 'yindu'}, {'n': '英國', 'v': 'yingguo'}, {'n': '法國', 'v': 'faguo'}, {'n': '加拿大', 'v': 'jianada'}]},
|
||||
{'key': 'year', 'name': '年份', 'value': [{'n': '全部', 'v': ''}, {'n': '2025', 'v': '2025'}, {'n': '2024', 'v': '2024'}, {'n': '2023', 'v': '2023'}, {'n': '2022', 'v': '2022'}, {'n': '2021', 'v': '2021'}, {'n': '2020', 'v': '2020'}, {'n': '2019', 'v': '2019'}, {'n': '2018', 'v': '2018'}, {'n': '2017', 'v': '2017'}, {'n': '2016', 'v': '2016'}, {'n': '2015', 'v': '2015'}, {'n': '2014', 'v': '2014'}, {'n': '2013', 'v': '2013'}, {'n': '90後', 'v': '1990,1999'}, {'n': '80後', 'v': '1980,1989'}, {'n': '更早', 'v': '1900,1980'}]}],
|
||||
'2': [{'key': 'type', 'name': '類型', 'value': [{'n': '全部', 'v': '2'}, {'n': '大陸劇', 'v': '15'}, {'n': '香港劇', 'v': '16'}, {'n': '台灣劇', 'v': '918'}, {'n': '日劇', 'v': '18'}, {'n': '韓劇', 'v': '915'}, {'n': '美劇', 'v': '916'}, {'n': '英劇', 'v': '923'}, {'n': '歐美劇', 'v': '17'}, {'n': '泰劇', 'v': '922'}, {'n': '亞洲劇', 'v': '19'}]},
|
||||
{'key': 'area', 'name': '地區', 'value': [{'n': '全部', 'v': ''}, {'n': '大陸', 'v': 'dalu'}, {'n': '美國', 'v': 'meiguo'}, {'n': '香港', 'v': 'xianggang'}, {'n': '台灣', 'v': 'taiwan'}, {'n': '韓國', 'v': 'hanguo'}, {'n': '日本', 'v': 'riben'}, {'n': '泰國', 'v': 'taiguo'}, {'n': '新加坡', 'v': 'xinjiapo'}, {'n': '馬來西亞', 'v': 'malaixiya'}, {'n': '印度', 'v': 'yindu'}, {'n': '英國', 'v': 'yingguo'}, {'n': '法國', 'v': 'faguo'}, {'n': '加拿大', 'v': 'jianada'}]},
|
||||
{'key': 'year', 'name': '年份', 'value': [{'n': '全部', 'v': ''}, {'n': '2025', 'v': '2025'}, {'n': '2024', 'v': '2024'}, {'n': '2023', 'v': '2023'}, {'n': '2022', 'v': '2022'}, {'n': '2021', 'v': '2021'}, {'n': '2020', 'v': '2020'}, {'n': '2019', 'v': '2019'}, {'n': '2018', 'v': '2018'}, {'n': '2017', 'v': '2017'}, {'n': '2016', 'v': '2016'}, {'n': '2015', 'v': '2015'}, {'n': '2014', 'v': '2014'}, {'n': '2013', 'v': '2013'}, {'n': '90後', 'v': '1990,1999'}, {'n': '80後', 'v': '1980,1989'}, {'n': '更早', 'v': '1900,1980'}]}],
|
||||
'3': [{'key': 'type', 'name': '類型', 'value': [{'n': '全部', 'v': '3'}, {'n': '國漫', 'v': '906'}, {'n': '日漫', 'v': '904'}, {'n': '美漫', 'v': '905'}, {'n': '其他動漫', 'v': '903'}]},
|
||||
{'key': 'area', 'name': '地區', 'value': [{'n': '全部', 'v': ''}, {'n': '大陸', 'v': 'dalu'}, {'n': '美國', 'v': 'meiguo'}, {'n': '日本', 'v': 'riben'}]},
|
||||
{'key': 'year', 'name': '年份', 'value': [{'n': '全部', 'v': ''}, {'n': '2025', 'v': '2025'}, {'n': '2024', 'v': '2024'}, {'n': '2023', 'v': '2023'}, {'n': '2022', 'v': '2022'}, {'n': '2021', 'v': '2021'}, {'n': '2020', 'v': '2020'}, {'n': '2019', 'v': '2019'}, {'n': '2018', 'v': '2018'}, {'n': '2017', 'v': '2017'}, {'n': '2016', 'v': '2016'}, {'n': '2015', 'v': '2015'}, {'n': '2014', 'v': '2014'}, {'n': '2013', 'v': '2013'}, {'n': '90後', 'v': '1990,1999'}, {'n': '80後', 'v': '1980,1989'}, {'n': '更早', 'v': '1900,1980'}]}],
|
||||
'4': [{'key': 'type', 'name': '類型', 'value': [{'n': '全部', 'v': '4'}, {'n': '大陸綜藝', 'v': '911'}, {'n': '港台綜藝', 'v': '907'}, {'n': '韓綜', 'v': '908'}, {'n': '日綜', 'v': '912'}, {'n': '泰綜', 'v': '913'}, {'n': '歐美綜藝', 'v': '909'}]},
|
||||
{'key': 'area', 'name': '地區', 'value': [{'n': '全部', 'v': ''}, {'n': '大陸', 'v': 'dalu'}, {'n': '美國', 'v': 'meiguo'}, {'n': '香港', 'v': 'xianggang'}, {'n': '台灣', 'v': 'taiwan'}, {'n': '韓國', 'v': 'hanguo'}, {'n': '日本', 'v': 'riben'}, {'n': '泰國', 'v': 'taiguo'}]},
|
||||
{'key': 'year', 'name': '年份', 'value': [{'n': '全部', 'v': ''}, {'n': '2025', 'v': '2025'}, {'n': '2024', 'v': '2024'}, {'n': '2023', 'v': '2023'}, {'n': '2022', 'v': '2022'}, {'n': '2021', 'v': '2021'}, {'n': '2020', 'v': '2020'}, {'n': '2019', 'v': '2019'}, {'n': '2018', 'v': '2018'}, {'n': '2017', 'v': '2017'}, {'n': '2016', 'v': '2016'}, {'n': '2015', 'v': '2015'}, {'n': '2014', 'v': '2014'}, {'n': '2013', 'v': '2013'}, {'n': '90後', 'v': '1990,1999'}, {'n': '80後', 'v': '1980,1989'}, {'n': '更早', 'v': '1900,1980'}]}]
|
||||
}
|
||||
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
data = self.getpq()
|
||||
return {'list': self.getlist(data(".update_area_lists .i_list"))}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
_type = extend.get('type', tid)
|
||||
_area = extend.get('area', '')
|
||||
_year = extend.get('year', '')
|
||||
|
||||
# 構建篩選 URL,根據網站實際格式
|
||||
# 基礎格式:/lm/{type}/sx---{year}-----{page}.html
|
||||
# 如果有地區:/lm/{type}/sx------{area}--{page}.html
|
||||
# 如果年份和地區同時存在:/lm/{type}/sx---{year}---{area}--{page}.html
|
||||
if _year and _area:
|
||||
url = f'{self.host}/lm/{_type}/sx---{_year}---{_area}--{pg}.html'
|
||||
elif _year:
|
||||
url = f'{self.host}/lm/{_type}/sx---{_year}-----{pg}.html'
|
||||
elif _area:
|
||||
url = f'{self.host}/lm/{_type}/sx------{_area}--{pg}.html'
|
||||
else:
|
||||
url = f'{self.host}/lm/{_type}/sx--------{pg}.html'
|
||||
|
||||
print(f"篩選 URL: {url}") # 調試用,確認 URL 是否正確
|
||||
|
||||
data = self.getpq(url)
|
||||
vdata = self.getlist(data(".update_area_lists .i_list"))
|
||||
pagecount = 9999
|
||||
try:
|
||||
pagination = data('.pagination .page-numbers')
|
||||
last_page = pagination[-2].text if len(pagination) > 1 else '1'
|
||||
pagecount = int(last_page) if last_page.isdigit() else 9999
|
||||
except:
|
||||
pass
|
||||
result['list'] = vdata
|
||||
result['page'] = pg
|
||||
result['pagecount'] = pagecount
|
||||
result['limit'] = 24
|
||||
result['total'] = pagecount * 24 if pagecount != 9999 else 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(ids[0])
|
||||
full_title = data('title').text().split(' - ')[0]
|
||||
vn = full_title.split('線上觀看')[0].split('手機播放')[0].rstrip(',')
|
||||
|
||||
vod = {
|
||||
'vod_id': ids[0],
|
||||
'vod_name': vn,
|
||||
'vod_pic': data('.vod-pic').attr('data-original') or '',
|
||||
'vod_remarks': data('.meta-post').eq(0).text().replace('', '').replace('', '').strip() or '',
|
||||
'vod_play_from': '',
|
||||
'vod_play_url': ''
|
||||
}
|
||||
|
||||
play_from_list = []
|
||||
play_url_list = []
|
||||
|
||||
tabs = data('#sea-tab li a')
|
||||
for tab in tabs.items():
|
||||
play_from = tab.text().strip().replace('\n', '').split('</span>')[-1]
|
||||
play_from_list.append(play_from)
|
||||
|
||||
tab_contents = data('#sea-tab-content .tab-pane')
|
||||
for i, content in enumerate(tab_contents.items()):
|
||||
episodes = []
|
||||
play_links = content('.play-list a')
|
||||
for link in play_links.items():
|
||||
ep_name = link.text() or f"第 {len(episodes) + 1} 集"
|
||||
ep_url = f"{self.host}{link.attr('href')}"
|
||||
episodes.append(f"{ep_name}${ep_url}")
|
||||
episodes.reverse()
|
||||
play_url_list.append('#'.join(episodes))
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(play_from_list)
|
||||
vod['vod_play_url'] = '$$$'.join(play_url_list)
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.getpq(f'/ss.html?wd={key}&page={pg}')
|
||||
return {'list': self.getlist(data(".update_area_lists .i_list")), 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5410.0 Safari/537.36',
|
||||
'Referer': f'{self.host}/',
|
||||
'Origin': self.host,
|
||||
}
|
||||
|
||||
data = self.getpq(id)
|
||||
|
||||
iframe_url = data('iframe').attr('src')
|
||||
if iframe_url:
|
||||
if not iframe_url.startswith('http'):
|
||||
iframe_url = f"{self.host}{iframe_url}"
|
||||
if iframe_url.endswith('.m3u8') or iframe_url.endswith('.mp4'):
|
||||
return {'parse': 0, 'url': iframe_url, 'header': headers}
|
||||
return {'parse': 1, 'url': iframe_url, 'header': headers}
|
||||
|
||||
video_url = data('video source').attr('src')
|
||||
if video_url:
|
||||
if not video_url.startswith('http'):
|
||||
video_url = f"{self.host}{video_url}"
|
||||
return {'parse': 0, 'url': video_url, 'header': headers}
|
||||
|
||||
scripts = data('script')
|
||||
for script in scripts.items():
|
||||
script_text = script.text()
|
||||
if 'var player_data' in script_text or '.m3u8' in script_text or '.mp4' in script_text:
|
||||
import re
|
||||
urls = re.findall(r'(https?://[^\s\'"]+\.(m3u8|mp4))', script_text)
|
||||
if urls:
|
||||
return {'parse': 0, 'url': urls[0][0], 'header': headers}
|
||||
|
||||
return {'parse': 1, 'url': id, 'header': headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def gethost(self):
|
||||
try:
|
||||
response = self.fetch('https://www.minijj.com', headers=self.headers, allow_redirects=False)
|
||||
return response.headers.get('Location', 'https://www.minijj.com')
|
||||
except Exception as e:
|
||||
print(f"獲取主頁失敗: {str(e)}")
|
||||
return "https://www.minijj.com"
|
||||
|
||||
def e64(self, text):
|
||||
try:
|
||||
text_bytes = text.encode('utf-8')
|
||||
encoded_bytes = b64encode(text_bytes)
|
||||
return encoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64編碼錯誤: {str(e)}")
|
||||
return ""
|
||||
|
||||
def d64(self, encoded_text):
|
||||
try:
|
||||
encoded_bytes = encoded_text.encode('utf-8')
|
||||
decoded_bytes = b64decode(encoded_bytes)
|
||||
return decoded_bytes.decode('utf-8')
|
||||
except Exception as e:
|
||||
print(f"Base64解碼錯誤: {str(e)}")
|
||||
return ""
|
||||
|
||||
def getlist(self, data):
|
||||
vlist = []
|
||||
for i in data.items():
|
||||
vlist.append({
|
||||
'vod_id': i('a').attr('href'),
|
||||
'vod_name': i('.meta-title').text(),
|
||||
'vod_pic': i('img').attr('data-original'),
|
||||
'vod_remarks': i('.meta-post').text().replace('', '').replace('', '').strip(),
|
||||
})
|
||||
return vlist
|
||||
|
||||
def getpq(self, path=''):
|
||||
h = '' if path.startswith('http') else self.host
|
||||
response = self.session.get(f'{h}{path}')
|
||||
response.encoding = 'utf-8'
|
||||
text = response.text
|
||||
try:
|
||||
return pq(text)
|
||||
except Exception as e:
|
||||
print(f"解析 HTML 失敗: {str(e)}")
|
||||
return pq(text.encode('utf-8'))
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#author恰逢
|
||||
import json,re,sys,base64,requests
|
||||
from Crypto.Cipher import AES
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
SELECTORS=['.video-item','.video-list .item','.list-item','.post-item']
|
||||
def getName(self):return"黑料不打烊"
|
||||
def init(self,extend=""):pass
|
||||
def homeContent(self,filter):
|
||||
cateManual={"最新黑料":"hlcg","今日热瓜":"jrrs","每日TOP10":"mrrb","周报精选":"zbjx","月榜热瓜":"ybrg","反差女友":"fczq","校园黑料":"xycg","网红黑料":"whhl","明星丑闻":"mxcw","原创社区":"ycsq","推特社区":"ttsq","社会新闻":"shxw","官场爆料":"gchl","影视短剧":"ysdj","全球奇闻":"qqqw","黑料课堂":"hlkt","每日大赛":"mrds","激情小说":"jqxs","桃图杂志":"ttzz","深夜综艺":"syzy","独家爆料":"djbl"}
|
||||
return{'class':[{'type_name':k,'type_id':v}for k,v in cateManual.items()]}
|
||||
def homeVideoContent(self):return{}
|
||||
def categoryContent(self,tid,pg,filter,extend):
|
||||
url=f'https://heiliao.com/{tid}/'if int(pg)==1 else f'https://heiliao.com/{tid}/page/{pg}/'
|
||||
videos=self.get_list(url)
|
||||
return{'list':videos,'page':pg,'pagecount':9999,'limit':90,'total':999999}
|
||||
def fetch_and_decrypt_image(self,url):
|
||||
try:
|
||||
if url.startswith('//'):url='https:'+url
|
||||
elif url.startswith('/'):url='https://heiliao.com'+url
|
||||
r=requests.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36','Referer':'https://heiliao.com/'},timeout=15,verify=False)
|
||||
if r.status_code!=200:return b''
|
||||
return AES.new(b'f5d965df75336270',AES.MODE_CBC,b'97b60394abc2fbe1').decrypt(r.content)
|
||||
except: return b''
|
||||
def _extract_img_from_onload(self,node):
|
||||
try:
|
||||
m=re.search(r"load(?:Share)?Img\s*\([^,]+,\s*['\"]([^'\"]+)['\"]",(node.attr('onload')or''))
|
||||
return m.group(1)if m else''
|
||||
except:return''
|
||||
def _should_decrypt(self,url:str)->bool:
|
||||
u=(url or'').lower();return any(x in u for x in['pic.gylhaa.cn','new.slfpld.cn','/upload_01/','/upload/'])
|
||||
def _abs(self,u:str)->str:
|
||||
if not u:return''
|
||||
if u.startswith('//'):return'https:'+u
|
||||
if u.startswith('/'):return'https://heiliao.com'+u
|
||||
return u
|
||||
def e64(self,s:str)->str:
|
||||
try:return base64.b64encode((s or'').encode()).decode()
|
||||
except:return''
|
||||
def d64(self,s:str)->str:
|
||||
try:return base64.b64decode((s or'').encode()).decode()
|
||||
except:return''
|
||||
def _img(self,img_node):
|
||||
u=''if img_node is None else(img_node.attr('src')or img_node.attr('data-src')or'')
|
||||
enc=''if img_node is None else self._extract_img_from_onload(img_node)
|
||||
t=enc or u
|
||||
return f"{self.getProxyUrl()}&url={self.e64(t)}&type=hlimg"if t and(enc or self._should_decrypt(t))else self._abs(t)
|
||||
def _parse_items(self,root):
|
||||
vids=[]
|
||||
for sel in self.SELECTORS:
|
||||
for it in root(sel).items():
|
||||
title=it.find('.title, h3, h4, .video-title').text()
|
||||
if not title:continue
|
||||
link=it.find('a').attr('href')
|
||||
if not link:continue
|
||||
vids.append({'vod_id':self._abs(link),'vod_name':title,'vod_pic':self._img(it.find('img')),'vod_remarks':it.find('.date, .time, .remarks, .duration').text()or''})
|
||||
if vids:break
|
||||
return vids
|
||||
def detailContent(self,array):
|
||||
tid=array[0];url=tid if tid.startswith('http')else f'https://heiliao.com{tid}'
|
||||
rsp=self.fetch(url)
|
||||
if not rsp:return{'list':[]}
|
||||
rsp.encoding='utf-8';html_text=rsp.text
|
||||
try:root_text=pq(html_text)
|
||||
except:root_text=None
|
||||
try:root_content=pq(rsp.content)
|
||||
except:root_content=None
|
||||
title=(root_text('title').text()if root_text else'')or''
|
||||
if' - 黑料网'in title:title=title.replace(' - 黑料网','')
|
||||
pic=''
|
||||
if root_text:
|
||||
og=root_text('meta[property="og:image"]').attr('content')
|
||||
if og and(og.endswith('.png')or og.endswith('.jpg')or og.endswith('.jpeg')):pic=og
|
||||
else:pic=self._img(root_text('.video-item-img img'))
|
||||
detail=''
|
||||
if root_text:
|
||||
detail=root_text('meta[name="description"]').attr('content')or''
|
||||
if not detail:detail=root_text('.content').text()[:200]
|
||||
play_from,play_url=[],[]
|
||||
if root_content:
|
||||
for i,p in enumerate(root_content('.dplayer').items()):
|
||||
c=p.attr('config')
|
||||
if not c:continue
|
||||
try:s=(c.replace('"','"').replace('"','"').replace('&','&').replace('&','&').replace('<','<').replace('<','<').replace('>','>').replace('>','>'));u=(json.loads(s).get('video',{})or{}).get('url','')
|
||||
except:m=re.search(r'"url"\s*:\s*"([^"]+)"',c);u=m.group(1)if m else''
|
||||
if u:
|
||||
u=u.replace('\\/','/');u=self._abs(u)
|
||||
play_from.append(f'视频{i+1}');play_url.append(u)
|
||||
if not play_url:
|
||||
for pat in[r'https://hls\.[^"\']+\.m3u8[^"\']*',r'https://[^"\']+\.m3u8\?auth_key=[^"\']+',r'//hls\.[^"\']+\.m3u8[^"\']*']:
|
||||
for u in re.findall(pat,html_text):
|
||||
u=self._abs(u);play_from.append(f'视频{len(play_from)+1}');play_url.append(u)
|
||||
if len(play_url)>=3:break
|
||||
if play_url:break
|
||||
if not play_url:
|
||||
js_patterns=[r'video[\s\S]{0,500}?url[\s"\'`:=]+([^"\'`\s]+)',r'videoUrl[\s"\'`:=]+([^"\'`\s]+)',r'src[\s"\'`:=]+([^"\'`\s]+\.m3u8[^"\'`\s]*)']
|
||||
for pattern in js_patterns:
|
||||
js_urls=re.findall(pattern,html_text)
|
||||
for js_url in js_urls:
|
||||
if'.m3u8'in js_url:
|
||||
if js_url.startswith('//'):js_url='https:'+js_url
|
||||
elif js_url.startswith('/'):js_url='https://heiliao.com'+js_url
|
||||
elif not js_url.startswith('http'):js_url='https://'+js_url
|
||||
play_from.append(f'视频{len(play_from)+1}');play_url.append(js_url)
|
||||
if len(play_url)>=3:break
|
||||
if play_url:break
|
||||
if not play_url:
|
||||
play_from.append('示例视频');play_url.append("https://hls.obmoti.cn/videos5/b9699667fbbffcd464f8874395b91c81/b9699667fbbffcd464f8874395b91c81.m3u8?auth_key=1760372539-68ed273b94e7a-0-3a53bc0df110c5f149b7d374122ef1ed&v=2")
|
||||
return{'list':[{'vod_id':tid,'vod_name':title,'vod_pic':pic,'vod_content':detail,'vod_play_from':'$$$'.join(play_from),'vod_play_url':'$$$'.join(play_url)}]}
|
||||
def searchContent(self,key,quick,pg="1"):
|
||||
rsp=self.fetch(f'https://heiliao.com/index/search?word={key}')
|
||||
if not rsp:return{'list':[]}
|
||||
return{'list':self._parse_items(pq(rsp.text))}
|
||||
def playerContent(self,flag,id,vipFlags):
|
||||
return{"parse":0,"playUrl":"","url":id,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}}
|
||||
def get_list(self,url):
|
||||
rsp=self.fetch(url)
|
||||
return[]if not rsp else self._parse_items(pq(rsp.text))
|
||||
def fetch(self,url,params=None,cookies=None,headers=None,timeout=5,verify=True,stream=False,allow_redirects=True):
|
||||
h=headers or{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}
|
||||
return super().fetch(url,params=params,cookies=cookies,headers=h,timeout=timeout,verify=verify,stream=stream,allow_redirects=allow_redirects)
|
||||
def localProxy(self,param):
|
||||
try:
|
||||
if param.get('type')=='hlimg':
|
||||
url=self.d64(param.get('url'))
|
||||
if url.startswith('//'):url='https:'+url
|
||||
elif url.startswith('/'):url='https://heiliao.com'+url
|
||||
r=requests.get(url,headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"},timeout=15,verify=False)
|
||||
if r.status_code!=200:return[404,'text/plain','']
|
||||
b=AES.new(b'f5d965df75336270',AES.MODE_CBC,b'97b60394abc2fbe1').decrypt(r.content)
|
||||
ct='image/jpeg'
|
||||
if b.startswith(b'\x89PNG'):ct='image/png'
|
||||
elif b.startswith(b'GIF8'):ct='image/gif'
|
||||
return[200,ct,b]
|
||||
except:pass
|
||||
return[404,'text/plain','']
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import quote, urlparse
|
||||
from Crypto.Hash import SHA256
|
||||
sys.path.append("..")
|
||||
import json
|
||||
import time
|
||||
from pyquery import PyQuery as pq
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='https://www.knvod.com'
|
||||
|
||||
headers = {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'Origin': host,
|
||||
'Referer': f"{host}/",
|
||||
'Cookie':'X-Robots-Tag=CDN-VERIFY'
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getpq(self.fetch(self.host,headers=self.headers).text)
|
||||
result = {}
|
||||
classes = []
|
||||
for k in data('.head-more.box a').items():
|
||||
i=k.attr('href')
|
||||
if i and '/show' in i:
|
||||
classes.append({
|
||||
'type_name': k.text(),
|
||||
'type_id': re.findall(r'\d+', i)[0]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['list']=self.getlist(data('.border-box.public-r .public-list-div'))
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
data=self.getpq(self.fetch(f"{self.host}/show/{tid}--------{pg}---/",headers=self.headers).text)
|
||||
result = {}
|
||||
result['list'] = self.getlist(data('.border-box.public-r .public-list-div'))
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data = self.getpq(self.fetch(f"{self.host}/list/{ids[0]}/", headers=self.headers).text)
|
||||
v=data('.detail-info.lightSpeedIn .slide-info')
|
||||
vod = {
|
||||
'vod_year': v.eq(-1).text().split(':',1)[-1],
|
||||
'vod_remarks': v.eq(0),
|
||||
'vod_actor': v.eq(3).text().split(':',1)[-1],
|
||||
'vod_director': v.eq(2).text().split(':',1)[-1],
|
||||
'vod_content': data('.switch-box #height_limit').text()
|
||||
}
|
||||
np=data('.anthology.wow.fadeInUp')
|
||||
ndata=np('.anthology-tab .swiper-wrapper .swiper-slide')
|
||||
pdata=np('.anthology-list .anthology-list-box ul')
|
||||
play,names=[],[]
|
||||
for i in range(len(ndata)):
|
||||
n=ndata.eq(i)('a')
|
||||
n('span').remove()
|
||||
names.append(n.text())
|
||||
vs=[]
|
||||
for v in pdata.eq(i)('li').items():
|
||||
vs.append(f"{v.text()}${v('a').attr('href')}")
|
||||
play.append('#'.join(vs))
|
||||
vod["vod_play_from"] = "$$$".join(names)
|
||||
vod["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [vod]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
data = self.fetch(f"{self.host}/index.php/ajax/suggest?mid=1&wd={key}&limit=9999×tamp={int(time.time()*1000)}", headers=self.headers).json()
|
||||
videos=[]
|
||||
for i in data['list']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['pic']
|
||||
})
|
||||
return {'list':videos,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
h={
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
'Origin': self.host
|
||||
}
|
||||
data = self.getpq(self.fetch(f"{self.host}{id}", headers=self.headers).text)
|
||||
try:
|
||||
jstr = data('.player-box .player-left script').eq(1).text()
|
||||
jsdata = json.loads(jstr.split('=',1)[-1])
|
||||
url = jsdata.get('url')
|
||||
if not re.search(r'\.m3u8|\.mp4',jsdata['url']):
|
||||
jxd=self.fetch(f"{self.host}/static/player/{jsdata['from']}.js", headers=self.headers).text
|
||||
jx=re.search(r'http.*?url=', jxd)
|
||||
if not jx:raise Exception('未找到jx')
|
||||
parsed_url = urlparse(jx.group())
|
||||
jxhost = parsed_url.scheme + "://" + parsed_url.netloc
|
||||
title=data('head title').eq(0).text().split('-')[0]
|
||||
next=f"{self.host.split('//')[-1]}{jsdata['link_next']}" if jsdata.get('link_next') else ''
|
||||
cd=self.fetch(f"{jx.group()}{jsdata['url']}&next=//{next}&title={quote(title)}", headers=self.headers).text
|
||||
match = re.search(r'var\s+config\s*=\s*(\{[\s\S]*?\})', cd)
|
||||
if not match:raise Exception('未找到config')
|
||||
cm=re.sub(r',\s*}(?=\s*$)', '}', match.group(1))
|
||||
config=json.loads(cm)
|
||||
config.update({'key':self.sha256(f"{self.gettime()}knvod")})
|
||||
config.pop('next',None)
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.8 Mobile/15E148 Safari/604.1',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'DNT': '1',
|
||||
'Origin': jxhost,
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
h['Origin']=jxhost
|
||||
jd=self.post(f"{jxhost}/post.php", headers=headers, data=json.dumps(config))
|
||||
data=json.loads(jd.content.decode('utf-8-sig'))
|
||||
url=data.get('knvod')
|
||||
p = 0
|
||||
if not url:raise Exception('未找到播放地址')
|
||||
except Exception as e:
|
||||
print('错误信息:',e)
|
||||
p,url=1,f"{self.host}{id}"
|
||||
return {"parse": p, "url": url, "header": h}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getlist(self,data):
|
||||
videos=[]
|
||||
for i in data.items():
|
||||
id = i('a').attr('href')
|
||||
if id:
|
||||
id = re.search(r'\d+', id).group(0)
|
||||
img = i('img').attr('data-src')
|
||||
if img and 'url=' in img and 'http' not in img: img = f'{self.host}{img}'
|
||||
videos.append({
|
||||
'vod_id': id,
|
||||
'vod_name': i('a').attr('title'),
|
||||
'vod_pic': img,
|
||||
'vod_remarks': i('.public-prt').text() or i('.public-list-prb').text()
|
||||
})
|
||||
return videos
|
||||
|
||||
def getpq(self, data):
|
||||
try:
|
||||
return pq(data)
|
||||
except Exception as e:
|
||||
print(f"{str(e)}")
|
||||
return pq(data.encode('utf-8'))
|
||||
|
||||
def gettime(self):
|
||||
current_time = int(time.time())
|
||||
hourly_timestamp = current_time - (current_time % 3600)
|
||||
return hourly_timestamp
|
||||
|
||||
def sha256(self, text):
|
||||
sha = SHA256.new()
|
||||
sha.update(text.encode())
|
||||
return sha.hexdigest()
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
# coding=utf-8
|
||||
#!/usr/bin/env python3
|
||||
#七哥定制版
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# 禁用SSL证书验证警告
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "永乐视频"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.ylys.tv/"
|
||||
self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': self.host}
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False
|
||||
self.session.headers.update(self.headers)
|
||||
|
||||
def fetch(self, url, timeout=30):
|
||||
try:
|
||||
response = self.session.get(url, timeout=timeout, verify=False)
|
||||
response.encoding = response.encoding if response.encoding != 'ISO-8859-1' else 'UTF-8'
|
||||
return response
|
||||
except:
|
||||
return None
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {
|
||||
"class": [{'type_id': str(i), 'type_name': t} for i, t in enumerate(['电影', '剧集', '综艺', '动漫'], 1)],
|
||||
"filters": self._get_filters(),
|
||||
"list": []
|
||||
}
|
||||
rsp = self.fetch(self.host)
|
||||
if rsp and rsp.status_code == 200:
|
||||
result['list'] = self._extract_videos(rsp.text, 20)
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {"list": [], "page": int(pg), "pagecount": 99, "limit": 20, "total": 1980}
|
||||
url = f"{self.host}/vodtype/{tid}/page/{pg}/" if int(pg) > 1 else f"{self.host}/vodtype/{tid}/"
|
||||
rsp = self.fetch(url)
|
||||
if rsp and rsp.status_code == 200:
|
||||
result['list'] = self._extract_videos(rsp.text)
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
result = {"list": []}
|
||||
search_key = urllib.parse.quote(key)
|
||||
url = f"{self.host}/vodsearch/{search_key}-------------/page/{pg}/" if int(pg) > 1 else f"{self.host}/vodsearch/{search_key}-------------/"
|
||||
rsp = self.fetch(url)
|
||||
if rsp and rsp.status_code == 200:
|
||||
result['list'] = self._extract_search_results(rsp.text)
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {"list": []}
|
||||
vid = ids[0]
|
||||
rsp = self.fetch(f"{self.host}/voddetail/{vid}/")
|
||||
if not rsp or rsp.status_code != 200:
|
||||
return result
|
||||
|
||||
html = rsp.text
|
||||
play_from, play_url = self._extract_play_info(html, vid)
|
||||
|
||||
if play_from:
|
||||
result['list'] = [{
|
||||
'vod_id': vid,
|
||||
'vod_name': self._extract_title(html),
|
||||
'vod_pic': self._extract_pic(html),
|
||||
'vod_content': self._extract_desc(html),
|
||||
'vod_remarks': self._extract_remarks(html),
|
||||
'vod_play_from': "$$$".join(play_from),
|
||||
'vod_play_url': "$$$".join(play_url)
|
||||
}]
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {"parse": 1, "playUrl": "", "url": ""}
|
||||
if "-" not in id:
|
||||
return result
|
||||
|
||||
rsp = self.fetch(f"{self.host}/play/{id}/")
|
||||
if not rsp or rsp.status_code != 200:
|
||||
return result
|
||||
|
||||
real_url_match = re.search(r'var player_aaaa=.*?"url":"([^"]+\.m3u8)"', rsp.text, re.S | re.I)
|
||||
if real_url_match:
|
||||
real_url = real_url_match.group(1).replace(r'\u002F', '/').replace(r'\/', '/')
|
||||
result["parse"] = 0
|
||||
result["url"] = real_url
|
||||
else:
|
||||
result["url"] = f"{self.host}/play/{id}/"
|
||||
return result
|
||||
|
||||
def _get_filters(self):
|
||||
return {
|
||||
"1": [{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"},
|
||||
{"n": "爱情片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "恐怖片", "v": "11"}
|
||||
]}],
|
||||
"2": [{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "国产剧", "v": "13"}, {"n": "港台剧", "v": "14"},
|
||||
{"n": "日剧", "v": "15"}, {"n": "韩剧", "v": "33"}, {"n": "欧美剧", "v": "16"}
|
||||
]}],
|
||||
"3": [{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "内地综艺", "v": "27"}, {"n": "港台综艺", "v": "28"},
|
||||
{"n": "日本综艺", "v": "29"}, {"n": "韩国综艺", "v": "36"}
|
||||
]}],
|
||||
"4": [{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "国产动漫", "v": "31"}, {"n": "日本动漫", "v": "32"},
|
||||
{"n": "欧美动漫", "v": "42"}, {"n": "其他动漫", "v": "43"}
|
||||
]}]
|
||||
}
|
||||
|
||||
def _extract_videos(self, html, limit=0):
|
||||
videos = []
|
||||
pattern = r'<a href="/voddetail/(\d+)/".*?title="([^"]+)".*?<div class="module-item-note">([^<]+)</div>.*?data-original="([^"]+)"'
|
||||
for vid, title, remark, pic in re.findall(pattern, html, re.S | re.I):
|
||||
videos.append({
|
||||
'vod_id': vid.strip(),
|
||||
'vod_name': title.strip(),
|
||||
'vod_pic': (self.host + pic if pic.startswith('/') else pic).strip(),
|
||||
'vod_remarks': remark.strip()
|
||||
})
|
||||
return videos[:limit] if limit and len(videos) > limit else videos
|
||||
|
||||
def _extract_search_results(self, html):
|
||||
videos = []
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
for item in soup.select('.module-card-item'):
|
||||
link = item.select_one('a[href^="/voddetail/"]')
|
||||
if not link:
|
||||
continue
|
||||
|
||||
href = link.get('href', '')
|
||||
vid_match = re.search(r'/voddetail/(\d+)/', href)
|
||||
if not vid_match:
|
||||
continue
|
||||
|
||||
vid = vid_match.group(1)
|
||||
title_elem = item.select_one('.module-card-item-title strong')
|
||||
img_elem = item.select_one('img')
|
||||
pic = (img_elem.get('data-original') or img_elem.get('src')) if img_elem else ""
|
||||
note_elem = item.select_one('.module-item-note')
|
||||
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title_elem.get_text(strip=True) if title_elem else "",
|
||||
'vod_pic': self.host + pic if pic.startswith('/') else pic,
|
||||
'vod_remarks': note_elem.get_text(strip=True) if note_elem else ""
|
||||
})
|
||||
return videos
|
||||
|
||||
def _extract_play_info(self, html, vid):
|
||||
play_from, play_url = [], []
|
||||
line_pattern = r'<(?:div|a)[^>]*class="[^"]*module-tab-item[^"]*"[^>]*>(?:.*?<span>([^<]+)</span>.*?<small>(\d+)</small>|.*?<span>([^<]+)</span>.*?<small class="no">(\d+)</small>)</(?:div|a)>'
|
||||
|
||||
for match in re.findall(line_pattern, html, re.S | re.I):
|
||||
line_name = match[0] or match[2]
|
||||
if line_name in play_from:
|
||||
continue
|
||||
|
||||
play_from.append(line_name)
|
||||
line_id = self._get_line_id(html, vid, line_name)
|
||||
|
||||
ep_matches = re.findall(rf'<a class="module-play-list-link" href="/play/{vid}-{line_id}-(\d+)/"[^>]*>.*?<span>([^<]+)</span></a>', html, re.S | re.I)
|
||||
eps = [f"{ep_name.strip()}${vid}-{line_id}-{ep_num.strip()}" for ep_num, ep_name in ep_matches]
|
||||
play_url.append("#".join(eps))
|
||||
|
||||
return play_from, play_url
|
||||
|
||||
def _get_line_id(self, html, vid, line_name):
|
||||
line_id_match = re.search(rf'<a[^>]*href="/play/{vid}-(\d+)-1/"[^>]*>.*?<span>{re.escape(line_name)}</span>', html, re.S | re.I)
|
||||
if line_id_match:
|
||||
return line_id_match.group(1)
|
||||
|
||||
line_id_map = {"全球3线": "3", "大陆0线": "1", "大陆3线": "4", "大陆5线": "2", "大陆6线": "3"}
|
||||
return line_id_map.get(line_name, "1")
|
||||
|
||||
def _extract_title(self, html):
|
||||
title_match = re.search(r'<meta property="og:title" content="([^"]+)-[^-]+$"', html, re.S | re.I)
|
||||
return title_match.group(1).strip() if title_match else ""
|
||||
|
||||
def _extract_pic(self, html):
|
||||
pic_match = re.search(r'<meta property="og:image" content="([^"]+)"', html, re.S | re.I)
|
||||
pic = pic_match.group(1).strip() if pic_match else ""
|
||||
return self.host + pic if pic and pic.startswith('/') else pic
|
||||
|
||||
def _extract_desc(self, html):
|
||||
desc_match = re.search(r'<meta property="og:description" content="([^"]+)"', html, re.S | re.I)
|
||||
return desc_match.group(1).strip() if desc_match else "暂无简介"
|
||||
|
||||
def _extract_remarks(self, html):
|
||||
year_match = re.search(r'<a title="(\d+)" href="/vodshow/\d+-----------\1/">', html, re.S | re.I)
|
||||
year = year_match.group(1) if year_match else "未知年份"
|
||||
|
||||
area_match = re.search(r'<a title="([^"]+)" href="/vodshow/\d+-%E5%A2%A8%E8%A5%BF%E5%93%A5----------/">', html, re.S | re.I)
|
||||
area = area_match.group(1) if area_match else "未知产地"
|
||||
|
||||
type_match = re.search(r'vod_class":"([^"]+)"', html, re.S | re.I)
|
||||
type_str = type_match.group(1).replace(",", "/") if type_match else "未知类型"
|
||||
|
||||
return f"{year} | {area} | {type_str}"
|
||||
|
||||
# 本地测试
|
||||
if __name__ == "__main__":
|
||||
spider = Spider()
|
||||
spider.init()
|
||||
|
||||
# 测试详情页解析
|
||||
detail_result = spider.detailContent(["86027"])
|
||||
if detail_result['list']:
|
||||
detail = detail_result['list'][0]
|
||||
print(f"视频名称: {detail.get('vod_name', '未知')}")
|
||||
|
||||
# 测试搜索功能
|
||||
search_result = spider.searchContent("仙逆", False, 1)
|
||||
print(f"搜索结果数量: {len(search_result['list'])}")
|
||||
|
||||
# 测试播放功能
|
||||
play_result = spider.playerContent("", "86027-5-1", {})
|
||||
print(f"播放URL: {play_result.get('url', '')}")
|
||||
+436
@@ -0,0 +1,436 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 泥视频 - https://www.nivod.vip/
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import quote, unquote
|
||||
from pyquery import PyQuery as pq
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "泥视频"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://www.nivod.vip'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
"""获取首页内容和分类"""
|
||||
try:
|
||||
response = self.fetch_with_encoding(self.host, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
result = {}
|
||||
classes = []
|
||||
|
||||
# 获取分类导航
|
||||
nav_items = doc('.navbar a')
|
||||
for item in nav_items.items():
|
||||
text = item.text().strip()
|
||||
href = item.attr('href')
|
||||
if text and href and href != '/' and '/t/' in href:
|
||||
# 提取分类ID
|
||||
type_id = href.split('/t/')[-1].rstrip('/')
|
||||
if type_id.isdigit():
|
||||
classes.append({
|
||||
'type_name': text,
|
||||
'type_id': type_id
|
||||
})
|
||||
|
||||
# 获取首页视频列表
|
||||
videos = []
|
||||
video_items = doc('.module-item')
|
||||
for item in video_items.items():
|
||||
try:
|
||||
title = item.attr('title') or ''
|
||||
href = item.attr('href') or ''
|
||||
|
||||
if title and href:
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/nivod/')[-1].rstrip('/')
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
img_elem = item.find('img')
|
||||
pic = ''
|
||||
if img_elem:
|
||||
# 优先获取data-original(真实图片URL),然后是data-src,最后是src
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息
|
||||
note_elem = item.find('.module-item-note')
|
||||
remarks = note_elem.text() if note_elem else ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': remarks
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析视频项时出错: {e}")
|
||||
continue
|
||||
|
||||
result['class'] = classes
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取首页内容时出错: {e}")
|
||||
return {'class': [], 'list': []}
|
||||
|
||||
def homeVideoContent(self):
|
||||
"""获取推荐视频"""
|
||||
return {'list': []}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""获取分类内容"""
|
||||
try:
|
||||
# 构建分类URL
|
||||
url = f"{self.host}/t/{tid}/"
|
||||
if int(pg) > 1:
|
||||
url = f"{self.host}/t/{tid}/page/{pg}/"
|
||||
|
||||
response = self.fetch_with_encoding(url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取视频列表
|
||||
videos = []
|
||||
video_items = doc('.module-item')
|
||||
for item in video_items.items():
|
||||
try:
|
||||
title = item.attr('title') or ''
|
||||
href = item.attr('href') or ''
|
||||
|
||||
if title and href:
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/nivod/')[-1].rstrip('/')
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
img_elem = item.find('img')
|
||||
pic = ''
|
||||
if img_elem:
|
||||
# 优先获取data-original(真实图片URL),然后是data-src,最后是src
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息
|
||||
note_elem = item.find('.module-item-note')
|
||||
remarks = note_elem.text() if note_elem else ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': remarks
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析分类视频项时出错: {e}")
|
||||
continue
|
||||
|
||||
result = {
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999, # 设置一个较大的值
|
||||
'limit': 80,
|
||||
'total': 999999
|
||||
}
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取分类内容时出错: {e}")
|
||||
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 80, 'total': 0}
|
||||
|
||||
def detailContent(self, ids):
|
||||
"""获取视频详情"""
|
||||
try:
|
||||
vod_id = ids[0]
|
||||
url = f"{self.host}/nivod/{vod_id}/"
|
||||
|
||||
response = self.fetch_with_encoding(url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取标题
|
||||
title_elem = doc('h1')
|
||||
title = self.fix_encoding(title_elem.text()) if title_elem else ''
|
||||
|
||||
# 获取视频信息
|
||||
info_elem = doc('.module-info')
|
||||
content = self.fix_encoding(info_elem.text()) if info_elem else ''
|
||||
|
||||
# 获取播放源和播放列表
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# 获取播放源标签
|
||||
tab_items = doc('.module-tab-item')
|
||||
play_lists = doc('.module-play-list')
|
||||
|
||||
for i, tab in enumerate(tab_items.items()):
|
||||
# 分别提取播放源名称和集数
|
||||
span_elem = tab.find('span')
|
||||
small_elem = tab.find('small')
|
||||
|
||||
source_name = ''
|
||||
if span_elem:
|
||||
source_name = self.fix_encoding(span_elem.text().strip())
|
||||
# 如果有集数信息,添加到播放源名称后
|
||||
if small_elem:
|
||||
episode_count = self.fix_encoding(small_elem.text().strip())
|
||||
source_name = f"{source_name}{episode_count}"
|
||||
else:
|
||||
# 如果没有span元素,使用整个文本
|
||||
source_name = self.fix_encoding(tab.text().strip())
|
||||
|
||||
if source_name:
|
||||
play_from.append(source_name)
|
||||
|
||||
# 获取对应的播放列表
|
||||
episodes = []
|
||||
if i < len(play_lists):
|
||||
episode_items = play_lists.eq(i).find('a')
|
||||
for ep in episode_items.items():
|
||||
ep_title = self.fix_encoding(ep.text().strip())
|
||||
ep_href = ep.attr('href')
|
||||
if ep_title and ep_href:
|
||||
episodes.append(f"{ep_title}${ep_href}")
|
||||
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
vod = {
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': '',
|
||||
'vod_year': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': '',
|
||||
'vod_director': '',
|
||||
'vod_content': content,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url)
|
||||
}
|
||||
|
||||
return {'list': [vod]}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取视频详情时出错: {e}")
|
||||
return {'list': []}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
"""搜索内容"""
|
||||
try:
|
||||
# 使用正确的搜索URL格式
|
||||
search_url = f"{self.host}/s/-------------/"
|
||||
params = {'wd': key}
|
||||
|
||||
response = self.fetch_with_encoding(search_url, params=params, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 获取搜索结果
|
||||
videos = []
|
||||
video_items = doc('.module-item')
|
||||
for item in video_items.items():
|
||||
try:
|
||||
# 搜索页面的结构不同,需要从内部链接获取信息
|
||||
# 查找详情链接(通常是第一个或标题链接)
|
||||
detail_links = item.find('a[href*="/nivod/"]')
|
||||
if not detail_links:
|
||||
continue
|
||||
|
||||
# 获取第一个详情链接
|
||||
detail_link = detail_links.eq(0)
|
||||
href = detail_link.attr('href')
|
||||
|
||||
if not href:
|
||||
continue
|
||||
|
||||
# 提取视频ID
|
||||
vod_id = href.split('/nivod/')[-1].rstrip('/')
|
||||
|
||||
# 获取标题 - 尝试多种方式
|
||||
title = ''
|
||||
# 方法1: 从链接的strong标签获取
|
||||
strong_elem = detail_link.find('strong')
|
||||
if strong_elem:
|
||||
title = self.fix_encoding(strong_elem.text().strip())
|
||||
|
||||
# 方法2: 从图片的alt属性获取
|
||||
if not title:
|
||||
img_elem = item.find('img')
|
||||
if img_elem:
|
||||
title = self.fix_encoding(img_elem.attr('alt') or '')
|
||||
|
||||
# 方法3: 从链接文本获取
|
||||
if not title:
|
||||
title = self.fix_encoding(detail_link.text().strip())
|
||||
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# 获取图片 - 优先获取data-original(真实图片),避免懒加载占位图
|
||||
img_elem = item.find('img')
|
||||
pic = ''
|
||||
if img_elem:
|
||||
# 优先获取data-original(真实图片URL),然后是data-src,最后是src
|
||||
pic = img_elem.attr('data-original') or img_elem.attr('data-src') or img_elem.attr('src') or ''
|
||||
if pic and not pic.startswith('http'):
|
||||
pic = self.host + pic if pic.startswith('/') else ''
|
||||
|
||||
# 获取备注信息
|
||||
note_elem = item.find('.module-item-note')
|
||||
remarks = self.fix_encoding(note_elem.text()) if note_elem else ''
|
||||
|
||||
videos.append({
|
||||
'vod_id': vod_id,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_year': '',
|
||||
'vod_remarks': remarks
|
||||
})
|
||||
except Exception as e:
|
||||
self.log(f"解析搜索结果时出错: {e}")
|
||||
continue
|
||||
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"搜索时出错: {e}")
|
||||
return {'list': [], 'page': pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
"""获取播放地址"""
|
||||
try:
|
||||
# 播放页面URL
|
||||
play_url = f"{self.host}{id}"
|
||||
|
||||
response = self.fetch_with_encoding(play_url, headers=self.headers)
|
||||
doc = self.getpq(response.text)
|
||||
|
||||
# 查找播放器配置
|
||||
scripts = doc('script')
|
||||
for script in scripts.items():
|
||||
script_text = script.text()
|
||||
if 'player' in script_text and ('url' in script_text):
|
||||
# 尝试提取播放地址
|
||||
url_match = re.search(r'"url"\s*:\s*"([^"]+)"', script_text)
|
||||
if url_match:
|
||||
video_url = url_match.group(1)
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': video_url,
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
# 如果没有找到直接播放地址,返回播放页面让系统解析
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': play_url,
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"获取播放地址时出错: {e}")
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': f"{self.host}{id}",
|
||||
'header': self.headers
|
||||
}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def fix_encoding(self, text):
|
||||
"""修复UTF-8编码问题"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
try:
|
||||
# 检查是否包含乱码特征(常见的UTF-8乱码模式)
|
||||
garbled_patterns = [
|
||||
'\u00e4\u00b8', '\u00e5', '\u00e6', '\u00e7', '\u00e8', '\u00e9', # 常见乱码前缀
|
||||
'\u00c3\u00a4', '\u00c3\u00a5', '\u00c3\u00a6', # UTF-8被误解为Latin1
|
||||
'\u00ef\u00bc', '\u00e2\u0080' # 标点符号乱码
|
||||
]
|
||||
|
||||
has_garbled = any(pattern in text for pattern in garbled_patterns)
|
||||
|
||||
if has_garbled:
|
||||
self.log("检测到编码问题,尝试修复...")
|
||||
|
||||
# 方法1: 尝试Latin1->UTF-8转换
|
||||
try:
|
||||
fixed = text.encode('latin1').decode('utf-8')
|
||||
# 检查是否修复成功(包含中文字符)
|
||||
if re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log("使用Latin1->UTF-8修复成功")
|
||||
return fixed
|
||||
except Exception as e:
|
||||
self.log(f"Latin1->UTF-8修复失败: {e}")
|
||||
|
||||
# 方法2: 尝试其他编码转换
|
||||
encodings = ['cp1252', 'iso-8859-1']
|
||||
for encoding in encodings:
|
||||
try:
|
||||
fixed = text.encode(encoding).decode('utf-8')
|
||||
if re.search(r'[\u4e00-\u9fff]', fixed):
|
||||
self.log(f"使用{encoding}->UTF-8修复成功")
|
||||
return fixed
|
||||
except:
|
||||
continue
|
||||
|
||||
self.log("编码修复失败,返回原文本")
|
||||
|
||||
return text
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"编码修复异常: {e}")
|
||||
return text
|
||||
|
||||
def fetch_with_encoding(self, url, **kwargs):
|
||||
"""带编码处理的请求方法"""
|
||||
try:
|
||||
response = self.fetch(url, **kwargs)
|
||||
# 确保使用UTF-8编码
|
||||
response.encoding = 'utf-8'
|
||||
return response
|
||||
except Exception as e:
|
||||
self.log(f"请求失败: {e}")
|
||||
raise
|
||||
|
||||
def getpq(self, text):
|
||||
"""安全的pyquery解析"""
|
||||
try:
|
||||
return pq(text)
|
||||
except Exception as e:
|
||||
self.log(f"pyquery解析出错: {e}")
|
||||
try:
|
||||
return pq(text.encode('utf-8'))
|
||||
except:
|
||||
return pq('')
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
# 温馨提示:搜索只能搜拼音联想
|
||||
# 播放需要挂代理
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from Crypto.Hash import MD5
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.uid = self.getuid()
|
||||
self.token, self.code = self.getuserinfo()
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host = 'https://tvapi211.magicetech.com'
|
||||
|
||||
headers = {'User-Agent': 'okhttp/3.11.0'}
|
||||
|
||||
def homeContent(self, filter):
|
||||
body = {'token': self.token, 'authcode': self.code}
|
||||
data = self.post(f'{self.host}/hr_1_1_0/apptvapi/web/index.php/video/filter-header', json=self.getbody(body),
|
||||
headers=self.headers).json()
|
||||
result = {}
|
||||
classes = []
|
||||
filters = {}
|
||||
for k in data['data']:
|
||||
classes.append({
|
||||
'type_name': k['channel_name'],
|
||||
'type_id': str(k['channel_id']),
|
||||
})
|
||||
filters[str(k['channel_id'])] = []
|
||||
for i in k['search_box']:
|
||||
if len(i['list']):
|
||||
filters[str(k['channel_id'])].append({
|
||||
'key': i['field'],
|
||||
'name': i['label'],
|
||||
'value': [{'n': j['display'], 'v': str(j['value'])} for j in i['list'] if j['value']]
|
||||
})
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
body = {'token': self.token, 'authcode': self.code}
|
||||
data = self.post(f'{self.host}/hr_1_1_0/apptvapi/web/index.php/video/index-tv', json=self.getbody(body),
|
||||
headers=self.headers).json()
|
||||
return {'list': self.getlist(data['data'][0]['banner'])}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {'token': self.token, 'authcode': self.code, 'channel_id': tid, 'area': extend.get('area', '0'),
|
||||
'year': extend.get('year', '0'), 'sort': extend.get('sort', '0'), 'tag': extend.get('tag', 'hot'),
|
||||
'status': extend.get('status', '0'), 'page_num': pg, 'page_size': '24'}
|
||||
data = self.post(f'{self.host}/hr_1_1_0/apptvapi/web/index.php/video/filter-video', json=self.getbody(body),
|
||||
headers=self.headers).json()
|
||||
result = {}
|
||||
result['list'] = self.getlist(data['data']['list'])
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
ids = ids[0].split('@')
|
||||
body = {'token': self.token, 'authcode': self.code, 'channel_id': ids[0], 'video_id': ids[1]}
|
||||
data = self.post(f'{self.host}/hr_1_1_0/apptvapi/web/index.php/video/detail', json=self.getbody(body),
|
||||
headers=self.headers).json()
|
||||
vdata = {}
|
||||
for k in data['data']['chapters']:
|
||||
i = k['sourcelist']
|
||||
for j in i:
|
||||
if j['source_name'] not in vdata: vdata[j['source_name']] = []
|
||||
vdata[j['source_name']].append(f"{k['title']}${j['source_url']}")
|
||||
plist, names = [], []
|
||||
for key, value in vdata.items():
|
||||
names.append(key)
|
||||
plist.append('#'.join(value))
|
||||
vod = {
|
||||
'vod_play_from': '$$$'.join(names),
|
||||
'vod_play_url': '$$$'.join(plist),
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
body = {'token': self.token, 'authcode': self.code, 'keyword': key, 'page_num': pg}
|
||||
data = self.post(f'{self.host}/hr_1_1_0/apptvapi/web/index.php/search/letter-result', json=self.getbody(body),
|
||||
headers=self.headers).json()
|
||||
return {'list': self.getlist(data['data']['list'])}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
# https://rysp.tv
|
||||
# https://aigua.tv
|
||||
result = {
|
||||
"parse": 0,
|
||||
"url": id,
|
||||
"header": {
|
||||
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 11; M2012K10C Build/RP1A.200720.011)",
|
||||
"Origin": "https://aigua.tv",
|
||||
"Referer": "https://aigua.tv/"
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
|
||||
def getuserinfo(self):
|
||||
data = self.post(f'{self.host}/hr_1_1_0/apptvapi/web/index.php/user/auth-login', json=self.getbody(),
|
||||
headers=self.headers).json()
|
||||
v = data['data']
|
||||
return v['user_token'], v['authcode']
|
||||
|
||||
def getuid(self):
|
||||
uid = self.getCache('uid')
|
||||
if not uid:
|
||||
uid = str(uuid.uuid4())
|
||||
self.setCache('uid', uid)
|
||||
return uid
|
||||
|
||||
def getbody(self, json_data=None):
|
||||
if json_data is None: json_data = {}
|
||||
params = {"product": "4", "ver": "1.1.0", "debug": "1", "appId": "1", "osType": "3", "marketChannel": "tv",
|
||||
"sysVer": "11", "time": str(int(time.time())), "packageName": "com.gzsptv.gztvvideo",
|
||||
"udid": self.uid, }
|
||||
json_data.update(params)
|
||||
sorted_json = dict(sorted(json_data.items(), key=lambda item: item[0]))
|
||||
text = '&'.join(f"{k}={v}" for k, v in sorted_json.items() if v != '')
|
||||
md5_hash = self.md5(f"jI7POOBbmiUZ0lmi{text}D9ShYdN51ksWptpkTu11yenAJu7Zu3cR").upper()
|
||||
json_data.update({'sign': md5_hash})
|
||||
return json_data
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def getlist(self, data):
|
||||
videos = []
|
||||
for i in data:
|
||||
if type(i.get('video')) == dict: i = i['video']
|
||||
videos.append({
|
||||
'vod_id': f"{i.get('channel_id')}@{i.get('video_id')}",
|
||||
'vod_name': i.get('video_name'),
|
||||
'vod_pic': i.get('cover'),
|
||||
'vod_year': i.get('score'),
|
||||
'vod_remarks': i.get('flag'),
|
||||
})
|
||||
return videos
|
||||
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from base64 import b64decode, b64encode
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Hash import MD5
|
||||
from Crypto.Util.Padding import unpad, pad
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.ut = False
|
||||
# self.did, self.ntid =self.getdid()
|
||||
self.did, self.ntid = 'e59eb2465f61b9ca','65a0de19b3a2ec93fa479ad6'
|
||||
self.token, self.uid = self.gettoken()
|
||||
self.phost, self.phz,self.mphost=self.getpic()
|
||||
# self.phost, self.phz,self.mphost = ('https://dbtp.tgydy.com','.log','https://dplay.nbzsmc.com')
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
host='http://192.151.245.34:8089'
|
||||
|
||||
def md5(self, text):
|
||||
h = MD5.new()
|
||||
h.update(text.encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
|
||||
def uuid(self):
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def getdid(self):
|
||||
did = self.random_str(16)
|
||||
ntid = self.random_str(24)
|
||||
return did, ntid
|
||||
# try:
|
||||
# if self.getCache('did'):
|
||||
# return self.getCache('did'), self.getCache('ntid')
|
||||
# else:
|
||||
# self.setCache('did', did)
|
||||
# self.setCache('ntid', ntid)
|
||||
# return did, ntid
|
||||
# except Exception as e:
|
||||
# self.setCache('did', did)
|
||||
# self.setCache('ntid', ntid)
|
||||
# return did, ntid
|
||||
|
||||
def aes(self, text, bool=True):
|
||||
key = b64decode('c0k4N1RfKTY1U1cjJERFRA==')
|
||||
iv = b64decode('VzIjQWRDVkdZSGFzSEdEVA==')
|
||||
if bool:
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
ct_bytes = cipher.encrypt(pad(text.encode("utf-8"), AES.block_size))
|
||||
ct = b64encode(ct_bytes).decode("utf-8")
|
||||
return ct
|
||||
else:
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size)
|
||||
ptt=json.loads(pt.decode("utf-8"))
|
||||
return ptt
|
||||
|
||||
def random_str(self,length=24):
|
||||
hex_chars = '0123456789abcdef'
|
||||
return ''.join(random.choice(hex_chars) for _ in range(length))
|
||||
|
||||
def gettoken(self):
|
||||
params={"deviceId":self.did,"deviceModel":"8848钛晶手机","devicePlatform":"1","tenantId":self.ntid}
|
||||
data=self.getdata('/supports/anonyLogin',params)
|
||||
self.ut=True
|
||||
return data['data']['token'], data['data']['userId']
|
||||
|
||||
def getdata(self,path,params=None):
|
||||
t = int(time.time()*1000)
|
||||
n=self.md5(f'{self.uuid()}{t}')
|
||||
if params:
|
||||
ct=self.aes(json.dumps(params))
|
||||
else:
|
||||
ct=f'{t}{n}'
|
||||
s=self.md5(f'{ct}[email protected]')
|
||||
headers = {
|
||||
'User-Agent': 'okhttp-okgo/jeasonlzy',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8',
|
||||
'tenantId': self.ntid,
|
||||
'n': n,
|
||||
't': str(int(t/1000)),
|
||||
's': s,
|
||||
}
|
||||
if self.ut:
|
||||
headers['ta-token'] = self.token
|
||||
headers['userId'] = self.uid
|
||||
if params:
|
||||
params={'ct':ct}
|
||||
response = self.post(f'{self.host}{path}', headers=headers, json=params).text
|
||||
else:
|
||||
response = self.fetch(f'{self.host}{path}', headers=headers).text
|
||||
data=self.aes(response[1:-1],False)
|
||||
return data
|
||||
|
||||
def getpic(self):
|
||||
try:
|
||||
at = int(time.time() * 1000)
|
||||
t=str(int(at/ 1000))
|
||||
n = self.md5(f'{self.uuid()}{at}')
|
||||
headers = {
|
||||
'Host': '192.151.245.34:8089',
|
||||
'User-Agent': 'okhttp-okgo/jeasonlzy',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8',
|
||||
'tenantId': self.ntid,
|
||||
'userId': self.uid,
|
||||
'ta-token': self.token,
|
||||
'n': n,
|
||||
't': t,
|
||||
's': self.md5(f'{t}{n}[email protected]')
|
||||
}
|
||||
params = {
|
||||
'tenantId': self.ntid,
|
||||
}
|
||||
response = self.fetch(f'{self.host}/supports/configs', params=params, headers=headers).text
|
||||
data=self.aes(response[1:-1],False)
|
||||
config = {
|
||||
'image_cdn': '',
|
||||
'image_cdn_path': '',
|
||||
'cdn-domain': ''
|
||||
}
|
||||
for item in data.get('data', []):
|
||||
name = item.get('name')
|
||||
records = item.get('records', [])
|
||||
|
||||
if name in config and records:
|
||||
value = records[0].get('value', '')
|
||||
if name == 'cdn-domain':
|
||||
value = value.split('#')[0]
|
||||
config[name] = value
|
||||
|
||||
return config['image_cdn'], config['image_cdn_path'], config['cdn-domain']
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in getpic: {e}")
|
||||
return 'https://dbtp.tgydy.com', '.log', 'https://dplay.nbzsmc.com'
|
||||
|
||||
def getlist(self,data):
|
||||
vod=[]
|
||||
for i in data:
|
||||
vod.append({
|
||||
'vod_id': f'{i.get("movieId")}@{i.get("entryNum")}',
|
||||
'vod_name': i.get('title'),
|
||||
'vod_pic': f'{self.getProxyUrl()}&path={i.get("thumbnail")}',
|
||||
'vod_year': i.get('score'),
|
||||
'vod_remarks': f'{i.get("entryNum")}集'
|
||||
})
|
||||
return vod
|
||||
|
||||
def homeContent(self, filter):
|
||||
data=self.getdata('/movies/classifies')
|
||||
result = {}
|
||||
cateManual = {
|
||||
"榜单": "ranking/getTodayHotRank",
|
||||
"专辑": "getTMovieFolderPage",
|
||||
"剧场": "getClassMoviePage2",
|
||||
"演员": "follow/getRecommendActorPage",
|
||||
}
|
||||
classes = []
|
||||
for k in cateManual:
|
||||
classes.append({
|
||||
'type_name': k,
|
||||
'type_id': cateManual[k]
|
||||
})
|
||||
filters = {}
|
||||
if data.get('data'):
|
||||
filters["getClassMoviePage2"] = [
|
||||
{
|
||||
"key": "type",
|
||||
"name": "分类",
|
||||
"value": [
|
||||
{"n": item["name"], "v": item["classifyId"]}
|
||||
for item in data["data"]
|
||||
]
|
||||
}
|
||||
]
|
||||
filters["ranking/getTodayHotRank"] = [
|
||||
{
|
||||
"key": "type",
|
||||
"name": "榜单",
|
||||
"value": [
|
||||
{"n": "播放榜", "v": "getWeekHotPlayRank"},
|
||||
{"n": "高赞榜", "v": "getWeekStarRank"},
|
||||
{"n": "追剧榜", "v": "getSubTMoviePage"},
|
||||
{"n": "高分榜", "v": "ranking/getScoreRank"}
|
||||
]
|
||||
}
|
||||
]
|
||||
filters["follow/getRecommendActorPage"] = [
|
||||
{
|
||||
"key": "type",
|
||||
"name": "性别",
|
||||
"value": [
|
||||
{"n": "男", "v": "0"},
|
||||
{"n": "女", "v": "1"}
|
||||
]
|
||||
}
|
||||
]
|
||||
result['class'] = classes
|
||||
result['filters'] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
params = {"pageNo":"1","pageSize":"30","platform":"1","deviceId":self.did,"tenantId":self.ntid}
|
||||
data=self.getdata('/news/getRecommendTMoviePage',params)
|
||||
vod=self.getlist(data['data']['records'])
|
||||
return {'list':vod}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params={}
|
||||
path = f'/news/{tid}'
|
||||
if tid=='getClassMoviePage2':
|
||||
parama={"pageNo":pg,"pageSize":"30","orderFlag":"0","haveActor":"-1","classifyId":extend.get('type','-1'),"tagId":""}
|
||||
elif 'rank' in tid:
|
||||
path=f'/news/{extend.get("type") or tid}'
|
||||
parama={"pageNo":pg,"pageSize":"30"}
|
||||
elif 'follow' in tid:
|
||||
parama={"pageNo":pg,"pageSize":"20"}
|
||||
if extend.get('type'):
|
||||
path=f'/news/getActorPage'
|
||||
parama={"pageNo":pg,"pageSize":"50","sex":extend.get('type')}
|
||||
elif tid=='getTMovieFolderPage':
|
||||
parama={"pageNo":pg,"pageSize":"20"}
|
||||
elif '@' in tid:
|
||||
path='/news/getActorTMoviePage'
|
||||
parama={"id":tid.split('@')[0],"pageNo":pg,"pageSize":"30"}
|
||||
params['platform'] = '1'
|
||||
params['deviceId'] = self.did
|
||||
params['tenantId'] = self.ntid
|
||||
data=self.getdata(path,parama)
|
||||
vods=[]
|
||||
if 'follow' in tid:
|
||||
for i in data['data']['records']:
|
||||
vods.append({
|
||||
'vod_id': f'{i.get("id")}@',
|
||||
'vod_name': i.get('name'),
|
||||
'vod_pic': i.get('avatar'),
|
||||
'vod_tag': 'folder',
|
||||
'vod_remarks': f'作品{i.get("movieNum")}',
|
||||
'style': {"type": "oval"}
|
||||
})
|
||||
else:
|
||||
vdata=data['data']['records']
|
||||
if tid=='getTMovieFolderPage':
|
||||
vdata=[j for i in data['data']['records'] for j in i['movieList']]
|
||||
vods=self.getlist(vdata)
|
||||
result = {}
|
||||
result['list'] = vods
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
ids=ids[0].split('@')
|
||||
params = {"pageNo": "1", "pageSize": ids[1], "movieId": ids[0], "platform": "1", "deviceId": self.did, "tenantId": self.ntid}
|
||||
data = self.getdata('/news/getEntryPage', params)
|
||||
print(data)
|
||||
plist=[f'第{i.get("entryNum")}集${i.get("mp4PlayAddress") or i.get("playAddress")}' for i in data['data']['records']]
|
||||
vod = {
|
||||
'vod_play_from': '爱看短剧',
|
||||
'vod_play_url': '#'.join(plist),
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
params = {"pageNo": pg, "pageSize": "20", "keyWord": key, "orderFlag": "0", "platform": "1", "deviceId": self.did, "tenantId": self.ntid}
|
||||
data = self.getdata('/news/searchTMoviePage', params)
|
||||
vod = self.getlist(data['data']['records'])
|
||||
return {'list':vod,'page':pg}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
return {'parse': 0, 'url': f'{self.mphost}{id}', 'header': {'User-Agent':'Dalvik/2.1.0 (Linux; U; Android 11; M2012K10C Build/RP1A.200720.011)'}}
|
||||
|
||||
def localProxy(self, param):
|
||||
type=param.get('path').split('.')[-1]
|
||||
data=self.fetch(f'{self.phost}{param.get("path")}{self.phz}',headers={'User-Agent':'Dalvik/2.1.0 (Linux; U; Android 11; M2012K10C Build/RP1A.200720.011)'})
|
||||
def decrypt(encrypted_text):
|
||||
try:
|
||||
key = base64.urlsafe_b64decode("iM41VipvCFtToAFFRExEXw==")
|
||||
iv = base64.urlsafe_b64decode("0AXRTXzmMSrlRSemWb4sVQ==")
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted_padded = cipher.decrypt(encrypted_text)
|
||||
decrypted_data = unpad(decrypted_padded, AES.block_size)
|
||||
return decrypted_data
|
||||
except (binascii.Error, ValueError):
|
||||
return None
|
||||
return [200, f'image/{type}', decrypt(data.content)]
|
||||
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
# by嗷呜(finally)
|
||||
import sys
|
||||
import os
|
||||
sys.path.append("..")
|
||||
import re
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import string
|
||||
from Crypto.Util.Padding import unpad
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5, AES
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "电影猎手"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.device = self.device_id()
|
||||
self.host = self.gethost()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
t = str(int(time.time()))
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
filters = {}
|
||||
classes = []
|
||||
bba = self.url()
|
||||
data = self.fetch(f"{self.host}/api/v1/app/config?pack={bba[0]}&signature={bba[1]}", headers=self.header()).text
|
||||
data1 = self.aes(data)
|
||||
dy = {"class":"类型","area":"地区","lang":"语言","year":"年份","letter":"字母","by":"排序","sort":"排序"}
|
||||
data1['data']['movie_screen']['sort'].pop(0)
|
||||
for item in data1['data']['movie_screen']['sort']:
|
||||
item['n'] = item.pop('name')
|
||||
item['v'] = item.pop('value')
|
||||
for item in data1['data']['movie_screen']['filter']:
|
||||
has_non_empty_field = False
|
||||
classes.append({"type_name": item["name"], "type_id": str(item["id"])})
|
||||
for key in dy:
|
||||
if key in item and item[key]:
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["id"])] = []
|
||||
filters[str(item["id"])].append(
|
||||
{"key": 'sort', "name": '排序', "value": data1['data']['movie_screen']['sort']})
|
||||
for dkey in item:
|
||||
if dkey in dy and item[dkey]:
|
||||
item[dkey].pop(0)
|
||||
value_array = [
|
||||
{"n": value.strip(), "v": value.strip()}
|
||||
for value in item[dkey]
|
||||
if value.strip() != ""
|
||||
]
|
||||
filters[str(item["id"])].append(
|
||||
{"key": dkey, "name": dy[dkey], "value": value_array}
|
||||
)
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
bba = self.url()
|
||||
url = f'{self.host}/api/v1/movie/index_recommend?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()
|
||||
videos = []
|
||||
for item in data['data']:
|
||||
if len(item['list']) > 0:
|
||||
for it in item['list']:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"type_id": tid, "sort": extend.get("sort", "by_default"), "class": extend.get("class", "类型"),
|
||||
"area": extend.get("area", "地区"), "year": extend.get("year", "年份"), "page": str(pg),
|
||||
"pageSize": "21"}
|
||||
result = {}
|
||||
list = []
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/screen/list?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data']['list']
|
||||
for item in data:
|
||||
list.append(self.voides(item))
|
||||
result["list"] = list
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = {"id": ids[0]}
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie/detail?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
video = {'vod_name': data.get('name'),'type_name': data.get('type_name'),'vod_year': data.get('year'),'vod_area': data.get('area'),'vod_remarks': data.get('dynami'),'vod_content': data.get('content')}
|
||||
play = []
|
||||
names = []
|
||||
tasks = []
|
||||
for itt in data["play_from"]:
|
||||
name = itt["name"]
|
||||
a = []
|
||||
if len(itt["list"]) > 0:
|
||||
names.append(name)
|
||||
play.append(self.playeach(itt['list']))
|
||||
else:
|
||||
tasks.append({"movie_id": ids[0], "from_code": itt["code"]})
|
||||
names.append(name)
|
||||
if tasks:
|
||||
with ThreadPoolExecutor(max_workers=len(tasks)) as executor:
|
||||
results = executor.map(self.playlist, tasks)
|
||||
for result in results:
|
||||
if result:
|
||||
play.append(result)
|
||||
else:
|
||||
play.append("")
|
||||
video["vod_play_from"] = "$$$".join(names)
|
||||
video["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [video]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
body = {"keyword": key, "sort": "", "type_id": "0", "page": str(pg), "pageSize": "10",
|
||||
"res_type": "by_movie_name"}
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/search?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data'].get('list')
|
||||
videos = []
|
||||
for it in data:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos, "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id
|
||||
if "m3u8" not in url and "mp4" not in url:
|
||||
try:
|
||||
add = id.split('|||')
|
||||
data = {"from_code": add[0], "play_url": add[1], "episode_id": add[2], "type": "play"}
|
||||
bba = self.url(data)
|
||||
data2 = self.fetch(f"{self.host}/api/v1/movie_addr/parse_url?pack={bba[0]}&signature={bba[1]}",
|
||||
headers=self.header()).json()['data']
|
||||
url = data2.get('play_url') or data2.get('download_url')
|
||||
try:
|
||||
url1 = self.fetch(url, headers=self.header(), allow_redirects=False).headers['Location']
|
||||
if url1 and "http" in url1:
|
||||
url = url1
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
if '.jpg' in url or '.jpeg' in url or '.png' in url:
|
||||
url = self.getProxyUrl() + "&url=" + b64encode(url.encode('utf-8')).decode('utf-8') + "&type=m3u8"
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["url"] = url
|
||||
result["header"] = {'user-agent': 'okhttp/4.9.2'}
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
url = b64decode(param["url"]).decode('utf-8')
|
||||
durl = url[:url.rfind('/')]
|
||||
data = self.fetch(url, headers=self.header()).content.decode("utf-8")
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
# if 'URI="' in string and 'http' not in string:
|
||||
# lines[index] = index
|
||||
# 暂时预留,貌似用不到
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
lines[index] = durl + ('' if string.startswith('/') else '/') + string
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def device_id(self):
|
||||
characters = string.ascii_lowercase + string.digits
|
||||
random_string = ''.join(random.choices(characters, k=32))
|
||||
return random_string
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/4.9.2',
|
||||
'Connection': 'Keep-Alive',
|
||||
}
|
||||
response = self.fetch('https://app-site.ecoliving168.com/domain_v5.json', headers=headers).json()
|
||||
url = response['api_service'].replace('/api/', '')
|
||||
return url
|
||||
|
||||
def header(self):
|
||||
headers = {
|
||||
'User-Agent': 'Android',
|
||||
'Accept': 'application/prs.55App.v2+json',
|
||||
'timestamp': self.t,
|
||||
'x-client-setting': '{"pure-mode":1}',
|
||||
'x-client-uuid': '{"device_id":' + self.device + '}, "type":1,"brand":"Redmi", "model":"M2012K10C", "system_version":30, "sdk_version":"3.1.0.7"}',
|
||||
'x-client-version': '3096 '
|
||||
}
|
||||
return headers
|
||||
|
||||
def url(self, id=None):
|
||||
if not id:
|
||||
id = {}
|
||||
id["timestamp"] = self.t
|
||||
public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA02F/kPg5A2NX4qZ5JSns+bjhVMCC6JbTiTKpbgNgiXU+Kkorg6Dj76gS68gB8llhbUKCXjIdygnHPrxVHWfzmzisq9P9awmXBkCk74Skglx2LKHa/mNz9ivg6YzQ5pQFUEWS0DfomGBXVtqvBlOXMCRxp69oWaMsnfjnBV+0J7vHbXzUIkqBLdXSNfM9Ag5qdRDrJC3CqB65EJ3ARWVzZTTcXSdMW9i3qzEZPawPNPe5yPYbMZIoXLcrqvEZnRK1oak67/ihf7iwPJqdc+68ZYEmmdqwunOvRdjq89fQMVelmqcRD9RYe08v+xDxG9Co9z7hcXGTsUquMxkh29uNawIDAQAB'
|
||||
encrypted_text = json.dumps(id)
|
||||
public_key = RSA.import_key(b64decode(public_key))
|
||||
cipher = PKCS1_v1_5.new(public_key)
|
||||
encrypted_message = cipher.encrypt(encrypted_text.encode('utf-8'))
|
||||
encrypted_message_base64 = b64encode(encrypted_message).decode('utf-8')
|
||||
result = encrypted_message_base64.replace('+', '-').replace('/', '_').replace('=', '')
|
||||
key = '635a580fcb5dc6e60caa39c31a7bde48'
|
||||
sign = hmac.new(key.encode(), result.encode(), hashlib.md5).hexdigest()
|
||||
return result, sign
|
||||
|
||||
def playlist(self, body):
|
||||
try:
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie_addr/list?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
return self.playeach(data)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def playeach(self,data):
|
||||
play_urls = []
|
||||
for it in data:
|
||||
if re.search(r"mp4|m3u8", it["play_url"]):
|
||||
play_urls.append(f"{it['episode_name']}${it['play_url']}")
|
||||
else:
|
||||
play_urls.append(
|
||||
f"{it['episode_name']}${it['from_code']}|||{it['play_url']}|||{it['episode_id']}"
|
||||
)
|
||||
return '#'.join(play_urls)
|
||||
|
||||
def voides(self, item):
|
||||
if item['name'] or item['title']:
|
||||
voide = {
|
||||
"vod_id": item.get('id') or item.get('click'),
|
||||
'vod_name': item.get('name') or item.get('title'),
|
||||
'vod_pic': item.get('cover') or item.get('image'),
|
||||
'vod_year': item.get('year') or item.get('label'),
|
||||
'vod_remarks': item.get('dynamic') or item.get('sub_title')
|
||||
}
|
||||
return voide
|
||||
|
||||
def aes(self, text):
|
||||
text = text.replace('-', '+').replace('_', '/') + '=='
|
||||
key = b"e6d5de5fcc51f53d"
|
||||
iv = b"2f13eef7dfc6c613"
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size).decode("utf-8")
|
||||
return json.loads(pt)
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# by @嗷呜
|
||||
import sys
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
ahost='https://api.cenguigui.cn'
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="134", "Google Chrome";v="134"',
|
||||
'DNT': '1',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
}
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [{'type_id': '推荐榜', 'type_name': '🔥 推荐榜'},
|
||||
{'type_id': '新剧', 'type_name': '🎬 新剧'},
|
||||
{'type_id': '逆袭', 'type_name': '🎬 逆袭'},
|
||||
{'type_id': '霸总', 'type_name': '🎬 霸总'},
|
||||
{'type_id': '现代言情', 'type_name': '🎬 现代言情'},
|
||||
{'type_id': '打脸虐渣', 'type_name': '🎬 打脸虐渣'},
|
||||
{'type_id': '豪门恩怨', 'type_name': '🎬 豪门恩怨'},
|
||||
{'type_id': '神豪', 'type_name': '🎬 神豪'},
|
||||
{'type_id': '马甲', 'type_name': '🎬 马甲'},
|
||||
{'type_id': '都市日常', 'type_name': '🎬 都市日常'},
|
||||
{'type_id': '战神归来', 'type_name': '🎬 战神归来'},
|
||||
{'type_id': '小人物', 'type_name': '🎬 小人物'},
|
||||
{'type_id': '女性成长', 'type_name': '🎬 女性成长'},
|
||||
{'type_id': '大女主', 'type_name': '🎬 大女主'},
|
||||
{'type_id': '穿越', 'type_name': '🎬 穿越'},
|
||||
{'type_id': '都市修仙', 'type_name': '🎬 都市修仙'},
|
||||
{'type_id': '强者回归', 'type_name': '🎬 强者回归'},
|
||||
{'type_id': '亲情', 'type_name': '🎬 亲情'},
|
||||
{'type_id': '古装', 'type_name': '🎬 古装'},
|
||||
{'type_id': '重生', 'type_name': '🎬 重生'},
|
||||
{'type_id': '闪婚', 'type_name': '🎬 闪婚'},
|
||||
{'type_id': '赘婿逆袭', 'type_name': '🎬 赘婿逆袭'},
|
||||
{'type_id': '虐恋', 'type_name': '🎬 虐恋'},
|
||||
{'type_id': '追妻', 'type_name': '🎬 追妻'},
|
||||
{'type_id': '天下无敌', 'type_name': '🎬 天下无敌'},
|
||||
{'type_id': '家庭伦理', 'type_name': '🎬 家庭伦理'},
|
||||
{'type_id': '萌宝', 'type_name': '🎬 萌宝'},
|
||||
{'type_id': '古风权谋', 'type_name': '🎬 古风权谋'},
|
||||
{'type_id': '职场', 'type_name': '🎬 职场'},
|
||||
{'type_id': '奇幻脑洞', 'type_name': '🎬 奇幻脑洞'},
|
||||
{'type_id': '异能', 'type_name': '🎬 异能'},
|
||||
{'type_id': '无敌神医', 'type_name': '🎬 无敌神医'},
|
||||
{'type_id': '古风言情', 'type_name': '🎬 古风言情'},
|
||||
{'type_id': '传承觉醒', 'type_name': '🎬 传承觉醒'},
|
||||
{'type_id': '现言甜宠', 'type_name': '🎬 现言甜宠'},
|
||||
{'type_id': '奇幻爱情', 'type_name': '🎬 奇幻爱情'},
|
||||
{'type_id': '乡村', 'type_name': '🎬 乡村'},
|
||||
{'type_id': '历史古代', 'type_name': '🎬 历史古代'},
|
||||
{'type_id': '王妃', 'type_name': '🎬 王妃'},
|
||||
{'type_id': '高手下山', 'type_name': '🎬 高手下山'},
|
||||
{'type_id': '娱乐圈', 'type_name': '🎬 娱乐圈'},
|
||||
{'type_id': '强强联合', 'type_name': '🎬 强强联合'},
|
||||
{'type_id': '破镜重圆', 'type_name': '🎬 破镜重圆'},
|
||||
{'type_id': '暗恋成真', 'type_name': '🎬 暗恋成真'},
|
||||
{'type_id': '民国', 'type_name': '🎬 民国'},
|
||||
{'type_id': '欢喜冤家', 'type_name': '🎬 欢喜冤家'},
|
||||
{'type_id': '系统', 'type_name': '🎬 系统'},
|
||||
{'type_id': '真假千金', 'type_name': '🎬 真假千金'},
|
||||
{'type_id': '龙王', 'type_name': '🎬 龙王'},
|
||||
{'type_id': '校园', 'type_name': '🎬 校园'},
|
||||
{'type_id': '穿书', 'type_name': '🎬 穿书'},
|
||||
{'type_id': '女帝', 'type_name': '🎬 女帝'},
|
||||
{'type_id': '团宠', 'type_name': '🎬 团宠'},
|
||||
{'type_id': '年代爱情', 'type_name': '🎬 年代爱情'},
|
||||
{'type_id': '玄幻仙侠', 'type_name': '🎬 玄幻仙侠'},
|
||||
{'type_id': '青梅竹马', 'type_name': '🎬 青梅竹马'},
|
||||
{'type_id': '悬疑推理', 'type_name': '🎬 悬疑推理'},
|
||||
{'type_id': '皇后', 'type_name': '🎬 皇后'},
|
||||
{'type_id': '替身', 'type_name': '🎬 替身'},
|
||||
{'type_id': '大叔', 'type_name': '🎬 大叔'},
|
||||
{'type_id': '喜剧', 'type_name': '🎬 喜剧'},
|
||||
{'type_id': '剧情', 'type_name': '🎬 剧情'}]}
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
pass
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
params = {
|
||||
'classname': tid,
|
||||
'offset': str((int(pg) - 1)),
|
||||
}
|
||||
data = self.fetch(f'{self.ahost}/api/duanju/api.php', params=params, headers=self.headers).json()
|
||||
videos = []
|
||||
for k in data['data']:
|
||||
videos.append({
|
||||
'vod_id': k.get('book_id'),
|
||||
'vod_name': k.get('title'),
|
||||
'vod_pic': k.get('cover'),
|
||||
'vod_year': k.get('score'),
|
||||
'vod_remarks': f"{k.get('sub_title')}|{k.get('episode_cnt')}"
|
||||
})
|
||||
result = {}
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
v=self.fetch(f'{self.ahost}/api/duanju/api.php', params={'book_id': ids[0]}, headers=self.headers).json()
|
||||
vod = {
|
||||
'type_name': v.get('category'),
|
||||
'vod_year': v.get('time'),
|
||||
'vod_remarks': v.get('duration'),
|
||||
'vod_content': v.get('desc'),
|
||||
'vod_play_from': '嗷呜爱看短剧',
|
||||
'vod_play_url': '#'.join([f"{i['title']}${i['video_id']}" for i in v['data']])
|
||||
}
|
||||
return {'list':[vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.categoryContent(key, pg, True, {})
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
data=self.fetch(f'{self.ahost}/api/duanju/api.php', params={'video_id': id}, headers=self.headers).json()
|
||||
return {'parse': 0, 'url': data['data']['url'], 'header': self.headers}
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,217 @@
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
self.name = "电影云集"
|
||||
self.host = "https://dyyjpro.com"
|
||||
self.timeout = 10000
|
||||
self.limit = 20
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
}
|
||||
self.default_image = "https://picsum.photos/300/400"
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def init(self, extend=""):
|
||||
print(f"============{extend}============")
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [
|
||||
{"type_name": "电影", "type_id": "dianying"},
|
||||
{"type_name": "剧集", "type_id": "剧集"},
|
||||
{"type_name": "动漫", "type_id": "dongman"},
|
||||
{"type_name": "综艺", "type_id": "zongyi"},
|
||||
{"type_name": "短剧", "type_id": "短剧"},
|
||||
{"type_name": "学习", "type_id": "xuexi"},
|
||||
{"type_name": "读物", "type_id": "读物"},
|
||||
{"type_name": "音频", "type_id": "音频"}
|
||||
]
|
||||
}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {}
|
||||
url = f"{self.host}/category/{tid}/" if pg == 1 else f"{self.host}/category/{tid}/page/{pg}/"
|
||||
|
||||
try:
|
||||
rsp = self.fetch(url, headers=self.headers, timeout=self.timeout)
|
||||
if rsp:
|
||||
videos = self._parse_video_list(rsp.text)
|
||||
result.update({
|
||||
'list': videos,
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': self.limit,
|
||||
'total': 999999
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Category parse error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
def _parse_video_list(self, html_text):
|
||||
videos = []
|
||||
|
||||
def build_full_url(href):
|
||||
if href.startswith("http"):
|
||||
return href
|
||||
return f"{self.host}{href}" if href.startswith("/") else f"{self.host}/{href}"
|
||||
|
||||
try:
|
||||
pattern = r'<article[^>]*class="[^"]*post-item[^"]*"[^>]*>(.*?)</article>'
|
||||
for match in re.finditer(pattern, html_text, re.S):
|
||||
item_html = match.group(1)
|
||||
href_match = re.search(r'<a[^>]*href="([^"]*)"[^>]*>', item_html, re.S)
|
||||
if not href_match:
|
||||
continue
|
||||
href = href_match.group(1)
|
||||
title_match = re.search(r'<a[^>]*title="([^"]*)"', item_html, re.S)
|
||||
title = title_match.group(1).strip() if title_match else ""
|
||||
|
||||
if not title:
|
||||
h2_match = re.search(r'<h2[^>]*>(.*?)</h2>', item_html, re.S)
|
||||
if h2_match:
|
||||
title = re.sub(r'<[^>]+>', '', h2_match.group(1)).strip()
|
||||
|
||||
if not href or not title:
|
||||
continue
|
||||
img_match = re.search(r'<img[^>]*src="([^"]*)"[^>]*>', item_html, re.S)
|
||||
if not img_match:
|
||||
img_match = re.search(r'data-bg="([^"]*)"', item_html, re.S)
|
||||
|
||||
img_url = img_match.group(1) if img_match else self.default_image
|
||||
|
||||
videos.append({
|
||||
"vod_id": build_full_url(href),
|
||||
"vod_name": title,
|
||||
"vod_pic": build_full_url(img_url) if img_url.startswith("/") else img_url,
|
||||
"vod_remarks": "",
|
||||
"vod_content": title
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Parse video list error: {e}")
|
||||
return videos[:self.limit]
|
||||
|
||||
def detailContent(self, array):
|
||||
result = {'list': []}
|
||||
if array:
|
||||
try:
|
||||
vod_id = array[0]
|
||||
detail_url = vod_id if vod_id.startswith("http") else f"{self.host}{vod_id}"
|
||||
rsp = self.fetch(detail_url, headers=self.headers, timeout=self.timeout)
|
||||
if rsp:
|
||||
vod = self._parse_detail_page(rsp.text, detail_url)
|
||||
if vod:
|
||||
result['list'] = [vod]
|
||||
except Exception as e:
|
||||
print(f"Detail parse error: {e}")
|
||||
return result
|
||||
|
||||
def _parse_detail_page(self, html_text, detail_url):
|
||||
try:
|
||||
title_match = re.search(r'<h1[^>]*>(.*?)</h1>', html_text, re.S)
|
||||
title = title_match.group(1).strip() if title_match else "未知标题"
|
||||
title = re.sub(r'<[^>]+>', '', title).strip()
|
||||
content_match = re.search(r'<div[^>]*class="[^"]*post-content[^"]*"[^>]*>.*?<p>(.*?)</p>', html_text, re.S)
|
||||
content = content_match.group(1).strip() if content_match else title
|
||||
img_match = re.search(r'<meta[^>]*property="og:image"[^>]*content="([^"]*)"', html_text, re.S)
|
||||
if not img_match:
|
||||
img_match = re.search(r'<img[^>]*class="[^"]*wp-post-image[^"]*"[^>]*src="([^"]*)"', html_text, re.S)
|
||||
img_url = img_match.group(1) if img_match else self.default_image
|
||||
if img_url and not img_url.startswith("http"):
|
||||
img_url = f"{self.host}{img_url}" if img_url.startswith("/") else f"{self.host}/{img_url}"
|
||||
pan_links = []
|
||||
link_pattern = r'<a[^>]*href="([^"]*)"[^>]*>.*?</a>'
|
||||
for match in re.finditer(link_pattern, html_text, re.S):
|
||||
href = match.group(1)
|
||||
if href and ("pan.baidu.com" in href or "pan.quark.cn" in href):
|
||||
pan_links.append(href)
|
||||
play_from = []
|
||||
play_url = []
|
||||
baidu_links = [link for link in pan_links if "pan.baidu.com" in link]
|
||||
quark_links = [link for link in pan_links if "pan.quark.cn" in link]
|
||||
all_links = []
|
||||
if baidu_links:
|
||||
all_links.extend([f"百度网盘${link}" for link in baidu_links])
|
||||
if quark_links:
|
||||
all_links.extend([f"夸克网盘${link}" for link in quark_links])
|
||||
if all_links:
|
||||
play_from.append("电影云集")
|
||||
play_url.append("#".join(all_links))
|
||||
else:
|
||||
play_from = ["无资源"]
|
||||
play_url = ["暂无资源$#"]
|
||||
|
||||
return {
|
||||
"vod_id": detail_url,
|
||||
"vod_name": title,
|
||||
"vod_pic": img_url,
|
||||
"vod_content": content,
|
||||
"vod_remarks": f"共{len(pan_links)}个网盘源" if pan_links else "暂无网盘资源",
|
||||
"vod_play_from": "$$$".join(play_from),
|
||||
"vod_play_url": "$$$".join(play_url)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Parse detail page error: {e}")
|
||||
return {
|
||||
"vod_id": detail_url,
|
||||
"vod_name": "未知标题",
|
||||
"vod_pic": self.default_image,
|
||||
"vod_content": f"加载详情页失败:{str(e)}",
|
||||
"vod_remarks": "",
|
||||
"vod_play_from": "无资源",
|
||||
"vod_play_url": "暂无资源$#"
|
||||
}
|
||||
|
||||
def searchContent(self, key, quick, pg):
|
||||
result = {'list': []}
|
||||
try:
|
||||
encoded_key = key.replace(" ", "+")
|
||||
url = f"{self.host}/?cat=&s={encoded_key}" if pg == 1 else f"{self.host}/page/{pg}?cat=&s={encoded_key}"
|
||||
rsp = self.fetch(url, headers=self.headers, timeout=self.timeout)
|
||||
if rsp:
|
||||
result['list'] = self._parse_video_list(rsp.text)
|
||||
except Exception as e:
|
||||
print(f"Search error: {e}")
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
if id.startswith("http"):
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": f"push://{id}",
|
||||
"header": json.dumps(self.headers)
|
||||
}
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": id,
|
||||
"header": json.dumps(self.headers)
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
return {
|
||||
'list': []
|
||||
}
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
video_formats = ['.mp4', '.m3u8', '.flv', '.avi', '.mkv', '.wmv', '.rmvb', '.mov']
|
||||
return any(url.lower().endswith(fmt) for fmt in video_formats)
|
||||
|
||||
def localProxy(self, url, param):
|
||||
return {
|
||||
"parse": 0,
|
||||
"playUrl": "",
|
||||
"url": url,
|
||||
"header": ""
|
||||
}
|
||||
|
||||
def manualVideoCheck(self, url):
|
||||
return True
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
# by嗷呜(finally)
|
||||
import sys
|
||||
import os
|
||||
sys.path.append("..")
|
||||
import re
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import string
|
||||
from Crypto.Util.Padding import unpad
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5, AES
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
import time
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def getName(self):
|
||||
return "电影猎手"
|
||||
|
||||
def init(self, extend=""):
|
||||
self.device = self.device_id()
|
||||
self.host = self.gethost()
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def action(self, action):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
t = str(int(time.time()))
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
filters = {}
|
||||
classes = []
|
||||
bba = self.url()
|
||||
data = self.fetch(f"{self.host}/api/v1/app/config?pack={bba[0]}&signature={bba[1]}", headers=self.header()).text
|
||||
data1 = self.aes(data)
|
||||
dy = {"class":"类型","area":"地区","lang":"语言","year":"年份","letter":"字母","by":"排序","sort":"排序"}
|
||||
data1['data']['movie_screen']['sort'].pop(0)
|
||||
for item in data1['data']['movie_screen']['sort']:
|
||||
item['n'] = item.pop('name')
|
||||
item['v'] = item.pop('value')
|
||||
for item in data1['data']['movie_screen']['filter']:
|
||||
has_non_empty_field = False
|
||||
classes.append({"type_name": item["name"], "type_id": str(item["id"])})
|
||||
for key in dy:
|
||||
if key in item and item[key]:
|
||||
has_non_empty_field = True
|
||||
break
|
||||
if has_non_empty_field:
|
||||
filters[str(item["id"])] = []
|
||||
filters[str(item["id"])].append(
|
||||
{"key": 'sort', "name": '排序', "value": data1['data']['movie_screen']['sort']})
|
||||
for dkey in item:
|
||||
if dkey in dy and item[dkey]:
|
||||
item[dkey].pop(0)
|
||||
value_array = [
|
||||
{"n": value.strip(), "v": value.strip()}
|
||||
for value in item[dkey]
|
||||
if value.strip() != ""
|
||||
]
|
||||
filters[str(item["id"])].append(
|
||||
{"key": dkey, "name": dy[dkey], "value": value_array}
|
||||
)
|
||||
result["class"] = classes
|
||||
result["filters"] = filters
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
bba = self.url()
|
||||
url = f'{self.host}/api/v1/movie/index_recommend?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()
|
||||
videos = []
|
||||
for item in data['data']:
|
||||
if len(item['list']) > 0:
|
||||
for it in item['list']:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos}
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
body = {"type_id": tid, "sort": extend.get("sort", "by_default"), "class": extend.get("class", "类型"),
|
||||
"area": extend.get("area", "地区"), "year": extend.get("year", "年份"), "page": str(pg),
|
||||
"pageSize": "21"}
|
||||
result = {}
|
||||
list = []
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/screen/list?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data']['list']
|
||||
for item in data:
|
||||
list.append(self.voides(item))
|
||||
result["list"] = list
|
||||
result["page"] = pg
|
||||
result["pagecount"] = 9999
|
||||
result["limit"] = 90
|
||||
result["total"] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
body = {"id": ids[0]}
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie/detail?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
video = {'vod_name': data.get('name'),'type_name': data.get('type_name'),'vod_year': data.get('year'),'vod_area': data.get('area'),'vod_remarks': data.get('dynami'),'vod_content': data.get('content')}
|
||||
play = []
|
||||
names = []
|
||||
tasks = []
|
||||
for itt in data["play_from"]:
|
||||
name = itt["name"]
|
||||
a = []
|
||||
if len(itt["list"]) > 0:
|
||||
names.append(name)
|
||||
play.append(self.playeach(itt['list']))
|
||||
else:
|
||||
tasks.append({"movie_id": ids[0], "from_code": itt["code"]})
|
||||
names.append(name)
|
||||
if tasks:
|
||||
with ThreadPoolExecutor(max_workers=len(tasks)) as executor:
|
||||
results = executor.map(self.playlist, tasks)
|
||||
for result in results:
|
||||
if result:
|
||||
play.append(result)
|
||||
else:
|
||||
play.append("")
|
||||
video["vod_play_from"] = "$$$".join(names)
|
||||
video["vod_play_url"] = "$$$".join(play)
|
||||
result = {"list": [video]}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg=1):
|
||||
body = {"keyword": key, "sort": "", "type_id": "0", "page": str(pg), "pageSize": "10",
|
||||
"res_type": "by_movie_name"}
|
||||
bba = self.url(body)
|
||||
url = f"{self.host}/api/v1/movie/search?pack={bba[0]}&signature={bba[1]}"
|
||||
data = self.fetch(url, headers=self.header()).json()['data'].get('list')
|
||||
videos = []
|
||||
for it in data:
|
||||
try:
|
||||
videos.append(self.voides(it))
|
||||
except Exception as e:
|
||||
continue
|
||||
result = {"list": videos, "page": pg}
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id
|
||||
if "m3u8" not in url and "mp4" not in url:
|
||||
try:
|
||||
add = id.split('|||')
|
||||
data = {"from_code": add[0], "play_url": add[1], "episode_id": add[2], "type": "play"}
|
||||
bba = self.url(data)
|
||||
data2 = self.fetch(f"{self.host}/api/v1/movie_addr/parse_url?pack={bba[0]}&signature={bba[1]}",
|
||||
headers=self.header()).json()['data']
|
||||
url = data2.get('play_url') or data2.get('download_url')
|
||||
try:
|
||||
url1 = self.fetch(url, headers=self.header(), allow_redirects=False).headers['Location']
|
||||
if url1 and "http" in url1:
|
||||
url = url1
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
pass
|
||||
if '.jpg' in url or '.jpeg' in url or '.png' in url:
|
||||
url = self.getProxyUrl() + "&url=" + b64encode(url.encode('utf-8')).decode('utf-8') + "&type=m3u8"
|
||||
result = {}
|
||||
result["parse"] = 0
|
||||
result["url"] = url
|
||||
result["header"] = {'user-agent': 'okhttp/4.9.2'}
|
||||
return result
|
||||
|
||||
def localProxy(self, param):
|
||||
url = b64decode(param["url"]).decode('utf-8')
|
||||
durl = url[:url.rfind('/')]
|
||||
data = self.fetch(url, headers=self.header()).content.decode("utf-8")
|
||||
lines = data.strip().split('\n')
|
||||
for index, string in enumerate(lines):
|
||||
# if 'URI="' in string and 'http' not in string:
|
||||
# lines[index] = index
|
||||
# 暂时预留,貌似用不到
|
||||
if '#EXT' not in string and 'http' not in string:
|
||||
lines[index] = durl + ('' if string.startswith('/') else '/') + string
|
||||
data = '\n'.join(lines)
|
||||
return [200, "application/vnd.apple.mpegur", data]
|
||||
|
||||
def device_id(self):
|
||||
characters = string.ascii_lowercase + string.digits
|
||||
random_string = ''.join(random.choices(characters, k=32))
|
||||
return random_string
|
||||
|
||||
def gethost(self):
|
||||
headers = {
|
||||
'User-Agent': 'okhttp/4.9.2',
|
||||
'Connection': 'Keep-Alive',
|
||||
}
|
||||
response = self.fetch('https://app-site.ecoliving168.com/domain_v5.json', headers=headers).json()
|
||||
url = response['api_service'].replace('/api/', '')
|
||||
return url
|
||||
|
||||
def header(self):
|
||||
headers = {
|
||||
'User-Agent': 'Android',
|
||||
'Accept': 'application/prs.55App.v2+json',
|
||||
'timestamp': self.t,
|
||||
'x-client-setting': '{"pure-mode":1}',
|
||||
'x-client-uuid': '{"device_id":' + self.device + '}, "type":1,"brand":"Redmi", "model":"M2012K10C", "system_version":30, "sdk_version":"3.1.0.7"}',
|
||||
'x-client-version': '3096 '
|
||||
}
|
||||
return headers
|
||||
|
||||
def url(self, id=None):
|
||||
if not id:
|
||||
id = {}
|
||||
id["timestamp"] = self.t
|
||||
public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA02F/kPg5A2NX4qZ5JSns+bjhVMCC6JbTiTKpbgNgiXU+Kkorg6Dj76gS68gB8llhbUKCXjIdygnHPrxVHWfzmzisq9P9awmXBkCk74Skglx2LKHa/mNz9ivg6YzQ5pQFUEWS0DfomGBXVtqvBlOXMCRxp69oWaMsnfjnBV+0J7vHbXzUIkqBLdXSNfM9Ag5qdRDrJC3CqB65EJ3ARWVzZTTcXSdMW9i3qzEZPawPNPe5yPYbMZIoXLcrqvEZnRK1oak67/ihf7iwPJqdc+68ZYEmmdqwunOvRdjq89fQMVelmqcRD9RYe08v+xDxG9Co9z7hcXGTsUquMxkh29uNawIDAQAB'
|
||||
encrypted_text = json.dumps(id)
|
||||
public_key = RSA.import_key(b64decode(public_key))
|
||||
cipher = PKCS1_v1_5.new(public_key)
|
||||
encrypted_message = cipher.encrypt(encrypted_text.encode('utf-8'))
|
||||
encrypted_message_base64 = b64encode(encrypted_message).decode('utf-8')
|
||||
result = encrypted_message_base64.replace('+', '-').replace('/', '_').replace('=', '')
|
||||
key = '635a580fcb5dc6e60caa39c31a7bde48'
|
||||
sign = hmac.new(key.encode(), result.encode(), hashlib.md5).hexdigest()
|
||||
return result, sign
|
||||
|
||||
def playlist(self, body):
|
||||
try:
|
||||
bba = self.url(body)
|
||||
url = f'{self.host}/api/v1/movie_addr/list?pack={bba[0]}&signature={bba[1]}'
|
||||
data = self.fetch(url, headers=self.header()).json()['data']
|
||||
return self.playeach(data)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def playeach(self,data):
|
||||
play_urls = []
|
||||
for it in data:
|
||||
if re.search(r"mp4|m3u8", it["play_url"]):
|
||||
play_urls.append(f"{it['episode_name']}${it['play_url']}")
|
||||
else:
|
||||
play_urls.append(
|
||||
f"{it['episode_name']}${it['from_code']}|||{it['play_url']}|||{it['episode_id']}"
|
||||
)
|
||||
return '#'.join(play_urls)
|
||||
|
||||
def voides(self, item):
|
||||
if item['name'] or item['title']:
|
||||
voide = {
|
||||
"vod_id": item.get('id') or item.get('click'),
|
||||
'vod_name': item.get('name') or item.get('title'),
|
||||
'vod_pic': item.get('cover') or item.get('image'),
|
||||
'vod_year': item.get('year') or item.get('label'),
|
||||
'vod_remarks': item.get('dynamic') or item.get('sub_title')
|
||||
}
|
||||
return voide
|
||||
|
||||
def aes(self, text):
|
||||
text = text.replace('-', '+').replace('_', '/') + '=='
|
||||
key = b"e6d5de5fcc51f53d"
|
||||
iv = b"2f13eef7dfc6c613"
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(b64decode(text)), AES.block_size).decode("utf-8")
|
||||
return json.loads(pt)
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author : Doubebly
|
||||
# @Time : 2025/1/21 23:07
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "JieYingShi"
|
||||
|
||||
def init(self, extend):
|
||||
self.home_url = 'https://www.hkybqufgh.com'
|
||||
self.error_url = 'https://json.doube.eu.org/error/4gtv/index.m3u8'
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
}
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
|
||||
return {'class': [
|
||||
{
|
||||
'type_id': '1',
|
||||
'type_name': '电影'
|
||||
},
|
||||
{
|
||||
'type_id': '2',
|
||||
'type_name': '电视剧'
|
||||
},
|
||||
{
|
||||
'type_id': '4',
|
||||
'type_name': '动漫'
|
||||
},
|
||||
{
|
||||
'type_id': '3',
|
||||
'type_name': '综艺'
|
||||
}
|
||||
]}
|
||||
|
||||
def homeVideoContent(self):
|
||||
a = self.get_data(self.home_url)
|
||||
return {'list': a, 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
url = self.home_url + f'/vod/show/id/{cid}/page/{page}'
|
||||
data = self.get_data(url)
|
||||
return {'list': data, 'parse': 0, 'jx': 0}
|
||||
|
||||
|
||||
def detailContent(self, did):
|
||||
ids = did[0]
|
||||
data = self.get_detail_data(ids)
|
||||
return {"list": data, 'parse': 0, 'jx': 0}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
if int(page) > 1:
|
||||
return {'list': [], 'parse': 0, 'jx': 0}
|
||||
url = self.home_url + f'/vod/search/{key}'
|
||||
data = self.get_data(url)
|
||||
return {'list': data, 'parse': 0, 'jx': 0}
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
url = self.get_play_data(pid)
|
||||
return {"url": url, "header": self.headers, "parse": 1, "jx": 0}
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
return '正在Destroy'
|
||||
|
||||
|
||||
def get_data(self, url):
|
||||
data = []
|
||||
try:
|
||||
res = requests.get(url, headers=self.headers)
|
||||
if res.status_code != 200:
|
||||
return data
|
||||
vod_id_s = re.findall(r'\\"vodId\\":(.*?),', res.text)
|
||||
vod_name_s = re.findall(r'\\"vodName\\":\\"(.*?)\\"', res.text)
|
||||
vod_pic_s = re.findall(r'\\"vodPic\\":\\"(.*?)\\"', res.text)
|
||||
vod_remarks_s = re.findall(r'\\"vodRemarks\\":\\"(.*?)\\"', res.text)
|
||||
|
||||
for i in range(len(vod_id_s)):
|
||||
data.append(
|
||||
{
|
||||
'vod_id': vod_id_s[i],
|
||||
'vod_name': vod_name_s[i],
|
||||
'vod_pic': vod_pic_s[i],
|
||||
'vod_remarks': vod_remarks_s[i],
|
||||
}
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
print(e)
|
||||
return data
|
||||
|
||||
def get_detail_data(self, ids):
|
||||
url = self.home_url + f'/api/mw-movie/anonymous/video/detail?id={ids}'
|
||||
t = str(int(time.time() * 1000))
|
||||
headers = self.get_headers(t, f'id={ids}&key=cb808529bae6b6be45ecfab29a4889bc&t={t}')
|
||||
try:
|
||||
res = requests.get(url, headers=headers)
|
||||
if res.status_code != 200:
|
||||
return []
|
||||
i = res.json()['data']
|
||||
urls = []
|
||||
for ii in res.json()['data']['episodeList']:
|
||||
name = ii['name']
|
||||
url = ii['nid']
|
||||
urls.append(f'{name}${ids}-{url}')
|
||||
data = {
|
||||
'type_name': i['vodClass'],
|
||||
'vod_id': i['vodId'],
|
||||
'vod_name': i['vodName'],
|
||||
'vod_remarks': i['vodRemarks'],
|
||||
'vod_year': i['vodYear'],
|
||||
'vod_area': i['vodArea'],
|
||||
'vod_actor': i['vodActor'],
|
||||
'vod_director': i['vodDirector'],
|
||||
'vod_content': i['vodContent'],
|
||||
'vod_play_from': '默认',
|
||||
'vod_play_url': '#'.join(urls),
|
||||
|
||||
}
|
||||
return [data]
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(e)
|
||||
return []
|
||||
|
||||
def get_play_data(self, play):
|
||||
info = play.split('-')
|
||||
_id = info[0]
|
||||
_pid = info[1]
|
||||
url = self.home_url + f'/api/mw-movie/anonymous/v2/video/episode/url?id={_id}&nid={_pid}'
|
||||
t = str(int(time.time() * 1000))
|
||||
headers = self.get_headers(t, f'id={_id}&nid={_pid}&key=cb808529bae6b6be45ecfab29a4889bc&t={t}')
|
||||
try:
|
||||
res = requests.get(url, headers=headers)
|
||||
if res.status_code != 200:
|
||||
return self.error_url
|
||||
return res.json()['data']['list'][0]['url']
|
||||
except requests.RequestException as e:
|
||||
print(e)
|
||||
return self.error_url
|
||||
|
||||
@staticmethod
|
||||
def get_headers(t, e):
|
||||
sign = hashlib.sha1(hashlib.md5(e.encode()).hexdigest().encode()).hexdigest()
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'sign': sign,
|
||||
'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
||||
't': t,
|
||||
'referer': 'https://www.hkybqufgh.com/',
|
||||
}
|
||||
return headers
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author : Doubebly
|
||||
# @Time : 2025/5/29 22:07
|
||||
|
||||
|
||||
import sys
|
||||
import hashlib
|
||||
import time
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "Aidianying"
|
||||
|
||||
def init(self, extend):
|
||||
self.home_url = 'https://m.sdzhgt.com/'
|
||||
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
|
||||
self.error_url = "https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4"
|
||||
|
||||
def getDependence(self):
|
||||
return []
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
return {
|
||||
'class': [{'type_id': '1', 'type_name': '电影'},
|
||||
{'type_id': '2', 'type_name': '电视剧'},
|
||||
{'type_id': '3', 'type_name': '综艺'},
|
||||
{'type_id': '4', 'type_name': '动漫'}],
|
||||
'filters': {
|
||||
'1': [
|
||||
{'key': 'type',
|
||||
'name': '类型',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '喜剧', 'v': '/type/22'},
|
||||
{'n': '动作', 'v': '/type/23'},
|
||||
{'n': '科幻', 'v': '/type/30'},
|
||||
{'n': '爱情', 'v': '/type/26'},
|
||||
{'n': '悬疑', 'v': '/type/27'},
|
||||
{'n': '奇幻', 'v': '/type/87'},
|
||||
{'n': '剧情', 'v': '/type/37'},
|
||||
{'n': '恐怖', 'v': '/type/36'},
|
||||
{'n': '犯罪', 'v': '/type/35'},
|
||||
{'n': '动画', 'v': '/type/33'},
|
||||
{'n': '惊悚', 'v': '/type/34'},
|
||||
{'n': '战争', 'v': '/type/25'},
|
||||
{'n': '冒险', 'v': '/type/31'},
|
||||
{'n': '灾难', 'v': '/type/81'},
|
||||
{'n': '伦理', 'v': '/type/83'},
|
||||
{'n': '其他', 'v': '/type/43'}]},
|
||||
{'key': 'area',
|
||||
'name': '地区',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '中国大陆', 'v': '/area/中国大陆'},
|
||||
{'n': '中国香港', 'v': '/area/中国香港'},
|
||||
{'n': '中国台湾', 'v': '/area/中国台湾'},
|
||||
{'n': '美国', 'v': '/area/美国'},
|
||||
{'n': '日本', 'v': '/area/日本'},
|
||||
{'n': '韩国', 'v': '/area/韩国'},
|
||||
{'n': '印度', 'v': '/area/印度'},
|
||||
{'n': '泰国', 'v': '/area/泰国'},
|
||||
{'n': '其他', 'v': '/area/其他'}]},
|
||||
{'key': 'year',
|
||||
'name': '年份',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '2024', 'v': '/year/2024'},
|
||||
{'n': '2023', 'v': '/year/2023'},
|
||||
{'n': '2022', 'v': '/year/2022'},
|
||||
{'n': '2021', 'v': '/year/2021'},
|
||||
{'n': '2020', 'v': '/year/2020'},
|
||||
{'n': '2019', 'v': '/year/2019'},
|
||||
{'n': '2018', 'v': '/year/2018'},
|
||||
{'n': '2017', 'v': '/year/2017'},
|
||||
{'n': '2016', 'v': '/year/2016'},
|
||||
{'n': '2015', 'v': '/year/2015'},
|
||||
{'n': '2014', 'v': '/year/2014'},
|
||||
{'n': '2013', 'v': '/year/2013'},
|
||||
{'n': '2012', 'v': '/year/2012'},
|
||||
{'n': '2011', 'v': '/year/2011'},
|
||||
{'n': '2010', 'v': '/year/2010'},
|
||||
{'n': '2009~2000', 'v': '/year/2009~2000'}]},
|
||||
{'key': 'lang',
|
||||
'name': '语言',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '国语', 'v': '/lang/国语'},
|
||||
{'n': '英语', 'v': '/lang/英语'},
|
||||
{'n': '粤语', 'v': '/lang/粤语'},
|
||||
{'n': '韩语', 'v': '/lang/韩语'},
|
||||
{'n': '日语', 'v': '/lang/日语'},
|
||||
{'n': '其他', 'v': '/lang/其他'}]},
|
||||
{'key': 'by',
|
||||
'name': '排序',
|
||||
'value': [{'n': '上映时间', 'v': '/sortType/1/sortOrder/0'},
|
||||
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
|
||||
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
|
||||
],
|
||||
'2': [
|
||||
{'key': 'type',
|
||||
'name': '类型',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '国产剧', 'v': '/type/14'},
|
||||
{'n': '欧美剧', 'v': '/type/15'},
|
||||
{'n': '港台剧', 'v': '/type/16'},
|
||||
{'n': '日韩剧', 'v': '/type/62'},
|
||||
{'n': '其他剧', 'v': '/type/68'}]},
|
||||
{'key': 'class',
|
||||
'name': '剧情',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '古装', 'v': '/class/古装'},
|
||||
{'n': '战争', 'v': '/class/战争'},
|
||||
{'n': '喜剧', 'v': '/class/喜剧'},
|
||||
{'n': '家庭', 'v': '/class/家庭'},
|
||||
{'n': '犯罪', 'v': '/class/犯罪'},
|
||||
{'n': '动作', 'v': '/class/动作'},
|
||||
{'n': '奇幻', 'v': '/class/奇幻'},
|
||||
{'n': '剧情', 'v': '/class/剧情'},
|
||||
{'n': '历史', 'v': '/class/历史'},
|
||||
{'n': '短片', 'v': '/class/短片'}]},
|
||||
{'key': 'area',
|
||||
'name': '地区',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '中国大陆', 'v': '/area/中国大陆'},
|
||||
{'n': '中国香港', 'v': '/area/中国香港'},
|
||||
{'n': '中国台湾', 'v': '/area/中国台湾'},
|
||||
{'n': '日本', 'v': '/area/日本'},
|
||||
{'n': '韩国', 'v': '/area/韩国'},
|
||||
{'n': '美国', 'v': '/area/美国'},
|
||||
{'n': '泰国', 'v': '/area/泰国'},
|
||||
{'n': '其他', 'v': '/area/其他'}]},
|
||||
{'key': 'year',
|
||||
'name': '时间',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '2024', 'v': '/year/2024'},
|
||||
{'n': '2023', 'v': '/year/2023'},
|
||||
{'n': '2022', 'v': '/year/2022'},
|
||||
{'n': '2021', 'v': '/year/2021'},
|
||||
{'n': '2020', 'v': '/year/2020'},
|
||||
{'n': '2019', 'v': '/year/2019'},
|
||||
{'n': '2018', 'v': '/year/2018'},
|
||||
{'n': '2017', 'v': '/year/2017'},
|
||||
{'n': '2016', 'v': '/year/2016'},
|
||||
{'n': '2015', 'v': '/year/2015'},
|
||||
{'n': '2014', 'v': '/year/2014'},
|
||||
{'n': '2013', 'v': '/year/2013'},
|
||||
{'n': '2012', 'v': '/year/2012'},
|
||||
{'n': '2011', 'v': '/year/2011'},
|
||||
{'n': '2010', 'v': '/year/2010'}]},
|
||||
{'key': 'lang',
|
||||
'name': '语言',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '普通话', 'v': '/lang/普通话'},
|
||||
{'n': '英语', 'v': '/lang/英语'},
|
||||
{'n': '粤语', 'v': '/lang/粤语'},
|
||||
{'n': '韩语', 'v': '/lang/韩语'},
|
||||
{'n': '日语', 'v': '/lang/日语'},
|
||||
{'n': '泰语', 'v': '/lang/泰语'},
|
||||
{'n': '其他', 'v': '/lang/其他'}, ]},
|
||||
{'key': 'by',
|
||||
'name': '排序',
|
||||
'value': [{'n': '最近更新', 'v': '/sortType/1/sortOrder/0'},
|
||||
{'n': '添加时间', 'v': '/sortType/2/sortOrder/0'},
|
||||
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
|
||||
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
|
||||
],
|
||||
'3': [
|
||||
{'key': 'type',
|
||||
'name': '类型',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '国产综艺', 'v': '/type/69'},
|
||||
{'n': '港台综艺', 'v': '/type/70'},
|
||||
{'n': '日韩综艺', 'v': '/type/72'},
|
||||
{'n': '欧美综艺', 'v': '/type/73'}]},
|
||||
{'key': 'class',
|
||||
'name': '剧情',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '真人秀', 'v': '/class/真人秀'},
|
||||
{'n': '音乐', 'v': '/class/音乐'},
|
||||
{'n': '脱口秀', 'v': '/class/脱口秀'}]},
|
||||
{'key': 'area',
|
||||
'name': '地区',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '中国大陆', 'v': '/area/中国大陆'},
|
||||
{'n': '中国香港', 'v': '/area/中国香港'},
|
||||
{'n': '中国台湾', 'v': '/area/中国台湾'},
|
||||
{'n': '日本', 'v': '/area/日本'},
|
||||
{'n': '韩国', 'v': '/area/韩国'},
|
||||
{'n': '美国', 'v': '/area/美国'},
|
||||
{'n': '其他', 'v': '/area/其他'}]},
|
||||
{'key': 'year',
|
||||
'name': '时间',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '2024', 'v': '/year/2024'},
|
||||
{'n': '2023', 'v': '/year/2023'},
|
||||
{'n': '2022', 'v': '/year/2022'},
|
||||
{'n': '2021', 'v': '/year/2021'},
|
||||
{'n': '2020', 'v': '/year/2020'}]},
|
||||
{'key': 'lang',
|
||||
'name': '语言',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '国语', 'v': '/lang/国语'},
|
||||
{'n': '英语', 'v': '/lang/英语'},
|
||||
{'n': '粤语', 'v': '/lang/粤语'},
|
||||
{'n': '韩语', 'v': '/lang/韩语'},
|
||||
{'n': '日语', 'v': '/lang/日语'},
|
||||
{'n': '其他', 'v': '/lang/其他'}, ]},
|
||||
{'key': 'by',
|
||||
'name': '排序',
|
||||
'value': [{'n': '最近更新', 'v': '/sortType/1/sortOrder/0'},
|
||||
{'n': '添加时间', 'v': '/sortType/2/sortOrder/0'},
|
||||
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
|
||||
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
|
||||
],
|
||||
'4': [
|
||||
{'key': 'type',
|
||||
'name': '类型',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '国产动漫', 'v': '/type/75'},
|
||||
{'n': '日韩动漫', 'v': '/type/76'},
|
||||
{'n': '欧美动漫', 'v': '/type/77'}]},
|
||||
{'key': 'class',
|
||||
'name': '剧情',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '喜剧', 'v': '/class/喜剧'},
|
||||
{'n': '科幻', 'v': '/class/科幻'},
|
||||
{'n': '热血', 'v': '/class/热血'},
|
||||
{'n': '冒险', 'v': '/class/冒险'},
|
||||
{'n': '动作', 'v': '/class/动作'},
|
||||
{'n': '运动', 'v': '/class/运动'},
|
||||
{'n': '战争', 'v': '/class/战争'},
|
||||
{'n': '儿童', 'v': '/class/儿童'}]},
|
||||
{'key': 'area',
|
||||
'name': '地区',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '中国大陆', 'v': '/area/中国大陆'},
|
||||
{'n': '日本', 'v': '/area/日本'},
|
||||
{'n': '美国', 'v': '/area/美国'},
|
||||
{'n': '其他', 'v': '/area/其他'}]},
|
||||
{'key': 'year',
|
||||
'name': '时间',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '2024', 'v': '/year/2024'},
|
||||
{'n': '2023', 'v': '/year/2023'},
|
||||
{'n': '2022', 'v': '/year/2022'},
|
||||
{'n': '2021', 'v': '/year/2021'},
|
||||
{'n': '2020', 'v': '/year/2020'},
|
||||
{'n': '2019', 'v': '/year/2019'},
|
||||
{'n': '2018', 'v': '/year/2018'},
|
||||
{'n': '2017', 'v': '/year/2017'},
|
||||
{'n': '2016', 'v': '/year/2016'},
|
||||
{'n': '2015', 'v': '/year/2015'},
|
||||
{'n': '2014', 'v': '/year/2014'},
|
||||
{'n': '2013', 'v': '/year/2013'},
|
||||
{'n': '2012', 'v': '/year/2012'},
|
||||
{'n': '2011', 'v': '/year/2011'},
|
||||
{'n': '2010', 'v': '/year/2010'}]},
|
||||
{'key': 'lang',
|
||||
'name': '语言',
|
||||
'value': [{'n': '全部', 'v': ''},
|
||||
{'n': '国语', 'v': '/lang/国语'},
|
||||
{'n': '英语', 'v': '/lang/英语'},
|
||||
{'n': '日语', 'v': '/lang/日语'},
|
||||
{'n': '其他', 'v': '/lang/其他'}]},
|
||||
{'key': 'by',
|
||||
'name': '排序',
|
||||
'value': [{'n': '最近更新', 'v': '/sortType/1/sortOrder/0'},
|
||||
{'n': '添加时间', 'v': '/sortType/2/sortOrder/0'},
|
||||
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
|
||||
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def homeVideoContent(self):
|
||||
video_list = []
|
||||
t = str(int(time.time() * 1000))
|
||||
# t = '1723292093234'
|
||||
data = f'key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
|
||||
data_md5 = hashlib.md5(data.encode()).hexdigest()
|
||||
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
|
||||
h = {
|
||||
"User-Agent": self.ua,
|
||||
'referer': self.home_url, 't': t, 'sign': data_sha1}
|
||||
try:
|
||||
res = requests.get(f'{self.home_url}/api/mw-movie/anonymous/home/hotSearch', headers=h)
|
||||
data_list = res.json()['data']
|
||||
for i in data_list:
|
||||
video_list.append(
|
||||
{
|
||||
'vod_id': i['vodId'],
|
||||
'vod_name': i['vodName'],
|
||||
'vod_pic': i['vodPic'],
|
||||
'vod_remarks': i['vodVersion'] if i['typeId1'] == 1 else i['vodRemarks']
|
||||
}
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
return {
|
||||
'list': [],
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
return {
|
||||
'list': video_list,
|
||||
'parse': 0,
|
||||
'jx': 0
|
||||
}
|
||||
|
||||
def categoryContent(self, cid, page, filter, ext):
|
||||
t = cid
|
||||
_type = ext.get('type') if ext.get('type') else ''
|
||||
__class = ext.get('class') if ext.get('class') else ''
|
||||
_area = ext.get('area') if ext.get('area') else ''
|
||||
_year = ext.get('year') if ext.get('year') else ''
|
||||
_lang = ext.get('lang') if ext.get('lang') else ''
|
||||
_by = ext.get('by') if ext.get('by') else ''
|
||||
video_list = []
|
||||
h = {
|
||||
"User-Agent": self.ua,
|
||||
'referer': self.home_url,
|
||||
}
|
||||
try:
|
||||
res = requests.get(
|
||||
f'{self.home_url}/vod/show/id/{t}{_type}{__class}{_area}{_year}{_lang}{_by}/page/{page}',
|
||||
headers=h)
|
||||
aa = re.findall(r'\\"list\\":(.*?)}}}]', res.text)
|
||||
if not aa:
|
||||
return {'list': [], 'parse': 0, 'jx': 0}
|
||||
bb = aa[0].replace('\\"', '"')
|
||||
data_list = json.loads(bb)
|
||||
for i in data_list:
|
||||
video_list.append(
|
||||
{
|
||||
'vod_id': i['vodId'],
|
||||
'vod_name': i['vodName'],
|
||||
'vod_pic': i['vodPic'],
|
||||
'vod_remarks': i['vodVersion'] if i['typeId1'] == 1 else i['vodRemarks']
|
||||
}
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
return {'list': [], 'msg': e}
|
||||
return {'list': video_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def detailContent(self, did):
|
||||
ids = did[0]
|
||||
video_list = []
|
||||
t = str(int(time.time() * 1000))
|
||||
# t = '1723292093234'
|
||||
data = f'id={ids}&key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
|
||||
data_md5 = hashlib.md5(data.encode()).hexdigest()
|
||||
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
|
||||
h = {
|
||||
"User-Agent": self.ua,
|
||||
'referer': self.home_url,
|
||||
't': t, 'sign': data_sha1
|
||||
}
|
||||
try:
|
||||
res = requests.get(f'{self.home_url}/api/mw-movie/anonymous/video/detail?id={ids}', headers=h)
|
||||
data = res.json()['data']
|
||||
play_list = data['episodeList']
|
||||
vod_play_url = []
|
||||
for i in play_list:
|
||||
name = i['name']
|
||||
url = ids + '/' + str(i['nid'])
|
||||
vod_play_url.append(name + '$' + url)
|
||||
|
||||
video_list.append(
|
||||
{
|
||||
'type_name': data['typeName'],
|
||||
'vod_id': ids,
|
||||
'vod_name': data['vodName'],
|
||||
'vod_remarks': data['vodRemarks'],
|
||||
'vod_year': data['vodYear'],
|
||||
'vod_area': data['vodArea'],
|
||||
'vod_actor': data['vodActor'],
|
||||
'vod_director': data['vodDirector'],
|
||||
'vod_content': data['vodContent'],
|
||||
'vod_play_from': '老僧酿酒',
|
||||
'vod_play_url': '#'.join(vod_play_url)
|
||||
|
||||
}
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
return {'list': [], 'msg': e}
|
||||
return {"list": video_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def searchContent(self, key, quick, page='1'):
|
||||
wd = key
|
||||
video_list = []
|
||||
t = str(int(time.time() * 1000))
|
||||
data = f'keyword={wd}&pageNum={page}&pageSize=12&key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
|
||||
data_md5 = hashlib.md5(data.encode()).hexdigest()
|
||||
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
|
||||
h = {
|
||||
"User-Agent": self.ua,
|
||||
'referer': self.home_url,
|
||||
't': t, 'sign': data_sha1
|
||||
}
|
||||
try:
|
||||
response = requests.get(
|
||||
f'{self.home_url}/api/mw-movie/anonymous/video/searchByWord?keyword={wd}&pageNum={page}&pageSize=12',
|
||||
headers=h,
|
||||
)
|
||||
data_list = response.json()['data']['result']['list']
|
||||
for i in data_list:
|
||||
video_list.append(
|
||||
{
|
||||
'vod_id': i['vodId'],
|
||||
'vod_name': i['vodName'],
|
||||
'vod_pic': i['vodPic'],
|
||||
'vod_remarks': i['vodVersion'] if i['typeId1'] == 1 else i['vodRemarks']
|
||||
}
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
return {'list': [], 'msg': e}
|
||||
return {'list': video_list, 'parse': 0, 'jx': 0}
|
||||
|
||||
def playerContent(self, flag, pid, vipFlags):
|
||||
url = pid
|
||||
play_url = self.error_url
|
||||
data = url.split('/')
|
||||
_id = data[0]
|
||||
_nid = data[1]
|
||||
t = str(int(time.time() * 1000))
|
||||
# t = '1723292093234'
|
||||
data = f'id={_id}&nid={_nid}&key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
|
||||
data_md5 = hashlib.md5(data.encode()).hexdigest()
|
||||
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
|
||||
h = {
|
||||
"User-Agent": self.ua,
|
||||
'referer': self.home_url,
|
||||
't': t, 'sign': data_sha1
|
||||
}
|
||||
h2 = {
|
||||
"User-Agent": self.ua,
|
||||
}
|
||||
try:
|
||||
res = requests.get(
|
||||
f'{self.home_url}/api/mw-movie/anonymous/v2/video/episode/url?id={_id}&nid={_nid}',
|
||||
headers=h)
|
||||
play_url = res.json()['data']['list'][0]['url']
|
||||
except requests.RequestException as e:
|
||||
return {"url": play_url, "header": h2, "parse": 0, "jx": 0}
|
||||
|
||||
return {"url": play_url, "header": h2, "parse": 0, "jx": 0}
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
return '正在Destroy'
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 1,
|
||||
quickSearch: 1,
|
||||
title: '飞快',
|
||||
lang: 'hipy'
|
||||
})
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
from urllib.parse import quote_plus, unquote
|
||||
from lxml import etree
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "飞快"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
cateManual = {
|
||||
"电影": "1",
|
||||
"剧集": "2",
|
||||
"综艺": "3",
|
||||
"动漫": "4"
|
||||
}
|
||||
classes = [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]
|
||||
result['class'] = classes
|
||||
|
||||
return result
|
||||
|
||||
def _parse_video_item(self, a_element):
|
||||
"""解析单个视频项的公共方法"""
|
||||
try:
|
||||
href = a_element.xpath('./@href')[0] if a_element.xpath('./@href') else ''
|
||||
m = re.search(r'/voddetail/(\d+)\.html', href)
|
||||
if not m:
|
||||
return None
|
||||
sid = m.group(1)
|
||||
|
||||
title_nodes = (a_element.xpath('.//div[contains(@class, "module-poster-item-title")]//text()') or
|
||||
a_element.xpath('.//div[contains(@class, "module-card-item-title")]/a//text()') or
|
||||
a_element.xpath('./@title') or a_element.xpath('.//img/@alt'))
|
||||
name = title_nodes[0].strip() if title_nodes else f"视频_{sid}"
|
||||
|
||||
img = self._parse_image_url(a_element)
|
||||
|
||||
remark_nodes = a_element.xpath('.//div[contains(@class, "module-item-note")]//text()')
|
||||
remark = ''.join([x.strip() for x in remark_nodes if x.strip()]) if remark_nodes else ""
|
||||
|
||||
return {
|
||||
"vod_id": sid,
|
||||
"vod_name": name,
|
||||
"vod_pic": img,
|
||||
"vod_remarks": remark
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _parse_image_url(self, element):
|
||||
"""解析图片URL的公共方法"""
|
||||
img_nodes = (element.xpath('.//img[contains(@class, "lazy")]/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img[contains(@class, "lazy")]/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]'))
|
||||
|
||||
if img_nodes:
|
||||
img = img_nodes[0]
|
||||
if img.startswith('/'):
|
||||
img = 'https://feikuai.tv' + img
|
||||
return img
|
||||
return ''
|
||||
|
||||
def homeVideoContent(self):
|
||||
recommend_list = []
|
||||
try:
|
||||
url = "https://feikuai.tv/"
|
||||
rsp = self.fetch(url, headers=self.header)
|
||||
if not rsp or not rsp.text:
|
||||
return {'list': recommend_list}
|
||||
|
||||
root = self._parse_html(rsp)
|
||||
if not root:
|
||||
return {'list': recommend_list}
|
||||
|
||||
recommend_links = root.xpath(
|
||||
'//div[contains(@class, "module-focus")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-hot")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-recommend")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]'
|
||||
)
|
||||
|
||||
seen = set()
|
||||
for a in recommend_links:
|
||||
video_item = self._parse_video_item(a)
|
||||
if video_item and video_item["vod_id"] not in seen:
|
||||
seen.add(video_item["vod_id"])
|
||||
recommend_list.append(video_item)
|
||||
|
||||
recommend_list = recommend_list[:30]
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': recommend_list}
|
||||
|
||||
def _parse_html(self, rsp):
|
||||
try:
|
||||
parser = etree.HTMLParser(encoding='utf-8', recover=True, remove_blank_text=True)
|
||||
if hasattr(rsp, 'content'):
|
||||
return etree.HTML(rsp.content, parser=parser)
|
||||
return etree.HTML(rsp.text.encode('utf-8', errors='ignore'), parser=parser)
|
||||
except Exception:
|
||||
|
||||
return None
|
||||
|
||||
def _build_vodshow_url(self, tid, pg, ext):
|
||||
area = (ext.get('area') or ext.get('1') or '').strip()
|
||||
cate = (ext.get('class') or ext.get('3') or '').strip()
|
||||
year = (ext.get('year') or ext.get('11') or '').strip()
|
||||
|
||||
enc_area = quote_plus(area) if area else ''
|
||||
enc_cate = quote_plus(cate) if cate else ''
|
||||
pg_str = '' if str(pg) in ('', '1') else str(pg)
|
||||
return f'https://feikuai.tv/vodshow/{tid}-{enc_area}--{enc_cate}-----{pg_str}---{year}.html'
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid == '0':
|
||||
return self.homeVideoContent()
|
||||
|
||||
result = {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
try:
|
||||
ext = json.loads(extend) if extend and isinstance(extend, str) else {}
|
||||
url = self._build_vodshow_url(tid, pg, ext)
|
||||
except Exception:
|
||||
url = f'https://feikuai.tv/vodshow/{tid}-----------.html'
|
||||
|
||||
headers = self.header.copy()
|
||||
|
||||
headers['Referer'] = f'https://feikuai.tv/vodtype/{tid}.html'
|
||||
|
||||
rsp = self.fetch(url, headers=headers)
|
||||
if not rsp or not rsp.text:
|
||||
return result
|
||||
|
||||
root = self._parse_html(rsp)
|
||||
videos = []
|
||||
seen = set()
|
||||
try:
|
||||
links = root.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//a[contains(@class, "module-poster-item") and contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
for a in links:
|
||||
video_item = self._parse_video_item(a)
|
||||
if video_item and video_item["vod_id"] not in seen:
|
||||
seen.add(video_item["vod_id"])
|
||||
videos.append(video_item)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def _decode_url_field(self, raw, encrypt):
|
||||
try:
|
||||
if not raw:
|
||||
return ''
|
||||
enc = str(encrypt or '0').strip()
|
||||
if enc == '1':
|
||||
txt = unquote(raw)
|
||||
elif enc == '2':
|
||||
try:
|
||||
b = base64.b64decode(raw + '===')
|
||||
txt = unquote(b.decode('utf-8', errors='ignore'))
|
||||
except Exception:
|
||||
txt = unquote(raw)
|
||||
else:
|
||||
txt = raw
|
||||
|
||||
txt = re.sub(r'%u([0-9a-fA-F]{4})',
|
||||
lambda m: chr(int(m.group(1), 16)), txt)
|
||||
return txt
|
||||
except Exception:
|
||||
return raw
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
tid = str(ids[0]).strip()
|
||||
url = f'https://feikuai.tv/voddetail/{tid}.html'
|
||||
try:
|
||||
rsp = self.fetch(url, headers=self.header)
|
||||
if not rsp or not rsp.text:
|
||||
return {'list': []}
|
||||
root = self._parse_html(rsp)
|
||||
if not root:
|
||||
return {'list': []}
|
||||
except Exception:
|
||||
return {'list': []}
|
||||
|
||||
title = root.xpath('//h1/text() | //div[contains(@class, "module-info-heading")]//h1/text()')
|
||||
title = title[0].strip() if title else ''
|
||||
pic = ''
|
||||
try:
|
||||
pnodes = root.xpath('//div[contains(@class, "module-info-poster")]//img/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if not pnodes:
|
||||
pnodes = root.xpath('//div[contains(@class, "module-info-poster")]//img/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if not pnodes:
|
||||
pnodes = root.xpath('//img[contains(@class, "lazy")]/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if not pnodes:
|
||||
pnodes = root.xpath('//img[contains(@class, "lazy")]/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]')
|
||||
if pnodes:
|
||||
pic = pnodes[0]
|
||||
if pic.startswith('/'):
|
||||
pic = 'https://feikuai.tv' + pic
|
||||
except Exception:
|
||||
pic = ''
|
||||
|
||||
detail = ''
|
||||
try:
|
||||
dnodes = root.xpath('//div[contains(@class, "module-info-introduction-content")]//text()')
|
||||
detail = '\n'.join([x.strip() for x in dnodes if x.strip()]) if dnodes else ''
|
||||
except Exception:
|
||||
detail = ''
|
||||
|
||||
vod = {
|
||||
"vod_id": tid,
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"type_name": "",
|
||||
"vod_year": "",
|
||||
"vod_area": "",
|
||||
"vod_remarks": "",
|
||||
"vod_actor": "",
|
||||
"vod_director": "",
|
||||
"vod_content": detail
|
||||
}
|
||||
|
||||
playFrom = []
|
||||
playList = []
|
||||
try:
|
||||
ep_links = root.xpath('//div[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")] | //ul[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")]')
|
||||
groups = {}
|
||||
for a in ep_links:
|
||||
try:
|
||||
href = a.xpath('./@href')[0] if a.xpath('./@href') else ''
|
||||
m = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', href)
|
||||
if not m:
|
||||
continue
|
||||
vid, sid, epid = m.groups()
|
||||
if vid != tid:
|
||||
continue
|
||||
name = ''.join(a.xpath('string(.)')).strip()
|
||||
if not name or name in ('立即播放', '收藏', '追更', '分享', '报错', '下载'):
|
||||
continue
|
||||
if sid not in groups:
|
||||
groups[sid] = []
|
||||
groups[sid].append(f"{name}${vid}-{sid}-{epid}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
ordered_sids = []
|
||||
for blk in root.xpath('//div[contains(@class, "his-tab-list")]'):
|
||||
try:
|
||||
first = blk.xpath('.//a[contains(@href, "/vodplay/")][1]/@href')
|
||||
if first:
|
||||
mm = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', first[0])
|
||||
if mm:
|
||||
sid = mm.group(2)
|
||||
if sid not in ordered_sids:
|
||||
ordered_sids.append(sid)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
labels = [x.strip() for x in root.xpath('//div[contains(@class, "module-tab-items-box")]//div[contains(@class, "module-tab-item")]//span/text()') if x.strip()]
|
||||
|
||||
for idx, sid in enumerate(ordered_sids):
|
||||
sname = labels[idx] if idx < len(labels) else f"线路{sid}"
|
||||
playFrom.append(sname)
|
||||
playList.append('#'.join(groups.get(sid, [])))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
vod['vod_play_from'] = '$$$'.join(playFrom) if playFrom else ""
|
||||
vod['vod_play_url'] = '$$$'.join(playList)
|
||||
return {'list': [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
videos = []
|
||||
try:
|
||||
pg = str(pg)
|
||||
if pg in ('', '1'):
|
||||
url = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
headers = self.header.copy()
|
||||
headers['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
else:
|
||||
url = f'https://feikuai.tv/label/search_ajax.html?wd={quote_plus(key)}&by=time&order=desc&page={pg}'
|
||||
headers = self.header.copy()
|
||||
headers['X-Requested-With'] = 'XMLHttpRequest'
|
||||
headers['Accept'] = '*/*'
|
||||
headers['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
|
||||
rsp = self.fetch(url, headers=headers)
|
||||
if not rsp or not rsp.text:
|
||||
return {'list': []}
|
||||
|
||||
root = self._parse_html(rsp)
|
||||
items = root.xpath('//div[@id="resultList"]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
root.xpath('//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
seen = set()
|
||||
for a in items:
|
||||
video_item = self._parse_video_item(a)
|
||||
if video_item and video_item["vod_id"] not in seen:
|
||||
seen.add(video_item["vod_id"])
|
||||
videos.append(video_item)
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': videos}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
play_url = f'https://feikuai.tv/vodplay/{id}.html'
|
||||
vurl = play_url
|
||||
try:
|
||||
rsp = self.fetch(play_url, headers=self.header, timeout=45)
|
||||
if not rsp or not rsp.text:
|
||||
return {"parse": 0, "url": vurl, "header": self.header}
|
||||
pattern = r'(?:var\s+)?player_[a-zA-Z0-9_]+\s*=\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})(?=\s*</script>)'
|
||||
m = re.search(pattern, rsp.text, re.S)
|
||||
if m:
|
||||
data = json.loads(m.group(1))
|
||||
vurl = data.get('url') or ''
|
||||
vurl = self._decode_url_field(vurl, str(data.get('encrypt', '0')))
|
||||
if vurl.startswith('//'):
|
||||
vurl = 'https:' + vurl
|
||||
if vurl.endswith('.m3u8'):
|
||||
return {"parse": 1, "url": vurl, "header": self.header}
|
||||
except Exception:
|
||||
pass
|
||||
return {"parse": 0, "url": vurl, "header": self.header}
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return url
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return []
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def getProxyUrl(self, local=True):
|
||||
return 'http://127.0.0.1:9978/proxy?do=py'
|
||||
|
||||
header = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
"""
|
||||
@header({
|
||||
searchable: 1,
|
||||
filterable: 1,
|
||||
quickSearch: 1,
|
||||
title: '飞快',
|
||||
author: 'EylinSir修复版-网盘推送播放',
|
||||
lang: 'hipy'
|
||||
})
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
from urllib.parse import quote_plus, unquote
|
||||
from lxml import etree
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self):
|
||||
return "飞快"
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
_r1 = {}
|
||||
_c1 = {
|
||||
"电影": "1",
|
||||
"剧集": "2",
|
||||
"综艺": "3",
|
||||
"动漫": "4"
|
||||
}
|
||||
_c2 = [{'type_name': _k1, 'type_id': _v1} for _k1, _v1 in _c1.items()]
|
||||
_r1['class'] = _c2
|
||||
return _r1
|
||||
|
||||
def homeVideoContent(self):
|
||||
_l1 = []
|
||||
try:
|
||||
_u1 = "".join(['h', 't', 't', 'p', 's', ':', '/', '/', 'f', 'e', 'i', 'k', 'u', 'a', 'i', '.', 't', 'v', '/'])
|
||||
_r2 = self.fetch(_u1, headers=self._get_header())
|
||||
if not _r2 or not _r2.text:
|
||||
return {'list': _l1}
|
||||
|
||||
_h1 = self._parse_dom(_r2)
|
||||
if not _h1:
|
||||
return {'list': _l1}
|
||||
|
||||
_x1 = _h1.xpath(
|
||||
'//div[contains(@class, "module-focus")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-hot")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-recommend")]//a[contains(@href, "/voddetail/")] | '
|
||||
'//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]'
|
||||
)
|
||||
|
||||
_s1 = set()
|
||||
for _a1 in _x1:
|
||||
_v1 = self._parse_item(_a1)
|
||||
if _v1 and _v1["vod_id"] not in _s1:
|
||||
_s1.add(_v1["vod_id"])
|
||||
_l1.append(_v1)
|
||||
|
||||
_l1 = _l1[:30]
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': _l1}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
if tid == '0':
|
||||
return self.homeVideoContent()
|
||||
|
||||
_r3 = {'list': [], 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999}
|
||||
try:
|
||||
_e1 = json.loads(extend) if extend and isinstance(extend, str) else {}
|
||||
_u2 = self._build_url(tid, pg, _e1)
|
||||
except Exception:
|
||||
_u2 = f'{"".join(["h","t","t","p","s",":","/","/","f","e","i","k","u","a","i",".","t","v","/","v","o","d","s","h","o","w","/",tid,"-","-","-","-","-","-","-","-","-",".","h","t","m","l"])}'
|
||||
|
||||
_h2 = self._get_header().copy()
|
||||
_h2['Referer'] = f'https://feikuai.tv/vodtype/{tid}.html'
|
||||
|
||||
_r4 = self.fetch(_u2, headers=_h2)
|
||||
if not _r4 or not _r4.text:
|
||||
return _r3
|
||||
|
||||
_h3 = self._parse_dom(_r4)
|
||||
_v2 = []
|
||||
_s2 = set()
|
||||
try:
|
||||
_l2 = _h3.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h3.xpath('//a[contains(@class, "module-poster-item") and contains(@href, "/voddetail/")]') or \
|
||||
_h3.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
for _a2 in _l2:
|
||||
_v3 = self._parse_item(_a2)
|
||||
if _v3 and _v3["vod_id"] not in _s2:
|
||||
_s2.add(_v3["vod_id"])
|
||||
_v2.append(_v3)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_r3['list'] = _v2
|
||||
return _r3
|
||||
|
||||
def detailContent(self, ids):
|
||||
if not ids:
|
||||
return {'list': []}
|
||||
_t1 = str(ids[0]).strip()
|
||||
_u3 = f'https://feikuai.tv/voddetail/{_t1}.html'
|
||||
try:
|
||||
_r5 = self.fetch(_u3, headers=self._get_header())
|
||||
if not _r5 or not _r5.text:
|
||||
return {'list': []}
|
||||
_h4 = self._parse_dom(_r5)
|
||||
if not _h4:
|
||||
return {'list': []}
|
||||
except Exception:
|
||||
return {'list': []}
|
||||
|
||||
_t2 = _h4.xpath('//h1/text() | //div[contains(@class, "module-info-heading")]//h1/text()')
|
||||
_t3 = _t2[0].strip() if _t2 else ''
|
||||
_p1 = self._get_img(_h4)
|
||||
_d1 = self._get_desc(_h4)
|
||||
|
||||
_v4 = {
|
||||
"vod_id": _t1,
|
||||
"vod_name": _t3,
|
||||
"vod_pic": _p1,
|
||||
"type_name": "",
|
||||
"vod_year": "",
|
||||
"vod_area": "",
|
||||
"vod_remarks": "",
|
||||
"vod_actor": "",
|
||||
"vod_director": "",
|
||||
"vod_content": _d1
|
||||
}
|
||||
|
||||
_f1, _l3 = self._get_sources(_h4, _t1)
|
||||
|
||||
if not _f1 or not _l3:
|
||||
_f1 = ['飞快']
|
||||
_l3 = [f'{"".join(["暂","无","播","放","源"])}${"".join(["h","t","t","p","s",":","/","/","f","e","i","k","u","a","i",".","t","v","/","v","o","d","d","e","t","a","i","l","/",_t1,".","h","t","m","l"])}']
|
||||
|
||||
_v4['vod_play_from'] = '$$$'.join(_f1) if _f1 else ""
|
||||
_v4['vod_play_url'] = '$$$'.join(_l3)
|
||||
return {'list': [_v4]}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
_v5 = []
|
||||
try:
|
||||
_p2 = str(pg)
|
||||
if _p2 in ('', '1'):
|
||||
_u4 = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
_h5 = self._get_header().copy()
|
||||
_h5['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
else:
|
||||
_u4 = f'https://feikuai.tv/label/search_ajax.html?wd={quote_plus(key)}&by=time&order=desc&page={_p2}'
|
||||
_h5 = self._get_header().copy()
|
||||
_h5['X-Requested-With'] = 'XMLHttpRequest'
|
||||
_h5['Accept'] = '*/*'
|
||||
_h5['Referer'] = f'https://feikuai.tv/vodsearch/-------------.html?wd={quote_plus(key)}'
|
||||
|
||||
_r6 = self.fetch(_u4, headers=_h5)
|
||||
if not _r6 or not _r6.text:
|
||||
return {'list': []}
|
||||
|
||||
_h6 = self._parse_dom(_r6)
|
||||
_i1 = _h6.xpath('//div[@id="resultList"]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h6.xpath('//div[contains(@class, "module-card-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h6.xpath('//div[contains(@class, "module-items") and contains(@class, "module-poster-items")]//a[contains(@href, "/voddetail/")]') or \
|
||||
_h6.xpath('//a[contains(@href, "/voddetail/")]')
|
||||
|
||||
_s3 = set()
|
||||
for _a3 in _i1:
|
||||
_v6 = self._parse_item(_a3)
|
||||
if _v6 and _v6["vod_id"] not in _s3:
|
||||
_s3.add(_v6["vod_id"])
|
||||
_v5.append(_v6)
|
||||
except Exception:
|
||||
pass
|
||||
return {'list': _v5}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
|
||||
if isinstance(id, str) and id.startswith(''.join(['p','u','s','h',':','/','/'])):
|
||||
return {"parse": 0, "url": id}
|
||||
|
||||
|
||||
_u5 = f'https://feikuai.tv/vodplay/{id}.html'
|
||||
_v7 = _u5
|
||||
try:
|
||||
_r7 = self.fetch(_u5, headers=self._get_header(), timeout=45)
|
||||
if not _r7 or not _r7.text:
|
||||
return {"parse": 0, "url": _v7, "header": self._get_header()}
|
||||
|
||||
_p3 = r'(?:var\s+)?player_[a-zA-Z0-9_]+\s*=\s*(\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\})(?=\s*</script>)'
|
||||
_m1 = re.search(_p3, _r7.text, re.S)
|
||||
if _m1:
|
||||
_d2 = json.loads(_m1.group(1))
|
||||
_v7 = _d2.get('url') or ''
|
||||
_v7 = self._decode_str(_v7, str(_d2.get('encrypt', '0')))
|
||||
if _v7.startswith('//'):
|
||||
_v7 = 'https:' + _v7
|
||||
if _v7.endswith('.m3u8'):
|
||||
return {"parse": 1, "url": _v7, "header": self._get_header()}
|
||||
except Exception:
|
||||
pass
|
||||
return {"parse": 0, "url": _v7, "header": self._get_header()}
|
||||
|
||||
def _parse_item(self, a_element):
|
||||
try:
|
||||
_h7 = a_element.xpath('./@href')[0] if a_element.xpath('./@href') else ''
|
||||
_m2 = re.search(r'/voddetail/(\d+)\.html', _h7)
|
||||
if not _m2:
|
||||
return None
|
||||
_s4 = _m2.group(1)
|
||||
|
||||
_t4 = (a_element.xpath('.//div[contains(@class, "module-poster-item-title")]//text()') or
|
||||
a_element.xpath('.//div[contains(@class, "module-card-item-title")]/a//text()') or
|
||||
a_element.xpath('./@title') or a_element.xpath('.//img/@alt'))
|
||||
_n1 = _t4[0].strip() if _t4 else f"视频_{_s4}"
|
||||
|
||||
_i2 = self._get_img(a_element)
|
||||
|
||||
_r8 = a_element.xpath('.//div[contains(@class, "module-item-note")]//text()')
|
||||
_r9 = ''.join([x.strip() for x in _r8 if x.strip()]) if _r8 else ""
|
||||
|
||||
return {
|
||||
"vod_id": _s4,
|
||||
"vod_name": _n1,
|
||||
"vod_pic": _i2,
|
||||
"vod_remarks": _r9
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_img(self, element):
|
||||
_i3 = (element.xpath('.//img[contains(@class, "lazy")]/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img[contains(@class, "lazy")]/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@data-original[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]') or
|
||||
element.xpath('.//img/@src[contains(., ".webp") or contains(., ".jpg") or contains(., ".png")]'))
|
||||
|
||||
if _i3:
|
||||
_i4 = _i3[0]
|
||||
if _i4.startswith('/'):
|
||||
_i4 = 'https://feikuai.tv' + _i4
|
||||
return _i4
|
||||
return ''
|
||||
|
||||
def _get_desc(self, root):
|
||||
try:
|
||||
_d3 = root.xpath('//div[contains(@class, "module-info-introduction-content")]//text()')
|
||||
_d4 = '\n'.join([x.strip() for x in _d3 if x.strip()]) if _d3 else ''
|
||||
return _d4
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def _parse_dom(self, rsp):
|
||||
try:
|
||||
_p4 = etree.HTMLParser(encoding='utf-8', recover=True, remove_blank_text=True)
|
||||
if hasattr(rsp, 'content'):
|
||||
return etree.HTML(rsp.content, parser=_p4)
|
||||
return etree.HTML(rsp.text.encode('utf-8', errors='ignore'), parser=_p4)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _build_url(self, tid, pg, ext):
|
||||
_a1 = (ext.get('area') or ext.get('1') or '').strip()
|
||||
_c3 = (ext.get('class') or ext.get('3') or '').strip()
|
||||
_y1 = (ext.get('year') or ext.get('11') or '').strip()
|
||||
|
||||
_e2 = quote_plus(_a1) if _a1 else ''
|
||||
_e3 = quote_plus(_c3) if _c3 else ''
|
||||
_p5 = '' if str(pg) in ('', '1') else str(pg)
|
||||
return f'https://feikuai.tv/vodshow/{tid}-{_e2}--{_e3}-----{_p5}---{_y1}.html'
|
||||
|
||||
def _get_sources(self, root, tid):
|
||||
_f2 = []
|
||||
_l4 = []
|
||||
|
||||
self._get_normal_sources(root, tid, _f2, _l4)
|
||||
|
||||
self._get_pan_sources(root, tid, _f2, _l4)
|
||||
|
||||
return _f2, _l4
|
||||
|
||||
def _get_normal_sources(self, root, tid, playFrom, playList):
|
||||
try:
|
||||
_e4 = root.xpath('//div[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")] | //ul[contains(@class, "module-play-list")]//a[contains(@href, "/vodplay/")]')
|
||||
_g1 = {}
|
||||
for _a4 in _e4:
|
||||
try:
|
||||
_h8 = _a4.xpath('./@href')[0] if _a4.xpath('./@href') else ''
|
||||
_m3 = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', _h8)
|
||||
if not _m3:
|
||||
continue
|
||||
_v8, _s5, _e5 = _m3.groups()
|
||||
if _v8 != tid:
|
||||
continue
|
||||
_n2 = ''.join(_a4.xpath('string(.)')).strip()
|
||||
if not _n2 or _n2 in ('立即播放', '收藏', '追更', '分享', '报错', '下载'):
|
||||
continue
|
||||
if _s5 not in _g1:
|
||||
_g1[_s5] = []
|
||||
_g1[_s5].append(f"{_n2}${_v8}-{_s5}-{_e5}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
_o1 = []
|
||||
for _b1 in root.xpath('//div[contains(@class, "his-tab-list")]'):
|
||||
try:
|
||||
_f3 = _b1.xpath('.//a[contains(@href, "/vodplay/")][1]/@href')
|
||||
if _f3:
|
||||
_m4 = re.search(r'/vodplay/(\d+)-(\d+)-(\d+)\.html', _f3[0])
|
||||
if _m4:
|
||||
_s6 = _m4.group(2)
|
||||
if _s6 not in _o1:
|
||||
_o1.append(_s6)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
_l5 = [x.strip() for x in root.xpath('//div[contains(@class, "module-tab-items-box")]//div[contains(@class, "module-tab-item")]//span/text()') if x.strip()]
|
||||
|
||||
for _i5, _s7 in enumerate(_o1):
|
||||
_s8 = _l5[_i5] if _i5 < len(_l5) else f"线路{_s7}"
|
||||
playFrom.append(_s8)
|
||||
playList.append('#'.join(_g1.get(_s7, [])))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _get_pan_sources(self, root, tid, playFrom, playList):
|
||||
try:
|
||||
_d5 = root.xpath('//div[@id="download-list"]')
|
||||
if not _d5:
|
||||
return
|
||||
|
||||
_t5 = root.xpath('//div[@id="y-downList"]//div[contains(@class, "module-tab-item")]')
|
||||
|
||||
_p6 = {
|
||||
'百度网盘': '百度网盘',
|
||||
'夸克网盘': '夸克网盘',
|
||||
'迅雷云盘': '迅雷云盘',
|
||||
'阿里云盘': '阿里云盘',
|
||||
'天翼云盘': '天翼云盘',
|
||||
'UC网盘': 'UC网盘',
|
||||
'115网盘': '115网盘',
|
||||
'移动云盘': '移动云盘'
|
||||
}
|
||||
|
||||
for _t6 in _t5:
|
||||
try:
|
||||
_s9 = ''.join(_t6.xpath('.//span/text()')).strip()
|
||||
if not _s9 or _s9 == '磁力链接':
|
||||
continue
|
||||
|
||||
_s10 = _p6.get(_s9, _s9)
|
||||
|
||||
_t7 = _t6.xpath('./@data-index')
|
||||
if not _t7:
|
||||
continue
|
||||
_t8 = _t7[0]
|
||||
|
||||
_c4 = root.xpath(f'//div[@id="tab-content-{_t8}"]//div[@class="module-row-info"]//a')
|
||||
|
||||
_e6 = []
|
||||
for _i6, _l6 in enumerate(_c4, 1):
|
||||
try:
|
||||
_u6 = _l6.xpath('./@href')
|
||||
if not _u6:
|
||||
continue
|
||||
_u7 = _u6[0].strip()
|
||||
|
||||
_h9 = _l6.xpath('.//h4/text()')
|
||||
if _h9:
|
||||
_e7 = _h9[0].strip()
|
||||
_e7 = re.sub(r'@一键搜片-\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$', '', _e7).strip()
|
||||
else:
|
||||
_e7 = f"资源{_i6}"
|
||||
|
||||
_u8 = self._process_url(_u7)
|
||||
if _u8:
|
||||
_e6.append(f"{_e7}${_u8}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if _e6:
|
||||
playFrom.append(_s10)
|
||||
playList.append('#'.join(_e6))
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _process_url(self, raw_url):
|
||||
try:
|
||||
if not raw_url:
|
||||
return None
|
||||
|
||||
if not raw_url.startswith(('http://', 'https://')):
|
||||
if raw_url.startswith('//'):
|
||||
_u9 = 'https:' + raw_url
|
||||
elif raw_url.startswith('/'):
|
||||
_u9 = 'https://feikuai.tv' + raw_url
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
_u9 = raw_url
|
||||
|
||||
return f"push://{_u9}"
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _decode_str(self, raw, encrypt):
|
||||
try:
|
||||
if not raw:
|
||||
return ''
|
||||
_e8 = str(encrypt or '0').strip()
|
||||
if _e8 == '1':
|
||||
_t9 = unquote(raw)
|
||||
elif _e8 == '2':
|
||||
try:
|
||||
_b2 = base64.b64decode(raw + '===')
|
||||
_t9 = unquote(_b2.decode('utf-8', errors='ignore'))
|
||||
except Exception:
|
||||
_t9 = unquote(raw)
|
||||
else:
|
||||
_t9 = raw
|
||||
|
||||
_t9 = re.sub(r'%u([0-9a-fA-F]{4})',
|
||||
lambda _m5: chr(int(_m5.group(1), 16)), _t9)
|
||||
return _t9
|
||||
except Exception:
|
||||
return raw
|
||||
|
||||
def _get_header(self):
|
||||
return {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Connection": "keep-alive"
|
||||
}
|
||||
|
||||
def localProxy(self, params):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
return url
|
||||
|
||||
def manualVideoCheck(self):
|
||||
return []
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
# coding = utf-8
|
||||
# !/usr/bin/python
|
||||
|
||||
"""
|
||||
|
||||
作者 丢丢喵 🚓 内容均从互联网收集而来 仅供交流学习使用 版权归原创者所有 如侵犯了您的权益 请通知作者 将及时删除侵权内容
|
||||
====================Diudiumiao====================
|
||||
|
||||
"""
|
||||
|
||||
from Crypto.Util.Padding import unpad
|
||||
from urllib.parse import unquote
|
||||
from Crypto.Cipher import ARC4
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.Cipher import AES
|
||||
from bs4 import BeautifulSoup
|
||||
from base64 import b64decode
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import binascii
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
xurl = "https://fantuansjz.com"
|
||||
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
|
||||
pm = ''
|
||||
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
global headers
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
|
||||
if pl == 3:
|
||||
plx = []
|
||||
while True:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
break
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
break
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
plx.append(middle_text)
|
||||
text = text.replace(start_str + middle_text + end_str, '')
|
||||
if len(plx) > 0:
|
||||
purl = ''
|
||||
for i in range(len(plx)):
|
||||
matches = re.findall(start_index1, plx[i])
|
||||
output = ""
|
||||
for match in matches:
|
||||
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
|
||||
if match3:
|
||||
number = match3.group(1)
|
||||
else:
|
||||
number = 0
|
||||
if 'http' not in match[0]:
|
||||
output += f"#{match[1]}${number}{xurl}{match[0]}"
|
||||
else:
|
||||
output += f"#{match[1]}${number}{match[0]}"
|
||||
output = output[1:]
|
||||
purl = purl + output + "$$$"
|
||||
purl = purl[:-3]
|
||||
return purl
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
start_index = text.find(start_str)
|
||||
if start_index == -1:
|
||||
return ""
|
||||
end_index = text.find(end_str, start_index + len(start_str))
|
||||
if end_index == -1:
|
||||
return ""
|
||||
|
||||
if pl == 0:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
return middle_text.replace("\\", "")
|
||||
|
||||
if pl == 1:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
jg = ' '.join(matches)
|
||||
return jg
|
||||
|
||||
if pl == 2:
|
||||
middle_text = text[start_index + len(start_str):end_index]
|
||||
matches = re.findall(start_index1, middle_text)
|
||||
if matches:
|
||||
new_list = [f'{item}' for item in matches]
|
||||
jg = '$$$'.join(new_list)
|
||||
return jg
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {}
|
||||
result = {"class": [{"type_id": "1", "type_name": "电影"},
|
||||
{"type_id": "2", "type_name": "剧集"},
|
||||
{"type_id": "3", "type_name": "综艺"},
|
||||
{"type_id": "4", "type_name": "动漫"},
|
||||
{"type_id": "40", "type_name": "豆瓣"}],
|
||||
|
||||
"list": [],
|
||||
"filters": {"1": [{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "2025", "v": "2025"},
|
||||
{"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"},
|
||||
{"n": "2022", "v": "2022"},
|
||||
{"n": "2021", "v": "2021"},
|
||||
{"n": "2020", "v": "2020"},
|
||||
{"n": "2019", "v": "2019"},
|
||||
{"n": "2018", "v": "2018"}]}],
|
||||
"2": [{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "2025", "v": "2025"},
|
||||
{"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"},
|
||||
{"n": "2022", "v": "2022"},
|
||||
{"n": "2021", "v": "2021"},
|
||||
{"n": "2020", "v": "2020"},
|
||||
{"n": "2019", "v": "2019"},
|
||||
{"n": "2018", "v": "2018"}]}],
|
||||
"3": [{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "2025", "v": "2025"},
|
||||
{"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"},
|
||||
{"n": "2022", "v": "2022"},
|
||||
{"n": "2021", "v": "2021"},
|
||||
{"n": "2020", "v": "2020"},
|
||||
{"n": "2019", "v": "2019"},
|
||||
{"n": "2018", "v": "2018"}]}],
|
||||
"4": [{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "2025", "v": "2025"},
|
||||
{"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"},
|
||||
{"n": "2022", "v": "2022"},
|
||||
{"n": "2021", "v": "2021"},
|
||||
{"n": "2020", "v": "2020"},
|
||||
{"n": "2019", "v": "2019"},
|
||||
{"n": "2018", "v": "2018"}]}],
|
||||
"40": [{"key": "年代",
|
||||
"name": "年代",
|
||||
"value": [{"n": "全部", "v": ""},
|
||||
{"n": "2025", "v": "2025"},
|
||||
{"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"},
|
||||
{"n": "2022", "v": "2022"},
|
||||
{"n": "2021", "v": "2021"},
|
||||
{"n": "2020", "v": "2020"},
|
||||
{"n": "2019", "v": "2019"},
|
||||
{"n": "2018", "v": "2018"}]}]}}
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
|
||||
try:
|
||||
detail = requests.get(url=xurl, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('ul', class_="fed-list-info")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('li')
|
||||
|
||||
for vod in vods:
|
||||
names = vod.find('a', class_="fed-list-title")
|
||||
name = names.text.strip()
|
||||
|
||||
id = names['href']
|
||||
|
||||
pics = vod.find('a', class_="fed-list-pics")
|
||||
pic = pics['data-original']
|
||||
|
||||
if 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="fed-list-remarks")
|
||||
remark = remarks.text.strip()
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": '▶️' + remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result = {'list': videos}
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if pg:
|
||||
page = int(pg)
|
||||
else:
|
||||
page = 1
|
||||
|
||||
if '年代' in ext.keys():
|
||||
NdType = ext['年代']
|
||||
else:
|
||||
NdType = ''
|
||||
|
||||
if page == 1:
|
||||
url = f'{xurl}/sjvodtype/{cid}.html'
|
||||
|
||||
else:
|
||||
url = f'{xurl}/sjvodshow/{cid}--------{str(page)}---{NdType}.html'
|
||||
|
||||
try:
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('ul', class_="fed-list-info")
|
||||
|
||||
for soup in soups:
|
||||
vods = soup.find_all('li')
|
||||
|
||||
for vod in vods:
|
||||
names = vod.find('a', class_="fed-list-title")
|
||||
name = names.text.strip()
|
||||
|
||||
id = names['href']
|
||||
|
||||
pics = vod.find('a', class_="fed-list-pics")
|
||||
pic = pics['data-original']
|
||||
|
||||
if 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="fed-list-remarks")
|
||||
remark = remarks.text.strip()
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": '▶️' + remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
except:
|
||||
pass
|
||||
result = {'list': videos}
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
global pm
|
||||
did = ids[0]
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if 'http' not in did:
|
||||
did = xurl + did
|
||||
|
||||
res = requests.get(url=did, headers=headerx)
|
||||
res.encoding = "utf-8"
|
||||
res = res.text
|
||||
|
||||
url = 'https://m.baidu.com/'
|
||||
response = requests.get(url)
|
||||
response.encoding = 'utf-8'
|
||||
code = response.text
|
||||
name = self.extract_middle_text(code, "s1='", "'", 0)
|
||||
Jumps = self.extract_middle_text(code, "s2='", "'", 0)
|
||||
|
||||
content = '😸🎉剧情介绍📢' + self.extract_middle_text(res,'剧情介绍:','">', 0)
|
||||
|
||||
director = self.extract_middle_text(res, '导演:', '</li>',1,'target=".*?">(.*?)</a>')
|
||||
|
||||
actor = self.extract_middle_text(res, '主演:', '</li>',1,'target=".*?">(.*?)</a>')
|
||||
|
||||
remarks = self.extract_middle_text(res, 'fed-text-white fed-text-center">', '</span>', 0)
|
||||
|
||||
year = self.extract_middle_text(res, '年份:', '</li>', 1,'target=".*?">(.*?)</a>')
|
||||
|
||||
area = self.extract_middle_text(res, '地区:', '</li>', 1,'target=".*?">(.*?)</a>')
|
||||
|
||||
if name not in content:
|
||||
bofang = Jumps
|
||||
else:
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find('ul', class_="fed-padding")
|
||||
|
||||
soup = soups.find_all('a')
|
||||
|
||||
jishu = 0
|
||||
xian = []
|
||||
xianlu = ''
|
||||
bofang = ''
|
||||
gl = []
|
||||
|
||||
for sou in soup:
|
||||
jishu = jishu + 1
|
||||
|
||||
name = sou.text.strip()
|
||||
|
||||
if any(item in name for item in gl):
|
||||
continue
|
||||
|
||||
xian.append(jishu)
|
||||
|
||||
xianlu = xianlu + name + '$$$'
|
||||
|
||||
xianlu = xianlu[:-3]
|
||||
|
||||
for psou in xian:
|
||||
jishu = psou - 1
|
||||
|
||||
soups = doc.find_all('ul', class_="fed-tabs-btm")[jishu]
|
||||
|
||||
soup = soups.find_all('a')
|
||||
|
||||
for sou in soup:
|
||||
|
||||
id = sou['href']
|
||||
|
||||
if 'http' not in id:
|
||||
id = xurl + id
|
||||
|
||||
name = sou.text.strip()
|
||||
|
||||
bofang = bofang + name + '$' + id + '#'
|
||||
|
||||
bofang = bofang[:-1] + '$$$'
|
||||
|
||||
bofang = bofang[:-3]
|
||||
|
||||
videos.append({
|
||||
"vod_id": did,
|
||||
"vod_director": director,
|
||||
"vod_actor": actor,
|
||||
"vod_remarks": remarks,
|
||||
"vod_year": year,
|
||||
"vod_area": area,
|
||||
"vod_content": content,
|
||||
"vod_play_from": xianlu,
|
||||
"vod_play_url": bofang
|
||||
})
|
||||
|
||||
result['list'] = videos
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
parts = id.split("http")
|
||||
|
||||
xiutan = 0
|
||||
|
||||
if xiutan == 0:
|
||||
if len(parts) > 1:
|
||||
before_https, after_https = parts[0], 'http' + parts[1]
|
||||
|
||||
if '/tp/jd.m3u8' in after_https:
|
||||
url = after_https
|
||||
else:
|
||||
res = requests.get(url=after_https, headers=headerx)
|
||||
res = res.text
|
||||
|
||||
url = self.extract_middle_text(res, '},"url":"', '"', 0).replace('\\', '')
|
||||
|
||||
result = {}
|
||||
result["parse"] = xiutan
|
||||
result["playUrl"] = ''
|
||||
result["url"] = url
|
||||
result["header"] = headerx
|
||||
return result
|
||||
|
||||
def searchContentPage(self, key, quick, page):
|
||||
result = {}
|
||||
videos = []
|
||||
|
||||
if not page:
|
||||
page = '1'
|
||||
if page == '1':
|
||||
url = f'{xurl}/sjvodsearch/-------------.html?wd={key}'
|
||||
|
||||
else:
|
||||
url = f'{xurl}/sjvodsearch/{key}----------{str(page)}---.html'
|
||||
|
||||
detail = requests.get(url=url, headers=headerx)
|
||||
detail.encoding = "utf-8"
|
||||
res = detail.text
|
||||
doc = BeautifulSoup(res, "lxml")
|
||||
|
||||
soups = doc.find_all('dl', class_="fed-list-deta")
|
||||
|
||||
for vod in soups:
|
||||
names = vod.find('h3', class_="fed-part-eone")
|
||||
name = names.text.strip()
|
||||
|
||||
ids = vod.find('a', class_="fed-list-pics")
|
||||
id = ids['href']
|
||||
id = id.replace('/sjvodplay/', '/sjvoddetail/').replace('-1-1', '')
|
||||
|
||||
pic = ids['data-original']
|
||||
|
||||
if 'http' not in pic:
|
||||
pic = xurl + pic
|
||||
|
||||
remarks = vod.find('span', class_="fed-list-remarks")
|
||||
remark = remarks.text.strip()
|
||||
|
||||
video = {
|
||||
"vod_id": id,
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": '▶️' + remark
|
||||
}
|
||||
videos.append(video)
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = page
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return self.searchContentPage(key, quick, '1')
|
||||
|
||||
def localProxy(self, params):
|
||||
if params['type'] == "m3u8":
|
||||
return self.proxyM3u8(params)
|
||||
elif params['type'] == "media":
|
||||
return self.proxyMedia(params)
|
||||
elif params['type'] == "ts":
|
||||
return self.proxyTs(params)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user