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; } = "Нет";
}