272 lines
11 KiB
C#
272 lines
11 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using BarrageGrab.Modles;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace BarrageGrab
|
|
{
|
|
/// <summary>
|
|
/// B站直播API帮助类
|
|
/// </summary>
|
|
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";
|
|
|
|
/// <summary>
|
|
/// 通过B站官方API获取房间信息 (推荐方式,稳定可靠)
|
|
/// 流程: get_info API 获取房间基本信息 -> Master/info API 获取主播昵称/头像
|
|
/// </summary>
|
|
public static async Task<BiliRoomInfo> 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<int>() != 0)
|
|
{
|
|
Logger.LogWarn($"B站 get_info API 返回错误: {roomJson["message"]?.Value<string>()}");
|
|
return null;
|
|
}
|
|
|
|
var data = roomJson["data"];
|
|
var info = new BiliRoomInfo
|
|
{
|
|
WebRoomId = webRoomId,
|
|
RoomId = data["room_id"]?.Value<long>().ToString() ?? webRoomId,
|
|
ShortId = data["short_id"]?.Value<long>().ToString() ?? "0",
|
|
Uid = data["uid"]?.Value<long>().ToString() ?? "",
|
|
Title = data["title"]?.Value<string>() ?? "",
|
|
IsLive = (data["live_status"]?.Value<int>() ?? 0) == 1,
|
|
Online = data["online"]?.Value<long>() ?? 0,
|
|
AreaName = data["area_name"]?.Value<string>() ?? "",
|
|
ParentAreaName = data["parent_area_name"]?.Value<string>() ?? "",
|
|
CoverUrl = data["user_cover"]?.Value<string>()
|
|
?? data["keyframe"]?.Value<string>()
|
|
?? ""
|
|
};
|
|
|
|
// 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<int>() == 0)
|
|
{
|
|
var masterData = masterJson["data"];
|
|
info.Nickname = masterData["info"]?["uname"]?.Value<string>() ?? "";
|
|
info.HeadUrl = masterData["info"]?["face"]?.Value<string>() ?? "";
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从B站直播间页面HTML中解析房间信息 (备用方案)
|
|
/// 注意: B站前端改版后页面可能不再注入 __INITIAL_STATE__, 此方法可能失效
|
|
/// </summary>
|
|
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<string>() ?? "";
|
|
info.IsLive = (roomInfoNode["live_id"]?.Value<long>() ?? 0) != 0;
|
|
info.Online = roomInfoNode["online"]?.Value<long>() ?? 0;
|
|
info.AreaName = roomInfoNode["area_name"]?.Value<string>() ?? "";
|
|
info.ParentAreaName = roomInfoNode["parent_area_name"]?.Value<string>() ?? "";
|
|
info.CoverUrl = roomInfoNode["cover"]?.Value<string>()
|
|
?? roomInfoNode["keyframe"]?.Value<string>()
|
|
?? "";
|
|
}
|
|
|
|
if (anchorInfoNode != null)
|
|
{
|
|
var baseInfo = anchorInfoNode["base_info"];
|
|
info.Nickname = baseInfo?["uname"]?.Value<string>()
|
|
?? anchorInfoNode["uname"]?.Value<string>()
|
|
?? "";
|
|
info.HeadUrl = baseInfo?["face"]?.Value<string>()
|
|
?? anchorInfoNode["face"]?.Value<string>()
|
|
?? "";
|
|
|
|
var relationInfo = anchorInfoNode["relation_info"];
|
|
if (relationInfo != null)
|
|
{
|
|
info.Attention = relationInfo["attribute"]?.Value<int>() ?? 0;
|
|
}
|
|
}
|
|
|
|
Logger.LogInfo($"B站HTML解析房间成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogWarn($"解析B站页面房间信息时出错: {ex.Message}");
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从HTML中提取 __INITIAL_STATE__ 后的完整JSON对象 (括号配对算法)
|
|
/// 不受 < 字符或 ;(function 等干扰, 比正则更可靠
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>JSON token 转字符串 (兼容整数/字符串类型)</summary>
|
|
private static string TokenToString(JToken token, string defaultValue)
|
|
{
|
|
if (token == null) return defaultValue;
|
|
return token.Type == JTokenType.Integer
|
|
? token.Value<long>().ToString()
|
|
: token.Value<string>() ?? 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// B站房间信息
|
|
/// </summary>
|
|
public class BiliRoomInfo
|
|
{
|
|
/// <summary>用户访问的房间号 (URL中的ID, 可能是短ID或真实room_id)</summary>
|
|
public string WebRoomId { get; set; }
|
|
/// <summary>真实的房间长ID</summary>
|
|
public string RoomId { get; set; }
|
|
/// <summary>短ID (无短ID时为"0")</summary>
|
|
public string ShortId { get; set; }
|
|
/// <summary>主播UID</summary>
|
|
public string Uid { get; set; }
|
|
/// <summary>直播间标题</summary>
|
|
public string Title { get; set; }
|
|
/// <summary>主播昵称</summary>
|
|
public string Nickname { get; set; }
|
|
/// <summary>主播头像</summary>
|
|
public string HeadUrl { get; set; }
|
|
/// <summary>直播间封面</summary>
|
|
public string CoverUrl { get; set; }
|
|
/// <summary>是否直播中</summary>
|
|
public bool IsLive { get; set; }
|
|
/// <summary>在线人数</summary>
|
|
public long Online { get; set; }
|
|
/// <summary>子分区名</summary>
|
|
public string AreaName { get; set; }
|
|
/// <summary>父分区名</summary>
|
|
public string ParentAreaName { get; set; }
|
|
/// <summary>关注状态 (0未关注 2已关注 6已互关)</summary>
|
|
public int Attention { get; set; }
|
|
/// <summary>直播间地址</summary>
|
|
public string LiveUrl { get; set; }
|
|
}
|
|
}
|