diff --git a/xbpq/撸一撸.py b/xbpq/撸一撸.py new file mode 100644 index 0000000..3a465af --- /dev/null +++ b/xbpq/撸一撸.py @@ -0,0 +1,590 @@ +# coding=utf-8 +import sys +import json +import re +import requests +import base64 +from bs4 import BeautifulSoup +from urllib.parse import unquote, urljoin + +try: + from base.spider import Spider as BaseSpider +except ImportError: + class BaseSpider(): + def fetch(self, url, headers=None, timeout=10): + try: + res = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) + res.encoding = 'utf-8' + return res + except Exception as e: + print(f"fetch error: {e}") + return None + +class Spider(BaseSpider): + def getName(self): + return "撸一天" + + def init(self, extend=""): + self.host = "https://luyitian.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', + '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' + }) + + def homeVideoContent(self): + return {"list": []} + + def localProxy(self, params): + return [200, "video/MP2T", ""] + + def isVideoFormat(self, url): + return False + + def manualVideoCheck(self): + return False + + def fetch(self, url, headers=None, timeout=5): + try: + req_headers = headers or self.session.headers + res = self.session.get(url, headers=req_headers, timeout=timeout, allow_redirects=True) + res.encoding = 'utf-8' + return res + except Exception as e: + print(f"fetch error: {e}") + return None + + def _get_topic_filters(self): + url = f"{self.host}/topic/" + res = self.fetch(url, timeout=5) + if not res: + return [] + soup = BeautifulSoup(res.text, 'html.parser') + topic_links = soup.select('a[href*="/topicdetail-"]') + if not topic_links: + topic_links = soup.select('a[href*="/topicdetail"]') + filters = [] + seen = set() + for a in topic_links: + href = a.get('href', '') + match = re.search(r'/topicdetail-(\d+)', href) + if not match: + match = re.search(r'/topicdetail/(\d+)', href) + if not match: + continue + tid = match.group(1) + name = a.get_text(strip=True) or a.get('title', '') or f"专题{tid}" + if len(name) < 2: + continue + if tid not in seen: + seen.add(tid) + filters.append({"n": name, "v": tid}) + return filters + + def homeContent(self, filter): + classes = [ + {"type_name": "最近更新", "type_id": "new"}, + {"type_name": "热门影片", "type_id": "hot"}, + {"type_name": "影片专题", "type_id": "topic"}, + {"type_name": "中文字幕", "type_id": "28"}, + {"type_name": "国产", "type_id": "20"}, + {"type_name": "日本有码", "type_id": "21"}, + {"type_name": "日本无码", "type_id": "22"}, + {"type_name": "欧美", "type_id": "23"}, + {"type_name": "动漫", "type_id": "24"}, + {"type_name": "伦理", "type_id": "25"}, + {"type_name": "韩国", "type_id": "36"}, + {"type_name": "另类", "type_id": "41"} + ] + + filters = { + "28": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "28"}, + {"n": "日本中字", "v": "51"} + ]}], + "20": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "20"}, + {"n": "国产精品", "v": "26"}, + {"n": "国产剧情", "v": "27"}, + {"n": "国产自拍", "v": "29"}, + {"n": "国产主播", "v": "35"}, + {"n": "国模私拍", "v": "85"}, + {"n": "网红明星", "v": "91"}, + {"n": "国产SM", "v": "105"}, + {"n": "台湾辣妹", "v": "107"}, + {"n": "香港正妹", "v": "108"} + ]}], + "21": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "21"}, + {"n": "人妻", "v": "31"}, + {"n": "素人", "v": "44"}, + {"n": "口爆颜射", "v": "46"}, + {"n": "萝莉少女", "v": "47"}, + {"n": "美乳巨乳", "v": "48"}, + {"n": "制服诱惑", "v": "52"}, + {"n": "调教", "v": "57"}, + {"n": "出轨", "v": "58"}, + {"n": "有码精品", "v": "101"} + ]}], + "22": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "22"}, + {"n": "无码精品", "v": "102"} + ]}], + "23": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "23"}, + {"n": "欧美精品", "v": "104"} + ]}], + "24": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "24"}, + {"n": "动漫精品", "v": "103"} + ]}], + "25": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "25"}, + {"n": "综合三级", "v": "39"} + ]}], + "36": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "36"}, + {"n": "韩国主播", "v": "37"} + ]}], + "41": [{"key": "tid", "name": "子分类", "value": [ + {"n": "全部", "v": "41"}, + {"n": "Cosplay", "v": "106"} + ]}] + } + + topic_values = self._get_topic_filters() + if topic_values: + filters["topic"] = [{ + "key": "tid", + "name": "专题", + "value": topic_values + }] + + return {'class': classes, 'filters': filters} + + def categoryContent(self, tid, pg, filter, extend): + pg = int(pg) + result = {"list": [], "page": pg, "pagecount": 999, "limit": 20, "total": 9999} + + real_tid = extend.get('tid', tid) + soup = None + + if real_tid.isdigit() and tid == "topic": + urls_to_try = [ + f"{self.host}/topicdetail-{real_tid}/", + f"{self.host}/topicdetail-{real_tid}.html", + f"{self.host}/topicdetail/{real_tid}/", + f"{self.host}/topicdetail/{real_tid}.html", + f"{self.host}/topicdetail-{real_tid}-{pg}/", + f"{self.host}/topicdetail/{real_tid}-{pg}/" + ] + for url in urls_to_try: + res = self.fetch(url, headers={'Referer': self.host}) + if res and res.status_code == 200 and ('video-img-box' in res.text or 'vodlist' in res.text or 'vodplay' in res.text): + soup = BeautifulSoup(res.text, 'html.parser') + break + if not soup: + return result + + elif real_tid in ["new", "hot", "topic"]: + if real_tid == "topic": + return result + if pg > 1: + url = f"{self.host}/label/{real_tid}/page/{pg}/" + else: + url = f"{self.host}/label/{real_tid}/" + res = self.fetch(url, headers={'Referer': self.host}) + if not res: + url = f"{self.host}/label/{real_tid}/" + res = self.fetch(url, headers={'Referer': self.host}) + if not res: + return result + soup = BeautifulSoup(res.text, 'html.parser') + + else: + urls_to_try = [ + f"{self.host}/vodtype/{real_tid}-{pg}.html", + f"{self.host}/vodtype/{real_tid}-{pg}/", + f"{self.host}/type/{real_tid}-{pg}.html", + f"{self.host}/type/{real_tid}-{pg}/", + f"{self.host}/vodtype/{real_tid}/", + f"{self.host}/vodtype/{real_tid}.html" + ] + res = None + for url in urls_to_try: + res = self.fetch(url, headers={'Referer': self.host}) + if res and res.status_code == 200: + if 'video-img-box' in res.text or 'vodlist' in res.text or 'item' in res.text: + break + res = None + if not res: + return result + soup = BeautifulSoup(res.text, 'html.parser') + + vod_list = [] + items = soup.select('.video-img-box') or soup.select('.video-film-list .video-item') or soup.select('.vodlist_item') or soup.select('.item') + + for item in items: + a = item.select_one('a') + if not a: + continue + href = a.get('href', '') + vid_match = re.search(r'/vodplay/(\d+)', href) or \ + re.search(r'/voddetail/(\d+)', href) or \ + re.search(r'/vod/(\d+)', href) or \ + re.search(r'/play/(\d+)', href) + vid = vid_match.group(1) if vid_match else href + + name = "" + img = item.select_one('img') + if img and img.get('alt'): + name = img['alt'] + if not name and a.get('title'): + name = a['title'] + if not name: + title_elem = item.select_one('.title a') or item.select_one('.detail .title a') + if title_elem: + name = title_elem.get_text(strip=True) + if not name: + name = a.get_text(strip=True) + if not name: + name = "未知标题" + + pic = "" + if img: + pic = img.get('data-src') or img.get('src', '') + if pic and not pic.startswith('http'): + pic = urljoin(self.host, pic) + + remark = "" + remark_elem = item.select_one('.sub-title') or item.select_one('.remarks') or item.select_one('.video-remarks') + if remark_elem: + remark = remark_elem.get_text(strip=True) + if len(remark) > 20: + remark = remark[:20] + else: + text = item.get_text(strip=True) + parts = [p.strip() for p in text.split('\n') if p.strip()] + if parts: + remark = parts[-1][:20] + + vod_list.append({ + "vod_id": vid, + "vod_name": name.strip(), + "vod_pic": pic, + "vod_remarks": remark + }) + + result['list'] = vod_list + + page_elem = soup.select_one('.pagination a:last-child') or soup.select_one('.page a:last-child') + if page_elem and page_elem.get('href'): + try: + nums = re.findall(r'(\d+)', page_elem['href']) + if nums: + result['pagecount'] = max(int(nums[-1]), 1) + except: + pass + + return result + + def detailContent(self, ids): + vid = ids[0] + url = f"{self.host}/vodplay/{vid}-1-1/" + res = self.fetch(url, headers={'Referer': self.host}) + if not res: + return {"list": []} + + soup = BeautifulSoup(res.text, 'html.parser') + raw_title = soup.title.text.split('|')[0].replace('在线播放在线观看','').replace('《','').replace('》','').strip() + + vod = { + "vod_id": vid, + "vod_name": raw_title, + "vod_type": "视频", + "vod_content": "资源来自于网络", + "vod_play_from": "Luyitian", + "vod_play_url": f"播放${vid}-1-1" + } + return {"list": [vod]} + + def searchContent(self, key, quick, pg=1): + url = f"{self.host}/vodsearch/{key}----------{pg}---/" + res = self.fetch(url, headers={'Referer': self.host}) + if not res: + return {"list": []} + + soup = BeautifulSoup(res.text, 'html.parser') + vod_list = [] + items = soup.select('.video-img-box') or soup.select('.video-film-list .video-item') + + for item in items: + a = item.select_one('a') + if not a: + continue + href = a.get('href', '') + vid_match = re.search(r'/vodplay/(\d+)', href) or re.search(r'/voddetail/(\d+)', href) + vid = vid_match.group(1) if vid_match else href + + name = "" + img = item.select_one('img') + if img and img.get('alt'): + name = img['alt'] + if not name and a.get('title'): + name = a['title'] + if not name: + title_elem = item.select_one('.title a') + if title_elem: + name = title_elem.get_text(strip=True) + if not name: + name = a.get_text(strip=True) + if not name: + name = "搜索结果" + + pic = "" + if img: + pic = img.get('data-src') or img.get('src', '') + + vod_list.append({ + "vod_id": vid, + "vod_name": name.strip(), + "vod_pic": pic, + "vod_remarks": "" + }) + return {"list": vod_list} + + def _js_decode(self, js_str): + b64_match = re.search(r'atob\s*\(\s*["\']([^"\']+)["\']\s*\)', js_str) + if b64_match: + try: + decoded = base64.b64decode(b64_match.group(1)).decode('utf-8') + return decoded + except: + pass + unescape_match = re.search(r'unescape\s*\(\s*["\']([^"\']+)["\']\s*\)', js_str) + if unescape_match: + try: + decoded = unquote(unescape_match.group(1)) + return decoded + except: + pass + url_match = re.search(r'(https?://[^\s"\']+\.m3u8[^\s"\']*)', js_str, re.I) + if url_match: + return url_match.group(1) + return None + + def _sniff_xhr(self, html, page_url): + patterns = [ + r'fetch\s*\(\s*["\']([^"\']+\.m3u8[^"\']*)["\']', + r'XMLHttpRequest.*?\.open\s*\(\s*["\']GET["\']\s*,\s*["\']([^"\']+\.m3u8[^"\']*)["\']', + r'\.get\s*\(\s*["\']([^"\']+\.m3u8[^"\']*)["\']', + r'url\s*:\s*["\']([^"\']+\.m3u8[^"\']*)["\']', + r'src\s*=\s*["\']([^"\']+\.m3u8[^"\']*)["\']', + ] + for pat in patterns: + match = re.search(pat, html, re.I) + if match: + url = match.group(1) + if not url.startswith('http'): + url = urljoin(page_url, url) + return url + + scripts = re.findall(r']*>(.*?)', html, re.I | re.S) + for script_content in scripts: + if script_content.strip(): + found = self._js_decode(script_content) + if found and '.m3u8' in found: + return found + return None + + def _skip_ad_time(self, m3u8_text, skip_seconds=25): + """ + 解析 m3u8,累积分片时长,跳过前 skip_seconds 秒,生成新的 m3u8。 + 同时插入 #EXT-X-START 标签让播放器从指定时间开始。 + """ + lines = m3u8_text.splitlines() + header_lines = [] # 保留到分片前的全局标签 + media_sequence = 0 # 初始序列号 + target_duration = None + video_segments = [] # 每个元素 (extinf_line, ts_line) + + i = 0 + # 先提取头部标签 + while i < len(lines) and not lines[i].startswith('#EXTINF'): + line = lines[i] + if line.startswith('#EXT-X-MEDIA-SEQUENCE'): + try: + media_sequence = int(line.split(':')[1]) + except: + pass + header_lines.append(line) + elif line.startswith('#EXT-X-TARGETDURATION'): + header_lines.append(line) + try: + target_duration = float(line.split(':')[1]) + except: + pass + elif not line.startswith('#'): + # 意外出现非注释行,可能是分片,跳过 + break + else: + header_lines.append(line) + i += 1 + + # 读取分片段 + while i < len(lines): + if lines[i].startswith('#EXTINF'): + extinf = lines[i] + i += 1 + if i < len(lines): + ts_line = lines[i] + video_segments.append((extinf, ts_line)) + i += 1 + else: + break + elif lines[i].startswith('#'): + # 其他标签,忽略或酌情处理 + i += 1 + else: + # 孤立的 ts,视为一个片段 + video_segments.append(('', lines[i])) + i += 1 + + # 累加时长 + accumulated = 0.0 + start_index = 0 + for idx, (extinf, _) in enumerate(video_segments): + dur = 0.0 + match = re.search(r'#EXTINF:\s*([\d.]+)', extinf) + if match: + dur = float(match.group(1)) + elif target_duration: + dur = target_duration + else: + dur = 3.0 # 假设 3 秒 + accumulated += dur + if accumulated >= skip_seconds: + start_index = idx + break + else: + # 总时长不足 skip_seconds,不跳过 + start_index = 0 + + # 裁剪片段,更新序列号 + new_segments = video_segments[start_index:] + new_media_sequence = media_sequence + start_index + + # 构建新的 m3u8 内容 + new_lines = [] + # 添加原始头部但移除原有的 #EXT-X-MEDIA-SEQUENCE 和可能冲突的标签 + for line in header_lines: + if line.startswith('#EXT-X-MEDIA-SEQUENCE'): + continue + if line.startswith('#EXT-X-START'): + continue + new_lines.append(line) + # 添加新的序列号 + new_lines.append(f'#EXT-X-MEDIA-SEQUENCE:{new_media_sequence}') + # 添加起始跳转标签 + new_lines.append(f'#EXT-X-START:TIME-OFFSET={skip_seconds}') + # 添加分片 + for extinf, ts in new_segments: + if extinf: + new_lines.append(extinf) + new_lines.append(ts) + + return '\n'.join(new_lines) + + def _get_m3u8_content(self, url, referer): + try: + headers = self.session.headers.copy() + headers['Referer'] = referer + resp = requests.get(url, headers=headers, timeout=5) + if resp.status_code == 200: + resp.encoding = 'utf-8' + return resp.text + except Exception as e: + print(f"下载 m3u8 失败: {e}") + return None + + def playerContent(self, flag, id, vipFlags=None): + play_url = f"{self.host}/vodplay/{id}/" + res = self.fetch(play_url, headers={'Referer': self.host}, timeout=5) + if not res: + return {"parse": 1, "url": play_url} + + html = res.text + m3u8_url = None + + match = re.search(r'var\s+player_aaaa\s*=\s*(\{.*?\});', html, re.DOTALL) + if match: + try: + json_str = match.group(1).strip() + if json_str.endswith(','): + json_str = json_str[:-1] + config = json.loads(json_str) + m3u8_url = config.get('url', '') + except: + pass + + if not m3u8_url: + m3u8_url = self._js_decode(html) + + if not m3u8_url: + m3u8_url = self._sniff_xhr(html, play_url) + + if not m3u8_url: + return {"parse": 1, "url": play_url} + + m3u8_url = unquote(m3u8_url) + if m3u8_url.startswith('//'): + m3u8_url = 'https:' + m3u8_url + elif not m3u8_url.startswith('http'): + m3u8_url = urljoin(self.host, m3u8_url) + + # 下载原始 m3u8 + m3u8_text = self._get_m3u8_content(m3u8_url, play_url) + if not m3u8_text: + return { + "parse": 0, + "playUrl": "", + "url": m3u8_url, + "header": { + "User-Agent": self.session.headers['User-Agent'], + "Referer": play_url, + "Origin": self.host + } + } + + # 跳过前25秒 + filtered_m3u8 = self._skip_ad_time(m3u8_text, skip_seconds=25) + + # 如果没有变化(不足25秒),直接返回原始 + if filtered_m3u8 == m3u8_text: + return { + "parse": 0, + "playUrl": "", + "url": m3u8_url, + "header": { + "User-Agent": self.session.headers['User-Agent'], + "Referer": play_url, + "Origin": self.host + } + } + + # 打包为 data URI + encoded_m3u8 = base64.b64encode(filtered_m3u8.encode('utf-8')).decode('ascii') + data_url = f"data:application/vnd.apple.mpegurl;base64,{encoded_m3u8}" + + return { + "parse": 0, + "playUrl": "", + "url": data_url, + "header": { + "User-Agent": self.session.headers['User-Agent'], + "Referer": play_url, + "Origin": self.host + } + } \ No newline at end of file diff --git a/xbpq/极乐禁区.json b/xbpq/极乐禁区.json new file mode 100644 index 0000000..52e1e59 --- /dev/null +++ b/xbpq/极乐禁区.json @@ -0,0 +1,28 @@ +{ + "作者": "h", + "站名": "极乐禁区", + "请求头": "User-Agent$MOBILE_UA", + "编码": "UTF-8", + "主页url":"https://iegeewiet-4mag.hscwang26y2m.xyz/jlhs", + "直接播放": "1", + "搜索模式": "1", + "数组": "*||百大女优--
  • ||番号仓库--
  • ||国产传媒--
  • ||91探花--
  • ", + "标题": "\">&&||百大女优--#;\">&&||番号仓库--#;\">&&||国产传媒--#;\">&&||91探花--#;\">&&", + "副标题": "

    &&

    ", + "图片": "data-original=\"&&\"||百大女优--*
  • ", + "搜索图片": "&&||91porn--
    &&
    分割(后:||色花堂--
    &&||91大神--
  • &&
    ||18动漫--
    &&||猎奇之家--
    ||啪啪视频--
  • &&||国产片商--
  • &&
  • ||无码黑料--
    &&
    ||Javffm--
    ||七猫高清--
    &&", + "标题": "title=\"&&\"||91porn--m-t-5\">&&||探花AV--\"&&\"||色花堂--<h6&&||91大神--title=\"&&\"||18动漫--alt=\"&&\"||猎奇之家--title=\"&&\"||啪啪视频--title=\"&&\"||国产片商--title=\"&&\"||无码黑料--title=\"&&\"||Javffm--alt=\"&&\"||七猫高清--decodeURIComponent(\"&&\"[工具:解url]||1024看--alt=\"&&\"", + "副标题":"*-&&||91porn--\"duration\">&&||色花堂--
    &&||猎奇之家--&&||七猫高清--+&&", + "链接": "href=\"&&\"||91porn--&&||91porn--
    &&
    分割(后:||色花堂--
    &&||91大神--
  • &&
    ||18动漫--
    &&||猎奇之家--
    ||啪啪视频--
  • &&||国产片商--
  • &&
  • ||无码黑料--
    &&
    ||Javffm--
    ||七猫高清--
    &&&&||探花AV--\"&&\"||色花堂--<h6&&||91大神--title=\"&&\"||18动漫--alt=\"&&\"||猎奇之家--title=\"&&\"||啪啪视频--title=\"&&\"||国产片商--title=\"&&\"||无码黑料--title=\"&&\"||Javffm--alt=\"&&\"||七猫高清--decodeURIComponent(\"&&\"[工具:解url]", + "副标题":"*-&&||91porn--\"duration\">&&||色花堂--