diff --git a/FGBLH/Web鱼壳海豚.json b/FGBLH/Web鱼壳海豚.json
index 778a2287..69d6f68c 100644
--- a/FGBLH/Web鱼壳海豚.json
+++ b/FGBLH/Web鱼壳海豚.json
@@ -808,6 +808,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
+ {
+ "key": "猫咪TV",
+ "name": "🐬猫咪TV.py|🔞[成人]",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪TV.py"
+ },
{
"key": "BadNews",
"name": "🐬BadNews.py|🔞[成人](挂梯)",
@@ -928,6 +934,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/8X8X.py"
},
+ {
+ "key": "Jable",
+ "name": "🐬Jable.py|🔞[成人]",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
+ },
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞[成人]",
diff --git a/FGBLH/Web鱼壳海豚py.json b/FGBLH/Web鱼壳海豚py.json
index de45a594..bc31699c 100644
--- a/FGBLH/Web鱼壳海豚py.json
+++ b/FGBLH/Web鱼壳海豚py.json
@@ -551,6 +551,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/采集聚合成人.py"
},
+ {
+ "key": "猫咪TV",
+ "name": "🐬猫咪TV.py|🔞[成人]",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪TV.py"
+ },
{
"key": "樱花传媒",
"name": "🐬樱花传媒.py|🔞[成人]",
@@ -671,6 +677,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/8X8X.py"
},
+ {
+ "key": "Jable",
+ "name": "🐬Jable.py|🔞[成人]",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
+ },
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞[成人]",
diff --git a/FGBLH/ok海豚.json b/FGBLH/ok海豚.json
index 31e738e9..a077e7ff 100644
--- a/FGBLH/ok海豚.json
+++ b/FGBLH/ok海豚.json
@@ -747,6 +747,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py"
},
+ {
+ "key": "猫咪TV",
+ "name": "🐬猫咪TV.py|🔞",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪TV.py"
+ },
{
"key": "樱花传媒",
"name": "🐬樱花传媒.py|🔞",
@@ -867,6 +873,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/8X8X.py"
},
+ {
+ "key": "Jable",
+ "name": "🐬Jable.py|🔞",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
+ },
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞",
diff --git a/FGBLH/ok海豚py.json b/FGBLH/ok海豚py.json
index f01fcb44..065c7af1 100644
--- a/FGBLH/ok海豚py.json
+++ b/FGBLH/ok海豚py.json
@@ -1,5 +1,5 @@
{
- "spider": "./tvbox.jar",
+ "spider": "",
"logo": "https://img.freepik.com/free-vector/cute-dolphin-swimming-cartoon-vector-icon-illustration-animal-nature-icon-isolated-flat-vector_138676-12582.jpg?semt=ais_hybrid&w=740&q=80",
"wallpaper":"http://tool.teyonds.com/api",
"warningText": "注意:如果别人倒卖海豚影视接口收费的都是骗子,没有qq群微信群,只有tg官方交流群 TG:@hshsjk",
@@ -580,6 +580,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/蜜桃视频.py"
},
+ {
+ "key": "猫咪TV",
+ "name": "🐬猫咪TV.py|🔞",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/猫咪TV.py"
+ },
{
"key": "樱花传媒",
"name": "🐬樱花传媒.py|🔞",
@@ -700,6 +706,12 @@
"type": 3,
"api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/8X8X.py"
},
+ {
+ "key": "Jable",
+ "name": "🐬Jable.py|🔞",
+ "type": 3,
+ "api": "https://ghfast.top/https://raw.githubusercontent.com/FGBLH/HKL/refs/heads/main/py/Jable.py"
+ },
{
"key": "Xvideos",
"name": "🐬Xvideos.py|🔞",
diff --git a/FGBLH/py/Jable.py b/FGBLH/py/Jable.py
new file mode 100644
index 00000000..011daa5c
--- /dev/null
+++ b/FGBLH/py/Jable.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+import re
+import urllib.parse
+import requests
+
+try:
+ from base.spider import Spider as BaseSpider
+except ImportError:
+ class BaseSpider:
+ pass
+
+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):
+ super().__init__()
+ self.name = "JableTV"
+ self.session = requests.Session()
+ self.session.headers.update(self.HEADERS)
+ self._class_cache = None
+
+ def init(self, extend="{}"):
+ return None
+
+ def getName(self):
+ return self.name
+
+ def homeContent(self, filter):
+ 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):
+ 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):
+ 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
+
+ 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):
+ 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]*?
\s*)', html or "", re.S | re.I)
+ if not cards:
+ cards = re.findall(r'(]+href=["\'][^"\']*/videos/[^"\']+["\'][\s\S]*? )', html or "", re.S | re.I)
+ for item in cards:
+ href = self._match(item, r'href=["\']([^"\']*/videos/[^"\']+)["\']')
+ if not href:
+ continue
+ name = self._clean(self._match(item, r']*class=["\'][^"\']*title[^"\']*["\'][^>]*>\s*]*>(.*?) ') or self._match(item, r'title=["\']([^"\']+)') or self._match(item, r'alt=["\']([^"\']+)'))
+ pic = self._match(item, r'(?:data-src|data-original|data-lazy-src|data-lazyload)=["\']([^"\']+)') or self._match(item, r' ]+src=["\']([^"\']+)')
+ remarks = self._clean(self._match(item, r']+class=["\'][^"\']*(?:duration|label|badge)[^"\']*["\'][^>]*>(.*?) ') or self._match(item, r'(\d{1,2}:\d{2}(?::\d{2})?)'))
+ full = self._fix_url(urllib.parse.urljoin(self.BASE_URL, href))
+ if full not in seen and name and not re.fullmatch(r'\d{1,2}:\d{2}(?::\d{2})?', name):
+ seen.add(full)
+ data.append({"vod_id": full, "vod_name": name, "vod_pic": urllib.parse.urljoin(self.BASE_URL, pic), "vod_remarks": remarks})
+ return data
+
+ def _get(self, url, headers=None):
+ for real in self._candidate_urls(self._fix_url(url)):
+ h = dict(self.HEADERS)
+ h["Referer"] = self.BASE_URL + "/"
+ if headers:
+ h.update(headers)
+ try:
+ r = self.session.get(real, headers=h, timeout=15, verify=False)
+ r.encoding = "utf-8"
+ if r.status_code < 400 and "Just a moment" not in r.text and "cf-browser-verification" not in r.text:
+ return r.text
+ except Exception:
+ continue
+ return ""
+
+ def _candidate_urls(self, url):
+ urls = [url]
+ for host in self.FALLBACK_URLS:
+ p = urllib.parse.urlparse(url)
+ if p.netloc and host not in url:
+ urls.append(host + p.path + ("?" + p.query if p.query else ""))
+ return list(dict.fromkeys(urls))
+
+ def _fix_url(self, url):
+ return str(url or "").replace("https://jable.tv", self.BASE_URL).replace("http://jable.tv", self.BASE_URL).replace("https://www.jable.tv", self.BASE_URL)
+
+ def _m3u8(self, html):
+ return self._match(html, r'var\s+hlsUrl\s*=\s*["\']([^"\']+\.m3u8[^"\']*)') or self._match(html, r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']')
+
+ def _match(self, text, pattern):
+ m = re.search(pattern, text or "", re.S | re.I)
+ return m.group(1).strip() if m else ""
+
+ def _clean(self, text):
+ text = re.sub(r'<.*?>', '', text or '')
+ text = text.replace(' ', ' ').replace('&', '&').replace('&', '&').replace('"', '"')
+ return re.sub(r'\s+', ' ', text).strip()
+
+ def _to_int(self, value, default=0):
+ try:
+ return int(value)
+ except Exception:
+ return default
\ No newline at end of file
diff --git a/FGBLH/py/猫咪TV.py b/FGBLH/py/猫咪TV.py
new file mode 100644
index 00000000..687afad6
--- /dev/null
+++ b/FGBLH/py/猫咪TV.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+import re
+import urllib.parse
+import requests
+try:
+ from base.spider import Spider as BaseSpider
+except ImportError:
+ class BaseSpider:
+ def __init__(self):
+ return None
+class Spider(BaseSpider):
+ BASE_URL = "https://maomi66.cc"
+ HEADERS = {
+ "User-Agent": "Mozilla/5.0 (Linux; Android 12; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36",
+ "Referer": "https://maomi66.cc/",
+ "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"
+ }
+ def __init__(self):
+ self.session = requests.Session()
+ self.session.headers.update(self.HEADERS)
+ self._class_cache = []
+ def getName(self):
+ return "猫咪AV"
+ def init(self, extend=""):
+ return None
+ def isVideoFormat(self, url):
+ return bool(re.search(r'\.(m3u8|mp4|flv|avi|mkv|mov)(\?|$)', url or '', re.I))
+ def manualVideoCheck(self):
+ return True
+ def homeContent(self, filter):
+ html = self._get(self.BASE_URL)
+ classes = self._classes(html)
+ return {"class": classes, "list": self._parse_list(html), "filters": {}, "parse": 0, "jx": 0}
+ def homeVideoContent(self):
+ return {"list": self._parse_list(self._get(self.BASE_URL))}
+ def categoryContent(self, tid, pg, filter, extend):
+ page = self._to_int(pg, 1)
+ html = self._get(self.BASE_URL + "/list/%s-%s.html" % (tid, page))
+ data = self._parse_list(html)
+ return {"list": data, "page": page, "pagecount": page + 1 if data else page, "limit": len(data) or 20, "total": (page + 1) * (len(data) or 20)}
+ def detailContent(self, ids):
+ vid = ids[0] if isinstance(ids, list) and ids else str(ids)
+ url = vid if str(vid).startswith("http") else self.BASE_URL + "/video/%s.html" % vid
+ html = self._get(url)
+ title = self._clean(self._match(html, r']*>(.*?) ') or self._match(html, r']*>(.*?) ') or self._match(html, r']*>(.*?) '))
+ if not title:
+ title = "视频%s" % re.sub(r'\D+', '', str(vid))
+ pic = self._fix(self._match(html, r' ]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)') or self._match(html, r']+poster=["\']([^"\']+)') or self._match(html, r'(?:data-original|data-src|src)=["\']([^"\']+\.(?:jpg|jpeg|png|webp|gif)[^"\']*)'))
+ play = self._extract_play(html)
+ tags = []
+ for x in re.findall(r']+href=["\']/list/\d+-1\.html["\'][^>]*>(.*?) ', html, re.S):
+ t = self._clean(x)
+ if t and t not in tags:
+ tags.append(t)
+ content = self._clean(self._match(html, r']+class=["\'][^"\']*(?:des|intro|content|info)[^"\']*["\'][^>]*>(.*?)
')) or title
+ vod = {
+ "vod_id": str(vid).split("/")[-1].replace(".html", ""),
+ "vod_name": title,
+ "vod_pic": pic,
+ "type_name": "/".join(tags[:3]),
+ "vod_year": "",
+ "vod_area": "",
+ "vod_remarks": "",
+ "vod_actor": "",
+ "vod_director": "",
+ "vod_content": content,
+ "vod_play_from": "默认",
+ "vod_play_url": "播放$%s" % (play or url)
+ }
+ return {"list": [vod]}
+ def searchContent(self, key, quick, pg="1"):
+ q = urllib.parse.quote(str(key or ""))
+ page = self._to_int(pg, 1)
+ html = self._get(self.BASE_URL + "/search.php?content=%s&type=1&page=%s" % (q, page))
+ data = self._parse_list(html)
+ if not data:
+ html = self._get(self.BASE_URL + "/search.php?content=%s&type=1" % q)
+ data = self._parse_list(html)
+ return {"list": data, "page": page, "pagecount": page + 1 if data else page, "limit": len(data) or 20, "total": (page + 1) * (len(data) or 20)}
+ def playerContent(self, flag, id, vipFlags):
+ url = urllib.parse.unquote(str(id or ""))
+ if "/video/" in url or re.fullmatch(r'\d+', url):
+ page = url if url.startswith("http") else self.BASE_URL + "/video/%s.html" % url
+ play = self._extract_play(self._get(page))
+ url = play or page
+ return {"parse": 0, "playUrl": "", "url": self._fix(url), "header": self.HEADERS}
+ def _classes(self, html):
+ arr = []
+ for tid, name in re.findall(r'href=["\']/list/(\d+)-1\.html["\'][^>]*>(.*?)', html or "", re.S):
+ name = self._clean(name)
+ if tid and name and not any(x["type_id"] == tid for x in arr):
+ arr.append({"type_id": tid, "type_name": name})
+ if not arr:
+ arr = [
+ {"type_id": "69829818", "type_name": "国产精品"},
+ {"type_id": "71188148", "type_name": "国产自拍"},
+ {"type_id": "43659662", "type_name": "日本精品"},
+ {"type_id": "37440125", "type_name": "欧美极品"},
+ {"type_id": "19211697", "type_name": "中文字幕"},
+ {"type_id": "77777777", "type_name": "动漫精品"}
+ ]
+ self._class_cache = arr
+ return arr
+ def _parse_list(self, html):
+ out = []
+ blocks = re.findall(r'', html or "", re.I)
+ if not blocks:
+ blocks = re.findall(r']+href=["\']/video/\d+\.html["\'][\s\S]*? ', html or "", re.I)
+ for item in blocks:
+ vid = self._match(item, r'href=["\'][^"\']*/video/(\d+)\.html["\']')
+ if not vid:
+ continue
+ name = self._clean(self._match(item, r']*>\s*]*>(.*?) ') or self._match(item, r'title=["\']([^"\']+)') or self._match(item, r'alt=["\']([^"\']+)'))
+ pic = self._fix(self._match(item, r'data-original=["\']([^"\']+)') or self._match(item, r'data-src=["\']([^"\']+)') or self._match(item, r' ]+src=["\']([^"\']+)'))
+ remark = self._clean(self._match(item, r']*>(.*?) ') or self._match(item, r']*>(.*?) '))
+ if not name:
+ name = "视频%s" % vid
+ vod = {"vod_id": vid, "vod_name": name, "vod_pic": pic, "vod_remarks": remark}
+ if not any(x["vod_id"] == vid for x in out):
+ out.append(vod)
+ return out
+ def _extract_play(self, html):
+ play = self._match(html, r'hls\.loadSource\(["\']([^"\']+)["\']\)') or self._match(html, r'video\.src\s*=\s*["\']([^"\']+)["\']') or self._match(html, r']+src=["\']([^"\']+)["\']') or self._match(html, r'["\'](https?://[^"\']+play\.php\?[^"\']+)["\']') or self._match(html, r'["\'](/play\.php\?[^"\']+)["\']')
+ return self._fix(play)
+ def _get(self, url):
+ if not url:
+ return ""
+ url = self._fix(url)
+ headers = dict(self.HEADERS)
+ headers["Referer"] = self.BASE_URL + "/"
+ try:
+ r = self.session.get(url, headers=headers, timeout=12, verify=False)
+ if not r.encoding or r.encoding.lower() == "iso-8859-1":
+ r.encoding = r.apparent_encoding or "utf-8"
+ return r.text
+ except requests.RequestException:
+ return ""
+ def _match(self, text, pattern, default=""):
+ m = re.search(pattern, text or "", re.S | re.I)
+ if not m:
+ return default
+ return m.group(1) if m.lastindex else m.group(0)
+ def _clean(self, text):
+ text = re.sub(r'', iframe_html, re.S)
+ all_script = '\n'.join(scripts)
+ script_links = set(re.findall(r'''['"](https?://[^'" ]+\.(?:m3u8|mp4)[^'" ]*)['"]''', all_script))
+ for link in script_links:
+ link = clean_url(link)
+ if any(ext in link for ext in ['.m3u8', '.mp4']) and link not in seen_media:
+ seen_media.add(link)
+ media_urls.append(link)
+ # 尝试 Base64 解码
+ b64_links = re.findall(r'["\']([A-Za-z0-9+/=]{20,})["\']', iframe_html)
+ for b64_str in b64_links:
+ decoded = self._try_decode_b64(b64_str)
+ if decoded.startswith('http') and any(ext in decoded for ext in ['.m3u8', '.mp4']):
+ link = clean_url(decoded)
+ if link not in seen_media:
+ seen_media.add(link)
+ media_urls.append(link)
+ else:
+ # iframe 加载失败,保存外链用于二次解析
+ if full not in seen_iframe:
+ seen_iframe.add(full)
+ iframe_urls.append(full)
+
+ # --- 策略3:JS 变量/JSON 中的链接 ---
+ scripts = re.findall(r'', html, re.S)
+ all_js = '\n'.join(scripts)
+ js_patterns = [
+ r'''url\s*:\s*['"]([^'"]+)['"]''',
+ r'''file\s*:\s*['"]([^'"]+)['"]''',
+ r'''src\s*:\s*['"]([^'"]+)['"]''',
+ r'''video\s*:\s*['"]([^'"]+)['"]''',
+ r'''player_aaaa\s*=\s*['"]([^'"]+)['"]''',
+ r'''playurl\s*=\s*['"]([^'"]+)['"]''',
+ r'''["\'](https?://[^"\']+?\.(?:m3u8|mp4)[^"\']*?)["\']''',
+ ]
+ for pat in js_patterns:
+ for match in re.finditer(pat, all_js, re.IGNORECASE):
+ val = match.group(1)
+ # 先尝试 Base64 解码
+ decoded = self._try_decode_b64(val)
+ if decoded.startswith('http') and any(ext in decoded for ext in ['.m3u8', '.mp4']):
+ link = clean_url(decoded)
+ if link not in seen_media:
+ seen_media.add(link)
+ media_urls.append(link)
+ elif val.startswith('http') and any(ext in val for ext in ['.m3u8', '.mp4']):
+ link = clean_url(val)
+ if link not in seen_media:
+ seen_media.add(link)
+ media_urls.append(link)
+
+ # --- 策略4:HTML5 标签 ---
+ for media in set(re.findall(r'<(?:video|source)[^>]+src=["\']([^"\']+)["\']', html)):
+ link = clean_url(media)
+ if any(ext in link for ext in ['.m3u8', '.mp4', '.flv', '.ts']) and link not in seen_media:
+ seen_media.add(link)
+ media_urls.append(link)
+
+ # --- 策略5:自定义 data 属性解码 ---
+ for attr_val in re.findall(r'data-(?:url|video|src)=["\']([^"\']+)["\']', html):
+ decoded = self._try_decode_b64(attr_val)
+ if decoded.startswith('http') and any(ext in decoded for ext in ['.m3u8', '.mp4']):
+ link = clean_url(decoded)
+ if link not in seen_media:
+ seen_media.add(link)
+ media_urls.append(link)
+
+ # 构建最终播放列表
+ play_list = []
+ sources = []
+
+ if media_urls:
+ # 有直接媒体链接,全部标记为直链
+ for u in media_urls:
+ play_list.append(f'直链${u}')
+ sources.append('直链')
+ else:
+ # 没有媒体链接,使用 iframe 外链作为解析源
+ if iframe_urls:
+ for u in iframe_urls:
+ play_list.append(f'网页解析${u}')
+ sources.append('网页解析')
+ else:
+ # 完全没找到任何链接
+ self._log('未提取到任何播放地址')
+ return {
+ 'vod_id': vid,
+ 'vod_name': title or vid,
+ 'vod_pic': cover or '',
+ 'vod_play_from': '',
+ 'vod_play_url': '',
+ 'vod_content': title or '',
+ }
+
+ self._log(f'提取到 {len(play_list)} 个播放地址:')
+ for idx, p in enumerate(play_list):
+ self._log(f' [{idx}] {p[:120]}')
+
+ return {
+ 'vod_id': vid,
+ 'vod_name': title or vid,
+ 'vod_pic': cover or '',
+ 'vod_play_from': '$$$'.join(sources),
+ 'vod_play_url': '#'.join(play_list),
+ 'vod_content': title or '',
+ }
+
+ def detailContent(self, ids):
+ try:
+ vid = str(ids[0] if isinstance(ids, list) else ids)
+ detail = self._fetch_detail(vid)
+ return {'list': [detail]}
+ except Exception as e:
+ self._log(f'detailContent 异常: {e}')
+ return {'list': []}
+
+ # ==================== 播放器(区分直链和解析链接) ====================
+ def playerContent(self, flag, id, vipFlags=None):
+ try:
+ # 如果是网页解析链接(flag 包含“网页解析”),让 TVBox 用 WebView 解析
+ if flag and '网页解析' in flag:
+ full_url = id if id.startswith('http') else urljoin(self.host, id)
+ return {'parse': 1, 'url': full_url, 'header': ''}
+
+ # 处理直链媒体
+ if id.startswith('http'):
+ headers = {
+ 'Referer': self.host + '/',
+ 'Origin': self.host,
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ if '.m3u8' in id:
+ # 可选:使用本地代理清洗广告
+ proxy_url = self._proxy_m3u8_url(id, self.host)
+ return {'parse': 0, 'url': proxy_url, 'header': json.dumps(headers)}
+ else:
+ return {'parse': 0, 'url': id, 'header': json.dumps(headers)}
+ elif id.startswith('/'):
+ full = urljoin(self.host, id)
+ return self.playerContent(flag, full, vipFlags)
+ else:
+ return {'parse': 0, 'url': '', 'header': {}}
+ except Exception as e:
+ self._log(f'playerContent 异常: {e}')
+ return {'parse': 0, 'url': '', 'header': {}}
+
+ # ==================== m3u8 清洗(保持不变) ====================
+ def _proxy_m3u8_url(self, url, referer=''):
+ try:
+ if hasattr(self, 'getProxyUrl'):
+ base = self.getProxyUrl()
+ if '?' not in base:
+ base += '?do=py'
+ return base + '&type=m3u8&url=' + quote(url, safe='') + '&referer=' + quote(referer or self.host, safe='')
+ except:
+ pass
+ return url
+
+ def _get_m3u8_content(self, url, referer):
+ try:
+ headers = self.session.headers.copy()
+ headers['Referer'] = referer
+ resp = requests.get(url, headers=headers, timeout=15)
+ if resp.status_code == 200:
+ resp.encoding = 'utf-8'
+ return resp.text
+ except Exception as e:
+ self._log(f'下载 m3u8 失败: {e}')
+ return None
+
+ def _clean_m3u8(self, m3u8_text, m3u8_url='', referer='', skip_seconds=25):
+ """清洗 m3u8:去除广告分片"""
+ text = (m3u8_text or '').replace('\r', '')
+ if '#EXT-X-STREAM-INF' in text:
+ out = []
+ for raw in text.splitlines():
+ line = raw.strip()
+ if not line:
+ continue
+ if line.startswith('#'):
+ out.append(line)
+ else:
+ abs_url = urljoin(m3u8_url, line)
+ if '.m3u8' in line.lower():
+ out.append(self._proxy_m3u8_url(abs_url, referer))
+ else:
+ out.append(abs_url)
+ return '\n'.join(out) + '\n'
+
+ header, segments, tail, media_sequence, target_duration = self._parse_m3u8_segments(text)
+ if not segments:
+ return text
+
+ marker = self._main_path_marker(m3u8_url)
+ stat = {}
+ for seg in segments:
+ key = self._segment_host_key(seg['uri'], m3u8_url)
+ stat[key] = stat.get(key, 0.0) + float(seg.get('dur') or 0)
+ main_key = max(stat.items(), key=lambda x: x[1])[0] if stat else ('', '')
+ total_dur = sum(stat.values()) or 0
+ main_dur = stat.get(main_key, 0)
+
+ cleaned = []
+ removed = 0
+ for idx, seg in enumerate(segments):
+ key = self._segment_host_key(seg['uri'], m3u8_url)
+ is_front = idx < 12
+ abs_uri = urljoin(m3u8_url, seg.get('uri', ''))
+ is_ad = self._is_ad_segment(seg['uri'], seg.get('dur'), seg.get('tags'))
+ if marker and marker not in urlparse(abs_uri).path.lower():
+ is_ad = True
+ tags_text = '\n'.join(seg.get('tags') or []).upper()
+ if is_front and 'METHOD=NONE' in tags_text and marker and marker not in urlparse(abs_uri).path.lower():
+ is_ad = True
+ if (not is_ad) and is_front and total_dur > 0 and main_dur >= total_dur * 0.6:
+ if key != main_key and stat.get(key, 0) <= 90:
+ is_ad = True
+ if is_ad:
+ removed += 1
+ continue
+ seg['_idx'] = idx
+ cleaned.append(seg)
+
+ if removed == 0 and len(segments) > 4:
+ acc = 0.0
+ cut = 0
+ for idx, seg in enumerate(segments[:12]):
+ key = self._segment_host_key(seg['uri'], m3u8_url)
+ if key == main_key and acc >= 3:
+ break
+ acc += float(seg.get('dur') or target_duration or 3)
+ cut = idx + 1
+ if acc >= skip_seconds:
+ break
+ if cut > 0 and cut < len(segments):
+ first_key = self._segment_host_key(segments[0]['uri'], m3u8_url)
+ if first_key != main_key:
+ cleaned = segments[cut:]
+ removed = cut
+
+ if not cleaned:
+ cleaned = segments
+ removed = 0
+
+ new_lines = []
+ has_m3u = False
+ for line in header:
+ if line.startswith('#EXTM3U'):
+ has_m3u = True
+ if line.startswith('#EXT-X-MEDIA-SEQUENCE') or line.startswith('#EXT-X-START'):
+ continue
+ if line.startswith('#EXT-X-KEY') and 'METHOD=NONE' in line.upper() and removed > 0:
+ continue
+ new_lines.append(line)
+ if not has_m3u:
+ new_lines.insert(0, '#EXTM3U')
+ first_idx = cleaned[0].get('_idx', removed) if cleaned else removed
+ new_lines.append(f'#EXT-X-MEDIA-SEQUENCE:{media_sequence + first_idx}')
+ for seg in cleaned:
+ for tag in seg.get('tags') or []:
+ if tag.startswith('#EXT-X-KEY') or tag.startswith('#EXT-X-MAP'):
+ def _fix_uri(m):
+ return 'URI="' + urljoin(m3u8_url, m.group(1)) + '"'
+ tag = re.sub(r'URI="([^"]+)"', _fix_uri, tag)
+ new_lines.append(tag)
+ new_lines.append(urljoin(m3u8_url, seg.get('uri', '')))
+ if tail:
+ for line in tail:
+ if line.startswith('#EXT-X-ENDLIST'):
+ new_lines.append(line)
+ elif '#EXT-X-ENDLIST' in text:
+ new_lines.append('#EXT-X-ENDLIST')
+ self._log(f'm3u8清洗: 原{len(segments)}片 → 删除{removed}片广告,保留{len(cleaned)}片')
+ return '\n'.join(new_lines) + '\n'
+
+ def _parse_m3u8_segments(self, text):
+ lines = [x.strip() for x in (text or '').replace('\r', '').split('\n') if x.strip()]
+ header, segments, tail = [], [], []
+ pending_tags = []
+ media_sequence = 0
+ target_duration = 0
+ started = False
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+ if line.startswith('#EXT-X-MEDIA-SEQUENCE'):
+ try: media_sequence = int(line.split(':', 1)[1])
+ except: pass
+ if not started: header.append(line)
+ else: pending_tags.append(line)
+ elif line.startswith('#EXT-X-TARGETDURATION'):
+ try: target_duration = float(line.split(':', 1)[1])
+ except: pass
+ if not started: header.append(line)
+ else: pending_tags.append(line)
+ elif line.startswith('#EXTINF'):
+ started = True
+ dur = target_duration or 3.0
+ m = re.search(r'#EXTINF:\s*([\d.]+)', line)
+ if m:
+ try: dur = float(m.group(1))
+ except: pass
+ tags = pending_tags + [line]
+ pending_tags = []
+ uri = ''
+ j = i + 1
+ while j < len(lines):
+ if lines[j].startswith('#'):
+ tags.append(lines[j])
+ j += 1
+ continue
+ uri = lines[j]
+ break
+ if uri:
+ segments.append({'tags': tags, 'uri': uri, 'dur': dur})
+ i = j
+ else:
+ tail.extend(tags)
+ elif line.startswith('#EXT-X-ENDLIST'):
+ tail.append(line)
+ elif line.startswith('#'):
+ if started: pending_tags.append(line)
+ else: header.append(line)
+ else:
+ started = True
+ dur = target_duration or 3.0
+ segments.append({'tags': pending_tags, 'uri': line, 'dur': dur})
+ pending_tags = []
+ i += 1
+ return header, segments, tail, media_sequence, target_duration
+
+ def _is_ad_segment(self, uri, dur=0, prev_tags=None):
+ u = (uri or '').strip().lower()
+ if not u: return False
+ ad_words = ['ad', 'ads', 'advert', 'sponsor', 'pre', 'preroll', '片头', '广告', '/gg/', '_gg', 'gg_', '/adv/']
+ if any(w in u for w in ad_words): return True
+ try:
+ if 0 < float(dur) <= 1.2: return True
+ except: pass
+ return False
+
+ def _segment_host_key(self, uri, base_url):
+ try:
+ full = urljoin(base_url, uri)
+ p = urlparse(full)
+ path = re.sub(r'/[^/]*$', '/', p.path or '/')
+ return (p.netloc.lower(), path.lower())
+ except:
+ return ('', '')
+
+ def _main_path_marker(self, m3u8_url):
+ try:
+ p = urlparse(m3u8_url).path
+ m = re.search(r'(/\d{8}/[^/]+/\d+kb/hls/)', p)
+ if m: return m.group(1).lower()
+ m = re.search(r'(/\d{8}/[^/]+/)', p)
+ if m: return m.group(1).lower()
+ except: pass
+ return ''
+
+ # ==================== 搜索 ====================
+ def searchContent(self, key, quick=False, pg='1'):
+ try:
+ page = int(pg) if pg else 1
+ urls = [
+ f'{self.host}/search?keyword={quote(key)}&page={page}',
+ f'{self.host}/search.php?content={quote(key)}&page={page}',
+ ]
+ items = []
+ for url in urls:
+ html = self._fetch(url, referer=self.host)
+ if html:
+ items = self._parse_list(html)
+ if items:
+ break
+ return {
+ 'list': items,
+ 'page': page,
+ 'pagecount': page + 1,
+ 'limit': len(items),
+ 'total': page * len(items)
+ }
+ except Exception as e:
+ self._log(f'searchContent 异常: {e}')
+ return {'list': [], 'page': int(pg) if pg else 1, 'pagecount': 1, 'limit': 0, 'total': 0}
\ No newline at end of file
diff --git a/jaychouqq/yingshi/py9/猫咪TV.py b/jaychouqq/yingshi/py9/猫咪TV.py
new file mode 100644
index 00000000..687afad6
--- /dev/null
+++ b/jaychouqq/yingshi/py9/猫咪TV.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+import re
+import urllib.parse
+import requests
+try:
+ from base.spider import Spider as BaseSpider
+except ImportError:
+ class BaseSpider:
+ def __init__(self):
+ return None
+class Spider(BaseSpider):
+ BASE_URL = "https://maomi66.cc"
+ HEADERS = {
+ "User-Agent": "Mozilla/5.0 (Linux; Android 12; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36",
+ "Referer": "https://maomi66.cc/",
+ "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"
+ }
+ def __init__(self):
+ self.session = requests.Session()
+ self.session.headers.update(self.HEADERS)
+ self._class_cache = []
+ def getName(self):
+ return "猫咪AV"
+ def init(self, extend=""):
+ return None
+ def isVideoFormat(self, url):
+ return bool(re.search(r'\.(m3u8|mp4|flv|avi|mkv|mov)(\?|$)', url or '', re.I))
+ def manualVideoCheck(self):
+ return True
+ def homeContent(self, filter):
+ html = self._get(self.BASE_URL)
+ classes = self._classes(html)
+ return {"class": classes, "list": self._parse_list(html), "filters": {}, "parse": 0, "jx": 0}
+ def homeVideoContent(self):
+ return {"list": self._parse_list(self._get(self.BASE_URL))}
+ def categoryContent(self, tid, pg, filter, extend):
+ page = self._to_int(pg, 1)
+ html = self._get(self.BASE_URL + "/list/%s-%s.html" % (tid, page))
+ data = self._parse_list(html)
+ return {"list": data, "page": page, "pagecount": page + 1 if data else page, "limit": len(data) or 20, "total": (page + 1) * (len(data) or 20)}
+ def detailContent(self, ids):
+ vid = ids[0] if isinstance(ids, list) and ids else str(ids)
+ url = vid if str(vid).startswith("http") else self.BASE_URL + "/video/%s.html" % vid
+ html = self._get(url)
+ title = self._clean(self._match(html, r']*>(.*?) ') or self._match(html, r']*>(.*?) ') or self._match(html, r']*>(.*?) '))
+ if not title:
+ title = "视频%s" % re.sub(r'\D+', '', str(vid))
+ pic = self._fix(self._match(html, r' ]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)') or self._match(html, r']+poster=["\']([^"\']+)') or self._match(html, r'(?:data-original|data-src|src)=["\']([^"\']+\.(?:jpg|jpeg|png|webp|gif)[^"\']*)'))
+ play = self._extract_play(html)
+ tags = []
+ for x in re.findall(r']+href=["\']/list/\d+-1\.html["\'][^>]*>(.*?) ', html, re.S):
+ t = self._clean(x)
+ if t and t not in tags:
+ tags.append(t)
+ content = self._clean(self._match(html, r']+class=["\'][^"\']*(?:des|intro|content|info)[^"\']*["\'][^>]*>(.*?)
')) or title
+ vod = {
+ "vod_id": str(vid).split("/")[-1].replace(".html", ""),
+ "vod_name": title,
+ "vod_pic": pic,
+ "type_name": "/".join(tags[:3]),
+ "vod_year": "",
+ "vod_area": "",
+ "vod_remarks": "",
+ "vod_actor": "",
+ "vod_director": "",
+ "vod_content": content,
+ "vod_play_from": "默认",
+ "vod_play_url": "播放$%s" % (play or url)
+ }
+ return {"list": [vod]}
+ def searchContent(self, key, quick, pg="1"):
+ q = urllib.parse.quote(str(key or ""))
+ page = self._to_int(pg, 1)
+ html = self._get(self.BASE_URL + "/search.php?content=%s&type=1&page=%s" % (q, page))
+ data = self._parse_list(html)
+ if not data:
+ html = self._get(self.BASE_URL + "/search.php?content=%s&type=1" % q)
+ data = self._parse_list(html)
+ return {"list": data, "page": page, "pagecount": page + 1 if data else page, "limit": len(data) or 20, "total": (page + 1) * (len(data) or 20)}
+ def playerContent(self, flag, id, vipFlags):
+ url = urllib.parse.unquote(str(id or ""))
+ if "/video/" in url or re.fullmatch(r'\d+', url):
+ page = url if url.startswith("http") else self.BASE_URL + "/video/%s.html" % url
+ play = self._extract_play(self._get(page))
+ url = play or page
+ return {"parse": 0, "playUrl": "", "url": self._fix(url), "header": self.HEADERS}
+ def _classes(self, html):
+ arr = []
+ for tid, name in re.findall(r'href=["\']/list/(\d+)-1\.html["\'][^>]*>(.*?)', html or "", re.S):
+ name = self._clean(name)
+ if tid and name and not any(x["type_id"] == tid for x in arr):
+ arr.append({"type_id": tid, "type_name": name})
+ if not arr:
+ arr = [
+ {"type_id": "69829818", "type_name": "国产精品"},
+ {"type_id": "71188148", "type_name": "国产自拍"},
+ {"type_id": "43659662", "type_name": "日本精品"},
+ {"type_id": "37440125", "type_name": "欧美极品"},
+ {"type_id": "19211697", "type_name": "中文字幕"},
+ {"type_id": "77777777", "type_name": "动漫精品"}
+ ]
+ self._class_cache = arr
+ return arr
+ def _parse_list(self, html):
+ out = []
+ blocks = re.findall(r'', html or "", re.I)
+ if not blocks:
+ blocks = re.findall(r']+href=["\']/video/\d+\.html["\'][\s\S]*? ', html or "", re.I)
+ for item in blocks:
+ vid = self._match(item, r'href=["\'][^"\']*/video/(\d+)\.html["\']')
+ if not vid:
+ continue
+ name = self._clean(self._match(item, r']*>\s*]*>(.*?) ') or self._match(item, r'title=["\']([^"\']+)') or self._match(item, r'alt=["\']([^"\']+)'))
+ pic = self._fix(self._match(item, r'data-original=["\']([^"\']+)') or self._match(item, r'data-src=["\']([^"\']+)') or self._match(item, r' ]+src=["\']([^"\']+)'))
+ remark = self._clean(self._match(item, r']*>(.*?) ') or self._match(item, r']*>(.*?) '))
+ if not name:
+ name = "视频%s" % vid
+ vod = {"vod_id": vid, "vod_name": name, "vod_pic": pic, "vod_remarks": remark}
+ if not any(x["vod_id"] == vid for x in out):
+ out.append(vod)
+ return out
+ def _extract_play(self, html):
+ play = self._match(html, r'hls\.loadSource\(["\']([^"\']+)["\']\)') or self._match(html, r'video\.src\s*=\s*["\']([^"\']+)["\']') or self._match(html, r']+src=["\']([^"\']+)["\']') or self._match(html, r'["\'](https?://[^"\']+play\.php\?[^"\']+)["\']') or self._match(html, r'["\'](/play\.php\?[^"\']+)["\']')
+ return self._fix(play)
+ def _get(self, url):
+ if not url:
+ return ""
+ url = self._fix(url)
+ headers = dict(self.HEADERS)
+ headers["Referer"] = self.BASE_URL + "/"
+ try:
+ r = self.session.get(url, headers=headers, timeout=12, verify=False)
+ if not r.encoding or r.encoding.lower() == "iso-8859-1":
+ r.encoding = r.apparent_encoding or "utf-8"
+ return r.text
+ except requests.RequestException:
+ return ""
+ def _match(self, text, pattern, default=""):
+ m = re.search(pattern, text or "", re.S | re.I)
+ if not m:
+ return default
+ return m.group(1) if m.lastindex else m.group(0)
+ def _clean(self, text):
+ text = re.sub(r'