From 7f3d6d31ce4a1d87b8a3ed77e1f52429072ea15f Mon Sep 17 00:00:00 2001 From: XC-Pro <19840+xc-pro@noreply.example.org> Date: Thu, 13 Aug 2026 18:03:44 +0200 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E3=80=8Capi=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/咖啡直播.py | 375 +++++++++++++++++ api/极影.js | 1068 +++++++++++++++++++++++++++++++++++++++++++++++ api/荐片.js | 152 +++++++ api/金牌影院.js | 169 ++++++++ 4 files changed, 1764 insertions(+) create mode 100644 api/咖啡直播.py create mode 100644 api/极影.js create mode 100644 api/荐片.js create mode 100644 api/金牌影院.js diff --git a/api/咖啡直播.py b/api/咖啡直播.py new file mode 100644 index 0000000..b0c181b --- /dev/null +++ b/api/咖啡直播.py @@ -0,0 +1,375 @@ +# coding=utf-8 +#!/usr/bin/env python3 +# @name 咖啡直播 +# @author 转自 OmniBox JS +# @description 体育赛事录像回放 + 直播(足球/篮球/NBA) +# @version 2.0.0 + +import json +import requests + + +class Spider: + + def getName(self): + return "咖啡直播" + + def getDependence(self): + return [] + + def init(self, extend=""): + self.host = "https://kafeizhibo.cc" + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + "Accept": "application/json, text/plain, */*", + "Referer": "https://kafeizhibo.com/live/all" + } + + def log(self, msg): + print("[咖啡直播] " + str(msg)) + + # ========================================================= + # 工具 + # ========================================================= + + def _normalize_url(self, path): + if not path: + return "" + if path.startswith("http"): + return path + if path.startswith("//"): + return "https:" + path + return self.host + ("" if path.startswith("/") else "/") + path + + def _get(self, path, params=None, referer=None): + h = dict(self.headers) + if referer: + h["Referer"] = referer + resp = requests.get(self.host + path, headers=h, params=params, timeout=10) + return resp.json() + + # ========================================================= + # 直播部分 + # ========================================================= + + def _fetch_live_all(self): + """GET /api/v1/archor — 返回所有正在直播的频道""" + return self._get("/api/v1/archor", referer=self.host + "/live/all") + + def _parse_live_list(self, items, category_filter=None): + """ + category: 1=足球, 2=篮球, None=全部 + 每个 archor 代表一个独立直播频道(同一场球可能有多个频道) + 合并同 match_id 的频道到一个 vod,多线路在 detail 里处理 + """ + # 按 room_id 去重(同一 room_id 只取第一个,避免重复) + seen_rooms = set() + result = [] + for item in items: + if category_filter and item.get("category") != category_filter: + continue + room_id = str(item.get("room_id", "")) + if room_id in seen_rooms: + continue + seen_rooms.add(room_id) + + home = item.get("home_team", "") + away = item.get("away_team", "") + league = item.get("league_name", "") + h_score = item.get("home_score", 0) + a_score = item.get("away_score", 0) + title = "{} vs {} ({})".format(home, away, league) + + pic = self._normalize_url(item.get("screenshot", "")) + if not pic or "default" in pic: + mi = item.get("match_info") or {} + pic = mi.get("home_team_logo", "") + + result.append({ + "vod_id": "live_{}".format(room_id), + "vod_name": "🔴 " + title, + "vod_pic": pic, + "vod_remarks": "{} - {} | {}".format(h_score, a_score, item.get("name", "")), + }) + return result + + def _detail_live(self, room_id): + """GET /api/v1/room/{room_id} — 获取直播间多线路""" + try: + data = self._get( + "/api/v1/room/{}".format(room_id), + referer=self.host + "/room/{}".format(room_id) + ) + if data.get("code") != 200 or not data.get("data"): + return {"list": []} + + d = data["data"] + room_info = d.get("room_info", {}) + signals = d.get("signals", []) + + home = room_info.get("home_team", "") + away = room_info.get("away_team", "") + league = room_info.get("league", "") + h_score = room_info.get("home_score", 0) + a_score = room_info.get("away_score", 0) + title = "{} vs {} ({})".format(home, away, league) + + teams = d.get("teams", {}) + pic = (teams.get("home") or {}).get("logo", "") + + # 每条 signal 是一个线路(官方直播/原声直播) + episodes = [] + for sig in signals: + url = sig.get("stream_url", "") + if url: + name = sig.get("name", "线路") + episodes.append("{}${}".format(name, url)) + + # 如果 signals 为空,fallback 到 archor + if not episodes: + archor = d.get("archor", {}) + url = archor.get("stream_url", "") + if url: + episodes.append("{}${}".format(archor.get("name", "直播"), url)) + + vod = { + "vod_id": "live_{}".format(room_id), + "vod_name": "🔴 " + title, + "vod_pic": pic, + "vod_content": "{} {} vs {},比分 {} - {}".format( + league, home, away, h_score, a_score + ), + "vod_play_from": "直播线路", + "vod_play_url": "#".join(episodes), + } + return {"list": [vod]} + except Exception as e: + self.log("直播详情失败: " + str(e)) + return {"list": []} + + # ========================================================= + # 录像部分 + # ========================================================= + + def _fetch_recordings(self, page=1, size=30, league=None, type_id=None): + params = {"page": page, "size": size} + if league: + params["league"] = league + elif type_id and type_id not in ("all", "nba", "live_all", "live_1", "live_2"): + params["type"] = type_id + h = dict(self.headers) + h["Referer"] = self.host + "/pc/replay" + resp = requests.get(self.host + "/api/v1/recordings", headers=h, params=params, timeout=10) + return resp.json() + + def _parse_video_list(self, items): + result = [] + for item in items: + title = "{} vs {} ({})".format( + item["home_team"], item["away_team"], item["league_name"] + ) + score = "{} - {}".format(item["home_score"], item["away_score"]) + pic = item.get("cover_image", "") + if pic and not pic.startswith("http"): + pic = self._normalize_url(pic) + if not pic or "default_cover" in pic: + pic = item.get("home_team_logo", "") + remarks = "{} | {} | {}个录像".format( + score, item["start_time"], item.get("recording_count", 0) + ) + result.append({ + "vod_id": str(item["match_id"]), + "vod_name": title, + "vod_pic": pic, + "vod_remarks": remarks, + }) + return result + + def _detail_recording(self, vid): + try: + h = dict(self.headers) + h["Referer"] = self.host + "/pc/replay" + resp = requests.get( + "{}/api/v1/match/{}/recordings".format(self.host, vid), + headers=h, + timeout=10 + ) + data = resp.json() + if data.get("code") != 200 or not data.get("data"): + return {"list": []} + + match = data["data"]["match"] + replays = data["data"].get("replays", []) + highlights = data["data"].get("highlights", []) + + title = "{} vs {} ({})".format( + match["home_team"], match["away_team"], match["league_name"] + ) + pic = match.get("home_team_logo") or match.get("away_team_logo") or "" + + episodes = [] + for idx, rec in enumerate(replays): + if rec.get("video_url"): + name = rec.get("title") or "录像{}".format(idx + 1) + episodes.append("{}${}".format(name, rec["video_url"])) + for idx, rec in enumerate(highlights): + if rec.get("video_url"): + name = rec.get("title") or "集锦{}".format(idx + 1) + episodes.append("{}${}".format(name, rec["video_url"])) + + vod = { + "vod_id": str(vid), + "vod_name": title, + "vod_pic": pic, + "vod_content": "{} {} {} vs {},比分 {} - {},比赛时间:{}".format( + match["league_name"], match.get("match_round", ""), + match["home_team"], match["away_team"], + match["home_score"], match["away_score"], + match["start_time"] + ), + "vod_play_from": "录像源", + "vod_play_url": "#".join(episodes), + } + return {"list": [vod]} + except Exception as e: + self.log("录像详情失败: " + str(e)) + return {"list": []} + + # ========================================================= + # FongMi 接口 + # ========================================================= + + def homeContent(self, filter): + categories = [ + # 直播分类 + {"type_id": "live_all", "type_name": "🔴 直播全部"}, + {"type_id": "live_1", "type_name": "🔴 直播足球"}, + {"type_id": "live_2", "type_name": "🔴 直播篮球"}, + # 录像分类 + {"type_id": "all", "type_name": "录像全部"}, + {"type_id": "1", "type_name": "录像足球"}, + {"type_id": "2", "type_name": "录像篮球"}, + {"type_id": "nba", "type_name": "录像NBA"}, + ] + # 首页展示直播列表 + try: + data = self._fetch_live_all() + vod_list = self._parse_live_list(data.get("data", [])) if data.get("code") == 200 else [] + except Exception as e: + self.log("首页失败: " + str(e)) + vod_list = [] + return {"class": categories, "list": vod_list} + + def homeVideoContent(self): + return {"list": []} + + def categoryContent(self, tid, pg, filter, extend): + pg = int(pg) if pg else 1 + + # ---- 直播分类 ---- + if tid in ("live_all", "live_1", "live_2"): + try: + data = self._fetch_live_all() + if data.get("code") == 200: + cat = None if tid == "live_all" else int(tid.split("_")[1]) + vod_list = self._parse_live_list(data.get("data", []), category_filter=cat) + else: + vod_list = [] + except Exception as e: + self.log("直播分类失败: " + str(e)) + vod_list = [] + return {"list": vod_list, "page": 1, "pagecount": 1, "limit": 100, "total": len(vod_list)} + + # ---- 录像分类 ---- + try: + if tid == "nba": + data = self._fetch_recordings(pg, 20, league="NBA") + size = 20 + elif tid == "all": + data = self._fetch_recordings(pg, 30) + size = 30 + else: + data = self._fetch_recordings(pg, 30, type_id=tid) + size = 30 + + vod_list = [] + pagecount = 1 + if data.get("code") == 200 and data.get("data"): + vod_list = self._parse_video_list(data["data"]) + pagecount = pg + 1 if len(data["data"]) == size else pg + except Exception as e: + self.log("录像分类失败: " + str(e)) + vod_list = [] + pagecount = 1 + + return {"list": vod_list, "page": pg, "pagecount": pagecount, "limit": 30, "total": len(vod_list)} + + def detailContent(self, ids): + vid = ids[0] if isinstance(ids, list) and ids else str(ids) + if vid.startswith("live_"): + room_id = vid[5:] # 去掉 "live_" 前缀 + return self._detail_live(room_id) + else: + return self._detail_recording(vid) + + def searchContent(self, key, quick, pg=1): + if not key: + return {"list": []} + keyword = key.lower() + result = [] + + # 搜索直播 + try: + data = self._fetch_live_all() + if data.get("code") == 200: + for item in data.get("data", []): + if (keyword in item.get("home_team", "").lower() + or keyword in item.get("away_team", "").lower() + or keyword in item.get("league_name", "").lower() + or keyword in item.get("title", "").lower()): + room_id = str(item.get("room_id", "")) + home = item.get("home_team", "") + away = item.get("away_team", "") + league = item.get("league_name", "") + result.append({ + "vod_id": "live_{}".format(room_id), + "vod_name": "🔴 {} vs {} ({})".format(home, away, league), + "vod_pic": "", + "vod_remarks": "直播中", + }) + except Exception as e: + self.log("搜索直播失败: " + str(e)) + + # 搜索录像 + try: + data = self._fetch_recordings(1, 100) + if data.get("code") == 200: + for item in data["data"]: + if (keyword in item["home_team"].lower() + or keyword in item["away_team"].lower() + or keyword in item["league_name"].lower()): + title = "{} vs {} ({})".format( + item["home_team"], item["away_team"], item["league_name"] + ) + result.append({ + "vod_id": str(item["match_id"]), + "vod_name": title, + "vod_pic": "", + "vod_remarks": "{} - {}".format(item["home_score"], item["away_score"]), + }) + except Exception as e: + self.log("搜索录像失败: " + str(e)) + + return {"list": result, "page": 1, "pagecount": 1} + + def playerContent(self, flag, id, vipFlags): + return { + "parse": 0, + "playUrl": "", + "url": id, + "header": json.dumps({ + "User-Agent": self.headers["User-Agent"], + "Referer": self.host, + "Origin": self.host, + }) + } diff --git a/api/极影.js b/api/极影.js new file mode 100644 index 0000000..efc5234 --- /dev/null +++ b/api/极影.js @@ -0,0 +1,1068 @@ +import { Crypto, _ } from 'assets://js/lib/cat.js' + +let host = ''; +let header = { + 'User-Agent': 'okhttp/3.12.11' +}; +let siteKey = ''; +let siteType = ''; +let siteJx = ''; + +const urlPattern1 = /api\.php\/.*?\/vod/; +const urlPattern2 = /api\.php\/.+?\.vod/; +const parsePattern = /\/.+\\?.+=/; +const parsePattern1 = /.*(url|v|vid|php\?id)=/; +const parsePattern2 = /https?:\/\/[^\/]*/; + +const htmlVideoKeyMatch = [ + /player=new/, + /
0) { + let episodeName = segment.substring(0, dollarInSegment); + let urlPart = segment.substring(dollarInSegment); + episodeName = episodeName.replace(/奇迹云/g, ''); + result += episodeName + urlPart; + } else { + result += segment.replace(/奇迹云/g, ''); + } + result += sep; + i = found + sep.length; + } else { + // 最后一段 + let segment = playUrl.substring(i); + let dollarInSegment = segment.indexOf('$'); + if (dollarInSegment > 0) { + let episodeName = segment.substring(0, dollarInSegment); + let urlPart = segment.substring(dollarInSegment); + episodeName = episodeName.replace(/奇迹云/g, ''); + result += episodeName + urlPart; + } else { + result += segment.replace(/奇迹云/g, ''); + } + break; + } + } + return result; +} + +async function detail(ids) { + try { + // 苹果CMS V10模式检测 + if (host.includes('/vod') || host.includes('/provide/vod')) { + const url = `${host}?ac=detail&ids=${ids}`; + const json = await request(url, getHeaders(url)); + const obj = JSON.parse(json); + const result = { list: [] }; + const vod = {}; + + const data = obj.list && obj.list[0] ? obj.list[0] : {}; + vod.vod_id = data.vod_id || ids; + vod.vod_name = (data.vod_name || "").replace(/奇迹云/g, ''); + vod.vod_pic = data.vod_pic || ""; + vod.type_name = (data.type_name || "").replace(/奇迹云/g, ''); + vod.vod_year = (data.vod_year || "").replace(/奇迹云/g, ''); + vod.vod_area = (data.vod_area || "").replace(/奇迹云/g, ''); + vod.vod_remarks = (data.vod_remarks || "").replace(/奇迹云/g, ''); + vod.vod_actor = (data.vod_actor || "").replace(/奇迹云/g, ''); + vod.vod_director = (data.vod_director || "").replace(/奇迹云/g, ''); + vod.vod_content = (data.vod_content || "").replace(/奇迹云/g, ''); + + // 处理线路名称 + let playFrom = data.vod_play_from || ""; + if (playFrom) { + let lines = playFrom.split('$$$'); + let newLines = []; + for (let line of lines) { + if (line && line.toLowerCase().includes('qijiyun4k')) { + newLines.push('拒绝收费'); + } else { + newLines.push((line || "").replace(/奇迹云/g, '')); + } + } + vod.vod_play_from = newLines.join('$$$'); + } else { + vod.vod_play_from = ""; + } + + // 处理选集:只替换文字,保留原始分隔符和结构 + vod.vod_play_url = replacePlayUrlText(data.vod_play_url || ""); + + result.list.push(vod); + return JSON.stringify(result); + } else { + // 原有AppYsV2模式 + const apiUrl = host; + const url = getPlayUrlPrefix(apiUrl) + ids; + + const json = await request(url, getHeaders(url)); + const obj = JSON.parse(json); + const result = { + list: [], + }; + const vod = {}; + genPlayList(apiUrl, obj, json, vod, ids); + + // 替换详情中的所有文字 + if (vod.vod_name) vod.vod_name = vod.vod_name.replace(/奇迹云/g, ''); + if (vod.type_name) vod.type_name = vod.type_name.replace(/奇迹云/g, ''); + if (vod.vod_year) vod.vod_year = vod.vod_year.replace(/奇迹云/g, ''); + if (vod.vod_area) vod.vod_area = vod.vod_area.replace(/奇迹云/g, ''); + if (vod.vod_remarks) vod.vod_remarks = vod.vod_remarks.replace(/奇迹云/g, ''); + if (vod.vod_actor) vod.vod_actor = vod.vod_actor.replace(/奇迹云/g, ''); + if (vod.vod_director) vod.vod_director = vod.vod_director.replace(/奇迹云/g, ''); + if (vod.vod_content) vod.vod_content = vod.vod_content.replace(/奇迹云/g, ''); + + // 替换线路名称 + if (vod.vod_play_from) { + let lines = vod.vod_play_from.split('$$$'); + let newLines = []; + for (let line of lines) { + if (line && line.toLowerCase().includes('qijiyun4k')) { + newLines.push('拒绝收费'); + } else { + newLines.push((line || "").replace(/奇迹云/g, '')); + } + } + vod.vod_play_from = newLines.join('$$$'); + } + + // 处理选集:只替换文字,保留原始分隔符和结构 + vod.vod_play_url = replacePlayUrlText(vod.vod_play_url || ""); + + result.list.push(vod); + return JSON.stringify(result); + } + } catch (e) { + SpiderDebug.log(e); + } + return ""; +} + +async function play(flag, id, vipFlags) { + try { + let parseUrls = siteJx[flag]; + if (!parseUrls) { + if (siteJx.hasOwnProperty('*')) { + parseUrls = siteJx['*']; + } else { + parseUrls = []; + } + } + + if (parseUrls.length > 0) { + const result = await getFinalVideo(flag, parseUrls, id); + if (result !== null) { + return JSON.stringify(result); + } + } + + if (isVideoFormat(id)) { + const result = { + parse: 0, + playUrl: "", + url: id + }; + return JSON.stringify(result); + } else { + const result = { + parse: 1, + jx: "1", + url: id + }; + return JSON.stringify(result); + } + } catch (e) { + SpiderDebug.log(e); + } + return ""; +} + +async function search(key, quick) { + try { + // 苹果CMS V10模式检测 + if (host.includes('/vod') || host.includes('/provide/vod')) { + const url = `${host}?ac=videolist&wd=${encodeURIComponent(key)}&pg=1`; + const json = await request(url, getHeaders(url)); + const obj = JSON.parse(json); + const videos = []; + + if (obj.list && Array.isArray(obj.list)) { + for (const item of obj.list) { + videos.push({ + vod_id: item.vod_id, + vod_name: (item.vod_name || "").replace(/奇迹云/g, ''), + vod_pic: item.vod_pic || "", + vod_remarks: (item.vod_remarks || "").replace(/奇迹云/g, '') + }); + } + } + return JSON.stringify({ list: videos }); + } else { + // 原有AppYsV2模式 + const apiUrl = host; + const url = getSearchUrl(apiUrl, encodeURIComponent(key)); + const json = await request(url, getHeaders(url)); + const obj = JSON.parse(json); + let jsonArray = null; + const videos = []; + + if (obj.list instanceof Array) { + jsonArray = obj.list; + } else if (obj.data instanceof Object && obj.data.list instanceof Array) { + jsonArray = obj.data.list; + } else if (obj.data instanceof Array) { + jsonArray = obj.data; + } + + if (jsonArray !== null) { + for (const vObj of jsonArray) { + if (vObj.vod_id) { + const v = { + vod_id: vObj.vod_id, + vod_name: (vObj.vod_name || "").replace(/奇迹云/g, ''), + vod_pic: vObj.vod_pic, + vod_remarks: (vObj.vod_remarks || "").replace(/奇迹云/g, '') + }; + videos.push(v); + } else { + const v = { + vod_id: vObj.nextlink, + vod_name: (vObj.title || "").replace(/奇迹云/g, ''), + vod_pic: vObj.pic, + vod_remarks: (vObj.state || "").replace(/奇迹云/g, '') + }; + videos.push(v); + } + } + } + + const result = { list: videos }; + return JSON.stringify(result); + } + } catch (error) { + SpiderDebug.log(error); + } + return ""; +} + +// 辅助函数 +async function getFinalVideo(flag, parseUrls, url) { + let htmlPlayUrl = ""; + for (const parseUrl of parseUrls) { + if (parseUrl === "" || parseUrl === "null") { + continue; + } + const playUrl = parseUrl + url; + const content = await request(playUrl, null, 10000); + let tryJson = null; + try { + tryJson = jsonParse(url, content); + } catch (error) { } + + if (tryJson !== null && tryJson.hasOwnProperty("url") && tryJson.hasOwnProperty("header")) { + tryJson.header = JSON.stringify(tryJson.header); + return tryJson; + } + + if (content.includes(" { + if (purl && !parseUrls.includes(purl)) { + parseUrls.push(purl); + } + }); + } + if (from.player_info?.parse2) { + const parse2 = from.player_info.parse2.split(","); + parse2.forEach(purl => { + if (purl && !parseUrls.includes(purl)) { + parseUrls.push(purl); + } + }); + } + parseUrlMap.set(flag, parseUrls); + } catch (e) { + SpiderDebug.log(e); + } + } + } + } else if (urlPattern1.test(URL)) { + const data = object.list && object.list[0] ? object.list[0] : {}; + vod.vod_id = data.vod_id || vid; + vod.vod_name = data.vod_name || ""; + vod.vod_pic = data.vod_pic || ""; + vod.type_name = data.type_name || ""; + vod.vod_year = data.vod_year || ""; + vod.vod_area = data.vod_area || ""; + vod.vod_remarks = data.vod_remarks || ""; + vod.vod_actor = data.vod_actor || ""; + vod.vod_director = data.vod_director || ""; + vod.vod_content = data.vod_content || ""; + + vod.vod_play_from = data.vod_play_from || ""; + vod.vod_play_url = data.vod_play_url || ""; + } + + // 合并播放源 + if (playFlags.length > 0 && playUrls.length > 0) { + vod.vod_play_from = playFlags.join("$$$"); + vod.vod_play_url = playUrls.join("$$$"); + } +} + +function jsonParse(input, json) { + try { + let jsonPlayData = JSON.parse(json); + if (jsonPlayData.hasOwnProperty("data") && typeof jsonPlayData.data === "object" && !jsonPlayData.hasOwnProperty("url")) { + jsonPlayData = jsonPlayData.data; + } + + let url = jsonPlayData.url; + + if (url.startsWith("//")) { + url = "https:" + url; + } + if (!url.trim().startsWith("http")) { + return null; + } + if (url === input) { + if (isVip(url) || !isVideoFormat(url)) { + return null; + } + } + if (isBlackVodUrl(input, url)) { + return null; + } + + let headers = {}; + if (jsonPlayData.hasOwnProperty("header")) { + headers = jsonPlayData.header; + } else if (jsonPlayData.hasOwnProperty("Header")) { + headers = jsonPlayData.Header; + } else if (jsonPlayData.hasOwnProperty("headers")) { + headers = jsonPlayData.headers; + } else if (jsonPlayData.hasOwnProperty("Headers")) { + headers = jsonPlayData.Headers; + } + + let ua = ""; + if (jsonPlayData.hasOwnProperty("user-agent")) { + ua = jsonPlayData["user-agent"]; + } else if (jsonPlayData.hasOwnProperty("User-Agent")) { + ua = jsonPlayData["User-Agent"]; + } + if (ua.trim().length > 0) { + headers["User-Agent"] = " " + ua; + } + + let referer = ""; + if (jsonPlayData.hasOwnProperty("referer")) { + referer = jsonPlayData.referer; + } else if (jsonPlayData.hasOwnProperty("Referer")) { + referer = jsonPlayData.Referer; + } + if (referer.trim().length > 0) { + headers["Referer"] = " " + referer; + } + + headers = fixJsonVodHeader(headers, input, url); + + const taskResult = { + header: headers, + url: url, + parse: "0" + }; + + return taskResult; + } catch (error) { + SpiderDebug.log(error); + } + return null; +} + +function isVip(url) { + try { + let isVip = false; + const host = new URL(url).hostname; + const vipWebsites = ["iqiyi.com", "v.qq.com", "youku.com", "le.com", "tudou.com", "mgtv.com", "sohu.com", "acfun.cn", "bilibili.com", "baofeng.com", "pptv.com"]; + for (let b = 0; b < vipWebsites.length; b++) { + if (host.includes(vipWebsites[b])) { + if (vipWebsites[b] === "iqiyi.com") { + if (url.includes("iqiyi.com/a_") || url.includes("iqiyi.com/w_") || url.includes("iqiyi.com/v_")) { + isVip = true; + break; + } + } else { + isVip = true; + break; + } + } + } + return isVip; + } catch (e) { + SpiderDebug.log(e); + } + return false; +} + +function isBlackVodUrl(input, url) { + return url.includes("973973.xyz") || url.includes(".fit:"); +} + +function fixJsonVodHeader(headers, input, url) { + if (headers === null) { + headers = {}; + } + + if (input.includes("www.mgtv.com")) { + headers["Referer"] = " "; + headers["User-Agent"] = " Mozilla/5.0"; + } else if (url.includes("titan.mgtv")) { + headers["Referer"] = " "; + headers["User-Agent"] = " Mozilla/5.0"; + } else if (input.includes("bilibili")) { + headers["Referer"] = " https://www.bilibili.com/"; + headers["User-Agent"] = " " + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"; + } + + return headers; +} + +const snifferMatch = /http((?!http).){26,}?\.(m3u8|mp4|flv|avi|mkv|rm|wmv|mpg)\?.*|http((?!http).){26,}\.(m3u8|mp4|flv|avi|mkv|rm|wmv|mpg)|http((?!http).){26,}\/m3u8\?pt=m3u8.*|http((?!http).)*?default\.ixigua\.com\/.*|http((?!http).)*?cdn-tos[^\?]*|http((?!http).)*?\/obj\/tos[^\?]*|http.*?\/player\/m3u8play\.php\?url=.*|http.*?\/player\/.*?[pP]lay\.php\?url=.*|http.*?\/playlist\/m3u8\/\?vid=.*|http.*?\.php\?type=m3u8&.*|http.*?\/download.aspx\?.*|http.*?\/api\/up_api.php\?.*|https.*?\.66yk\.cn.*|http((?!http).)*?netease\.com\/file\/.*/; + +function isVideoFormat(url) { + if (snifferMatch.test(url)) { + return !url.includes("cdn-tos") || !url.includes(".js"); + } + return false; +} + +function isVideo(url) { + return !url.includes(".mp4") && !url.includes(".m3u8"); +} + +function UA(url) { + if (url.includes(".vod")) { + return "okhttp/4.1.0"; + } + return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"; +} + +function getCateUrl(URL) { + if (URL.includes("api.php/app") || URL.includes("xgapp")) { + return URL + "nav?token="; + } else if (URL.includes(".vod")) { + return URL + "/types"; + } else { + return ""; + } +} + +function getPlayUrlPrefix(URL) { + if (URL.includes("api.php/app") || URL.includes("xgapp")) { + return URL + "video_detail?id="; + } else if (URL.includes(".vod")) { + return URL + "/detail?vod_id="; + } else { + return ""; + } +} + +function getRecommendUrl(URL) { + if (URL.includes("api.php/app") || URL.includes("xgapp")) { + return URL + "index_video?token="; + } else if (URL.includes(".vod")) { + return URL + "/vodPhbAll"; + } else { + return ""; + } +} + +function getFilterTypes(URL, typeExtend) { + let str = ""; + + if (typeExtend !== null) { + for (let key in typeExtend) { + if (key === "class" || key === "area" || key === "lang" || key === "year") { + try { + str += "筛选" + key + "+全部=+" + typeExtend[key].replace(/,/g, "+") + "\n"; + } catch (e) { } + } + } + } + + if (URL.includes(".vod")) { + str += "\n" + "排序+全部=+最新=time+最热=hits+评分=score"; + } else if (URL.includes("api.php/app") || URL.includes("xgapp")) { + // Do nothing, leave the string as it is. + } else { + str = "分类+全部=+电影=movie+连续剧=tvplay+综艺=tvshow+动漫=comic+4K=movie_4k+体育=tiyu\n筛选class+全部=+喜剧+爱情+恐怖+动作+科幻+剧情+战争+警匪+犯罪+动画+奇幻+武侠+冒险+枪战+恐怖+悬疑+惊悚+经典+青春+文艺+微电影+古装+历史+运动+农村+惊悚+惊悚+伦理+情色+福利+三级+儿童+网络电影\n筛选area+全部=+大陆+香港+台湾+美国+英国+法国+日本+韩国+德国+泰国+印度+西班牙+加拿大+其他\n筛选year+全部=+2025+2024+2023+2022+2021+2020+2019+2018+2017+2016+2015+2014+2013+2012+2011+2010+2009+2008+2007+2006+2005+2004+2003+2002+2001+2000"; + } + + return str; +} + +function getCateFilterUrlSuffix(URL) { + if (URL.includes("api.php/app") || URL.includes("xgapp")) { + return "&class=筛选class&area=筛选area&lang=筛选lang&year=筛选year&limit=18&pg=#PN#"; + } else if (URL.includes(".vod")) { + return "&class=筛选class&area=筛选area&lang=筛选lang&year=筛选year&by=排序&limit=18&page=#PN#"; + } else { + return "&page=#PN#&area=筛选area&type=筛选class&start=筛选year"; + } +} + +function getCateFilterUrlPrefix(URL) { + if (URL.includes("api.php/app") || URL.includes("xgapp")) { + return URL + "video?tid="; + } else if (URL.includes(".vod")) { + return URL + "?type="; + } else { + return URL + "?ac=list&class="; + } +} + +function isBan(key) { + return key === "伦理" || key === "情色" || key === "福利"; +} + +function getSearchUrl(URL, KEY) { + if (URL.includes(".vod")) { + return URL + "?wd=" + KEY + "&page="; + } else if (URL.includes("api.php/app") || URL.includes("xgapp")) { + return URL + "search?text=" + KEY + "&pg="; + } else if (urlPattern1.test(URL)) { + return URL + "?ac=list&zm=" + KEY + "&page="; + } + return ""; +} + +function findJsonArray(obj, match, result) { + Object.keys(obj).forEach((k) => { + try { + const o = obj[k]; + if (k === match && Array.isArray(o)) { + result.push(o); + } + if (typeof o === "object" && o !== null) { + if (Array.isArray(o)) { + o.forEach((item) => { + if (typeof item === "object" && item !== null) { + findJsonArray(item, match, result); + } + }); + } else { + findJsonArray(o, match, result); + } + } + } catch (e) { + SpiderDebug.log(e); + } + }); +} + +function jsonArr2Str(array) { + const strings = []; + for (let i = 0; i < array.length; i++) { + try { + strings.push(array[i]); + } catch (e) { + SpiderDebug.log(e); + } + } + return strings.join(","); +} + +function getHeaders(URL) { + const headers = {}; + headers["User-Agent"] = UA(URL); + return headers; +} + +function isJsonString(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +} + +export function __jsEvalReturn() { + return { + init: init, + home: home, + homeVod: homeVod, + category: category, + detail: detail, + play: play, + search: search, + }; +} \ No newline at end of file diff --git a/api/荐片.js b/api/荐片.js new file mode 100644 index 0000000..e29984c --- /dev/null +++ b/api/荐片.js @@ -0,0 +1,152 @@ +/** + * 荐片 新式 JS0 接口源 + * 适配 FongMi TVBox 最新规范 + */ + +let host = 'https://api.ztcgi.com'; +let UA = 'Mozilla/5.0 (Linux; Android 9; V2196A Build/PQ3A.190705.08211809; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Mobile Safari/537.36;webank/h5face;webank/1.0;netType:NETWORK_WIFI;appVersion:416;packageName:com.jp3.xg3'; +let imghost = ''; + +/** + * 初始化配置 + */ +async function init(cfg) { + try { + let res = await req(`${host}/api/appAuthConfig`, { headers: { 'User-Agent': UA } }); + let config = JSON.parse(res.content); + imghost = `https://${config.data.imgDomain}`; + } catch (e) { + imghost = 'https://img.jianpian.com'; + } +} + +/** + * 首页分类与筛选 + */ +async function home(filter) { + let classes = [ + {type_id: '1', type_name: '电影'}, + {type_id: '2', type_name: '电视剧'}, + {type_id: '3', type_name: '动漫'}, + {type_id: '4', type_name: '综艺'} + ]; + + // 筛选数据模板 + const filterItem = [ + {"key": "cateId", "name": "分类", "value": [{"v": "1", "n": "剧情"}, {"v": "2", "n": "爱情"}, {"v": "3", "n": "动画"}, {"v": "4", "n": "喜剧"}, {"v": "5", "n": "战争"}, {"v": "6", "n": "歌舞"}, {"v": "7", "n": "古装"}, {"v": "8", "n": "奇幻"}, {"v": "9", "n": "冒险"}, {"v": "10", "n": "动作"}, {"v": "11", "n": "科幻"}, {"v": "12", "n": "悬疑"}, {"v": "13", "n": "犯罪"}, {"v": "14", "n": "家庭"}, {"v": "15", "n": "传记"}, {"v": "16", "n": "运动"}, {"v": "18", "n": "惊悚"}, {"v": "20", "n": "短片"}, {"v": "21", "n": "历史"}, {"v": "22", "n": "音乐"}, {"v": "23", "n": "西部"}, {"v": "24", "n": "武侠"}, {"v": "25", "n": "恐怖"}]}, + {"key": "area", "name": "地區", "value": [{"v": "1", "n": "国产"}, {"v": "3", "n": "中国香港"}, {"v": "6", "n": "中国台湾"}, {"v": "5", "n": "美国"}, {"v": "18", "n": "韩国"}, {"v": "2", "n": "日本"}]}, + {"key": "year", "name": "年代", "value": [{"v": "107", "n": "2025"}, {"v": "119", "n": "2024"}, {"v": "153", "n": "2023"}, {"v": "101", "n": "2022"}, {"v": "118", "n": "2021"}, {"v": "16", "n": "2020"}, {"v": "7", "n": "2019"}, {"v": "22", "n": "2016"}, {"v": "2015", "n": "2015以前"}]}, + {"key": "sort", "name": "排序", "value": [{"v": "update", "n": "最新"}, {"v": "hot", "n": "最热"}, {"v": "rating", "n": "评分"}]} + ]; + + let filterObj = {"1": filterItem, "2": filterItem, "3": filterItem, "4": filterItem}; + + return JSON.stringify({ + class: classes, + filters: filterObj + }); +} + +/** + * 首页推荐 + */ +async function homeVod() { + let html = await req(`${host}/api/slide/list?pos_id=88`, { headers: { 'User-Agent': UA, 'Referer': host } }); + let res = JSON.parse(html.content); + let videos = res.data.map(item => ({ + vod_id: item.jump_id, + vod_name: item.title, + vod_pic: item.thumbnail.includes('http') ? item.thumbnail : `${imghost}${item.thumbnail}`, + vod_remarks: "" + })); + return JSON.stringify({ list: videos }); +} + +/** + * 分类列表 + */ +async function category(tid, pg, filter, extend) { + let url = `${host}/api/crumb/list?fcate_pid=${tid}&category_id=&area=${extend.area || ''}&year=${extend.year || ''}&type=${extend.cateId || ''}&sort=${extend.sort || ''}&page=${pg}`; + let html = await req(url, { headers: { 'User-Agent': UA, 'Referer': host } }); + let res = JSON.parse(html.content); + let videos = res.data.map(item => ({ + vod_id: item.id, + vod_name: item.title, + vod_pic: item.path.includes('http') ? item.path : `${imghost}${item.path}`, + vod_remarks: item.mask + })); + return JSON.stringify({ + page: pg, + list: videos + }); +} + +/** + * 详情页 + */ +async function detail(id) { + let html = await req(`${host}/api/video/detailv2?id=${id}`, { headers: { 'User-Agent': UA, 'Referer': host } }); + let data = JSON.parse(html.content).data; + + // 线路处理:将“常规线路”显示为“边下边播” + let play_from = data.source_list_source.map(item => item.name).join('$$$').replace(/常规线路/g, '边下边播'); + let play_url = data.source_list_source.map(play => + play.source_list.map(({source_name, url}) => `${source_name}$${url}`).join('#') + ).join('$$$'); + + let vod = { + vod_id: data.id, + vod_name: data.title, + vod_year: data.year, + vod_area: data.area, + vod_remarks: data.mask, + vod_content: data.description, + vod_play_from: play_from, + vod_play_url: play_url, + vod_pic: data.thumbnail.includes('http') ? data.thumbnail : `${imghost}${data.thumbnail}` + }; + + return JSON.stringify({ list: [vod] }); +} + +/** + * 搜索功能 + */ +async function search(wd, quick) { + let url = `${host}/api/v2/search/videoV2?key=${encodeURIComponent(wd)}&category_id=88&page=1&pageSize=20`; + let html = await req(url, { headers: { 'User-Agent': UA, 'Referer': host } }); + let res = JSON.parse(html.content); + let videos = res.data.map(item => ({ + vod_id: item.id, + vod_name: item.title, + vod_pic: item.thumbnail.includes('http') ? item.thumbnail : `${imghost}${item.thumbnail}`, + vod_remarks: item.mask + })); + return JSON.stringify({ list: videos }); +} + +/** + * 播放解析 + */ +async function play(flag, id, flags) { + let playUrl = id; + // 判断是否需要添加专用协议前缀 + if (!id.includes(".m3u8") && !id.includes(".mp4")) { + playUrl = `tvbox-xg:${id}`; + } + return JSON.stringify({ + parse: 0, + url: playUrl + }); +} + +// 导出标准接口对象 +export default { + init, + home, + homeVod, + category, + detail, + search, + play +}; diff --git a/api/金牌影院.js b/api/金牌影院.js new file mode 100644 index 0000000..74784cc --- /dev/null +++ b/api/金牌影院.js @@ -0,0 +1,169 @@ +import 'assets://js/lib/crypto-js.js'; +const { HOSTS, KEY, USER_AGENT } = { + HOSTS: ["https://hnytxj.com","https://www.hkybqufgh.com","https://www.sizhengxt.com","https://www.sdzhgt.com","https://www.jiabaide.cn","https://m.9zhoukj.com","https://m.cqzuoer.com","https://www.hellosht52bwb.com"], + KEY: "cb808529bae6b6be45ecfab29a4889bc", + USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.61 Safari/537.36" +}; +let currentHost = ''; +const guid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = Math.random() * 16 | 0; + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); +}); +const md5 = s => CryptoJS.MD5(s).toString(); +const sha1 = s => CryptoJS.SHA1(s).toString(); +const toQueryString = obj => Object.keys(obj).filter(k => obj[k] != null && obj[k] !== '').map(k => `${k}=${obj[k]}`).join('&'); +const getHeaders = (params = {}) => { + const t = Date.now().toString(); + const sign = sha1(md5(toQueryString({ ...params, key: KEY, t }))); + return { + 'User-Agent': USER_AGENT, + 'Accept': 'application/json, text/plain, */*', + 'sign': sign, + 't': t, + 'deviceid': guid() + }; +}; + +const normalizeFieldName = k => { + const l = k.toLowerCase(); + if (l.startsWith('vod') && l.length > 3) return 'vod_' + l.slice(3); + if (l.startsWith('type') && l.length > 4) return 'type_' + l.slice(4); + return l; +}; + +const normalizeVodList = list => (list || []).map(item => { + const res = {}; + for (const [k, v] of Object.entries(item || {})) if (v != null) res[normalizeFieldName(k)] = v; + return res; +}); + +async function reqSafe(url, options = {}) { + try { + const res = await req(url, options); + return JSON.parse(res.content); + } catch (e) { + return {}; + } +} + +async function init() { + currentHost = HOSTS[Math.floor(Math.random() * HOSTS.length)]; + return true; +} + +async function home() { + const [cRes, fRes] = await Promise.all([ + reqSafe(`${currentHost}/api/mw-movie/anonymous/get/filer/type`, { headers: getHeaders() }), + reqSafe(`${currentHost}/api/mw-movie/anonymous/v1/get/filer/list`, { headers: getHeaders() }) + ]); + + const classes = (cRes.data || []).map(k => ({ type_name: k.typeName, type_id: k.typeId.toString() })); + const fData = fRes.data || {}; + const baseSort = [{ n: "最近更新", v: "2" }, { n: "人气高低", v: "3" }, { n: "评分高低", v: "4" }]; + const filters = {}; + for (const [tid, d] of Object.entries(fData)) { + const sortValues = tid === '1' ? baseSort.slice(1) : baseSort; + const arr = [ + { key: "type", name: "类型", value: (d.typeList || []).map(i => ({ n: i.itemText, v: i.itemValue })) }, + { key: "area", name: "地区", value: (d.districtList || []).map(i => ({ n: i.itemText, v: i.itemText })) }, + { key: "year", name: "年份", value: (d.yearList || []).map(i => ({ n: i.itemText, v: i.itemText })) }, + { key: "lang", name: "语言", value: (d.languageList || []).map(i => ({ n: i.itemText, v: i.itemText })) }, + { key: "sort", name: "排序", value: sortValues } + ]; + if (d.plotList?.length) arr.splice(1, 0, { key: "v_class", name: "剧情", value: d.plotList.map(i => ({ n: i.itemText, v: i.itemText })) }); + filters[tid] = arr; + } + return JSON.stringify({ class: classes, filters }); +} + +async function homeVod() { + const [r1, r2] = await Promise.all([ + reqSafe(`${currentHost}/api/mw-movie/anonymous/v1/home/all/list`, { headers: getHeaders() }), + reqSafe(`${currentHost}/api/mw-movie/anonymous/home/hotSearch`, { headers: getHeaders() }) + ]); + let list = []; + const data1 = r1.data || {}; + for (const k in data1) if (data1[k]?.list) list.push(...data1[k].list); + if (Array.isArray(r2.data)) list.push(...r2.data); + return JSON.stringify({ list: normalizeVodList(list) }); +} + +async function category(tid, pg, _, ext = {}) { + const params = { + area: ext.area || '', + filterStatus: "1", + lang: ext.lang || '', + pageNum: pg, + pageSize: "30", + sort: ext.sort || '1', + sortBy: "1", + type: ext.type || '', + type1: tid, + v_class: ext.v_class || '', + year: ext.year || '' + }; + + const url = `${currentHost}/api/mw-movie/anonymous/video/list?${toQueryString(params)}`; + const res = await reqSafe(url, { headers: getHeaders(params) }); + const vodList = normalizeVodList(res.data?.list || []); + return JSON.stringify({ + list: vodList, + page: +pg, + pagecount: 9999, + limit: 90, + total: 999999 + }); +} + +async function detail(id) { + const res = await reqSafe(`${currentHost}/api/mw-movie/anonymous/video/detail?id=${id}`, { + headers: getHeaders({ id }) + }); + const vod = normalizeVodList([res.data])[0]; + if (!vod) { + return JSON.stringify({ list: [{ vod_id: id, vod_name: '加载失败', vod_play_url: '' }] }); + } + + vod.vod_play_from = '金牌影院'; + if (vod.episodelist?.length) { + const name = vod.episodelist.length > 1 ? vod.episodelist[0].name : vod.vod_name; + vod.vod_play_url = vod.episodelist.map(ep => `${name}$${id}@@${ep.nid}`).join('#'); + delete vod.episodelist; + } + return JSON.stringify({ list: [vod] }); +} + +async function play(_, id) { + const [vid, nid] = id.split('@@'); + const url = `${currentHost}/api/mw-movie/anonymous/v2/video/episode/url?clientType=1&id=${vid}&nid=${nid}`; + const res = await reqSafe(url, { headers: getHeaders({ clientType: '1', id: vid, nid: nid }) }); + const urls = []; + for (const item of res.data?.list || []) { + urls.push(item.resolutionName, item.url); + } + return JSON.stringify({ + parse: 0, + url: urls, + header: { + 'User-Agent': USER_AGENT, + 'sec-ch-ua-platform': '"Windows"', + 'DNT': '1', + 'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"', + 'sec-ch-ua-mobile': '?0', + 'Origin': currentHost, + 'Referer': currentHost + '/' + } + }); +} + +async function search(wd, _, pg = "1") { + const params = { keyword: wd, pageNum: pg, pageSize: "8", sourceCode: "1" }; + const url = `${currentHost}/api/mw-movie/anonymous/video/searchByWord?${toQueryString(params)}`; + const res = await reqSafe(url, { headers: getHeaders(params) }); + const list = normalizeVodList(res.data?.result?.list || []); + return JSON.stringify({ list, page: +pg }); +} + +export function __jsEvalReturn() { + return { init, home, homeVod, category, detail, play, proxy: null, search }; +}