emby-toolbox/EmbyToolbox/Services/AppSettingsService.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

387 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json;
using System.IO;
using System.Linq;
namespace EmbyToolbox.Services;
public sealed class AppSettingsService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
private readonly string _settingsFilePath;
public AppSettingsService()
{
var appDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"EmbyToolbox");
_settingsFilePath = Path.Combine(appDataDir, "settings.json");
}
/// <summary>Нормализация списка профилей (в т.ч. после загрузки .conv_setup).</summary>
public static List<ConversionProfileSettingsEntry> NormalizeStoredConversionProfiles(
List<ConversionProfileSettingsEntry>? profiles) =>
NormalizeProfiles(profiles);
public AppSettings Load()
{
try
{
if (File.Exists(_settingsFilePath))
{
var json = File.ReadAllText(_settingsFilePath);
var loaded = JsonSerializer.Deserialize<AppSettings>(json, JsonOptions);
if (loaded is not null)
{
loaded.ProcessingTempDirectory = NormalizeTempDirectory(loaded.ProcessingTempDirectory);
loaded.MinimumFileLogLevel = NormalizeLogLevel(loaded.MinimumFileLogLevel);
loaded.HardwareAcceleration = NormalizeHardwareAcceleration(loaded.HardwareAcceleration);
loaded.ConversionProfiles = NormalizeProfiles(loaded.ConversionProfiles);
EnsureDirectoryExists(loaded.ProcessingTempDirectory);
return loaded;
}
}
}
catch
{
// If settings are corrupted or unreadable, fall back to defaults.
}
var defaults = CreateDefaults();
EnsureDirectoryExists(defaults.ProcessingTempDirectory);
return defaults;
}
public void Save(AppSettings settings)
{
var sanitized = new AppSettings
{
ProcessingTempDirectory = NormalizeTempDirectory(settings.ProcessingTempDirectory),
MinimumFileLogLevel = NormalizeLogLevel(settings.MinimumFileLogLevel),
HardwareAcceleration = NormalizeHardwareAcceleration(settings.HardwareAcceleration),
ConversionProfiles = NormalizeProfiles(settings.ConversionProfiles),
IsLogCollapsed = settings.IsLogCollapsed,
NotifyCompletionSoundAfterQueue = settings.NotifyCompletionSoundAfterQueue,
NotifyWindowsToastAfterQueue = settings.NotifyWindowsToastAfterQueue,
LastSeriesRenamerFolder = settings.LastSeriesRenamerFolder,
LastConversionFilesFolder = settings.LastConversionFilesFolder,
LastConversionFolder = settings.LastConversionFolder,
LastMergeFolder = settings.LastMergeFolder,
LastVideoInfoFolder = settings.LastVideoInfoFolder,
LastTempFolder = settings.LastTempFolder,
LastOutputFolder = settings.LastOutputFolder,
LastTrackExtractDestinationFolder = settings.LastTrackExtractDestinationFolder,
LastCommonFolder = settings.LastCommonFolder,
CopyPreviousTrackSettings = settings.CopyPreviousTrackSettings,
DisableSubtitleDefault = settings.DisableSubtitleDefault
};
var settingsDir = Path.GetDirectoryName(_settingsFilePath)!;
Directory.CreateDirectory(settingsDir);
EnsureDirectoryExists(sanitized.ProcessingTempDirectory);
var json = JsonSerializer.Serialize(sanitized, JsonOptions);
File.WriteAllText(_settingsFilePath, json);
}
private static AppSettings CreateDefaults()
{
return new AppSettings
{
ProcessingTempDirectory = NormalizeTempDirectory(null),
MinimumFileLogLevel = LogLevel.Info.ToString(),
HardwareAcceleration = HardwareAccelerationMode.Auto,
IsLogCollapsed = true,
CopyPreviousTrackSettings = false,
DisableSubtitleDefault = false,
NotifyCompletionSoundAfterQueue = true,
NotifyWindowsToastAfterQueue = true,
ConversionProfiles = CreateDefaultProfiles()
};
}
private static List<ConversionProfileSettingsEntry> NormalizeProfiles(List<ConversionProfileSettingsEntry>? profiles)
{
var defaults = CreateDefaultProfiles();
var defaultByName = defaults.ToDictionary(p => p.Profile, StringComparer.OrdinalIgnoreCase);
var mergedBuiltIn = new Dictionary<string, ConversionProfileSettingsEntry>(StringComparer.OrdinalIgnoreCase);
foreach (var name in new[] { "Emby", "Web", "Archive" })
{
mergedBuiltIn[name] = CloneEntry(defaultByName[name]);
}
var customByName = new Dictionary<string, ConversionProfileSettingsEntry>(StringComparer.OrdinalIgnoreCase);
if (profiles is not null)
{
foreach (var p in profiles)
{
if (string.IsNullOrWhiteSpace(p.Profile))
{
continue;
}
var name = p.Profile.Trim();
if (IsBuiltInProfileName(name))
{
var def = defaultByName[name];
mergedBuiltIn[name] = MergeEntry(def, p);
}
else
{
customByName[name] = SanitizeCustomEntry(p);
}
}
}
var result = new List<ConversionProfileSettingsEntry>();
foreach (var name in new[] { "Emby", "Web", "Archive" })
{
result.Add(mergedBuiltIn[name]);
}
result.AddRange(customByName.Values.OrderBy(p => p.Profile, StringComparer.CurrentCultureIgnoreCase));
return result;
}
private static bool IsBuiltInProfileName(string name)
{
return name.Equals("Emby", StringComparison.OrdinalIgnoreCase)
|| name.Equals("Web", StringComparison.OrdinalIgnoreCase)
|| name.Equals("Archive", StringComparison.OrdinalIgnoreCase);
}
private static ConversionProfileSettingsEntry CloneEntry(ConversionProfileSettingsEntry source)
{
return new ConversionProfileSettingsEntry
{
Profile = source.Profile,
Container = source.Container,
Video = source.Video,
PixelFormat = source.PixelFormat,
Resolution = source.Resolution,
Fps = source.Fps,
Audio = source.Audio,
Bitrate = source.Bitrate,
VideoBitrateMode = source.VideoBitrateMode,
VideoBitrateMbps = source.VideoBitrateMbps,
Subtitles = source.Subtitles,
ExternalTracks = source.ExternalTracks,
ExternalSubtitles = source.ExternalSubtitles,
Fonts = source.Fonts
};
}
private static ConversionProfileSettingsEntry MergeEntry(ConversionProfileSettingsEntry def, ConversionProfileSettingsEntry fromFile)
{
return new ConversionProfileSettingsEntry
{
Profile = def.Profile,
Container = CoalesceField(fromFile.Container, def.Container),
Video = CoalesceField(fromFile.Video, def.Video),
PixelFormat = CoalesceField(fromFile.PixelFormat, def.PixelFormat),
Resolution = CoalesceField(fromFile.Resolution, def.Resolution),
Fps = CoalesceField(fromFile.Fps, def.Fps),
Audio = CoalesceField(fromFile.Audio, def.Audio),
Bitrate = CoalesceField(fromFile.Bitrate, def.Bitrate),
VideoBitrateMode = CoalesceField(fromFile.VideoBitrateMode, def.VideoBitrateMode),
VideoBitrateMbps = fromFile.VideoBitrateMbps > 0 ? fromFile.VideoBitrateMbps : def.VideoBitrateMbps,
Subtitles = CoalesceField(fromFile.Subtitles, def.Subtitles),
ExternalTracks = CoalesceField(fromFile.ExternalTracks, def.ExternalTracks),
ExternalSubtitles = CoalesceField(fromFile.ExternalSubtitles, def.ExternalSubtitles),
Fonts = CoalesceField(fromFile.Fonts, def.Fonts)
};
}
private static string CoalesceField(string? value, string fallback)
{
return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
}
private static ConversionProfileSettingsEntry SanitizeCustomEntry(ConversionProfileSettingsEntry p)
{
return new ConversionProfileSettingsEntry
{
Profile = p.Profile.Trim(),
Container = p.Container?.Trim() ?? "MKV",
Video = p.Video?.Trim() ?? "H.264",
PixelFormat = p.PixelFormat?.Trim() ?? "yuv420p",
Resolution = p.Resolution?.Trim() ?? "Без изменений",
Fps = p.Fps?.Trim() ?? "Без изменений",
Audio = p.Audio?.Trim() ?? "AAC",
Bitrate = p.Bitrate?.Trim() ?? "256 kbps",
VideoBitrateMode = string.IsNullOrWhiteSpace(p.VideoBitrateMode) ? VideoBitratePolicy.Auto : p.VideoBitrateMode.Trim(),
VideoBitrateMbps = p.VideoBitrateMbps > 0 ? p.VideoBitrateMbps : null,
Subtitles = p.Subtitles?.Trim() ?? "Да",
ExternalTracks = p.ExternalTracks?.Trim() ?? "Да",
ExternalSubtitles = p.ExternalSubtitles?.Trim() ?? "Да",
Fonts = p.Fonts?.Trim() ?? "Нет"
};
}
private static List<ConversionProfileSettingsEntry> CreateDefaultProfiles()
{
return
[
new()
{
Profile = "Emby",
Container = "MKV",
Video = "H.264",
PixelFormat = "yuv420p",
Resolution = "Без изменений",
Fps = "Без изменений",
Audio = "AAC",
Bitrate = "256 kbps",
VideoBitrateMode = VideoBitratePolicy.Auto,
Subtitles = "Да",
ExternalTracks = "Да",
ExternalSubtitles = "Да",
Fonts = "Да"
},
new()
{
Profile = "Web",
Container = "MP4",
Video = "H.264",
PixelFormat = "yuv420p",
Resolution = "Без изменений",
Fps = "Без изменений",
Audio = "AAC",
Bitrate = "256 kbps",
VideoBitrateMode = "8 Mbps",
Subtitles = "Да",
ExternalTracks = "Да",
ExternalSubtitles = "Да",
Fonts = "Нет"
},
new()
{
Profile = "Archive",
Container = "MP4",
Video = "H.264",
PixelFormat = "yuv420p",
Resolution = "Максимум 1080p",
Fps = "Максимум 30",
Audio = "AAC",
Bitrate = "256 kbps",
VideoBitrateMode = "4 Mbps",
Subtitles = "Да",
ExternalTracks = "Да",
ExternalSubtitles = "Да",
Fonts = "Нет"
}
];
}
private static string NormalizeTempDirectory(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return Path.Combine(Path.GetTempPath(), "EmbyToolbox");
}
return value.Trim();
}
private static void EnsureDirectoryExists(string path)
{
if (!string.IsNullOrWhiteSpace(path))
{
Directory.CreateDirectory(path);
}
}
private static string NormalizeLogLevel(string? value)
{
if (Enum.TryParse<LogLevel>(value, ignoreCase: true, out var level))
{
return level.ToString();
}
return LogLevel.Info.ToString();
}
private static string NormalizeHardwareAcceleration(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return HardwareAccelerationMode.Auto;
}
var normalized = value.Trim();
return normalized switch
{
HardwareAccelerationMode.Auto => HardwareAccelerationMode.Auto,
HardwareAccelerationMode.Nvenc => HardwareAccelerationMode.Nvenc,
HardwareAccelerationMode.Qsv => HardwareAccelerationMode.Qsv,
HardwareAccelerationMode.Amf => HardwareAccelerationMode.Amf,
HardwareAccelerationMode.Cpu => HardwareAccelerationMode.Cpu,
_ => HardwareAccelerationMode.Auto
};
}
}
public sealed record AppSettings
{
public string ProcessingTempDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "EmbyToolbox");
public string MinimumFileLogLevel { get; set; } = LogLevel.Info.ToString();
public string HardwareAcceleration { get; set; } = HardwareAccelerationMode.Auto;
public bool IsLogCollapsed { get; set; } = true;
public List<ConversionProfileSettingsEntry> ConversionProfiles { get; set; } = [];
public string? LastSeriesRenamerFolder { get; set; }
public string? LastConversionFilesFolder { get; set; }
public string? LastConversionFolder { get; set; }
public string? LastMergeFolder { get; set; }
public string? LastVideoInfoFolder { get; set; }
public string? LastTempFolder { get; set; }
public string? LastOutputFolder { get; set; }
public string? LastTrackExtractDestinationFolder { get; set; }
public string? LastCommonFolder { get; set; }
/// <summary>Конвертация: применять сохранённый snapshot дорожек с предыдущего настроенного файла.</summary>
public bool CopyPreviousTrackSettings { get; set; }
/// <summary>Конвертация: выключать default у всех subtitle-дорожек.</summary>
public bool DisableSubtitleDefault { get; set; }
/// <summary>После завершения очереди конвертации воспроизводить системный звук Windows.</summary>
public bool NotifyCompletionSoundAfterQueue { get; set; } = true;
/// <summary>После завершения очереди конвертации показывать уведомление Windows.</summary>
public bool NotifyWindowsToastAfterQueue { get; set; } = true;
}
public static class HardwareAccelerationMode
{
public const string Auto = "Auto";
public const string Nvenc = "NVENC";
public const string Qsv = "QSV";
public const string Amf = "AMF";
public const string Cpu = "CPU";
}
public sealed record ConversionProfileSettingsEntry
{
public string Profile { get; set; } = string.Empty;
public string Container { get; set; } = "MKV";
public string Video { get; set; } = "H.264";
public string PixelFormat { get; set; } = "yuv420p";
public string Resolution { get; set; } = "Без изменений";
public string Fps { get; set; } = "Без изменений";
public string Audio { get; set; } = "AAC";
public string Bitrate { get; set; } = "256 kbps";
public string VideoBitrateMode { get; set; } = VideoBitratePolicy.Auto;
public double? VideoBitrateMbps { get; set; }
public string Subtitles { get; set; } = "Да";
public string ExternalTracks { get; set; } = "Да";
public string ExternalSubtitles { get; set; } = "Да";
public string Fonts { get; set; } = "Нет";
}