# -*- coding: utf-8 -*- # TVBox Python Spider for https://www.fqdm.cc # 适用于 FongMi 版本 import requests import re import json from urllib.parse import urljoin, quote class Spider: def __init__(self): self.siteUrl = "https://www.fqdm.cc" 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", "Referer": self.siteUrl } # ---------- 必需接口 ---------- def getDependence(self): return "" def init(self, ext=""): pass def homeContent(self, filter=None): categories = self._getCategories() html = self._fetch(self.siteUrl) videos = self._parseVideoList(html) return json.dumps({"class": categories, "list": videos}, ensure_ascii=False) def homeVideoContent(self, filter=None): html = self._fetch(self.siteUrl) videos = self._parseVideoList(html) return json.dumps({"list": videos}, ensure_ascii=False) def categoryContent(self, tid, pg, filter=False, extend=""): url = f"{self.siteUrl}/vodshow/{tid}--------{pg}---/" html = self._fetch(url) videos = self._parseVideoList(html) pagecount = self._getPageCount(html) or 999 return json.dumps({ "page": pg, "pagecount": pagecount, "limit": 24, "total": pagecount * 24, "list": videos }, ensure_ascii=False) def detailContent(self, ids): vid = ids[0] html = self._fetch(f"{self.siteUrl}/voddetail/{vid}.html") name = self._extract(html, r']*>([^<]+)') pic = self._extract(html, r'data-original="([^"]*)"') if not pic: pic = self._extract(html, r']*src="([^"]*)"') if pic: pic = urljoin(self.siteUrl, pic) desc = self._extract(html, r'class="content"[^>]*>([\s\S]*?)') if not desc: desc = self._extract(html, r'class="info"[^>]*>([\s\S]*?)') play_url = self._parsePlayList(html) vod = { "vod_id": vid, "vod_name": name.strip() if name else "", "vod_pic": pic or "", "vod_content": desc.strip() if desc else "", "vod_play_from": "fqdm", "vod_play_url": play_url } return json.dumps({"list": [vod]}, ensure_ascii=False) def searchContent(self, key, quick=False): url = f"{self.siteUrl}/vodsearch/-------------.html?wd={quote(key)}" html = self._fetch(url) videos = self._parseVideoList(html) return json.dumps({"list": videos}, ensure_ascii=False) def playerContent(self, flag, ids, vipFlags=None): if not ids.startswith("http"): playUrl = urljoin(self.siteUrl, ids) else: playUrl = ids html = self._fetch(playUrl) real_url = "" # 尝试匹配苹果 CMS 播放器对象 player_json = self._extract(html, r'player_\w+\s*=\s*({[\s\S]*?});') if player_json: try: data = json.loads(player_json) real_url = data.get("url") or data.get("url_next", "") except: pass if not real_url: real_url = self._extract(html, r']*src="([^"]*)"') if not real_url: iframe_src = self._extract(html, r']*src="([^"]*)"') if iframe_src: iframe_url = iframe_src if iframe_src.startswith("http") else urljoin(self.siteUrl, iframe_src) return self.playerContent(flag, iframe_url) if real_url and not real_url.startswith("http"): real_url = urljoin(self.siteUrl, real_url) return json.dumps({"url": real_url}, ensure_ascii=False) # ---------- 辅助方法 ---------- def _fetch(self, url): resp = requests.get(url, headers=self.headers, timeout=15) resp.encoding = "utf-8" return resp.text def _getCategories(self): html = self._fetch(self.siteUrl) categories = [] seen = set() # 标准格式:动漫 for cid, name in re.findall(r']*href="/vodshow/(\d+)[^"]*"[^>]*>([^<]+)', html): if cid not in seen: seen.add(cid) categories.append({"type_id": cid, "type_name": name.strip()}) if not categories: # 松散匹配 for cid, name in re.findall(r'href="/vodshow/(\d+)[^"]*"[^>]*>([^<]+)<', html): if cid not in seen: seen.add(cid) categories.append({"type_id": cid, "type_name": name.strip()}) if not categories: categories = [{"type_id": "1", "type_name": "动漫"}] return categories def _parseVideoList(self, html): # 找到所有包含 voddetail/数字.html 的 标签块 a_blocks = re.findall(r']*href="/voddetail/(\d+)\.html"[^>]*>([\s\S]*?)', html) if not a_blocks: # 备用:可能结构较乱,只匹配至下一个 ]*href="/voddetail/(\d+)\.html"[^>]*>(.*?)', html, re.DOTALL) videos = [] for vid, content in a_blocks: # 标题 title = "" title_match = re.search(r'title="([^"]*)"', content) if title_match: title = title_match.group(1).strip() else: # 去除 HTML 标签取纯文本 text = re.sub(r'<[^>]+>', '', content).strip() if text: title = text # 图片 pic_url = "" pic_match = re.search(r'data-original="([^"]*)"', content) if not pic_match: pic_match = re.search(r']*src="([^"]*)"', content) if pic_match: pic_url = pic_match.group(1) if not pic_url.startswith(("http://", "https://")): pic_url = urljoin(self.siteUrl, pic_url) if pic_url: videos.append({ "vod_id": vid, "vod_name": title, "vod_pic": pic_url, "vod_remarks": "" }) else: # 即使没有图片也加入,避免漏掉 videos.append({ "vod_id": vid, "vod_name": title, "vod_pic": "", "vod_remarks": "" }) return videos def _parsePlayList(self, html): items = re.findall(r']*href="(/vodplay/[^"]+)"[^>]*>([^<]+)', html) if not items: items = re.findall(r'href="(/vodplay/[^"]+)"[^>]*>([^<]+)<', html) episodes = [] for path, name in items: full_url = urljoin(self.siteUrl, path) episodes.append(f"{name.strip()}${full_url}") return "#".join(episodes) def _getPageCount(self, html): import math total_match = re.search(r'共(\d+)条', html) if total_match: total = int(total_match.group(1)) return math.ceil(total / 24) return None @staticmethod def _extract(html, pattern): m = re.search(pattern, html, re.DOTALL) return m.group(1).strip() if m else ""