Refactor downloader and file manager for improved rclone integration and add healthcheck and smoke test options

- Renamed download flags in ContentDownloader for clarity.
- Enhanced FileManager with methods to build upload paths and verify existing files for rclone uploads.
- Updated StreamProcessor to return success status for stream processing.
- Added rclone smoke test and healthcheck functions to validate configuration and tool availability.
- Improved environment variable handling with a utility function.
- Updated TwitchArchive to incorporate new rclone verification and processing logic.
- Added unit tests for new functionality and refactored existing tests for clarity and coverage.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
MaddoScientisto 2026-04-25 11:54:03 +02:00
commit f97e0200d6
23 changed files with 1013 additions and 289 deletions

View file

@ -17,49 +17,39 @@
</div>
<div class="card">
<label title="Set a quality override for this streamer">Quality</label>
<InputCheckbox Value="overrideQuality" ValueChanged="@( (bool v) => overrideQuality = v )" ValueExpression="() => overrideQuality" title="Override default quality" /> Override
<InputText @bind="model.Quality" disabled="@(!overrideQuality)" placeholder="@(global?.DefaultQuality ?? "")" title="Enter a quality string (e.g., best, 720p)" />
<label title="Quality for this streamer">Quality</label>
<InputText @bind-Value="model.Quality" placeholder="@(global?.DefaultQuality ?? "")" title="Enter a quality string (e.g., best, 720p)" />
</div>
<div>
<label title="Toggle cloud upload for this streamer">Upload to Cloud</label>
<InputCheckbox Value="overrideUpload" ValueChanged="@( (bool v) => overrideUpload = v )" ValueExpression="() => overrideUpload" title="Override upload-to-cloud setting" /> Override
<InputCheckbox Value="uploadToCloudVal" ValueChanged="@( (bool v) => uploadToCloudVal = v )" ValueExpression="() => uploadToCloudVal" disabled="@(!overrideUpload)" title="Upload to configured cloud destination" />
<InputCheckbox Value="uploadToCloudVal" ValueChanged="@( (bool v) => uploadToCloudVal = v )" ValueExpression="() => uploadToCloudVal" title="Upload to configured cloud destination" />
</div>
<div class="card">
<label title="Cloud destination (e.g., rclone remote) for this streamer">Upload Destination</label>
<InputText @bind="model.UploadDestination" title="Cloud destination (e.g., rclone remote)" />
<InputText @bind-Value="model.UploadDestination" title="Cloud destination (e.g., rclone remote)" />
</div>
<div class="card">
<label title="Optional: override system Streamlink path for this streamer">Streamlink Path (override)</label>
<InputCheckbox Value="overrideStreamlink" ValueChanged="@( (bool v) => overrideStreamlink = v )" ValueExpression="() => overrideStreamlink" title="Override streamlink path" /> Override
<InputText @bind="model.StreamlinkPath" disabled="@(!overrideStreamlink)" placeholder="@(global?.StreamlinkPath ?? "")" title="Full path to streamlink executable" />
</div>
@* Streamlink path is global-only; not configurable per-streamer *@
<div class="card">
<h4>Per-streamer overrides</h4>
<h4>Per-streamer settings</h4>
<div>
<label>Download VOD</label>
<InputCheckbox Value="overrideDownloadVOD" ValueChanged="@( (bool v) => overrideDownloadVOD = v )" ValueExpression="() => overrideDownloadVOD" /> Override
<InputCheckbox Value="downloadVODVal" ValueChanged="@( (bool v) => downloadVODVal = v )" ValueExpression="() => downloadVODVal" disabled="@(!overrideDownloadVOD)" />
<InputCheckbox Value="downloadVODVal" ValueChanged="@( (bool v) => downloadVODVal = v )" ValueExpression="() => downloadVODVal" />
</div>
<div>
<label>Download CHAT</label>
<InputCheckbox Value="overrideDownloadCHAT" ValueChanged="@( (bool v) => overrideDownloadCHAT = v )" ValueExpression="() => overrideDownloadCHAT" /> Override
<InputCheckbox Value="downloadCHATVal" ValueChanged="@( (bool v) => downloadCHATVal = v )" ValueExpression="() => downloadCHATVal" disabled="@(!overrideDownloadCHAT)" />
<InputCheckbox Value="downloadCHATVal" ValueChanged="@( (bool v) => downloadCHATVal = v )" ValueExpression="() => downloadCHATVal" />
</div>
<div>
<label>Merge Video & Chat</label>
<InputCheckbox Value="overrideMergeVideoChat" ValueChanged="@( (bool v) => overrideMergeVideoChat = v )" ValueExpression="() => overrideMergeVideoChat" /> Override
<InputCheckbox Value="mergeVideoChatVal" ValueChanged="@( (bool v) => mergeVideoChatVal = v )" ValueExpression="() => mergeVideoChatVal" disabled="@(!overrideMergeVideoChat)" />
<InputCheckbox Value="mergeVideoChatVal" ValueChanged="@( (bool v) => mergeVideoChatVal = v )" ValueExpression="() => mergeVideoChatVal" />
</div>
<div>
<label>Merge Chat Layout</label>
<InputCheckbox Value="overrideMergeChatLayout" ValueChanged="@( (bool v) => overrideMergeChatLayout = v )" ValueExpression="() => overrideMergeChatLayout" /> Override
<InputSelect @bind-Value="mergeChatLayoutVal" disabled="@(!overrideMergeChatLayout)">
<InputSelect @bind-Value="mergeChatLayoutVal">
<option value="side-by-side">Side by side</option>
<option value="stacked">Stacked</option>
<option value="overlay">Overlay</option>
@ -67,105 +57,35 @@
</div>
<div>
<label>VOD Timeout (sec)</label>
<InputCheckbox Value="overrideVodTimeout" ValueChanged="@( (bool v) => overrideVodTimeout = v )" ValueExpression="() => overrideVodTimeout" /> Override
<InputNumber @bind-Value="vodTimeoutVal" disabled="@(!overrideVodTimeout)" />
<InputNumber @bind-Value="vodTimeoutVal" />
</div>
<div>
<label>Delete Files</label>
<InputCheckbox Value="overrideDeleteFiles" ValueChanged="@( (bool v) => overrideDeleteFiles = v )" ValueExpression="() => overrideDeleteFiles" /> Override
<InputCheckbox Value="deleteFilesVal" ValueChanged="@( (bool v) => deleteFilesVal = v )" ValueExpression="() => deleteFilesVal" disabled="@(!overrideDeleteFiles)" />
</div>
<div>
<label>HLS Segments (live)</label>
<InputCheckbox Value="overrideHlsSegments" ValueChanged="@( (bool v) => overrideHlsSegments = v )" ValueExpression="() => overrideHlsSegments" /> Override
<InputNumber @bind-Value="hlsSegmentsVal" disabled="@(!overrideHlsSegments)" />
</div>
<div>
<label>FFmpeg HW Accel</label>
<InputCheckbox Value="overrideFfmpegHwaccel" ValueChanged="@( (bool v) => overrideFfmpegHwaccel = v )" ValueExpression="() => overrideFfmpegHwaccel" /> Override
<InputSelect @bind-Value="ffmpegHwaccelVal" disabled="@(!overrideFfmpegHwaccel)">
<option value="auto">Auto</option>
<option value="none">None</option>
<option value="vaapi">VAAPI</option>
<option value="dxva2">DXVA2</option>
<option value="qsv">QSV</option>
<option value="cuda">CUDA</option>
</InputSelect>
</div>
<div>
<label>FFmpeg Threads</label>
<InputCheckbox Value="overrideFfmpegThreads" ValueChanged="@( (bool v) => overrideFfmpegThreads = v )" ValueExpression="() => overrideFfmpegThreads" /> Override
<InputNumber @bind-Value="ffmpegThreadsVal" disabled="@(!overrideFfmpegThreads)" />
</div>
<div>
<label>FFmpeg Audio Bitrate</label>
<InputCheckbox Value="overrideFfmpegAudioBitrate" ValueChanged="@( (bool v) => overrideFfmpegAudioBitrate = v )" ValueExpression="() => overrideFfmpegAudioBitrate" /> Override
<InputText @bind-Value="ffmpegAudioBitrateVal" disabled="@(!overrideFfmpegAudioBitrate)" />
<InputCheckbox Value="deleteFilesVal" ValueChanged="@( (bool v) => deleteFilesVal = v )" ValueExpression="() => deleteFilesVal" />
</div>
<div>
<label>Download Live CHAT</label>
<InputCheckbox Value="overrideDownloadLiveCHAT" ValueChanged="@( (bool v) => overrideDownloadLiveCHAT = v )" ValueExpression="() => overrideDownloadLiveCHAT" /> Override
<InputCheckbox Value="downloadLiveCHATVal" ValueChanged="@( (bool v) => downloadLiveCHATVal = v )" ValueExpression="() => downloadLiveCHATVal" disabled="@(!overrideDownloadLiveCHAT)" />
<InputCheckbox Value="downloadLiveCHATVal" ValueChanged="@( (bool v) => downloadLiveCHATVal = v )" ValueExpression="() => downloadLiveCHATVal" />
</div>
<div>
<label>Upload Pre-Merge Video</label>
<InputCheckbox Value="overrideUploadPreMergeVideo" ValueChanged="@( (bool v) => overrideUploadPreMergeVideo = v )" ValueExpression="() => overrideUploadPreMergeVideo" /> Override
<InputCheckbox Value="uploadPreMergeVideoVal" ValueChanged="@( (bool v) => uploadPreMergeVideoVal = v )" ValueExpression="() => uploadPreMergeVideoVal" disabled="@(!overrideUploadPreMergeVideo)" />
<InputCheckbox Value="uploadPreMergeVideoVal" ValueChanged="@( (bool v) => uploadPreMergeVideoVal = v )" ValueExpression="() => uploadPreMergeVideoVal" />
</div>
<div>
<label>Upload Merged Video</label>
<InputCheckbox Value="overrideUploadMergedVideo" ValueChanged="@( (bool v) => overrideUploadMergedVideo = v )" ValueExpression="() => overrideUploadMergedVideo" /> Override
<InputCheckbox Value="uploadMergedVideoVal" ValueChanged="@( (bool v) => uploadMergedVideoVal = v )" ValueExpression="() => uploadMergedVideoVal" disabled="@(!overrideUploadMergedVideo)" />
<InputCheckbox Value="uploadMergedVideoVal" ValueChanged="@( (bool v) => uploadMergedVideoVal = v )" ValueExpression="() => uploadMergedVideoVal" />
</div>
<div>
<label>Upload Chat Video</label>
<InputCheckbox Value="overrideUploadChatVideo" ValueChanged="@( (bool v) => overrideUploadChatVideo = v )" ValueExpression="() => overrideUploadChatVideo" /> Override
<InputCheckbox Value="uploadChatVideoVal" ValueChanged="@( (bool v) => uploadChatVideoVal = v )" ValueExpression="() => uploadChatVideoVal" disabled="@(!overrideUploadChatVideo)" />
<InputCheckbox Value="uploadChatVideoVal" ValueChanged="@( (bool v) => uploadChatVideoVal = v )" ValueExpression="() => uploadChatVideoVal" />
</div>
<div>
<label>Only Raw</label>
<InputCheckbox Value="overrideOnlyRaw" ValueChanged="@( (bool v) => overrideOnlyRaw = v )" ValueExpression="() => overrideOnlyRaw" /> Override
<InputCheckbox Value="onlyRawVal" ValueChanged="@( (bool v) => onlyRawVal = v )" ValueExpression="() => onlyRawVal" disabled="@(!overrideOnlyRaw)" />
<InputCheckbox Value="onlyRawVal" ValueChanged="@( (bool v) => onlyRawVal = v )" ValueExpression="() => onlyRawVal" />
</div>
<div>
<label>Clean Raw</label>
<InputCheckbox Value="overrideCleanRaw" ValueChanged="@( (bool v) => overrideCleanRaw = v )" ValueExpression="() => overrideCleanRaw" /> Override
<InputCheckbox Value="cleanRawVal" ValueChanged="@( (bool v) => cleanRawVal = v )" ValueExpression="() => cleanRawVal" disabled="@(!overrideCleanRaw)" />
</div>
<div>
<label>HLS Segments (VOD)</label>
<InputCheckbox Value="overrideHlsSegmentsVOD" ValueChanged="@( (bool v) => overrideHlsSegmentsVOD = v )" ValueExpression="() => overrideHlsSegmentsVOD" /> Override
<InputNumber @bind-Value="hlsSegmentsVODVal" disabled="@(!overrideHlsSegmentsVOD)" />
</div>
<div>
<label>Streamlink ttvlol</label>
<InputCheckbox Value="overrideStreamlinkTtvlol" ValueChanged="@( (bool v) => overrideStreamlinkTtvlol = v )" ValueExpression="() => overrideStreamlinkTtvlol" /> Override
<InputCheckbox Value="streamlinkTtvlolVal" ValueChanged="@( (bool v) => streamlinkTtvlolVal = v )" ValueExpression="() => streamlinkTtvlolVal" disabled="@(!overrideStreamlinkTtvlol)" />
</div>
<div>
<label>FFmpeg Audio Codec</label>
<InputCheckbox Value="overrideFfmpegAudioCodec" ValueChanged="@( (bool v) => overrideFfmpegAudioCodec = v )" ValueExpression="() => overrideFfmpegAudioCodec" /> Override
<InputText @bind-Value="ffmpegAudioCodecVal" disabled="@(!overrideFfmpegAudioCodec)" />
</div>
<div>
<label>FFmpeg Audio Sample Rate</label>
<InputCheckbox Value="overrideFfmpegAudioSamplerate" ValueChanged="@( (bool v) => overrideFfmpegAudioSamplerate = v )" ValueExpression="() => overrideFfmpegAudioSamplerate" /> Override
<InputNumber @bind-Value="ffmpegAudioSamplerateVal" disabled="@(!overrideFfmpegAudioSamplerate)" />
</div>
<div>
<label>FFmpeg Error Recovery</label>
<InputCheckbox Value="overrideFfmpegErrorRecovery" ValueChanged="@( (bool v) => overrideFfmpegErrorRecovery = v )" ValueExpression="() => overrideFfmpegErrorRecovery" /> Override
<InputCheckbox Value="ffmpegErrorRecoveryVal" ValueChanged="@( (bool v) => ffmpegErrorRecoveryVal = v )" ValueExpression="() => ffmpegErrorRecoveryVal" disabled="@(!overrideFfmpegErrorRecovery)" />
</div>
<div>
<label>FFmpeg Faststart</label>
<InputCheckbox Value="overrideFfmpegFaststart" ValueChanged="@( (bool v) => overrideFfmpegFaststart = v )" ValueExpression="() => overrideFfmpegFaststart" /> Override
<InputCheckbox Value="ffmpegFaststartVal" ValueChanged="@( (bool v) => ffmpegFaststartVal = v )" ValueExpression="() => ffmpegFaststartVal" disabled="@(!overrideFfmpegFaststart)" />
</div>
<div>
<label>FFmpeg Progress</label>
<InputCheckbox Value="overrideFfmpegProgress" ValueChanged="@( (bool v) => overrideFfmpegProgress = v )" ValueExpression="() => overrideFfmpegProgress" /> Override
<InputCheckbox Value="ffmpegProgressVal" ValueChanged="@( (bool v) => ffmpegProgressVal = v )" ValueExpression="() => ffmpegProgressVal" disabled="@(!overrideFfmpegProgress)" />
<InputCheckbox Value="cleanRawVal" ValueChanged="@( (bool v) => cleanRawVal = v )" ValueExpression="() => cleanRawVal" />
</div>
</div>
@ -193,37 +113,10 @@
@code {
[Parameter]
public string Username { get; set; } = string.Empty;
private TwitchArchive.Core.Config.StreamerConfig model = new();
private TwitchArchive.Core.Config.GlobalConfig? global;
private bool saved = false;
private bool showConfirm = false;
private bool overrideQuality = false;
private bool overrideUpload = false;
private bool overrideStreamlink = false;
private bool overrideDownloadVOD = false;
private bool overrideDownloadCHAT = false;
private bool overrideMergeVideoChat = false;
private bool overrideMergeChatLayout = false;
private bool overrideVodTimeout = false;
private bool overrideDeleteFiles = false;
private bool overrideHlsSegments = false;
private bool overrideFfmpegHwaccel = false;
private bool overrideFfmpegThreads = false;
private bool overrideFfmpegAudioBitrate = false;
private bool overrideDownloadLiveCHAT = false;
private bool overrideUploadPreMergeVideo = false;
private bool overrideUploadMergedVideo = false;
private bool overrideUploadChatVideo = false;
private bool overrideOnlyRaw = false;
private bool overrideCleanRaw = false;
private bool overrideHlsSegmentsVOD = false;
private bool overrideStreamlinkTtvlol = false;
private bool overrideFfmpegAudioCodec = false;
private bool overrideFfmpegAudioSamplerate = false;
private bool overrideFfmpegErrorRecovery = false;
private bool overrideFfmpegFaststart = false;
private bool overrideFfmpegProgress = false;
// local values for nullable per-streamer settings (bind safely)
private bool downloadVODVal;
@ -233,28 +126,18 @@
private string mergeChatLayoutVal = "side-by-side";
private int? vodTimeoutVal;
private bool deleteFilesVal;
private int? hlsSegmentsVal;
private string ffmpegHwaccelVal = "auto";
private int? ffmpegThreadsVal;
private string? ffmpegAudioBitrateVal;
private bool uploadPreMergeVideoVal;
private bool uploadMergedVideoVal;
private bool uploadChatVideoVal;
private bool onlyRawVal;
private bool cleanRawVal;
private int? hlsSegmentsVODVal;
private bool streamlinkTtvlolVal;
private string? ffmpegAudioCodecVal;
private int? ffmpegAudioSamplerateVal;
private bool ffmpegErrorRecoveryVal;
private bool ffmpegFaststartVal;
private bool ffmpegProgressVal;
private bool uploadToCloudVal;
protected override void OnInitialized()
{
global = ConfigService.LoadGlobal();
var s = ConfigService.LoadStreamer(Username);
var isNew = s == null;
if (s != null) model = s;
// initialize local values from model or global defaults
downloadVODVal = model.DownloadVOD ?? global?.Defaults.DownloadVOD ?? true;
@ -264,57 +147,67 @@
mergeChatLayoutVal = model.MergeChatLayout ?? global?.Defaults.MergeChatLayout ?? "side-by-side";
vodTimeoutVal = model.VodTimeout ?? global?.Defaults.VodTimeout ?? 300;
deleteFilesVal = model.DeleteFiles ?? global?.Defaults.DeleteFiles ?? false;
hlsSegmentsVal = model.HlsSegments ?? global?.Defaults.HlsSegments ?? 3;
ffmpegHwaccelVal = model.FfmpegHwaccel ?? global?.Defaults.FfmpegHwaccel ?? "auto";
ffmpegThreadsVal = model.FfmpegThreads ?? global?.Defaults.FfmpegThreads ?? 0;
ffmpegAudioBitrateVal = model.FfmpegAudioBitrate ?? global?.Defaults.FfmpegAudioBitrate ?? "192k";
uploadPreMergeVideoVal = model.UploadPreMergeVideo ?? global?.Defaults.UploadPreMergeVideo ?? true;
uploadMergedVideoVal = model.UploadMergedVideo ?? global?.Defaults.UploadMergedVideo ?? true;
uploadChatVideoVal = model.UploadChatVideo ?? global?.Defaults.UploadChatVideo ?? false;
onlyRawVal = model.OnlyRaw ?? global?.Defaults.OnlyRaw ?? false;
cleanRawVal = model.CleanRaw ?? global?.Defaults.CleanRaw ?? true;
hlsSegmentsVODVal = model.HlsSegmentsVOD ?? global?.Defaults.HlsSegmentsVOD ?? 10;
streamlinkTtvlolVal = model.StreamlinkTtvlol ?? global?.Defaults.StreamlinkTtvlol ?? false;
ffmpegAudioCodecVal = model.FfmpegAudioCodec ?? global?.Defaults.FfmpegAudioCodec ?? "aac";
ffmpegAudioSamplerateVal = model.FfmpegAudioSamplerate ?? global?.Defaults.FfmpegAudioSamplerate ?? 48000;
ffmpegErrorRecoveryVal = model.FfmpegErrorRecovery ?? global?.Defaults.FfmpegErrorRecovery ?? true;
ffmpegFaststartVal = model.FfmpegFaststart ?? global?.Defaults.FfmpegFaststart ?? true;
ffmpegProgressVal = model.FfmpegProgress ?? global?.Defaults.FfmpegProgress ?? false;
uploadToCloudVal = model.UploadToCloud ?? global?.UploadToCloud ?? false;
// when creating a new streamer config, populate model with global defaults so
// the streamer config stores initial values and subsequent edits use streamer values
if (isNew)
{
model.Quality = model.Quality ?? global?.DefaultQuality;
model.UploadToCloud = uploadToCloudVal;
model.UploadDestination = model.UploadDestination ?? global?.UploadDestination;
model.DownloadVOD = downloadVODVal;
model.DownloadCHAT = downloadCHATVal;
model.DownloadLiveCHAT = downloadLiveCHATVal;
model.MergeVideoChat = mergeVideoChatVal;
model.MergeChatLayout = mergeChatLayoutVal;
model.VodTimeout = vodTimeoutVal;
model.DeleteFiles = deleteFilesVal;
model.UploadPreMergeVideo = uploadPreMergeVideoVal;
model.UploadMergedVideo = uploadMergedVideoVal;
model.UploadChatVideo = uploadChatVideoVal;
model.OnlyRaw = onlyRawVal;
model.CleanRaw = cleanRawVal;
}
}
private void Save()
{
model.Username = Username;
if (!overrideQuality) model.Quality = null;
if (string.IsNullOrWhiteSpace(model.Quality)) model.Quality = null;
// Upload to cloud
model.UploadToCloud = overrideUpload ? uploadToCloudVal : (bool?)null;
// Streamlink path
model.StreamlinkPath = overrideStreamlink ? model.StreamlinkPath : null;
// Per-streamer values: map local values when overridden, otherwise clear
model.DownloadVOD = overrideDownloadVOD ? downloadVODVal : (bool?)null;
model.DownloadCHAT = overrideDownloadCHAT ? downloadCHATVal : (bool?)null;
model.DownloadLiveCHAT = overrideDownloadLiveCHAT ? downloadLiveCHATVal : (bool?)null;
model.MergeVideoChat = overrideMergeVideoChat ? mergeVideoChatVal : (bool?)null;
model.MergeChatLayout = overrideMergeChatLayout ? mergeChatLayoutVal : null;
model.VodTimeout = overrideVodTimeout ? vodTimeoutVal : (int?)null;
model.DeleteFiles = overrideDeleteFiles ? deleteFilesVal : (bool?)null;
model.HlsSegments = overrideHlsSegments ? hlsSegmentsVal : (int?)null;
model.FfmpegHwaccel = overrideFfmpegHwaccel ? ffmpegHwaccelVal : null;
model.FfmpegThreads = overrideFfmpegThreads ? ffmpegThreadsVal : (int?)null;
model.FfmpegAudioBitrate = overrideFfmpegAudioBitrate ? ffmpegAudioBitrateVal : null;
model.UploadPreMergeVideo = overrideUploadPreMergeVideo ? uploadPreMergeVideoVal : (bool?)null;
model.UploadMergedVideo = overrideUploadMergedVideo ? uploadMergedVideoVal : (bool?)null;
model.UploadChatVideo = overrideUploadChatVideo ? uploadChatVideoVal : (bool?)null;
model.OnlyRaw = overrideOnlyRaw ? onlyRawVal : (bool?)null;
model.CleanRaw = overrideCleanRaw ? cleanRawVal : (bool?)null;
model.HlsSegmentsVOD = overrideHlsSegmentsVOD ? hlsSegmentsVODVal : (int?)null;
model.StreamlinkTtvlol = overrideStreamlinkTtvlol ? streamlinkTtvlolVal : (bool?)null;
model.FfmpegAudioCodec = overrideFfmpegAudioCodec ? ffmpegAudioCodecVal : null;
model.FfmpegAudioSamplerate = overrideFfmpegAudioSamplerate ? ffmpegAudioSamplerateVal : (int?)null;
model.FfmpegErrorRecovery = overrideFfmpegErrorRecovery ? ffmpegErrorRecoveryVal : (bool?)null;
model.FfmpegFaststart = overrideFfmpegFaststart ? ffmpegFaststartVal : (bool?)null;
model.FfmpegProgress = overrideFfmpegProgress ? ffmpegProgressVal : (bool?)null;
model.UploadToCloud = uploadToCloudVal;
// Ensure global-only settings are not stored per-streamer
model.StreamlinkPath = null;
model.HlsSegments = null;
model.HlsSegmentsVOD = null;
model.StreamlinkTtvlol = null;
model.FfmpegHwaccel = null;
model.FfmpegThreads = null;
model.FfmpegAudioBitrate = null;
model.FfmpegAudioCodec = null;
model.FfmpegAudioSamplerate = null;
model.FfmpegErrorRecovery = null;
model.FfmpegFaststart = null;
model.FfmpegProgress = null;
// Per-streamer values: always map local values into the model
model.DownloadVOD = downloadVODVal;
model.DownloadCHAT = downloadCHATVal;
model.DownloadLiveCHAT = downloadLiveCHATVal;
model.MergeVideoChat = mergeVideoChatVal;
model.MergeChatLayout = mergeChatLayoutVal;
model.VodTimeout = vodTimeoutVal;
model.DeleteFiles = deleteFilesVal;
model.UploadPreMergeVideo = uploadPreMergeVideoVal;
model.UploadMergedVideo = uploadMergedVideoVal;
model.UploadChatVideo = uploadChatVideoVal;
model.OnlyRaw = onlyRawVal;
model.CleanRaw = cleanRawVal;
ConfigService.SaveStreamer(model);
saved = true;
}