using System; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using BarrageGrab.Modles; using Newtonsoft.Json.Linq; namespace BarrageGrab { /// /// B站直播API帮助类 /// internal class BiliApiHelper { private const string UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"; /// /// 通过B站官方API获取房间信息 (推荐方式,稳定可靠) /// 流程: get_info API 获取房间基本信息 -> Master/info API 获取主播昵称/头像 /// public static async Task GetRoomInfoByApi(string webRoomId, string cookie = null) { if (string.IsNullOrWhiteSpace(webRoomId)) return null; var client = CreateHttpClient(cookie); try { // 1. 获取房间信息 (get_info 支持短ID/长ID自动转换) string roomInfoUrl = $"https://api.live.bilibili.com/room/v1/Room/get_info?room_id={webRoomId}"; var roomResp = await client.GetStringAsync(roomInfoUrl); var roomJson = JObject.Parse(roomResp); if (roomJson["code"]?.Value() != 0) { Logger.LogWarn($"B站 get_info API 返回错误: {roomJson["message"]?.Value()}"); return null; } var data = roomJson["data"]; var info = new BiliRoomInfo { WebRoomId = webRoomId, RoomId = data["room_id"]?.Value().ToString() ?? webRoomId, ShortId = data["short_id"]?.Value().ToString() ?? "0", Uid = data["uid"]?.Value().ToString() ?? "", Title = data["title"]?.Value() ?? "", IsLive = (data["live_status"]?.Value() ?? 0) == 1, Online = data["online"]?.Value() ?? 0, AreaName = data["area_name"]?.Value() ?? "", ParentAreaName = data["parent_area_name"]?.Value() ?? "", CoverUrl = data["user_cover"]?.Value() ?? data["keyframe"]?.Value() ?? "" }; // 2. 获取主播昵称和头像 if (!string.IsNullOrWhiteSpace(info.Uid)) { try { string masterUrl = $"https://api.live.bilibili.com/live_user/v1/Master/info?uid={info.Uid}"; var masterResp = await client.GetStringAsync(masterUrl); var masterJson = JObject.Parse(masterResp); if (masterJson["code"]?.Value() == 0) { var masterData = masterJson["data"]; info.Nickname = masterData["info"]?["uname"]?.Value() ?? ""; info.HeadUrl = masterData["info"]?["face"]?.Value() ?? ""; } } catch (Exception ex) { Logger.LogWarn($"获取B站主播信息失败: {ex.Message}"); } } if (string.IsNullOrWhiteSpace(info.Nickname)) { info.Nickname = $"B站用户{info.Uid}"; } Logger.LogInfo($"B站API获取房间信息成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}, Online={info.Online}, IsLive={info.IsLive}"); return info; } catch (Exception ex) { Logger.LogError(ex, $"B站API获取房间信息失败: {ex.Message}"); return null; } } /// /// 从B站直播间页面HTML中解析房间信息 (备用方案) /// 注意: B站前端改版后页面可能不再注入 __INITIAL_STATE__, 此方法可能失效 /// public static BiliRoomInfo ParseRoomInfoFromHtml(string html, string webRoomId) { var info = new BiliRoomInfo { WebRoomId = webRoomId }; if (string.IsNullOrWhiteSpace(html)) return info; try { // 用括号配对算法可靠提取 __INITIAL_STATE__ 的 JSON string jsonStr = ExtractJsonObject(html, "__INITIAL_STATE__"); if (string.IsNullOrWhiteSpace(jsonStr)) { // B站改版后可能不再注入 __INITIAL_STATE__, 返回空对象由调用方走API return info; } JObject root; try { root = JObject.Parse(jsonStr); } catch (Exception ex) { Logger.LogWarn($"B站 __INITIAL_STATE__ JSON 解析失败: {ex.Message}"); return info; } var roomInfoNode = root["roomInfo"]?["roomInfo"]; var anchorInfoNode = root["roomInfo"]?["anchorInfo"]; if (roomInfoNode != null) { info.RoomId = TokenToString(roomInfoNode["room_id"], webRoomId); info.ShortId = TokenToString(roomInfoNode["short_id"], "0"); info.Uid = TokenToString(roomInfoNode["uid"], ""); info.Title = roomInfoNode["title"]?.Value() ?? ""; info.IsLive = (roomInfoNode["live_id"]?.Value() ?? 0) != 0; info.Online = roomInfoNode["online"]?.Value() ?? 0; info.AreaName = roomInfoNode["area_name"]?.Value() ?? ""; info.ParentAreaName = roomInfoNode["parent_area_name"]?.Value() ?? ""; info.CoverUrl = roomInfoNode["cover"]?.Value() ?? roomInfoNode["keyframe"]?.Value() ?? ""; } if (anchorInfoNode != null) { var baseInfo = anchorInfoNode["base_info"]; info.Nickname = baseInfo?["uname"]?.Value() ?? anchorInfoNode["uname"]?.Value() ?? ""; info.HeadUrl = baseInfo?["face"]?.Value() ?? anchorInfoNode["face"]?.Value() ?? ""; var relationInfo = anchorInfoNode["relation_info"]; if (relationInfo != null) { info.Attention = relationInfo["attribute"]?.Value() ?? 0; } } Logger.LogInfo($"B站HTML解析房间成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}"); } catch (Exception ex) { Logger.LogWarn($"解析B站页面房间信息时出错: {ex.Message}"); } return info; } /// /// 从HTML中提取 __INITIAL_STATE__ 后的完整JSON对象 (括号配对算法) /// 不受 < 字符或 ;(function 等干扰, 比正则更可靠 /// private static string ExtractJsonObject(string html, string marker) { int idx = html.IndexOf(marker); if (idx < 0) return null; // 找到 = 后面的第一个 { int eqIdx = html.IndexOf('=', idx); if (eqIdx < 0) return null; int braceIdx = html.IndexOf('{', eqIdx); if (braceIdx < 0) return null; // 逐字符扫描计算 { } 嵌套层级, 同时处理字符串和转义 int depth = 0; bool inString = false; bool escape = false; for (int i = braceIdx; i < html.Length; i++) { char c = html[i]; if (escape) { escape = false; continue; } if (c == '\\') { escape = true; continue; } if (c == '"') { inString = !inString; continue; } if (inString) continue; if (c == '{') depth++; else if (c == '}') { depth--; if (depth == 0) { return html.Substring(braceIdx, i - braceIdx + 1); } } } return null; } /// JSON token 转字符串 (兼容整数/字符串类型) private static string TokenToString(JToken token, string defaultValue) { if (token == null) return defaultValue; return token.Type == JTokenType.Integer ? token.Value().ToString() : token.Value() ?? defaultValue; } private static HttpClient CreateHttpClient(string cookie) { var client = new HttpClient(); client.DefaultRequestHeaders.Add("User-Agent", UA); client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*"); client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9"); client.DefaultRequestHeaders.Add("Referer", "https://live.bilibili.com/"); if (!string.IsNullOrWhiteSpace(cookie)) { client.DefaultRequestHeaders.Add("Cookie", cookie); } return client; } } /// /// B站房间信息 /// public class BiliRoomInfo { /// 用户访问的房间号 (URL中的ID, 可能是短ID或真实room_id) public string WebRoomId { get; set; } /// 真实的房间长ID public string RoomId { get; set; } /// 短ID (无短ID时为"0") public string ShortId { get; set; } /// 主播UID public string Uid { get; set; } /// 直播间标题 public string Title { get; set; } /// 主播昵称 public string Nickname { get; set; } /// 主播头像 public string HeadUrl { get; set; } /// 直播间封面 public string CoverUrl { get; set; } /// 是否直播中 public bool IsLive { get; set; } /// 在线人数 public long Online { get; set; } /// 子分区名 public string AreaName { get; set; } /// 父分区名 public string ParentAreaName { get; set; } /// 关注状态 (0未关注 2已关注 6已互关) public int Attention { get; set; } /// 直播间地址 public string LiveUrl { get; set; } } }