376 lines
14 KiB
Python
376 lines
14 KiB
Python
# 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,
|
|
})
|
|
}
|