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:
MaddoScientisto 2026-02-22 23:06:40 +01:00
commit e5e60999bf
24 changed files with 1151 additions and 163 deletions

View file

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

View file

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

View file

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

View file

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