feat: expand platform adapters and preview tooling
This commit is contained in:
+6
-2
@@ -6,7 +6,10 @@ namespace LiveRecorder.Application.Abstractions.Notifications;
|
||||
|
||||
public interface IEmailNotificationService
|
||||
{
|
||||
Task SendLiveStartedAsync(LiveRoom liveRoom, CancellationToken cancellationToken = default);
|
||||
Task SendLiveStartedAsync(
|
||||
LiveRoom liveRoom,
|
||||
CancellationToken cancellationToken = default,
|
||||
string? eventScriptOutput = null);
|
||||
|
||||
Task SendExceptionAsync(
|
||||
string source,
|
||||
@@ -14,7 +17,8 @@ public interface IEmailNotificationService
|
||||
string? detail = null,
|
||||
LiveRoom? liveRoom = null,
|
||||
RecordTask? recordTask = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
CancellationToken cancellationToken = default,
|
||||
string? eventScriptOutput = null);
|
||||
|
||||
Task SendTestAsync(SendTestEmailRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
+6
-2
@@ -6,7 +6,10 @@ namespace LiveRecorder.Application.Abstractions.Notifications;
|
||||
|
||||
public interface IWebhookNotificationService
|
||||
{
|
||||
Task SendLiveStartedAsync(LiveRoom liveRoom, CancellationToken cancellationToken = default);
|
||||
Task SendLiveStartedAsync(
|
||||
LiveRoom liveRoom,
|
||||
CancellationToken cancellationToken = default,
|
||||
string? eventScriptOutput = null);
|
||||
|
||||
Task SendExceptionAsync(
|
||||
string source,
|
||||
@@ -14,7 +17,8 @@ public interface IWebhookNotificationService
|
||||
string? detail = null,
|
||||
LiveRoom? liveRoom = null,
|
||||
RecordTask? recordTask = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
CancellationToken cancellationToken = default,
|
||||
string? eventScriptOutput = null);
|
||||
|
||||
Task<WebhookTestResultDto> SendTestAsync(
|
||||
SendTestWebhookRequest request,
|
||||
|
||||
@@ -14,6 +14,8 @@ public interface ILiveDanmakuAdapter
|
||||
public interface ILiveDanmakuAdapterFactory
|
||||
{
|
||||
ILiveDanmakuAdapter GetByPlatform(LivePlatformType platformType);
|
||||
|
||||
ILiveDanmakuAdapter? TryGetByPlatform(LivePlatformType platformType);
|
||||
}
|
||||
|
||||
public interface ILiveDanmakuConnection : IAsyncDisposable
|
||||
|
||||
@@ -5,11 +5,17 @@ namespace LiveRecorder.Application.Abstractions.Scripting;
|
||||
|
||||
public interface IEventScriptService
|
||||
{
|
||||
Task RunLiveStartedAsync(LiveRoom liveRoom, DateTimeOffset occurredAt, CancellationToken cancellationToken = default);
|
||||
Task<EventScriptExecutionResultDto?> RunLiveStartedAsync(
|
||||
LiveRoom liveRoom,
|
||||
DateTimeOffset occurredAt,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task RunLiveEndedAsync(LiveRoom liveRoom, DateTimeOffset occurredAt, CancellationToken cancellationToken = default);
|
||||
Task<EventScriptExecutionResultDto?> RunLiveEndedAsync(
|
||||
LiveRoom liveRoom,
|
||||
DateTimeOffset occurredAt,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task RunSegmentCompletedAsync(
|
||||
Task<EventScriptExecutionResultDto?> RunSegmentCompletedAsync(
|
||||
LiveRoom? liveRoom,
|
||||
RecordSession recordSession,
|
||||
RecordTask recordTask,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using LiveRecorder.Domain.Enums;
|
||||
|
||||
namespace LiveRecorder.Application.Common;
|
||||
|
||||
public sealed record LivePlatformDefinition(
|
||||
LivePlatformType Type,
|
||||
string Key,
|
||||
string DisplayName,
|
||||
string DefaultReferer,
|
||||
string DefaultUserAgent);
|
||||
|
||||
public static class LivePlatformCatalog
|
||||
{
|
||||
public const string GenericDesktopUserAgent =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0";
|
||||
|
||||
private static readonly LivePlatformDefinition[] Definitions =
|
||||
[
|
||||
new(LivePlatformType.Douyin, "douyin", "Douyin", "https://live.douyin.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Bilibili, "bilibili", "Bilibili", "https://live.bilibili.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Huya, "huya", "Huya", "https://www.huya.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Douyu, "douyu", "Douyu", "https://www.douyu.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Kuaishou, "kuaishou", "Kuaishou", "https://live.kuaishou.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.TikTok, "tiktok", "TikTok", "https://www.tiktok.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Xiaohongshu, "xiaohongshu", "Xiaohongshu", "https://www.xiaohongshu.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.YouTube, "youtube", "YouTube", "https://www.youtube.com/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Twitch, "twitch", "Twitch", "https://www.twitch.tv/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.PandaTV, "pandatv", "PandaTV", "https://www.pandalive.co.kr/", GenericDesktopUserAgent),
|
||||
new(LivePlatformType.Migu, "migu", "Migu", "https://www.miguvideo.com/", GenericDesktopUserAgent)
|
||||
];
|
||||
|
||||
private static readonly IReadOnlyDictionary<LivePlatformType, LivePlatformDefinition> DefinitionByType =
|
||||
Definitions.ToDictionary(static item => item.Type);
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, LivePlatformDefinition> DefinitionByKey =
|
||||
Definitions.ToDictionary(static item => item.Key, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static IReadOnlyList<LivePlatformDefinition> All => Definitions;
|
||||
|
||||
public static LivePlatformDefinition Get(LivePlatformType platformType)
|
||||
{
|
||||
if (!TryGet(platformType, out var definition))
|
||||
{
|
||||
throw new NotSupportedException($"Unsupported live platform: {platformType}.");
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
public static bool TryGet(LivePlatformType platformType, out LivePlatformDefinition definition) =>
|
||||
DefinitionByType.TryGetValue(platformType, out definition!);
|
||||
|
||||
public static bool TryGetByKey(string? key, out LivePlatformDefinition definition)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
definition = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
return DefinitionByKey.TryGetValue(key.Trim(), out definition!);
|
||||
}
|
||||
|
||||
public static string GetKey(LivePlatformType platformType) => Get(platformType).Key;
|
||||
|
||||
public static string GetDisplayName(LivePlatformType platformType) =>
|
||||
TryGet(platformType, out var definition)
|
||||
? definition.DisplayName
|
||||
: platformType.ToString();
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using LiveRecorder.Domain.Enums;
|
||||
using System.Text.Json.Serialization;
|
||||
using LiveRecorder.Application.Common;
|
||||
using LiveRecorder.Application.Models.Cleanup;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
|
||||
namespace LiveRecorder.Application.Models.Settings;
|
||||
|
||||
@@ -17,6 +19,41 @@ public sealed class PlatformProxySettingsDto
|
||||
public string ProxyUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class PlatformRequestSettingsDto
|
||||
{
|
||||
public PlatformProxySettingsDto Proxy { get; set; } = new();
|
||||
|
||||
public string UserAgent { get; set; } = string.Empty;
|
||||
|
||||
public string Referer { get; set; } = string.Empty;
|
||||
|
||||
public string Cookie { get; set; } = string.Empty;
|
||||
|
||||
public static PlatformRequestSettingsDto CreateDefault(LivePlatformType platformType)
|
||||
{
|
||||
var definition = LivePlatformCatalog.Get(platformType);
|
||||
return new PlatformRequestSettingsDto
|
||||
{
|
||||
Proxy = new PlatformProxySettingsDto(),
|
||||
UserAgent = definition.DefaultUserAgent,
|
||||
Referer = definition.DefaultReferer,
|
||||
Cookie = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public PlatformRequestSettingsDto Clone() => new()
|
||||
{
|
||||
Proxy = new PlatformProxySettingsDto
|
||||
{
|
||||
Enabled = Proxy.Enabled,
|
||||
ProxyUrl = Proxy.ProxyUrl
|
||||
},
|
||||
UserAgent = UserAgent,
|
||||
Referer = Referer,
|
||||
Cookie = Cookie
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class WebDavUploadSettingsDto
|
||||
{
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
@@ -105,11 +142,8 @@ public sealed class SystemSettingsDto
|
||||
|
||||
public UploadTargetType UploadTarget { get; set; } = UploadTargetType.None;
|
||||
|
||||
public PlatformProxySettingsDto DouyinProxy { get; set; } = new();
|
||||
|
||||
public PlatformProxySettingsDto BilibiliProxy { get; set; } = new();
|
||||
|
||||
public PlatformProxySettingsDto HuyaProxy { get; set; } = new();
|
||||
public IDictionary<string, PlatformRequestSettingsDto> PlatformRequestSettings { get; set; } =
|
||||
CreatePlatformRequestSettingsMap();
|
||||
|
||||
public WebDavUploadSettingsDto WebDavUpload { get; set; } = new();
|
||||
|
||||
@@ -189,6 +223,10 @@ public sealed class SystemSettingsDto
|
||||
<li><strong>Detected At (Beijing Time):</strong> {{detectedAtUtc}}</li>
|
||||
</ul>
|
||||
<p><strong>Source URL:</strong> <a href="{{sourceUrl}}">{{sourceUrl}}</a></p>
|
||||
<div style="margin-top: 16px;">
|
||||
<strong>Event Script Output:</strong>
|
||||
</div>
|
||||
<div style="margin-top: 8px; padding: 12px 14px; border-radius: 8px; background: #f5f5f5; white-space: pre-wrap;">{{eventScriptOutput}}</div>
|
||||
</div>
|
||||
""";
|
||||
|
||||
@@ -224,12 +262,93 @@ public sealed class SystemSettingsDto
|
||||
|
||||
public bool NotifyWebhookOnException { get; set; } = true;
|
||||
|
||||
public string DouyinUserAgent { get; set; } =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||
[JsonIgnore]
|
||||
public PlatformProxySettingsDto DouyinProxy
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).Proxy;
|
||||
set => SetPlatformProxy(LivePlatformType.Douyin, value);
|
||||
}
|
||||
|
||||
public string DouyinReferer { get; set; } = "https://live.douyin.com/";
|
||||
[JsonIgnore]
|
||||
public PlatformProxySettingsDto BilibiliProxy
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Bilibili).Proxy;
|
||||
set => SetPlatformProxy(LivePlatformType.Bilibili, value);
|
||||
}
|
||||
|
||||
public string DouyinCookie { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public PlatformProxySettingsDto HuyaProxy
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Huya).Proxy;
|
||||
set => SetPlatformProxy(LivePlatformType.Huya, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DouyinUserAgent
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).UserAgent;
|
||||
set => UpdatePlatformRequestSettings(LivePlatformType.Douyin, settings => settings.UserAgent = value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DouyinReferer
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).Referer;
|
||||
set => UpdatePlatformRequestSettings(LivePlatformType.Douyin, settings => settings.Referer = value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DouyinCookie
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).Cookie;
|
||||
set => UpdatePlatformRequestSettings(LivePlatformType.Douyin, settings => settings.Cookie = value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public PlatformRequestSettingsDto GetPlatformRequestSettings(LivePlatformType platformType)
|
||||
{
|
||||
var platformKey = LivePlatformCatalog.GetKey(platformType);
|
||||
if (!PlatformRequestSettings.TryGetValue(platformKey, out var settings) || settings is null)
|
||||
{
|
||||
settings = PlatformRequestSettingsDto.CreateDefault(platformType);
|
||||
PlatformRequestSettings[platformKey] = settings;
|
||||
}
|
||||
|
||||
settings.Proxy ??= new PlatformProxySettingsDto();
|
||||
settings.UserAgent = string.IsNullOrWhiteSpace(settings.UserAgent)
|
||||
? LivePlatformCatalog.Get(platformType).DefaultUserAgent
|
||||
: settings.UserAgent.Trim();
|
||||
settings.Referer = string.IsNullOrWhiteSpace(settings.Referer)
|
||||
? LivePlatformCatalog.Get(platformType).DefaultReferer
|
||||
: settings.Referer.Trim();
|
||||
settings.Cookie = settings.Cookie?.Trim() ?? string.Empty;
|
||||
settings.Proxy.ProxyUrl = settings.Proxy.ProxyUrl?.Trim() ?? string.Empty;
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static Dictionary<string, PlatformRequestSettingsDto> CreatePlatformRequestSettingsMap()
|
||||
{
|
||||
return LivePlatformCatalog.All.ToDictionary(
|
||||
static item => item.Key,
|
||||
static item => PlatformRequestSettingsDto.CreateDefault(item.Type),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void SetPlatformProxy(LivePlatformType platformType, PlatformProxySettingsDto? proxy)
|
||||
{
|
||||
UpdatePlatformRequestSettings(
|
||||
platformType,
|
||||
settings =>
|
||||
{
|
||||
settings.Proxy = proxy ?? new PlatformProxySettingsDto();
|
||||
settings.Proxy.ProxyUrl = settings.Proxy.ProxyUrl?.Trim() ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdatePlatformRequestSettings(LivePlatformType platformType, Action<PlatformRequestSettingsDto> update)
|
||||
{
|
||||
var settings = GetPlatformRequestSettings(platformType);
|
||||
update(settings);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpdateSystemSettingsRequest
|
||||
@@ -292,11 +411,8 @@ public sealed class UpdateSystemSettingsRequest
|
||||
|
||||
public UploadTargetType UploadTarget { get; set; } = UploadTargetType.None;
|
||||
|
||||
public PlatformProxySettingsDto DouyinProxy { get; set; } = new();
|
||||
|
||||
public PlatformProxySettingsDto BilibiliProxy { get; set; } = new();
|
||||
|
||||
public PlatformProxySettingsDto HuyaProxy { get; set; } = new();
|
||||
public IDictionary<string, PlatformRequestSettingsDto> PlatformRequestSettings { get; set; } =
|
||||
SystemSettingsDto.CreatePlatformRequestSettingsMap();
|
||||
|
||||
public WebDavUploadSettingsDto WebDavUpload { get; set; } = new();
|
||||
|
||||
@@ -376,6 +492,10 @@ public sealed class UpdateSystemSettingsRequest
|
||||
<li><strong>Detected At (Beijing Time):</strong> {{detectedAtUtc}}</li>
|
||||
</ul>
|
||||
<p><strong>Source URL:</strong> <a href="{{sourceUrl}}">{{sourceUrl}}</a></p>
|
||||
<div style="margin-top: 16px;">
|
||||
<strong>Event Script Output:</strong>
|
||||
</div>
|
||||
<div style="margin-top: 8px; padding: 12px 14px; border-radius: 8px; background: #f5f5f5; white-space: pre-wrap;">{{eventScriptOutput}}</div>
|
||||
</div>
|
||||
""";
|
||||
|
||||
@@ -411,12 +531,85 @@ public sealed class UpdateSystemSettingsRequest
|
||||
|
||||
public bool NotifyWebhookOnException { get; set; } = true;
|
||||
|
||||
public string DouyinUserAgent { get; set; } =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||
[JsonIgnore]
|
||||
public PlatformProxySettingsDto DouyinProxy
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).Proxy;
|
||||
set => SetPlatformProxy(LivePlatformType.Douyin, value);
|
||||
}
|
||||
|
||||
public string DouyinReferer { get; set; } = "https://live.douyin.com/";
|
||||
[JsonIgnore]
|
||||
public PlatformProxySettingsDto BilibiliProxy
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Bilibili).Proxy;
|
||||
set => SetPlatformProxy(LivePlatformType.Bilibili, value);
|
||||
}
|
||||
|
||||
public string DouyinCookie { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public PlatformProxySettingsDto HuyaProxy
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Huya).Proxy;
|
||||
set => SetPlatformProxy(LivePlatformType.Huya, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DouyinUserAgent
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).UserAgent;
|
||||
set => UpdatePlatformRequestSettings(LivePlatformType.Douyin, settings => settings.UserAgent = value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DouyinReferer
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).Referer;
|
||||
set => UpdatePlatformRequestSettings(LivePlatformType.Douyin, settings => settings.Referer = value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DouyinCookie
|
||||
{
|
||||
get => GetPlatformRequestSettings(LivePlatformType.Douyin).Cookie;
|
||||
set => UpdatePlatformRequestSettings(LivePlatformType.Douyin, settings => settings.Cookie = value?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
public PlatformRequestSettingsDto GetPlatformRequestSettings(LivePlatformType platformType)
|
||||
{
|
||||
var platformKey = LivePlatformCatalog.GetKey(platformType);
|
||||
if (!PlatformRequestSettings.TryGetValue(platformKey, out var settings) || settings is null)
|
||||
{
|
||||
settings = PlatformRequestSettingsDto.CreateDefault(platformType);
|
||||
PlatformRequestSettings[platformKey] = settings;
|
||||
}
|
||||
|
||||
settings.Proxy ??= new PlatformProxySettingsDto();
|
||||
settings.UserAgent = string.IsNullOrWhiteSpace(settings.UserAgent)
|
||||
? LivePlatformCatalog.Get(platformType).DefaultUserAgent
|
||||
: settings.UserAgent.Trim();
|
||||
settings.Referer = string.IsNullOrWhiteSpace(settings.Referer)
|
||||
? LivePlatformCatalog.Get(platformType).DefaultReferer
|
||||
: settings.Referer.Trim();
|
||||
settings.Cookie = settings.Cookie?.Trim() ?? string.Empty;
|
||||
settings.Proxy.ProxyUrl = settings.Proxy.ProxyUrl?.Trim() ?? string.Empty;
|
||||
return settings;
|
||||
}
|
||||
|
||||
private void SetPlatformProxy(LivePlatformType platformType, PlatformProxySettingsDto? proxy)
|
||||
{
|
||||
UpdatePlatformRequestSettings(
|
||||
platformType,
|
||||
settings =>
|
||||
{
|
||||
settings.Proxy = proxy ?? new PlatformProxySettingsDto();
|
||||
settings.Proxy.ProxyUrl = settings.Proxy.ProxyUrl?.Trim() ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdatePlatformRequestSettings(LivePlatformType platformType, Action<PlatformRequestSettingsDto> update)
|
||||
{
|
||||
var settings = GetPlatformRequestSettings(platformType);
|
||||
update(settings);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SendTestEmailRequest
|
||||
@@ -451,6 +644,10 @@ public sealed class SendTestEmailRequest
|
||||
<li><strong>Detected At (Beijing Time):</strong> {{detectedAtUtc}}</li>
|
||||
</ul>
|
||||
<p><strong>Source URL:</strong> <a href="{{sourceUrl}}">{{sourceUrl}}</a></p>
|
||||
<div style="margin-top: 16px;">
|
||||
<strong>Event Script Output:</strong>
|
||||
</div>
|
||||
<div style="margin-top: 8px; padding: 12px 14px; border-radius: 8px; background: #f5f5f5; white-space: pre-wrap;">{{eventScriptOutput}}</div>
|
||||
</div>
|
||||
""";
|
||||
|
||||
@@ -486,6 +683,17 @@ public sealed class TestEventScriptRequest
|
||||
public int TimeoutSeconds { get; set; } = 60;
|
||||
}
|
||||
|
||||
public sealed class EventScriptExecutionResultDto
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
|
||||
public required string Message { get; init; }
|
||||
|
||||
public string? Detail { get; init; }
|
||||
|
||||
public string? CustomLogOutput { get; init; }
|
||||
}
|
||||
|
||||
public sealed class EventScriptTestResultDto
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
|
||||
@@ -21,4 +21,7 @@ public sealed class LiveDanmakuAdapterFactory : ILiveDanmakuAdapterFactory
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public ILiveDanmakuAdapter? TryGetByPlatform(LivePlatformType platformType) =>
|
||||
_adapterByPlatform.TryGetValue(platformType, out var adapter) ? adapter : null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@ public sealed class LiveRoomService
|
||||
"""^(?:(?:https?:\/\/)?live\.bilibili\.com\/(?:blanc\/|h5\/)?)?(?<id>\d+)\/?(?:[#\?].*)?$""",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly Regex YouTubeVideoIdRegex = new(
|
||||
@"[A-Za-z0-9_-]{11}",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly Regex TikTokHandleRegex = new(
|
||||
@"@(?<handle>[A-Za-z0-9._-]{2,})",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
private readonly ILiveRoomRepository _liveRoomRepository;
|
||||
private readonly IRecordSessionRepository _recordSessionRepository;
|
||||
private readonly ILivePlatformAdapterFactory _livePlatformAdapterFactory;
|
||||
@@ -507,6 +515,15 @@ public sealed class LiveRoomService
|
||||
{
|
||||
LivePlatformType.Douyin => ParseDouyinRoomLocally(input),
|
||||
LivePlatformType.Bilibili => ParseBilibiliRoomLocally(input),
|
||||
LivePlatformType.Huya => ParsePathRoomLocally(input, platform, "https://www.huya.com/"),
|
||||
LivePlatformType.Douyu => ParsePathRoomLocally(input, platform, "https://www.douyu.com/"),
|
||||
LivePlatformType.Kuaishou => ParsePathRoomLocally(input, platform, "https://live.kuaishou.com/"),
|
||||
LivePlatformType.TikTok => ParseTikTokRoomLocally(input),
|
||||
LivePlatformType.Xiaohongshu => ParsePathRoomLocally(input, platform, "https://www.xiaohongshu.com/"),
|
||||
LivePlatformType.YouTube => ParseYouTubeRoomLocally(input),
|
||||
LivePlatformType.Twitch => ParseTwitchRoomLocally(input),
|
||||
LivePlatformType.PandaTV => ParsePathRoomLocally(input, platform, "https://www.pandalive.co.kr/"),
|
||||
LivePlatformType.Migu => ParsePathRoomLocally(input, platform, "https://www.miguvideo.com/"),
|
||||
_ => throw new NotSupportedException(
|
||||
"This platform still requires live room detection during import. Please enable detection or choose a supported platform.")
|
||||
};
|
||||
@@ -530,6 +547,53 @@ public sealed class LiveRoomService
|
||||
return LivePlatformType.Bilibili;
|
||||
}
|
||||
|
||||
if (input.Contains("huya.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Huya;
|
||||
}
|
||||
|
||||
if (input.Contains("douyu.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Douyu;
|
||||
}
|
||||
|
||||
if (input.Contains("kuaishou.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Kuaishou;
|
||||
}
|
||||
|
||||
if (input.Contains("tiktok.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.TikTok;
|
||||
}
|
||||
|
||||
if (input.Contains("xiaohongshu.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
input.Contains("xhslink.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Xiaohongshu;
|
||||
}
|
||||
|
||||
if (input.Contains("youtube.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
input.Contains("youtu.be", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.YouTube;
|
||||
}
|
||||
|
||||
if (input.Contains("twitch.tv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Twitch;
|
||||
}
|
||||
|
||||
if (input.Contains("pandalive.co.kr", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.PandaTV;
|
||||
}
|
||||
|
||||
if (input.Contains("miguvideo.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Migu;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(
|
||||
"Unable to infer the platform without detection. Please specify a platform override or keep detection enabled.");
|
||||
}
|
||||
@@ -566,6 +630,74 @@ public sealed class LiveRoomService
|
||||
$"https://live.bilibili.com/{roomId}");
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParsePathRoomLocally(string input, LivePlatformType platformType, string rootUrl)
|
||||
{
|
||||
if (!TryExtractPathBasedRoomId(input, out var roomId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unable to extract the {platformType} room id locally. Please keep detection enabled for this link.");
|
||||
}
|
||||
|
||||
return new ParsedLiveRoom(
|
||||
platformType,
|
||||
roomId,
|
||||
input.Trim(),
|
||||
$"{rootUrl.TrimEnd('/')}/{roomId}");
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParseTikTokRoomLocally(string input)
|
||||
{
|
||||
var match = TikTokHandleRegex.Match(input.Trim());
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to extract the TikTok handle locally. Please keep detection enabled for this link.");
|
||||
}
|
||||
|
||||
var handle = match.Groups["handle"].Value;
|
||||
return new ParsedLiveRoom(
|
||||
LivePlatformType.TikTok,
|
||||
handle,
|
||||
input.Trim(),
|
||||
$"https://www.tiktok.com/@{handle}/live");
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParseYouTubeRoomLocally(string input)
|
||||
{
|
||||
var roomId = ExtractYouTubeVideoId(input);
|
||||
if (string.IsNullOrWhiteSpace(roomId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to extract the YouTube video id locally. Please keep detection enabled for this link.");
|
||||
}
|
||||
|
||||
return new ParsedLiveRoom(
|
||||
LivePlatformType.YouTube,
|
||||
roomId,
|
||||
input.Trim(),
|
||||
$"https://www.youtube.com/watch?v={roomId}");
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParseTwitchRoomLocally(string input)
|
||||
{
|
||||
if (!TryExtractPathBasedRoomId(input, out var roomId))
|
||||
{
|
||||
roomId = input.Trim().Trim('/');
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(roomId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to extract the Twitch channel login locally. Please keep detection enabled for this link.");
|
||||
}
|
||||
|
||||
return new ParsedLiveRoom(
|
||||
LivePlatformType.Twitch,
|
||||
roomId,
|
||||
input.Trim(),
|
||||
$"https://www.twitch.tv/{roomId}");
|
||||
}
|
||||
|
||||
private static string? ExtractDouyinRoomId(string input)
|
||||
{
|
||||
var trimmedInput = input.Trim();
|
||||
@@ -593,6 +725,56 @@ public sealed class LiveRoomService
|
||||
.FirstOrDefault(static item => !string.IsNullOrWhiteSpace(item) && item.All(char.IsDigit));
|
||||
}
|
||||
|
||||
private static string? ExtractYouTubeVideoId(string input)
|
||||
{
|
||||
var trimmedInput = input.Trim();
|
||||
var directMatch = YouTubeVideoIdRegex.Match(trimmedInput);
|
||||
if (directMatch.Success && directMatch.Index == 0 && directMatch.Length == trimmedInput.Length)
|
||||
{
|
||||
return directMatch.Value;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(trimmedInput, UriKind.Absolute, out var uri))
|
||||
{
|
||||
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
|
||||
if (query.TryGetValue("v", out var videoId) &&
|
||||
!string.IsNullOrWhiteSpace(videoId.ToString()) &&
|
||||
YouTubeVideoIdRegex.IsMatch(videoId.ToString()))
|
||||
{
|
||||
return videoId.ToString();
|
||||
}
|
||||
|
||||
var segment = uri.Segments
|
||||
.Select(static item => item.Trim('/'))
|
||||
.FirstOrDefault(static item => !string.IsNullOrWhiteSpace(item) && YouTubeVideoIdRegex.IsMatch(item));
|
||||
if (!string.IsNullOrWhiteSpace(segment))
|
||||
{
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryExtractPathBasedRoomId(string input, out string roomId)
|
||||
{
|
||||
roomId = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmedInput = input.Trim().Trim('/');
|
||||
if (!Uri.TryCreate(trimmedInput, UriKind.Absolute, out var uri))
|
||||
{
|
||||
roomId = trimmedInput;
|
||||
return !string.IsNullOrWhiteSpace(roomId);
|
||||
}
|
||||
|
||||
roomId = uri.AbsolutePath.Trim('/');
|
||||
return !string.IsNullOrWhiteSpace(roomId);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<Guid, RecordingExecutionSettings>> BuildEffectiveSettingsLookupAsync(
|
||||
IReadOnlyCollection<LiveRoom> rooms,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
@@ -60,9 +60,15 @@ public sealed class LiveRoomStatusService
|
||||
return;
|
||||
}
|
||||
|
||||
await _emailNotificationService.SendLiveStartedAsync(liveRoom, cancellationToken);
|
||||
await _webhookNotificationService.SendLiveStartedAsync(liveRoom, cancellationToken);
|
||||
await _eventScriptService.RunLiveStartedAsync(liveRoom, observedAt, cancellationToken);
|
||||
var eventScriptResult = await _eventScriptService.RunLiveStartedAsync(liveRoom, observedAt, cancellationToken);
|
||||
await _emailNotificationService.SendLiveStartedAsync(
|
||||
liveRoom,
|
||||
cancellationToken,
|
||||
eventScriptOutput: eventScriptResult?.CustomLogOutput);
|
||||
await _webhookNotificationService.SendLiveStartedAsync(
|
||||
liveRoom,
|
||||
cancellationToken,
|
||||
eventScriptOutput: eventScriptResult?.CustomLogOutput);
|
||||
liveRoom.MarkLiveNotificationSent(observedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using LiveRecorder.Application.Abstractions.Persistence;
|
||||
using LiveRecorder.Application.Abstractions.Settings;
|
||||
using LiveRecorder.Application.Common;
|
||||
using LiveRecorder.Application.Models.Cleanup;
|
||||
using LiveRecorder.Application.Models.Settings;
|
||||
using LiveRecorder.Domain.Entities;
|
||||
@@ -154,21 +155,7 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
UploadTarget = Enum.TryParse(GetValue(lookup, UploadTargetKey, "None"), true, out UploadTargetType uploadTarget)
|
||||
? uploadTarget
|
||||
: UploadTargetType.None,
|
||||
DouyinProxy = new PlatformProxySettingsDto
|
||||
{
|
||||
Enabled = bool.TryParse(GetValue(lookup, DouyinProxyEnabledKey, "false"), out var douyinProxyEnabled) && douyinProxyEnabled,
|
||||
ProxyUrl = GetValue(lookup, DouyinProxyUrlKey, string.Empty)
|
||||
},
|
||||
BilibiliProxy = new PlatformProxySettingsDto
|
||||
{
|
||||
Enabled = bool.TryParse(GetValue(lookup, BilibiliProxyEnabledKey, "false"), out var bilibiliProxyEnabled) && bilibiliProxyEnabled,
|
||||
ProxyUrl = GetValue(lookup, BilibiliProxyUrlKey, string.Empty)
|
||||
},
|
||||
HuyaProxy = new PlatformProxySettingsDto
|
||||
{
|
||||
Enabled = bool.TryParse(GetValue(lookup, HuyaProxyEnabledKey, "false"), out var huyaProxyEnabled) && huyaProxyEnabled,
|
||||
ProxyUrl = GetValue(lookup, HuyaProxyUrlKey, string.Empty)
|
||||
},
|
||||
PlatformRequestSettings = BuildPlatformRequestSettingsMap(lookup),
|
||||
WebDavUpload = new WebDavUploadSettingsDto
|
||||
{
|
||||
Endpoint = GetValue(lookup, WebDavEndpointKey, string.Empty),
|
||||
@@ -274,13 +261,7 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
WebhookBodyTemplate = GetValue(lookup, WebhookBodyTemplateKey, string.Empty),
|
||||
WebhookTimeoutSeconds = GetIntValue(lookup, WebhookTimeoutSecondsKey, 15, 1, 300),
|
||||
NotifyWebhookOnLiveStarted = bool.TryParse(GetValue(lookup, NotifyWebhookOnLiveStartedKey, "true"), out var notifyWebhookOnLiveStarted) && notifyWebhookOnLiveStarted,
|
||||
NotifyWebhookOnException = bool.TryParse(GetValue(lookup, NotifyWebhookOnExceptionKey, "true"), out var notifyWebhookOnException) && notifyWebhookOnException,
|
||||
DouyinUserAgent = GetValue(
|
||||
lookup,
|
||||
DouyinUserAgentKey,
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
|
||||
DouyinReferer = GetValue(lookup, DouyinRefererKey, "https://live.douyin.com/"),
|
||||
DouyinCookie = GetValue(lookup, DouyinCookieKey, string.Empty)
|
||||
NotifyWebhookOnException = bool.TryParse(GetValue(lookup, NotifyWebhookOnExceptionKey, "true"), out var notifyWebhookOnException) && notifyWebhookOnException
|
||||
};
|
||||
}
|
||||
|
||||
@@ -289,9 +270,6 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var douyinProxy = request.DouyinProxy ?? new PlatformProxySettingsDto();
|
||||
var bilibiliProxy = request.BilibiliProxy ?? new PlatformProxySettingsDto();
|
||||
var huyaProxy = request.HuyaProxy ?? new PlatformProxySettingsDto();
|
||||
var webDavUpload = request.WebDavUpload ?? new WebDavUploadSettingsDto();
|
||||
var s3Upload = request.S3Upload ?? new S3UploadSettingsDto();
|
||||
|
||||
@@ -351,12 +329,15 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
await UpsertAsync(S3SecretKeyKey, s3Upload.SecretKey, now, cancellationToken);
|
||||
await UpsertAsync(S3PrefixKey, s3Upload.Prefix.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(S3ForcePathStyleKey, s3Upload.ForcePathStyle.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(DouyinProxyEnabledKey, douyinProxy.Enabled.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(DouyinProxyUrlKey, douyinProxy.ProxyUrl.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(BilibiliProxyEnabledKey, bilibiliProxy.Enabled.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(BilibiliProxyUrlKey, bilibiliProxy.ProxyUrl.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(HuyaProxyEnabledKey, huyaProxy.Enabled.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(HuyaProxyUrlKey, huyaProxy.ProxyUrl.Trim(), now, cancellationToken);
|
||||
foreach (var platformDefinition in LivePlatformCatalog.All)
|
||||
{
|
||||
var platformRequestSettings = request.GetPlatformRequestSettings(platformDefinition.Type);
|
||||
await UpsertAsync(GetPlatformProxyEnabledKey(platformDefinition.Key), platformRequestSettings.Proxy.Enabled.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(GetPlatformProxyUrlKey(platformDefinition.Key), platformRequestSettings.Proxy.ProxyUrl.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(GetPlatformUserAgentKey(platformDefinition.Key), platformRequestSettings.UserAgent.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(GetPlatformRefererKey(platformDefinition.Key), platformRequestSettings.Referer.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(GetPlatformCookieKey(platformDefinition.Key), platformRequestSettings.Cookie.Trim(), now, cancellationToken);
|
||||
}
|
||||
await UpsertAsync(EnableEventScriptsKey, request.EnableEventScripts.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(EnableLiveStartedScriptKey, request.EnableLiveStartedScript.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(LiveStartedScriptModeKey, NormalizeEventScriptMode(request.LiveStartedScriptMode), now, cancellationToken);
|
||||
@@ -398,14 +379,50 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
await UpsertAsync(WebhookTimeoutSecondsKey, Math.Clamp(request.WebhookTimeoutSeconds, 1, 300).ToString(), now, cancellationToken);
|
||||
await UpsertAsync(NotifyWebhookOnLiveStartedKey, request.NotifyWebhookOnLiveStarted.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(NotifyWebhookOnExceptionKey, request.NotifyWebhookOnException.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(DouyinUserAgentKey, request.DouyinUserAgent.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(DouyinRefererKey, request.DouyinReferer.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(DouyinCookieKey, request.DouyinCookie.Trim(), now, cancellationToken);
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
return await GetAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static Dictionary<string, PlatformRequestSettingsDto> BuildPlatformRequestSettingsMap(
|
||||
IReadOnlyDictionary<string, string> lookup)
|
||||
{
|
||||
var result = SystemSettingsDto.CreatePlatformRequestSettingsMap();
|
||||
|
||||
foreach (var platformDefinition in LivePlatformCatalog.All)
|
||||
{
|
||||
var settings = result[platformDefinition.Key];
|
||||
settings.Proxy.Enabled = bool.TryParse(
|
||||
GetPlatformValue(
|
||||
lookup,
|
||||
GetPlatformProxyEnabledKey(platformDefinition.Key),
|
||||
GetLegacyProxyEnabledKey(platformDefinition.Type),
|
||||
"false"),
|
||||
out var proxyEnabled) && proxyEnabled;
|
||||
settings.Proxy.ProxyUrl = GetPlatformValue(
|
||||
lookup,
|
||||
GetPlatformProxyUrlKey(platformDefinition.Key),
|
||||
GetLegacyProxyUrlKey(platformDefinition.Type),
|
||||
string.Empty);
|
||||
settings.UserAgent = GetPlatformValue(
|
||||
lookup,
|
||||
GetPlatformUserAgentKey(platformDefinition.Key),
|
||||
GetLegacyUserAgentKey(platformDefinition.Type),
|
||||
platformDefinition.DefaultUserAgent);
|
||||
settings.Referer = GetPlatformValue(
|
||||
lookup,
|
||||
GetPlatformRefererKey(platformDefinition.Key),
|
||||
GetLegacyRefererKey(platformDefinition.Type),
|
||||
platformDefinition.DefaultReferer);
|
||||
settings.Cookie = GetPlatformValue(
|
||||
lookup,
|
||||
GetPlatformCookieKey(platformDefinition.Key),
|
||||
GetLegacyCookieKey(platformDefinition.Type),
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string GetValue(IReadOnlyDictionary<string, string> lookup, string key, string fallback) =>
|
||||
lookup.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) ? value : fallback;
|
||||
|
||||
@@ -498,6 +515,69 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
.OrderBy(static item => item)
|
||||
.ToArray());
|
||||
|
||||
private static string GetPlatformProxyEnabledKey(string platformKey) =>
|
||||
$"platform_request.{platformKey}.proxy.enabled";
|
||||
|
||||
private static string GetPlatformProxyUrlKey(string platformKey) =>
|
||||
$"platform_request.{platformKey}.proxy.url";
|
||||
|
||||
private static string GetPlatformUserAgentKey(string platformKey) =>
|
||||
$"platform_request.{platformKey}.user_agent";
|
||||
|
||||
private static string GetPlatformRefererKey(string platformKey) =>
|
||||
$"platform_request.{platformKey}.referer";
|
||||
|
||||
private static string GetPlatformCookieKey(string platformKey) =>
|
||||
$"platform_request.{platformKey}.cookie";
|
||||
|
||||
private static string? GetLegacyProxyEnabledKey(LivePlatformType platformType) =>
|
||||
platformType switch
|
||||
{
|
||||
LivePlatformType.Douyin => DouyinProxyEnabledKey,
|
||||
LivePlatformType.Bilibili => BilibiliProxyEnabledKey,
|
||||
LivePlatformType.Huya => HuyaProxyEnabledKey,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string? GetLegacyProxyUrlKey(LivePlatformType platformType) =>
|
||||
platformType switch
|
||||
{
|
||||
LivePlatformType.Douyin => DouyinProxyUrlKey,
|
||||
LivePlatformType.Bilibili => BilibiliProxyUrlKey,
|
||||
LivePlatformType.Huya => HuyaProxyUrlKey,
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static string? GetLegacyUserAgentKey(LivePlatformType platformType) =>
|
||||
platformType == LivePlatformType.Douyin ? DouyinUserAgentKey : null;
|
||||
|
||||
private static string? GetLegacyRefererKey(LivePlatformType platformType) =>
|
||||
platformType == LivePlatformType.Douyin ? DouyinRefererKey : null;
|
||||
|
||||
private static string? GetLegacyCookieKey(LivePlatformType platformType) =>
|
||||
platformType == LivePlatformType.Douyin ? DouyinCookieKey : null;
|
||||
|
||||
private static string GetPlatformValue(
|
||||
IReadOnlyDictionary<string, string> lookup,
|
||||
string key,
|
||||
string? legacyKey,
|
||||
string fallback)
|
||||
{
|
||||
if (lookup.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(legacyKey) &&
|
||||
lookup.TryGetValue(legacyKey, out var legacyValue) &&
|
||||
!string.IsNullOrWhiteSpace(legacyValue))
|
||||
{
|
||||
return legacyValue.Trim();
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private async Task UpsertAsync(string key, string value, DateTimeOffset updatedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _appSettingRepository.GetByKeyAsync(key, cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user