Files
2026-07-29 19:20:34 +00:00

429 lines
16 KiB
Python

# -*- coding: utf-8 -*-
import sys
import re
import json
import base64
import threading
import requests
import urllib3
import os
import time
import random
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import unquote, quote, urljoin, urlparse
urllib3.disable_warnings()
sys.path.append('..')
from base.spider import Spider as BaseSpider
# ==================== 本地代理(解决图片跨域/防盗链) ====================
_proxy_port = 0
_proxy_started = False
_proxy_session = requests.Session()
_proxy_session.verify = False
_proxy_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://maomi66.cc/',
}
class _ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
class _ProxyHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
real_url = unquote(self.path[1:])
if not real_url or not real_url.startswith('http'):
self.send_response(404); self.end_headers(); return
r = _proxy_session.get(real_url, headers=_proxy_headers, timeout=20, verify=False)
ct = r.headers.get('Content-Type', 'image/jpeg')
self.send_response(200)
self.send_header('Content-Type', ct)
self.send_header('Content-Length', len(r.content))
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(r.content)
except BrokenPipeError:
pass
except Exception:
self.send_response(404); self.end_headers()
def log_message(self, format, *args):
pass
def _find_free_port():
import socket
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.bind(('127.0.0.1', 0))
port = sk.getsockname()[1]
sk.close()
return port
def _start_proxy():
global _proxy_port, _proxy_started
if _proxy_started:
return
_proxy_port = _find_free_port()
server = _ThreadedHTTPServer(('127.0.0.1', _proxy_port), _ProxyHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
_proxy_started = True
# ==================== Spider 主体 ====================
class Spider(BaseSpider):
session = requests.Session()
host = 'https://maomi66.cc'
play_host = 'https://m.892539.xyz'
def __init__(self):
super().__init__()
self._categories_cache = None
self._debug = True
def _log(self, msg):
if self._debug:
print(f'[maomi66] {msg}')
def getName(self):
return 'maomi66'
def isVideoFormat(self, url):
if not url:
return False
return '.m3u8' in url or '.mp4' in url or '.ts' in url
def manualVideoCheck(self):
return False
def destroy(self):
pass
def localProxy(self, param):
return [404, 'text/plain', '']
def init(self, extend=''):
self.session.verify = False
self.session.headers.update(self._get_headers())
_start_proxy()
text = self._fetch(self.host)
if text:
self._load_categories(text)
def _get_headers(self, referer=None):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/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',
'Referer': referer or self.host + '/',
}
return headers
def _proxy_url(self, url):
if not url:
return ''
if url.startswith('http://127.0.0.1'):
return url
return f'http://127.0.0.1:{_proxy_port}/{quote(url, safe="")}'
def _fetch(self, url, referer=None, retries=3):
for i in range(retries):
try:
if referer is None:
referer = self.host + '/'
headers = self._get_headers(referer)
if i > 0:
time.sleep(random.uniform(0.5, 1.5))
r = self.session.get(url, headers=headers, timeout=30, verify=False)
r.encoding = 'utf-8'
if r.status_code == 200:
text = r.text
if 'location.href' in text and ('请稍后' in text or '数据处理中' in text):
jump_match = re.search(r'location\.href\s*=\s*[\'"]([^\'"]+)[\'"]', text)
if jump_match:
jump_path = jump_match.group(1)
jump_url = urljoin(url, jump_path) if not jump_path.startswith('http') else jump_path
self._log(f'遇到跳转页,跟随至: {jump_url}')
r2 = self.session.get(jump_url, headers=headers, timeout=30, verify=False)
r2.encoding = 'utf-8'
if r2.status_code == 200:
return r2.text
return text
elif r.status_code in [403, 429, 503]:
self._log(f'请求被拦截 [{r.status_code}],重试 {i+1}/{retries}')
else:
return ''
except Exception as e:
self._log(f'请求异常 [{e}],重试 {i+1}/{retries}')
return ''
# ==================== 分类加载 ====================
def _load_categories(self, text):
if not text:
return []
cats = []
seen = set()
tid_blacklist = {
'69799699', '69809699', '69819699', '69829699',
'69839699', '69849699', '69859699', '69869699'
}
pattern = r'<a href="/list/(\d+)-1\.html"(?:[^>]*?)>([^<]+)</a>'
for tid, name in re.findall(pattern, text):
name = name.strip()
if not name or name in seen:
continue
if tid in tid_blacklist:
self._log(f'过滤分类: [{tid}] {name}')
continue
seen.add(name)
cats.append({'type_id': tid, 'type_name': name})
self._categories_cache = cats
self._log(f'加载分类: {len(cats)} 个')
return cats
def _get_category_name(self, tid):
for cat in self._categories_cache or []:
if cat['type_id'] == tid:
return cat['type_name']
return tid
# ==================== 列表解析 ====================
def _parse_list(self, html):
items = []
li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', html, re.S)
for li in li_blocks:
a_match = re.search(r'<a class="xx-thumb" href="(/(?:video|torrent)/([^"]+)\.html)"', li)
if not a_match:
continue
href = a_match.group(1)
vid = a_match.group(2)
if href.startswith('/torrent/'):
continue
img_match = re.search(r'<img[^>]+data-original="([^"]+)"', li)
pic = img_match.group(1) if img_match else ''
title = vid
h5_match = re.search(r'<h5><a[^>]*>(.*?)</a></h5>', li, re.S)
if h5_match:
title = re.sub(r'<[^>]+>', '', h5_match.group(1)).strip()
items.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': self._proxy_url(pic),
'vod_remarks': '',
})
return items
def _get_list(self, tid, page):
url = f'{self.host}/list/{tid}-{page}.html'
html = self._fetch(url, referer=f'{self.host}/list/{tid}-1.html')
if not html:
return []
return self._parse_list(html)
# ==================== 首页 ====================
def homeContent(self, filter):
try:
text = self._fetch(self.host)
if text:
self._load_categories(text)
cats = self._categories_cache or []
return {
'class': cats,
'filters': {},
'type': '影视',
'list': [],
'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0
}
except Exception as e:
self._log(f'homeContent 异常: {e}')
return {'class': [], 'filters': {}, 'type': '影视', 'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0}
def homeVideoContent(self):
return {'list': []}
# ==================== 分类内容 ====================
def categoryContent(self, tid, pg, filter, extend):
try:
page = int(pg) if pg else 1
items = self._get_list(tid, page)
total_page = page + 1
if page == 1:
html = self._fetch(f'{self.host}/list/{tid}-1.html')
if html:
pages = re.findall(r'/list/\d+-(\d+)\.html', html)
if pages:
total_page = max(int(p) for p in pages)
return {
'list': items, 'page': page, 'pagecount': total_page,
'limit': len(items), 'total': total_page * len(items)
}
except Exception as e:
self._log(f'categoryContent 异常: {e}')
return {'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0}
# ==================== 详情页 ====================
def _fetch_detail(self, vid):
url = f'{self.host}/video/{vid}.html'
self._log(f'获取详情: {url}')
html = self._fetch(url, referer=self.host)
if html:
detail = self._parse_detail(html, vid, url)
if detail and detail.get('vod_play_url'):
return detail
return None
def _parse_detail(self, html, vid, base_url):
title = ''
m = re.search(r'<h1[^>]*>(.*?)</h1>', html, re.S)
if m:
title = re.sub(r'<[^>]+>', '', m.group(1)).strip()
if not title:
m = re.search(r'<title>([^<]+)</title>', html)
if m:
title = m.group(1).strip()
cover = ''
m = re.search(r'<meta[^>]*property="og:image"[^>]*content="([^"]+)"', html)
if m:
cover = m.group(1)
if not cover:
m = re.search(r'<img[^>]+class="[^"]*cover[^"]*"[^>]+src="([^"]+)"', html, re.S)
if m:
cover = m.group(1)
if not cover:
m = re.search(r'data-original="([^"]+)"', html)
if m:
cover = m.group(1)
play_urls = []
seen = set()
def add(label, url):
if url in seen:
return
seen.add(url)
play_urls.append(f'{label}${url}')
site_id = ''
source_id = ''
comment_match = re.search(r'<!--\s*source_id:(\d+),\s*site_id:(\d+)', html)
if comment_match:
source_id = comment_match.group(1)
site_id = comment_match.group(2)
self._log(f'从注释提取: site_id={site_id}, source_id={source_id}')
if not site_id or not source_id:
hls_match = re.search(r'hls\.loadSource\([\'"]([^\'"]+)[\'"]\)', html)
if hls_match:
play_url = hls_match.group(1)
sid_match = re.search(r'site_id=(\d+)', play_url)
src_match = re.search(r'source_id=(\d+)', play_url)
if sid_match and src_match:
site_id = sid_match.group(1)
source_id = src_match.group(1)
self._log(f'从HLS脚本提取: site_id={site_id}, source_id={source_id}')
if not site_id:
m_sid = re.search(r'site_id[=:]\s*(\d+)', html)
if m_sid:
site_id = m_sid.group(1)
if not source_id:
m_src = re.search(r'source_id[=:]\s*(\d+)', html)
if m_src:
source_id = m_src.group(1)
if site_id and source_id:
play_url = f'{self.play_host}/play.php?site_id={site_id}&source_id={source_id}'
add('HLS直链', play_url)
for media in set(re.findall(r'https?://[^\s"\'<>]+\.(?:m3u8|mp4|flv|mkv|ts)(?:\?[^\s"\'<>]*)?', html)):
add('媒体直链', media)
for src in set(re.findall(r'<iframe[^>]+(?:src|data-src)=["\']([^"\']+)["\']', html)):
if any(k in src for k in ['play.php', 'm3u8', 'mp4', 'embed', 'player']):
add('外链', src if src.startswith('http') else urljoin(base_url, src))
scripts = re.findall(r'<script[^>]*>(.*?)</script>', html, re.S)
for script in scripts:
for b64 in re.findall(r'["\']([A-Za-z0-9+/]{20,}={0,2})["\']', script):
try:
dec = base64.b64decode(b64).decode('utf-8')
if dec.startswith('http') and any(x in dec for x in ['.m3u8', '.mp4', 'play.php']):
add('Base64', dec)
except:
pass
if not play_urls:
add('默认线路', base_url)
sources = []
urls = []
for pu in play_urls:
sn, url = pu.split('$', 1)
sources.append(sn)
urls.append(f'{sn}${url}')
return {
'vod_id': vid,
'vod_name': title or vid,
'vod_pic': self._proxy_url(cover) if cover else '',
'vod_play_from': '$$$'.join(sources) if sources else '默认',
'vod_play_url': '#'.join(urls) if urls else f'默认${base_url}',
'vod_content': title or '',
}
def detailContent(self, ids):
try:
vid = str(ids[0] if isinstance(ids, list) else ids)
detail = self._fetch_detail(vid)
if not detail:
return {'list': []}
return {'list': [detail]}
except Exception as e:
self._log(f'detailContent 异常: {e}')
return {'list': []}
# ==================== 播放器 ====================
def playerContent(self, flag, id, vipFlags=None):
try:
if id and not id.startswith('http'):
detail = self._fetch_detail(id)
if detail and detail.get('vod_play_url'):
first = detail['vod_play_url'].split('#')[0]
if '$' in first:
id = first.split('$', 1)[1]
else:
id = first
referer = self.host
if id and id.startswith('http'):
parsed = urlparse(id)
if parsed.netloc:
referer = f'{parsed.scheme}://{parsed.netloc}/'
return {
'parse': 0,
'url': id,
'header': {
'Referer': referer,
'User-Agent': 'Mozilla/5.0',
}
}
except Exception as e:
self._log(f'playerContent 异常: {e}')
return {'parse': 0, 'url': '', 'header': {}}
# ==================== 搜索 ====================
def searchContent(self, key, quick, pg='1'):
try:
page = int(pg) if pg else 1
url = f'{self.host}/search.php?content={quote(key)}&type=1&page={page}'
html = self._fetch(url, referer=self.host)
items = self._parse_list(html) if html else []
return {
'list': items, 'page': page, 'pagecount': page + 1,
'limit': len(items), 'total': page * len(items)
}
except Exception as e:
self._log(f'searchContent 异常: {e}')
return {'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0}