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"); } /// Нормализация списка профилей (в т.ч. после загрузки .conv_setup). public static List NormalizeStoredConversionProfiles( List? profiles) => NormalizeProfiles(profiles); public AppSettings Load() { try { if (File.Exists(_settingsFilePath)) { var json = File.ReadAllText(_settingsFilePath); var loaded = JsonSerializer.Deserialize(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 NormalizeProfiles(List? profiles) { var defaults = CreateDefaultProfiles(); var defaultByName = defaults.ToDictionary(p => p.Profile, StringComparer.OrdinalIgnoreCase); var mergedBuiltIn = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var name in new[] { "Emby", "Web", "Archive" }) { mergedBuiltIn[name] = CloneEntry(defaultByName[name]); } var customByName = new Dictionary(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(); 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 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(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 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; } /// Конвертация: применять сохранённый snapshot дорожек с предыдущего настроенного файла. public bool CopyPreviousTrackSettings { get; set; } /// Конвертация: выключать default у всех subtitle-дорожек. public bool DisableSubtitleDefault { get; set; } /// После завершения очереди конвертации воспроизводить системный звук Windows. public bool NotifyCompletionSoundAfterQueue { get; set; } = true; /// После завершения очереди конвертации показывать уведомление Windows. 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; } = "Нет"; }