refactoring de pmoconfig
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -2397,13 +2397,17 @@ name = "pmoconfig"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"axum",
|
||||||
"dirs",
|
"dirs",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"pmoutils",
|
"pmoutils",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"utoipa",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2555,6 +2559,7 @@ dependencies = [
|
|||||||
name = "pmoserver"
|
name = "pmoserver"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-embed",
|
"axum-embed",
|
||||||
|
|||||||
@@ -9,9 +9,21 @@ pmoutils ={ path = "../pmoutils" }
|
|||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_yaml = "0.9.33"
|
serde_yaml = "0.9.33"
|
||||||
|
serde_json = "1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
uuid = { version = "1.18.1", features = ["v4"] }
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
|
||||||
|
# Async
|
||||||
|
tokio = { version = "1.0", features = ["full"], optional = true }
|
||||||
|
|
||||||
|
# Serveur HTTP (pour l'API REST)
|
||||||
|
axum = { version = "0.8", optional = true }
|
||||||
|
utoipa = { version = "5.3", features = ["axum_extras"], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
api = ["dep:tokio", "dep:axum", "dep:utoipa"]
|
||||||
154
pmoconfig/src/api.rs
Normal file
154
pmoconfig/src/api.rs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
use crate::Config;
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
use serde_yaml::Value;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Structure pour récupérer une valeur de configuration
|
||||||
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ConfigValue {
|
||||||
|
/// Chemin de la clé (ex: "host.http_port")
|
||||||
|
pub path: String,
|
||||||
|
/// Valeur au format JSON
|
||||||
|
pub value: JsonValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure pour mettre à jour une valeur de configuration
|
||||||
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpdateConfigRequest {
|
||||||
|
/// Chemin de la clé (ex: "host.http_port")
|
||||||
|
pub path: String,
|
||||||
|
/// Nouvelle valeur au format JSON
|
||||||
|
pub value: JsonValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure pour la réponse d'une mise à jour
|
||||||
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UpdateConfigResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Erreur API
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiError(anyhow::Error);
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": self.0.to_string()
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for ApiError
|
||||||
|
where
|
||||||
|
E: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(err: E) -> Self {
|
||||||
|
Self(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/config - Récupérer toute la configuration
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/config",
|
||||||
|
tag = "config",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Configuration complète", body = serde_json::Value)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_full_config(
|
||||||
|
State(config): State<Arc<Config>>,
|
||||||
|
) -> Result<Json<JsonValue>, ApiError> {
|
||||||
|
let value = config.get_value(&[])?;
|
||||||
|
let json_value = yaml_to_json(&value)?;
|
||||||
|
Ok(Json(json_value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /api/config/{path} - Récupérer une valeur à un chemin spécifique
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/config/{path}",
|
||||||
|
tag = "config",
|
||||||
|
params(
|
||||||
|
("path" = String, Path, description = "Chemin de la configuration (séparé par des points, ex: host.http_port)")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Valeur de configuration", body = ConfigValue),
|
||||||
|
(status = 404, description = "Chemin non trouvé")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_config_value(
|
||||||
|
State(config): State<Arc<Config>>,
|
||||||
|
Path(path): Path<String>,
|
||||||
|
) -> Result<Json<ConfigValue>, ApiError> {
|
||||||
|
let path_parts: Vec<&str> = path.split('.').collect();
|
||||||
|
let value = config.get_value(&path_parts)?;
|
||||||
|
let json_value = yaml_to_json(&value)?;
|
||||||
|
|
||||||
|
Ok(Json(ConfigValue {
|
||||||
|
path,
|
||||||
|
value: json_value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/config - Mettre à jour une valeur de configuration
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/config",
|
||||||
|
tag = "config",
|
||||||
|
request_body = UpdateConfigRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Configuration mise à jour", body = UpdateConfigResponse)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn update_config_value(
|
||||||
|
State(config): State<Arc<Config>>,
|
||||||
|
Json(request): Json<UpdateConfigRequest>,
|
||||||
|
) -> Result<Json<UpdateConfigResponse>, ApiError> {
|
||||||
|
let path_parts: Vec<&str> = request.path.split('.').collect();
|
||||||
|
let yaml_value = json_to_yaml(&request.value)?;
|
||||||
|
|
||||||
|
config.set_value(&path_parts, yaml_value)?;
|
||||||
|
|
||||||
|
Ok(Json(UpdateConfigResponse {
|
||||||
|
success: true,
|
||||||
|
message: format!("Configuration updated at path: {}", request.path),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convertit une valeur YAML en JSON
|
||||||
|
fn yaml_to_json(yaml: &Value) -> Result<JsonValue, ApiError> {
|
||||||
|
// Serialize YAML to string then parse as JSON
|
||||||
|
let yaml_str = serde_yaml::to_string(yaml)?;
|
||||||
|
Ok(serde_json::from_str(&yaml_str)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convertit une valeur JSON en YAML
|
||||||
|
fn json_to_yaml(json: &JsonValue) -> Result<Value, ApiError> {
|
||||||
|
// Serialize JSON to string then parse as YAML
|
||||||
|
let json_str = serde_json::to_string(json)?;
|
||||||
|
Ok(serde_yaml::from_str(&json_str)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crée le router API pour la configuration
|
||||||
|
pub fn create_router(config: Arc<Config>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/api/config", get(get_full_config))
|
||||||
|
.route("/api/config", post(update_config_value))
|
||||||
|
.route("/api/config/:path", get(get_config_value))
|
||||||
|
.with_state(config)
|
||||||
|
}
|
||||||
@@ -1,3 +1,29 @@
|
|||||||
|
//! # PMOMusic Configuration Module
|
||||||
|
//!
|
||||||
|
//! This module provides configuration management for PMOMusic, including:
|
||||||
|
//! - Loading configuration from YAML files
|
||||||
|
//! - Merging with embedded default configuration
|
||||||
|
//! - Environment variable overrides
|
||||||
|
//! - Type-safe getters and setters for configuration values
|
||||||
|
//! - Thread-safe singleton access pattern
|
||||||
|
//!
|
||||||
|
//! ## Usage
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use pmoconfig::get_config;
|
||||||
|
//!
|
||||||
|
//! // Get the global configuration
|
||||||
|
//! let config = get_config();
|
||||||
|
//!
|
||||||
|
//! // Access configuration values
|
||||||
|
//! let port = config.get_http_port();
|
||||||
|
//! let cache_dir = config.get_cover_cache_dir()?;
|
||||||
|
//!
|
||||||
|
//! // Update configuration values
|
||||||
|
//! config.set_http_port(9000)?;
|
||||||
|
//! # Ok::<(), anyhow::Error>(())
|
||||||
|
//! ```
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@@ -5,12 +31,21 @@ use pmoutils::guess_local_ip;
|
|||||||
use serde_yaml::{Mapping, Number, Value};
|
use serde_yaml::{Mapping, Number, Value};
|
||||||
use std::{
|
use std::{
|
||||||
env, fs,
|
env, fs,
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
use tracing::{info, warn};
|
use tracing::info;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Modules conditionnels pour l'API REST
|
||||||
|
#[cfg(feature = "api")]
|
||||||
|
pub mod api;
|
||||||
|
#[cfg(feature = "api")]
|
||||||
|
pub mod openapi;
|
||||||
|
|
||||||
|
#[cfg(feature = "api")]
|
||||||
|
pub use openapi::ApiDoc;
|
||||||
|
|
||||||
// Configuration par défaut intégrée
|
// Configuration par défaut intégrée
|
||||||
const DEFAULT_CONFIG: &str = include_str!("pmomusic.yaml");
|
const DEFAULT_CONFIG: &str = include_str!("pmomusic.yaml");
|
||||||
|
|
||||||
@@ -19,11 +54,91 @@ lazy_static! {
|
|||||||
Arc::new(Config::load_config("").expect("Failed to load PMOMusic configuration"));
|
Arc::new(Config::load_config("").expect("Failed to load PMOMusic configuration"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENV_CONFIG_FILE: &str = "PMOMUSIC_CONFIG";
|
const ENV_CONFIG_DIR: &str = "PMOMUSIC_CONFIG";
|
||||||
const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__";
|
const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__";
|
||||||
|
|
||||||
|
// Default values for configuration
|
||||||
|
const DEFAULT_HTTP_PORT: u16 = 8080;
|
||||||
|
const DEFAULT_COVER_CACHE_DIR: &str = "cache_covers";
|
||||||
|
const DEFAULT_COVER_CACHE_SIZE: usize = 2000;
|
||||||
|
const DEFAULT_AUDIO_CACHE_DIR: &str = "cache_audio";
|
||||||
|
const DEFAULT_AUDIO_CACHE_SIZE: usize = 500;
|
||||||
|
const DEFAULT_LOG_BUFFER_CAPACITY: usize = 1000;
|
||||||
|
const DEFAULT_LOG_MIN_LEVEL: &str = "TRACE";
|
||||||
|
const DEFAULT_LOG_ENABLE_CONSOLE: bool = true;
|
||||||
|
|
||||||
|
/// Macro to generate getter/setter for String values
|
||||||
|
macro_rules! impl_string_config {
|
||||||
|
($(#[$meta:meta])* $getter:ident, $setter:ident, $path:expr, $default:expr) => {
|
||||||
|
$(#[$meta])*
|
||||||
|
pub fn $getter(&self) -> Result<String> {
|
||||||
|
match self.get_value($path)? {
|
||||||
|
Value::String(s) => Ok(s),
|
||||||
|
_ => Err(anyhow!(concat!(stringify!($getter), " not configured"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(#[$meta])*
|
||||||
|
pub fn $setter(&self, value: &str) -> Result<()> {
|
||||||
|
self.set_value($path, Value::String(value.to_string()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to generate getter/setter for usize values with default
|
||||||
|
macro_rules! impl_usize_config {
|
||||||
|
($getter:ident, $setter:ident, $path:expr, $default:expr) => {
|
||||||
|
pub fn $getter(&self) -> Result<usize> {
|
||||||
|
match self.get_value($path)? {
|
||||||
|
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
||||||
|
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
||||||
|
_ => Ok($default),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn $setter(&self, size: usize) -> Result<()> {
|
||||||
|
let n = Number::from(size);
|
||||||
|
self.set_value($path, Value::Number(n))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to generate getter/setter for bool values with default
|
||||||
|
macro_rules! impl_bool_config {
|
||||||
|
($getter:ident, $setter:ident, $path:expr, $default:expr) => {
|
||||||
|
pub fn $getter(&self) -> Result<bool> {
|
||||||
|
match self.get_value($path)? {
|
||||||
|
Value::Bool(b) => Ok(b),
|
||||||
|
_ => Ok($default),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn $setter(&self, value: bool) -> Result<()> {
|
||||||
|
self.set_value($path, Value::Bool(value))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration manager for PMOMusic
|
||||||
|
///
|
||||||
|
/// This structure manages the application configuration, including:
|
||||||
|
/// - Loading configuration from YAML files
|
||||||
|
/// - Merging with default configuration
|
||||||
|
/// - Handling environment variable overrides
|
||||||
|
/// - Providing typed getters/setters for configuration values
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use pmoconfig::get_config;
|
||||||
|
///
|
||||||
|
/// let config = get_config();
|
||||||
|
/// let port = config.get_http_port();
|
||||||
|
/// println!("HTTP port: {}", port);
|
||||||
|
/// ```
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
config_dir: String,
|
||||||
path: String,
|
path: String,
|
||||||
data: Mutex<Value>,
|
data: Mutex<Value>,
|
||||||
}
|
}
|
||||||
@@ -33,6 +148,7 @@ impl Clone for Config {
|
|||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
let data = self.data.lock().unwrap().clone();
|
let data = self.data.lock().unwrap().clone();
|
||||||
Self {
|
Self {
|
||||||
|
config_dir: self.config_dir.clone(),
|
||||||
path: self.path.clone(),
|
path: self.path.clone(),
|
||||||
data: Mutex::new(data),
|
data: Mutex::new(data),
|
||||||
}
|
}
|
||||||
@@ -40,100 +156,144 @@ impl Clone for Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load_config(filename: &str) -> Result<Self> {
|
/// Finds a config directory by trying different locations in order
|
||||||
let mut path = filename.to_string();
|
fn find_config_dir(directory: &str) -> String {
|
||||||
let mut data: Option<Vec<u8>> = None;
|
// 1. Try provided directory
|
||||||
|
if !directory.is_empty() {
|
||||||
|
return directory.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try environment variable
|
||||||
|
if let Ok(env_path) = env::var(ENV_CONFIG_DIR) {
|
||||||
|
info!(env_var=ENV_CONFIG_DIR, path=%env_path, "Trying to load config from env");
|
||||||
|
return env_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Try current directory
|
||||||
|
if Path::new(".pmomusic").exists() {
|
||||||
|
return ".pmomusic".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Try home directory
|
||||||
|
if let Some(home) = home_dir() {
|
||||||
|
let home_config = home.join(".pmomusic");
|
||||||
|
if home_config.exists() {
|
||||||
|
return home_config.to_string_lossy().to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback
|
||||||
|
".pmomusic".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates and prepares a config directory
|
||||||
|
fn validate_config_dir(path: &Path) -> Result<()> {
|
||||||
|
// Create if doesn't exist
|
||||||
|
if !path.exists() {
|
||||||
|
fs::create_dir_all(path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's a directory
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(anyhow!("Le chemin spécifié n'est pas un répertoire"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test write permission
|
||||||
|
let test_file = path.join(".write_test");
|
||||||
|
fs::write(&test_file, b"test")?;
|
||||||
|
fs::remove_file(&test_file)?;
|
||||||
|
|
||||||
|
// Test read permission
|
||||||
|
fs::read_dir(path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines and validates the configuration directory
|
||||||
|
///
|
||||||
|
/// The directory is searched in the following order:
|
||||||
|
/// 1. The provided `directory` parameter if not empty
|
||||||
|
/// 2. The `PMOMUSIC_CONFIG` environment variable
|
||||||
|
/// 3. `.pmomusic` in the current directory
|
||||||
|
/// 4. `.pmomusic` in the user's home directory
|
||||||
|
///
|
||||||
|
/// The directory is created if it doesn't exist, and validated for read/write permissions.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the directory cannot be created or validated
|
||||||
|
pub fn config_dir(directory: &str) -> String {
|
||||||
|
let dir_path = Self::find_config_dir(directory);
|
||||||
|
let path = Path::new(&dir_path);
|
||||||
|
|
||||||
|
Self::validate_config_dir(path)
|
||||||
|
.expect("Impossible de valider le répertoire de configuration");
|
||||||
|
|
||||||
|
dir_path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the configuration from the specified directory
|
||||||
|
///
|
||||||
|
/// This method:
|
||||||
|
/// 1. Determines the configuration directory
|
||||||
|
/// 2. Loads the default embedded configuration
|
||||||
|
/// 3. Merges it with the external config.yaml file if present
|
||||||
|
/// 4. Applies environment variable overrides
|
||||||
|
/// 5. Saves the merged configuration
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `directory` - The directory containing the config.yaml file, or empty to use defaults
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the loaded `Config` or an error
|
||||||
|
pub fn load_config(directory: &str) -> Result<Self> {
|
||||||
|
// Obtenir le répertoire de configuration
|
||||||
|
let config_dir = Self::config_dir(directory);
|
||||||
|
info!(config_dir=%config_dir, "Using config directory");
|
||||||
|
|
||||||
|
// Construire le chemin du fichier config.yaml
|
||||||
|
let config_file_path = Path::new(&config_dir).join("config.yaml");
|
||||||
|
let path = config_file_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// Charger la configuration par défaut
|
||||||
let mut default_value: Value = serde_yaml::from_str(DEFAULT_CONFIG)?;
|
let mut default_value: Value = serde_yaml::from_str(DEFAULT_CONFIG)?;
|
||||||
|
|
||||||
// Essayer de charger depuis différents emplacements
|
// Essayer de charger le fichier de configuration
|
||||||
if !filename.is_empty() {
|
let yaml_data = if let Ok(data) = fs::read(&path) {
|
||||||
info!(config_file=%path, "Trying to load config");
|
info!(config_file=%path, "Loaded config file");
|
||||||
data = fs::read(&path).ok();
|
data
|
||||||
if data.is_none() {
|
|
||||||
warn!(config_file=%path, "Cannot read config file");
|
|
||||||
path.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.is_empty() {
|
|
||||||
if let Ok(env_path) = env::var(ENV_CONFIG_FILE) {
|
|
||||||
info!(env_var=ENV_CONFIG_FILE, path=%env_path, "Trying to load config from env");
|
|
||||||
path = env_path.clone();
|
|
||||||
data = fs::read(&path).ok();
|
|
||||||
if data.is_none() {
|
|
||||||
warn!(config_file=%path, "Cannot read config file from env var");
|
|
||||||
path.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.is_empty() {
|
|
||||||
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
||||||
path = current_dir
|
|
||||||
.join(".pmomusic.yml")
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
info!(config_file=%path, "Trying to load config file from current directory");
|
|
||||||
data = fs::read(&path).ok();
|
|
||||||
if data.is_none() {
|
|
||||||
warn!(config_file=%path, "Cannot read config file in current dir");
|
|
||||||
path.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.is_empty() {
|
|
||||||
path = Self::get_home_yml_path();
|
|
||||||
info!(config_file=%path, "Trying to load config file from home directory");
|
|
||||||
data = fs::read(&path).ok();
|
|
||||||
if data.is_none() {
|
|
||||||
warn!(config_file=%path, "Cannot read config file in home directory");
|
|
||||||
path.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let yaml_data = if let Some(d) = data {
|
|
||||||
d
|
|
||||||
} else {
|
} else {
|
||||||
info!("Using default embedded config");
|
info!(config_file=%path, "Config file not found, using default embedded config");
|
||||||
DEFAULT_CONFIG.as_bytes().to_vec()
|
DEFAULT_CONFIG.as_bytes().to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Merger avec la config par défaut
|
||||||
let external_value: Value = serde_yaml::from_slice(&yaml_data)?;
|
let external_value: Value = serde_yaml::from_slice(&yaml_data)?;
|
||||||
merge_yaml(&mut default_value, &external_value);
|
merge_yaml(&mut default_value, &external_value);
|
||||||
let mut config_value = Self::lower_keys_value(default_value);
|
let mut config_value = Self::lower_keys_value(default_value);
|
||||||
|
|
||||||
|
// Appliquer les overrides depuis les variables d'environnement
|
||||||
Self::apply_env_overrides(&mut config_value);
|
Self::apply_env_overrides(&mut config_value);
|
||||||
|
|
||||||
if path.is_empty() || !Self::is_writable(&path) {
|
// Créer la configuration
|
||||||
let candidates = [
|
|
||||||
filename.to_string(),
|
|
||||||
env::var(ENV_CONFIG_FILE).unwrap_or_default(),
|
|
||||||
".pmomusic.yml".to_string(),
|
|
||||||
Self::get_home_yml_path(),
|
|
||||||
];
|
|
||||||
for candidate in candidates.iter().filter(|c| !c.is_empty()) {
|
|
||||||
if Self::is_writable(candidate) {
|
|
||||||
path = candidate.clone();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.is_empty() {
|
|
||||||
return Err(anyhow!("Cannot find a place to store config file"));
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(config_file=%path, "Config file will be stored here");
|
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
|
config_dir,
|
||||||
path,
|
path,
|
||||||
data: Mutex::new(config_value),
|
data: Mutex::new(config_value),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sauvegarder la configuration
|
||||||
config.save()?;
|
config.save()?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves the current configuration to the config.yaml file
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` indicating success or failure
|
||||||
pub fn save(&self) -> Result<()> {
|
pub fn save(&self) -> Result<()> {
|
||||||
let data = self.data.lock().unwrap();
|
let data = self.data.lock().unwrap();
|
||||||
let yaml = serde_yaml::to_string(&*data)?;
|
let yaml = serde_yaml::to_string(&*data)?;
|
||||||
@@ -141,6 +301,16 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets a configuration value at the specified path and saves it
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path` - Array of keys representing the path (e.g., `&["host", "http_port"]`)
|
||||||
|
/// * `value` - The YAML value to set
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` indicating success or failure
|
||||||
pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> {
|
pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> {
|
||||||
let mut data = self.data.lock().unwrap();
|
let mut data = self.data.lock().unwrap();
|
||||||
Self::set_value_internal(&mut data, path, value.clone())?;
|
Self::set_value_internal(&mut data, path, value.clone())?;
|
||||||
@@ -171,6 +341,15 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets a configuration value at the specified path
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path` - Array of keys representing the path (e.g., `&["host", "http_port"]`)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the YAML value or an error if the path doesn't exist
|
||||||
pub fn get_value(&self, path: &[&str]) -> Result<Value> {
|
pub fn get_value(&self, path: &[&str]) -> Result<Value> {
|
||||||
let data = self.data.lock().unwrap();
|
let data = self.data.lock().unwrap();
|
||||||
Self::get_value_internal(&data, path)
|
Self::get_value_internal(&data, path)
|
||||||
@@ -194,14 +373,6 @@ impl Config {
|
|||||||
Ok(current.clone())
|
Ok(current.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_home_yml_path() -> String {
|
|
||||||
home_dir()
|
|
||||||
.map(|p| p.join(".pmomusic.yml"))
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_env_overrides(config: &mut Value) {
|
fn apply_env_overrides(config: &mut Value) {
|
||||||
for (key, value) in env::vars() {
|
for (key, value) in env::vars() {
|
||||||
if key.starts_with(ENV_PREFIX) {
|
if key.starts_with(ENV_PREFIX) {
|
||||||
@@ -244,17 +415,64 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_writable(path: &str) -> bool {
|
/// Résout un chemin relatif ou absolu et crée le répertoire si nécessaire
|
||||||
let path = Path::new(path);
|
fn resolve_and_create_dir(&self, dir_path: &str) -> Result<String> {
|
||||||
if let Some(parent) = path.parent() {
|
let path = Path::new(dir_path);
|
||||||
fs::metadata(parent)
|
|
||||||
.map(|m| !m.permissions().readonly())
|
// Déterminer si le chemin est relatif ou absolu
|
||||||
.unwrap_or(false)
|
let absolute_path = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
false
|
// Chemin relatif : le résoudre par rapport à config_dir
|
||||||
|
Path::new(&self.config_dir).join(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Créer le répertoire s'il n'existe pas
|
||||||
|
if !absolute_path.exists() {
|
||||||
|
fs::create_dir_all(&absolute_path)?;
|
||||||
|
info!(directory=%absolute_path.display(), "Created cache directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retourner le chemin absolu
|
||||||
|
Ok(absolute_path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic function to get cache directory
|
||||||
|
fn get_cache_dir(&self, cache_type: &str, default_dir: &str) -> Result<String> {
|
||||||
|
let dir_path = match self.get_value(&["host", cache_type, "directory"]) {
|
||||||
|
Ok(Value::String(s)) => s,
|
||||||
|
_ => default_dir.to_string(),
|
||||||
|
};
|
||||||
|
self.resolve_and_create_dir(&dir_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic function to set cache directory
|
||||||
|
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()> {
|
||||||
|
self.set_value(&["host", cache_type, "directory"], Value::String(directory))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic function to get cache size
|
||||||
|
fn get_cache_size(&self, cache_type: &str, default_size: usize) -> Result<usize> {
|
||||||
|
match self.get_value(&["host", cache_type, "size"])? {
|
||||||
|
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
||||||
|
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
||||||
|
_ => Ok(default_size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generic function to set cache size
|
||||||
|
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()> {
|
||||||
|
let n = Number::from(size);
|
||||||
|
self.set_value(&["host", cache_type, "size"], Value::Number(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the base URL for the HTTP server
|
||||||
|
///
|
||||||
|
/// Returns the configured base URL, or attempts to guess the local IP address if not configured.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The base URL as a String
|
||||||
pub fn get_base_url(&self) -> String {
|
pub fn get_base_url(&self) -> String {
|
||||||
match self.get_value(&["host", "base_url"]) {
|
match self.get_value(&["host", "base_url"]) {
|
||||||
Ok(Value::String(s)) if !s.is_empty() => s,
|
Ok(Value::String(s)) if !s.is_empty() => s,
|
||||||
@@ -269,32 +487,58 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the HTTP port from configuration
|
||||||
|
///
|
||||||
|
/// Returns the configured HTTP port, or the default port (8080) if not configured or invalid.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The HTTP port as a u16
|
||||||
pub fn get_http_port(&self) -> u16 {
|
pub fn get_http_port(&self) -> u16 {
|
||||||
match self.get_value(&["host", "http_port"]) {
|
match self.get_value(&["host", "http_port"]) {
|
||||||
Ok(Value::Number(n)) if n.is_i64() => n.as_i64().unwrap() as u16,
|
Ok(Value::Number(n)) if n.is_i64() => n.as_i64().unwrap() as u16,
|
||||||
Ok(Value::String(s)) => match s.parse::<u16>() {
|
Ok(Value::String(s)) => match s.parse::<u16>() {
|
||||||
Ok(port) => port,
|
Ok(port) => port,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::warn!("Invalid HTTP port '{}', using default 8080", s);
|
tracing::warn!("Invalid HTTP port '{}', using default {}", s, DEFAULT_HTTP_PORT);
|
||||||
8080
|
DEFAULT_HTTP_PORT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
tracing::warn!("HTTP port not a number or string, using default 8080");
|
tracing::warn!("HTTP port not a number or string, using default {}", DEFAULT_HTTP_PORT);
|
||||||
8080
|
DEFAULT_HTTP_PORT
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("Failed to get HTTP port: {}, using default 8080", err);
|
tracing::warn!("Failed to get HTTP port: {}, using default {}", err, DEFAULT_HTTP_PORT);
|
||||||
8080
|
DEFAULT_HTTP_PORT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the HTTP port in configuration
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `port` - The port number to set
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` indicating success or failure
|
||||||
pub fn set_http_port(&self, port: u16) -> Result<()> {
|
pub fn set_http_port(&self, port: u16) -> Result<()> {
|
||||||
let n = Number::from(port);
|
let n = Number::from(port);
|
||||||
self.set_value(&["host", "http_port"], Value::Number(n))
|
self.set_value(&["host", "http_port"], Value::Number(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the UDN (Unique Device Name) for a device, generating one if it doesn't exist
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `devtype` - The device type (e.g., "mediarenderer")
|
||||||
|
/// * `name` - The device name
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the UDN string, generating a new UUID if not found
|
||||||
pub fn get_device_udn(&self, devtype: &str, name: &str) -> Result<String> {
|
pub fn get_device_udn(&self, devtype: &str, name: &str) -> Result<String> {
|
||||||
let path = &["devices", devtype, name, "udn"];
|
let path = &["devices", devtype, name, "udn"];
|
||||||
match self.get_value(path) {
|
match self.get_value(path) {
|
||||||
@@ -307,146 +551,173 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the UDN (Unique Device Name) for a device
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `devtype` - The device type (e.g., "mediarenderer")
|
||||||
|
/// * `name` - The device name
|
||||||
|
/// * `udn` - The UDN to set
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` indicating success or failure
|
||||||
pub fn set_device_udn(&self, devtype: &str, name: &str, udn: String) -> Result<()> {
|
pub fn set_device_udn(&self, devtype: &str, name: &str, udn: String) -> Result<()> {
|
||||||
self.set_value(&["devices", devtype, name, "udn"], Value::String(udn))
|
self.set_value(&["devices", devtype, name, "udn"], Value::String(udn))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the cover cache directory
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the absolute path to the cover cache directory (default: "cache_covers")
|
||||||
pub fn get_cover_cache_dir(&self) -> Result<String> {
|
pub fn get_cover_cache_dir(&self) -> Result<String> {
|
||||||
match self.get_value(&["host", "cover_cache", "directory"])? {
|
self.get_cache_dir("cover_cache", DEFAULT_COVER_CACHE_DIR)
|
||||||
Value::String(s) => Ok(s),
|
|
||||||
_ => Ok("./.pmomusic_covers".to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the cover cache directory
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `directory` - The directory path (absolute or relative to config dir)
|
||||||
pub fn set_cover_cache_dir(&self, directory: String) -> Result<()> {
|
pub fn set_cover_cache_dir(&self, directory: String) -> Result<()> {
|
||||||
self.set_value(
|
self.set_cache_dir("cover_cache", directory)
|
||||||
&["host", "cover_cache", "directory"],
|
|
||||||
Value::String(directory),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the maximum number of items in the cover cache
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the cache size (default: 2000)
|
||||||
pub fn get_cover_cache_size(&self) -> Result<usize> {
|
pub fn get_cover_cache_size(&self) -> Result<usize> {
|
||||||
match self.get_value(&["host", "cover_cache", "size"])? {
|
self.get_cache_size("cover_cache", DEFAULT_COVER_CACHE_SIZE)
|
||||||
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
|
||||||
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
|
||||||
_ => Ok(2000),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum number of items in the cover cache
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `size` - The maximum cache size
|
||||||
pub fn set_cover_cache_size(&self, size: usize) -> Result<()> {
|
pub fn set_cover_cache_size(&self, size: usize) -> Result<()> {
|
||||||
let n = Number::from(size);
|
self.set_cache_size("cover_cache", size)
|
||||||
self.set_value(&["host", "cover_cache", "size"], Value::Number(n))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the audio cache directory
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the absolute path to the audio cache directory (default: "cache_audio")
|
||||||
pub fn get_audio_cache_dir(&self) -> Result<String> {
|
pub fn get_audio_cache_dir(&self) -> Result<String> {
|
||||||
match self.get_value(&["host", "audio_cache", "directory"])? {
|
self.get_cache_dir("audio_cache", DEFAULT_AUDIO_CACHE_DIR)
|
||||||
Value::String(s) => Ok(s),
|
|
||||||
_ => Ok("./.pmomusic_audio".to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the audio cache directory
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `directory` - The directory path (absolute or relative to config dir)
|
||||||
pub fn set_audio_cache_dir(&self, directory: String) -> Result<()> {
|
pub fn set_audio_cache_dir(&self, directory: String) -> Result<()> {
|
||||||
self.set_value(
|
self.set_cache_dir("audio_cache", directory)
|
||||||
&["host", "audio_cache", "directory"],
|
|
||||||
Value::String(directory),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the maximum number of items in the audio cache
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing the cache size (default: 500)
|
||||||
pub fn get_audio_cache_size(&self) -> Result<usize> {
|
pub fn get_audio_cache_size(&self) -> Result<usize> {
|
||||||
match self.get_value(&["host", "audio_cache", "size"])? {
|
self.get_cache_size("audio_cache", DEFAULT_AUDIO_CACHE_SIZE)
|
||||||
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
|
||||||
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
|
||||||
_ => Ok(500),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum number of items in the audio cache
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `size` - The maximum cache size
|
||||||
pub fn set_audio_cache_size(&self, size: usize) -> Result<()> {
|
pub fn set_audio_cache_size(&self, size: usize) -> Result<()> {
|
||||||
let n = Number::from(size);
|
self.set_cache_size("audio_cache", size)
|
||||||
self.set_value(&["host", "audio_cache", "size"], Value::Number(n))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère le nom d'utilisateur Qobuz depuis la configuration
|
impl_string_config!(
|
||||||
pub fn get_qobuz_username(&self) -> Result<String> {
|
/// Gets the Qobuz username from configuration
|
||||||
match self.get_value(&["accounts", "qobuz", "username"])? {
|
get_qobuz_username,
|
||||||
Value::String(s) => Ok(s),
|
set_qobuz_username,
|
||||||
_ => Err(anyhow!("Qobuz username not configured")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Définit le nom d'utilisateur Qobuz dans la configuration
|
|
||||||
pub fn set_qobuz_username(&self, username: &str) -> Result<()> {
|
|
||||||
self.set_value(
|
|
||||||
&["accounts", "qobuz", "username"],
|
&["accounts", "qobuz", "username"],
|
||||||
Value::String(username.to_string()),
|
""
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Récupère le mot de passe Qobuz depuis la configuration
|
impl_string_config!(
|
||||||
pub fn get_qobuz_password(&self) -> Result<String> {
|
/// Gets the Qobuz password from configuration
|
||||||
match self.get_value(&["accounts", "qobuz", "password"])? {
|
get_qobuz_password,
|
||||||
Value::String(s) => Ok(s),
|
set_qobuz_password,
|
||||||
_ => Err(anyhow!("Qobuz password not configured")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Définit le mot de passe Qobuz dans la configuration
|
|
||||||
pub fn set_qobuz_password(&self, password: &str) -> Result<()> {
|
|
||||||
self.set_value(
|
|
||||||
&["accounts", "qobuz", "password"],
|
&["accounts", "qobuz", "password"],
|
||||||
Value::String(password.to_string()),
|
""
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Récupère les credentials Qobuz (username + password) depuis la configuration
|
/// Gets the Qobuz credentials (username and password) from configuration
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns a `Result` containing a tuple of (username, password)
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if either username or password is not configured
|
||||||
pub fn get_qobuz_credentials(&self) -> Result<(String, String)> {
|
pub fn get_qobuz_credentials(&self) -> Result<(String, String)> {
|
||||||
let username = self.get_qobuz_username()?;
|
let username = self.get_qobuz_username()?;
|
||||||
let password = self.get_qobuz_password()?;
|
let password = self.get_qobuz_password()?;
|
||||||
Ok((username, password))
|
Ok((username, password))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_log_cache_size(&self) -> Result<usize> {
|
impl_usize_config!(get_log_cache_size, set_log_cache_size, &["host", "logger", "buffer_capacity"], DEFAULT_LOG_BUFFER_CAPACITY);
|
||||||
match self.get_value(&["host", "logger", "buffer_capacity"])? {
|
|
||||||
Value::Number(n) => n
|
|
||||||
.as_u64()
|
|
||||||
.map(|v| v as usize)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Number is not an unsigned integer")),
|
|
||||||
_ => Ok(1000),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_log_cache_size(&self, size: usize) -> Result<()> {
|
impl_bool_config!(get_log_enable_console, set_log_enable_console, &["host", "logger", "enable_console"], DEFAULT_LOG_ENABLE_CONSOLE);
|
||||||
let n = Number::from(size);
|
|
||||||
self.set_value(&["host", "logger", "buffer_capacity"], Value::Number(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_log_enable_console(&self) -> Result<bool> {
|
|
||||||
match self.get_value(&["host", "logger", "enable_console"])? {
|
|
||||||
Value::Bool(b) => Ok(b),
|
|
||||||
_ => Ok(true),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_log_enable_console(&self, enable: bool) -> Result<()> {
|
|
||||||
self.set_value(&["host", "logger", "enable_console"], Value::Bool(enable))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Récupère le niveau de log minimum depuis la configuration
|
||||||
pub fn get_log_min_level(&self) -> Result<String> {
|
pub fn get_log_min_level(&self) -> Result<String> {
|
||||||
match self.get_value(&["host", "logger", "min_level"])? {
|
match self.get_value(&["host", "logger", "min_level"])? {
|
||||||
Value::String(s) => Ok(s),
|
Value::String(s) => Ok(s),
|
||||||
_ => Ok("TRACE".to_string()),
|
_ => Ok(DEFAULT_LOG_MIN_LEVEL.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Définit le niveau de log minimum dans la configuration
|
||||||
pub fn set_log_min_level(&self, level: String) -> Result<()> {
|
pub fn set_log_min_level(&self, level: String) -> Result<()> {
|
||||||
self.set_value(&["host", "logger", "min_level"], Value::String(level))
|
self.set_value(&["host", "logger", "min_level"], Value::String(level))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne l'instance globale
|
/// Returns the global configuration instance
|
||||||
|
///
|
||||||
|
/// This function provides access to the singleton configuration instance,
|
||||||
|
/// which is lazily loaded on first access.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// An `Arc<Config>` pointing to the global configuration
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use pmoconfig::get_config;
|
||||||
|
///
|
||||||
|
/// let config = get_config();
|
||||||
|
/// let port = config.get_http_port();
|
||||||
|
/// ```
|
||||||
pub fn get_config() -> Arc<Config> {
|
pub fn get_config() -> Arc<Config> {
|
||||||
CONFIG.clone()
|
CONFIG.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Merges external YAML configuration into default configuration
|
||||||
|
///
|
||||||
|
/// This function recursively merges two YAML value trees:
|
||||||
|
/// - For mappings (objects), it merges keys from external into default
|
||||||
|
/// - For scalars and sequences, external values replace default values
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `default` - The default configuration to merge into (modified in place)
|
||||||
|
/// * `external` - The external configuration to merge from
|
||||||
fn merge_yaml(default: &mut Value, external: &Value) {
|
fn merge_yaml(default: &mut Value, external: &Value) {
|
||||||
match (default, external) {
|
match (default, external) {
|
||||||
(Value::Mapping(dmap), Value::Mapping(emap)) => {
|
(Value::Mapping(dmap), Value::Mapping(emap)) => {
|
||||||
|
|||||||
29
pmoconfig/src/openapi.rs
Normal file
29
pmoconfig/src/openapi.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
info(
|
||||||
|
title = "PMOMusic Configuration API",
|
||||||
|
version = "0.1.0",
|
||||||
|
description = "API REST pour gérer la configuration de PMOMusic",
|
||||||
|
contact(
|
||||||
|
name = "PMOMusic Team",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
paths(
|
||||||
|
crate::api::get_full_config,
|
||||||
|
crate::api::get_config_value,
|
||||||
|
crate::api::update_config_value,
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(
|
||||||
|
crate::api::ConfigValue,
|
||||||
|
crate::api::UpdateConfigRequest,
|
||||||
|
crate::api::UpdateConfigResponse,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = "config", description = "Endpoints de gestion de la configuration")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct ApiDoc;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
host:
|
host:
|
||||||
http_port: "8080"
|
http_port: "8080"
|
||||||
cover_cache:
|
cover_cache:
|
||||||
directory: "./.pmomusic_covers"
|
directory: "cache_covers"
|
||||||
size: 2000
|
size: 2000
|
||||||
audio_cache:
|
audio_cache:
|
||||||
directory: "./.pmomusic_audio"
|
directory: "cache_audio"
|
||||||
size: 500
|
size: 500
|
||||||
logger:
|
logger:
|
||||||
buffer_capacity: 200
|
buffer_capacity: 200
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pmoconfig = { path = "../pmoconfig" }
|
pmoconfig = { path = "../pmoconfig", features = ["api"] }
|
||||||
|
|
||||||
|
anyhow = "1.0"
|
||||||
axum = "0.8.4"
|
axum = "0.8.4"
|
||||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
|
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|||||||
49
pmoserver/src/config_ext.rs
Normal file
49
pmoserver/src/config_ext.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! Extension pour intégrer l'API de configuration de pmoconfig dans pmoserver
|
||||||
|
//!
|
||||||
|
//! Ce module fournit le trait `ConfigExt` qui permet d'ajouter facilement
|
||||||
|
//! l'API REST de configuration au serveur.
|
||||||
|
|
||||||
|
use crate::Server;
|
||||||
|
use anyhow::Result;
|
||||||
|
use pmoconfig::{api, get_config, ApiDoc};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
/// Trait d'extension pour ajouter l'API de configuration à pmoserver
|
||||||
|
pub trait ConfigExt {
|
||||||
|
/// Initialise l'API de configuration et enregistre les routes HTTP
|
||||||
|
///
|
||||||
|
/// # Routes enregistrées
|
||||||
|
///
|
||||||
|
/// - `GET /api/config` - Récupérer toute la configuration
|
||||||
|
/// - `GET /api/config/{path}` - Récupérer une valeur spécifique (ex: host.http_port)
|
||||||
|
/// - `POST /api/config` - Mettre à jour une valeur
|
||||||
|
/// - `GET /swagger-ui/config` - Documentation interactive Swagger
|
||||||
|
///
|
||||||
|
/// # Exemple
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// use pmoserver::{ServerBuilder, ConfigExt};
|
||||||
|
///
|
||||||
|
/// #[tokio::main]
|
||||||
|
/// async fn main() -> anyhow::Result<()> {
|
||||||
|
/// let mut server = ServerBuilder::new_configured().build();
|
||||||
|
/// server.init_config_api().await?;
|
||||||
|
/// server.start().await;
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
async fn init_config_api(&mut self) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigExt for Server {
|
||||||
|
async fn init_config_api(&mut self) -> Result<()> {
|
||||||
|
let config = get_config();
|
||||||
|
|
||||||
|
// API REST pour la configuration
|
||||||
|
let api_router = api::create_router(config);
|
||||||
|
let openapi = ApiDoc::openapi();
|
||||||
|
self.add_openapi(api_router, openapi, "config").await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,9 +68,11 @@
|
|||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
pub mod config_ext;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
pub use config_ext::ConfigExt;
|
||||||
pub use logs::{
|
pub use logs::{
|
||||||
LogState, LoggingOptions, LogsApiDoc, SseLayer, create_logs_router, init_logging, log_dump,
|
LogState, LoggingOptions, LogsApiDoc, SseLayer, create_logs_router, init_logging, log_dump,
|
||||||
log_setup_get, log_setup_post, log_sse,
|
log_setup_get, log_setup_post, log_sse,
|
||||||
|
|||||||
Reference in New Issue
Block a user