using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using TransmissionRssManager.Core; namespace TransmissionRssManager.Services { /// /// Service for managing application configuration /// File-based implementation that does not use a database /// public class ConfigService : IConfigService { private readonly ILogger _logger; private readonly string _configFilePath; private AppConfig _cachedConfig; private readonly object _lockObject = new object(); public ConfigService(ILogger logger) { _logger = logger; // Determine the appropriate config file path string baseDir = AppContext.BaseDirectory; string etcConfigPath = "/etc/transmission-rss-manager/appsettings.json"; string localConfigPath = Path.Combine(baseDir, "appsettings.json"); // Check if config exists in /etc (preferred) or in app directory _configFilePath = File.Exists(etcConfigPath) ? etcConfigPath : localConfigPath; _logger.LogInformation($"Using configuration file: {_configFilePath}"); } // Implement the interface methods required by IConfigService public AppConfig GetConfiguration() { // Non-async method required by interface if (_cachedConfig != null) { return _cachedConfig; } try { // Load synchronously since this is a sync method _cachedConfig = LoadConfigFromFileSync(); return _cachedConfig; } catch (Exception ex) { _logger.LogError(ex, "Error loading configuration, using default values"); return CreateDefaultConfig(); } } public async Task SaveConfigurationAsync(AppConfig config) { try { _cachedConfig = config; await SaveConfigToFileAsync(config); _logger.LogInformation("Configuration saved successfully to file"); } catch (Exception ex) { _logger.LogError(ex, "Error saving configuration to file"); throw; } } // Additional methods for backward compatibility public async Task GetConfigAsync() { if (_cachedConfig != null) { return _cachedConfig; } try { _cachedConfig = await LoadConfigFromFileAsync(); return _cachedConfig; } catch (Exception ex) { _logger.LogError(ex, "Error loading configuration, using default values"); return CreateDefaultConfig(); } } public async Task SaveConfigAsync(AppConfig config) { await SaveConfigurationAsync(config); } public async Task GetSettingAsync(string key, string defaultValue = "") { var config = await GetConfigAsync(); switch (key) { case "Transmission.Host": return config.Transmission.Host ?? defaultValue; case "Transmission.Port": return config.Transmission.Port.ToString(); case "Transmission.Username": return config.Transmission.Username ?? defaultValue; case "Transmission.Password": return config.Transmission.Password ?? defaultValue; case "Transmission.UseHttps": return config.Transmission.UseHttps.ToString(); case "AutoDownloadEnabled": return config.AutoDownloadEnabled.ToString(); case "CheckIntervalMinutes": return config.CheckIntervalMinutes.ToString(); case "DownloadDirectory": return config.DownloadDirectory ?? defaultValue; case "MediaLibraryPath": return config.MediaLibraryPath ?? defaultValue; case "PostProcessing.Enabled": return config.PostProcessing.Enabled.ToString(); case "PostProcessing.ExtractArchives": return config.PostProcessing.ExtractArchives.ToString(); case "PostProcessing.OrganizeMedia": return config.PostProcessing.OrganizeMedia.ToString(); case "PostProcessing.MinimumSeedRatio": return config.PostProcessing.MinimumSeedRatio.ToString(); case "UserPreferences.EnableDarkMode": return config.UserPreferences.EnableDarkMode.ToString(); case "UserPreferences.AutoRefreshUIEnabled": return config.UserPreferences.AutoRefreshUIEnabled.ToString(); case "UserPreferences.AutoRefreshIntervalSeconds": return config.UserPreferences.AutoRefreshIntervalSeconds.ToString(); case "UserPreferences.NotificationsEnabled": return config.UserPreferences.NotificationsEnabled.ToString(); default: _logger.LogWarning($"Unknown setting key: {key}"); return defaultValue; } } public async Task SaveSettingAsync(string key, string value) { var config = await GetConfigAsync(); bool changed = false; try { switch (key) { case "Transmission.Host": config.Transmission.Host = value; changed = true; break; case "Transmission.Port": if (int.TryParse(value, out int port)) { config.Transmission.Port = port; changed = true; } break; case "Transmission.Username": config.Transmission.Username = value; changed = true; break; case "Transmission.Password": config.Transmission.Password = value; changed = true; break; case "Transmission.UseHttps": if (bool.TryParse(value, out bool useHttps)) { config.Transmission.UseHttps = useHttps; changed = true; } break; case "AutoDownloadEnabled": if (bool.TryParse(value, out bool autoDownload)) { config.AutoDownloadEnabled = autoDownload; changed = true; } break; case "CheckIntervalMinutes": if (int.TryParse(value, out int interval)) { config.CheckIntervalMinutes = interval; changed = true; } break; case "DownloadDirectory": config.DownloadDirectory = value; changed = true; break; case "MediaLibraryPath": config.MediaLibraryPath = value; changed = true; break; case "PostProcessing.Enabled": if (bool.TryParse(value, out bool ppEnabled)) { config.PostProcessing.Enabled = ppEnabled; changed = true; } break; case "PostProcessing.ExtractArchives": if (bool.TryParse(value, out bool extractArchives)) { config.PostProcessing.ExtractArchives = extractArchives; changed = true; } break; case "PostProcessing.OrganizeMedia": if (bool.TryParse(value, out bool organizeMedia)) { config.PostProcessing.OrganizeMedia = organizeMedia; changed = true; } break; case "PostProcessing.MinimumSeedRatio": if (float.TryParse(value, out float seedRatio)) { config.PostProcessing.MinimumSeedRatio = seedRatio; changed = true; } break; case "UserPreferences.EnableDarkMode": if (bool.TryParse(value, out bool darkMode)) { config.UserPreferences.EnableDarkMode = darkMode; changed = true; } break; case "UserPreferences.AutoRefreshUIEnabled": if (bool.TryParse(value, out bool autoRefresh)) { config.UserPreferences.AutoRefreshUIEnabled = autoRefresh; changed = true; } break; case "UserPreferences.AutoRefreshIntervalSeconds": if (int.TryParse(value, out int refreshInterval)) { config.UserPreferences.AutoRefreshIntervalSeconds = refreshInterval; changed = true; } break; case "UserPreferences.NotificationsEnabled": if (bool.TryParse(value, out bool notifications)) { config.UserPreferences.NotificationsEnabled = notifications; changed = true; } break; default: _logger.LogWarning($"Unknown setting key: {key}"); break; } if (changed) { await SaveConfigAsync(config); } } catch (Exception ex) { _logger.LogError(ex, $"Error saving setting {key}"); throw; } } private AppConfig LoadConfigFromFileSync() { try { if (!File.Exists(_configFilePath)) { _logger.LogWarning($"Configuration file not found at {_configFilePath}, creating default config"); var defaultConfig = CreateDefaultConfig(); // Save synchronously since we're in a sync method File.WriteAllText(_configFilePath, JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); return defaultConfig; } string json = File.ReadAllText(_configFilePath); var config = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (config == null) { throw new InvalidOperationException("Failed to deserialize configuration"); } // Fill in any missing values with defaults EnsureCompleteConfig(config); return config; } catch (Exception ex) { _logger.LogError(ex, "Error loading configuration from file"); throw; } } private async Task LoadConfigFromFileAsync() { try { if (!File.Exists(_configFilePath)) { _logger.LogWarning($"Configuration file not found at {_configFilePath}, creating default config"); var defaultConfig = CreateDefaultConfig(); await SaveConfigToFileAsync(defaultConfig); return defaultConfig; } string json = await File.ReadAllTextAsync(_configFilePath); var config = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (config == null) { throw new InvalidOperationException("Failed to deserialize configuration"); } // Fill in any missing values with defaults EnsureCompleteConfig(config); return config; } catch (Exception ex) { _logger.LogError(ex, "Error loading configuration from file"); throw; } } private async Task SaveConfigToFileAsync(AppConfig config) { try { string json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); // Create directory if it doesn't exist string directory = Path.GetDirectoryName(_configFilePath); if (!Directory.Exists(directory) && !string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } // Use a temporary file to avoid corruption if the process crashes during write string tempFilePath = _configFilePath + ".tmp"; await File.WriteAllTextAsync(tempFilePath, json); // Atomic file replacement (as much as the filesystem allows) if (File.Exists(_configFilePath)) { File.Replace(tempFilePath, _configFilePath, _configFilePath + ".bak"); } else { File.Move(tempFilePath, _configFilePath); } } catch (Exception ex) { _logger.LogError(ex, "Error saving configuration to file"); throw; } } private AppConfig CreateDefaultConfig() { return new AppConfig { Transmission = new TransmissionConfig { Host = "localhost", Port = 9091, Username = "", Password = "", UseHttps = false }, AutoDownloadEnabled = true, CheckIntervalMinutes = 30, DownloadDirectory = "/var/lib/transmission-daemon/downloads", MediaLibraryPath = "/media/library", PostProcessing = new PostProcessingConfig { Enabled = false, ExtractArchives = true, OrganizeMedia = true, MinimumSeedRatio = 1.0f }, UserPreferences = new UserPreferencesConfig { EnableDarkMode = true, AutoRefreshUIEnabled = true, AutoRefreshIntervalSeconds = 30, NotificationsEnabled = true } }; } private void EnsureCompleteConfig(AppConfig config) { // Create new instances for any null nested objects config.Transmission ??= new TransmissionConfig { Host = "localhost", Port = 9091, Username = "", Password = "", UseHttps = false }; config.PostProcessing ??= new PostProcessingConfig { Enabled = false, ExtractArchives = true, OrganizeMedia = true, MinimumSeedRatio = 1.0f }; config.UserPreferences ??= new UserPreferencesConfig { EnableDarkMode = true, AutoRefreshUIEnabled = true, AutoRefreshIntervalSeconds = 30, NotificationsEnabled = true }; // Ensure default values for string properties if they're null config.DownloadDirectory ??= "/var/lib/transmission-daemon/downloads"; config.MediaLibraryPath ??= "/media/library"; config.Transmission.Host ??= "localhost"; config.Transmission.Username ??= ""; config.Transmission.Password ??= ""; } } }