diff --git a/src/Services/ConfigService.cs b/src/Services/ConfigService.cs
index fce7b78..612062b 100644
--- a/src/Services/ConfigService.cs
+++ b/src/Services/ConfigService.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -7,84 +9,317 @@ 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 _configPath;
+ private readonly string _configFilePath;
private AppConfig _cachedConfig;
+ private readonly object _lockObject = new object();
public ConfigService(ILogger logger)
{
_logger = logger;
- // Get config directory
- string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
- string configDir = Path.Combine(homeDir, ".config", "transmission-rss-manager");
+ // 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");
- // Ensure directory exists
- if (!Directory.Exists(configDir))
- {
- Directory.CreateDirectory(configDir);
- }
+ // Check if config exists in /etc (preferred) or in app directory
+ _configFilePath = File.Exists(etcConfigPath) ? etcConfigPath : localConfigPath;
- _configPath = Path.Combine(configDir, "config.json");
- _cachedConfig = LoadConfiguration();
+ _logger.LogInformation($"Using configuration file: {_configFilePath}");
}
- public AppConfig GetConfiguration()
+ public async Task GetConfigAsync()
{
- return _cachedConfig;
- }
-
- public async Task SaveConfigurationAsync(AppConfig config)
- {
- _cachedConfig = config;
-
- var options = new JsonSerializerOptions
+ if (_cachedConfig != null)
{
- WriteIndented = true
- };
-
- string json = JsonSerializer.Serialize(config, options);
- await File.WriteAllTextAsync(_configPath, json);
-
- _logger.LogInformation("Configuration saved successfully");
- }
-
- private AppConfig LoadConfiguration()
- {
- if (!File.Exists(_configPath))
- {
- _logger.LogInformation("No configuration file found, creating default");
- var defaultConfig = CreateDefaultConfig();
- SaveConfigurationAsync(defaultConfig).Wait();
- return defaultConfig;
+ return _cachedConfig;
}
try
{
- string json = File.ReadAllText(_configPath);
- var config = JsonSerializer.Deserialize(json);
-
+ _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)
+ {
+ 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;
+ }
+ }
+
+ 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 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)
{
- _logger.LogWarning("Failed to deserialize config, creating default");
- return CreateDefaultConfig();
+ 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");
- return CreateDefaultConfig();
+ _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()
{
- string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
-
return new AppConfig
{
Transmission = new TransmissionConfig
@@ -92,21 +327,64 @@ namespace TransmissionRssManager.Services
Host = "localhost",
Port = 9091,
Username = "",
- Password = ""
+ Password = "",
+ UseHttps = false
},
- AutoDownloadEnabled = false,
+ AutoDownloadEnabled = true,
CheckIntervalMinutes = 30,
- DownloadDirectory = Path.Combine(homeDir, "Downloads"),
- MediaLibraryPath = Path.Combine(homeDir, "Media"),
+ DownloadDirectory = "/var/lib/transmission-daemon/downloads",
+ MediaLibraryPath = "/media/library",
PostProcessing = new PostProcessingConfig
{
Enabled = false,
ExtractArchives = true,
OrganizeMedia = true,
- MinimumSeedRatio = 1,
- MediaExtensions = new System.Collections.Generic.List { ".mp4", ".mkv", ".avi" }
+ 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 ??= "";
+ }
}
}
\ No newline at end of file