diff --git a/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs b/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs index 0d3b8e8..3d93fac 100644 --- a/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs +++ b/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs @@ -33,6 +33,8 @@ namespace BililiveRecorder.Core.Api.Danmaku public Func? 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) { diff --git a/BililiveRecorder.Core/Api/Http/HttpApiClient.cs b/BililiveRecorder.Core/Api/Http/HttpApiClient.cs index bad7a1c..d8fa789 100644 --- a/BililiveRecorder.Core/Api/Http/HttpApiClient.cs +++ b/BililiveRecorder.Core/Api/Http/HttpApiClient.cs @@ -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> FetchAsync(HttpClient client, string url) where T : class + private async Task 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> FetchAsync(string url) where T : class + { + var text = await this.FetchAsTextAsync(url).ConfigureAwait(false); var obj = JsonConvert.DeserializeObject>(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(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(this.mainClient, url); + return this.FetchAsync(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() != 0) + return (false, $"Response:\n{resp}"); + + string message = $@"User: {jo["data"]?["uname"]?.ToObject()} +UID (from API response): {jo["data"]?["uid"]?.ToObject()} +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> 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(this.mainClient, url); + return this.FetchAsync(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 diff --git a/BililiveRecorder.Core/Api/IDanmakuServerApiClient.cs b/BililiveRecorder.Core/Api/IDanmakuServerApiClient.cs index 22c9c4d..82a8525 100644 --- a/BililiveRecorder.Core/Api/IDanmakuServerApiClient.cs +++ b/BililiveRecorder.Core/Api/IDanmakuServerApiClient.cs @@ -7,6 +7,7 @@ namespace BililiveRecorder.Core.Api internal interface IDanmakuServerApiClient : IDisposable { long GetUid(); + string? GetBuvid3(); Task> GetDanmakuServerAsync(int roomid); } } diff --git a/BililiveRecorder.Core/Api/IHttpClientAccessor.cs b/BililiveRecorder.Core/Api/IHttpClientAccessor.cs index 9aa260d..611be05 100644 --- a/BililiveRecorder.Core/Api/IHttpClientAccessor.cs +++ b/BililiveRecorder.Core/Api/IHttpClientAccessor.cs @@ -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(); } } diff --git a/BililiveRecorder.Core/Api/PolicyWrappedApiClient.cs b/BililiveRecorder.Core/Api/PolicyWrappedApiClient.cs index 1d22aa2..c298c48 100644 --- a/BililiveRecorder.Core/Api/PolicyWrappedApiClient.cs +++ b/BililiveRecorder.Core/Api/PolicyWrappedApiClient.cs @@ -18,6 +18,7 @@ namespace BililiveRecorder.Core.Api } public long GetUid() => this.client.GetUid(); + public string? GetBuvid3() => this.client.GetBuvid3(); public async Task> GetDanmakuServerAsync(int roomid) => await this.policies .Get(PolicyNames.PolicyDanmakuApiRequestAsync) diff --git a/BililiveRecorder.Core/DependencyInjectionExtensions.cs b/BililiveRecorder.Core/DependencyInjectionExtensions.cs index 52a1ca9..023fc4b 100644 --- a/BililiveRecorder.Core/DependencyInjectionExtensions.cs +++ b/BililiveRecorder.Core/DependencyInjectionExtensions.cs @@ -37,7 +37,7 @@ namespace BililiveRecorder.DependencyInjection public static IServiceCollection AddRecorderApiClients(this IServiceCollection services) => services .AddSingleton() - .AddSingleton(sp => sp.GetRequiredService()) + .AddSingleton(sp => sp.GetRequiredService()) .AddSingleton>() .AddSingleton(sp => sp.GetRequiredService>()) .AddSingleton(sp => sp.GetRequiredService>()) diff --git a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml index aba563d..b3cc50d 100644 --- a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml +++ b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml @@ -40,10 +40,10 @@ 建议使用小号;软件开发者不对账号发生的任何事情负责。 - 账号 UID 是直接从 Cookie 中的 DedeUserID 读取的,连接弹幕服务器时会用。 + 账号 UID 和 BUVID3 是直接从 Cookie 中的 DedeUserID 和 buvid3 读取的,连接弹幕服务器时会用。 录播姬没有主动断开重连弹幕服务器的功能,如果你设置 Cookie 的目的是以登录状态连接弹幕服务器,建议修改设置后重启录播姬。 Alt account highly recommended; developers are not responsible for anything happened to your account. - Account UID is read directly from DedeUserID in Cookie, and will be used when connecting to the danmaku server. + Account UID and BUVID3 is read directly from DedeUserID and buvid3 in Cookie, and will be used when connecting to the danmaku server. 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. diff --git a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs index b704c5c..3052158 100644 --- a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs +++ b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs @@ -17,10 +17,10 @@ namespace BililiveRecorder.WPF.Pages public partial class AdvancedSettingsPage { private static readonly ILogger logger = Log.ForContext(); - 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() != 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()); - b.Append("\nUID (from API response): "); - b.Append(jo["data"]?["uid"]?.ToObject()); - 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) diff --git a/test/BililiveRecorder.Core.UnitTests/Expectations/PublicApi.HasNoChangesAsync.verified.txt b/test/BililiveRecorder.Core.UnitTests/Expectations/PublicApi.HasNoChangesAsync.verified.txt index 3268ebc..9471bb6 100644 --- a/test/BililiveRecorder.Core.UnitTests/Expectations/PublicApi.HasNoChangesAsync.verified.txt +++ b/test/BililiveRecorder.Core.UnitTests/Expectations/PublicApi.HasNoChangesAsync.verified.txt @@ -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> TestCookieAsync(); } } namespace BililiveRecorder.Core.Config