Sync all projects
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import html as html_module
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.qmao.net'
|
||||
self.session = requests.Session()
|
||||
self.ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
|
||||
self.session.headers.update({'User-Agent': self.ua})
|
||||
self.cateManual = {
|
||||
'电影': '1',
|
||||
'电视剧': '2',
|
||||
'动漫': '3',
|
||||
'短剧': '4',
|
||||
}
|
||||
self._m = chr(0x661f) + chr(0x6cb3)
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = html_module.unescape(text)
|
||||
text = text.replace('\xa0', ' ').replace(' ', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, headers={'Referer': self.site})
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '七猫短剧'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def _extract_list(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
for m in re.finditer(r'href="/voddetail/(\d+)\.html"', html):
|
||||
vid = m.group(1)
|
||||
if vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+800]
|
||||
# 标题:分类页 BrowseList,首页 FeaturedList,搜索页 MTagBookList
|
||||
title = ''
|
||||
tm = re.search(r'(BrowseList|FeaturedList|MTagBookList)_bookName[^>]*>(.*?)</a>', snippet, re.DOTALL)
|
||||
if tm:
|
||||
title = self._clean(tm.group(2))
|
||||
if not title:
|
||||
tm = re.search(r'title="([^"]*)"', snippet)
|
||||
if tm:
|
||||
title = self._clean(tm.group(1))
|
||||
# 封面
|
||||
pic = ''
|
||||
pm = re.search(r'src="([^"]*\.(?:jpg|webp)[^"]*)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).strip()
|
||||
if not pic.startswith('http'):
|
||||
pic = self.site + pic
|
||||
# 备注
|
||||
note = ''
|
||||
nm = re.search(r'(BrowseList|FeaturedList)_(?:lastChapter|tagsBox)[^>]*>(.*?)</(?:a|div)', snippet, re.DOTALL)
|
||||
if nm:
|
||||
note = self._clean(nm.group(2))
|
||||
if not note:
|
||||
nm = re.search(r'(BrowseList|FeaturedList)_bookViewCount[^>]*>([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = self._clean(nm.group(2))
|
||||
# 搜索页:从 img alt 获取集数
|
||||
if not note:
|
||||
am = re.search(r'alt="([^"]*(?:集|完结)[^"]*)"', snippet)
|
||||
if am:
|
||||
note = self._clean(am.group(1))
|
||||
if title:
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
html = self._get(self.site)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
url = f'{self.site}/vodtype/{tid}.html'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if result['list'] else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ''
|
||||
if isinstance(ids, list):
|
||||
vid = ids[0] if ids else ''
|
||||
elif ids:
|
||||
vid = str(ids)
|
||||
if not vid:
|
||||
return result
|
||||
|
||||
# 播放页获取 player_aaaa
|
||||
play_html = self._get(f'{self.site}/vodplay/{vid}-1-1.html')
|
||||
pd = {}
|
||||
if play_html:
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', play_html)
|
||||
if m:
|
||||
try:
|
||||
pd = json.loads(m.group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 详情页
|
||||
detail_html = self._get(f'{self.site}/voddetail/{vid}.html')
|
||||
|
||||
# 标题
|
||||
title = pd.get('vod_data', {}).get('vod_name', '')
|
||||
if not title:
|
||||
m2 = re.search(r'dramaDetail_bookName[^>]*>([^<]+)<', detail_html)
|
||||
if m2:
|
||||
title = self._clean(m2.group(1))
|
||||
if not title:
|
||||
m2 = re.search(r'<title>([^<]+)', detail_html)
|
||||
if m2:
|
||||
title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
|
||||
|
||||
# 封面
|
||||
pic = ''
|
||||
m2 = re.search(r'dramaDetail_bookCover[^>]*>\s*<img[^>]*src="([^"]*)"', detail_html)
|
||||
if m2:
|
||||
pic = m2.group(1).strip()
|
||||
if not pic.startswith('http'):
|
||||
pic = self.site + pic
|
||||
|
||||
# 标签/类型
|
||||
vod_class = ''
|
||||
m2 = re.search(r'dramaDetail_tagsBox[^>]*>(.*?)</div>', detail_html, re.DOTALL)
|
||||
if m2:
|
||||
vod_class = self._clean(m2.group(1))
|
||||
if not vod_class:
|
||||
vod_class = pd.get('vod_data', {}).get('vod_class', '')
|
||||
|
||||
# 演员
|
||||
actor = pd.get('vod_data', {}).get('vod_actor', '')
|
||||
|
||||
# 导演
|
||||
director = pd.get('vod_data', {}).get('vod_director', '')
|
||||
if director:
|
||||
director = self._m + '、' + director
|
||||
else:
|
||||
director = self._m
|
||||
|
||||
# 播放列表 - 从播放页提取集数
|
||||
play_from = []
|
||||
play_url_list = []
|
||||
|
||||
if play_html:
|
||||
# 提取所有线路
|
||||
tabs = re.findall(r'episode_tabBtn[^>]*data-sid="(\d+)"[^>]*data-from="([^"]*)"[^>]*>([^<]*)<', play_html)
|
||||
if not tabs:
|
||||
tabs = re.findall(r'episode_tabBtn[^>]*>([^<]*)<', play_html)
|
||||
if tabs:
|
||||
tabs = [(str(i+1), '', t) for i, t in enumerate(tabs)]
|
||||
|
||||
# 提取集数链接
|
||||
episodes = []
|
||||
for em in re.finditer(r'<a[^>]*class="CatalogList_linkBox"[^>]*href="(/vodplay/[^"]+)"', play_html):
|
||||
ep_href = em.group(1)
|
||||
# 在 </a> 前提取集数编号
|
||||
snippet = play_html[em.start():em.start()+600]
|
||||
num = re.search(r'>\s*(?:<[^>]*>\s*)*(\d+)\s*</a>', snippet)
|
||||
if num:
|
||||
ep_num = num.group(1).strip()
|
||||
episodes.append(f'第{ep_num}集${ep_href}')
|
||||
else:
|
||||
episodes.append(f'播放${ep_href}')
|
||||
# 备用:data-part
|
||||
if not episodes:
|
||||
for em in re.finditer(r'href="(/vodplay/[^"]+)"[^>]*data-part="([^"]*)"', play_html):
|
||||
episodes.append(f'{em.group(2)}${em.group(1)}')
|
||||
if not episodes:
|
||||
for em in re.finditer(r'href="(/vodplay/[^"]+)"[^>]*>([^<]*)<', play_html):
|
||||
if em.group(2).strip():
|
||||
episodes.append(f'{em.group(2).strip()}${em.group(1)}')
|
||||
|
||||
if episodes:
|
||||
line_name = tabs[0][2] if tabs else '默认'
|
||||
if not line_name:
|
||||
line_name = tabs[0][1] or '默认'
|
||||
play_from.append(self._clean(line_name))
|
||||
play_url_list.append('#'.join(episodes))
|
||||
|
||||
# 如果没有从播放页拿到集数,用 player_aaaa 的 URL 直接播放
|
||||
if not play_from and pd:
|
||||
url = pd.get('url', '')
|
||||
from_flag = pd.get('from', '')
|
||||
if url:
|
||||
play_from.append(from_flag or '默认')
|
||||
play_url_list.append(f'播放${url}')
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': vod_class,
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': '',
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url_list)
|
||||
}
|
||||
result['list'].append(vod)
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
play_url = id
|
||||
if not play_url.startswith('http'):
|
||||
play_url = self.site + play_url
|
||||
|
||||
html = self._get(play_url)
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
|
||||
if m:
|
||||
pd = json.loads(m.group(1))
|
||||
url = pd.get('url', '')
|
||||
if url:
|
||||
result['parse'] = 0
|
||||
result['url'] = url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
|
||||
if not result:
|
||||
result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
wd = requests.utils.quote(key)
|
||||
url = f'{self.site}/vodsearch/-------------.html?wd={wd}'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
import hashlib
|
||||
import urllib3
|
||||
import concurrent.futures
|
||||
from urllib.parse import quote
|
||||
from base.spider import Spider
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
sys.path.append('..')
|
||||
|
||||
class Spider(Spider):
|
||||
host, userid, episode_list = '', '', []
|
||||
|
||||
# ---------- 加密与签名相关常量 ----------
|
||||
PUB_KEY_B64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoYt0BP77U+DM08BiI/QbSRIfxijXo85BTPqIM1Ow8BNwhLETzRIZ+dEwdWDbydG/PspgBAfRpGaYVdJYtvaC2JnoO8+Ik6qMWojfEJxSFLa0Pb0A892tun4gsxoEMjcreZ+YGyaBxAfqX0BSMfdrOgIYaZQjYrw9TRLlUT31QoQIDAQAB"
|
||||
APP_SIGN_SHA1 = "09a8dc51639a31801af5f6418caebfabc695eb24"
|
||||
DEVICE_ID = "2d590b9842d064a1"
|
||||
|
||||
# RSA 私钥(用于解密响应)
|
||||
PRIV_KEY_B64 = """MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCquQQ5r6+yJI8CDFkXRp8vUsdD45ov8EP12ooLs56ca2DQXaSNGS9910bAPVA9chkp0mKIvKqjAsHz5Tl9EeNPblarGEeJUIxpxZtiSqNTpvtiD/TjhpzuHYic7RAfQ/h7p/ypE8ymU42pYjsB5t26Mv6XgkLV+jzrSf73HlCuS0iMyLmt6zz3Mw9izM13EpB8iFLtfbbYymycKTx4RAmPQLwhNGex/AlUIYxXP4R2yyaa4W6mEtc6aME2QuzJFxPgP3HJ9NBx/LWVn4skxWjZ7zg+VRQRHnjyVaSLu3Z5gN5ITWCyE32qaHJa6WBahZj5jWhRyAG1bQ+xKJa8lBL5AgMBAAECggEAUwv9SjJ0PSwbhNuM2w23kcWquROWhYtTA91zGY4esehqB/IFgb2mpIh8Gje5OKqwIu/8jpd4SiOlRYdUF8sD0DfUYRZGdj2AkFNX6tBz8tVfo6wvbB6naA1lzzBij1L5JO3qsjS3cJFkb+kg2yP66AC2Z+0tpfk8eRhdtshAZwfcd1DEGt1uAvYL1eaUK9HRvpt9lPeGcHERDl2hBd4uyaF0K1O+zF9y59nYbTySWPxRZq3sFEE85xRMlstD7YZi7W2gKvMFRD4/FKmrZ3m7aKJRITtyKOyyPcYmepNv3Qv7kk59Pg38n2WWQ0Ra/bCH3E48YNCnQvZMpitkTfJhoQKBgQDbnROOYTP8OTJ6f/qhoGjxeO3x1VOaOp8l0x7b0SCfoqNGS0Cyiqj72BmJtPMPqSTjn6MmNzqbg1KOdhXyzNozs+i5ccW1M56j96mr5I/Z0FpE3oyIHNfDDBlf9M8YQqEF9oYxniYYft9oapO7cRQkHER6qpvnHTavwlv4m78CXwKBgQDHAjs2YlpKDdI1lcbZJCc7TwtH+Pd2bUki8YXafWNcPhITQHbOZjr310eK1QJC6GJncjkOqbX7yv3ivvTO35FZTQhuA1xEG1P00FG8bE0tHYPIwQHi9y0eA5cieMdo8E6XYria1mw/3fqSQEsfZyJlR32JQIoGAipM8iO1X2nZpwKBgDkMFIhnt5lNQk+P7wsNIDWZtDWdtJnboHuy29E+Abt2A/O+mI/IdRz2hau/1WO8DFkUnszOi+rZshhPlGP90rCbi1igtTrcrdjp/KkqNjPea5R4OwkgdOu1uOG0NheXNzzVTQaWjk7Opjn5dWa7eP/oV+GFb/oZHJuLYVizHGsBAoGADA7rjZEKDYCm4w5PPSr+oY5ZjaPdQrS+gLqHtMRyN82fBMGcMUdqfUfzEstzVqCEDeaS5HuOBlK3bXzKkppjUTjksN3NQmcxgBz7RuJ9DqXCLXDcb2cwuafYCYOt+YLOEEgwDVm+t2P44dG5e46hO+fICH/7nP+WlpD5buz4GfMCgYB57r3g/6hi9WUDnfc7ZAzWMqR0EhJVYKYy+KFEtdIPzhkkIHq5RASe88E9kzoGoZFdb3tIjvGZWcHerirrqWkMsuQtP/Qi0zjieid5tAPj+r4kbiCVTw0E0jnmPBzGInQi7lpeTTKnG1fbyS5lBS+WmHfIuzpECgCkxhaT+LJJkg=="""
|
||||
|
||||
headers = {
|
||||
'User-Agent': "okhttp/4.12.0",
|
||||
'Connection': "Keep-Alive",
|
||||
'Accept-Encoding': "gzip",
|
||||
'Content-Type': "application/json;charset=UTF-8",
|
||||
'Cache-Control': "no-cache",
|
||||
'token': "",
|
||||
'deviceId': DEVICE_ID,
|
||||
'client': "app",
|
||||
'deviceType': "Android"
|
||||
}
|
||||
|
||||
# ---------- RSA 加密 ----------
|
||||
def rsa_encrypt(self, data: str) -> str:
|
||||
key = RSA.import_key(base64.b64decode(self.PUB_KEY_B64))
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
encrypted = cipher.encrypt(data.encode('utf-8'))
|
||||
return base64.b64encode(encrypted).decode('utf-8')
|
||||
|
||||
# ---------- RSA 解密(支持分块) ----------
|
||||
def rsa_decrypt(self, encrypted_b64: str) -> str:
|
||||
key = RSA.import_key(base64.b64decode(self.PRIV_KEY_B64))
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
encrypted_bytes = base64.b64decode(encrypted_b64)
|
||||
block_size = 256
|
||||
decrypted_parts = []
|
||||
for i in range(0, len(encrypted_bytes), block_size):
|
||||
block = encrypted_bytes[i:i+block_size]
|
||||
decrypted_parts.append(cipher.decrypt(block, None))
|
||||
return b''.join(decrypted_parts).decode('utf-8')
|
||||
|
||||
# ---------- 构建签名参数 ----------
|
||||
def build_params_string(self, episode_id="", episode_index="", vid="", player_id="", type_id="", user_id=""):
|
||||
return (f"episodeId{episode_id}"
|
||||
f"episodeIndex{episode_index}"
|
||||
f"id{vid}"
|
||||
f"playerId{player_id}"
|
||||
f"source0"
|
||||
f"typeId{type_id}"
|
||||
f"userId{user_id}")
|
||||
|
||||
def generate_sign(self, timestamp: str, params_str: str, device_id: str) -> str:
|
||||
raw = f"SaltLSFBTimestamp{timestamp}Params{params_str}ClientappDeviceId{device_id}"
|
||||
b64 = base64.b64encode(raw.encode('utf-8')).decode('utf-8')
|
||||
md5 = hashlib.md5(b64.encode('utf-8')).hexdigest().upper()
|
||||
return md5
|
||||
|
||||
def build_encrypted_headers(self, body_json: str, params_str: str) -> dict:
|
||||
timestamp = str(int(time.time()))
|
||||
encrypted_key = self.rsa_encrypt(body_json)
|
||||
snjm = self.rsa_encrypt("113")
|
||||
appsign = self.rsa_encrypt(self.APP_SIGN_SHA1)
|
||||
sign = self.generate_sign(timestamp, params_str, self.DEVICE_ID)
|
||||
|
||||
headers = {
|
||||
"snjm": snjm,
|
||||
"appsign": appsign,
|
||||
"timestamp": timestamp,
|
||||
"sign": sign,
|
||||
"deviceId": self.DEVICE_ID,
|
||||
"token": self.headers.get('token', ''),
|
||||
"client": "app",
|
||||
"deviceType": "Android",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "okhttp/4.12.0"
|
||||
}
|
||||
return headers, {"key": encrypted_key}
|
||||
|
||||
# ---------- 原有接口(保持不变) ----------
|
||||
def init(self, extend=''):
|
||||
self.headers['deviceId'] = self.DEVICE_ID
|
||||
self.host = 'http://qkys.qukanwh.com'
|
||||
response = self.fetch(f'{self.host}/api/v1/app/user/visitorInfo', headers=self.headers).json()
|
||||
self.userid = response['data']['id']
|
||||
token = response['data']['token']
|
||||
self.headers['token'] = token
|
||||
|
||||
def homeContent(self, filter):
|
||||
response = self.post(f'{self.host}/api/v1/app/screen/screenType', headers=self.headers).json()
|
||||
data = response['data']
|
||||
classes = []
|
||||
for i in data:
|
||||
classes.append({'type_id': i['id'], 'type_name': i['name']})
|
||||
return {'class': classes}
|
||||
|
||||
def homeVideoContent(self):
|
||||
response = self.post(f'{self.host}/api/v1/app/recommend/recommendList', headers=self.headers).json()
|
||||
data = response['data']
|
||||
videos = []
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_to_id = {
|
||||
executor.submit(
|
||||
self.post,
|
||||
f'{self.host}/api/v1/app/recommend/recommendSubList',
|
||||
data=json.dumps({
|
||||
"condition": item['id'],
|
||||
"pageNum": 1,
|
||||
"pageSize": 6
|
||||
}),
|
||||
headers=self.headers
|
||||
): item['id'] for item in data
|
||||
}
|
||||
for future in concurrent.futures.as_completed(future_to_id):
|
||||
try:
|
||||
response = future.result().json()
|
||||
for video in response['data']['records']:
|
||||
videos.append({
|
||||
"vod_id": video['id'],
|
||||
"vod_name": video['name'],
|
||||
"vod_pic": video['cover']
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Request failed for item {future_to_id[future]}: {str(e)}")
|
||||
return {'list': videos}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
payload = {
|
||||
"condition": {
|
||||
"classify": "",
|
||||
"region": "",
|
||||
"sreecnTypeEnum": "NEWEST",
|
||||
"typeId": tid,
|
||||
"year": ""
|
||||
},
|
||||
"pageNum": pg,
|
||||
"pageSize": 40
|
||||
}
|
||||
response = self.post(f'{self.host}/api/v1/app/screen/screenMovie', data=json.dumps(payload), headers=self.headers).json()
|
||||
videos = []
|
||||
for i in response['data']['records']:
|
||||
videos.append({
|
||||
"vod_id": i['id'],
|
||||
"vod_name": i['name'],
|
||||
"vod_pic": i['cover'],
|
||||
"vod_remarks": i['area'],
|
||||
"vod_year": i['year']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
payload = {
|
||||
"condition": {
|
||||
"value": key
|
||||
},
|
||||
"pageNum": pg,
|
||||
"pageSize": 40
|
||||
}
|
||||
response = self.post(f'{self.host}/api/v1/app/search/searchMovie', data=json.dumps(payload), headers=self.headers).json()
|
||||
videos = []
|
||||
for i in response['data']['records']:
|
||||
videos.append({
|
||||
'vod_id': i['id'],
|
||||
'vod_name': i['name'],
|
||||
'vod_pic': i['cover'],
|
||||
'vod_remarks': i['area'],
|
||||
'vod_year': i['year'],
|
||||
'vod_area': i['area'],
|
||||
'vod_content': i['desc']
|
||||
})
|
||||
return {'list': videos, 'page': pg}
|
||||
|
||||
# ---------- 详情页(已集成解密) ----------
|
||||
def detailContent(self, ids):
|
||||
type_id = "M15" # 注意:原脚本写死为 M17,可根据需要修改
|
||||
vid = ids[0]
|
||||
body = {
|
||||
"id": vid,
|
||||
"source": 0,
|
||||
"typeId": type_id,
|
||||
"userId": self.userid,
|
||||
"episodeId": "",
|
||||
"episodeIndex": "",
|
||||
"playerId": ""
|
||||
}
|
||||
body_json = json.dumps(body, separators=(',', ':'))
|
||||
params_str = self.build_params_string(
|
||||
episode_id="",
|
||||
episode_index="",
|
||||
vid=str(vid),
|
||||
player_id="",
|
||||
type_id=type_id,
|
||||
user_id=str(self.userid)
|
||||
)
|
||||
headers, payload = self.build_encrypted_headers(body_json, params_str)
|
||||
|
||||
# 发送加密请求
|
||||
resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(payload), headers=headers).json()
|
||||
encrypted_data = resp_raw.get('data')
|
||||
if not encrypted_data:
|
||||
raise Exception("响应中 data 为空")
|
||||
# 解密 data 字段
|
||||
decrypted_json_str = self.rsa_decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_json_str)
|
||||
|
||||
# 后续处理与原脚本相同
|
||||
currentplayerid = data['playerId']
|
||||
play_urls = []
|
||||
play_url = []
|
||||
show = []
|
||||
for i in data['episodeList']:
|
||||
play_url.append(f"{i['episode']}${ids[0]}@{currentplayerid}@{i['id']}@episode")
|
||||
play_urls.append('#'.join(play_url))
|
||||
moviePlayerList = data['moviePlayerList']
|
||||
for i2 in moviePlayerList:
|
||||
if i2['id'] == currentplayerid:
|
||||
show.append(i2['moviePlayerName'])
|
||||
for j in moviePlayerList:
|
||||
playerid = j['id']
|
||||
episodeTotal = j.get('episodeTotal')
|
||||
if playerid == currentplayerid or episodeTotal is None:
|
||||
continue
|
||||
play_url = []
|
||||
for k in range(1, episodeTotal + 1):
|
||||
play_url.append(f"第{k}集${k}@{playerid}@{ids[0]}@virtual")
|
||||
play_urls.append('#'.join(play_url))
|
||||
if j['moviePlayerName'] not in show:
|
||||
show.append(j['moviePlayerName'])
|
||||
|
||||
# 获取简介(此接口可能无需加密,保持原样)
|
||||
payload_desc = {
|
||||
"id": ids[0],
|
||||
"typeId": type_id
|
||||
}
|
||||
response_desc = self.post(f'{self.host}/api/v1/app/play/movieDesc', data=json.dumps(payload_desc), headers=self.headers).json()
|
||||
data2 = response_desc['data']
|
||||
|
||||
video = {
|
||||
'vod_id': data2['id'],
|
||||
'vod_name': data2['name'],
|
||||
'vod_pic': data2['cover'],
|
||||
'vod_content': data2['introduce'],
|
||||
'vod_year': data2['year'],
|
||||
'vod_area': data2['area'],
|
||||
'vod_remarks': '',
|
||||
'vod_score': data2['score'],
|
||||
'type_name': data2['classify'],
|
||||
'vod_director': data2['director'],
|
||||
'vod_actor': data2['star'],
|
||||
'vod_play_from': '$$$'.join(show),
|
||||
'vod_play_url': '$$$'.join(play_urls)
|
||||
}
|
||||
return {'list': [video]}
|
||||
|
||||
# ---------- 播放页(已集成解密) ----------
|
||||
def playerContent(self, flag, id, vipflags):
|
||||
param, playerid, param2, param3 = id.split('@')
|
||||
if param3 == 'virtual':
|
||||
payload = {
|
||||
"episodeIndex": str(int(param) - 1),
|
||||
"id": int(param2),
|
||||
"playerId": playerid,
|
||||
"source": 0,
|
||||
"typeId": "M15",
|
||||
"userId": self.userid,
|
||||
"episodeId": ""
|
||||
}
|
||||
else:
|
||||
payload = {
|
||||
"episodeId": param2,
|
||||
"id": int(param),
|
||||
"playerId": playerid,
|
||||
"source": 0,
|
||||
"typeId": "M15",
|
||||
"userId": self.userid,
|
||||
"episodeIndex": ""
|
||||
}
|
||||
body_json = json.dumps(payload, separators=(',', ':'))
|
||||
print(body_json)
|
||||
params_str = self.build_params_string(
|
||||
episode_id=payload.get("episodeId", ""),
|
||||
episode_index=payload.get("episodeIndex", ""),
|
||||
vid=str(payload["id"]),
|
||||
player_id=payload["playerId"],
|
||||
type_id=payload["typeId"],
|
||||
user_id=str(payload["userId"])
|
||||
)
|
||||
print(params_str)
|
||||
headers, encrypted_payload = self.build_encrypted_headers(body_json, params_str)
|
||||
print(headers)
|
||||
print(encrypted_payload)
|
||||
# 获取播放信息(加密响应)
|
||||
resp_raw = self.post(f'{self.host}/api/v1/app/play/movieDetails', data=json.dumps(encrypted_payload), headers=headers).json()
|
||||
encrypted_data = resp_raw.get('data')
|
||||
if not encrypted_data:
|
||||
raise Exception("响应中 data 为空")
|
||||
decrypted_json_str = self.rsa_decrypt(encrypted_data)
|
||||
data = json.loads(decrypted_json_str)
|
||||
print(data)
|
||||
parse_url = data['url']
|
||||
playerid = data['playerId']
|
||||
|
||||
# 调用分析接口(注:analysisMovieUrl 的响应可能也是加密的,但原脚本直接取 data,这里暂不做额外解密)
|
||||
analysis_body = {
|
||||
"playerUrl": parse_url,
|
||||
"playerId": playerid
|
||||
}
|
||||
analysis_json = json.dumps(analysis_body, separators=(',', ':'))
|
||||
# analysisMovieUrl 接口的参数拼接?理论上也需要签名,但原脚本是 GET 方式,为了兼容,我们沿用原脚本的 GET 方式
|
||||
# 原脚本使用 fetch GET 带参数,并未加密。这里也采用 GET 方式,不使用加密 headers
|
||||
resp_analysis = self.fetch(f"{self.host}/api/v1/app/play/analysisMovieUrl?playerUrl={quote(parse_url,safe='')}&playerId={playerid}", headers=self.headers).json()
|
||||
url = resp_analysis.get('data')
|
||||
|
||||
return {'jx': '0', 'parse': '0', 'url': url, 'header': {'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'}}
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def localProxy(self, param):
|
||||
pass
|
||||
@@ -0,0 +1,343 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.cd-zj.com'
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'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',
|
||||
'Referer': 'https://www.cd-zj.com/'
|
||||
})
|
||||
self.cateManual = {
|
||||
'\u7535\u5f71': '1',
|
||||
'\u7535\u89c6\u5267': '2',
|
||||
'\u7efc\u827a': '3',
|
||||
'\u52a8\u6f2b': '4',
|
||||
'\u70ed\u95e8\u77ed\u5267': '5',
|
||||
'\u817e\u8bafSVIP': 'label/qq',
|
||||
'\u4f18\u9177SVIP': 'label/youku',
|
||||
'B\u7ad9SVIP': 'label/bli',
|
||||
}
|
||||
|
||||
def init(self, extend=""):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return "\u67ab\u53f64K\u5907\u7528"
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = text.replace(' ', ' ').replace('&', '&').replace('\u3000', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15)
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def getVid(self, url):
|
||||
if not url:
|
||||
return ''
|
||||
m = re.search(r'/detail/(\d+)\.html', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.search(r'/play/(\d+)-', url)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ''
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
html = self._get(self.site)
|
||||
seen = set()
|
||||
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
|
||||
href = m.group(1)
|
||||
title = m.group(2)
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+500]
|
||||
pic = ''
|
||||
pm = re.search(r'data-src="([^"]+)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
note = ''
|
||||
nm = re.search(r'ft2">([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = nm.group(1)
|
||||
if title:
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'homeVideoContent error: {e}')
|
||||
return {'list': videos, 'parse': 0, 'jx': 0}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
if str(tid).startswith('label/'):
|
||||
if page == 1:
|
||||
url = f'{self.site}/{tid}.html'
|
||||
else:
|
||||
url = f'{self.site}/{tid}-{page}.html'
|
||||
else:
|
||||
if page == 1:
|
||||
url = f'{self.site}/type/{tid}.html'
|
||||
else:
|
||||
url = f'{self.site}/type/{tid}-{page}.html'
|
||||
|
||||
html = self._get(url)
|
||||
seen = set()
|
||||
for m in re.finditer(r'class="public-list-exp"[^>]*href="([^"]+)"[^>]*title="([^"]*)"', html):
|
||||
href = m.group(1)
|
||||
title = m.group(2)
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+500]
|
||||
pic = ''
|
||||
pm = re.search(r'data-src="([^"]+)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
note = ''
|
||||
nm = re.search(r'ft2">([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = nm.group(1)
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'categoryContent error: {e}')
|
||||
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if len(result['list']) > 0 else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ids[0] if ids else ''
|
||||
if not vid:
|
||||
return result
|
||||
try:
|
||||
html = self._get(f'{self.site}/detail/{vid}.html')
|
||||
|
||||
title = ''
|
||||
tm = re.search(r'<title>\u300a(.+?)\u300b', html)
|
||||
if tm:
|
||||
title = tm.group(1)
|
||||
if not title:
|
||||
tm = re.search(r'<title>([^<]+)', html)
|
||||
if tm:
|
||||
title = self._clean(tm.group(1))
|
||||
|
||||
pic = ''
|
||||
pm = re.search(r'lazy1[^>]*data-src="([^"]+)"', html)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
|
||||
desc = ''
|
||||
dm = re.search(r'<meta name="description" content="(.+?)"', html)
|
||||
if dm:
|
||||
desc = dm.group(1).replace('\u5267\u60c5\u4ecb\u7ecd\uff1a', '').strip()
|
||||
|
||||
actor = ''
|
||||
director = ''
|
||||
info = re.search(r'slide-info(.*?)(?:anthology|swiper)', html, re.DOTALL)
|
||||
if info:
|
||||
block = info.group(1)
|
||||
am = re.search(r'\u4e3b\u6f14[:\uff1a]\s*([^\n<]+)', block)
|
||||
if am:
|
||||
actor = am.group(1).strip()
|
||||
dm2 = re.search(r'\u5bfc\u6f14[:\uff1a]\s*([^\n<]+)', block)
|
||||
if dm2:
|
||||
director = dm2.group(1).strip()
|
||||
|
||||
play_from = []
|
||||
play_url = []
|
||||
|
||||
# \u627e anthology-tab \u533a\u5757\u5185\u7684\u6240\u6709 <a class="swiper-slide">
|
||||
tab_block = re.search(r'class="anthology-tab[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
|
||||
if tab_block:
|
||||
tabs = re.findall(r'<a[^>]*class="swiper-slide"[^>]*>(.*?)</a>', tab_block.group(1), re.DOTALL)
|
||||
else:
|
||||
tabs = []
|
||||
|
||||
# \u627e\u6240\u6709 anthology-list-box \u533a\u5757
|
||||
panels = re.findall(r'class="anthology-list-box[^"]*"[^>]*>(.*?)</div>\s*</div>', html, re.DOTALL)
|
||||
|
||||
for i, tab in enumerate(tabs):
|
||||
tab_name = self._clean(tab) or f'\u7ebf\u8def{i+1}'
|
||||
play_from.append(tab_name)
|
||||
episodes = []
|
||||
if i < len(panels):
|
||||
for em in re.finditer(r'<a[^>]*href="([^"]+)"[^>]*>([^<]+)<', panels[i]):
|
||||
ep_href = em.group(1)
|
||||
ep_name = em.group(2).strip()
|
||||
if ep_name and ep_href:
|
||||
episodes.append(f'{ep_name}${ep_href}')
|
||||
play_url.append('#'.join(episodes))
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': '',
|
||||
'vod_year': '',
|
||||
'vod_area': '',
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director if director else bytes.fromhex('e6989fe6b2b3').decode('utf-8'),
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from) if play_from else '',
|
||||
'vod_play_url': '$$$'.join(play_url) if play_url else ''
|
||||
}
|
||||
result['list'].append(vod)
|
||||
except Exception as e:
|
||||
print(f'detailContent error: {e}')
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
play_url = id
|
||||
if id and not id.startswith('http'):
|
||||
play_url = self.site + id
|
||||
|
||||
html = self._get(play_url)
|
||||
|
||||
# \u4f18\u5148\u4ece player_aaaa JSON \u63d0\u53d6 m3u8 \u76f4\u94fe
|
||||
m = re.search(r'player_aaaa\s*=\s*(\{.+?\})\s*<', html)
|
||||
if m:
|
||||
try:
|
||||
data = json.loads(m.group(1))
|
||||
m3u8 = data.get('url', '')
|
||||
if m3u8 and '.m3u8' in m3u8:
|
||||
result['parse'] = 0
|
||||
result['url'] = m3u8
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'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',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
# \u5907\u7528: \u4ece\u9875\u9762\u4e2d\u627e m3u8 \u94fe\u63a5
|
||||
m = re.search(r'url":\s*"(https?://[^"]*\.m3u8[^"]*)"', html)
|
||||
if m:
|
||||
result['parse'] = 0
|
||||
result['url'] = m.group(1).replace('\\/', '/')
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'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',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
# \u5907\u7528: iframe
|
||||
m = re.search(r'<iframe[^>]+src="([^"]+)"', html)
|
||||
if m:
|
||||
result['parse'] = 1
|
||||
result['url'] = m.group(1)
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'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',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
else:
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'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',
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
result['parse'] = 1
|
||||
result['url'] = id
|
||||
result['jx'] = 0
|
||||
result['header'] = {}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
try:
|
||||
url = f'{self.site}/cupfox-search/-------------.html'
|
||||
params = {'wd': key}
|
||||
if page > 1:
|
||||
params['page'] = page
|
||||
|
||||
html = self._get(url)
|
||||
seen = set()
|
||||
for m in re.finditer(r'href="(/detail/\d+\.html)"[^>]*title="([^"]*)"', html):
|
||||
href = m.group(1)
|
||||
title = m.group(2)
|
||||
vid = self.getVid(href)
|
||||
if not vid or vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
snippet = html[m.start():m.start()+500]
|
||||
pic = ''
|
||||
pm = re.search(r'data-src="([^"]+)"', snippet)
|
||||
if pm:
|
||||
pic = pm.group(1).replace('&', '&')
|
||||
note = ''
|
||||
nm = re.search(r'ft2">([^<]+)<', snippet)
|
||||
if nm:
|
||||
note = nm.group(1)
|
||||
if title:
|
||||
result['list'].append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'searchContent error: {e}')
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -0,0 +1,283 @@
|
||||
#coding=utf-8
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import html as html_module
|
||||
import requests
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.site = 'https://www.jxuma.com'
|
||||
self.session = requests.Session()
|
||||
self.ua = 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
|
||||
self.session.headers.update({'User-Agent': self.ua})
|
||||
self.cateManual = {
|
||||
'电影': '1',
|
||||
'电视剧': '2',
|
||||
'综艺': '3',
|
||||
'动漫': '4',
|
||||
'短剧': '36',
|
||||
}
|
||||
self._m = chr(0x661f) + chr(0x6cb3)
|
||||
|
||||
def _clean(self, text):
|
||||
if not text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', text)
|
||||
text = html_module.unescape(text)
|
||||
text = text.replace('\xa0', ' ')
|
||||
text = ' '.join(text.split())
|
||||
return text.strip()
|
||||
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = self.session.get(url, timeout=15, headers={'Referer': self.site})
|
||||
r.encoding = 'utf-8'
|
||||
return r.text
|
||||
except:
|
||||
return ''
|
||||
|
||||
def init(self, extend=''):
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
return '麻花影视'
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
result = {'class': [], 'filters': {}, 'list': [], 'parse': 0, 'jx': 0}
|
||||
for k, v in self.cateManual.items():
|
||||
result['class'].append({'type_id': str(v), 'type_name': k})
|
||||
return result
|
||||
|
||||
def _extract_list(self, html):
|
||||
videos = []
|
||||
seen = set()
|
||||
for m in re.finditer(r'href="/umo/(\d+)\.html"[^>]*?title="([^"]*)"', html):
|
||||
vid = m.group(1)
|
||||
title = m.group(2).strip()
|
||||
if vid in seen or not title:
|
||||
continue
|
||||
snippet = html[m.start():m.start()+400]
|
||||
pm = re.search(r'data-original="([^"]*)"', snippet)
|
||||
pic = pm.group(1).strip() if pm else ''
|
||||
if vid in seen:
|
||||
continue
|
||||
seen.add(vid)
|
||||
note = ''
|
||||
nm = re.search(r'pic-text text-right">([^<]*)', snippet)
|
||||
if nm:
|
||||
note = nm.group(1).strip()
|
||||
videos.append({
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'vod_remarks': note
|
||||
})
|
||||
return videos
|
||||
|
||||
def homeVideoContent(self):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
html = self._get(self.site)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
page = int(pg) if pg else 1
|
||||
url = f'{self.site}/jxk/{tid}.html'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
result['page'] = page
|
||||
result['pagecount'] = page + 1 if result['list'] else page
|
||||
result['limit'] = len(result['list'])
|
||||
result['total'] = len(result['list'])
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
vid = ''
|
||||
if isinstance(ids, list):
|
||||
vid = ids[0] if ids else ''
|
||||
elif ids:
|
||||
vid = str(ids)
|
||||
if not vid:
|
||||
return result
|
||||
# 用播放页第一集来获取 player_aaaa 数据
|
||||
html = self._get(f'{self.site}/aey/{vid}/1-1.html')
|
||||
if not html:
|
||||
return result
|
||||
|
||||
# 提取 player_aaaa
|
||||
pd = {}
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
|
||||
if m:
|
||||
try:
|
||||
pd = json.loads(m.group(1))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 从详情页获取更多信息
|
||||
detail_html = self._get(f'{self.site}/umo/{vid}.html')
|
||||
|
||||
# 标题
|
||||
title = pd.get('vod_data', {}).get('vod_name', '')
|
||||
if not title:
|
||||
m2 = re.search(r'<h1[^>]*class="title"[^>]*>([^<]*)', detail_html)
|
||||
if m2:
|
||||
title = self._clean(m2.group(1))
|
||||
if not title:
|
||||
m2 = re.search(r'<title>([^<]+)', detail_html)
|
||||
if m2:
|
||||
title = self._clean(re.sub(r'\s*[-–—].*$', '', m2.group(1)))
|
||||
|
||||
# 封面
|
||||
pic = ''
|
||||
m2 = re.search(r'data-original="([^"]+)"[^>]*class="[^"]*cover[^"]*"', detail_html)
|
||||
if not m2:
|
||||
m2 = re.search(r'data-original="([^"]+)"[^>]*rel="nofollow"', detail_html)
|
||||
if m2:
|
||||
pic = m2.group(1).strip()
|
||||
|
||||
# 类型、地区、年份、语言、主演、导演、简介
|
||||
def extract_info(pattern, text):
|
||||
m3 = re.search(pattern, text)
|
||||
if m3:
|
||||
return self._clean(m3.group(1))
|
||||
return ''
|
||||
|
||||
vod_class = extract_info(r'类型:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
area = extract_info(r'地区:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
year = extract_info(r'年份:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
lang = extract_info(r'语言:</span>(.*?)(?:</a>|<span)', detail_html)
|
||||
actor = extract_info(r'主演:</span>(.*?)(?:</p>|</div>)', detail_html)
|
||||
if not actor:
|
||||
actor = pd.get('vod_data', {}).get('vod_actor', '')
|
||||
director = extract_info(r'导演:</span>(.*?)(?:</p>|</div>)', detail_html)
|
||||
if not director:
|
||||
director = pd.get('vod_data', {}).get('vod_director', '')
|
||||
if director:
|
||||
director = self._m + '、' + director
|
||||
else:
|
||||
director = self._m
|
||||
|
||||
desc = extract_info(r'detail-sketch">(.*?)</span>', detail_html)
|
||||
|
||||
# 播放列表 - 从详情页提取所有线路和集数
|
||||
play_from = []
|
||||
play_url_list = []
|
||||
|
||||
# 提取线路名(在 playlist data-toggle="tab" 里)
|
||||
line_names = re.findall(r'playlist\d+" data-toggle="tab"[^>]*rel="nofollow">([^<]+)<', detail_html)
|
||||
# 如果没找到,尝试提取 pannel__head 里的文字
|
||||
if not line_names:
|
||||
line_names = re.findall(r'pannel__head[^>]*>([^<]*)<', detail_html)
|
||||
|
||||
# 提取每个播放面板的链接
|
||||
link_groups = re.findall(r'tab-pane fade[^>]*>(.*?)</ul>', detail_html, re.DOTALL)
|
||||
|
||||
for i, group_html in enumerate(link_groups):
|
||||
line_name = line_names[i] if i < len(line_names) else f'线路{i+1}'
|
||||
line_name = self._clean(line_name)
|
||||
episodes = []
|
||||
for em in re.finditer(r'href="(/aey/\d+/(\d+-\d+)\.html)"[^>]*>([^<]*)<', group_html):
|
||||
ep_href = em.group(1)
|
||||
ep_label = em.group(3).strip()
|
||||
if ep_label and ep_href:
|
||||
episodes.append(f'{ep_label}${ep_href}')
|
||||
if episodes:
|
||||
play_from.append(line_name)
|
||||
play_url_list.append('#'.join(episodes))
|
||||
|
||||
# 把华为云排到第一个(1080p)
|
||||
for i, name in enumerate(play_from):
|
||||
if '华为' in name and i > 0:
|
||||
play_from.insert(0, play_from.pop(i))
|
||||
play_url_list.insert(0, play_url_list.pop(i))
|
||||
break
|
||||
|
||||
# 备用:如果详情页没有找到播放列表,直接用播放页的 URL
|
||||
if not play_from and pd:
|
||||
url = pd.get('url', '')
|
||||
from_flag = pd.get('from', '')
|
||||
if url:
|
||||
play_from.append(from_flag or '线路①')
|
||||
play_url_list.append(f'播放${url}')
|
||||
|
||||
vod = {
|
||||
'vod_id': vid,
|
||||
'vod_name': title,
|
||||
'vod_pic': pic,
|
||||
'type_name': vod_class,
|
||||
'vod_year': year,
|
||||
'vod_area': area,
|
||||
'vod_lang': lang,
|
||||
'vod_remarks': '',
|
||||
'vod_actor': actor,
|
||||
'vod_director': director,
|
||||
'vod_content': desc,
|
||||
'vod_play_from': '$$$'.join(play_from),
|
||||
'vod_play_url': '$$$'.join(play_url_list)
|
||||
}
|
||||
result['list'].append(vod)
|
||||
return result
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
result = {}
|
||||
try:
|
||||
# id 格式: /aey/119317/1-1.html
|
||||
play_url = id
|
||||
if not play_url.startswith('http'):
|
||||
play_url = self.site + play_url
|
||||
|
||||
html = self._get(play_url)
|
||||
m = re.search(r'var player_aaaa=(\{[^<]+\})', html)
|
||||
if m:
|
||||
pd = json.loads(m.group(1))
|
||||
url = pd.get('url', '')
|
||||
if url:
|
||||
result['parse'] = 0
|
||||
result['url'] = url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
return result
|
||||
|
||||
# 备用:嗅探
|
||||
result['parse'] = 1
|
||||
result['url'] = play_url
|
||||
result['jx'] = 0
|
||||
result['header'] = {
|
||||
'User-Agent': self.ua,
|
||||
'Referer': self.site + '/'
|
||||
}
|
||||
except Exception as e:
|
||||
print(f'playerContent error: {e}')
|
||||
|
||||
if not result:
|
||||
result = {'parse': 1, 'url': '', 'jx': 0, 'header': {}}
|
||||
return result
|
||||
|
||||
def searchContent(self, key, quick, pg='1'):
|
||||
result = {'list': [], 'parse': 0, 'jx': 0}
|
||||
wd = requests.utils.quote(key)
|
||||
url = f'{self.site}/search/-------------.html?wd={wd}'
|
||||
html = self._get(url)
|
||||
if html:
|
||||
result['list'] = self._extract_list(html)
|
||||
return result
|
||||
|
||||
def localProxy(self, params):
|
||||
return [200, "video/MP2T", {}, ""]
|
||||
@@ -82,6 +82,30 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/麻花影视.py"
|
||||
},
|
||||
{
|
||||
"key": "guanfeng",
|
||||
"name": "🐬观风影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/观风影视.py"
|
||||
},
|
||||
{
|
||||
"key": "SZ",
|
||||
"name": "🐬山楂影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/山楂影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qmvm",
|
||||
"name": "🐬七猫影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七猫影视.py"
|
||||
},
|
||||
{
|
||||
"key": "nmvm",
|
||||
"name": "🐬农民影视.py",
|
||||
|
||||
+25
-1
@@ -19,7 +19,7 @@
|
||||
},
|
||||
{
|
||||
"key": "fY",
|
||||
"name": "🐬枫叶影院(关梯子使用)",
|
||||
"name": "🐬枫叶影院.py(关梯子使用)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/枫叶影院.py"
|
||||
},
|
||||
@@ -41,12 +41,36 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/麻花影视.py"
|
||||
},
|
||||
{
|
||||
"key": "guanfeng",
|
||||
"name": "🐬观风影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/观风影视.py"
|
||||
},
|
||||
{
|
||||
"key": "SZ",
|
||||
"name": "🐬山楂影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/山楂影视.py"
|
||||
},
|
||||
{
|
||||
"key": "nmvm",
|
||||
"name": "🐬农民影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/农民影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qmvm",
|
||||
"name": "🐬七猫影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七猫影视.py"
|
||||
},
|
||||
{
|
||||
"key": "xc",
|
||||
"name": "🐬星辰影院.py(关梯)",
|
||||
|
||||
@@ -82,6 +82,30 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/麻花影视.py"
|
||||
},
|
||||
{
|
||||
"key": "guanfeng",
|
||||
"name": "🐬观风影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/观风影视.py"
|
||||
},
|
||||
{
|
||||
"key": "SZ",
|
||||
"name": "🐬山楂影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/山楂影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qmvm",
|
||||
"name": "🐬七猫影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七猫影视.py"
|
||||
},
|
||||
{
|
||||
"key": "nmvm",
|
||||
"name": "🐬农民影视.py",
|
||||
|
||||
+25
-1
@@ -113,6 +113,30 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/麻花影视.py"
|
||||
},
|
||||
{
|
||||
"key": "guanfeng",
|
||||
"name": "🐬观风影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/观风影视.py"
|
||||
},
|
||||
{
|
||||
"key": "SZ",
|
||||
"name": "🐬山楂影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/山楂影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qmvm",
|
||||
"name": "🐬七猫影视.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七猫影视.py"
|
||||
},
|
||||
{
|
||||
"key": "nmvm",
|
||||
"name": "🐬农民影视.py[追剧]",
|
||||
@@ -562,7 +586,7 @@
|
||||
},
|
||||
{
|
||||
"key": "Pandalive",
|
||||
"name": "🐬Pandalive直播.py|🔞[成人直播]",
|
||||
"name": "🐬韩国Pandalive直播.py|🔞[成人直播]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Pandalive直播.py"
|
||||
},
|
||||
|
||||
+25
-1
@@ -65,6 +65,30 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/麻花影视.py"
|
||||
},
|
||||
{
|
||||
"key": "guanfeng",
|
||||
"name": "🐬观风影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/观风影视.py"
|
||||
},
|
||||
{
|
||||
"key": "SZ",
|
||||
"name": "🐬山楂影视.py(关梯)[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/山楂影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qmvm",
|
||||
"name": "🐬七猫影视.py[追剧]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七猫影视.py"
|
||||
},
|
||||
{
|
||||
"key": "nmvm",
|
||||
"name": "🐬农民影视.py[追剧]",
|
||||
@@ -163,7 +187,7 @@
|
||||
},
|
||||
{
|
||||
"key": "Pandalive",
|
||||
"name": "🐬Pandalive直播.py|🔞[成人直播]",
|
||||
"name": "🐬韩国Pandalive直播.py|🔞[成人直播]",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/Pandalive直播.py"
|
||||
},
|
||||
|
||||
@@ -106,6 +106,30 @@
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/瓜子APP.py"
|
||||
},
|
||||
{
|
||||
"key": "MH",
|
||||
"name": "🐬麻花影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/麻花影视.py"
|
||||
},
|
||||
{
|
||||
"key": "guanfeng",
|
||||
"name": "🐬观风影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/观风影视.py"
|
||||
},
|
||||
{
|
||||
"key": "SZ",
|
||||
"name": "🐬山楂影视.py(关梯)",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/山楂影视.py"
|
||||
},
|
||||
{
|
||||
"key": "qmvm",
|
||||
"name": "🐬七猫影视.py",
|
||||
"type": 3,
|
||||
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/GHK/refs/heads/main/py/七猫影视.py"
|
||||
},
|
||||
{
|
||||
"key": "nmvm",
|
||||
"name": "🐬农民影视.py",
|
||||
|
||||
Reference in New Issue
Block a user