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
},
UserPreferences = new UserPreferences
{
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
};
config.UserPreferences ??= new UserPreferences
{
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 ??= "";
}
}
}