391 lines
16 KiB
C#
391 lines
16 KiB
C#
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,
|
||
RemoveForeignTracksByDefault = settings.RemoveForeignTracksByDefault
|
||
};
|
||
|
||
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,
|
||
RemoveForeignTracksByDefault = 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; }
|
||
|
||
public bool RemoveForeignTracksByDefault { 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; } = "Нет";
|
||
}
|