diff --git a/FGBLH/Web鱼壳海豚.json b/FGBLH/Web鱼壳海豚.json index fb443815..e95564cb 100644 --- a/FGBLH/Web鱼壳海豚.json +++ b/FGBLH/Web鱼壳海豚.json @@ -820,6 +820,24 @@ "type": 3, "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/catemby.py" }, + { + "key": "大屌视频", + "name": "🐬大屌视频.py|🔞[成人]", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/大屌视频.py" + }, + { + "key": "高端视频", + "name": "🐬高端视频.py|🔞[成人]", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/高端视频.py" + }, + { + "key": "猫咪AV", + "name": "🐬猫咪AV.py|🔞[成人]", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪AV.py" + }, { "key": "妈妈自拍", "name": "🐬妈妈自拍.py|🔞[成人]", diff --git a/FGBLH/Web鱼壳海豚py.json b/FGBLH/Web鱼壳海豚py.json index f4cea297..8e0be3a8 100644 --- a/FGBLH/Web鱼壳海豚py.json +++ b/FGBLH/Web鱼壳海豚py.json @@ -563,6 +563,24 @@ "type": 3, "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/catemby.py" }, + { + "key": "大屌视频", + "name": "🐬大屌视频.py|🔞[成人]", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/大屌视频.py" + }, + { + "key": "高端视频", + "name": "🐬高端视频.py|🔞[成人]", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/高端视频.py" + }, + { + "key": "猫咪AV", + "name": "🐬猫咪AV.py|🔞[成人]", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪AV.py" + }, { "key": "妈妈自拍", "name": "🐬妈妈自拍.py|🔞[成人]", diff --git a/FGBLH/ok海豚.json b/FGBLH/ok海豚.json index 365e3738..d7af8ec5 100644 --- a/FGBLH/ok海豚.json +++ b/FGBLH/ok海豚.json @@ -748,6 +748,24 @@ "type": 3, "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py" }, + { + "key": "大屌视频", + "name": "🐬大屌视频.py|🔞", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/大屌视频.py" + }, + { + "key": "高端视频", + "name": "🐬高端视频.py|🔞", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/高端视频.py" + }, + { + "key": "猫咪AV", + "name": "🐬猫咪AV.py|🔞", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪AV.py" + }, { "key": "妈妈自拍", "name": "🐬妈妈自拍.py|🔞", diff --git a/FGBLH/ok海豚py.json b/FGBLH/ok海豚py.json index adfa76c8..6964deba 100644 --- a/FGBLH/ok海豚py.json +++ b/FGBLH/ok海豚py.json @@ -580,6 +580,24 @@ "type": 3, "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py" }, + { + "key": "大屌视频", + "name": "🐬大屌视频.py|🔞", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/大屌视频.py" + }, + { + "key": "高端视频", + "name": "🐬高端视频.py|🔞", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/高端视频.py" + }, + { + "key": "猫咪AV", + "name": "🐬猫咪AV.py|🔞", + "type": 3, + "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪AV.py" + }, { "key": "妈妈自拍", "name": "🐬妈妈自拍.py|🔞", diff --git a/FGBLH/py/大屌视频.py b/FGBLH/py/大屌视频.py new file mode 100644 index 00000000..73916fdf --- /dev/null +++ b/FGBLH/py/大屌视频.py @@ -0,0 +1,675 @@ +# -*- 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 + +# ========================== 全局本地代理服务器 ========================== +# 用于解决图片防盗链、跨域问题,所有外域图片均通过 127.0.0.1 代理访问 +_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 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Referer': 'https://dadiao.cc/', +} + +class _ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + daemon_threads = True + allow_reuse_address = 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 + parsed = urlparse(real_url) + referer = f'{parsed.scheme}://{parsed.netloc}/' if parsed.netloc else 'https://dadiao.cc/' + headers = dict(_proxy_headers) + headers['Referer'] = referer + r = _proxy_session.get(real_url, headers=headers, timeout=20, verify=False, stream=True) + content_type = r.headers.get('Content-Type', 'image/jpeg') + content_length = r.headers.get('Content-Length') + self.send_response(200) + self.send_header('Content-Type', content_type) + if content_length: + self.send_header('Content-Length', content_length) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.end_headers() + for chunk in r.iter_content(chunk_size=8192): + if chunk: + self.wfile.write(chunk) + except BrokenPipeError: + pass + except Exception as e: + try: + self.send_response(404) + self.end_headers() + except: + pass + + 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://dadiao.cc' + play_host = 'https://m.892539.xyz' + + def __init__(self): + super().__init__() + self._categories_cache = None + self._zone_map = {} + self._sub_cat_names = {} + self._debug = True + self._log('Spider 初始化完成') + + def _log(self, msg): + if self._debug: + print(f'[dadiao] {msg}') + + def getName(self): + return 'dadiao' + + def isVideoFormat(self, url): + if not url: + return False + return '.m3u8' in url or '.mp4' in url or '.ts' in url or url.startswith('magnet:') + + 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 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;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 + if url.startswith('//'): + url = 'https:' + url + elif url.startswith('/'): + url = urljoin(self.host, 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: + return r.text + elif r.status_code in [403, 429, 503]: + self._log(f'请求被拦截 [{r.status_code}],重试 {i+1}/{retries}') + else: + self._log(f'请求返回 [{r.status_code}],终止') + return '' + except Exception as e: + self._log(f'请求异常 [{e}],重试 {i+1}/{retries}') + return '' + + # ========================== 分类加载(蜜桃式:区域为class,子分类为filters) ========================== + def _load_categories(self, text): + if not text: + self._log('首页HTML为空,无法加载分类') + return [] + self._zone_map = {} + self._sub_cat_names = {} + + # 按 zone-tag 与 cate-items 顺序配对提取 + zone_names = re.findall(r'
\s*(.*?)\s*
', text) + items_blocks = re.findall(r'
(.*?)
', text, re.S) + + if len(zone_names) == len(items_blocks) and len(zone_names) > 0: + self._log(f'区域配对成功: {len(zone_names)} 个区域') + for i, zone_name in enumerate(zone_names): + zone_name = zone_name.strip() + items_html = items_blocks[i] + tids = [] + for tid, name in re.findall(r']*>([^<]+)', items_html): + name = name.strip() + if not name: + continue + tids.append(tid) + self._sub_cat_names[tid] = name + if tids: + self._zone_map[zone_name] = tids + self._log(f'区域[{zone_name}]: {len(tids)} 个子分类') + else: + self._log('区域配对失败,启用兜底提取') + seen_tid = set() + for tid, name in re.findall(r']*>([^<]+)', text): + name = name.strip() + if not name or tid in seen_tid: + continue + seen_tid.add(tid) + self._sub_cat_names[tid] = name + if self._sub_cat_names: + all_tids = list(self._sub_cat_names.keys()) + self._zone_map['全部视频'] = all_tids + self._log(f'兜底提取: {len(all_tids)} 个分类归入"全部视频"') + + self._categories_cache = list(self._zone_map.keys()) + self._log(f'分类加载完成: 共 {len(self._categories_cache)} 个区域') + return self._categories_cache + + def _get_zone_tids(self, zone_name): + return self._zone_map.get(zone_name, []) + + def _get_sub_name(self, tid): + return self._sub_cat_names.get(tid, tid) + + # ========================== 列表解析(修复封面图片) ========================== + def _parse_list(self, html): + items = [] + li_blocks = re.findall( + r'
  • \s*]*>.*?
    .*?
    \s*
  • ', + html, re.S + ) + for href, vid in li_blocks: + li_match = re.search( + r'
  • \s*]*>.*?\s*
    .*?
    \s*
  • ', + html, re.S + ) + if not li_match: + continue + li_html = li_match.group(0) + + title = vid + h5_match = re.search(r'
    ]*>(.*?)
    ', li_html, re.S) + if h5_match: + title = re.sub(r'<[^>]+>', '', h5_match.group(1)).strip() + + pic = '' + pic_match = re.search(r']+data-original="([^"]+)"', li_html, re.S) + if pic_match: + pic = pic_match.group(1).strip() + else: + pic_match = re.search(r']+src="([^"]+)"', li_html, re.S) + if pic_match: + pic = pic_match.group(1).strip() + + if pic and not pic.endswith('loading.svg') and not pic.endswith('loading.gif'): + if pic.startswith('//'): + pic = 'https:' + pic + elif pic.startswith('/'): + pic = urljoin(self.host, pic) + else: + pic = '' + + is_torrent = href.startswith('/torrent/') + items.append({ + 'vod_id': f'torrent_{vid}' if is_torrent else vid, + 'vod_name': title, + 'vod_pic': self._proxy_url(pic), + 'vod_remarks': '磁力' if is_torrent else '', + }) + 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) + + # ========================== 首页(蜜桃式:区域为class + 子分类filters) ========================== + def homeContent(self, filter): + try: + text = self._fetch(self.host) + if text: + self._load_categories(text) + + classes = [] + filters = {} + + # 遍历所有区域,构建 class 和 filter + for zone_name, tids in self._zone_map.items(): + # 区域作为一级分类 + zone_tid = 'zone:' + ','.join(tids) + classes.append({'type_id': zone_tid, 'type_name': zone_name}) + + # 该区域的子分类作为顶部筛选器 + sub_values = [] + for tid in tids: + sub_name = self._sub_cat_names.get(tid, tid) + sub_values.append({'n': sub_name, 'v': tid}) + if sub_values: + filters[zone_tid] = [ + { + 'key': 'sub', + 'name': '子分类', + 'value': sub_values + } + ] + else: + filters[zone_tid] = [] + + return { + 'class': classes, + 'filters': 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: + # 兼容 TVBox 不同版本:extend 可能是 JSON 字符串 + if isinstance(extend, str): + try: + extend = json.loads(extend) + except: + extend = {} + + # 路由1:区域分类(tid 以 zone: 开头) + if tid and tid.startswith('zone:'): + tids = tid.replace('zone:', '').split(',') + if extend and isinstance(extend, dict) and 'sub' in extend: + target_tid = extend['sub'] + self._log(f'区域[{tid}] 选择子分类: {target_tid}') + return self._do_category(target_tid, pg) + else: + # 默认显示该区域下第一个子分类 + if tids: + self._log(f'区域[{tid}] 默认显示首个子分类: {tids[0]}') + return self._do_category(tids[0], pg) + return {'list': [], 'page': 1, 'pagecount': 1, 'limit': 0, 'total': 0} + + # 路由2:直接传入子分类ID(数字ID) + else: + self._log(f'直接加载子分类: {tid}') + return self._do_category(tid, pg) + + except Exception as e: + self._log(f'categoryContent 异常: {e}') + return { + 'list': [], 'page': 1, 'pagecount': 1, + 'limit': 0, 'total': 0 + } + + def _do_category(self, tid, pg): + 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) + } + + # ========================== 详情页 ========================== + def _fetch_detail(self, vid): + if vid.startswith('torrent_'): + real_id = vid.replace('torrent_', '') + url = f'{self.host}/torrent/{real_id}.html' + self._log(f'获取磁力详情: {url}') + html = self._fetch(url, referer=self.host) + if html: + return self._parse_detail(html, vid, url, is_torrent=True) + return None + + 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, is_torrent=False): + title = '' + m = re.search(r']*>(.*?)', html, re.S) + if m: + title = re.sub(r'<[^>]+>', '', m.group(1)).strip() + if not title: + m = re.search(r'([^<]+)', html) + if m: + title = m.group(1).strip() + + cover = '' + m = re.search(r']*property="og:image"[^>]*content="([^"]+)"', html) + if m: + cover = m.group(1).strip() + if not cover: + m = re.search(r']+class="[^"]*cover[^"]*"[^>]+src="([^"]+)"', html, re.S) + if m: + cover = m.group(1).strip() + if not cover: + m = re.search(r']+data-original="([^"]+)"', html) + if m: + cover = m.group(1).strip() + if not cover: + m = re.search(r']+poster="([^"]+)"', html) + if m: + cover = m.group(1).strip() + + if cover: + if cover.startswith('//'): + cover = 'https:' + cover + elif cover.startswith('/'): + cover = urljoin(self.host, cover) + + play_urls = [] + seen = set() + + def add(label, url): + if not url or url in seen: + return + seen.add(url) + play_urls.append(f'{label}${url}') + + if not is_torrent: + # ==================== 视频解析逻辑 ==================== + site_id = '' + source_id = '' + + # 1. 优先从页面底部 HTML 注释提取 + comment_match = re.search(r'