Merge pull request #142 from Bililive/dev

Release 1.1.26
This commit is contained in:
Genteure 2020-12-23 20:39:03 +08:00 committed by GitHub
commit d37a3c2922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 346 additions and 171 deletions

View File

@ -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

View File

@ -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)&#xD;&#xA;powershell -ExecutionPolicy Bypass -File .\CI\patch_buildinfo.ps1 Cli" />
</Target>
</Project>

View File

@ -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;
}
}
}
}

View File

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

View File

@ -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>

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

View 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; }
}
}

View File

@ -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 = "";
}
}

View File

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

View File

@ -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; }

View File

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

View File

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

View File

@ -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>

View File

@ -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)
{

View File

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

View File

@ -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}"

View File

@ -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()
{

View File

@ -33,7 +33,7 @@
x:Key="UniformGridLayout"
MinItemWidth="220"
MinItemHeight="100"
ItemsStretch="Fill"
ItemsStretch="None"
MinRowSpacing="7"
MinColumnSpacing="5" />
</ui:Page.Resources>

View File

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

View File

@ -198,7 +198,7 @@ namespace BililiveRecorder.WPF.Pages
catch (Exception ex)
{
error = "发生了未知错误";
logger.Error(ex, "选择工作目录时发生了未知错误");
logger.Warn(ex, "选择工作目录时发生了未知错误");
continue;
}
}

View File

@ -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>

View File

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

View File

@ -1 +1 @@
1.1.25
1.1.26

View File

@ -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'))