diff --git a/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewer.csproj b/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewer.csproj
index 7884e08..3fde397 100644
--- a/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewer.csproj
+++ b/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewer.csproj
@@ -6,4 +6,30 @@
Exe
+
+
+ Always
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewerModule.cs b/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewerModule.cs
new file mode 100644
index 0000000..0d95c07
--- /dev/null
+++ b/BilibiliLiveDanmuPreviewer/BilibiliLiveDanmuPreviewerModule.cs
@@ -0,0 +1,91 @@
+global using BilibiliApi.Clients;
+global using BilibiliApi.Model.RoomInfo;
+global using BilibiliLiveDanmuPreviewer;
+global using JetBrains.Annotations;
+global using Microsoft;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.DependencyInjection.Extensions;
+global using Microsoft.Extensions.Logging;
+global using Serilog;
+global using System.Net;
+global using System.Reactive.Linq;
+global using Volo.Abp;
+global using Volo.Abp.Autofac;
+global using Volo.Abp.DependencyInjection;
+global using Volo.Abp.Modularity;
+global using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace BilibiliLiveDanmuPreviewer;
+
+[DependsOn(
+ typeof(AbpAutofacModule)
+)]
+[UsedImplicitly]
+internal class BilibiliLiveDanmuPreviewerModule : AbpModule
+{
+ public override Task PreConfigureServicesAsync(ServiceConfigurationContext context)
+ {
+ context.Services.ReplaceConfiguration(new ConfigurationBuilder().AddJsonFile(@"appsettings.json", true, true).Build());
+
+ return Task.CompletedTask;
+ }
+
+ public override Task ConfigureServicesAsync(ServiceConfigurationContext context)
+ {
+ IConfiguration configuration = context.Services.GetConfiguration();
+
+ context.Services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger(), true));
+#if DEBUG
+ Serilog.Debugging.SelfLog.Enable(msg =>
+ {
+ System.Diagnostics.Debug.Print(msg);
+ System.Diagnostics.Debugger.Break();
+ });
+#endif
+
+ context.Services.AddHttpClient(@"bilibili").ConfigureHttpClient((provider, client) =>
+ {
+ client.DefaultRequestVersion = HttpVersion.Version20;
+ client.Timeout = TimeSpan.FromSeconds(10);
+ client.DefaultRequestHeaders.Accept.ParseAdd(@"application/json, text/javascript, */*; q=0.01");
+ client.DefaultRequestHeaders.Referrer = new Uri(@"https://live.bilibili.com/");
+
+ IConfiguration config = provider.GetRequiredService();
+
+ IConfigurationSection httpClientConfig = config.GetSection(@"HttpClient");
+ string? cookie = httpClientConfig.GetValue(@"Cookie");
+ string? userAgent = httpClientConfig.GetValue(@"UserAgent");
+
+ if (!string.IsNullOrWhiteSpace(cookie))
+ {
+ client.DefaultRequestHeaders.Add(@"Cookie", cookie);
+ }
+
+ if (!string.IsNullOrWhiteSpace(userAgent))
+ {
+ client.DefaultRequestHeaders.UserAgent.Clear();
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
+ }
+ });
+
+ context.Services.TryAddSingleton(provider =>
+ {
+ IHttpClientFactory httpClientFactory = provider.GetRequiredService();
+ return new BilibiliApiClient(httpClientFactory.CreateClient(@"bilibili"));
+ });
+
+ context.Services.AddDistributedMemoryCache();
+
+ context.Services.TryAddTransient();
+
+ return Task.CompletedTask;
+ }
+
+ public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
+ {
+ context.ServiceProvider.GetRequiredService().Dispose();
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/BilibiliLiveDanmuPreviewer/InteractiveType.cs b/BilibiliLiveDanmuPreviewer/InteractiveType.cs
new file mode 100644
index 0000000..8c16aa5
--- /dev/null
+++ b/BilibiliLiveDanmuPreviewer/InteractiveType.cs
@@ -0,0 +1,11 @@
+namespace BilibiliLiveDanmuPreviewer;
+
+public enum InteractiveType
+{
+ 进入 = 1,
+ 关注 = 2,
+ 分享直播间 = 3,
+ 特别关注 = 4,
+ 互相关注 = 5,
+ 点赞 = 6
+}
diff --git a/BilibiliLiveDanmuPreviewer/MainService.cs b/BilibiliLiveDanmuPreviewer/MainService.cs
new file mode 100644
index 0000000..02973b9
--- /dev/null
+++ b/BilibiliLiveDanmuPreviewer/MainService.cs
@@ -0,0 +1,286 @@
+using BilibiliApi.Enums;
+using BilibiliApi.Model.Danmu;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+
+namespace BilibiliLiveDanmuPreviewer;
+
+[UsedImplicitly]
+public class MainService : ServiceBase
+{
+ public async ValueTask DoAsync(CancellationToken cancellationToken = default)
+ {
+ long[]? roomList = Configuration.GetSection(@"RoomList").Get();
+ Verify.Operation(roomList is not null && roomList.Any(), @"无房间号");
+
+ List list = new(roomList.Length);
+
+ foreach (long roomId in roomList)
+ {
+ BilibiliApiClient apiClient = ServiceProvider.GetRequiredService();
+ RoomInfoMessage.RoomInfoData roomInfo = await apiClient.GetRoomInfoDataAsync(roomId, cancellationToken);
+
+ Assumes.NotNull(roomInfo.room_info);
+
+ long realId = roomInfo.room_info.room_id;
+ Assumes.False(realId is 0);
+
+ Task task = Task.Run(async () =>
+ {
+ IDanmuClient client = ServiceProvider.GetRequiredService();
+
+ cancellationToken.Register(() => client.Dispose());
+
+ client.RoomId = realId;
+ client.Received.Subscribe(ParseDanmu);
+
+ await client.StartAsync();
+ }, cancellationToken);
+ list.Add(task);
+ }
+
+ await Task.WhenAll(list);
+
+ return;
+
+ void ParseDanmu(DanmuPacket packet)
+ {
+ try
+ {
+ switch (packet.Operation)
+ {
+ case Operation.HeartbeatReply:
+ {
+ SequenceReader reader = new(packet.Body);
+ reader.TryReadBigEndian(out int num);
+ Logger.LogDebug(@"收到弹幕 [{operation}] 人气值: {number}", packet.Operation, num);
+ break;
+ }
+ case Operation.SendMsgReply:
+ {
+ string json = Encoding.UTF8.GetString(packet.Body);
+ ParseSendMsgReplyBody(json);
+ break;
+ }
+ case Operation.AuthReply:
+ {
+ Logger.LogDebug(@"收到弹幕 [{operation}]: {body}", packet.Operation, Encoding.UTF8.GetString(packet.Body));
+ break;
+ }
+ default:
+ {
+ Logger.LogDebug(@"收到弹幕 [{operation}]", packet.Operation);
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, @"弹幕解析失败:{operation} {protocolVersion} {body}", packet.Operation, packet.ProtocolVersion, Encoding.UTF8.GetString(packet.Body));
+ }
+
+ return;
+
+ void ParseSendMsgReplyBody(string json)
+ {
+ if (Configuration.GetSection(@"IsLogJson").Get())
+ {
+ Logger.LogDebug(@"收到弹幕 [{operation}]: {body}", packet.Operation, json);
+ }
+
+ using JsonDocument document = JsonDocument.Parse(json);
+ JsonElement root = document.RootElement;
+
+ string? cmd = root.GetProperty(@"cmd").GetString();
+
+ if (cmd is null)
+ {
+ return;
+ }
+
+ HashSet? ignoreCmd = Configuration.GetSection(@"IgnoreCmd").Get>();
+ if (ignoreCmd is not null && ignoreCmd.Contains(cmd))
+ {
+ return;
+ }
+
+ switch (cmd)
+ {
+ case @"COMMON_NOTICE_DANMAKU":
+ {
+ DefaultInterpolatedStringHandler handler = new();
+ foreach (JsonElement segments in root.GetProperty(@"data").GetProperty(@"content_segments").EnumerateArray())
+ {
+ string? text = segments.GetProperty(@"text").GetString();
+ if (text is not null)
+ {
+ handler.AppendLiteral(text);
+ }
+ }
+ Logger.LogInformation(@"[{cmd}] {notice}", cmd, handler.ToStringAndClear());
+ break;
+ }
+ case @"NOTICE_MSG":
+ {
+ string? msg = root.GetProperty(@"msg_self").GetString();
+
+ Logger.LogInformation(@"[{cmd}] {notice}", cmd, msg);
+ break;
+ }
+ case @"LIVE":
+ {
+ Logger.LogInformation(@"{action}", @"开播");
+ break;
+ }
+ case @"PREPARING":
+ {
+ if (root.TryGetProperty(@"round", out JsonElement element) && element.TryGetInt64(out long round) && round is 1)
+ {
+ Logger.LogInformation(@"{action}", @"轮播");
+ }
+ else
+ {
+ Logger.LogInformation(@"{action}", @"下播");
+ }
+
+ break;
+ }
+ case @"ROOM_CHANGE":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? title = data.GetProperty(@"title").GetString();
+ string? areaName = data.GetProperty(@"area_name").GetString();
+ string? parentAreaName = data.GetProperty(@"parent_area_name").GetString();
+
+ Logger.LogInformation(@"[{cmd}] 标题:{title} 分区:{parentArea}|{area}", cmd, title, areaName, parentAreaName);
+ break;
+ }
+ case @"WATCHED_CHANGE":
+ {
+ Logger.LogInformation(@"{roomId} 人看过", root.GetProperty(@"data").GetProperty(@"num").GetInt64());
+ break;
+ }
+ case @"WARNING":
+ {
+ Logger.LogInformation(@"超管警告:{message}", root.GetProperty(@"msg").GetString());
+
+ break;
+ }
+ case @"CUT_OFF":
+ {
+ Logger.LogInformation(@"直播被切断:{message}", root.GetProperty(@"msg").GetString());
+
+ break;
+ }
+ case @"INTERACT_WORD":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? userName = data.GetProperty(@"uname").GetString();
+ InteractiveType type = (InteractiveType)data.GetProperty(@"msg_type").GetInt32();
+
+ Logger.LogInformation(@"{user}{action}了直播间", userName, type);
+ break;
+ }
+ case @"ENTRY_EFFECT":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? msg = data.GetProperty(@"copy_writing").GetString();
+ PrivilegeType type = (PrivilegeType)data.GetProperty(@"privilege_type").GetInt32();
+
+ if (msg is not null && Enum.IsDefined(type))
+ {
+ int start = msg.IndexOf(@"<%", StringComparison.Ordinal);
+ msg = msg.Remove(start, 2);
+
+ int end = msg.LastIndexOf(@"%>", StringComparison.Ordinal);
+ msg = msg.Remove(end, 2);
+
+ Logger.LogInformation(@"{msg}", msg);
+ }
+
+ break;
+ }
+ case @"DANMU_MSG":
+ {
+ JsonElement info = root.GetProperty(@"info");
+ string? msg = info[1].GetString();
+ string? userName = info[2][1].GetString();
+
+ Logger.LogInformation(@"{user}:{msg}", userName, msg);
+ break;
+ }
+ case @"LIKE_INFO_V3_UPDATE":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ Logger.LogInformation(@"点赞数: {count}", data.GetProperty(@"click_count").GetInt64());
+ break;
+ }
+ case @"LIKE_INFO_V3_CLICK":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? userName = data.GetProperty(@"uname").GetString();
+ string? msg = data.GetProperty(@"like_text").GetString();
+
+ Logger.LogInformation(@"{user} {text}", userName, msg);
+ break;
+ }
+ case @"SEND_GIFT":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? userName = data.GetProperty(@"uname").GetString();
+ string? action = data.GetProperty(@"action").GetString();
+ string? giftName = data.GetProperty(@"giftName").GetString();
+ long num = data.GetProperty(@"num").GetInt64();
+
+ Logger.LogInformation(@"{user} {action} {giftName}x{num}", userName, action, giftName, num);
+ break;
+ }
+ case @"COMBO_SEND":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? userName = data.GetProperty(@"uname").GetString();
+ string? action = data.GetProperty(@"action").GetString();
+ string? giftName = data.GetProperty(@"gift_name").GetString();
+ long num = data.GetProperty(@"total_num").GetInt64();
+
+ Logger.LogInformation(@"{user} {action} {giftName}x{num}", userName, action, giftName, num);
+
+ break;
+ }
+ case @"GUARD_BUY":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? userName = data.GetProperty(@"username").GetString();
+ string? giftName = data.GetProperty(@"gift_name").GetString();
+ long num = data.GetProperty(@"num").GetInt64();
+
+ Logger.LogInformation(@"{user} 购买了 {giftName}x{num}", userName, giftName, num);
+ break;
+ }
+ case @"SUPER_CHAT_MESSAGE":
+ {
+ JsonElement data = root.GetProperty(@"data");
+ string? userName = data.GetProperty(@"user_info").GetProperty(@"uname").GetString();
+ string? giftName = data.GetProperty(@"gift").GetProperty(@"gift_name").GetString();
+ long num = data.GetProperty(@"gift").GetProperty(@"num").GetInt64();
+ string? message = data.GetProperty(@"message").GetString();
+ long price = data.GetProperty(@"price").GetInt64();
+
+ Logger.LogInformation(@"{user} 购买了 {price}元{giftName}x{num}:{message}", userName, price, giftName, num, message);
+ break;
+ }
+ default:
+ {
+ if (Configuration.GetSection(@"IsLogUnresolvedCmd").Get())
+ {
+ Logger.LogInformation(@"收到弹幕 [{operation}]: {body}", packet.Operation, json);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/BilibiliLiveDanmuPreviewer/PrivilegeType.cs b/BilibiliLiveDanmuPreviewer/PrivilegeType.cs
new file mode 100644
index 0000000..8f2e767
--- /dev/null
+++ b/BilibiliLiveDanmuPreviewer/PrivilegeType.cs
@@ -0,0 +1,8 @@
+namespace BilibiliLiveDanmuPreviewer;
+
+public enum PrivilegeType
+{
+ 总督 = 1,
+ 提督 = 2,
+ 舰长 = 3
+}
diff --git a/BilibiliLiveDanmuPreviewer/Program.cs b/BilibiliLiveDanmuPreviewer/Program.cs
index 3751555..5ef244f 100644
--- a/BilibiliLiveDanmuPreviewer/Program.cs
+++ b/BilibiliLiveDanmuPreviewer/Program.cs
@@ -1,2 +1,31 @@
-// See https://aka.ms/new-console-template for more information
-Console.WriteLine("Hello, World!");
+using IAbpApplicationWithInternalServiceProvider application = await AbpApplicationFactory.CreateAsync(options => options.UseAutofac());
+
+try
+{
+ await application.InitializeAsync();
+ MainService service = application.ServiceProvider.GetRequiredService();
+ try
+ {
+ using CancellationTokenSource cts = new();
+ using (Observable.FromEventPattern(typeof(Console), nameof(Console.CancelKeyPress)).Subscribe(e =>
+ {
+ // ReSharper disable once AccessToDisposedClosure
+ cts.Cancel();
+ e.EventArgs.Cancel = true;
+ }))
+ {
+ await service.DoAsync(cts.Token);
+ }
+ }
+ catch (Exception ex)
+ {
+ application.ServiceProvider.GetRequiredService>().LogException(ex);
+ return 1;
+ }
+
+ return 0;
+}
+finally
+{
+ await application.ShutdownAsync();
+}
diff --git a/BilibiliLiveDanmuPreviewer/ServiceBase.cs b/BilibiliLiveDanmuPreviewer/ServiceBase.cs
new file mode 100644
index 0000000..94ae36a
--- /dev/null
+++ b/BilibiliLiveDanmuPreviewer/ServiceBase.cs
@@ -0,0 +1,14 @@
+namespace BilibiliLiveDanmuPreviewer;
+
+public abstract class ServiceBase : ITransientDependency
+{
+ public IAbpLazyServiceProvider LazyServiceProvider { get; set; } = null!; // 属性注入
+
+ protected IServiceProvider ServiceProvider => LazyServiceProvider.LazyGetRequiredService();
+
+ protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService();
+
+ protected ILogger Logger => LazyServiceProvider.LazyGetService(_ => LoggerFactory.CreateLogger(GetType()));
+
+ protected IConfiguration Configuration => LazyServiceProvider.LazyGetRequiredService();
+}
diff --git a/BilibiliLiveDanmuPreviewer/appsettings.json b/BilibiliLiveDanmuPreviewer/appsettings.json
new file mode 100644
index 0000000..51057da
--- /dev/null
+++ b/BilibiliLiveDanmuPreviewer/appsettings.json
@@ -0,0 +1,45 @@
+{
+ "RoomList": [ 5050 ],
+ "HttpClient": {
+ "Cookie": "",
+ "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
+ },
+ "IsLogJson": false,
+ "IsLogUnresolvedCmd": false,
+ "IgnoreCmd": [ "NOTICE_MSG" ],
+ "Serilog": {
+ "Using": [ "Serilog.Sinks.Async", "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft": "Information",
+ "Volo.Abp": "Warning",
+ "System.Net.Http.HttpClient": "Warning"
+ }
+ },
+ "WriteTo:Async": {
+ "Name": "Async",
+ "Args": {
+ "configure": [
+ {
+ "Name": "Console",
+ "Args": {
+ "restrictedToMinimumLevel": "Information",
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss}] [{Level}] [{BiliBiliLiveRoomId}] {Message:lj}{NewLine}{Exception}"
+ }
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "Logs/BilibiliLiveDanmuPreviewer.log",
+ "outputTemplate": "[{Timestamp:O}] [{Level}] [{BiliBiliLiveRoomId}] {Message:lj}{NewLine}{Exception}",
+ "rollingInterval": "Day",
+ "retainedFileCountLimit ": 93
+ }
+ }
+ ]
+ }
+ },
+ "Enrich": [ "FromLogContext" ]
+ }
+}
diff --git a/BilibiliLiveRecordDownLoader.BilibiliApi/Clients/DanmuClientBase.cs b/BilibiliLiveRecordDownLoader.BilibiliApi/Clients/DanmuClientBase.cs
index 3d74c32..077e50f 100644
--- a/BilibiliLiveRecordDownLoader.BilibiliApi/Clients/DanmuClientBase.cs
+++ b/BilibiliLiveRecordDownLoader.BilibiliApi/Clients/DanmuClientBase.cs
@@ -402,31 +402,10 @@ private async ValueTask ProcessDanMuPacketAsync(DanmuPacket packet, Cancellation
}
}
+ return;
+
void EmitDanmu()
{
-#if DEBUG
- switch (packet.Operation)
- {
- case Operation.HeartbeatReply:
- {
- SequenceReader reader = new(packet.Body);
- reader.TryReadBigEndian(out int num);
- _logger.LogDebug(@"收到弹幕[{operation}] 人气值: {number}", packet.Operation, num);
- break;
- }
- case Operation.SendMsgReply:
- case Operation.AuthReply:
- {
- _logger.LogDebug(@"收到弹幕[{operation}]:{body}", packet.Operation, Encoding.UTF8.GetString(packet.Body));
- break;
- }
- default:
- {
- _logger.LogDebug(@"收到弹幕[{operation}]", packet.Operation);
- break;
- }
- }
-#endif
_danMuSubj.OnNext(packet);
}
}
diff --git a/BilibiliLiveRecordDownLoader.BilibiliApi/Model/Danmu/DanMuPacket.cs b/BilibiliLiveRecordDownLoader.BilibiliApi/Model/Danmu/DanMuPacket.cs
index 5729724..db30590 100644
--- a/BilibiliLiveRecordDownLoader.BilibiliApi/Model/Danmu/DanMuPacket.cs
+++ b/BilibiliLiveRecordDownLoader.BilibiliApi/Model/Danmu/DanMuPacket.cs
@@ -36,7 +36,7 @@ public struct DanmuPacket
///
public ReadOnlySequence Body;
- public void GetHeaderBytes(Span span)
+ public readonly void GetHeaderBytes(Span span)
{
BinaryPrimitives.WriteInt32BigEndian(span, PacketLength);
BinaryPrimitives.WriteInt16BigEndian(span[4..], HeaderLength);