feat(core): connect to danmaku server with buvid3 from cookie & refactor HttpApiClient (#507)

* feat(core): connect to danmaku server with buvid3

* make buvid3 nullable & add json serializer setting

* fix test

* fix regex & make regex static

* remove anonHttpClient

* remove MainHttpClient

* split FetchAsTextAsync

* GetAnonymousCookieAsync

* use template string in TestCookieAsync

* fix test

* background get anonymous cookie

* make jsonSerializerSettings static

* fix uid parse logic

* fix buvid match

* remove GetAnonCookie

* restore merge typo

* fix comment

* rename ICookieTester
This commit is contained in:
进栈检票 2023-07-16 16:01:50 +08:00 committed by GitHub
parent a073e5fe4d
commit adc91cc4f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 72 additions and 79 deletions

View File

@ -33,6 +33,8 @@ namespace BililiveRecorder.Core.Api.Danmaku
public Func<string, string?>? BeforeHandshake { get; set; } = null;
private static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
public DanmakuClient(IDanmakuServerApiClient apiClient, ILogger logger)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
@ -98,7 +100,7 @@ namespace BililiveRecorder.Core.Api.Danmaku
this.danmakuTransport = transport;
await this.SendHelloAsync(roomid, this.apiClient.GetUid(), danmakuServerInfo.Token ?? string.Empty).ConfigureAwait(false);
await this.SendHelloAsync(roomid, this.apiClient.GetUid(), this.apiClient.GetBuvid3(), danmakuServerInfo.Token ?? string.Empty).ConfigureAwait(false);
await this.SendPingAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
@ -213,17 +215,18 @@ namespace BililiveRecorder.Core.Api.Danmaku
#region Send
private Task SendHelloAsync(int roomid, long uid, string token)
private Task SendHelloAsync(int roomid, long uid, string? buvid, string token)
{
var body = JsonConvert.SerializeObject(new
{
uid = uid,
roomid = roomid,
uid,
roomid,
protover = 0,
buvid,
platform = "web",
type = 2,
key = token,
}, Formatting.None);
}, Formatting.None, jsonSerializerSettings);
if (this.BeforeHandshake is { } func)
{

View File

@ -12,40 +12,29 @@ using Newtonsoft.Json.Linq;
namespace BililiveRecorder.Core.Api.Http
{
internal class HttpApiClient : IApiClient, IDanmakuServerApiClient, IHttpClientAccessor
internal class HttpApiClient : IApiClient, IDanmakuServerApiClient, ICookieTester
{
internal const string HttpHeaderAccept = "application/json, text/javascript, */*; q=0.01";
internal const string HttpHeaderReferer = "https://live.bilibili.com/";
internal const string HttpHeaderOrigin = "https://live.bilibili.com";
internal const string HttpHeaderUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36";
private readonly Regex matchCookieUidRegex = new Regex(@"DedeUserID=(\d+?);", RegexOptions.Compiled);
private static readonly Regex matchCookieUidRegex = new Regex(@"DedeUserID=(\d+?);", RegexOptions.Compiled);
private static readonly Regex matchCookieBuvid3Regex = new Regex(@"buvid3=(.+?);", RegexOptions.Compiled);
private long uid;
private string? buvid3;
private readonly GlobalConfig config;
private readonly HttpClient anonClient;
private HttpClient mainClient;
private HttpClient client;
private bool disposedValue;
public HttpClient MainHttpClient => this.mainClient;
public HttpApiClient(GlobalConfig config)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
config.PropertyChanged += this.Config_PropertyChanged;
this.mainClient = null!;
this.client = null!;
this.UpdateHttpClient();
this.anonClient = new HttpClient
{
Timeout = TimeSpan.FromMilliseconds(config.TimingApiTimeout)
};
var headers = this.anonClient.DefaultRequestHeaders;
headers.Add("Accept", HttpHeaderAccept);
headers.Add("Origin", HttpHeaderOrigin);
headers.Add("Referer", HttpHeaderReferer);
headers.Add("User-Agent", HttpHeaderUserAgent);
}
private void UpdateHttpClient()
@ -68,16 +57,20 @@ namespace BililiveRecorder.Core.Api.Http
if (!string.IsNullOrWhiteSpace(cookie_string))
{
headers.Add("Cookie", cookie_string);
long.TryParse(this.matchCookieUidRegex.Match(cookie_string).Groups[1].Value, out var uid);
long.TryParse(matchCookieUidRegex.Match(cookie_string).Groups[1].Value, out var uid);
this.uid = uid;
string buvid3 = matchCookieBuvid3Regex.Match(cookie_string).Groups[1].Value;
if (!string.IsNullOrWhiteSpace(buvid3))
this.buvid3 = buvid3;
else
this.buvid3 = null;
}
else
{
this.uid = 0;
}
var old = Interlocked.Exchange(ref this.mainClient, client);
var old = Interlocked.Exchange(ref this.client, client);
old?.Dispose();
}
@ -87,19 +80,21 @@ namespace BililiveRecorder.Core.Api.Http
this.UpdateHttpClient();
}
private static async Task<BilibiliApiResponse<T>> FetchAsync<T>(HttpClient client, string url) where T : class
private async Task<string> FetchAsTextAsync(string url)
{
// 记得 GetRoomInfoAsync 里复制了一份这里的代码,以后修改记得一起改了
var resp = await client.GetAsync(url).ConfigureAwait(false);
var resp = await this.client.GetAsync(url).ConfigureAwait(false);
if (resp.StatusCode == (HttpStatusCode)412)
throw new Http412Exception("Got HTTP Status 412 when requesting " + url);
resp.EnsureSuccessStatusCode();
var text = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
return await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
}
private async Task<BilibiliApiResponse<T>> FetchAsync<T>(string url) where T : class
{
var text = await this.FetchAsTextAsync(url).ConfigureAwait(false);
var obj = JsonConvert.DeserializeObject<BilibiliApiResponse<T>>(text);
return obj?.Code != 0 ? throw new BilibiliApiResponseCodeNotZeroException(obj?.Code, text) : obj;
}
@ -111,18 +106,7 @@ namespace BililiveRecorder.Core.Api.Http
var url = $@"{this.config.LiveApiHost}/xlive/web-room/v1/index/getInfoByRoom?room_id={roomid}";
// return FetchAsync<RoomInfo>(this.mainClient, url);
// 下面的代码是从 FetchAsync 里复制修改的
// 以后如果修改 FetchAsync 记得把这里也跟着改了
var resp = await this.mainClient.GetAsync(url).ConfigureAwait(false);
if (resp.StatusCode == (HttpStatusCode)412)
throw new Http412Exception("Got HTTP Status 412 when requesting " + url);
resp.EnsureSuccessStatusCode();
var text = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
var text = await this.FetchAsTextAsync(url).ConfigureAwait(false);
var jobject = JObject.Parse(text);
@ -141,18 +125,35 @@ namespace BililiveRecorder.Core.Api.Http
throw new ObjectDisposedException(nameof(HttpApiClient));
var url = $@"{this.config.LiveApiHost}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={roomid}&protocol=0,1&format=0,1,2&codec=0,1&qn={qn}&platform=web&ptype=8&dolby=5&panorama=1";
return FetchAsync<RoomPlayInfo>(this.mainClient, url);
return this.FetchAsync<RoomPlayInfo>(url);
}
public async Task<(bool, string)> TestCookieAsync()
{
// 需要测试 cookie 的情况不需要风控和失败检测
var resp = await this.client.GetStringAsync("https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info").ConfigureAwait(false);
var jo = JObject.Parse(resp);
if (jo["code"]?.ToObject<int>() != 0)
return (false, $"Response:\n{resp}");
string message = $@"User: {jo["data"]?["uname"]?.ToObject<string>()}
UID (from API response): {jo["data"]?["uid"]?.ToObject<string>()}
UID (from Cookie): {this.GetUid()}
BUVID3 (from Cookie): {this.GetBuvid3()}";
return (true, message);
}
public long GetUid() => this.uid;
public string? GetBuvid3() => this.buvid3;
public Task<BilibiliApiResponse<DanmuInfo>> GetDanmakuServerAsync(int roomid)
{
if (this.disposedValue)
throw new ObjectDisposedException(nameof(HttpApiClient));
var url = $@"{this.config.LiveApiHost}/xlive/web-room/v1/index/getDanmuInfo?id={roomid}&type=0";
return FetchAsync<DanmuInfo>(this.mainClient, url);
return this.FetchAsync<DanmuInfo>(url);
}
protected virtual void Dispose(bool disposing)
@ -163,8 +164,7 @@ namespace BililiveRecorder.Core.Api.Http
{
// dispose managed state (managed objects)
this.config.PropertyChanged -= this.Config_PropertyChanged;
this.mainClient.Dispose();
this.anonClient.Dispose();
this.client.Dispose();
}
// free unmanaged resources (unmanaged objects) and override finalizer

View File

@ -7,6 +7,7 @@ namespace BililiveRecorder.Core.Api
internal interface IDanmakuServerApiClient : IDisposable
{
long GetUid();
string? GetBuvid3();
Task<BilibiliApiResponse<DanmuInfo>> GetDanmakuServerAsync(int roomid);
}
}

View File

@ -1,10 +1,10 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace BililiveRecorder.Core.Api
{
public interface IHttpClientAccessor
public interface ICookieTester
{
HttpClient MainHttpClient { get; }
long GetUid();
Task<(bool, string)> TestCookieAsync();
}
}

View File

@ -18,6 +18,7 @@ namespace BililiveRecorder.Core.Api
}
public long GetUid() => this.client.GetUid();
public string? GetBuvid3() => this.client.GetBuvid3();
public async Task<BilibiliApiResponse<DanmuInfo>> GetDanmakuServerAsync(int roomid) => await this.policies
.Get<IAsyncPolicy>(PolicyNames.PolicyDanmakuApiRequestAsync)

View File

@ -37,7 +37,7 @@ namespace BililiveRecorder.DependencyInjection
public static IServiceCollection AddRecorderApiClients(this IServiceCollection services) => services
.AddSingleton<HttpApiClient>()
.AddSingleton<IHttpClientAccessor>(sp => sp.GetRequiredService<HttpApiClient>())
.AddSingleton<ICookieTester>(sp => sp.GetRequiredService<HttpApiClient>())
.AddSingleton<PolicyWrappedApiClient<HttpApiClient>>()
.AddSingleton<IApiClient>(sp => sp.GetRequiredService<PolicyWrappedApiClient<HttpApiClient>>())
.AddSingleton<IDanmakuServerApiClient>(sp => sp.GetRequiredService<PolicyWrappedApiClient<HttpApiClient>>())

View File

@ -40,10 +40,10 @@
<StackPanel Orientation="Vertical">
<TextBlock TextWrapping="Wrap">
建议使用小号;软件开发者不对账号发生的任何事情负责。<LineBreak/>
账号 UID 是直接从 Cookie 中的 DedeUserID 读取的,连接弹幕服务器时会用。<LineBreak/>
账号 UID 和 BUVID3 是直接从 Cookie 中的 DedeUserID 和 buvid3 读取的,连接弹幕服务器时会用。<LineBreak/>
录播姬没有主动断开重连弹幕服务器的功能,如果你设置 Cookie 的目的是以登录状态连接弹幕服务器,建议修改设置后重启录播姬。<LineBreak/>
Alt account highly recommended; developers are not responsible for anything happened to your account.<LineBreak/>
Account UID is read directly from DedeUserID in Cookie, and will be used when connecting to the danmaku server.<LineBreak/>
Account UID and BUVID3 is read directly from DedeUserID and buvid3 in Cookie, and will be used when connecting to the danmaku server.<LineBreak/>
BililiveRecorder does not have the ability to reconnect to the danmaku server. If you set Cookie to connect to the danmaku server in logged-in state, it is recommended to restart BililiveRecorder after changing the setting.
</TextBlock>
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasCookie}">

View File

@ -17,10 +17,10 @@ namespace BililiveRecorder.WPF.Pages
public partial class AdvancedSettingsPage
{
private static readonly ILogger logger = Log.ForContext<AdvancedSettingsPage>();
private readonly IHttpClientAccessor? httpApiClient;
private readonly ICookieTester? httpApiClient;
private readonly UserScriptRunner? userScriptRunner;
public AdvancedSettingsPage(IHttpClientAccessor? httpApiClient, UserScriptRunner? userScriptRunner)
public AdvancedSettingsPage(ICookieTester? httpApiClient, UserScriptRunner? userScriptRunner)
{
this.InitializeComponent();
this.httpApiClient = httpApiClient;
@ -29,7 +29,7 @@ namespace BililiveRecorder.WPF.Pages
public AdvancedSettingsPage()
: this(
(IHttpClientAccessor?)(RootPage.ServiceProvider?.GetService(typeof(IHttpClientAccessor))),
(ICookieTester?)(RootPage.ServiceProvider?.GetService(typeof(ICookieTester))),
(UserScriptRunner?)(RootPage.ServiceProvider?.GetService(typeof(UserScriptRunner)))
)
{ }
@ -66,29 +66,18 @@ namespace BililiveRecorder.WPF.Pages
private async Task TestCookieAsync()
{
bool succeed;
string message;
if (this.httpApiClient is null)
{
MessageBox.Show("No Http Client Available", "Cookie Test - Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
(succeed, message) = (false, "No Http Client Available");
else
(succeed, message) = await this.httpApiClient.TestCookieAsync().ConfigureAwait(false);
var resp = await this.httpApiClient.MainHttpClient.GetStringAsync("https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info").ConfigureAwait(false);
var jo = JObject.Parse(resp);
if (jo["code"]?.ToObject<int>() != 0)
{
MessageBox.Show("Response:\n" + resp, "Cookie Test - Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var b = new StringBuilder();
b.Append("User: ");
b.Append(jo["data"]?["uname"]?.ToObject<string>());
b.Append("\nUID (from API response): ");
b.Append(jo["data"]?["uid"]?.ToObject<string>());
b.Append("\nUID (from Cookie): ");
b.Append(this.httpApiClient.GetUid());
MessageBox.Show(b.ToString(), "Cookie Test - Successed", MessageBoxButton.OK, MessageBoxImage.Information);
if (succeed)
MessageBox.Show(message, "Cookie Test - Succeed", MessageBoxButton.OK, MessageBoxImage.Information);
else
MessageBox.Show(message, "Cookie Test - Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
}
private void TestScript_Click(object sender, RoutedEventArgs e)

View File

@ -1,10 +1,9 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("BililiveRecorder.Core.UnitTests")]
namespace BililiveRecorder.Core.Api
{
public interface IHttpClientAccessor
public interface ICookieTester
{
System.Net.Http.HttpClient MainHttpClient { get; }
long GetUid();
System.Threading.Tasks.Task<System.ValueTuple<bool, string>> TestCookieAsync();
}
}
namespace BililiveRecorder.Core.Config