372 lines
13 KiB
Python
372 lines
13 KiB
Python
import sys
|
|
import json
|
|
import requests
|
|
import random
|
|
from datetime import datetime
|
|
from typing import List
|
|
from urllib.parse import urlparse, urlencode
|
|
from requests.adapters import HTTPAdapter
|
|
from urllib3.util.retry import Retry
|
|
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
|
|
class CloudSearchSpider(Spider):
|
|
"""网盘资源搜索爬虫"""
|
|
|
|
DEFAULT_BASE_URL = "https://so.252035.xyz"
|
|
HEADERS = {
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
|
"Content-Type": "application/json"
|
|
}
|
|
SEARCH_PAGE_SIZE = 300
|
|
REQUEST_TIMEOUT = 30
|
|
MAX_RETRIES = 3
|
|
BACKOFF_FACTOR = 0.5
|
|
|
|
PAN_CONFIG = {
|
|
'ali': {
|
|
'api_type': 'aliyun',
|
|
'name': '阿里',
|
|
'keywords': ['alipan.com', 'aliyundrive.com'],
|
|
'icon': 'ali.png'
|
|
},
|
|
'quark': {
|
|
'api_type': 'quark',
|
|
'name': '夸克',
|
|
'keywords': ['pan.quark.cn'],
|
|
'icon': 'quark.png'
|
|
},
|
|
'uc': {
|
|
'api_type': 'uc',
|
|
'name': 'UC',
|
|
'keywords': ['drive.uc.cn'],
|
|
'icon': 'uc.png'
|
|
},
|
|
'xunlei': {
|
|
'api_type': 'xunlei',
|
|
'name': '迅雷',
|
|
'keywords': ['xunlei', 'thunder'],
|
|
'icon': 'xunlei.png'
|
|
},
|
|
'a123': {
|
|
'api_type': '123',
|
|
'name': '123',
|
|
'keywords': ['123684.com', '123685.com', '123912.com', '123pan.com', '123pan.cn', '123592.com'],
|
|
'icon': '123.png'
|
|
},
|
|
'a189': {
|
|
'api_type': 'tianyi',
|
|
'name': '天翼',
|
|
'keywords': ['cloud.189.cn'],
|
|
'icon': '189.png'
|
|
},
|
|
'a139': {
|
|
'api_type': 'mobile',
|
|
'name': '移动',
|
|
'keywords': ['caiyun.139.com'],
|
|
'icon': '139.png'
|
|
},
|
|
'a115': {
|
|
'api_type': '115',
|
|
'name': '115',
|
|
'keywords': ['115cdn.com', '115.com', 'anxia.com'],
|
|
'icon': '115.png'
|
|
},
|
|
'baidu': {
|
|
'api_type': 'baidu',
|
|
'name': '百度',
|
|
'keywords': ['baidu'],
|
|
'icon': 'baidu.png'
|
|
},
|
|
'pikpak': {
|
|
'api_type': 'pikpak',
|
|
'name': 'PikPak',
|
|
'keywords': ['pikpak'],
|
|
'icon': 'pikpak.png'
|
|
},
|
|
'magnet': {
|
|
'api_type': 'magnet',
|
|
'name': '磁力',
|
|
'keywords': ['magnet'],
|
|
'icon': 'cili.png'
|
|
},
|
|
'ed2k': {
|
|
'api_type': 'ed2k',
|
|
'name': '电驴',
|
|
'keywords': ['ed2k'],
|
|
'icon': ''
|
|
}
|
|
}
|
|
|
|
REVERSE_PAN_MAP = {v['api_type']: k for k, v in PAN_CONFIG.items()}
|
|
# 优化:从 PAN_CONFIG 自动派生网盘简称,消除数据冗余
|
|
PAN_SHORT_NAMES = {k: v['name'] for k, v in PAN_CONFIG.items()}
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.base_url = self.DEFAULT_BASE_URL
|
|
self.proxy = ''
|
|
self.pan_priority = ''
|
|
self.pan_order = ''
|
|
self.channels = ''
|
|
self.plugins = ''
|
|
self.cloud_types = ''
|
|
self.session = requests.Session()
|
|
self.session.headers.update(self.HEADERS)
|
|
|
|
# 增强重试策略
|
|
retries = Retry(
|
|
total=self.MAX_RETRIES,
|
|
backoff_factor=self.BACKOFF_FACTOR,
|
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
raise_on_status=False
|
|
)
|
|
self.session.mount('http://', HTTPAdapter(max_retries=retries))
|
|
self.session.mount('https://', HTTPAdapter(max_retries=retries))
|
|
|
|
def init(self, extend):
|
|
try:
|
|
extend_dict = json.loads(extend) if extend else {}
|
|
self.base_url = extend_dict.get('server', self.DEFAULT_BASE_URL).rstrip('/')
|
|
self.proxy = extend_dict.get('proxy', '')
|
|
self.pan_priority = extend_dict.get('pan_priority', '')
|
|
self.pan_order = extend_dict.get('pan_order', '')
|
|
# 新增三个搜索过滤参数
|
|
self.channels = extend_dict.get('channels', '')
|
|
self.plugins = extend_dict.get('plugins', '')
|
|
self.cloud_types = extend_dict.get('cloud_types', '')
|
|
except json.JSONDecodeError:
|
|
self._reset_to_defaults()
|
|
|
|
# 代理协议分离
|
|
if self.proxy:
|
|
self.session.proxies = {
|
|
"http": self.proxy,
|
|
"https": self.proxy
|
|
}
|
|
|
|
def _reset_to_defaults(self):
|
|
self.base_url = self.DEFAULT_BASE_URL
|
|
self.proxy = ''
|
|
self.pan_priority = ''
|
|
self.pan_order = ''
|
|
self.channels = ''
|
|
self.plugins = ''
|
|
self.cloud_types = ''
|
|
|
|
def getName(self):
|
|
return "盘搜"
|
|
|
|
def homeContent(self, filter):
|
|
return {'class': [{"type_id": "1", "type_name": "盘搜|聚合搜索"}], 'list': []}
|
|
|
|
def homeVideoContent(self):
|
|
return {}
|
|
|
|
def categoryContent(self, cid, page, filter, ext):
|
|
return {
|
|
'list': [{"vod_id": "1", "vod_name": "请在搜索框中输入关键词搜索", "vod_pic": "", "vod_remarks": "盘搜"}],
|
|
'page': 1, 'pagecount': 1, 'limit': 1, 'total': 1
|
|
}
|
|
|
|
def detailContent(self, did):
|
|
result = {'list': []}
|
|
if not did or not did[0]:
|
|
return result
|
|
|
|
resource_url = did[0]
|
|
pan_type = self._extract_pan_type_from_url(resource_url)
|
|
pan_config = self.PAN_CONFIG.get(pan_type, {})
|
|
pan_name = pan_config.get('name', '网盘资源')
|
|
pan_icon = self._get_icon_url(pan_config.get('icon', ''))
|
|
|
|
result['list'].append({
|
|
"vod_id": resource_url,
|
|
"vod_name": f"{pan_name}资源",
|
|
"vod_pic": pan_icon,
|
|
"vod_play_from": "盘搜",
|
|
"vod_play_url": f"盘搜${resource_url}",
|
|
"vod_content": f"网盘类型: {pan_name}\n资源链接: {resource_url}"
|
|
})
|
|
return result
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
return self._perform_search(key, pg)
|
|
|
|
def searchContentPage(self, key, quick, page):
|
|
return self._perform_search(key, page)
|
|
|
|
def _perform_search(self, keywords, page_str):
|
|
try:
|
|
page = int(page_str)
|
|
except (ValueError, TypeError):
|
|
page = 1
|
|
|
|
result = {'list': [], 'page': page, 'pagecount': 1, 'limit': self.SEARCH_PAGE_SIZE, 'total': 0}
|
|
if not keywords:
|
|
return result
|
|
|
|
try:
|
|
# 构建查询参数
|
|
params = {'kw': keywords}
|
|
if self.channels:
|
|
params['channels'] = self.channels
|
|
if self.plugins:
|
|
params['plugins'] = self.plugins
|
|
if self.cloud_types:
|
|
params['cloud_types'] = self.cloud_types
|
|
|
|
url = f"{self.base_url}/api/search"
|
|
search_response = self.session.get(
|
|
url,
|
|
params=params,
|
|
timeout=self.REQUEST_TIMEOUT
|
|
)
|
|
search_response.raise_for_status()
|
|
|
|
search_data = search_response.json()
|
|
if search_data.get('code') != 0:
|
|
return result
|
|
|
|
all_results = self._parse_and_sort_results(search_data, keywords)
|
|
total_count = len(all_results)
|
|
start_index = (page - 1) * self.SEARCH_PAGE_SIZE
|
|
paged_results = all_results[start_index:start_index + self.SEARCH_PAGE_SIZE]
|
|
|
|
result.update({
|
|
'list': paged_results,
|
|
'total': total_count,
|
|
'pagecount': max(1, (total_count + self.SEARCH_PAGE_SIZE - 1) // self.SEARCH_PAGE_SIZE)
|
|
})
|
|
|
|
except requests.RequestException:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
return result
|
|
|
|
def _parse_and_sort_results(self, data, keywords):
|
|
# 优化:移除此处的硬编码字典,它已在类级别定义
|
|
|
|
if self.pan_order:
|
|
enabled_pan_types = [pan.strip() for pan in self.pan_order.split(',') if pan.strip()]
|
|
use_pan_order = True
|
|
elif self.pan_priority:
|
|
enabled_pan_types = [pan.strip() for pan in self.pan_priority.split(',') if pan.strip()]
|
|
use_pan_order = False
|
|
else:
|
|
enabled_pan_types = []
|
|
use_pan_order = False
|
|
|
|
all_items = []
|
|
all_images = []
|
|
|
|
# 收集所有图片用于兜底随机
|
|
for items in data.get('data', {}).get('merged_by_type', {}).values():
|
|
for item in items:
|
|
if item.get('images'):
|
|
all_images.extend(item['images'])
|
|
|
|
# 设置随机种子,确保同关键词结果一致
|
|
random.seed(hash(keywords))
|
|
|
|
for cloud_type, items in data.get('data', {}).get('merged_by_type', {}).items():
|
|
pan_type = self.REVERSE_PAN_MAP.get(cloud_type, cloud_type)
|
|
if enabled_pan_types and pan_type not in enabled_pan_types:
|
|
continue
|
|
|
|
for item in items:
|
|
url = item.get('url')
|
|
if not url:
|
|
continue
|
|
|
|
pan_config = self.PAN_CONFIG.get(pan_type, {})
|
|
pan_icon = self._get_icon_url(pan_config.get('icon', ''))
|
|
# 优化:使用 self.PAN_SHORT_NAMES 替代本地字典
|
|
pan_short = self.PAN_SHORT_NAMES.get(pan_type, '网盘')
|
|
|
|
# 简化图片逻辑:优先自带 → 全局随机 → 图标
|
|
vod_pic = self._get_best_image(item, all_images, pan_icon)
|
|
|
|
dt_obj = self._to_datetime(item.get('datetime'))
|
|
time_str = dt_obj.strftime("%m-%d") if dt_obj else ""
|
|
|
|
source = item.get('source', '盘搜')
|
|
remarks = f"{pan_short}:{time_str}|{source}" if time_str else f"{pan_short}|{source}"
|
|
|
|
all_items.append({
|
|
"vod_id": url,
|
|
"vod_name": item.get('note', ''),
|
|
"vod_pic": vod_pic,
|
|
"vod_remarks": remarks,
|
|
"_timestamp": dt_obj.timestamp() if dt_obj else 0,
|
|
"_pan_type": pan_type
|
|
})
|
|
|
|
# 排序逻辑
|
|
if use_pan_order:
|
|
all_items.sort(key=lambda x: -x['_timestamp'])
|
|
elif enabled_pan_types:
|
|
pan_priority_order = {pan: idx for idx, pan in enumerate(enabled_pan_types)}
|
|
all_items.sort(key=lambda x: (pan_priority_order.get(x['_pan_type'], len(enabled_pan_types)), -x['_timestamp']))
|
|
else:
|
|
all_items.sort(key=lambda x: -x['_timestamp'])
|
|
|
|
# 清理临时字段
|
|
for item in all_items:
|
|
item.pop('_timestamp', None)
|
|
item.pop('_pan_type', None)
|
|
|
|
return all_items
|
|
|
|
def _get_best_image(self, item, all_images, pan_icon):
|
|
if item.get('images') and item['images']:
|
|
return item['images'][0]
|
|
if all_images:
|
|
return random.choice(all_images)
|
|
return pan_icon
|
|
|
|
def playerContent(self, flag, pid, vipFlags):
|
|
result = {"parse": 0, "header": self.HEADERS, "url": ""}
|
|
if not pid:
|
|
return result
|
|
|
|
url = pid.strip()
|
|
if not url.startswith('push:'):
|
|
if not url.startswith(('http://', 'https://')):
|
|
url = f'https://{url}'
|
|
result['url'] = f"push:{url}"
|
|
else:
|
|
result['url'] = url
|
|
|
|
return result
|
|
|
|
def _to_datetime(self, time_str):
|
|
if not time_str or time_str == "0001-01-01T00:00:00Z":
|
|
return None
|
|
try:
|
|
time_str_clean = time_str.replace('Z', '+00:00')
|
|
return datetime.fromisoformat(time_str_clean)
|
|
except (ValueError, TypeError):
|
|
try:
|
|
return datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
def _extract_pan_type_from_url(self, url):
|
|
if not url:
|
|
return "unknown"
|
|
url_lower = urlparse(url).netloc.lower() or url.lower()
|
|
for pan_type, config in self.PAN_CONFIG.items():
|
|
if any(keyword in url_lower for keyword in config['keywords']):
|
|
return pan_type
|
|
return "unknown"
|
|
|
|
def _get_icon_url(self, icon_name):
|
|
if not icon_name:
|
|
return ""
|
|
return f"http://127.0.0.1:9978/file/Download/lib/icon/{icon_name}"
|
|
|
|
Spider = CloudSearchSpider |