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; }
}
}