Add files via upload

This commit is contained in:
yigedashu
2026-01-27 10:59:41 +08:00
committed by GitHub
parent 8b5d7a8511
commit 8e7a390077
65 changed files with 26685 additions and 0 deletions
+432
View File
@@ -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
View File
@@ -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
View File
@@ -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('&', '&amp;')
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
View File
@@ -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
+499
View File
@@ -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
View File
@@ -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
View File
@@ -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
+383
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}&params={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
+62
View File
@@ -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
View File
@@ -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
+184
View File
@@ -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
View File
@@ -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
View File
@@ -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 = {'&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"'}
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
View File
@@ -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
View File
@@ -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
View File
@@ -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('&quot;','"').replace('&#34;','"').replace('&amp;','&').replace('&#38;','&').replace('&lt;','<').replace('&#60;','<').replace('&gt;','>').replace('&#62;','>'));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
View File
@@ -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&timestamp={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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+217
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
+460
View File
@@ -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
View File
@@ -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