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