using LiveRecorder.Application.Abstractions.Persistence; using LiveRecorder.Application.Abstractions.Settings; using LiveRecorder.Application.Models.Settings; using LiveRecorder.Domain.Entities; using LiveRecorder.Domain.Enums; namespace LiveRecorder.Application.Services; public sealed class SystemSettingsService : ISystemSettingsService { private const string FfmpegPathKey = "ffmpeg.path"; private const string OutputRootKey = "recording.output_root"; private const string OutputDirectoryTemplateKey = "recording.output_directory_template"; private const string OutputFileNameTemplateKey = "recording.output_file_name_template"; private const string DefaultQualityKey = "recording.default_quality"; private const string DefaultOutputFormatKey = "recording.default_output_format"; private const string SaveModeKey = "recording.save_mode"; private const string RecordingTemplateKey = "recording.template"; private const string SegmentDurationMinutesKey = "recording.segment_duration_minutes"; private const string MaxConcurrentFfmpegTranscodeTasksKey = "recording.max_concurrent_ffmpeg_transcode_tasks"; private const string Mp4FinalizeTimeoutMinutesKey = "recording.mp4_finalize_timeout_minutes"; private const string EnableStorageGuardKey = "storage.guard.enabled"; private const string PauseRecordingWhenFreeSpaceBelowMegabytesKey = "storage.guard.pause_recording_below_mb"; private const string ResumeRecordingWhenFreeSpaceAboveMegabytesKey = "storage.guard.resume_recording_above_mb"; private const string EnableReconnectKey = "recording.enable_auto_reconnect"; private const string ReconnectDelayMaxSecondsKey = "recording.reconnect_delay_max_seconds"; private const string ReadWriteTimeoutMillisecondsKey = "recording.read_write_timeout_milliseconds"; private const string EnableDanmakuRecordingKey = "recording.enable_danmaku_recording"; private const string DanmakuIncludeNonChatEventsKey = "recording.danmaku_include_non_chat_events"; private const string DanmakuMinPollIntervalMillisecondsKey = "recording.danmaku_min_poll_interval_milliseconds"; private const string DanmakuRetryDelayMaxSecondsKey = "recording.danmaku_retry_delay_max_seconds"; private const string EnableBackgroundPollingKey = "scheduler.enable_background_polling"; private const string AutoStartRecordingOnLiveKey = "scheduler.auto_start_recording_on_live"; private const string PollingIntervalSecondsKey = "scheduler.polling_interval_seconds"; private const string UseAliasForStorageKey = "recording.use_alias_for_storage"; private const string EnableFileUploadKey = "upload.enabled"; private const string EnableAutoUploadKey = "upload.auto_upload"; private const string DeleteLocalFilesAfterUploadKey = "upload.delete_local_files_after_upload"; private const string UploadTargetKey = "upload.target"; private const string WebDavEndpointKey = "upload.webdav.endpoint"; private const string WebDavBasePathKey = "upload.webdav.base_path"; private const string WebDavUsernameKey = "upload.webdav.username"; private const string WebDavPasswordKey = "upload.webdav.password"; private const string S3EndpointKey = "upload.s3.endpoint"; private const string S3BucketKey = "upload.s3.bucket"; private const string S3RegionKey = "upload.s3.region"; private const string S3AccessKeyKey = "upload.s3.access_key"; private const string S3SecretKeyKey = "upload.s3.secret_key"; private const string S3PrefixKey = "upload.s3.prefix"; private const string S3ForcePathStyleKey = "upload.s3.force_path_style"; private const string DouyinProxyEnabledKey = "platform_proxy.douyin.enabled"; private const string DouyinProxyUrlKey = "platform_proxy.douyin.url"; private const string BilibiliProxyEnabledKey = "platform_proxy.bilibili.enabled"; private const string BilibiliProxyUrlKey = "platform_proxy.bilibili.url"; private const string HuyaProxyEnabledKey = "platform_proxy.huya.enabled"; private const string HuyaProxyUrlKey = "platform_proxy.huya.url"; private const string EnableEventScriptsKey = "event_scripts.enabled"; private const string EnableLiveStartedScriptKey = "event_scripts.live_started.enabled"; private const string LiveStartedScriptModeKey = "event_scripts.live_started.mode"; private const string LiveStartedScriptPathKey = "event_scripts.live_started.path"; private const string LiveStartedScriptContentKey = "event_scripts.live_started.content"; private const string EnableLiveEndedScriptKey = "event_scripts.live_ended.enabled"; private const string LiveEndedScriptModeKey = "event_scripts.live_ended.mode"; private const string LiveEndedScriptPathKey = "event_scripts.live_ended.path"; private const string LiveEndedScriptContentKey = "event_scripts.live_ended.content"; private const string EnableSegmentCompletedScriptKey = "event_scripts.segment_completed.enabled"; private const string SegmentCompletedScriptModeKey = "event_scripts.segment_completed.mode"; private const string SegmentCompletedScriptPathKey = "event_scripts.segment_completed.path"; private const string SegmentCompletedScriptContentKey = "event_scripts.segment_completed.content"; private const string EventScriptTimeoutSecondsKey = "event_scripts.timeout_seconds"; private const string EnableRetentionCleanupKey = "retention.cleanup.enabled"; private const string RetentionDaysKey = "retention.cleanup.days"; private const string RetentionDeleteFilesKey = "retention.cleanup.delete_files"; private const string EnableEmailNotificationKey = "notification.email.enabled"; private const string EmailSmtpHostKey = "notification.email.smtp_host"; private const string EmailSmtpPortKey = "notification.email.smtp_port"; private const string EmailUseSslKey = "notification.email.use_ssl"; private const string EmailUsernameKey = "notification.email.username"; private const string EmailPasswordKey = "notification.email.password"; private const string EmailFromAddressKey = "notification.email.from_address"; private const string EmailFromDisplayNameKey = "notification.email.from_display_name"; private const string EmailToAddressesKey = "notification.email.to_addresses"; private const string NotifyOnLiveStartedKey = "notification.email.notify_live_started"; private const string NotifyOnExceptionKey = "notification.email.notify_exception"; private const string EmailLiveStartedSubjectTemplateKey = "notification.email.live_started.subject_template"; private const string EmailLiveStartedBodyTemplateHtmlKey = "notification.email.live_started.body_template_html"; private const string EmailExceptionSubjectTemplateKey = "notification.email.exception.subject_template"; private const string EmailExceptionBodyTemplateHtmlKey = "notification.email.exception.body_template_html"; private const string EnableWebhookNotificationKey = "notification.webhook.enabled"; private const string WebhookUrlKey = "notification.webhook.url"; private const string WebhookHeadersKey = "notification.webhook.headers"; private const string WebhookTimeoutSecondsKey = "notification.webhook.timeout_seconds"; private const string NotifyWebhookOnLiveStartedKey = "notification.webhook.notify_live_started"; private const string NotifyWebhookOnExceptionKey = "notification.webhook.notify_exception"; private const string DouyinUserAgentKey = "douyin.user_agent"; private const string DouyinRefererKey = "douyin.referer"; private const string DouyinCookieKey = "douyin.cookie"; private readonly IAppSettingRepository _appSettingRepository; private readonly IUnitOfWork _unitOfWork; public SystemSettingsService(IAppSettingRepository appSettingRepository, IUnitOfWork unitOfWork) { _appSettingRepository = appSettingRepository; _unitOfWork = unitOfWork; } public async Task GetAsync(CancellationToken cancellationToken = default) { var settings = await _appSettingRepository.ListAsync(cancellationToken); var lookup = settings.ToDictionary(static item => item.Key, static item => item.Value, StringComparer.OrdinalIgnoreCase); return new SystemSettingsDto { FfmpegPath = GetValue(lookup, FfmpegPathKey, "ffmpeg"), OutputRoot = GetValue(lookup, OutputRootKey, "records"), OutputDirectoryTemplate = GetValue(lookup, OutputDirectoryTemplateKey, "{platform}/{yyyy}/{MM}/{dd}/{anchor}"), OutputFileNameTemplate = GetValue(lookup, OutputFileNameTemplateKey, "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}"), DefaultQuality = GetValue(lookup, DefaultQualityKey, "origin"), DefaultOutputFormat = Enum.TryParse(GetValue(lookup, DefaultOutputFormatKey, "Mp4"), true, out RecordOutputFormat outputFormat) ? outputFormat : RecordOutputFormat.Mp4, SaveMode = Enum.TryParse(GetValue(lookup, SaveModeKey, "SingleFile"), true, out RecordSaveMode saveMode) ? saveMode : RecordSaveMode.SingleFile, RecordingTemplate = Enum.TryParse(GetValue(lookup, RecordingTemplateKey, "StreamCopy"), true, out RecordingTemplateType recordingTemplate) ? recordingTemplate : RecordingTemplateType.StreamCopy, SegmentDurationMinutes = GetIntValue(lookup, SegmentDurationMinutesKey, 30, 1, 720), MaxConcurrentFfmpegTranscodeTasks = GetIntValue(lookup, MaxConcurrentFfmpegTranscodeTasksKey, 1, 1, 16), Mp4FinalizeTimeoutMinutes = GetIntValue(lookup, Mp4FinalizeTimeoutMinutesKey, 60, 1, 1440), EnableStorageGuard = bool.TryParse(GetValue(lookup, EnableStorageGuardKey, "true"), out var enableStorageGuard) && enableStorageGuard, PauseRecordingWhenFreeSpaceBelowMegabytes = GetIntValue(lookup, PauseRecordingWhenFreeSpaceBelowMegabytesKey, 1024, 0, 1048576), ResumeRecordingWhenFreeSpaceAboveMegabytes = GetIntValue(lookup, ResumeRecordingWhenFreeSpaceAboveMegabytesKey, 4096, 0, 1048576), EnableAutoReconnect = bool.TryParse(GetValue(lookup, EnableReconnectKey, "true"), out var enableReconnect) && enableReconnect, ReconnectDelayMaxSeconds = GetIntValue(lookup, ReconnectDelayMaxSecondsKey, 5, 1, 300), ReadWriteTimeoutMilliseconds = GetIntValue(lookup, ReadWriteTimeoutMillisecondsKey, 15000000, 1000, 60000000), EnableDanmakuRecording = bool.TryParse(GetValue(lookup, EnableDanmakuRecordingKey, "true"), out var enableDanmakuRecording) && enableDanmakuRecording, DanmakuIncludeNonChatEvents = bool.TryParse(GetValue(lookup, DanmakuIncludeNonChatEventsKey, "true"), out var danmakuIncludeNonChatEvents) && danmakuIncludeNonChatEvents, DanmakuMinPollIntervalMilliseconds = GetIntValue(lookup, DanmakuMinPollIntervalMillisecondsKey, 1000, 100, 60000), DanmakuRetryDelayMaxSeconds = GetIntValue(lookup, DanmakuRetryDelayMaxSecondsKey, 15, 1, 300), EnableBackgroundPolling = bool.TryParse(GetValue(lookup, EnableBackgroundPollingKey, "true"), out var enableBackgroundPolling) && enableBackgroundPolling, AutoStartRecordingOnLive = bool.TryParse(GetValue(lookup, AutoStartRecordingOnLiveKey, "true"), out var autoStartRecordingOnLive) && autoStartRecordingOnLive, PollingIntervalSeconds = GetIntValue(lookup, PollingIntervalSecondsKey, 60, 10, 3600), UseAliasForStorage = bool.TryParse(GetValue(lookup, UseAliasForStorageKey, "false"), out var useAliasForStorage) && useAliasForStorage, EnableFileUpload = bool.TryParse(GetValue(lookup, EnableFileUploadKey, "false"), out var enableFileUpload) && enableFileUpload, EnableAutoUpload = bool.TryParse(GetValue(lookup, EnableAutoUploadKey, "false"), out var enableAutoUpload) && enableAutoUpload, DeleteLocalFilesAfterUpload = bool.TryParse(GetValue(lookup, DeleteLocalFilesAfterUploadKey, "false"), out var deleteLocalFilesAfterUpload) && deleteLocalFilesAfterUpload, 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) }, WebDavUpload = new WebDavUploadSettingsDto { Endpoint = GetValue(lookup, WebDavEndpointKey, string.Empty), BasePath = GetValue(lookup, WebDavBasePathKey, string.Empty), Username = GetValue(lookup, WebDavUsernameKey, string.Empty), Password = GetValue(lookup, WebDavPasswordKey, string.Empty) }, S3Upload = new S3UploadSettingsDto { Endpoint = GetValue(lookup, S3EndpointKey, string.Empty), Bucket = GetValue(lookup, S3BucketKey, string.Empty), Region = GetValue(lookup, S3RegionKey, string.Empty), AccessKey = GetValue(lookup, S3AccessKeyKey, string.Empty), SecretKey = GetValue(lookup, S3SecretKeyKey, string.Empty), Prefix = GetValue(lookup, S3PrefixKey, string.Empty), ForcePathStyle = bool.TryParse(GetValue(lookup, S3ForcePathStyleKey, "false"), out var s3ForcePathStyle) && s3ForcePathStyle }, EnableEventScripts = bool.TryParse(GetValue(lookup, EnableEventScriptsKey, "false"), out var enableEventScripts) && enableEventScripts, EnableLiveStartedScript = GetEventScriptEnabled( lookup, EnableLiveStartedScriptKey, enableEventScripts, LiveStartedScriptPathKey, LiveStartedScriptContentKey), LiveStartedScriptMode = GetEventScriptMode(lookup, LiveStartedScriptModeKey, LiveStartedScriptPathKey, LiveStartedScriptContentKey), LiveStartedScriptPath = GetValue(lookup, LiveStartedScriptPathKey, string.Empty), LiveStartedScriptContent = GetValue(lookup, LiveStartedScriptContentKey, string.Empty), EnableLiveEndedScript = GetEventScriptEnabled( lookup, EnableLiveEndedScriptKey, enableEventScripts, LiveEndedScriptPathKey, LiveEndedScriptContentKey), LiveEndedScriptMode = GetEventScriptMode(lookup, LiveEndedScriptModeKey, LiveEndedScriptPathKey, LiveEndedScriptContentKey), LiveEndedScriptPath = GetValue(lookup, LiveEndedScriptPathKey, string.Empty), LiveEndedScriptContent = GetValue(lookup, LiveEndedScriptContentKey, string.Empty), EnableSegmentCompletedScript = GetEventScriptEnabled( lookup, EnableSegmentCompletedScriptKey, enableEventScripts, SegmentCompletedScriptPathKey, SegmentCompletedScriptContentKey), SegmentCompletedScriptMode = GetEventScriptMode(lookup, SegmentCompletedScriptModeKey, SegmentCompletedScriptPathKey, SegmentCompletedScriptContentKey), SegmentCompletedScriptPath = GetValue(lookup, SegmentCompletedScriptPathKey, string.Empty), SegmentCompletedScriptContent = GetValue(lookup, SegmentCompletedScriptContentKey, string.Empty), EventScriptTimeoutSeconds = GetIntValue(lookup, EventScriptTimeoutSecondsKey, 60, 1, 3600), EnableRetentionCleanup = bool.TryParse(GetValue(lookup, EnableRetentionCleanupKey, "false"), out var enableRetentionCleanup) && enableRetentionCleanup, RetentionDays = GetIntValue(lookup, RetentionDaysKey, 30, 1, 3650), RetentionDeleteFiles = bool.TryParse(GetValue(lookup, RetentionDeleteFilesKey, "false"), out var retentionDeleteFiles) && retentionDeleteFiles, EnableEmailNotification = bool.TryParse(GetValue(lookup, EnableEmailNotificationKey, "false"), out var enableEmailNotification) && enableEmailNotification, EmailSmtpHost = GetValue(lookup, EmailSmtpHostKey, string.Empty), EmailSmtpPort = GetIntValue(lookup, EmailSmtpPortKey, 587, 1, 65535), EmailUseSsl = bool.TryParse(GetValue(lookup, EmailUseSslKey, "true"), out var emailUseSsl) && emailUseSsl, EmailUsername = GetValue(lookup, EmailUsernameKey, string.Empty), EmailPassword = GetValue(lookup, EmailPasswordKey, string.Empty), EmailFromAddress = GetValue(lookup, EmailFromAddressKey, string.Empty), EmailFromDisplayName = GetValue(lookup, EmailFromDisplayNameKey, "Live Recorder"), EmailToAddresses = GetValue(lookup, EmailToAddressesKey, string.Empty), NotifyOnLiveStarted = bool.TryParse(GetValue(lookup, NotifyOnLiveStartedKey, "true"), out var notifyOnLiveStarted) && notifyOnLiveStarted, NotifyOnException = bool.TryParse(GetValue(lookup, NotifyOnExceptionKey, "true"), out var notifyOnException) && notifyOnException, EmailLiveStartedSubjectTemplate = GetValue(lookup, EmailLiveStartedSubjectTemplateKey, "[{{appName}}] Live started: {{anchor}} {{title}} ({{roomId}})"), EmailLiveStartedBodyTemplateHtml = GetValue( lookup, EmailLiveStartedBodyTemplateHtmlKey, """

Live started

The monitored live room is now online.

Source URL: {{sourceUrl}}

"""), EmailExceptionSubjectTemplate = GetValue(lookup, EmailExceptionSubjectTemplateKey, "[{{appName}}] Exception: {{source}}"), EmailExceptionBodyTemplateHtml = GetValue( lookup, EmailExceptionBodyTemplateHtmlKey, """

Exception detected

{{summary}}

{{detail}}
"""), EnableWebhookNotification = bool.TryParse(GetValue(lookup, EnableWebhookNotificationKey, "false"), out var enableWebhookNotification) && enableWebhookNotification, WebhookUrl = GetValue(lookup, WebhookUrlKey, string.Empty), WebhookHeaders = GetValue(lookup, WebhookHeadersKey, 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) }; } public async Task UpdateAsync(UpdateSystemSettingsRequest request, CancellationToken cancellationToken = default) { 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(); await UpsertAsync(FfmpegPathKey, request.FfmpegPath.Trim(), now, cancellationToken); await UpsertAsync(OutputRootKey, request.OutputRoot.Trim(), now, cancellationToken); await UpsertAsync(OutputDirectoryTemplateKey, request.OutputDirectoryTemplate.Trim(), now, cancellationToken); await UpsertAsync(OutputFileNameTemplateKey, request.OutputFileNameTemplate.Trim(), now, cancellationToken); await UpsertAsync(DefaultQualityKey, request.DefaultQuality.Trim(), now, cancellationToken); await UpsertAsync(DefaultOutputFormatKey, request.DefaultOutputFormat.ToString(), now, cancellationToken); await UpsertAsync(SaveModeKey, request.SaveMode.ToString(), now, cancellationToken); await UpsertAsync(RecordingTemplateKey, request.RecordingTemplate.ToString(), now, cancellationToken); await UpsertAsync(SegmentDurationMinutesKey, request.SegmentDurationMinutes.ToString(), now, cancellationToken); await UpsertAsync( MaxConcurrentFfmpegTranscodeTasksKey, Math.Clamp(request.MaxConcurrentFfmpegTranscodeTasks, 1, 16).ToString(), now, cancellationToken); await UpsertAsync( Mp4FinalizeTimeoutMinutesKey, Math.Clamp(request.Mp4FinalizeTimeoutMinutes, 1, 1440).ToString(), now, cancellationToken); await UpsertAsync(EnableStorageGuardKey, request.EnableStorageGuard.ToString(), now, cancellationToken); await UpsertAsync( PauseRecordingWhenFreeSpaceBelowMegabytesKey, Math.Clamp(request.PauseRecordingWhenFreeSpaceBelowMegabytes, 0, 1048576).ToString(), now, cancellationToken); await UpsertAsync( ResumeRecordingWhenFreeSpaceAboveMegabytesKey, Math.Clamp(request.ResumeRecordingWhenFreeSpaceAboveMegabytes, 0, 1048576).ToString(), now, cancellationToken); await UpsertAsync(EnableReconnectKey, request.EnableAutoReconnect.ToString(), now, cancellationToken); await UpsertAsync(ReconnectDelayMaxSecondsKey, request.ReconnectDelayMaxSeconds.ToString(), now, cancellationToken); await UpsertAsync(ReadWriteTimeoutMillisecondsKey, request.ReadWriteTimeoutMilliseconds.ToString(), now, cancellationToken); await UpsertAsync(EnableDanmakuRecordingKey, request.EnableDanmakuRecording.ToString(), now, cancellationToken); await UpsertAsync(DanmakuIncludeNonChatEventsKey, request.DanmakuIncludeNonChatEvents.ToString(), now, cancellationToken); await UpsertAsync(DanmakuMinPollIntervalMillisecondsKey, request.DanmakuMinPollIntervalMilliseconds.ToString(), now, cancellationToken); await UpsertAsync(DanmakuRetryDelayMaxSecondsKey, request.DanmakuRetryDelayMaxSeconds.ToString(), now, cancellationToken); await UpsertAsync(EnableBackgroundPollingKey, request.EnableBackgroundPolling.ToString(), now, cancellationToken); await UpsertAsync(AutoStartRecordingOnLiveKey, request.AutoStartRecordingOnLive.ToString(), now, cancellationToken); await UpsertAsync(PollingIntervalSecondsKey, request.PollingIntervalSeconds.ToString(), now, cancellationToken); await UpsertAsync(UseAliasForStorageKey, request.UseAliasForStorage.ToString(), now, cancellationToken); await UpsertAsync(EnableFileUploadKey, request.EnableFileUpload.ToString(), now, cancellationToken); await UpsertAsync(EnableAutoUploadKey, request.EnableAutoUpload.ToString(), now, cancellationToken); await UpsertAsync(DeleteLocalFilesAfterUploadKey, request.DeleteLocalFilesAfterUpload.ToString(), now, cancellationToken); await UpsertAsync(UploadTargetKey, request.UploadTarget.ToString(), now, cancellationToken); await UpsertAsync(WebDavEndpointKey, webDavUpload.Endpoint.Trim(), now, cancellationToken); await UpsertAsync(WebDavBasePathKey, webDavUpload.BasePath.Trim(), now, cancellationToken); await UpsertAsync(WebDavUsernameKey, webDavUpload.Username.Trim(), now, cancellationToken); await UpsertAsync(WebDavPasswordKey, webDavUpload.Password, now, cancellationToken); await UpsertAsync(S3EndpointKey, s3Upload.Endpoint.Trim(), now, cancellationToken); await UpsertAsync(S3BucketKey, s3Upload.Bucket.Trim(), now, cancellationToken); await UpsertAsync(S3RegionKey, s3Upload.Region.Trim(), now, cancellationToken); await UpsertAsync(S3AccessKeyKey, s3Upload.AccessKey.Trim(), now, cancellationToken); 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); await UpsertAsync(EnableEventScriptsKey, request.EnableEventScripts.ToString(), now, cancellationToken); await UpsertAsync(EnableLiveStartedScriptKey, request.EnableLiveStartedScript.ToString(), now, cancellationToken); await UpsertAsync(LiveStartedScriptModeKey, NormalizeEventScriptMode(request.LiveStartedScriptMode), now, cancellationToken); await UpsertAsync(LiveStartedScriptPathKey, request.LiveStartedScriptPath.Trim(), now, cancellationToken); await UpsertAsync(LiveStartedScriptContentKey, request.LiveStartedScriptContent, now, cancellationToken); await UpsertAsync(EnableLiveEndedScriptKey, request.EnableLiveEndedScript.ToString(), now, cancellationToken); await UpsertAsync(LiveEndedScriptModeKey, NormalizeEventScriptMode(request.LiveEndedScriptMode), now, cancellationToken); await UpsertAsync(LiveEndedScriptPathKey, request.LiveEndedScriptPath.Trim(), now, cancellationToken); await UpsertAsync(LiveEndedScriptContentKey, request.LiveEndedScriptContent, now, cancellationToken); await UpsertAsync(EnableSegmentCompletedScriptKey, request.EnableSegmentCompletedScript.ToString(), now, cancellationToken); await UpsertAsync(SegmentCompletedScriptModeKey, NormalizeEventScriptMode(request.SegmentCompletedScriptMode), now, cancellationToken); await UpsertAsync(SegmentCompletedScriptPathKey, request.SegmentCompletedScriptPath.Trim(), now, cancellationToken); await UpsertAsync(SegmentCompletedScriptContentKey, request.SegmentCompletedScriptContent, now, cancellationToken); await UpsertAsync(EventScriptTimeoutSecondsKey, Math.Clamp(request.EventScriptTimeoutSeconds, 1, 3600).ToString(), now, cancellationToken); await UpsertAsync(EnableRetentionCleanupKey, request.EnableRetentionCleanup.ToString(), now, cancellationToken); await UpsertAsync(RetentionDaysKey, Math.Clamp(request.RetentionDays, 1, 3650).ToString(), now, cancellationToken); await UpsertAsync(RetentionDeleteFilesKey, request.RetentionDeleteFiles.ToString(), now, cancellationToken); await UpsertAsync(EnableEmailNotificationKey, request.EnableEmailNotification.ToString(), now, cancellationToken); await UpsertAsync(EmailSmtpHostKey, request.EmailSmtpHost.Trim(), now, cancellationToken); await UpsertAsync(EmailSmtpPortKey, request.EmailSmtpPort.ToString(), now, cancellationToken); await UpsertAsync(EmailUseSslKey, request.EmailUseSsl.ToString(), now, cancellationToken); await UpsertAsync(EmailUsernameKey, request.EmailUsername.Trim(), now, cancellationToken); await UpsertAsync(EmailPasswordKey, request.EmailPassword, now, cancellationToken); await UpsertAsync(EmailFromAddressKey, request.EmailFromAddress.Trim(), now, cancellationToken); await UpsertAsync(EmailFromDisplayNameKey, request.EmailFromDisplayName.Trim(), now, cancellationToken); await UpsertAsync(EmailToAddressesKey, request.EmailToAddresses.Trim(), now, cancellationToken); await UpsertAsync(NotifyOnLiveStartedKey, request.NotifyOnLiveStarted.ToString(), now, cancellationToken); await UpsertAsync(NotifyOnExceptionKey, request.NotifyOnException.ToString(), now, cancellationToken); await UpsertAsync(EmailLiveStartedSubjectTemplateKey, request.EmailLiveStartedSubjectTemplate.Trim(), now, cancellationToken); await UpsertAsync(EmailLiveStartedBodyTemplateHtmlKey, request.EmailLiveStartedBodyTemplateHtml.Trim(), now, cancellationToken); await UpsertAsync(EmailExceptionSubjectTemplateKey, request.EmailExceptionSubjectTemplate.Trim(), now, cancellationToken); await UpsertAsync(EmailExceptionBodyTemplateHtmlKey, request.EmailExceptionBodyTemplateHtml.Trim(), now, cancellationToken); await UpsertAsync(EnableWebhookNotificationKey, request.EnableWebhookNotification.ToString(), now, cancellationToken); await UpsertAsync(WebhookUrlKey, request.WebhookUrl.Trim(), now, cancellationToken); await UpsertAsync(WebhookHeadersKey, request.WebhookHeaders, now, cancellationToken); 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 string GetValue(IReadOnlyDictionary lookup, string key, string fallback) => lookup.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) ? value : fallback; private static string GetEventScriptMode( IReadOnlyDictionary lookup, string modeKey, string pathKey, string contentKey) { if (lookup.TryGetValue(modeKey, out var configuredMode) && !string.IsNullOrWhiteSpace(configuredMode)) { return NormalizeEventScriptMode(configuredMode); } var configuredPath = GetValue(lookup, pathKey, string.Empty); var configuredContent = GetValue(lookup, contentKey, string.Empty); return string.IsNullOrWhiteSpace(configuredContent) || !string.IsNullOrWhiteSpace(configuredPath) ? EventScriptSourceModes.Path : EventScriptSourceModes.Inline; } private static int GetIntValue( IReadOnlyDictionary lookup, string key, int fallback, int minimum, int maximum) { var raw = GetValue(lookup, key, fallback.ToString()); if (!int.TryParse(raw, out var parsedValue)) { return fallback; } return Math.Clamp(parsedValue, minimum, maximum); } private static string NormalizeEventScriptMode(string? value) => string.Equals(value?.Trim(), EventScriptSourceModes.Inline, StringComparison.OrdinalIgnoreCase) ? EventScriptSourceModes.Inline : EventScriptSourceModes.Path; private async Task UpsertAsync(string key, string value, DateTimeOffset updatedAt, CancellationToken cancellationToken) { var existing = await _appSettingRepository.GetByKeyAsync(key, cancellationToken); if (existing is null) { await _appSettingRepository.AddAsync(new AppSetting(key, value, updatedAt), cancellationToken); return; } existing.Update(value, updatedAt); _appSettingRepository.Update(existing); } private static bool GetEventScriptEnabled( IReadOnlyDictionary lookup, string enabledKey, bool legacyGlobalEnabled, string pathKey, string contentKey) { if (lookup.TryGetValue(enabledKey, out var configured) && bool.TryParse(configured, out var enabled)) { return enabled; } return legacyGlobalEnabled && HasAnyConfiguredValue(lookup, pathKey, contentKey); } private static bool HasAnyConfiguredValue( IReadOnlyDictionary lookup, string pathKey, string contentKey) { return !string.IsNullOrWhiteSpace(GetValue(lookup, pathKey, string.Empty)) || !string.IsNullOrWhiteSpace(GetValue(lookup, contentKey, string.Empty)); } }