Refactor global configuration page and navigation; add media library page; enhance streamer configuration with detailed options
- Removed the global configuration form and redirected to the consolidated settings page. - Updated the dashboard to provide feedback when no streamers are configured and added edit links for each streamer. - Introduced a new media library page to display media files from the configured archive root. - Enhanced the streamer configuration page with additional options for overrides and settings, including a confirmation modal for deletion. - Updated the layout and styles for improved user experience and navigation. - Switched from file-based password storage to database-backed user credentials management in AuthService. - Applied EF migrations on application startup to ensure database schema is up-to-date.
This commit is contained in:
parent
1ecf7501f4
commit
e5e60999bf
24 changed files with 1151 additions and 163 deletions
|
|
@ -14,12 +14,43 @@ namespace TwitchArchive.Core.Config
|
|||
|
||||
public ConfigurationService(string? basePath = null)
|
||||
{
|
||||
_basePath = basePath ?? Path.Combine(AppContext.BaseDirectory, "config");
|
||||
// If a basePath was explicitly provided, use it. Otherwise try to find a
|
||||
// repository-level `config` folder by walking parent directories from the
|
||||
// application base. This ensures the web app uses the same config/ files
|
||||
// as the repository (global.json, config/streamers/*.json) when available.
|
||||
if (!string.IsNullOrWhiteSpace(basePath))
|
||||
{
|
||||
_basePath = basePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = FindExistingConfigFolder();
|
||||
_basePath = found ?? Path.Combine(AppContext.BaseDirectory, "config");
|
||||
}
|
||||
|
||||
_streamersPath = Path.Combine(_basePath, "streamers");
|
||||
Directory.CreateDirectory(_basePath);
|
||||
Directory.CreateDirectory(_streamersPath);
|
||||
}
|
||||
|
||||
private string? FindExistingConfigFolder()
|
||||
{
|
||||
var start = AppContext.BaseDirectory ?? Environment.CurrentDirectory;
|
||||
var dir = new DirectoryInfo(start);
|
||||
for (int i = 0; i < 8 && dir != null; i++)
|
||||
{
|
||||
var candidate = Path.Combine(dir.FullName, "config");
|
||||
if (Directory.Exists(candidate))
|
||||
{
|
||||
// prefer candidate if it contains global.json or streamers
|
||||
if (File.Exists(Path.Combine(candidate, "global.json")) || Directory.Exists(Path.Combine(candidate, "streamers")))
|
||||
return candidate;
|
||||
}
|
||||
dir = dir.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public GlobalConfig LoadGlobal()
|
||||
{
|
||||
var file = Path.Combine(_basePath, "global.json");
|
||||
|
|
|
|||
|
|
@ -14,10 +14,44 @@ namespace TwitchArchive.Core.Config
|
|||
public int RefreshIntervalSeconds { get; init; }
|
||||
public int StreamSegmentThreads { get; init; }
|
||||
public string? DefaultQuality { get; init; }
|
||||
// Defaults that can be overridden per-streamer
|
||||
public DefaultsSection Defaults { get; init; } = new DefaultsSection();
|
||||
|
||||
public static EffectiveConfig Merge(GlobalConfig global, StreamerConfig? streamer)
|
||||
{
|
||||
streamer ??= new StreamerConfig();
|
||||
var d = new TwitchArchive.Core.Config.DefaultsSection();
|
||||
// start with global defaults
|
||||
if (global?.Defaults != null) d = global.Defaults;
|
||||
// apply per-streamer overrides when present
|
||||
var mergedDefaults = new TwitchArchive.Core.Config.DefaultsSection
|
||||
{
|
||||
DownloadVOD = streamer.DownloadVOD ?? d.DownloadVOD,
|
||||
DownloadCHAT = streamer.DownloadCHAT ?? d.DownloadCHAT,
|
||||
DownloadLiveCHAT = streamer.DownloadLiveCHAT ?? d.DownloadLiveCHAT,
|
||||
MergeVideoChat = streamer.MergeVideoChat ?? d.MergeVideoChat,
|
||||
MergeChatLayout = streamer.MergeChatLayout ?? d.MergeChatLayout,
|
||||
VodTimeout = streamer.VodTimeout ?? d.VodTimeout,
|
||||
UploadCloud = streamer.UploadCloud ?? d.UploadCloud,
|
||||
UploadPreMergeVideo = streamer.UploadPreMergeVideo ?? d.UploadPreMergeVideo,
|
||||
UploadMergedVideo = streamer.UploadMergedVideo ?? d.UploadMergedVideo,
|
||||
UploadChatVideo = streamer.UploadChatVideo ?? d.UploadChatVideo,
|
||||
DeleteFiles = streamer.DeleteFiles ?? d.DeleteFiles,
|
||||
OnlyRaw = streamer.OnlyRaw ?? d.OnlyRaw,
|
||||
CleanRaw = streamer.CleanRaw ?? d.CleanRaw,
|
||||
HlsSegments = streamer.HlsSegments ?? d.HlsSegments,
|
||||
HlsSegmentsVOD = streamer.HlsSegmentsVOD ?? d.HlsSegmentsVOD,
|
||||
StreamlinkTtvlol = streamer.StreamlinkTtvlol ?? d.StreamlinkTtvlol,
|
||||
FfmpegHwaccel = streamer.FfmpegHwaccel ?? d.FfmpegHwaccel,
|
||||
FfmpegThreads = streamer.FfmpegThreads ?? d.FfmpegThreads,
|
||||
FfmpegAudioCodec = streamer.FfmpegAudioCodec ?? d.FfmpegAudioCodec,
|
||||
FfmpegAudioSamplerate = streamer.FfmpegAudioSamplerate ?? d.FfmpegAudioSamplerate,
|
||||
FfmpegAudioBitrate = streamer.FfmpegAudioBitrate ?? d.FfmpegAudioBitrate,
|
||||
FfmpegErrorRecovery = streamer.FfmpegErrorRecovery ?? d.FfmpegErrorRecovery,
|
||||
FfmpegFaststart = streamer.FfmpegFaststart ?? d.FfmpegFaststart,
|
||||
FfmpegProgress = streamer.FfmpegProgress ?? d.FfmpegProgress
|
||||
};
|
||||
|
||||
return new EffectiveConfig
|
||||
{
|
||||
ArchiveRoot = streamer.Username != null ? (global.ArchiveRoot ?? string.Empty) : (global.ArchiveRoot ?? string.Empty),
|
||||
|
|
@ -29,7 +63,8 @@ namespace TwitchArchive.Core.Config
|
|||
UploadDestination = streamer.UploadDestination ?? global.UploadDestination,
|
||||
RefreshIntervalSeconds = global.RefreshIntervalSeconds,
|
||||
StreamSegmentThreads = global.StreamSegmentThreads,
|
||||
DefaultQuality = streamer.Quality ?? global.DefaultQuality
|
||||
DefaultQuality = streamer.Quality ?? global.DefaultQuality,
|
||||
Defaults = mergedDefaults
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace TwitchArchive.Core.Config
|
||||
|
|
@ -26,12 +27,98 @@ namespace TwitchArchive.Core.Config
|
|||
public string? UploadDestination { get; set; }
|
||||
|
||||
[JsonPropertyName("refresh_interval_seconds")]
|
||||
[Range(5, 86400, ErrorMessage = "Refresh interval must be between 5 and 86400 seconds.")]
|
||||
public int RefreshIntervalSeconds { get; set; } = 60;
|
||||
|
||||
[JsonPropertyName("stream_segment_threads")]
|
||||
[Range(1, 64, ErrorMessage = "Stream segment threads must be between 1 and 64.")]
|
||||
public int StreamSegmentThreads { get; set; } = 4;
|
||||
|
||||
[JsonPropertyName("default_quality")]
|
||||
public string? DefaultQuality { get; set; } = "best";
|
||||
|
||||
// Defaults section for per-streamer fallbacks
|
||||
[JsonPropertyName("defaults")]
|
||||
public DefaultsSection Defaults { get; set; } = new DefaultsSection();
|
||||
}
|
||||
|
||||
public class DefaultsSection
|
||||
{
|
||||
[JsonPropertyName("downloadVOD")]
|
||||
public bool DownloadVOD { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("downloadCHAT")]
|
||||
public bool DownloadCHAT { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("downloadLiveCHAT")]
|
||||
public bool DownloadLiveCHAT { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("mergeVideoChat")]
|
||||
public bool MergeVideoChat { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("mergeChatLayout")]
|
||||
public string MergeChatLayout { get; set; } = "side-by-side";
|
||||
|
||||
[JsonPropertyName("vodTimeout")]
|
||||
[Range(0, 86400, ErrorMessage = "VOD timeout must be between 0 and 86400 seconds.")]
|
||||
public int VodTimeout { get; set; } = 300;
|
||||
|
||||
[JsonPropertyName("uploadCloud")]
|
||||
public bool UploadCloud { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("uploadPreMergeVideo")]
|
||||
public bool UploadPreMergeVideo { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("uploadMergedVideo")]
|
||||
public bool UploadMergedVideo { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("uploadChatVideo")]
|
||||
public bool UploadChatVideo { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("deleteFiles")]
|
||||
public bool DeleteFiles { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("onlyRaw")]
|
||||
public bool OnlyRaw { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("cleanRaw")]
|
||||
public bool CleanRaw { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("hls_segments")]
|
||||
[Range(1, 50, ErrorMessage = "HLS segments must be between 1 and 50.")]
|
||||
public int HlsSegments { get; set; } = 3;
|
||||
|
||||
[JsonPropertyName("hls_segmentsVOD")]
|
||||
[Range(1, 200, ErrorMessage = "HLS segments (VOD) must be between 1 and 200.")]
|
||||
public int HlsSegmentsVOD { get; set; } = 10;
|
||||
|
||||
[JsonPropertyName("streamlink_ttvlol")]
|
||||
public bool StreamlinkTtvlol { get; set; } = false;
|
||||
|
||||
[JsonPropertyName("ffmpeg_hwaccel")]
|
||||
public string FfmpegHwaccel { get; set; } = "auto";
|
||||
|
||||
[JsonPropertyName("ffmpeg_threads")]
|
||||
[Range(0, 128, ErrorMessage = "FFmpeg threads must be between 0 and 128.")]
|
||||
public int FfmpegThreads { get; set; } = 0;
|
||||
|
||||
[JsonPropertyName("ffmpeg_audio_codec")]
|
||||
public string FfmpegAudioCodec { get; set; } = "aac";
|
||||
|
||||
[JsonPropertyName("ffmpeg_audio_samplerate")]
|
||||
[Range(8000, 192000, ErrorMessage = "Audio sample rate must be between 8000 and 192000.")]
|
||||
public int FfmpegAudioSamplerate { get; set; } = 48000;
|
||||
|
||||
[JsonPropertyName("ffmpeg_audio_bitrate")]
|
||||
public string FfmpegAudioBitrate { get; set; } = "192k";
|
||||
|
||||
[JsonPropertyName("ffmpeg_error_recovery")]
|
||||
public bool FfmpegErrorRecovery { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("ffmpeg_faststart")]
|
||||
public bool FfmpegFaststart { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("ffmpeg_progress")]
|
||||
public bool FfmpegProgress { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace TwitchArchive.Core.Config
|
||||
|
|
@ -5,6 +6,7 @@ namespace TwitchArchive.Core.Config
|
|||
public class StreamerConfig
|
||||
{
|
||||
[JsonPropertyName("username")]
|
||||
[Required]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
|
|
@ -21,5 +23,83 @@ namespace TwitchArchive.Core.Config
|
|||
|
||||
[JsonPropertyName("streamlink_path")]
|
||||
public string? StreamlinkPath { get; set; }
|
||||
|
||||
// Per-streamer override options matching GlobalConfig.Defaults
|
||||
[JsonPropertyName("downloadVOD")]
|
||||
public bool? DownloadVOD { get; set; }
|
||||
|
||||
[JsonPropertyName("downloadCHAT")]
|
||||
public bool? DownloadCHAT { get; set; }
|
||||
|
||||
[JsonPropertyName("downloadLiveCHAT")]
|
||||
public bool? DownloadLiveCHAT { get; set; }
|
||||
|
||||
[JsonPropertyName("mergeVideoChat")]
|
||||
public bool? MergeVideoChat { get; set; }
|
||||
|
||||
[JsonPropertyName("mergeChatLayout")]
|
||||
public string? MergeChatLayout { get; set; }
|
||||
|
||||
[JsonPropertyName("vodTimeout")]
|
||||
[Range(0, 86400, ErrorMessage = "VOD timeout must be between 0 and 86400 seconds.")]
|
||||
public int? VodTimeout { get; set; }
|
||||
|
||||
[JsonPropertyName("uploadCloud")]
|
||||
public bool? UploadCloud { get; set; }
|
||||
|
||||
[JsonPropertyName("uploadPreMergeVideo")]
|
||||
public bool? UploadPreMergeVideo { get; set; }
|
||||
|
||||
[JsonPropertyName("uploadMergedVideo")]
|
||||
public bool? UploadMergedVideo { get; set; }
|
||||
|
||||
[JsonPropertyName("uploadChatVideo")]
|
||||
public bool? UploadChatVideo { get; set; }
|
||||
|
||||
[JsonPropertyName("deleteFiles")]
|
||||
public bool? DeleteFiles { get; set; }
|
||||
|
||||
[JsonPropertyName("onlyRaw")]
|
||||
public bool? OnlyRaw { get; set; }
|
||||
|
||||
[JsonPropertyName("cleanRaw")]
|
||||
public bool? CleanRaw { get; set; }
|
||||
|
||||
[JsonPropertyName("hls_segments")]
|
||||
[Range(1, 50, ErrorMessage = "HLS segments must be between 1 and 50.")]
|
||||
public int? HlsSegments { get; set; }
|
||||
|
||||
[JsonPropertyName("hls_segmentsVOD")]
|
||||
[Range(1, 200, ErrorMessage = "HLS segments (VOD) must be between 1 and 200.")]
|
||||
public int? HlsSegmentsVOD { get; set; }
|
||||
|
||||
[JsonPropertyName("streamlink_ttvlol")]
|
||||
public bool? StreamlinkTtvlol { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_hwaccel")]
|
||||
public string? FfmpegHwaccel { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_threads")]
|
||||
[Range(0, 128, ErrorMessage = "FFmpeg threads must be between 0 and 128.")]
|
||||
public int? FfmpegThreads { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_audio_codec")]
|
||||
public string? FfmpegAudioCodec { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_audio_samplerate")]
|
||||
[Range(8000, 192000, ErrorMessage = "Audio sample rate must be between 8000 and 192000.")]
|
||||
public int? FfmpegAudioSamplerate { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_audio_bitrate")]
|
||||
public string? FfmpegAudioBitrate { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_error_recovery")]
|
||||
public bool? FfmpegErrorRecovery { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_faststart")]
|
||||
public bool? FfmpegFaststart { get; set; }
|
||||
|
||||
[JsonPropertyName("ffmpeg_progress")]
|
||||
public bool? FfmpegProgress { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue