From 44ae06c5eed6a0944fbecb99dff7ca194d9b4389 Mon Sep 17 00:00:00 2001 From: "Wang.Luo" <1593775941@qq.com> Date: Fri, 17 Oct 2025 01:31:47 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=B1=E4=BA=8E=E6=82=A8=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E7=9A=84"code=20differences"=E4=BF=A1=E6=81=AF=E4=B8=BA?= =?UTF-8?q?=E7=A9=BA=EF=BC=8C=E6=88=91=E6=97=A0=E6=B3=95=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=85=B7=E4=BD=93=E7=9A=84commit=20message=E3=80=82=E8=AF=B7?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=AE=9E=E9=99=85=E7=9A=84=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E4=BF=A1=E6=81=AF=EF=BC=8C=E4=BB=A5=E4=BE=BF?= =?UTF-8?q?=E6=88=91=E4=B8=BA=E6=82=A8=E7=94=9F=E6=88=90=E7=AC=A6=E5=90=88?= =?UTF-8?q?Angular=E8=A7=84=E8=8C=83=E7=9A=84=E4=B8=AD=E6=96=87commit=20me?= =?UTF-8?q?ssage=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Angular规范的commit message格式如下: - ``: 变更类型(如 feat, fix, docs, style, refactor, test, chore 等) - ``: 变更范围(如 component, service, module 等) - ``: 简短描述(中文) - ``: 详细描述(中文) 请提供代码变更详情,我将为您生成完整的commit message。 --- PyramidStore/plugin/adult/51吸瓜.py.bak | 404 +++++++++++++ PyramidStore/plugin/adult/今日看料.py | 716 ++++++++++++++++++++++++ PyramidStore/plugin/adult/黑料不打样.py | 139 +++++ PyramidStore/plugin/adult/黑料弹幕版.py | 444 +++++++++++++++ adult.json | 18 + 5 files changed, 1721 insertions(+) create mode 100644 PyramidStore/plugin/adult/51吸瓜.py.bak create mode 100644 PyramidStore/plugin/adult/今日看料.py create mode 100644 PyramidStore/plugin/adult/黑料不打样.py create mode 100644 PyramidStore/plugin/adult/黑料弹幕版.py diff --git a/PyramidStore/plugin/adult/51吸瓜.py.bak b/PyramidStore/plugin/adult/51吸瓜.py.bak new file mode 100644 index 0000000..81ba712 --- /dev/null +++ b/PyramidStore/plugin/adult/51吸瓜.py.bak @@ -0,0 +1,404 @@ +# -*- coding: utf-8 -*- +# 🌈 Love +import json +import random +import re +import sys +import threading +import time +from base64 import b64decode, b64encode +from urllib.parse import urlparse + +import requests +from Crypto.Cipher import AES +from Crypto.Util.Padding import unpad +from pyquery import PyQuery as pq +sys.path.append('..') +from base.spider import Spider + + +class Spider(Spider): + + def init(self, extend=""): + try:self.proxies = json.loads(extend) + except:self.proxies = {} + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache', + } + # Use working dynamic URLs directly + self.host = self.get_working_host() + self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"}) + self.log(f"使用站点: {self.host}") + print(f"使用站点: {self.host}") + pass + + def getName(self): + return "🌈 51吸瓜" + + def isVideoFormat(self, url): + # Treat direct media formats as playable without parsing + return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts']) + + def manualVideoCheck(self): + return False + + def destroy(self): + pass + + def homeContent(self, filter): + try: + response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + return {'class': [], 'list': []} + + data = self.getpq(response.text) + result = {} + classes = [] + + # Try to get categories from different possible locations + category_selectors = [ + '.category-list ul li', + '.nav-menu li', + '.menu li', + 'nav ul li' + ] + + for selector in category_selectors: + for k in data(selector).items(): + link = k('a') + href = (link.attr('href') or '').strip() + name = (link.text() or '').strip() + # Skip placeholder or invalid entries + if not href or href == '#' or not name: + continue + classes.append({ + 'type_name': name, + 'type_id': href + }) + if classes: + break + + # If no categories found, create some default ones + if not classes: + classes = [ + {'type_name': '首页', 'type_id': '/'}, + {'type_name': '最新', 'type_id': '/latest/'}, + {'type_name': '热门', 'type_id': '/hot/'} + ] + + result['class'] = classes + result['list'] = self.getlist(data('#index article a')) + return result + + except Exception as e: + print(f"homeContent error: {e}") + return {'class': [], 'list': []} + + def homeVideoContent(self): + try: + response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + return {'list': []} + data = self.getpq(response.text) + return {'list': self.getlist(data('#index article a, #archive article a'))} + except Exception as e: + print(f"homeVideoContent error: {e}") + return {'list': []} + + def categoryContent(self, tid, pg, filter, extend): + try: + if '@folder' in tid: + id = tid.replace('@folder', '') + videos = self.getfod(id) + else: + # Build URL properly + if tid.startswith('/'): + if pg and pg != '1': + url = f"{self.host}{tid}page/{pg}/" + else: + url = f"{self.host}{tid}" + else: + url = f"{self.host}/{tid}" + + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0} + + data = self.getpq(response.text) + videos = self.getlist(data('#archive article a, #index article a'), tid) + + result = {} + result['list'] = videos + result['page'] = pg + result['pagecount'] = 1 if '@folder' in tid else 99999 + result['limit'] = 90 + result['total'] = 999999 + return result + + except Exception as e: + print(f"categoryContent error: {e}") + return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0} + + def detailContent(self, ids): + try: + url = f"{self.host}{ids[0]}" if not ids[0].startswith('http') else ids[0] + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + + if response.status_code != 200: + return {'list': [{'vod_play_from': '51吸瓜', 'vod_play_url': f'页面加载失败${url}'}]} + + data = self.getpq(response.text) + vod = {'vod_play_from': '51吸瓜'} + + # Get content/description + try: + clist = [] + if data('.tags .keywords a'): + for k in data('.tags .keywords a').items(): + title = k.text() + href = k.attr('href') + if title and href: + clist.append('[a=cr:' + json.dumps({'id': href, 'name': title}) + '/]' + title + '[/a]') + vod['vod_content'] = ' '.join(clist) if clist else data('.post-title').text() + except: + vod['vod_content'] = data('.post-title').text() or '51吸瓜视频' + + # Get video URLs (build episode list when multiple players exist) + try: + plist = [] + used_names = set() + if data('.dplayer'): + for c, k in enumerate(data('.dplayer').items(), start=1): + config_attr = k.attr('data-config') + if config_attr: + try: + config = json.loads(config_attr) + video_url = config.get('video', {}).get('url', '') + # Determine a readable episode name from nearby headings if present + ep_name = '' + try: + parent = k.parents().eq(0) + # search up to a few ancestors for a heading text + for _ in range(3): + if not parent: break + heading = parent.find('h2, h3, h4').eq(0).text() or '' + heading = heading.strip() + if heading: + ep_name = heading + break + parent = parent.parents().eq(0) + except Exception: + ep_name = '' + base_name = ep_name if ep_name else f"视频{c}" + name = base_name + count = 2 + # Ensure the name is unique + while name in used_names: + name = f"{base_name} {count}" + count += 1 + used_names.add(name) + if video_url: + self.log(f"解析到视频: {name} -> {video_url}") + print(f"解析到视频: {name} -> {video_url}") + plist.append(f"{name}${video_url}") + except: + continue + + if plist: + self.log(f"拼装播放列表,共{len(plist)}个") + print(f"拼装播放列表,共{len(plist)}个") + vod['vod_play_url'] = '#'.join(plist) + else: + vod['vod_play_url'] = f"未找到视频源${url}" + + except Exception as e: + vod['vod_play_url'] = f"视频解析失败${url}" + + return {'list': [vod]} + + except Exception as e: + print(f"detailContent error: {e}") + return {'list': [{'vod_play_from': '51吸瓜', 'vod_play_url': f'详情页加载失败${ids[0] if ids else ""}'}]} + + def searchContent(self, key, quick, pg="1"): + try: + url = f"{self.host}/search/{key}/{pg}" if pg != "1" else f"{self.host}/search/{key}/" + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + + if response.status_code != 200: + return {'list': [], 'page': pg} + + data = self.getpq(response.text) + videos = self.getlist(data('#archive article a, #index article a')) + return {'list': videos, 'page': pg} + + except Exception as e: + print(f"searchContent error: {e}") + return {'list': [], 'page': pg} + + def playerContent(self, flag, id, vipFlags): + url = id + p = 1 + if self.isVideoFormat(url): + # m3u8/mp4 direct play; when using proxy setting, wrap to proxy for m3u8 + if '.m3u8' in url: + url = self.proxy(url) + p = 0 + self.log(f"播放请求: parse={p}, url={url}") + print(f"播放请求: parse={p}, url={url}") + return {'parse': p, 'url': url, 'header': self.headers} + + def localProxy(self, param): + if param.get('type') == 'img': + res=requests.get(param['url'], headers=self.headers, proxies=self.proxies, timeout=10) + return [200,res.headers.get('Content-Type'),self.aesimg(res.content)] + elif param.get('type') == 'm3u8':return self.m3Proxy(param['url']) + else:return self.tsProxy(param['url']) + + def proxy(self, data, type='m3u8'): + if data and len(self.proxies):return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}" + else:return data + + def m3Proxy(self, url): + url=self.d64(url) + ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False) + data = ydata.content.decode('utf-8') + if ydata.headers.get('Location'): + url = ydata.headers['Location'] + data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8') + lines = data.strip().split('\n') + last_r = url[:url.rfind('/')] + parsed_url = urlparse(url) + durl = parsed_url.scheme + "://" + parsed_url.netloc + iskey=True + for index, string in enumerate(lines): + if iskey and 'URI' in string: + pattern = r'URI="([^"]*)"' + match = re.search(pattern, string) + if match: + lines[index] = re.sub(pattern, f'URI="{self.proxy(match.group(1), "mkey")}"', string) + iskey=False + continue + if '#EXT' not in string: + if 'http' not in string: + domain = last_r if string.count('/') < 2 else durl + string = domain + ('' if string.startswith('/') else '/') + string + lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0]) + data = '\n'.join(lines) + return [200, "application/vnd.apple.mpegur", data] + + def tsProxy(self, url): + url = self.d64(url) + data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True) + return [200, data.headers['Content-Type'], data.content] + + def e64(self, text): + try: + text_bytes = text.encode('utf-8') + encoded_bytes = b64encode(text_bytes) + return encoded_bytes.decode('utf-8') + except Exception as e: + print(f"Base64编码错误: {str(e)}") + return "" + + def d64(self, encoded_text): + try: + encoded_bytes = encoded_text.encode('utf-8') + decoded_bytes = b64decode(encoded_bytes) + return decoded_bytes.decode('utf-8') + except Exception as e: + print(f"Base64解码错误: {str(e)}") + return "" + + def get_working_host(self): + """Get working host from known dynamic URLs""" + # Known working URLs from the dynamic gateway + dynamic_urls = [ + 'https://artist.vgwtswi.xyz', + 'https://ability.vgwtswi.xyz', + 'https://am.vgwtswi.xyz' + ] + + # Test each URL to find a working one + for url in dynamic_urls: + try: + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10) + if response.status_code == 200: + # Verify it has the expected content structure + data = self.getpq(response.text) + articles = data('#index article a') + if len(articles) > 0: + self.log(f"选用可用站点: {url}") + print(f"选用可用站点: {url}") + return url + except Exception as e: + continue + + # Fallback to first URL if none work (better than crashing) + self.log(f"未检测到可用站点,回退: {dynamic_urls[0]}") + print(f"未检测到可用站点,回退: {dynamic_urls[0]}") + return dynamic_urls[0] + + + def getlist(self, data, tid=''): + videos = [] + l = '/mrdg' in tid + for k in data.items(): + a = k.attr('href') + b = k('h2').text() + # Some pages might not include datePublished; use a fallback + c = k('span[itemprop="datePublished"]').text() or k('.post-meta, .entry-meta, time').text() + if a and b: + videos.append({ + 'vod_id': f"{a}{'@folder' if l else ''}", + 'vod_name': b.replace('\n', ' '), + 'vod_pic': self.getimg(k('script').text()), + 'vod_remarks': c or '', + 'vod_tag': 'folder' if l else '', + 'style': {"type": "rect", "ratio": 1.33} + }) + return videos + + def getfod(self, id): + url = f"{self.host}{id}" + data = self.getpq(requests.get(url, headers=self.headers, proxies=self.proxies).text) + vdata=data('.post-content[itemprop="articleBody"]') + r=['.txt-apps','.line','blockquote','.tags','.content-tabs'] + for i in r:vdata.remove(i) + p=vdata('p') + videos=[] + for i,x in enumerate(vdata('h2').items()): + c=i*2 + videos.append({ + 'vod_id': p.eq(c)('a').attr('href'), + 'vod_name': p.eq(c).text(), + 'vod_pic': f"{self.getProxyUrl()}&url={p.eq(c+1)('img').attr('data-xkrkllgl')}&type=img", + 'vod_remarks':x.text() + }) + return videos + + def getimg(self, text): + match = re.search(r"loadBannerDirect\('([^']+)'", text) + if match: + url = match.group(1) + return f"{self.getProxyUrl()}&url={url}&type=img" + else: + return '' + + def aesimg(self, word): + key = b'f5d965df75336270' + iv = b'97b60394abc2fbe1' + cipher = AES.new(key, AES.MODE_CBC, iv) + decrypted = unpad(cipher.decrypt(word), AES.block_size) + return decrypted + + def getpq(self, data): + try: + return pq(data) + except Exception as e: + print(f"{str(e)}") + return pq(data.encode('utf-8')) diff --git a/PyramidStore/plugin/adult/今日看料.py b/PyramidStore/plugin/adult/今日看料.py new file mode 100644 index 0000000..a1364d7 --- /dev/null +++ b/PyramidStore/plugin/adult/今日看料.py @@ -0,0 +1,716 @@ +# -*- coding: utf-8 -*- +# 🌈 Love +import json +import random +import re +import sys +import threading +import time +from base64 import b64decode, b64encode +from urllib.parse import urlparse, quote + +import requests +from pyquery import PyQuery as pq +sys.path.append('..') +from base.spider import Spider + + +class Spider(Spider): + + def init(self, extend=""): + try:self.proxies = json.loads(extend) + except:self.proxies = {} + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache', + } + # Use working dynamic URLs directly + self.host = self.get_working_host() + self.headers.update({'Origin': self.host, 'Referer': f"{self.host}/"}) + self.log(f"使用站点: {self.host}") + print(f"使用站点: {self.host}") + pass + + def getName(self): + return "🌈 今日看料" + + def isVideoFormat(self, url): + # Treat direct media formats as playable without parsing + return any(ext in (url or '') for ext in ['.m3u8', '.mp4', '.ts']) + + def manualVideoCheck(self): + return False + + def destroy(self): + pass + + def homeContent(self, filter): + try: + response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + return {'class': [], 'list': []} + + data = self.getpq(response.text) + result = {} + classes = [] + + # 优先从导航栏获取分类 + nav_selectors = [ + '#navbarCollapse .navbar-nav .nav-item .nav-link', + '.navbar-nav .nav-item .nav-link', + '#nav .menu-item a', + '.menu .menu-item a' + ] + + found_categories = False + for selector in nav_selectors: + for item in data(selector).items(): + href = item.attr('href') or '' + name = item.text().strip() + + # 过滤掉非分类链接 + if (not href or not name or + href == '#' or + href.startswith('http') or + 'about' in href.lower() or + 'contact' in href.lower() or + 'tags' in href.lower() or + 'top' in href.lower() or + 'start' in href.lower() or + 'time' in href.lower()): + continue + + # 确保是分类链接(包含category或明确的分类路径) + if '/category/' in href or any(cat in href for cat in ['/dy/', '/ks/', '/douyu/', '/hy/', '/hj/', '/tt/', '/wh/', '/asmr/', '/xb/', '/xsp/', '/rdgz/']): + # 处理相对路径 + if href.startswith('/'): + type_id = href + else: + type_id = f'/{href}' + + classes.append({ + 'type_name': name, + 'type_id': type_id + }) + found_categories = True + + # 如果导航栏没找到,尝试从分类下拉菜单获取 + if not found_categories: + category_selectors = [ + '.category-list a', + '.slide-toggle + .category-list a', + '.menu .category-list a' + ] + for selector in category_selectors: + for item in data(selector).items(): + href = item.attr('href') or '' + name = item.text().strip() + + if href and name and href != '#': + if href.startswith('/'): + type_id = href + else: + type_id = f'/{href}' + + classes.append({ + 'type_name': name, + 'type_id': type_id + }) + found_categories = True + + # 去重 + unique_classes = [] + seen_ids = set() + for cls in classes: + if cls['type_id'] not in seen_ids: + unique_classes.append(cls) + seen_ids.add(cls['type_id']) + + # 如果没有找到分类,创建默认分类 + if not unique_classes: + unique_classes = [ + {'type_name': '热点关注', 'type_id': '/category/rdgz/'}, + {'type_name': '抖音', 'type_id': '/category/dy/'}, + {'type_name': '快手', 'type_id': '/category/ks/'}, + {'type_name': '斗鱼', 'type_id': '/category/douyu/'}, + {'type_name': '虎牙', 'type_id': '/category/hy/'}, + {'type_name': '花椒', 'type_id': '/category/hj/'}, + {'type_name': '推特', 'type_id': '/category/tt/'}, + {'type_name': '网红', 'type_id': '/category/wh/'}, + {'type_name': 'ASMR', 'type_id': '/category/asmr/'}, + {'type_name': 'X播', 'type_id': '/category/xb/'}, + {'type_name': '小视频', 'type_id': '/category/xsp/'} + ] + + result['class'] = unique_classes + result['list'] = self.getlist(data('#index article a, #archive article a')) + return result + + except Exception as e: + print(f"homeContent error: {e}") + return {'class': [], 'list': []} + + def homeVideoContent(self): + try: + response = requests.get(self.host, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + return {'list': []} + data = self.getpq(response.text) + return {'list': self.getlist(data('#index article a, #archive article a'))} + except Exception as e: + print(f"homeVideoContent error: {e}") + return {'list': []} + + def categoryContent(self, tid, pg, filter, extend): + try: + # 修复URL构建 - 去除多余的斜杠 + base_url = tid.lstrip('/').rstrip('/') + if pg and pg != '1': + url = f"{self.host}{base_url}/{pg}/" + else: + url = f"{self.host}{base_url}/" + + print(f"分类页面URL: {url}") + + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + print(f"分类页面请求失败: {response.status_code}") + return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0} + + data = self.getpq(response.text) + videos = self.getlist(data('#archive article a, #index article a, .post-card'), tid) + + # 如果没有找到视频,尝试其他选择器 + if not videos: + videos = self.getlist(data('article a, .post a, .entry-title a'), tid) + + print(f"找到 {len(videos)} 个视频") + + # 改进的页数检测逻辑 + pagecount = self.detect_page_count(data, pg) + + result = {} + result['list'] = videos + result['page'] = pg + result['pagecount'] = pagecount + result['limit'] = 90 + result['total'] = 999999 + return result + + except Exception as e: + print(f"categoryContent error: {e}") + return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0} + + def tagContent(self, tid, pg, filter, extend): + """标签页面内容""" + try: + # 修复URL构建 - 去除多余的斜杠 + base_url = tid.lstrip('/').rstrip('/') + if pg and pg != '1': + url = f"{self.host}{base_url}/{pg}/" + else: + url = f"{self.host}{base_url}/" + + print(f"标签页面URL: {url}") + + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + if response.status_code != 200: + print(f"标签页面请求失败: {response.status_code}") + return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0} + + data = self.getpq(response.text) + videos = self.getlist(data('#archive article a, #index article a, .post-card'), tid) + + # 如果没有找到视频,尝试其他选择器 + if not videos: + videos = self.getlist(data('article a, .post a, .entry-title a'), tid) + + print(f"找到 {len(videos)} 个标签相关视频") + + # 页数检测 + pagecount = self.detect_page_count(data, pg) + + result = {} + result['list'] = videos + result['page'] = pg + result['pagecount'] = pagecount + result['limit'] = 90 + result['total'] = 999999 + return result + + except Exception as e: + print(f"tagContent error: {e}") + return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 90, 'total': 0} + + def detect_page_count(self, data, current_page): + """改进的页数检测方法""" + pagecount = 99999 # 默认大数字,允许无限翻页 + + # 方法1: 检查分页器中的所有页码链接 + page_numbers = [] + + # 查找所有可能的页码链接 + page_selectors = [ + '.page-navigator a', + '.pagination a', + '.pages a', + '.page-numbers a' + ] + + for selector in page_selectors: + for page_link in data(selector).items(): + href = page_link.attr('href') or '' + text = page_link.text().strip() + + # 从href中提取页码 + if href: + # 匹配 /category/dy/2/ 这种格式 + match = re.search(r'/(\d+)/?$', href.rstrip('/')) + if match: + page_num = int(match.group(1)) + if page_num not in page_numbers: + page_numbers.append(page_num) + + # 从文本中提取数字页码 + if text and text.isdigit(): + page_num = int(text) + if page_num not in page_numbers: + page_numbers.append(page_num) + + # 如果有找到页码,取最大值 + if page_numbers: + max_page = max(page_numbers) + print(f"从分页器检测到最大页码: {max_page}") + return max_page + + # 方法2: 检查是否存在"下一页"按钮 + next_selectors = [ + '.page-navigator .next', + '.pagination .next', + '.next-page', + 'a:contains("下一页")' + ] + + for selector in next_selectors: + if data(selector): + print("检测到下一页按钮,允许继续翻页") + return 99999 + + # 方法3: 如果当前页视频数量很少,可能没有下一页 + if len(data('#archive article, #index article, .post-card')) < 5: + print("当前页内容较少,可能没有下一页") + return int(current_page) + + print("使用默认页数: 99999") + return 99999 + + def detailContent(self, ids): + try: + url = f"{self.host}{ids[0]}" if not ids[0].startswith('http') else ids[0] + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + + if response.status_code != 200: + return {'list': [{'vod_play_from': '今日看料', 'vod_play_url': f'页面加载失败${url}'}]} + + data = self.getpq(response.text) + vod = {'vod_play_from': '今日看料'} + + # 获取标题 + title_selectors = ['.post-title', 'h1.entry-title', 'h1', '.post-card-title'] + for selector in title_selectors: + title_elem = data(selector) + if title_elem: + vod['vod_name'] = title_elem.text().strip() + break + + if 'vod_name' not in vod: + vod['vod_name'] = '今日看料视频' + + # 获取内容/描述 + try: + clist = [] + if data('.tags .keywords a'): + for k in data('.tags .keywords a').items(): + title = k.text() + href = k.attr('href') + if title and href: + # 使href相对路径 + if href.startswith(self.host): + href = href.replace(self.host, '') + clist.append('[a=cr:' + json.dumps({'id': href, 'name': title}) + '/]' + title + '[/a]') + vod['vod_content'] = ' '.join(clist) if clist else data('.post-content').text() or vod['vod_name'] + except: + vod['vod_content'] = vod['vod_name'] + + # 获取视频URLs + try: + plist = [] + used_names = set() + + # 查找DPlayer视频 + if data('.dplayer'): + for c, k in enumerate(data('.dplayer').items(), start=1): + config_attr = k.attr('data-config') + if config_attr: + try: + config = json.loads(config_attr) + video_url = config.get('video', {}).get('url', '') + if video_url: + name = f"视频{c}" + count = 2 + while name in used_names: + name = f"视频{c}_{count}" + count += 1 + used_names.add(name) + self.log(f"解析到视频: {name} -> {video_url}") + print(f"解析到视频: {name} -> {video_url}") + plist.append(f"{name}${video_url}") + except: + continue + + # 查找视频标签 + if not plist: + video_selectors = ['video source', 'video', 'iframe[src*="video"]', 'a[href*=".m3u8"]', 'a[href*=".mp4"]'] + for selector in video_selectors: + for c, elem in enumerate(data(selector).items(), start=1): + src = elem.attr('src') or elem.attr('href') or '' + if src and any(ext in src for ext in ['.m3u8', '.mp4', 'video']): + name = f"视频{c}" + count = 2 + while name in used_names: + name = f"视频{c}_{count}" + count += 1 + used_names.add(name) + plist.append(f"{name}${src}") + + if plist: + self.log(f"拼装播放列表,共{len(plist)}个") + print(f"拼装播放列表,共{len(plist)}个") + vod['vod_play_url'] = '#'.join(plist) + else: + vod['vod_play_url'] = f"正片${url}" + + except Exception as e: + print(f"视频解析错误: {e}") + vod['vod_play_url'] = f"正片${url}" + + return {'list': [vod]} + + except Exception as e: + print(f"detailContent error: {e}") + return {'list': [{'vod_play_from': '今日看料', 'vod_play_url': f'详情页加载失败${ids[0] if ids else ""}'}]} + + def searchContent(self, key, quick, pg="1"): + try: + # 优先使用标签搜索 + encoded_key = quote(key) + url = f"{self.host}/tag/{encoded_key}/{pg}" if pg != "1" else f"{self.host}/tag/{encoded_key}/" + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + + if response.status_code != 200: + # 尝试搜索页面 + url = f"{self.host}/search/{encoded_key}/{pg}" if pg != "1" else f"{self.host}/search/{encoded_key}/" + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + + if response.status_code != 200: + return {'list': [], 'page': pg} + + data = self.getpq(response.text) + videos = self.getlist(data('#archive article a, #index article a, .post-card')) + + # 使用改进的页数检测方法 + pagecount = self.detect_page_count(data, pg) + + return {'list': videos, 'page': pg, 'pagecount': pagecount} + + except Exception as e: + print(f"searchContent error: {e}") + return {'list': [], 'page': pg} + + def getTagsContent(self, pg="1"): + """获取标签页面内容""" + try: + url = f"{self.host}/tags.html" + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=15) + + if response.status_code != 200: + return {'list': [], 'page': pg} + + data = self.getpq(response.text) + tags = [] + + # 从标签页面提取所有标签 - 使用更宽松的选择器 + for tag_elem in data('a[href*="/tag/"]').items(): + tag_name = tag_elem.text().strip() + tag_href = tag_elem.attr('href') or '' + + if tag_name and tag_href and '/tag/' in tag_href and tag_name != '全部标签': # 排除标题链接 + # 处理为相对路径 + tag_id = tag_href.replace(self.host, '') + if not tag_id.startswith('/'): + tag_id = '/' + tag_id + + tags.append({ + 'vod_id': tag_id, + 'vod_name': f"🏷️ {tag_name}", + 'vod_pic': '', + 'vod_remarks': '标签', + 'vod_tag': 'tag', + 'style': {"type": "rect", "ratio": 1.33} + }) + + print(f"找到 {len(tags)} 个标签") + + # 分页处理 - 标签页面通常不需要分页 + result = {} + result['list'] = tags + result['page'] = pg + result['pagecount'] = 1 # 标签页面通常只有一页 + result['limit'] = 999 + result['total'] = len(tags) + return result + + except Exception as e: + print(f"getTagsContent error: {e}") + return {'list': [], 'page': pg} + + def playerContent(self, flag, id, vipFlags): + url = id + p = 1 + if self.isVideoFormat(url): + if '.m3u8' in url: + url = self.proxy(url) + p = 0 + self.log(f"播放请求: parse={p}, url={url}") + print(f"播放请求: parse={p}, url={url}") + return {'parse': p, 'url': url, 'header': self.headers} + + def localProxy(self, param): + try: + if param.get('type') == 'img': + img_url = self.d64(param['url']) + if not img_url.startswith(('http://', 'https://')): + if img_url.startswith('/'): + img_url = f"{self.host}{img_url}" + else: + img_url = f"{self.host}/{img_url}" + + res = requests.get(img_url, headers=self.headers, proxies=self.proxies, timeout=10) + return [200, res.headers.get('Content-Type', 'image/jpeg'), res.content] + elif param.get('type') == 'm3u8': + return self.m3Proxy(param['url']) + else: + return self.tsProxy(param['url']) + except Exception as e: + print(f"localProxy error: {e}") + return [500, "text/plain", f"Proxy error: {str(e)}".encode()] + + def proxy(self, data, type='m3u8'): + if data and len(self.proxies): + return f"{self.getProxyUrl()}&url={self.e64(data)}&type={type}" + else: + return data + + def m3Proxy(self, url): + try: + url = self.d64(url) + ydata = requests.get(url, headers=self.headers, proxies=self.proxies, allow_redirects=False) + data = ydata.content.decode('utf-8') + if ydata.headers.get('Location'): + url = ydata.headers['Location'] + data = requests.get(url, headers=self.headers, proxies=self.proxies).content.decode('utf-8') + lines = data.strip().split('\n') + last_r = url[:url.rfind('/')] + parsed_url = urlparse(url) + durl = parsed_url.scheme + "://" + parsed_url.netloc + iskey = True + for index, string in enumerate(lines): + if iskey and 'URI' in string: + pattern = r'URI="([^"]*)"' + match = re.search(pattern, string) + if match: + lines[index] = re.sub(pattern, f'URI="{self.proxy(match.group(1), "mkey")}"', string) + iskey = False + continue + if '#EXT' not in string: + if 'http' not in string: + domain = last_r if string.count('/') < 2 else durl + string = domain + ('' if string.startswith('/') else '/') + string + lines[index] = self.proxy(string, string.split('.')[-1].split('?')[0]) + data = '\n'.join(lines) + return [200, "application/vnd.apple.mpegur", data] + except Exception as e: + print(f"m3Proxy error: {e}") + return [500, "text/plain", f"m3u8 proxy error: {str(e)}".encode()] + + def tsProxy(self, url): + try: + url = self.d64(url) + data = requests.get(url, headers=self.headers, proxies=self.proxies, stream=True) + return [200, data.headers.get('Content-Type', 'video/mp2t'), data.content] + except Exception as e: + print(f"tsProxy error: {e}") + return [500, "text/plain", f"ts proxy error: {str(e)}".encode()] + + def e64(self, text): + try: + text_bytes = text.encode('utf-8') + encoded_bytes = b64encode(text_bytes) + return encoded_bytes.decode('utf-8') + except Exception as e: + print(f"Base64编码错误: {str(e)}") + return "" + + def d64(self, encoded_text): + try: + encoded_bytes = encoded_text.encode('utf-8') + decoded_bytes = b64decode(encoded_bytes) + return decoded_bytes.decode('utf-8') + except Exception as e: + print(f"Base64解码错误: {str(e)}") + return "" + + def get_working_host(self): + """Get working host from known dynamic URLs""" + dynamic_urls = [ + 'https://kanliao2.one/', + 'https://kanliao7.org/', + 'https://kanliao7.net/', + 'https://kanliao14.com/' + ] + + for url in dynamic_urls: + try: + response = requests.get(url, headers=self.headers, proxies=self.proxies, timeout=10) + if response.status_code == 200: + data = self.getpq(response.text) + articles = data('#index article a, #archive article a') + if len(articles) > 0: + self.log(f"选用可用站点: {url}") + print(f"选用可用站点: {url}") + return url + except Exception as e: + continue + + self.log(f"未检测到可用站点,回退: {dynamic_urls[0]}") + print(f"未检测到可用站点,回退: {dynamic_urls[0]}") + return dynamic_urls[0] + + def getlist(self, data, tid=''): + videos = [] + for k in data.items(): + a = k.attr('href') + b = k('h2').text() or k('.post-card-title').text() or k('.entry-title').text() or k.text() + c = k('span[itemprop="datePublished"]').text() or k('.post-meta, .entry-meta, time, .post-card-info').text() + + # 过滤广告:检查是否包含"热搜HOT"标志 + if self.is_advertisement(k): + print(f"过滤广告: {b}") + continue + + if a and b and b.strip(): + # 处理相对路径 + if not a.startswith('http'): + if a.startswith('/'): + vod_id = a + else: + vod_id = f'/{a}' + else: + vod_id = a + + videos.append({ + 'vod_id': vod_id, + 'vod_name': b.replace('\n', ' ').strip(), + 'vod_pic': self.get_article_img(k), + 'vod_remarks': c.strip() if c else '', + 'vod_tag': '', + 'style': {"type": "rect", "ratio": 1.33} + }) + return videos + + def is_advertisement(self, article_elem): + """判断是否为广告(包含热搜HOT标志)""" + # 检查.wraps元素是否包含"热搜HOT"文本 + hot_elements = article_elem.find('.wraps') + for elem in hot_elements.items(): + if '热搜HOT' in elem.text(): + return True + + # 检查标题是否包含广告关键词 + title = article_elem('h2').text() or article_elem('.post-card-title').text() or '' + ad_keywords = ['热搜HOT', '手机链接', 'DNS设置', '修改DNS', 'WIFI设置'] + if any(keyword in title for keyword in ad_keywords): + return True + + # 检查背景颜色是否为广告特有的渐变背景 + style = article_elem.attr('style') or '' + if 'background:' in style and any(gradient in style for gradient in ['-webkit-linear-gradient', 'linear-gradient']): + # 进一步检查是否包含特定的广告颜色组合 + ad_gradients = ['#ec008c,#fc6767', '#ffe259,#ffa751'] + if any(gradient in style for gradient in ad_gradients): + return True + + return False + + def get_article_img(self, article_elem): + """从文章元素中提取图片,多种方式尝试""" + # 方式1: 从script标签中提取loadBannerDirect + script_text = article_elem('script').text() + if script_text: + match = re.search(r"loadBannerDirect\('([^']+)'", script_text) + if match: + url = match.group(1) + if not url.startswith(('http://', 'https://')): + if url.startswith('/'): + url = f"{self.host}{url}" + else: + url = f"{self.host}/{url}" + return f"{self.getProxyUrl()}&url={self.e64(url)}&type=img" + + # 方式2: 从背景图片中提取 + bg_elem = article_elem.find('.blog-background') + if bg_elem: + style = bg_elem.attr('style') or '' + bg_match = re.search(r'background-image:\s*url\(["\']?([^"\'\)]+)["\']?\)', style) + if bg_match: + img_url = bg_match.group(1) + if img_url and not img_url.startswith('data:'): + if not img_url.startswith(('http://', 'https://')): + if img_url.startswith('/'): + img_url = f"{self.host}{img_url}" + else: + img_url = f"{self.host}/{img_url}" + return f"{self.getProxyUrl()}&url={self.e64(img_url)}&type=img" + + # 方式3: 从图片标签中提取 + img_elem = article_elem.find('img') + if img_elem: + data_src = img_elem.attr('data-src') + if data_src: + if not data_src.startswith(('http://', 'https://')): + if data_src.startswith('/'): + data_src = f"{self.host}{data_src}" + else: + data_src = f"{self.host}/{data_src}" + return f"{self.getProxyUrl()}&url={self.e64(data_src)}&type=img" + + src = img_elem.attr('src') + if src: + if not src.startswith(('http://', 'https://')): + if src.startswith('/'): + src = f"{self.host}{src}" + else: + src = f"{self.host}/{src}" + return f"{self.getProxyUrl()}&url={self.e64(src)}&type=img" + + return '' + + def getpq(self, data): + try: + return pq(data) + except Exception as e: + print(f"{str(e)}") + return pq(data.encode('utf-8')) \ No newline at end of file diff --git a/PyramidStore/plugin/adult/黑料不打样.py b/PyramidStore/plugin/adult/黑料不打样.py new file mode 100644 index 0000000..15fc547 --- /dev/null +++ b/PyramidStore/plugin/adult/黑料不打样.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +import json,re,sys,base64,requests +from Crypto.Cipher import AES +from pyquery import PyQuery as pq +sys.path.append('..') +from base.spider import Spider + +class Spider(Spider): + SELECTORS=['.video-item','.video-list .item','.list-item','.post-item'] + def getName(self):return"黑料不打烊" + def init(self,extend=""):pass + def homeContent(self,filter): + cateManual={"最新黑料":"hlcg","今日热瓜":"jrrs","每日TOP10":"mrrb","周报精选":"zbjx","月榜热瓜":"ybrg","反差女友":"fczq","校园黑料":"xycg","网红黑料":"whhl","明星丑闻":"mxcw","原创社区":"ycsq","推特社区":"ttsq","社会新闻":"shxw","官场爆料":"gchl","影视短剧":"ysdj","全球奇闻":"qqqw","黑料课堂":"hlkt","每日大赛":"mrds","激情小说":"jqxs","桃图杂志":"ttzz","深夜综艺":"syzy","独家爆料":"djbl"} + return{'class':[{'type_name':k,'type_id':v}for k,v in cateManual.items()]} + def homeVideoContent(self):return{} + def categoryContent(self,tid,pg,filter,extend): + url=f'https://heiliao.com/{tid}/'if int(pg)==1 else f'https://heiliao.com/{tid}/page/{pg}/' + videos=self.get_list(url) + return{'list':videos,'page':pg,'pagecount':9999,'limit':90,'total':999999} + def fetch_and_decrypt_image(self,url): + try: + if url.startswith('//'):url='https:'+url + elif url.startswith('/'):url='https://heiliao.com'+url + r=requests.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36','Referer':'https://heiliao.com/'},timeout=15,verify=False) + if r.status_code!=200:return b'' + return AES.new(b'f5d965df75336270',AES.MODE_CBC,b'97b60394abc2fbe1').decrypt(r.content) + except: return b'' + def _extract_img_from_onload(self,node): + try: + m=re.search(r"load(?:Share)?Img\s*\([^,]+,\s*['\"]([^'\"]+)['\"]",(node.attr('onload')or'')) + return m.group(1)if m else'' + except:return'' + def _should_decrypt(self,url:str)->bool: + u=(url or'').lower();return any(x in u for x in['pic.gylhaa.cn','new.slfpld.cn','/upload_01/','/upload/']) + def _abs(self,u:str)->str: + if not u:return'' + if u.startswith('//'):return'https:'+u + if u.startswith('/'):return'https://heiliao.com'+u + return u + def e64(self,s:str)->str: + try:return base64.b64encode((s or'').encode()).decode() + except:return'' + def d64(self,s:str)->str: + try:return base64.b64decode((s or'').encode()).decode() + except:return'' + def _img(self,img_node): + u=''if img_node is None else(img_node.attr('src')or img_node.attr('data-src')or'') + enc=''if img_node is None else self._extract_img_from_onload(img_node) + t=enc or u + return f"{self.getProxyUrl()}&url={self.e64(t)}&type=hlimg"if t and(enc or self._should_decrypt(t))else self._abs(t) + def _parse_items(self,root): + vids=[] + for sel in self.SELECTORS: + for it in root(sel).items(): + title=it.find('.title, h3, h4, .video-title').text() + if not title:continue + link=it.find('a').attr('href') + if not link:continue + vids.append({'vod_id':self._abs(link),'vod_name':title,'vod_pic':self._img(it.find('img')),'vod_remarks':it.find('.date, .time, .remarks, .duration').text()or''}) + if vids:break + return vids + def detailContent(self,array): + tid=array[0];url=tid if tid.startswith('http')else f'https://heiliao.com{tid}' + rsp=self.fetch(url) + if not rsp:return{'list':[]} + rsp.encoding='utf-8';html_text=rsp.text + try:root_text=pq(html_text) + except:root_text=None + try:root_content=pq(rsp.content) + except:root_content=None + title=(root_text('title').text()if root_text else'')or'' + if' - 黑料网'in title:title=title.replace(' - 黑料网','') + pic='' + if root_text: + og=root_text('meta[property="og:image"]').attr('content') + if og and(og.endswith('.png')or og.endswith('.jpg')or og.endswith('.jpeg')):pic=og + else:pic=self._img(root_text('.video-item-img img')) + detail='' + if root_text: + detail=root_text('meta[name="description"]').attr('content')or'' + if not detail:detail=root_text('.content').text()[:200] + play_from,play_url=[],[] + if root_content: + for i,p in enumerate(root_content('.dplayer').items()): + c=p.attr('config') + if not c:continue + try:s=(c.replace('"','"').replace('"','"').replace('&','&').replace('&','&').replace('<','<').replace('<','<').replace('>','>').replace('>','>'));u=(json.loads(s).get('video',{})or{}).get('url','') + except:m=re.search(r'"url"\s*:\s*"([^"]+)"',c);u=m.group(1)if m else'' + if u: + u=u.replace('\\/','/');u=self._abs(u) + play_from.append(f'视频{i+1}');play_url.append(u) + if not play_url: + for pat in[r'https://hls\.[^"\']+\.m3u8[^"\']*',r'https://[^"\']+\.m3u8\?auth_key=[^"\']+',r'//hls\.[^"\']+\.m3u8[^"\']*']: + for u in re.findall(pat,html_text): + u=self._abs(u);play_from.append(f'视频{len(play_from)+1}');play_url.append(u) + if len(play_url)>=3:break + if play_url:break + if not play_url: + js_patterns=[r'video[\s\S]{0,500}?url[\s"\'`:=]+([^"\'`\s]+)',r'videoUrl[\s"\'`:=]+([^"\'`\s]+)',r'src[\s"\'`:=]+([^"\'`\s]+\.m3u8[^"\'`\s]*)'] + for pattern in js_patterns: + js_urls=re.findall(pattern,html_text) + for js_url in js_urls: + if'.m3u8'in js_url: + if js_url.startswith('//'):js_url='https:'+js_url + elif js_url.startswith('/'):js_url='https://heiliao.com'+js_url + elif not js_url.startswith('http'):js_url='https://'+js_url + play_from.append(f'视频{len(play_from)+1}');play_url.append(js_url) + if len(play_url)>=3:break + if play_url:break + if not play_url: + play_from.append('示例视频');play_url.append("https://hls.obmoti.cn/videos5/b9699667fbbffcd464f8874395b91c81/b9699667fbbffcd464f8874395b91c81.m3u8?auth_key=1760372539-68ed273b94e7a-0-3a53bc0df110c5f149b7d374122ef1ed&v=2") + return{'list':[{'vod_id':tid,'vod_name':title,'vod_pic':pic,'vod_content':detail,'vod_play_from':'$$$'.join(play_from),'vod_play_url':'$$$'.join(play_url)}]} + def searchContent(self,key,quick,pg="1"): + rsp=self.fetch(f'https://heiliao.com/index/search?word={key}') + if not rsp:return{'list':[]} + return{'list':self._parse_items(pq(rsp.text))} + def playerContent(self,flag,id,vipFlags): + return{"parse":0,"playUrl":"","url":id,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"}} + def get_list(self,url): + rsp=self.fetch(url) + return[]if not rsp else self._parse_items(pq(rsp.text)) + def fetch(self,url,params=None,cookies=None,headers=None,timeout=5,verify=True,stream=False,allow_redirects=True): + h=headers or{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"} + return super().fetch(url,params=params,cookies=cookies,headers=h,timeout=timeout,verify=verify,stream=stream,allow_redirects=allow_redirects) + def localProxy(self,param): + try: + if param.get('type')=='hlimg': + url=self.d64(param.get('url')) + if url.startswith('//'):url='https:'+url + elif url.startswith('/'):url='https://heiliao.com'+url + r=requests.get(url,headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36","Referer":"https://heiliao.com/"},timeout=15,verify=False) + if r.status_code!=200:return[404,'text/plain',''] + b=AES.new(b'f5d965df75336270',AES.MODE_CBC,b'97b60394abc2fbe1').decrypt(r.content) + ct='image/jpeg' + if b.startswith(b'\x89PNG'):ct='image/png' + elif b.startswith(b'GIF8'):ct='image/gif' + return[200,ct,b] + except:pass + return[404,'text/plain',''] \ No newline at end of file diff --git a/PyramidStore/plugin/adult/黑料弹幕版.py b/PyramidStore/plugin/adult/黑料弹幕版.py new file mode 100644 index 0000000..3e8abde --- /dev/null +++ b/PyramidStore/plugin/adult/黑料弹幕版.py @@ -0,0 +1,444 @@ +# -*- coding: utf-8 -*- +import json, re, sys, base64, requests, threading, time, random, colorsys +from Crypto.Cipher import AES +from pyquery import PyQuery as pq +from urllib.parse import quote, unquote +sys.path.append('..') +from base.spider import Spider + + +class Spider(Spider): + SELECTORS = ['.video-item', '.video-list .item', '.list-item', '.post-item'] + + def init(self, extend='{}'): + """初始化配置(支持代理)""" + config = json.loads(extend) + self.proxies = config.get('proxy', {}) # 示例:{"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"} + self.plp = config.get('plp', '') + pass + + def getName(self): + return "黑料不打烊" + + def homeContent(self, filter): + cateManual = { + "最新黑料": "hlcg", "今日热瓜": "jrrs", "每日TOP10": "mrrb", "反差女友": "fczq", + "校园黑料": "xycg", "网红黑料": "whhl", "明星丑闻": "mxcw", "原创社区": "ycsq", + "推特社区": "ttsq", "社会新闻": "shxw", "官场爆料": "gchl", "影视短剧": "ysdj", + "全球奇闻": "qqqw", "黑料课堂": "hlkt", "每日大赛": "mrds", "激情小说": "jqxs", + "桃图杂志": "ttzz", "深夜综艺": "syzy", "独家爆料": "djbl" + } + return {'class': [{'type_name': k, 'type_id': v} for k, v in cateManual.items()]} + + def homeVideoContent(self): + return {} + + def categoryContent(self, tid, pg, filter, extend): + url = f'https://heiliao.com/{tid}/' if int(pg) == 1 else f'https://heiliao.com/{tid}/page/{pg}/' + videos = self.get_list(url) + return {'list': videos, 'page': pg, 'pagecount': 9999, 'limit': 90, 'total': 999999} + + def fetch_and_decrypt_image(self, url): + try: + if url.startswith('//'): + url = 'https:' + url + elif url.startswith('/'): + url = 'https://heiliao.com' + url + r = requests.get( + url, + headers={ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36', + 'Referer': 'https://heiliao.com/' + }, + timeout=15, + verify=False, + proxies=self.proxies # ✅ 使用代理 + ) + if r.status_code != 200: + return b'' + return AES.new(b'f5d965df75336270', AES.MODE_CBC, b'97b60394abc2fbe1').decrypt(r.content) + except Exception as e: + print(f'[ERROR] fetch_and_decrypt_image: {e}') + return b'' + def _extract_img_from_onload(self, node): + try: + m = re.search(r"load(?:Share)?Img\s*\([^,]+,\s*['\"]([^'\"]+)['\"]", (node.attr('onload') or '')) + return m.group(1) if m else '' + except: + return '' + + def _should_decrypt(self, url: str) -> bool: + u = (url or '').lower() + return any(x in u for x in ['pic.gylhaa.cn', 'new.slfpld.cn', '/upload_01/', '/upload/']) + + def _abs(self, u: str) -> str: + if not u: + return '' + if u.startswith('//'): + return 'https:' + u + if u.startswith('/'): + return 'https://heiliao.com' + u + return u + + def e64(self, s: str) -> str: + try: + return base64.b64encode((s or '').encode()).decode() + except: + return '' + + def d64(self, s: str) -> str: + try: + return base64.b64decode((s or '').encode()).decode() + except: + return '' + + def _img(self, img_node): + u = '' if img_node is None else (img_node.attr('src') or img_node.attr('data-src') or '') + enc = '' if img_node is None else self._extract_img_from_onload(img_node) + t = enc or u + return f"{self.getProxyUrl()}&url={self.e64(t)}&type=hlimg" if t and (enc or self._should_decrypt(t)) else self._abs(t) + + def _parse_items(self, root): + vids = [] + for sel in self.SELECTORS: + for it in root(sel).items(): + title = it.find('.title, h3, h4, .video-title').text() + if not title: + continue + link = it.find('a').attr('href') + if not link: + continue + vids.append({ + 'vod_id': self._abs(link), + 'vod_name': title, + 'vod_pic': self._img(it.find('img')), + 'vod_remarks': it.find('.date, .time, .remarks, .duration').text() or '' + }) + if vids: + break + return vids + + def detailContent(self, array): + tid = array[0] + url = tid if tid.startswith('http') else f'https://heiliao.com{tid}' + rsp = self.fetch(url) + if not rsp: + return {'list': []} + rsp.encoding = 'utf-8' + html_text = rsp.text + try: + root_text = pq(html_text) + except: + root_text = None + try: + root_content = pq(rsp.content) + except: + root_content = None + title = (root_text('title').text() if root_text else '') or '' + if ' - 黑料网' in title: + title = title.replace(' - 黑料网', '') + pic = '' + if root_text: + og = root_text('meta[property="og:image"]').attr('content') + if og and (og.endswith('.png') or og.endswith('.jpg') or og.endswith('.jpeg')): + pic = og + else: + pic = self._img(root_text('.video-item-img img')) + detail = '' + if root_text: + detail = root_text('meta[name="description"]').attr('content') or '' + if not detail: + detail = root_text('.content').text()[:200] + + play_from, play_url = [], [] + if root_content: + for i, p in enumerate(root_content('.dplayer').items()): + c = p.attr('config') + if not c: + continue + try: + s = (c.replace('"', '"') + .replace('"', '"') + .replace('&', '&') + .replace('&', '&') + .replace('<', '<') + .replace('<', '<') + .replace('>', '>') + .replace('>', '>')) + u = (json.loads(s).get('video', {}) or {}).get('url', '') + except: + m = re.search(r'"url"\s*:\s*"([^"]+)"', c) + u = m.group(1) if m else '' + if u: + u = u.replace('\\/', '/') + u = self._abs(u) + article_id = self._extract_article_id(tid) + if article_id: + play_from.append(f'视频{i + 1}') + play_url.append(f"{article_id}_dm_{u}") + else: + play_from.append(f'视频{i + 1}') + play_url.append(u) + if not play_url: + for pat in [ + r'https://hls\.[^"\']+\.m3u8[^"\']*', + r'https://[^"\']+\.m3u8\?auth_key=[^"\']+', + r'//hls\.[^"\']+\.m3u8[^"\']*' + ]: + for u in re.findall(pat, html_text): + u = self._abs(u) + article_id = self._extract_article_id(tid) + if article_id: + play_from.append(f'视频{len(play_from) + 1}') + play_url.append(f"{article_id}_dm_{u}") + else: + play_from.append(f'视频{len(play_from) + 1}') + play_url.append(u) + if len(play_url) >= 3: + break + if play_url: + break + + if not play_url: + js_patterns = [ + r'video[\s\S]{0,500}?url[\s"\'`:=]+([^"\'`\s]+)', + r'videoUrl[\s"\'`:=]+([^"\'`\s]+)', + r'src[\s"\'`:=]+([^"\'`\s]+\.m3u8[^"\'`\s]*)' + ] + for pattern in js_patterns: + js_urls = re.findall(pattern, html_text) + for js_url in js_urls: + if '.m3u8' in js_url: + if js_url.startswith('//'): + js_url = 'https:' + js_url + elif js_url.startswith('/'): + js_url = 'https://heiliao.com' + js_url + elif not js_url.startswith('http'): + js_url = 'https://' + js_url + article_id = self._extract_article_id(tid) + if article_id: + play_from.append(f'视频{len(play_from) + 1}') + play_url.append(f"{article_id}_dm_{js_url}") + else: + play_from.append(f'视频{len(play_from) + 1}') + play_url.append(js_url) + if len(play_url) >= 3: + break + if play_url: + break + + if not play_url: + article_id = self._extract_article_id(tid) + example_url = "https://hls.obmoti.cn/videos5/b9699667fbbffcd464f8874395b91c81/b9699667fbbffcd464f8874395b91c81.m3u8" + if article_id: + play_from.append('示例视频') + play_url.append(f"{article_id}_dm_{example_url}") + else: + play_from.append('示例视频') + play_url.append(example_url) + + return { + 'list': [{ + 'vod_id': tid, + 'vod_name': title, + 'vod_pic': pic, + 'vod_content': detail, + 'vod_play_from': '$$$'.join(play_from), + 'vod_play_url': '$$$'.join(play_url) + }] + } + + def searchContent(self, key, quick, pg="1"): + rsp = self.fetch(f'https://heiliao.com/index/search?word={key}') + if not rsp: + return {'list': []} + return {'list': self._parse_items(pq(rsp.text))} + + def playerContent(self, flag, id, vipFlags): + if '_dm_' in id: + aid, pid = id.split('_dm_', 1) + p = 0 if re.search(r'\.(m3u8|mp4|flv|ts|mkv|mov|avi|webm)', pid) else 1 + if not p: + pid = f"{self.getProxyUrl()}&pdid={quote(id)}&type=m3u8" + return { + 'parse': p, + 'url': pid, + 'header': { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36", + "Referer": "https://heiliao.com/" + } + } + else: + return { + "parse": 0, + "playUrl": "", + "url": id, + "header": { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36", + "Referer": "https://heiliao.com/" + } + } + + def get_list(self, url): + rsp = self.fetch(url) + return [] if not rsp else self._parse_items(pq(rsp.text)) + + def fetch(self, url, params=None, cookies=None, headers=None, timeout=5, verify=True, + stream=False, allow_redirects=True): + h = headers or { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.7049.96 Safari/537.36", + "Referer": "https://heiliao.com/" + } + try: + return requests.get( + url, + params=params, + cookies=cookies, + headers=h, + timeout=timeout, + verify=verify, + allow_redirects=allow_redirects, + proxies=self.proxies # ✅ 全局代理生效 + ) + except Exception as e: + print(f"[ERROR] fetch: {e}") + return None + + # --------------------------- localProxy 与弹幕 --------------------------- # + + def localProxy(self, param): + try: + xtype = param.get('type', '') + if xtype == 'hlimg': + url = self.d64(param.get('url')) + if url.startswith('//'): + url = 'https:' + url + elif url.startswith('/'): + url = 'https://heiliao.com' + url + r = requests.get( + url, + headers={"User-Agent": "Mozilla/5.0", "Referer": "https://heiliao.com/"}, + timeout=15, + verify=False, + proxies=self.proxies + ) + if r.status_code != 200: + return [404, 'text/plain', ''] + b = AES.new(b'f5d965df75336270', AES.MODE_CBC, b'97b60394abc2fbe1').decrypt(r.content) + ct = 'image/jpeg' + if b.startswith(b'\x89PNG'): + ct = 'image/png' + elif b.startswith(b'GIF8'): + ct = 'image/gif' + return [200, ct, b] + + elif xtype == 'm3u8': + path, url = unquote(param['pdid']).split('_dm_', 1) + data = requests.get( + url, + headers={"User-Agent": "Mozilla/5.0", "Referer": "https://heiliao.com/"}, + timeout=10, + proxies=self.proxies + ).text + lines = data.strip().split('\n') + times = 0.0 + for i in lines: + if i.startswith('#EXTINF:'): + times += float(i.split(':')[-1].replace(',', '')) + thread = threading.Thread(target=self.some_background_task, args=(path, int(times))) + thread.start() + print('[INFO] 获取视频时长成功', times) + return [200, 'text/plain', data] + + elif xtype == 'hlxdm': + article_id = param.get('path', '') + times = int(param.get('times', 0)) + comments = self._fetch_heiliao_comments(article_id) + return self._generate_danmaku_xml(comments, times) + except Exception as e: + print(f'[ERROR] localProxy: {e}') + return [404, 'text/plain', ''] + + def _extract_article_id(self, url): + try: + if '/archives/' in url: + match = re.search(r'/archives/(\d+)/?', url) + return match.group(1) if match else None + return None + except: + return None + + def _fetch_heiliao_comments(self, article_id, max_pages=3): + comments = [] + try: + for page in range(1, max_pages + 1): + url = f"https://heiliao.com/comments/1/{article_id}/{page}.json" + resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10, proxies=self.proxies) + if resp.status_code == 200: + data = resp.json() + if 'data' in data and 'list' in data['data'] and data['data']['list']: + for comment in data['data']['list']: + text = comment.get('content', '').strip() + if text and len(text) <= 100: + comments.append(text) + if 'comments' in comment and 'list' in comment['comments'] and comment['comments']['list']: + for reply in comment['comments']['list']: + reply_text = reply.get('content', '').strip() + if reply_text and len(reply_text) <= 100: + comments.append(reply_text) + if not data['data'].get('next', False): + break + else: + break + else: + break + except Exception as e: + print(f'[ERROR] _fetch_heiliao_comments: {e}') + return comments[:50] + + def _generate_danmaku_xml(self, comments, video_duration): + try: + total_comments = len(comments) + tsrt = f'共有{total_comments}条弹幕来袭!!!' + danmu_xml = f'\n\n' + danmu_xml += '\tchat.heiliao.com\n\t88888888\n' + danmu_xml += '\t0\n\t99999\n\t0\n' + danmu_xml += '\t0\n\theiliao\n' + danmu_xml += f'\t{tsrt}\n' + for i, comment in enumerate(comments): + base_time = (i / total_comments) * video_duration if total_comments > 0 else 0 + dm_time = round(max(0, min(base_time + random.uniform(-3, 3), video_duration)), 1) + dm_color = self._get_danmaku_color() + dm_text = re.sub(r'[<>&\u0000\b]', '', comment) + danmu_xml += f'\t{dm_text}\n' + danmu_xml += '' + return [200, "text/xml", danmu_xml] + except Exception as e: + print(f'[ERROR] _generate_danmaku_xml: {e}') + return [500, 'text/html', ''] + + def _get_danmaku_color(self): + if random.random() < 0.1: + h = random.random() + s = random.uniform(0.7, 1.0) + v = random.uniform(0.8, 1.0) + r, g, b = colorsys.hsv_to_rgb(h, s, v) + r = int(r * 255) + g = int(g * 255) + b = int(b * 255) + return str((r << 16) + (g << 8) + b) + else: + return '16777215' + + def some_background_task(self, article_id, video_duration): + try: + time.sleep(1) + danmaku_url = f"{self.getProxyUrl()}&path={quote(article_id)}×={video_duration}&type=hlxdm" + self.fetch(f"http://127.0.0.1:9978/action?do=refresh&type=danmaku&path={quote(danmaku_url)}") + print(f'[INFO] 弹幕刷新成功: {article_id}') + except Exception as e: + print(f'[ERROR] some_background_task: {e}') diff --git a/adult.json b/adult.json index dc070c5..eb80326 100644 --- a/adult.json +++ b/adult.json @@ -1243,6 +1243,24 @@ "quickSearch": 1, "filterable": 1 }, + { + "key": "黑料不打烊", + "name": "黑料不打烊", + "type": 3, + "api": "./PyramidStore/plugin/adult/黑料弹幕版.py", + "searchable": 1, + "quickSearch": 1, + "filterable": 1 + }, + { + "key": "今日看料", + "name": "今日看料", + "type": 3, + "api": "./PyramidStore/plugin/adult/今日看料.py", + "searchable": 1, + "quickSearch": 1, + "filterable": 1 + }, { "key": "花都", "name": "花都",