Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): get when no cookie & connect to danmaku server with buvid3 #503

Draft
wants to merge 14 commits into
base: dev
Choose a base branch
from
Draft
13 changes: 8 additions & 5 deletions BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ internal class DanmakuClient : IDanmakuClient, IDisposable

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));
Expand Down Expand Up @@ -98,7 +100,7 @@ public async Task ConnectAsync(int roomid, DanmakuTransportMode transportMode, C

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)
Expand Down Expand Up @@ -213,17 +215,18 @@ public void Dispose()

#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)
{
Expand Down
137 changes: 93 additions & 44 deletions BililiveRecorder.Core/Api/Http/HttpApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,34 @@
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 static readonly Regex matchSetCookieBuvid3Regex = new Regex(@"(buvid3=.+?;)", RegexOptions.Compiled);
private long uid;
private string? buvid3;
private Task? anon_cookie_task;
private CancellationTokenSource anon_cookie_task_cancel;

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.anon_cookie_task_cancel = new CancellationTokenSource();

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()
{
this.anon_cookie_task_cancel.Cancel();

var client = new HttpClient(new HttpClientHandler
{
UseCookies = false,
Expand All @@ -65,20 +61,28 @@
headers.Add("User-Agent", HttpHeaderUserAgent);

var cookie_string = this.config.Cookie;
if (!string.IsNullOrWhiteSpace(cookie_string))
{

bool anon = string.IsNullOrWhiteSpace(cookie_string);
if (!anon)
headers.Add("Cookie", cookie_string);

long.TryParse(this.matchCookieUidRegex.Match(cookie_string).Groups[1].Value, out var uid);
this.uid = uid;
// 注意 BackgroundGetAnonCookie 操作的是当前的 this.client 所以要提前 swap
var old = Interlocked.Exchange(ref this.client, client);
old?.Dispose();

if (anon)
{
this.uid = 0;
var new_task = Task.Run(() => this.BackgroundGetAnonCookie(), this.anon_cookie_task_cancel.Token);
var old_task = Interlocked.Exchange(ref this.anon_cookie_task, new_task);
old_task?.Dispose();
}
else
{
this.uid = 0;
long.TryParse(matchCookieUidRegex.Match(cookie_string).Groups[1].Value, out var uid);
this.uid = uid;
this.buvid3 = matchCookieBuvid3Regex.Match(cookie_string).Groups[1].Value;
}

var old = Interlocked.Exchange(ref this.mainClient, client);
old?.Dispose();
}

private void Config_PropertyChanged(object sender, PropertyChangedEventArgs e)
Expand All @@ -87,19 +91,26 @@
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 里复制了一份这里的代码,以后修改记得一起改了
if (this.anon_cookie_task is not null)
await Interlocked.Exchange(ref this.anon_cookie_task, null)!;

var resp = await client.GetAsync(url).ConfigureAwait(false);
var resp = await this.client.GetAsync(url).ConfigureAwait(false);

// 部分逻辑可能与 GetAnonCookieAsync 共享

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;
}
Expand All @@ -111,18 +122,7 @@

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

Expand All @@ -141,18 +141,68 @@
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 的情况不需要风控和失败检测,同时不需要等待未登录 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 static async Task<string?> GetAnonCookieAsync(HttpClient client)
{
var url = @"https://data.bilibili.com/v/";

var resp = await client.GetAsync(url).ConfigureAwait(false);

// 部分逻辑可能与 FetchAsTextAsync 共享
// 应该允许获取 cookie 失败,但对于风控情况仍应处理

if (resp.StatusCode == (HttpStatusCode)412)
throw new Http412Exception("Got HTTP Status 412 when requesting " + url);

if (!resp.IsSuccessStatusCode)
return null;

foreach (var setting_cookie in resp.Content.Headers.GetValues("Set-Cookie"))
{
var buvid3_cookie = matchSetCookieBuvid3Regex.Match(setting_cookie).Groups[1].Value;
if (buvid3_cookie != null)
return buvid3_cookie;
}

return null;
}

private async Task BackgroundGetAnonCookie()

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (any)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (any)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (linux-arm)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (linux-arm)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (linux-arm64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (linux-arm64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (linux-x64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (linux-x64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (osx-x64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (osx-x64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (osx-arm64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (osx-arm64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (win-x64)

Use "Async" suffix in names of methods that return an awaitable type

Check warning on line 187 in BililiveRecorder.Core/Api/Http/HttpApiClient.cs

View workflow job for this annotation

GitHub Actions / build_cli (win-x64)

Use "Async" suffix in names of methods that return an awaitable type
{
string? cookie_string = await GetAnonCookieAsync(this.client).ConfigureAwait(false);
var headers = this.client.DefaultRequestHeaders;
if (!string.IsNullOrWhiteSpace(cookie_string))
headers.Add("Cookie", cookie_string);
}

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)
Expand All @@ -163,8 +213,7 @@
{
// 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
Expand Down
1 change: 1 addition & 0 deletions BililiveRecorder.Core/Api/IDanmakuServerApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace BililiveRecorder.Core.Api
internal interface IDanmakuServerApiClient : IDisposable
{
long GetUid();
string? GetBuvid3();
Task<BilibiliApiResponse<DanmuInfo>> GetDanmakuServerAsync(int roomid);
}
}
4 changes: 2 additions & 2 deletions BililiveRecorder.Core/Api/IHttpClientAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Net.Http;
using System.Threading.Tasks;

namespace BililiveRecorder.Core.Api
{
public interface IHttpClientAccessor
{
HttpClient MainHttpClient { get; }
long GetUid();
Task<(bool, string)> TestCookieAsync();
}
}
1 change: 1 addition & 0 deletions BililiveRecorder.Core/Api/PolicyWrappedApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public PolicyWrappedApiClient(T client, IReadOnlyPolicyRegistry<string> policies
}

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)
Expand Down
4 changes: 2 additions & 2 deletions BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}">
Expand Down
31 changes: 10 additions & 21 deletions BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,29 +66,18 @@ private async void TestCookie_Click(object sender, RoutedEventArgs e)

private async Task TestCookieAsync()
{
if (this.httpApiClient is null)
{
MessageBox.Show("No Http Client Available", "Cookie Test - Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
bool succeed;
string message;

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());
if (this.httpApiClient is null)
(succeed, message) = (false, "No Http Client Available");
else
(succeed, message) = await this.httpApiClient.TestCookieAsync().ConfigureAwait(false);

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ namespace BililiveRecorder.Core.Api
{
public interface IHttpClientAccessor
{
System.Net.Http.HttpClient MainHttpClient { get; }
long GetUid();
System.Threading.Tasks.Task<System.ValueTuple<bool, string>> TestCookieAsync();
}
}
namespace BililiveRecorder.Core.Config
Expand Down