]*>(.*?)',html,re.S) + post=post_m.group(1) if post_m else '' + + # 提取图片 + img_urls=[] + for m in re.finditer(r"data-pic=['\"]([^'\"]+)['\"]",post): + u=m.group(1).strip() + if u and '/XPic/' not in u and '/images/' not in u and 'banner' not in u.lower(): + img_urls.append(self._clean_pic(u)) + + # 提取视频 + video_url='' + dp=re.search(r"data-config='([^']+)'",html) + if dp: + config_str=dp.group(1).replace('"','"').replace('&','&') + u2=re.search(r'"url2":"((?:[^"\\]|\\.)*)"',config_str) + if u2: + raw=u2.group(1).replace('\\/','/') + if raw and raw.startswith('http'):video_url=raw + if not video_url: + u1=re.search(r'"url":"((?:[^"\\]|\\.)*)"',config_str) + if u1: + raw=u1.group(1).replace('\\/','/') + if raw and raw.startswith('http'):video_url=raw + + # 封面图:取第一张内容图 + pic=img_urls[0] if img_urls else self._clean_pic('/usr/themes/Mirages/images/logo.webp') + + # 构建播放信息 + from_names=[] + ep_parts=[] + if img_urls: + from_names.append('图文') + ep_parts.append(f'图片$pics://{"&&".join(img_urls)}') + if video_url: + from_names.append('视频') + ep_parts.append(f'播放${video_url}') + + if from_names: + vod={"vod_id":id,"vod_name":title,"vod_remarks":"","vod_pic":pic,"vod_content":title[:500],"vod_play_from":"$$$".join(from_names),"vod_play_url":"$$$".join(ep_parts)} + else: + c=re.sub(r'<[^>]+>',' ',post).strip()[:500] + vod={"vod_id":id,"vod_name":title,"vod_remarks":"","vod_pic":pic,"vod_content":c,"vod_play_from":"文字","vod_play_url":f"阅读${c[:300]}"} + return {"list":[vod]} + except: + return {"list":[]} + + def searchContent(self,key,quick,pg="1"): + try: + page=int(pg) if pg else 1 + url=f"{self.host}/search/{quote(key)}/" if page==1 else f"{self.host}/search/{quote(key)}/page/{page}/" + html=self._get(url) + return {"list":self._parse_list(html),"page":page} + except: + return {"list":[],"page":int(pg) if pg else 1} + + def playerContent(self,flag,id,vipFlags): + try: + if flag=='图文' or id.startswith('pics://'): + return {"parse":0,"playUrl":"","url":id.replace('图片',''),"header":self.headers,"position":"0"} + if flag=='视频' or '播放' in id: + url=id.replace('播放','') + if '$$$' in url:url=url.split('$$$')[-1] + return {"parse":0,"url":url,"header":self.headers,"position":"0"} + if flag=='文字' or '阅读' in id: + content=id.replace('阅读','') + if '$$$' in content:content=content.split('$$$')[-1] + nj=json.dumps({"title":content[:50],"content":content},ensure_ascii=False) + return {"parse":0,"url":f"novel://{nj}","header":"","vod_player":"书","position":"0"} + if id.startswith('pics://'): + return {"parse":0,"playUrl":"","url":id,"header":self.headers,"position":"0"} + if id.startswith('http'): + return {"parse":0,"url":id,"header":self.headers,"position":"0"} + return {"parse":0,"url":id,"header":self.headers,"position":"0"} + except: + return {"parse":0,"url":"","position":"0"} diff --git a/yinshiyuan18/tv/py/hongguo.py b/yinshiyuan18/tv/py/hongguo.py new file mode 100644 index 00000000..3d1e5724 --- /dev/null +++ b/yinshiyuan18/tv/py/hongguo.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +""" +红果短剧 TVBox Python 源。 + +extend 可填写播放桥地址,例如: + http://192.168.1.4:9979 +或 JSON: + {"bridge":"http://192.168.1.4:9979"} +""" +import json +import re +import sys +import time +from urllib.parse import quote, urlencode + +import requests + +sys.path.append("../../") +try: + from base.spider import Spider +except ImportError: + class Spider: + pass + + +class Spider(Spider): + site = "https://hongguoduanju.com" + headers = { + "User-Agent": ( + "Mozilla/5.0 (Linux; Android 12; TV) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/126.0 Safari/537.36" + ), + "Accept-Language": "zh-CN,zh;q=0.9", + } + category_map = { + "热门": "sort_type=1", + "最新": "sort_type=2", + "都市": "background=cate_1", + "现代": "background=cate_757", + "古代": "background=cate_758", + "乡村": "background=cate_11", + "职场": "background=cate_127", + "校园": "background=cate_4", + "悬疑": "topic=cate_165", + "喜剧": "topic=cate_303", + "重生": "setting=cate_36", + "穿越": "setting=cate_37", + } + + def __init__(self): + self.bridge = "http://192.168.1.4:9979" + self._cache = {} + + def getName(self): + return "红果短剧" + + def init(self, extend=""): + if isinstance(extend, dict): + self.bridge = str(extend.get("bridge") or self.bridge).rstrip("/") + elif extend: + text = str(extend).strip() + try: + data = json.loads(text) + self.bridge = str(data.get("bridge") or self.bridge).rstrip("/") + except Exception: + if text.startswith("http"): + self.bridge = text.rstrip("/") + + def isVideoFormat(self, url): + return False + + def manualVideoCheck(self): + return False + + def destroy(self): + return + + def _get(self, url): + response = requests.get(url, headers=self.headers, timeout=25) + response.raise_for_status() + response.encoding = "utf-8" + return response.text + + def _router_data(self, url): + cached = self._cache.get(url) + if cached and time.time() - cached[0] < 300: + return cached[1] + html = self._get(url) + match = re.search( + r"window\._ROUTER_DATA\s*=\s*(\{.*?\})\s*", + html, + re.S, + ) + if not match: + raise RuntimeError("页面数据格式已变化") + data = json.loads(match.group(1)) + self._cache[url] = (time.time(), data) + return data + + @staticmethod + def _vod(item): + tags = item.get("tags") or [] + if isinstance(tags, list): + tags = " · ".join(str(x) for x in tags[:3]) + count = item.get("episode_cnt") or len(item.get("vid_list") or []) + remark = ("全%s集" % count) if count else str(tags or "") + return { + "vod_id": str(item.get("series_id") or ""), + "vod_name": str(item.get("series_name") or ""), + "vod_pic": str(item.get("series_cover") or ""), + "vod_remarks": remark, + } + + def _category_items(self, query): + url = self.site + "/category?" + query + data = self._router_data(url) + page = data.get("loaderData", {}).get("category_page", {}) + items = page.get("recommendList") or [] + if not items: + items = page.get("categoryData", {}).get("recommendList") or [] + seen = set() + result = [] + for item in items: + sid = str(item.get("series_id") or "") + if sid and sid not in seen: + seen.add(sid) + result.append(item) + return result + + def homeContent(self, filter): + return { + "class": [ + {"type_name": name, "type_id": query} + for name, query in self.category_map.items() + ], + "list": self.homeVideoContent().get("list", []), + } + + def homeVideoContent(self): + try: + items = self._category_items("sort_type=1")[:30] + return {"list": [self._vod(x) for x in items]} + except Exception as exc: + print("红果首页读取失败:", exc) + return {"list": []} + + def categoryContent(self, tid, pg, filter, extend): + page = max(1, int(pg or 1)) + per_page = 30 + try: + items = self._category_items(str(tid)) + start = (page - 1) * per_page + chunk = items[start:start + per_page] + page_count = max(1, (len(items) + per_page - 1) // per_page) + return { + "list": [self._vod(x) for x in chunk], + "page": page, + "pagecount": page_count, + "limit": per_page, + "total": len(items), + } + except Exception as exc: + print("红果分类读取失败:", exc) + return {"list": [], "page": page, "pagecount": page} + + def detailContent(self, ids): + series_id = str(ids[0]) + url = self.site + "/detail?series_id=" + quote(series_id) + try: + data = self._router_data(url) + detail = data.get("loaderData", {}).get("detail_page", {}) + series = detail.get("seriesDetail") or {} + vids = series.get("vid_list") or [] + episodes = [ + "第%d集$%s" % (index + 1, vid) + for index, vid in enumerate(vids) + if str(vid) + ] + tags = series.get("tags") or [] + if isinstance(tags, list): + tags = ",".join(str(x) for x in tags) + vod = { + "vod_id": series_id, + "vod_name": str(series.get("series_name") or "红果短剧"), + "vod_pic": str(series.get("series_cover") or ""), + "type_name": str(tags), + "vod_remarks": "全%s集" % (series.get("episode_cnt") or len(vids)), + "vod_content": str(series.get("series_intro") or ""), + "vod_play_from": "红果", + "vod_play_url": "#".join(episodes), + } + return {"list": [vod]} + except Exception as exc: + print("红果详情读取失败:", exc) + return {"list": []} + + def searchContent(self, key, quick, pg=1): + page = max(1, int(pg or 1)) + per_page = 30 + try: + items = self._category_items("sort_type=1") + keyword = str(key).strip().lower() + matches = [ + x for x in items + if keyword in str(x.get("series_name") or "").lower() + or keyword in str(x.get("series_intro") or "").lower() + ] + start = (page - 1) * per_page + return { + "list": [self._vod(x) for x in matches[start:start + per_page]], + "page": page, + } + except Exception as exc: + print("红果搜索失败:", exc) + return {"list": [], "page": page} + + def searchContentPage(self, key, quick, pg=1): + return self.searchContent(key, quick, pg) + + def playerContent(self, flag, pid, vipFlags): + url = self.bridge + "/play?" + urlencode({"vid": str(pid)}) + return { + "parse": 0, + "playUrl": "", + "url": url, + "header": { + "User-Agent": self.headers["User-Agent"], + "Referer": self.site + "/", + }, + } + + def localProxy(self, params): + return None diff --git a/yinshiyuan18/tv/py/ininAV.py b/yinshiyuan18/tv/py/ininAV.py new file mode 100644 index 00000000..6e38f265 --- /dev/null +++ b/yinshiyuan18/tv/py/ininAV.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +import re +from bs4 import BeautifulSoup +from base.spider import Spider + +class Spider(Spider): + def getName(self): + return "硬硬AV" + + def init(self, extend=""): + self.host = "https://ininav.com" + self.header = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "Referer": self.host + } + + def homeContent(self, filter): + # 根據 HTML 導航欄手動定義,確保分類標籤絕對不為空 + classes = [ + {"type_name": "中文字幕", "type_id": "/chinese-subtitles"}, + {"type_name": "亞洲國產", "type_id": "/asian/new"}, + {"type_name": "日本AV", "type_id": "/japanese/new"}, + {"type_name": "無碼流出", "type_id": "/uncensored-leak/new"}, + {"type_name": "FC2素人", "type_id": "/fc2/new"}, + {"type_name": "歐美系列", "type_id": "/western/new"}, + {"type_name": "韓國直播", "type_id": "/korean/new"}, + {"type_name": "裏番動漫", "type_id": "/hentai/new"} + ] + return {'class': classes} + + def homeVideoContent(self): + # 首頁推薦內容 + return self.categoryContent('/asian/new', 1, None, None) + + def categoryContent(self, tid, pg, filter, extend): + result = {} + page = int(pg) - 1 + url = f"{self.host}{tid}?page={page}" + + try: + # 加入 verify=False 解決部分盒子 SSL 握手失敗導致的空列表 + rsp = self.fetch(url, headers=self.header) + soup = BeautifulSoup(rsp.text, 'html.parser') + videos = [] + + # 根據 HTML 結構精準定位影片卡片容器 + items = soup.select('.views-view-grid .cf, .views-row, article') + + for item in items: + # 1. 抓取標題與 ID (對齊 ) + a_tag = item.select_one('a[href*="/video/"]') + if not a_tag: continue + + # 標題通常在 的 alt 或者 標籤內 + vod_id = a_tag['href'] + + # 2. 抓取圖片 (針對 標籤優化) + pic = "" + # 優先抓取 source 裡的 webp 地址 + source = item.select_one('source[type="image/webp"]') + if source and source.get('srcset'): + pic = source.get('srcset').split(' ')[0] + + # 如果沒有 source,抓取 img 標籤 + if not pic: + img = item.select_one('img') + if img: + pic = img.get('src') or img.get('data-src') or "" + + # 處理相對路徑 + if pic and pic.startswith('/'): + pic = self.host + pic + + # 3. 抓取標題 (如果 a_tag 內沒有文字,從 img alt 補全) + name = a_tag.get_text(strip=True) + if not name and item.select_one('img'): + name = item.select_one('img').get('alt', '') + if not name: name = "高清影片" + + videos.append({ + "vod_id": vod_id, + "vod_name": name, + "vod_pic": pic, + "vod_remarks": "" + }) + + result['list'] = videos + result['page'] = pg + result['pagecount'] = 99 + except: + result['list'] = [] + + return result + + def detailContent(self, array): + tid = array[0] + url = self.host + tid + rsp = self.fetch(url, headers=self.header) + html = rsp.text + + # 標題提取 + title = "" + t_match = re.search(r']*>(.*?)', html) + title = t_match.group(1).strip() if t_match else "影片詳情" + + # 圖片提取 + pic = "" + p_match = re.search(r'property="og:image:url"\s+content="(.*?)"', html) + pic = p_match.group(1) if p_match else "" + + # 核心:直接掃描源碼中的 m3u8 地址 + m3u8_links = re.findall(r'https?://[^\s"\'<>]+?index\.m3u8', html) + m3u8_links = list(dict.fromkeys(m3u8_links)) # 去重 + + play_list = [] + for i, link in enumerate(m3u8_links): + play_list.append(f"線路{i+1}${link}") + + vod = { + "vod_id": tid, + "vod_name": title, + "vod_pic": pic, + "type_name": "硬硬精選", + "vod_play_from": "硬硬雲", + "vod_play_url": "#".join(play_list) if play_list else "未找到資源$null" + } + return {"list": [vod]} + + def searchContent(self, key, quick, pg=1): + result = {} + # 該站分頁從 0 開始,所以 pg=1 對應 page=0 + page = int(pg) - 1 + # 對齊 URL 格式: /search?fulltext=關鍵字&page=頁碼 + url = f"{self.host}/search?fulltext={key}&page={page}" + + try: + rsp = self.fetch(url, headers=self.header) + soup = BeautifulSoup(rsp.text, 'html.parser') + videos = [] + + # 搜尋結果的結構通常與分類頁一致,使用相同的選擇器 + items = soup.select('.views-view-grid .cf, .views-row, article, .search-result') + + for item in items: + # 提取連結與 ID + a_tag = item.select_one('a[href*="/video/"]') + if not a_tag: continue + + vod_id = a_tag['href'] + + # 提取圖片 (優先處理 picture/source 組合) + pic = "" + source = item.select_one('source[type="image/webp"]') + if source and source.get('srcset'): + pic = source.get('srcset').split(' ')[0] + + if not pic: + img = item.select_one('img') + if img: + pic = img.get('src') or img.get('data-src') or "" + + if pic and pic.startswith('/'): + pic = self.host + pic + + # 提取標題 + name = a_tag.get_text(strip=True) + if not name and item.select_one('img'): + name = item.select_one('img').get('alt', '') + if not name: name = key # 若無標題則顯示關鍵字 + + videos.append({ + "vod_id": vod_id, + "vod_name": name, + "vod_pic": pic, + "vod_remarks": "" + }) + + result['list'] = videos + result['page'] = pg + result['pagecount'] = 99 + except Exception: + result['list'] = [] + + return result + + def playerContent(self, flag, id, vipFlags): + return { + "parse": 0, + "url": id, + "header": self.header + } \ No newline at end of file diff --git a/yinshiyuan18/tv/py/jab (1).py b/yinshiyuan18/tv/py/jab (1).py index dfcdac37..de6e6134 100644 --- a/yinshiyuan18/tv/py/jab (1).py +++ b/yinshiyuan18/tv/py/jab (1).py @@ -26,7 +26,7 @@ class Spider(Spider): except Exception as e: print(f"代理配置解析错误: {str(e)}") - self.site_url = "https://jable.tv" + self.site_url = "https://fs1.app" self.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', diff --git a/yinshiyuan18/tv/py/jabie (3).py b/yinshiyuan18/tv/py/jabie (3).py index 55784c93..9f3657d2 100644 --- a/yinshiyuan18/tv/py/jabie (3).py +++ b/yinshiyuan18/tv/py/jabie (3).py @@ -14,7 +14,7 @@ from base.spider import Spider class Spider(Spider): def init(self, extend=""): - self.site_url = "https://jable.tv" + self.site_url = "https://fs1.app" self.headers = { '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', diff --git a/yinshiyuan18/tv/py/jable.py b/yinshiyuan18/tv/py/jable.py index fb1bb6a8..011daa5c 100644 --- a/yinshiyuan18/tv/py/jable.py +++ b/yinshiyuan18/tv/py/jable.py @@ -1,132 +1,161 @@ -import sys, re, requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry -from base.spider import Spider +# -*- coding: utf-8 -*- +import re +import urllib.parse +import requests -requests.packages.urllib3.disable_warnings() +try: + from base.spider import Spider as BaseSpider +except ImportError: + class BaseSpider: + pass -class Spider(Spider): - def getName(self): return "Jable" +class Spider(BaseSpider): + BASE_URL = "https://jable.sbs" + FALLBACK_URLS = ["https://jable.sbs", "https://jable.tv"] + HEADERS = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Referer": BASE_URL + "/", + } - def init(self, extend=""): - self.siteUrl = "https://jable.tv" - self.headers = { - "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/avif,image/webp,image/apng,*/*;q=0.8", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", - "Referer": "https://jable.tv/", - } - self.sess = requests.Session() - self.sess.mount('https://', HTTPAdapter(max_retries=Retry(total=3, status_forcelist=[500, 502, 503, 504]))) + def __init__(self): + super().__init__() + self.name = "JableTV" + self.session = requests.Session() + self.session.headers.update(self.HEADERS) + self._class_cache = None - def fetch(self, url): - try: return self.sess.get(url, headers=self.headers, timeout=15, verify=False) - except: return None + def init(self, extend="{}"): + return None + + def getName(self): + return self.name def homeContent(self, filter): - r = self.fetch(self.siteUrl) - cats = [] - if r and r.ok: - # 动态抓取首页的所有 tag 作为分类 - for m in re.finditer(r']+class=["\']tag["\'][^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)', r.text, re.I): - url_path = m.group(1) - name = re.sub(r'<[^>]+>', '', m.group(2)).strip() - tid = url_path.strip('/') - if tid and name and tid not in [c['type_id'] for c in cats]: - cats.append({"type_id": tid, "type_name": name}) - - # 兜底静态分类 - if not cats: - cats = [ - {"type_id": "latest-updates", "type_name": "最近更新"}, - {"type_id": "hot", "type_name": "热门影片"}, - {"type_id": "categories/bdsm", "type_name": "主奴调教"}, - {"type_id": "categories/sex-only", "type_name": "直接开啪"}, - {"type_id": "categories/chinese-subtitle", "type_name": "中文字幕"}, - {"type_id": "categories/insult", "type_name": "凌辱快感"}, - {"type_id": "categories/uniform", "type_name": "制服诱惑"}, - {"type_id": "categories/roleplay", "type_name": "角色剧情"}, - {"type_id": "categories/private-cam", "type_name": "盗摄偷拍"}, - {"type_id": "categories/uncensored", "type_name": "无码破解"}, - {"type_id": "categories/pov", "type_name": "男友视角"}, - {"type_id": "categories/groupsex", "type_name": "多P群交"}, - {"type_id": "categories/pantyhose", "type_name": "丝袜美腿"}, - {"type_id": "categories/lesbian", "type_name": "女同欢愉"} - ] - return {'class': cats} + html = self._get(self.BASE_URL + "/latest-updates/") + return {"class": self._classes(), "filters": {}, "list": self._parse_list(html), "parse": 0, "jx": 0} + + def homeVideoContent(self): + return {"list": self._parse_list(self._get(self.BASE_URL + "/latest-updates/"))} def categoryContent(self, tid, pg, filter, extend): - url = f"{self.siteUrl}/{tid}/{pg}/" if str(pg) != '1' else f"{self.siteUrl}/{tid}/" - return self.postList(url, int(pg)) - - def searchContent(self, key, quick, pg=1): - url = f"{self.siteUrl}/search/{key}/{pg}/" if str(pg) != '1' else f"{self.siteUrl}/search/{key}/" - return self.postList(url, int(pg)) - - def postList(self, url, pg): - r = self.fetch(url) - l = [] - if r and r.ok: - blocks = r.text.split('
]*>\s*]*>(.*?)', block, re.S) - t = re.sub(r'<[^>]+>', '', title_match.group(1)).strip() if title_match else "未知" - - pic_match = re.search(r'data-src=["\']([^"\']+)["\']', block) or re.search(r'src=["\']([^"\']+)["\']', block) - p = pic_match.group(1) if pic_match else "" - - u = u if u.startswith("http") else f"{self.siteUrl}/{u.lstrip('/')}" - - l.append({ - # 传参大法:直接携带标题和图片给详情页 - 'vod_id': f"{u}@@@{t}@@@{p}", - 'vod_name': t, - 'vod_pic': p, - 'vod_remarks': '1080P', - 'style': {"type": "rect", "ratio": 1.33} - }) - return {'list': l, 'page': pg, 'pagecount': pg + 1 if len(l) else pg, 'limit': 24, 'total': 9999} + page = self._to_int(pg, 1) + path = str(tid or "latest-updates").strip("/") + url = self.BASE_URL + "/" + path + "/" if page <= 1 else self.BASE_URL + "/" + path + "/" + str(page) + "/" + data = self._parse_list(self._get(url)) + return {"page": page, "pagecount": page if len(data) < 10 else page + 1, "limit": 24, "total": 99999, "list": data, "parse": 0, "jx": 0} def detailContent(self, ids): - vid = ids[0] - name, pic = "未知", "" - - # 还原传参 - if "@@@" in vid: - parts = vid.split("@@@") - vid = parts[0] - name = parts[1] if len(parts) > 1 else name - pic = parts[2] if len(parts) > 2 else pic + result = {"list": [], "parse": 0, "jx": 0} + if not ids: + return result + url = self._fix_url(ids[0] if str(ids[0]).startswith("http") else self.BASE_URL + "/videos/" + str(ids[0]).strip("/") + "/") + html = self._get(url) + name = self._clean(self._match(html, r']*>(.*?)') or self._match(html, r']+property=["\']og:title["\'][^>]+content=["\']([^"\']+)') or self._match(html, r'(.*?)').split("-")[0]) + pic = self._match(html, r']+property=["\']og:image["\'][^>]+content=["\']([^"\']+)') or self._match(html, r']+poster=["\']([^"\']+)') or self._match(html, r']+(?:data-src|src)=["\']([^"\']+)') + tags = ",".join([self._clean(x) for x in re.findall(r']+href=["\'][^"\']*/tags/[^"\']+["\'][^>]*>(.*?)', html, re.S)]) + remarks = self._clean(" ".join(re.findall(r']*>(.*?)', html, re.S)[:3])) + content = self._clean(self._match(html, r']+class=["\'][^"\']*(?:description|info|text)[^"\']*["\'][^>]*>(.*?)
') or remarks or name) + m3u8 = self._m3u8(html) + result["list"].append({"vod_id": url, "vod_name": name, "vod_pic": urllib.parse.urljoin(self.BASE_URL, pic), "type_name": tags, "vod_year": "", "vod_area": "", "vod_remarks": remarks, "vod_actor": tags, "vod_director": "", "vod_content": content, "vod_play_from": "Jable", "vod_play_url": "正片$" + (m3u8 or url)}) + return result - # 获取详情页,通过正则直接抓取 m3u8 直链 - r = self.fetch(vid) - m3u8_url = "" - if r and r.ok: - m_m3u8 = re.search(r"https?://[^\s'\" ]+\.m3u8", r.text) - if m_m3u8: m3u8_url = m_m3u8.group(0) - - vod = { - 'vod_id': ids[0], - 'vod_name': name, - 'vod_pic': pic, - 'type_name': '视频', - 'vod_play_from': 'Jable', - # 如果成功获取直链,则传直链,否则传详情页URL让 playerContent 兜底 - 'vod_play_url': f"播放${m3u8_url}" if m3u8_url else f"播放${vid}" - } - return {'list': [vod]} + def searchContent(self, key, quick, pg="1"): + page = self._to_int(pg, 1) + q = urllib.parse.quote(str(key)) + url = self.BASE_URL + "/search/" + q + "/" if page <= 1 else self.BASE_URL + "/search/" + q + "/" + str(page) + "/" + data = self._parse_list(self._get(url)) + return {"page": page, "pagecount": page if len(data) < 10 else page + 1, "limit": 24, "total": 99999, "list": data, "parse": 0, "jx": 0} def playerContent(self, flag, id, vipFlags): - # 由于 detailContent 已经解析出了真实的 m3u8,这里直接返回交给底层播放器即可 - return { - "parse": 0, - "url": id, - "header": { - "Referer": "https://jable.tv/", - "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" - } - } + result = {"parse": 0, "playUrl": "", "url": id or "", "jx": 0, "header": {"User-Agent": self.HEADERS["User-Agent"], "Referer": self.BASE_URL + "/"}} + if not id: + return result + if ".m3u8" in id or ".mp4" in id: + return result + play_page = self._fix_url(id if str(id).startswith("http") else self.BASE_URL + "/videos/" + str(id).strip("/") + "/") + html = self._get(play_page) + m3u8 = self._m3u8(html) + if m3u8: + result["url"] = m3u8 + result["header"] = {"User-Agent": self.HEADERS["User-Agent"], "Referer": play_page, "Origin": self.BASE_URL} + else: + result["url"] = play_page + result["parse"] = 1 + return result + + def _classes(self, html=None): + if self._class_cache: + return self._class_cache + self._class_cache = [ + {"type_id": "latest-updates", "type_name": "最近更新"}, + {"type_id": "hot", "type_name": "热门影片"}, + {"type_id": "new-release", "type_name": "全新上市"}, + {"type_id": "tags/chinese-subtitle", "type_name": "中文字幕"}, + {"type_id": "tags/drama", "type_name": "剧情"}, + {"type_id": "tags/cosplay", "type_name": "角色扮演"}, + ] + return self._class_cache + + def _parse_list(self, html): + data, seen = [], set() + cards = re.findall(r'(]+class=["\'][^"\']*video-img-box[^"\']*["\'][\s\S]*?[\s\S]*?