mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 03:32:20 +08:00
commit
d37a3c2922
|
@ -5,12 +5,18 @@ root = true
|
|||
|
||||
# all files
|
||||
[*]
|
||||
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
|
||||
[NLog.{Debug,Release}.config]
|
||||
|
||||
indent_size = 2
|
||||
|
||||
[*.csproj]
|
||||
|
||||
indent_size = 2
|
||||
|
||||
[*.cs]
|
||||
|
@ -41,8 +47,8 @@ csharp_new_line_before_finally = true
|
|||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
#require members of object intializers to be on separate lines
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
#require braces to be on a new line for anonymous_types, lambdas, properties, accessors, methods, object_collection_array_initializers, control_blocks, and types (also known as "Allman" style)
|
||||
csharp_new_line_before_open_brace = anonymous_types, lambdas, properties, accessors, methods, object_collection_array_initializers, control_blocks, types
|
||||
#require braces to be on a new line for methods and types (also known as "Allman" style)
|
||||
csharp_new_line_before_open_brace = all
|
||||
|
||||
#Formatting - organize using options
|
||||
|
||||
|
@ -80,7 +86,7 @@ csharp_preserve_single_line_statements = true
|
|||
#Style - Code block preferences
|
||||
|
||||
#prefer curly braces even for one line of code
|
||||
csharp_prefer_braces = true:suggestion
|
||||
csharp_prefer_braces = false:suggestion
|
||||
|
||||
#Style - expression bodied member options
|
||||
|
||||
|
@ -90,8 +96,8 @@ csharp_style_expression_bodied_accessors = true:suggestion
|
|||
csharp_style_expression_bodied_constructors = false:suggestion
|
||||
#prefer expression-bodied members for indexers
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
#prefer block bodies for methods
|
||||
csharp_style_expression_bodied_methods = false:suggestion
|
||||
#prefer expression-bodied members for methods
|
||||
csharp_style_expression_bodied_methods = true:suggestion
|
||||
#prefer expression-bodied members for properties
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
|
@ -149,11 +155,11 @@ csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
|||
|
||||
#Style - qualification options
|
||||
|
||||
#prefer events not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
#prefer fields not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
#prefer methods not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
#prefer properties not to be prefaced with this. or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
#prefer events to be prefaced with this. in C# or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_event = true:suggestion
|
||||
#prefer fields to be prefaced with this. in C# or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_field = true:suggestion
|
||||
#prefer methods to be prefaced with this. in C# or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_method = true:suggestion
|
||||
#prefer properties to be prefaced with this. in C# or Me. in Visual Basic
|
||||
dotnet_style_qualification_for_property = true:suggestion
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<StartupObject>BililiveRecorder.Cli.Program</StartupObject>
|
||||
<RuntimeIdentifiers>win-x64;osx-x64;osx.10.11-x64;linux-arm64;linux-arm;linux-x64</RuntimeIdentifiers>
|
||||
<PublishDir Condition=" '$(RuntimeIdentifier)' == '' ">publish\any</PublishDir>
|
||||
<PublishDir Condition=" '$(RuntimeIdentifier)' != '' ">publish\$(RuntimeIdentifier)</PublishDir>
|
||||
<SelfContained Condition=" '$(RuntimeIdentifier)' == '' ">false</SelfContained>
|
||||
<SelfContained Condition=" '$(SelfContained)' == '' ">true</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="config.json" />
|
||||
<None Remove="NLog.config" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\TempBuildInfo\BuildInfo.Cli.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
@ -23,22 +26,17 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.4.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.6" />
|
||||
<PackageReference Include="NLog.Config" Version="4.7.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
|
||||
<ProjectReference Include="..\BililiveRecorder.FlvProcessor\BililiveRecorder.FlvProcessor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
|
||||
<Exec Command="cd $(SolutionDir)
powershell -ExecutionPolicy Bypass -File .\CI\patch_buildinfo.ps1 Cli" />
|
||||
</Target>
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -8,105 +8,110 @@ using BililiveRecorder.Core;
|
|||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using CommandLine;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Cli
|
||||
{
|
||||
class Program
|
||||
internal class Program
|
||||
{
|
||||
private static IContainer Container { get; set; }
|
||||
private static ILifetimeScope RootScope { get; set; }
|
||||
private static IRecorder Recorder { get; set; }
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static void Main(string[] _)
|
||||
private static void Main(string[] _)
|
||||
{
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterModule<FlvProcessorModule>();
|
||||
builder.RegisterModule<CoreModule>();
|
||||
builder.RegisterType<CommandConfigV1>().As<ConfigV1>().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
Container = builder.Build();
|
||||
var Container = builder.Build();
|
||||
var RootScope = Container.BeginLifetimeScope("recorder_root");
|
||||
|
||||
RootScope = Container.BeginLifetimeScope("recorder_root");
|
||||
Recorder = RootScope.Resolve<IRecorder>();
|
||||
if (!Recorder.Initialize(System.IO.Directory.GetCurrentDirectory()))
|
||||
var Recorder = RootScope.Resolve<IRecorder>();
|
||||
if (!Recorder.Initialize(Directory.GetCurrentDirectory()))
|
||||
{
|
||||
Console.WriteLine("Initialize Error");
|
||||
return;
|
||||
}
|
||||
|
||||
Parser.Default
|
||||
.ParseArguments<CommandConfigV1>(() => (CommandConfigV1)Recorder.Config, Environment.GetCommandLineArgs())
|
||||
.ParseArguments(() => (CommandConfigV1)Recorder.Config, Environment.GetCommandLineArgs())
|
||||
.WithParsed(Run);
|
||||
}
|
||||
|
||||
private static void Run(ConfigV1 option)
|
||||
{
|
||||
foreach (var room in option.RoomList)
|
||||
return;
|
||||
void Run(ConfigV1 option)
|
||||
{
|
||||
if (Recorder.Where(r => r.RoomId == room.Roomid).Count() == 0)
|
||||
option.EnabledFeature = EnabledFeature.RecordOnly;
|
||||
foreach (var room in option.RoomList)
|
||||
{
|
||||
Recorder.AddRoom(room.Roomid);
|
||||
if (Recorder.Where(r => r.RoomId == room.Roomid).Count() == 0)
|
||||
{
|
||||
Recorder.AddRoom(room.Roomid);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Using workDir: " + option.WorkDirectory + "\n\tconfig: " + JsonConvert.SerializeObject(option, Formatting.Indented));
|
||||
|
||||
logger.Info("开始录播");
|
||||
Task.WhenAll(Recorder.Select(x => Task.Run(() => x.Start()))).Wait();
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Task.WhenAll(Recorder.Select(x => Task.Run(() => x.StopRecord()))).Wait();
|
||||
logger.Info("停止录播");
|
||||
};
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("开始录播");
|
||||
Task.WhenAll(Recorder.Select(x => Task.Run(() => x.Start()))).Wait();
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Task.WhenAll(Recorder.Select(x => Task.Run(() => x.StopRecord()))).Wait();
|
||||
logger.Info("停止录播");
|
||||
};
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigV1Metadata
|
||||
{
|
||||
[Option('o', "dir", Default = ".", HelpText = "Output directory", Required = false)]
|
||||
[Utils.DoNotCopyProperty]
|
||||
public object WorkDirectory { get; set; }
|
||||
|
||||
[Option("cookie", HelpText = "Provide custom cookies", Required = false)]
|
||||
public object Cookie { get; set; }
|
||||
|
||||
[Option("avoidtxy", HelpText = "Avoid Tencent Cloud server", Required = false)]
|
||||
public object AvoidTxy { get; set; }
|
||||
|
||||
[Option("live_api_host", HelpText = "Use custom api host", Required = false)]
|
||||
public object LiveApiHost { get; set; }
|
||||
|
||||
[Option("record_filename_format", HelpText = "Recording name format", Required = false)]
|
||||
public object RecordFilenameFormat { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
[MetadataType(typeof(ConfigV1Metadata))]
|
||||
class CommandConfigV1 : ConfigV1
|
||||
public partial class CommandConfigV1 : ConfigV1
|
||||
{
|
||||
[Option('i', "id", HelpText = "room id", Required = true)]
|
||||
[Utils.DoNotCopyProperty]
|
||||
public string _RoomList
|
||||
{
|
||||
set
|
||||
{
|
||||
var roomids = value.Split(',');
|
||||
RoomList.Clear();
|
||||
this.RoomList.Clear();
|
||||
|
||||
foreach (var roomid in roomids)
|
||||
{
|
||||
var room = new RoomV1();
|
||||
room.Roomid = Int32.Parse(roomid);
|
||||
room.Roomid = int.Parse(roomid);
|
||||
room.Enabled = false;
|
||||
RoomList.Add(room);
|
||||
this.RoomList.Add(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Option('o', "dir", Default = ".", HelpText = "Output directory", Required = false)]
|
||||
public new string WorkDirectory
|
||||
{
|
||||
get => base.WorkDirectory;
|
||||
set => base.WorkDirectory = value;
|
||||
}
|
||||
|
||||
[Option("cookie", HelpText = "Provide custom cookies", Required = false)]
|
||||
public new string Cookie
|
||||
{
|
||||
get => base.Cookie;
|
||||
set => base.Cookie = value;
|
||||
}
|
||||
|
||||
[Option("live_api_host", HelpText = "Use custom api host", Required = false)]
|
||||
public new string LiveApiHost
|
||||
{
|
||||
get => base.LiveApiHost;
|
||||
set => base.LiveApiHost = value;
|
||||
}
|
||||
|
||||
[Option("record_filename_format", HelpText = "Recording name format", Required = false)]
|
||||
public new string RecordFilenameFormat
|
||||
{
|
||||
get => base.RecordFilenameFormat;
|
||||
set => base.RecordFilenameFormat = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ using BililiveRecorder.Core.Config;
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
|
||||
|
@ -17,6 +18,9 @@ namespace BililiveRecorder.Core
|
|||
WriteEndDocumentOnClose = true
|
||||
};
|
||||
|
||||
private static readonly Regex invalidXMLChars = new Regex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]", RegexOptions.Compiled);
|
||||
private static string RemoveInvalidXMLChars(string text) => string.IsNullOrEmpty(text) ? string.Empty : invalidXMLChars.Replace(text, string.Empty);
|
||||
|
||||
private XmlWriter xmlWriter = null;
|
||||
private DateTimeOffset offset = DateTimeOffset.UtcNow;
|
||||
private uint writeCount = 0;
|
||||
|
@ -102,7 +106,7 @@ namespace BililiveRecorder.Core
|
|||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
|
@ -117,7 +121,7 @@ namespace BililiveRecorder.Core
|
|||
xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(danmakuModel.CommentText);
|
||||
xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
||||
xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Version>0.0.0.0</Version>
|
||||
<Authors>Genteure</Authors>
|
||||
<Company>Genteure</Company>
|
||||
<Copyright>Copyright © 2018 - 2021 Genteure</Copyright>
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<UpgradeBackupLocation>
|
||||
</UpgradeBackupLocation>
|
||||
<OldToolsVersion>2.0</OldToolsVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
|
64
BililiveRecorder.Core/Callback/BasicWebhook.cs
Normal file
64
BililiveRecorder.Core/Callback/BasicWebhook.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core.Callback
|
||||
{
|
||||
public class BasicWebhook
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly HttpClient client;
|
||||
|
||||
private readonly ConfigV1 Config;
|
||||
|
||||
static BasicWebhook()
|
||||
{
|
||||
client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("User-Agent", $"BililiveRecorder/{typeof(BasicWebhook).Assembly.GetName().Version}-{BuildInfo.HeadShaShort}");
|
||||
}
|
||||
|
||||
public BasicWebhook(ConfigV1 config)
|
||||
{
|
||||
this.Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
public async void Send(RecordEndData data)
|
||||
{
|
||||
var urls = this.Config.WebHookUrls;
|
||||
if (string.IsNullOrWhiteSpace(urls)) return;
|
||||
|
||||
var dataStr = JsonConvert.SerializeObject(data, Formatting.None);
|
||||
using var body = new ByteArrayContent(Encoding.UTF8.GetBytes(dataStr));
|
||||
body.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||
|
||||
var tasks = urls
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(x => this.SendImplAsync(x, body));
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendImplAsync(string url, HttpContent data)
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
try
|
||||
{
|
||||
var result = await client.PostAsync(url, data).ConfigureAwait(false);
|
||||
result.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn(ex, "发送 Webhook 到 {url} 失败", url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
BililiveRecorder.Core/Callback/RecordEndData.cs
Normal file
18
BililiveRecorder.Core/Callback/RecordEndData.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core.Callback
|
||||
{
|
||||
public class RecordEndData
|
||||
{
|
||||
public Guid EventRandomId { get; set; } = Guid.NewGuid();
|
||||
|
||||
public int RoomId { get; set; } = 0;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public long FileSize { get; set; }
|
||||
public DateTimeOffset StartRecordTime { get; set; }
|
||||
public DateTimeOffset EndRecordTime { get; set; }
|
||||
}
|
||||
}
|
|
@ -148,6 +148,16 @@ namespace BililiveRecorder.Core.Config
|
|||
set => SetField(ref _clip_filename_format, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Webhook 地址 每行一个
|
||||
/// </summary>
|
||||
[JsonProperty("webhook_urls")]
|
||||
public string WebHookUrls
|
||||
{
|
||||
get => _webhook_urls;
|
||||
set => SetField(ref _webhook_urls, value);
|
||||
}
|
||||
|
||||
#region INotifyPropertyChanged
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
@ -186,5 +196,7 @@ namespace BililiveRecorder.Core.Config
|
|||
private uint _recordDanmakuFlushInterval = 20;
|
||||
|
||||
private string _liveApiHost = "https://api.live.bilibili.com";
|
||||
|
||||
private string _webhook_urls = "";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Net.Sockets;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
|
@ -15,6 +16,7 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
builder.RegisterType<ConfigV1>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<BililiveAPI>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<BasicWebhook>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<TcpClient>().AsSelf().ExternallyOwned();
|
||||
builder.RegisterType<StreamMonitor>().As<IStreamMonitor>().ExternallyOwned();
|
||||
builder.RegisterType<RecordedRoom>().As<IRecordedRoom>().ExternallyOwned();
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public interface IRecordedRoom : INotifyPropertyChanged, IDisposable
|
||||
|
@ -11,9 +13,12 @@ namespace BililiveRecorder.Core
|
|||
int ShortRoomId { get; }
|
||||
int RoomId { get; }
|
||||
string StreamerName { get; }
|
||||
string Title { get; }
|
||||
|
||||
event EventHandler<RecordEndData>? RecordEnded;
|
||||
|
||||
IStreamMonitor StreamMonitor { get; }
|
||||
IFlvStreamProcessor Processor { get; }
|
||||
IFlvStreamProcessor? Processor { get; }
|
||||
|
||||
bool IsMonitoring { get; }
|
||||
bool IsRecording { get; }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using NLog;
|
||||
|
@ -56,7 +57,6 @@ namespace BililiveRecorder.Core
|
|||
TriggerPropertyChanged(nameof(StreamerName));
|
||||
}
|
||||
}
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
|
@ -82,6 +82,9 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
}
|
||||
|
||||
private RecordEndData recordEndData;
|
||||
public event EventHandler<RecordEndData> RecordEnded;
|
||||
|
||||
private readonly IBasicDanmakuWriter basicDanmakuWriter;
|
||||
private readonly Func<IFlvStreamProcessor> newIFlvStreamProcessor;
|
||||
private IFlvStreamProcessor _processor;
|
||||
|
@ -191,10 +194,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public bool Start()
|
||||
{
|
||||
if (disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
}
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
|
||||
var r = StreamMonitor.Start();
|
||||
TriggerPropertyChanged(nameof(IsMonitoring));
|
||||
|
@ -203,10 +203,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public void Stop()
|
||||
{
|
||||
if (disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
}
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
|
||||
StreamMonitor.Stop();
|
||||
TriggerPropertyChanged(nameof(IsMonitoring));
|
||||
|
@ -214,41 +211,26 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public void RefreshRoomInfo()
|
||||
{
|
||||
if (disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
}
|
||||
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
StreamMonitor.FetchRoomInfoAsync();
|
||||
}
|
||||
|
||||
private void StreamMonitor_StreamStarted(object sender, StreamStartedArgs e)
|
||||
{
|
||||
lock (StartupTaskLock)
|
||||
{
|
||||
if (!IsRecording && (StartupTask?.IsCompleted ?? true))
|
||||
{
|
||||
StartupTask = _StartRecordAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StartRecord()
|
||||
{
|
||||
if (disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
}
|
||||
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
StreamMonitor.Check(TriggerType.Manual);
|
||||
}
|
||||
|
||||
public void StopRecord()
|
||||
{
|
||||
if (disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
}
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
|
||||
_retry = false;
|
||||
try
|
||||
|
@ -347,6 +329,16 @@ namespace BililiveRecorder.Core
|
|||
Processor.ClipLengthPast = _config.ClipLengthPast;
|
||||
Processor.CuttingNumber = _config.CuttingNumber;
|
||||
Processor.StreamFinalized += (sender, e) => { basicDanmakuWriter.Disable(); };
|
||||
Processor.FileFinalized += (sender, size) =>
|
||||
{
|
||||
if (recordEndData is null) return;
|
||||
var data = recordEndData;
|
||||
recordEndData = null;
|
||||
|
||||
data.EndRecordTime = DateTimeOffset.Now;
|
||||
data.FileSize = size;
|
||||
RecordEnded?.Invoke(this, data);
|
||||
};
|
||||
Processor.OnMetaData += (sender, e) =>
|
||||
{
|
||||
e.Metadata["BililiveRecorder"] = new Dictionary<string, object>()
|
||||
|
@ -484,40 +476,43 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
|
||||
// Called by API or GUI
|
||||
public void Clip()
|
||||
{
|
||||
Processor?.Clip();
|
||||
}
|
||||
public void Clip() => Processor?.Clip();
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
public void Shutdown() => Dispose(true);
|
||||
|
||||
private string GetStreamFilePath()
|
||||
private (string fullPath, string relativePath) GetStreamFilePath()
|
||||
{
|
||||
string path = FormatFilename(_config.RecordFilenameFormat);
|
||||
var path = FormatFilename(_config.RecordFilenameFormat);
|
||||
|
||||
// 有点脏的写法,不过凑合吧
|
||||
if (_config.RecordDanmaku)
|
||||
{
|
||||
var xmlpath = Path.ChangeExtension(path, "xml");
|
||||
var xmlpath = Path.ChangeExtension(path.fullPath, "xml");
|
||||
basicDanmakuWriter.EnableWithPath(xmlpath, this);
|
||||
}
|
||||
|
||||
recordEndData = new RecordEndData
|
||||
{
|
||||
RoomId = RoomId,
|
||||
Title = Title,
|
||||
Name = StreamerName,
|
||||
StartRecordTime = DateTimeOffset.Now,
|
||||
RelativePath = path.relativePath,
|
||||
};
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private string GetClipFilePath() => FormatFilename(_config.ClipFilenameFormat);
|
||||
private string GetClipFilePath() => FormatFilename(_config.ClipFilenameFormat).fullPath;
|
||||
|
||||
private string FormatFilename(string formatString)
|
||||
private (string fullPath, string relativePath) FormatFilename(string formatString)
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
string date = now.ToString("yyyyMMdd");
|
||||
string time = now.ToString("HHmmss");
|
||||
string randomStr = random.Next(100, 999).ToString();
|
||||
|
||||
var filename = formatString
|
||||
var relativePath = formatString
|
||||
.Replace(@"{date}", date)
|
||||
.Replace(@"{time}", time)
|
||||
.Replace(@"{random}", randomStr)
|
||||
|
@ -525,26 +520,28 @@ namespace BililiveRecorder.Core
|
|||
.Replace(@"{title}", Title.RemoveInvalidFileName())
|
||||
.Replace(@"{name}", StreamerName.RemoveInvalidFileName());
|
||||
|
||||
if (!filename.EndsWith(".flv", StringComparison.OrdinalIgnoreCase))
|
||||
filename += ".flv";
|
||||
if (!relativePath.EndsWith(".flv", StringComparison.OrdinalIgnoreCase))
|
||||
relativePath += ".flv";
|
||||
|
||||
filename = filename.RemoveInvalidFileName(ignore_slash: true);
|
||||
filename = Path.Combine(_config.WorkDirectory, filename);
|
||||
filename = Path.GetFullPath(filename);
|
||||
relativePath = relativePath.RemoveInvalidFileName(ignore_slash: true);
|
||||
var fullPath = Path.Combine(_config.WorkDirectory, relativePath);
|
||||
fullPath = Path.GetFullPath(fullPath);
|
||||
|
||||
if (!CheckPath(_config.WorkDirectory, Path.GetDirectoryName(filename)))
|
||||
if (!CheckPath(_config.WorkDirectory, Path.GetDirectoryName(fullPath)))
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Warn, "录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
|
||||
filename = Path.Combine(_config.WorkDirectory, RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
relativePath = Path.Combine(RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
fullPath = Path.Combine(_config.WorkDirectory, relativePath);
|
||||
}
|
||||
|
||||
if (new FileInfo(filename).Exists)
|
||||
if (new FileInfo(relativePath).Exists)
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Warn, "录制文件名冲突,请检查设置。将写入到默认路径。");
|
||||
filename = Path.Combine(_config.WorkDirectory, RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
relativePath = Path.Combine(RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
fullPath = Path.Combine(_config.WorkDirectory, relativePath);
|
||||
}
|
||||
|
||||
return filename;
|
||||
return (fullPath, relativePath);
|
||||
}
|
||||
|
||||
private static bool CheckPath(string parent, string child)
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.ComponentModel;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using NLog;
|
||||
|
||||
|
@ -26,14 +27,17 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public ConfigV1 Config { get; }
|
||||
|
||||
private BasicWebhook Webhook { get; }
|
||||
|
||||
public int Count => Rooms.Count;
|
||||
public bool IsReadOnly => true;
|
||||
public IRecordedRoom this[int index] => Rooms[index];
|
||||
|
||||
public Recorder(ConfigV1 config, Func<int, IRecordedRoom> iRecordedRoom)
|
||||
public Recorder(ConfigV1 config, BasicWebhook webhook, Func<int, IRecordedRoom> iRecordedRoom)
|
||||
{
|
||||
newIRecordedRoom = iRecordedRoom;
|
||||
Config = config;
|
||||
newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom));
|
||||
Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
Webhook = webhook ?? throw new ArgumentNullException(nameof(webhook));
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(3), DownloadWatchdog, tokenSource.Token);
|
||||
|
@ -96,6 +100,7 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
|
||||
logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId);
|
||||
rr.RecordEnded += this.RecordedRoom_RecordEnded;
|
||||
Rooms.Add(rr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -113,6 +118,7 @@ namespace BililiveRecorder.Core
|
|||
if (rr is null) return;
|
||||
if (!_valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
rr.Shutdown();
|
||||
rr.RecordEnded -= RecordedRoom_RecordEnded;
|
||||
logger.Debug("RemoveRoom 移除了直播间 {roomid}", rr.RoomId);
|
||||
Rooms.Remove(rr);
|
||||
}
|
||||
|
@ -133,6 +139,8 @@ namespace BililiveRecorder.Core
|
|||
Rooms.Clear();
|
||||
}
|
||||
|
||||
private void RecordedRoom_RecordEnded(object sender, RecordEndData e) => Webhook.Send(e);
|
||||
|
||||
public void SaveConfigToFile()
|
||||
{
|
||||
Config.RoomList = new List<RoomV1>();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Version>0.0.0.0</Version>
|
||||
<Authors>Genteure</Authors>
|
||||
<Company>Genteure</Company>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
|
@ -50,16 +50,16 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
public int TotalMaxTimestamp { get; private set; } = 0;
|
||||
public int CurrentMaxTimestamp { get => TotalMaxTimestamp - _writeTimeStamp; }
|
||||
public DateTime StartDateTime { get; private set; } = DateTime.Now;
|
||||
|
||||
private readonly Func<IFlvClipProcessor> funcFlvClipProcessor;
|
||||
private readonly Func<byte[], IFlvMetadata> funcFlvMetadata;
|
||||
private readonly Func<IFlvTag> funcFlvTag;
|
||||
|
||||
private Func<string> GetStreamFileName;
|
||||
private Func<(string fullPath, string relativePath)> GetStreamFileName;
|
||||
private Func<string> GetClipFileName;
|
||||
|
||||
public event TagProcessedEvent TagProcessed;
|
||||
public event EventHandler<long> FileFinalized;
|
||||
public event StreamFinalizedEvent StreamFinalized;
|
||||
public event FlvMetadataEvent OnMetaData;
|
||||
|
||||
|
@ -81,7 +81,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
}
|
||||
|
||||
public IFlvStreamProcessor Initialize(Func<string> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode)
|
||||
public IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode)
|
||||
{
|
||||
GetStreamFileName = getStreamFileName;
|
||||
GetClipFileName = getClipFileName;
|
||||
|
@ -93,10 +93,10 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
private void OpenNewRecordFile()
|
||||
{
|
||||
string path = GetStreamFileName();
|
||||
logger.Debug("打开新录制文件: " + path);
|
||||
try { Directory.CreateDirectory(Path.GetDirectoryName(path)); } catch (Exception) { }
|
||||
_targetFile = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite);
|
||||
var (fullPath, relativePath) = GetStreamFileName();
|
||||
logger.Debug("打开新录制文件: " + fullPath);
|
||||
try { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); } catch (Exception) { }
|
||||
_targetFile = new FileStream(fullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete);
|
||||
|
||||
if (_headerParsed)
|
||||
{
|
||||
|
@ -309,7 +309,6 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
_baseTimeStamp = tag.TimeStamp;
|
||||
_hasOffset = true;
|
||||
StartDateTime = DateTime.Now;
|
||||
logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
}
|
||||
}
|
||||
|
@ -325,7 +324,6 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
_baseTimeStamp = tag.TimeStamp;
|
||||
_hasOffset = true;
|
||||
StartDateTime = DateTime.Now;
|
||||
logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
}
|
||||
}
|
||||
|
@ -374,6 +372,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
try
|
||||
{
|
||||
var fileSize = _targetFile?.Length ?? -1;
|
||||
logger.Debug("正在关闭当前录制文件: " + _targetFile?.Name);
|
||||
Metadata["duration"] = CurrentMaxTimestamp / 1000.0;
|
||||
Metadata["lasttimestamp"] = (double)CurrentMaxTimestamp;
|
||||
|
@ -383,6 +382,9 @@ namespace BililiveRecorder.FlvProcessor
|
|||
// 11 for 1st tag header
|
||||
_targetFile?.Seek(13 + 11, SeekOrigin.Begin);
|
||||
_targetFile?.Write(metadata, 0, metadata.Length);
|
||||
|
||||
if (fileSize > 0)
|
||||
FileFinalized?.Invoke(this, fileSize);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public interface IFlvStreamProcessor : IDisposable
|
||||
{
|
||||
event TagProcessedEvent TagProcessed;
|
||||
event EventHandler<long> FileFinalized;
|
||||
event StreamFinalizedEvent StreamFinalized;
|
||||
event FlvMetadataEvent OnMetaData;
|
||||
|
||||
int TotalMaxTimestamp { get; }
|
||||
int CurrentMaxTimestamp { get; }
|
||||
DateTime StartDateTime { get; }
|
||||
|
||||
IFlvMetadata Metadata { get; set; }
|
||||
ObservableCollection<IFlvClipProcessor> Clips { get; }
|
||||
|
@ -19,7 +20,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
uint ClipLengthFuture { get; set; }
|
||||
uint CuttingNumber { get; set; }
|
||||
|
||||
IFlvStreamProcessor Initialize(Func<string> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode);
|
||||
IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode);
|
||||
IFlvClipProcessor Clip();
|
||||
void AddBytes(byte[] data);
|
||||
void FinallizeFile();
|
||||
|
|
|
@ -85,10 +85,10 @@
|
|||
Foreground="{Binding IsStreaming,Converter={StaticResource BooleanToLiveStatusColorBrushConverter}}"
|
||||
ToolTip="{Binding IsStreaming,Converter={StaticResource BooleanToLiveStatusTooltipConverter}}"/>
|
||||
<ui:PathIcon Height="10" Style="{StaticResource PathIconDataUpperCaseIdentifier}" />
|
||||
<TextBlock Text="{Binding RoomId, StringFormat=\{0\},Mode=OneWay}" ContextMenu="{StaticResource CopyTextContextMenu}" Margin="4,0"/>
|
||||
<TextBlock Text="{Binding RoomId, StringFormat=\{0\},Mode=OneWay}" ContextMenu="{StaticResource CopyTextContextMenu}" Margin="4,0" FontSize="11"/>
|
||||
<ui:PathIcon Height="10" Style="{StaticResource PathIconDataLowerCaseIdentifier}" Margin="3,0"
|
||||
Visibility="{Binding ShortRoomId,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
<TextBlock Text="{Binding ShortRoomId, StringFormat=\{0\},Mode=OneWay}" ContextMenu="{StaticResource CopyTextContextMenu}"
|
||||
<TextBlock Text="{Binding ShortRoomId, StringFormat=\{0\},Mode=OneWay}" ContextMenu="{StaticResource CopyTextContextMenu}" FontSize="11"
|
||||
Visibility="{Binding ShortRoomId,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Visibility="{Binding Visibility, ElementName=RecordingIcon}"
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.WPF.MockData
|
||||
{
|
||||
#if DEBUG
|
||||
|
@ -28,9 +30,11 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
public string StreamerName { get; set; }
|
||||
|
||||
public IStreamMonitor StreamMonitor { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
public IFlvStreamProcessor Processor { get; set; }
|
||||
public IStreamMonitor StreamMonitor { get; set; } = null!;
|
||||
|
||||
public IFlvStreamProcessor? Processor { get; set; }
|
||||
|
||||
public bool IsMonitoring { get; set; }
|
||||
|
||||
|
@ -48,7 +52,9 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
public Guid Guid { get; } = Guid.NewGuid();
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public event EventHandler<RecordEndData>? RecordEnded;
|
||||
|
||||
public void Clip()
|
||||
{
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
x:Key="UniformGridLayout"
|
||||
MinItemWidth="220"
|
||||
MinItemHeight="100"
|
||||
ItemsStretch="Fill"
|
||||
ItemsStretch="None"
|
||||
MinRowSpacing="7"
|
||||
MinColumnSpacing="5" />
|
||||
</ui:Page.Resources>
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
@ -238,14 +239,34 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
public List<IRecordedRoom> Sorted { get; private set; }
|
||||
|
||||
private void Sort()
|
||||
private int sortSeboucneCount = int.MinValue;
|
||||
private SemaphoreSlim sortSemaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
|
||||
private async void Sort()
|
||||
{
|
||||
logger.Debug("Sort called with {sortedBy} and {count} rooms.", SortedBy, Data?.Count ?? -1);
|
||||
// debounce && lock
|
||||
logger.Debug("Sort called.");
|
||||
var callCount = Interlocked.Increment(ref sortSeboucneCount);
|
||||
await Task.Delay(200);
|
||||
if (sortSeboucneCount != callCount)
|
||||
{
|
||||
logger.Debug("Sort cancelled by debounce.");
|
||||
return;
|
||||
}
|
||||
|
||||
await sortSemaphoreSlim.WaitAsync();
|
||||
try { SortImpl(); }
|
||||
finally { sortSemaphoreSlim.Release(); }
|
||||
}
|
||||
|
||||
private void SortImpl()
|
||||
{
|
||||
logger.Debug("SortImpl called with {sortedBy} and {count} rooms.", SortedBy, Data?.Count ?? -1);
|
||||
|
||||
if (Data is null)
|
||||
{
|
||||
Sorted = NullRoom.ToList();
|
||||
logger.Debug("Sort returned NullRoom.");
|
||||
logger.Debug("SortImpl returned NullRoom.");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -256,7 +277,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
_ => Data,
|
||||
};
|
||||
var result = orderedData.Concat(NullRoom).ToList();
|
||||
logger.Debug("Sorted with {count} items.", result.Count);
|
||||
logger.Debug("SortImpl sorted with {count} items.", result.Count);
|
||||
|
||||
{ // 崩溃问题信息收集。。虽然不觉得是这里的问题
|
||||
var dup = result.GroupBy(x => x?.Guid ?? Guid.Empty).Where(x => x.Count() != 1);
|
||||
|
|
|
@ -198,7 +198,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
catch (Exception ex)
|
||||
{
|
||||
error = "发生了未知错误";
|
||||
logger.Error(ex, "选择工作目录时发生了未知错误");
|
||||
logger.Warn(ex, "选择工作目录时发生了未知错误");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,6 +113,13 @@
|
|||
Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityCollapsedConverter}}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Webhook">
|
||||
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
||||
<TextBlock Text="Webhook 地址,一行一个"/>
|
||||
<TextBox AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible"
|
||||
Text="{Binding WebHookUrls,UpdateSourceTrigger=PropertyChanged,Delay=1000}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</ui:SimpleStackPanel>
|
||||
</ScrollViewer>
|
||||
</ui:Page>
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace BililiveRecorder.WPF
|
|||
if (!File.Exists("BILILIVE_RECORDER_DISABLE_SENTRY")
|
||||
&& string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("BILILIVE_RECORDER_DISABLE_SENTRY")))
|
||||
{
|
||||
o.Dsn = new Dsn("https://55afa848ac49493a80cc4366b34e9552@o210546.ingest.sentry.io/5556540");
|
||||
o.Dsn = new Dsn("https://efc16b0fd5604608b811c3b358e9d1f1@o210546.ingest.sentry.io/5556540");
|
||||
}
|
||||
|
||||
var v = typeof(Program).Assembly.GetName().Version;
|
||||
|
|
27
appveyor.yml
27
appveyor.yml
|
@ -38,6 +38,13 @@ before_build:
|
|||
|
||||
build_script:
|
||||
- ps: msbuild /nologo /v:m /p:Configuration="$env:CONFIGURATION" /p:SquirrelBuildTarget="$env:DEPLOY_SITE_GIT\BililiveRecorder" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /p:RuntimeIdentifier="linux-arm" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /p:RuntimeIdentifier="linux-arm64" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /p:RuntimeIdentifier="linux-x64" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /p:RuntimeIdentifier="osx.10.11-x64" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /p:RuntimeIdentifier="osx-x64" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
- ps: msbuild /nologo /v:m /t:BililiveRecorder_Cli:Publish /p:Configuration="$env:CONFIGURATION" /p:RuntimeIdentifier="win-x64" /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||
|
||||
for:
|
||||
-
|
||||
|
@ -59,7 +66,21 @@ for:
|
|||
configuration: Debug
|
||||
artifacts:
|
||||
- path: BililiveRecorder.WPF\bin\Debug
|
||||
name: BililiveRecorderDebugBuild
|
||||
name: BililiveRecorderWPFDebugBuild
|
||||
- path: BililiveRecorder.Cli\publish\any
|
||||
name: BililiveRecorderCliDebugBuild
|
||||
- path: BililiveRecorder.Cli\publish\linux-arm
|
||||
name: BililiveRecorderCliDebugBuild-linux-arm
|
||||
- path: BililiveRecorder.Cli\publish\linux-arm64
|
||||
name: BililiveRecorderCliDebugBuild-linux-arm64
|
||||
- path: BililiveRecorder.Cli\publish\linux-x64
|
||||
name: BililiveRecorderCliDebugBuild-linux-x64
|
||||
- path: BililiveRecorder.Cli\publish\osx.10.11-x64
|
||||
name: BililiveRecorderCliDebugBuild-osx.10.11-x64
|
||||
- path: BililiveRecorder.Cli\publish\osx-x64
|
||||
name: BililiveRecorderCliDebugBuild-osx-x64
|
||||
- path: BililiveRecorder.Cli\publish\win-x64
|
||||
name: BililiveRecorderCliDebugBuild-win-x64
|
||||
|
||||
#on_finish:
|
||||
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
on_finish:
|
||||
#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
|
|
Loading…
Reference in New Issue
Block a user