encore des problèmes de configuration
This commit is contained in:
47
Cargo.lock
generated
47
Cargo.lock
generated
@@ -447,9 +447,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "built"
|
name = "built"
|
||||||
version = "0.7.7"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
|
checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
@@ -633,7 +633,16 @@ version = "2.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23"
|
checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc-catalog",
|
"crc-catalog 1.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
|
||||||
|
dependencies = [
|
||||||
|
"crc-catalog 2.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -642,6 +651,12 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403"
|
checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc-catalog"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -998,7 +1013,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "fb6da14d3c6605689b5c9ed5187a5218a6d3888e14b747bc18fd4e4bafd452bd"
|
checksum = "fb6da14d3c6605689b5c9ed5187a5218a6d3888e14b747bc18fd4e4bafd452bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"built",
|
"built",
|
||||||
"crc",
|
"crc 2.1.0",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"heapless",
|
||||||
|
"log",
|
||||||
|
"md-5",
|
||||||
|
"num-traits",
|
||||||
|
"rustversion",
|
||||||
|
"seq-macro",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flacenc"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22846e369206f5c3caa4c59071041fe70c1c2863ebdd2341e6ad17490d53772b"
|
||||||
|
dependencies = [
|
||||||
|
"built",
|
||||||
|
"crc 3.3.0",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"heapless",
|
"heapless",
|
||||||
"log",
|
"log",
|
||||||
@@ -2359,7 +2392,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"claxon",
|
"claxon",
|
||||||
"flacenc",
|
"flacenc 0.4.0",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"lofty",
|
"lofty",
|
||||||
"pmocache",
|
"pmocache",
|
||||||
@@ -2498,7 +2531,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"claxon",
|
"claxon",
|
||||||
"flacenc",
|
"flacenc 0.5.0",
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"hound",
|
"hound",
|
||||||
@@ -2516,7 +2549,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|||||||
@@ -461,7 +461,10 @@ impl Config {
|
|||||||
pub fn get_managed_dir(&self, path: &[&str], default: &str) -> Result<String> {
|
pub fn get_managed_dir(&self, path: &[&str], default: &str) -> Result<String> {
|
||||||
let dir_path = match self.get_value(path) {
|
let dir_path = match self.get_value(path) {
|
||||||
Ok(Value::String(s)) => s,
|
Ok(Value::String(s)) => s,
|
||||||
_ => default.to_string(),
|
_ => {
|
||||||
|
self.set_managed_dir(path, default.to_string())?;
|
||||||
|
default.to_string()
|
||||||
|
},
|
||||||
};
|
};
|
||||||
self.resolve_and_create_dir(&dir_path)
|
self.resolve_and_create_dir(&dir_path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
# Sérialisation/Désérialisation JSON
|
# Sérialisation/Désérialisation JSON
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9.33"
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
@@ -31,13 +31,13 @@ async-stream = "0.3"
|
|||||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||||
|
|
||||||
# Gestion des erreurs
|
# Gestion des erreurs
|
||||||
thiserror = "1.0"
|
thiserror = "2.0.17"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
||||||
# Streaming de bytes
|
# Streaming de bytes
|
||||||
bytes = "1.5"
|
bytes = "1.5"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
flacenc = "0.4"
|
flacenc = "0.5.0"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
//! Ce module fournit le trait `RadioParadiseConfigExt` qui permet d'ajouter facilement
|
//! Ce module fournit le trait `RadioParadiseConfigExt` qui permet d'ajouter facilement
|
||||||
//! des méthodes de gestion de la configuration Radio Paradise à pmoconfig::Config.
|
//! des méthodes de gestion de la configuration Radio Paradise à pmoconfig::Config.
|
||||||
//!
|
//!
|
||||||
|
//! La configuration est minimale - seulement ce qui doit vraiment être configurable :
|
||||||
|
//! - Activation/désactivation de la source
|
||||||
|
//! - Chemin de la base de données d'historique
|
||||||
|
//! - Taille maximale de l'historique
|
||||||
|
//!
|
||||||
|
//! Tous les autres paramètres (polling, timeouts, etc.) sont des constantes
|
||||||
|
//! définies dans `paradise::constants`.
|
||||||
|
//!
|
||||||
//! # Exemple
|
//! # Exemple
|
||||||
//!
|
//!
|
||||||
//! ```rust,ignore
|
//! ```rust,ignore
|
||||||
@@ -10,24 +18,41 @@
|
|||||||
//! use pmoparadise::RadioParadiseConfigExt;
|
//! use pmoparadise::RadioParadiseConfigExt;
|
||||||
//!
|
//!
|
||||||
//! let config = get_config();
|
//! let config = get_config();
|
||||||
//! let history_db = config.get_paradise_history_database()?;
|
//!
|
||||||
//! let history_size = config.get_paradise_history_size()?;
|
//! // Check if enabled
|
||||||
|
//! if !config.get_paradise_enabled()? {
|
||||||
|
//! println!("Radio Paradise is disabled");
|
||||||
|
//! return Ok(());
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Get configuration
|
||||||
|
//! let db_path = config.get_paradise_history_database()?;
|
||||||
|
//! let max_tracks = config.get_paradise_history_size()?;
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use anyhow::Result;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use pmoconfig::Config;
|
use pmoconfig::Config;
|
||||||
use serde_yaml::{Number, Value};
|
use serde_yaml::{Number, Value};
|
||||||
|
|
||||||
/// Chemin par défaut de la base de données d'historique (relatif au config_dir)
|
use crate::paradise::constants;
|
||||||
const DEFAULT_HISTORY_DATABASE: &str = "paradise_history.db";
|
|
||||||
|
|
||||||
/// Nombre maximal par défaut de pistes dans l'historique
|
/// Nom du répertoire pour Radio Paradise (relatif au config_dir)
|
||||||
const DEFAULT_HISTORY_SIZE: usize = 100;
|
///
|
||||||
|
/// La base de données sera stockée dans `<config_dir>/paradise/history.db`
|
||||||
|
const DEFAULT_HISTORY_DATABASE_DIR: &str = "paradise";
|
||||||
|
|
||||||
/// Trait d'extension pour gérer la configuration Radio Paradise dans pmoconfig
|
/// Trait d'extension pour gérer la configuration Radio Paradise dans pmoconfig
|
||||||
///
|
///
|
||||||
/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
|
/// Ce trait étend `pmoconfig::Config` avec des méthodes spécifiques
|
||||||
/// à la configuration de Radio Paradise (historique, etc.).
|
/// à la configuration minimale de Radio Paradise.
|
||||||
|
///
|
||||||
|
/// # Auto-persist des valeurs par défaut
|
||||||
|
///
|
||||||
|
/// Tous les getters persistent automatiquement la valeur par défaut dans la
|
||||||
|
/// configuration si elle n'existe pas encore. Cela permet à l'utilisateur de
|
||||||
|
/// voir la configuration effective dans le fichier YAML et de la modifier facilement.
|
||||||
///
|
///
|
||||||
/// # Exemple
|
/// # Exemple
|
||||||
///
|
///
|
||||||
@@ -37,30 +62,62 @@ const DEFAULT_HISTORY_SIZE: usize = 100;
|
|||||||
///
|
///
|
||||||
/// let config = get_config();
|
/// let config = get_config();
|
||||||
///
|
///
|
||||||
/// // Récupérer le chemin de la base de données d'historique
|
/// // Premier appel : persiste "enabled: true" dans la config et retourne true
|
||||||
/// let db_path = config.get_paradise_history_database()?;
|
/// let enabled = config.get_paradise_enabled()?;
|
||||||
/// println!("History database: {}", db_path);
|
|
||||||
///
|
///
|
||||||
/// // Récupérer la taille maximale de l'historique
|
/// // Premier appel : persiste "max_tracks: 100" dans la config et retourne 100
|
||||||
/// let max_tracks = config.get_paradise_history_size()?;
|
/// let max_tracks = config.get_paradise_history_size()?;
|
||||||
/// println!("Max history tracks: {}", max_tracks);
|
///
|
||||||
|
/// // L'utilisateur peut maintenant éditer ces valeurs dans le fichier YAML
|
||||||
/// ```
|
/// ```
|
||||||
pub trait RadioParadiseConfigExt {
|
pub trait RadioParadiseConfigExt {
|
||||||
/// Récupère le chemin de la base de données d'historique
|
/// Vérifie si Radio Paradise est activé
|
||||||
///
|
|
||||||
/// Le chemin retourné est absolu, mais peut être configuré de manière relative
|
|
||||||
/// au répertoire de configuration (via `get_managed_dir`).
|
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// Le chemin absolu vers la base de données SQLite d'historique
|
/// `true` si la source est activée (default), `false` sinon.
|
||||||
/// (default: `<config_dir>/paradise_history.db`)
|
///
|
||||||
|
/// Si la valeur n'existe pas dans la configuration, elle est automatiquement
|
||||||
|
/// définie à `true` (activé par défaut) et persistée.
|
||||||
|
///
|
||||||
|
/// # Exemple
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// if config.get_paradise_enabled()? {
|
||||||
|
/// // Initialize Radio Paradise...
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn get_paradise_enabled(&self) -> Result<bool>;
|
||||||
|
|
||||||
|
/// Active ou désactive Radio Paradise
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `enabled` - `true` pour activer, `false` pour désactiver
|
||||||
|
///
|
||||||
|
/// # Exemple
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// // Disable Radio Paradise
|
||||||
|
/// config.set_paradise_enabled(false)?;
|
||||||
|
/// ```
|
||||||
|
fn set_paradise_enabled(&self, enabled: bool) -> Result<()>;
|
||||||
|
|
||||||
|
/// Récupère le chemin de la base de données d'historique
|
||||||
|
///
|
||||||
|
/// Le chemin retourné est absolu et pointe vers `<config_dir>/paradise/history.db`.
|
||||||
|
/// Le répertoire `paradise` est créé automatiquement s'il n'existe pas.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Le chemin absolu vers la base de données SQLite d'historique.
|
||||||
|
/// Exemple: `/home/user/.config/pmo/paradise/history.db`
|
||||||
///
|
///
|
||||||
/// # Exemple
|
/// # Exemple
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// let db_path = config.get_paradise_history_database()?;
|
/// let db_path = config.get_paradise_history_database()?;
|
||||||
/// // Exemple: "/home/user/.config/pmo/paradise_history.db"
|
/// let backend = SqliteHistoryBackend::new(&db_path)?;
|
||||||
/// ```
|
/// ```
|
||||||
fn get_paradise_history_database(&self) -> Result<String>;
|
fn get_paradise_history_database(&self) -> Result<String>;
|
||||||
|
|
||||||
@@ -68,16 +125,15 @@ pub trait RadioParadiseConfigExt {
|
|||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `path` - Chemin de la base de données (absolu ou relatif au config_dir)
|
/// * `path` - Chemin complet vers la base de données (doit inclure le nom du fichier)
|
||||||
|
///
|
||||||
|
/// Le répertoire parent sera extrait et stocké dans la configuration.
|
||||||
///
|
///
|
||||||
/// # Exemple
|
/// # Exemple
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// // Chemin relatif au config_dir
|
/// // Set custom path
|
||||||
/// config.set_paradise_history_database("my_paradise.db".to_string())?;
|
/// config.set_paradise_history_database("/var/lib/pmo/paradise.db".to_string())?;
|
||||||
///
|
|
||||||
/// // Ou chemin absolu
|
|
||||||
/// config.set_paradise_history_database("/var/lib/paradise.db".to_string())?;
|
|
||||||
/// ```
|
/// ```
|
||||||
fn set_paradise_history_database(&self, path: String) -> Result<()>;
|
fn set_paradise_history_database(&self, path: String) -> Result<()>;
|
||||||
|
|
||||||
@@ -85,7 +141,10 @@ pub trait RadioParadiseConfigExt {
|
|||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// Le nombre maximal de pistes à conserver dans l'historique (default: 100)
|
/// Le nombre maximal de pistes à conserver dans l'historique.
|
||||||
|
///
|
||||||
|
/// Si la valeur n'existe pas dans la configuration, elle est automatiquement
|
||||||
|
/// définie à la constante `HISTORY_DEFAULT_MAX_TRACKS` (100) et persistée.
|
||||||
///
|
///
|
||||||
/// # Exemple
|
/// # Exemple
|
||||||
///
|
///
|
||||||
@@ -104,32 +163,69 @@ pub trait RadioParadiseConfigExt {
|
|||||||
/// # Exemple
|
/// # Exemple
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// // Conserver les 200 dernières pistes
|
/// // Keep last 200 tracks
|
||||||
/// config.set_paradise_history_size(200)?;
|
/// config.set_paradise_history_size(200)?;
|
||||||
/// ```
|
/// ```
|
||||||
fn set_paradise_history_size(&self, size: usize) -> Result<()>;
|
fn set_paradise_history_size(&self, size: usize) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RadioParadiseConfigExt for Config {
|
impl RadioParadiseConfigExt for Config {
|
||||||
fn get_paradise_history_database(&self) -> Result<String> {
|
fn get_paradise_enabled(&self) -> Result<bool> {
|
||||||
// Utilise get_managed_dir qui gère automatiquement les chemins
|
match self.get_value(&["sources", "radio_paradise", "enabled"]) {
|
||||||
// relatifs au config_dir et les chemins absolus
|
Ok(Value::Bool(b)) => Ok(b),
|
||||||
self.get_managed_dir(
|
_ => {
|
||||||
&["sources", "radio_paradise", "history", "database"],
|
// Use default (enabled) and persist it
|
||||||
DEFAULT_HISTORY_DATABASE,
|
self.set_paradise_enabled(true)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_paradise_enabled(&self, enabled: bool) -> Result<()> {
|
||||||
|
self.set_value(
|
||||||
|
&["sources", "radio_paradise", "enabled"],
|
||||||
|
Value::Bool(enabled),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_paradise_history_database(&self) -> Result<String> {
|
||||||
|
// Get managed directory: ~/.config/pmo/paradise/
|
||||||
|
let dir = self.get_managed_dir(
|
||||||
|
&["sources", "radio_paradise", "database"],
|
||||||
|
DEFAULT_HISTORY_DATABASE_DIR,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
std::fs::create_dir_all(&dir)?;
|
||||||
|
|
||||||
|
// Build full path: ~/.config/pmo/paradise/history.db
|
||||||
|
let mut path = PathBuf::from(dir);
|
||||||
|
path.push("history.db");
|
||||||
|
|
||||||
|
Ok(path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn set_paradise_history_database(&self, path: String) -> Result<()> {
|
fn set_paradise_history_database(&self, path: String) -> Result<()> {
|
||||||
self.set_managed_dir(&["sources", "radio_paradise", "history", "database"], path)
|
// Extract parent directory from the full path
|
||||||
|
match PathBuf::from(&path).parent() {
|
||||||
|
Some(dir) => self.set_managed_dir(
|
||||||
|
&["sources", "radio_paradise", "database"],
|
||||||
|
dir.to_string_lossy().to_string(),
|
||||||
|
),
|
||||||
|
None => Err(anyhow!("Invalid database path: no parent directory")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_paradise_history_size(&self) -> Result<usize> {
|
fn get_paradise_history_size(&self) -> Result<usize> {
|
||||||
// Tente de lire depuis la configuration YAML
|
|
||||||
match self.get_value(&["sources", "radio_paradise", "history", "max_tracks"]) {
|
match self.get_value(&["sources", "radio_paradise", "history", "max_tracks"]) {
|
||||||
Ok(Value::Number(n)) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
Ok(Value::Number(n)) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
||||||
Ok(Value::Number(n)) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
Ok(Value::Number(n)) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
||||||
_ => Ok(DEFAULT_HISTORY_SIZE),
|
_ => {
|
||||||
|
// Use default and persist it
|
||||||
|
let default = constants::HISTORY_DEFAULT_MAX_TRACKS;
|
||||||
|
self.set_paradise_history_size(default)?;
|
||||||
|
Ok(default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,8 +244,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_default_values() {
|
fn test_default_values() {
|
||||||
// Les valeurs par défaut doivent être cohérentes
|
assert_eq!(DEFAULT_HISTORY_DATABASE_DIR, "paradise");
|
||||||
assert_eq!(DEFAULT_HISTORY_DATABASE, "paradise_history.db");
|
assert_eq!(constants::HISTORY_DEFAULT_MAX_TRACKS, 100);
|
||||||
assert_eq!(DEFAULT_HISTORY_SIZE, 100);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_database_path_construction() {
|
||||||
|
// Simulating path construction
|
||||||
|
let base = "/home/user/.config/pmo/paradise";
|
||||||
|
let mut path = PathBuf::from(base);
|
||||||
|
path.push("history.db");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
path.to_string_lossy(),
|
||||||
|
"/home/user/.config/pmo/paradise/history.db"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
//! a scaffolding of the final behaviour; commands sent to the worker are
|
//! a scaffolding of the final behaviour; commands sent to the worker are
|
||||||
//! logged but not yet executing the full download/buffering pipeline.
|
//! logged but not yet executing the full download/buffering pipeline.
|
||||||
|
|
||||||
use super::config::RadioParadiseConfig;
|
|
||||||
use super::history::HistoryBackend;
|
use super::history::HistoryBackend;
|
||||||
use super::playlist::{PlaylistEntry, SharedPlaylist};
|
use super::playlist::{PlaylistEntry, SharedPlaylist};
|
||||||
use super::worker::{ParadiseWorker, WorkerCommand};
|
use super::worker::{ParadiseWorker, WorkerCommand};
|
||||||
@@ -128,7 +127,7 @@ pub struct ParadiseChannel {
|
|||||||
struct ParadiseChannelInner {
|
struct ParadiseChannelInner {
|
||||||
descriptor: ChannelDescriptor,
|
descriptor: ChannelDescriptor,
|
||||||
client: RadioParadiseClient,
|
client: RadioParadiseClient,
|
||||||
config: Arc<RadioParadiseConfig>,
|
history_max_tracks: usize,
|
||||||
playlist: SharedPlaylist,
|
playlist: SharedPlaylist,
|
||||||
history: Arc<dyn HistoryBackend>,
|
history: Arc<dyn HistoryBackend>,
|
||||||
cache_manager: Arc<SourceCacheManager>,
|
cache_manager: Arc<SourceCacheManager>,
|
||||||
@@ -154,16 +153,16 @@ impl ParadiseChannel {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
descriptor: ChannelDescriptor,
|
descriptor: ChannelDescriptor,
|
||||||
base_client: RadioParadiseClient,
|
base_client: RadioParadiseClient,
|
||||||
config: Arc<RadioParadiseConfig>,
|
history_max_tracks: usize,
|
||||||
history: Arc<dyn HistoryBackend>,
|
history: Arc<dyn HistoryBackend>,
|
||||||
cache_manager: Arc<SourceCacheManager>,
|
cache_manager: Arc<SourceCacheManager>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let client = base_client.clone_with_channel(descriptor.id);
|
let client = base_client.clone_with_channel(descriptor.id);
|
||||||
let playlist = SharedPlaylist::new(config.history.max_tracks);
|
let playlist = SharedPlaylist::new(history_max_tracks);
|
||||||
let (worker, worker_tx) = ParadiseWorker::spawn(
|
let (worker, worker_tx) = ParadiseWorker::spawn(
|
||||||
descriptor,
|
descriptor,
|
||||||
client.clone(),
|
client.clone(),
|
||||||
config.clone(),
|
history_max_tracks,
|
||||||
playlist.clone(),
|
playlist.clone(),
|
||||||
history.clone(),
|
history.clone(),
|
||||||
cache_manager.clone(),
|
cache_manager.clone(),
|
||||||
@@ -173,7 +172,7 @@ impl ParadiseChannel {
|
|||||||
inner: Arc::new(ParadiseChannelInner {
|
inner: Arc::new(ParadiseChannelInner {
|
||||||
descriptor,
|
descriptor,
|
||||||
client,
|
client,
|
||||||
config,
|
history_max_tracks,
|
||||||
playlist,
|
playlist,
|
||||||
history,
|
history,
|
||||||
cache_manager,
|
cache_manager,
|
||||||
@@ -192,8 +191,8 @@ impl ParadiseChannel {
|
|||||||
&self.inner.playlist
|
&self.inner.playlist
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config(&self) -> &Arc<RadioParadiseConfig> {
|
pub fn history_max_tracks(&self) -> usize {
|
||||||
&self.inner.config
|
self.inner.history_max_tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn history_backend(&self) -> &Arc<dyn HistoryBackend> {
|
pub fn history_backend(&self) -> &Arc<dyn HistoryBackend> {
|
||||||
@@ -297,7 +296,7 @@ impl ParadiseChannel {
|
|||||||
if let Err(err) = self
|
if let Err(err) = self
|
||||||
.inner
|
.inner
|
||||||
.history
|
.history
|
||||||
.truncate(self.inner.config.history.max_tracks)
|
.truncate(self.inner.history_max_tracks)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
warn!(
|
warn!(
|
||||||
|
|||||||
@@ -1,310 +0,0 @@
|
|||||||
//! Configuration structures for the Radio Paradise orchestration layer.
|
|
||||||
//!
|
|
||||||
//! The YAML schema is described in the functional specification. We expose
|
|
||||||
//! strongly typed structs with sensible defaults so the rest of the crate can
|
|
||||||
//! depend on a stable configuration shape irrespective of how the data is
|
|
||||||
//! loaded (embedded defaults, pmoconfig overrides, tests, etc.).
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
/// Top-level configuration block.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct RadioParadiseConfig {
|
|
||||||
#[serde(default)]
|
|
||||||
pub enabled: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub channels: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub cache: CacheConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub history: HistoryConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub activity: ActivityConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub polling: PollingConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub stream: StreamConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub api: ApiConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RadioParadiseConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enabled: true,
|
|
||||||
channels: vec![
|
|
||||||
"main".to_string(),
|
|
||||||
"mellow".to_string(),
|
|
||||||
"rock".to_string(),
|
|
||||||
"eclectic".to_string(),
|
|
||||||
],
|
|
||||||
cache: CacheConfig::default(),
|
|
||||||
history: HistoryConfig::default(),
|
|
||||||
activity: ActivityConfig::default(),
|
|
||||||
polling: PollingConfig::default(),
|
|
||||||
stream: StreamConfig::default(),
|
|
||||||
api: ApiConfig::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cache related parameters.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CacheConfig {
|
|
||||||
#[serde(default = "CacheConfig::default_max_blocks")]
|
|
||||||
pub max_blocks_remembered: usize,
|
|
||||||
#[serde(default = "CacheConfig::default_track_id_bytes")]
|
|
||||||
pub track_id_hash_bytes: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CacheConfig {
|
|
||||||
const fn default_max_blocks() -> usize {
|
|
||||||
5
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_track_id_bytes() -> usize {
|
|
||||||
512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CacheConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
max_blocks_remembered: Self::default_max_blocks(),
|
|
||||||
track_id_hash_bytes: Self::default_track_id_bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Persisted history tuning.
|
|
||||||
///
|
|
||||||
/// Configuration for the SQLite-based history persistence.
|
|
||||||
/// The database path is managed via `RadioParadiseConfigExt` trait
|
|
||||||
/// and defaults to a relative path in the config directory.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct HistoryConfig {
|
|
||||||
#[serde(default = "HistoryConfig::default_max_tracks")]
|
|
||||||
pub max_tracks: usize,
|
|
||||||
#[serde(default = "HistoryConfig::default_database_path")]
|
|
||||||
pub database_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HistoryConfig {
|
|
||||||
const fn default_max_tracks() -> usize {
|
|
||||||
100
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_database_path() -> String {
|
|
||||||
// Default to a relative path (will be resolved by config_ext)
|
|
||||||
"paradise_history.db".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for HistoryConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
max_tracks: Self::default_max_tracks(),
|
|
||||||
database_path: Self::default_database_path(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RadioParadiseConfig {
|
|
||||||
pub fn load_from_pmoconfig() -> anyhow::Result<Self> {
|
|
||||||
let cfg = pmoconfig::get_config();
|
|
||||||
match cfg.get_value(&["sources", "radio_paradise"]) {
|
|
||||||
Ok(value) => Ok(serde_yaml::from_value(value).unwrap_or_default()),
|
|
||||||
Err(_) => Ok(Self::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Activity lifecycle tuning.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ActivityConfig {
|
|
||||||
#[serde(default = "ActivityConfig::default_cooling_timeout")]
|
|
||||||
pub cooling_timeout_seconds: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActivityConfig {
|
|
||||||
const fn default_cooling_timeout() -> u64 {
|
|
||||||
180
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cooling_timeout(&self) -> Duration {
|
|
||||||
Duration::from_secs(self.cooling_timeout_seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ActivityConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
cooling_timeout_seconds: Self::default_cooling_timeout(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Polling strategy configuration.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PollingConfig {
|
|
||||||
#[serde(default = "PollingConfig::default_interval_high")]
|
|
||||||
pub interval_high_buffer: u64,
|
|
||||||
#[serde(default = "PollingConfig::default_interval_medium")]
|
|
||||||
pub interval_medium_buffer: u64,
|
|
||||||
#[serde(default = "PollingConfig::default_interval_low")]
|
|
||||||
pub interval_low_buffer: u64,
|
|
||||||
#[serde(default)]
|
|
||||||
pub backoff_on_error: PollingBackoffConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PollingConfig {
|
|
||||||
const fn default_interval_high() -> u64 {
|
|
||||||
120
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_interval_medium() -> u64 {
|
|
||||||
60
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_interval_low() -> u64 {
|
|
||||||
20
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn high_interval(&self) -> Duration {
|
|
||||||
Duration::from_secs(self.interval_high_buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn medium_interval(&self) -> Duration {
|
|
||||||
Duration::from_secs(self.interval_medium_buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn low_interval(&self) -> Duration {
|
|
||||||
Duration::from_secs(self.interval_low_buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PollingConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
interval_high_buffer: Self::default_interval_high(),
|
|
||||||
interval_medium_buffer: Self::default_interval_medium(),
|
|
||||||
interval_low_buffer: Self::default_interval_low(),
|
|
||||||
backoff_on_error: PollingBackoffConfig::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Backoff policy for API errors.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct PollingBackoffConfig {
|
|
||||||
#[serde(default = "PollingBackoffConfig::default_initial")]
|
|
||||||
pub initial: u64,
|
|
||||||
#[serde(default = "PollingBackoffConfig::default_max")]
|
|
||||||
pub max: u64,
|
|
||||||
#[serde(default = "PollingBackoffConfig::default_multiplier")]
|
|
||||||
pub multiplier: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PollingBackoffConfig {
|
|
||||||
const fn default_initial() -> u64 {
|
|
||||||
20
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_max() -> u64 {
|
|
||||||
300
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_multiplier() -> f32 {
|
|
||||||
2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PollingBackoffConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
initial: Self::default_initial(),
|
|
||||||
max: Self::default_max(),
|
|
||||||
multiplier: Self::default_multiplier(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streaming pipeline configuration.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct StreamConfig {
|
|
||||||
#[serde(default = "StreamConfig::default_metadata_format")]
|
|
||||||
pub metadata_format: MetadataFormat,
|
|
||||||
#[serde(default)]
|
|
||||||
pub enable_gapless: bool,
|
|
||||||
#[serde(default = "StreamConfig::default_buffer_size")]
|
|
||||||
pub buffer_size_bytes: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StreamConfig {
|
|
||||||
fn default_metadata_format() -> MetadataFormat {
|
|
||||||
MetadataFormat::Icy
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_buffer_size() -> usize {
|
|
||||||
64 * 1024
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for StreamConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
metadata_format: MetadataFormat::Icy,
|
|
||||||
enable_gapless: true,
|
|
||||||
buffer_size_bytes: Self::default_buffer_size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Metadata transport for streaming.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum MetadataFormat {
|
|
||||||
Icy,
|
|
||||||
#[serde(other)]
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remote API tuning (timeouts, UA, etc.).
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ApiConfig {
|
|
||||||
#[serde(default = "ApiConfig::default_base_url")]
|
|
||||||
pub base_url: String,
|
|
||||||
#[serde(default = "ApiConfig::default_timeout")]
|
|
||||||
pub timeout_seconds: u64,
|
|
||||||
#[serde(default = "ApiConfig::default_user_agent")]
|
|
||||||
pub user_agent: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiConfig {
|
|
||||||
fn default_base_url() -> String {
|
|
||||||
"https://api.radioparadise.com".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_timeout() -> u64 {
|
|
||||||
30
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_user_agent() -> String {
|
|
||||||
"PMO-RadioParadise/1.0".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timeout(&self) -> Duration {
|
|
||||||
Duration::from_secs(self.timeout_seconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ApiConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
base_url: Self::default_base_url(),
|
|
||||||
timeout_seconds: Self::default_timeout(),
|
|
||||||
user_agent: Self::default_user_agent(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
208
pmoparadise/src/paradise/constants.rs
Normal file
208
pmoparadise/src/paradise/constants.rs
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
//! Constants for Radio Paradise orchestration layer.
|
||||||
|
//!
|
||||||
|
//! This module defines all the hardcoded parameters for the Radio Paradise
|
||||||
|
//! integration. These values are based on empirical testing and Radio Paradise's
|
||||||
|
//! infrastructure characteristics.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Activity Lifecycle
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Cooling timeout after all clients disconnect (seconds)
|
||||||
|
///
|
||||||
|
/// After the last client disconnects, the channel enters a "cooling" state
|
||||||
|
/// where it remains active for this duration before shutting down completely.
|
||||||
|
/// This avoids rapid start/stop cycles if clients reconnect quickly.
|
||||||
|
///
|
||||||
|
/// Value: 180 seconds (3 minutes) - good balance between responsiveness and stability
|
||||||
|
pub const COOLING_TIMEOUT_SECONDS: u64 = 180;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Polling Intervals
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// High buffer polling interval (seconds)
|
||||||
|
///
|
||||||
|
/// When the playlist buffer has 3+ blocks, poll less frequently to reduce
|
||||||
|
/// API load and network usage.
|
||||||
|
///
|
||||||
|
/// Value: 120 seconds (2 minutes)
|
||||||
|
pub const POLLING_INTERVAL_HIGH_BUFFER: u64 = 120;
|
||||||
|
|
||||||
|
/// Medium buffer polling interval (seconds)
|
||||||
|
///
|
||||||
|
/// When the playlist buffer has 2 blocks, poll at moderate frequency.
|
||||||
|
///
|
||||||
|
/// Value: 60 seconds (1 minute)
|
||||||
|
pub const POLLING_INTERVAL_MEDIUM_BUFFER: u64 = 60;
|
||||||
|
|
||||||
|
/// Low buffer polling interval (seconds)
|
||||||
|
///
|
||||||
|
/// When the playlist buffer has less than 2 blocks, poll frequently to
|
||||||
|
/// ensure continuous playback.
|
||||||
|
///
|
||||||
|
/// Value: 20 seconds
|
||||||
|
pub const POLLING_INTERVAL_LOW_BUFFER: u64 = 20;
|
||||||
|
|
||||||
|
/// Helper to get high buffer polling interval as Duration
|
||||||
|
pub fn polling_high_interval() -> Duration {
|
||||||
|
Duration::from_secs(POLLING_INTERVAL_HIGH_BUFFER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get medium buffer polling interval as Duration
|
||||||
|
pub fn polling_medium_interval() -> Duration {
|
||||||
|
Duration::from_secs(POLLING_INTERVAL_MEDIUM_BUFFER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get low buffer polling interval as Duration
|
||||||
|
pub fn polling_low_interval() -> Duration {
|
||||||
|
Duration::from_secs(POLLING_INTERVAL_LOW_BUFFER)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Polling Backoff (on API errors)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Initial backoff delay on API error (seconds)
|
||||||
|
///
|
||||||
|
/// When an API request fails, we wait this duration before retrying.
|
||||||
|
///
|
||||||
|
/// Value: 20 seconds
|
||||||
|
pub const BACKOFF_INITIAL_SECONDS: u64 = 20;
|
||||||
|
|
||||||
|
/// Maximum backoff delay (seconds)
|
||||||
|
///
|
||||||
|
/// Backoff is capped at this value to avoid waiting too long.
|
||||||
|
///
|
||||||
|
/// Value: 300 seconds (5 minutes)
|
||||||
|
pub const BACKOFF_MAX_SECONDS: u64 = 300;
|
||||||
|
|
||||||
|
/// Backoff multiplier
|
||||||
|
///
|
||||||
|
/// After each failure, the delay is multiplied by this factor.
|
||||||
|
/// Example: 20s → 40s → 80s → 160s → 300s (capped)
|
||||||
|
///
|
||||||
|
/// Value: 2.0 (exponential backoff)
|
||||||
|
pub const BACKOFF_MULTIPLIER: f32 = 2.0;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cache Tuning
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Maximum number of blocks to remember in the worker
|
||||||
|
///
|
||||||
|
/// This prevents unbounded memory growth by limiting how many block event IDs
|
||||||
|
/// we track to avoid re-processing.
|
||||||
|
///
|
||||||
|
/// Calculation: (4 channels + 1 buffer) × 3 blocks per channel = 15 blocks
|
||||||
|
/// Each block is ~20 minutes of audio, so 15 blocks ≈ 5 hours of history
|
||||||
|
///
|
||||||
|
/// Value: 15 blocks
|
||||||
|
pub const MAX_BLOCKS_REMEMBERED: usize = 15;
|
||||||
|
|
||||||
|
/// Number of bytes to use for track ID hashing
|
||||||
|
///
|
||||||
|
/// Track IDs are constructed by hashing block content and track position.
|
||||||
|
/// This value defines how much of the FLAC data we read for hashing.
|
||||||
|
///
|
||||||
|
/// Value: 512 bytes - sufficient for unique identification without excessive I/O
|
||||||
|
pub const TRACK_ID_HASH_BYTES: usize = 512;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// History
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Default maximum number of tracks to keep in history
|
||||||
|
///
|
||||||
|
/// This is used as the default if not configured via pmoconfig.
|
||||||
|
/// Users can override this value in their configuration.
|
||||||
|
///
|
||||||
|
/// Value: 100 tracks - represents ~5-8 hours of playback history
|
||||||
|
pub const HISTORY_DEFAULT_MAX_TRACKS: usize = 100;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Streaming
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Stream buffer size (bytes)
|
||||||
|
///
|
||||||
|
/// Buffer size for audio streaming. 64KB provides good balance between
|
||||||
|
/// latency and buffering efficiency.
|
||||||
|
///
|
||||||
|
/// Value: 64 KB
|
||||||
|
pub const STREAM_BUFFER_SIZE_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
|
/// Enable gapless playback
|
||||||
|
///
|
||||||
|
/// Radio Paradise blocks are designed for gapless playback - each block
|
||||||
|
/// transitions seamlessly to the next without audio gaps.
|
||||||
|
///
|
||||||
|
/// Value: true (always enabled)
|
||||||
|
pub const STREAM_GAPLESS: bool = true;
|
||||||
|
|
||||||
|
// Note: Metadata format is always ICY (Icecast/SHOUTcast metadata)
|
||||||
|
// No enum or constant needed as it's the only supported format
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// API Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Radio Paradise API base URL
|
||||||
|
///
|
||||||
|
/// Base URL for all Radio Paradise API requests.
|
||||||
|
/// This is hardcoded as Radio Paradise's API endpoint doesn't change.
|
||||||
|
///
|
||||||
|
/// Value: https://api.radioparadise.com
|
||||||
|
pub const API_BASE_URL: &str = "https://api.radioparadise.com";
|
||||||
|
|
||||||
|
/// API request timeout (seconds)
|
||||||
|
///
|
||||||
|
/// Maximum time to wait for an API response before considering it failed.
|
||||||
|
///
|
||||||
|
/// Value: 30 seconds
|
||||||
|
pub const API_TIMEOUT_SECONDS: u64 = 30;
|
||||||
|
|
||||||
|
/// User agent for API requests
|
||||||
|
///
|
||||||
|
/// Identifies PMOMusic in HTTP requests to Radio Paradise's servers.
|
||||||
|
///
|
||||||
|
/// Value: PMO-RadioParadise/1.0
|
||||||
|
pub const API_USER_AGENT: &str = "PMO-RadioParadise/1.0";
|
||||||
|
|
||||||
|
/// Helper to get API timeout as Duration
|
||||||
|
pub fn api_timeout() -> Duration {
|
||||||
|
Duration::from_secs(API_TIMEOUT_SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duration_helpers() {
|
||||||
|
assert_eq!(polling_high_interval(), Duration::from_secs(120));
|
||||||
|
assert_eq!(polling_medium_interval(), Duration::from_secs(60));
|
||||||
|
assert_eq!(polling_low_interval(), Duration::from_secs(20));
|
||||||
|
assert_eq!(api_timeout(), Duration::from_secs(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_constants_sanity() {
|
||||||
|
// Polling intervals should be ordered
|
||||||
|
assert!(POLLING_INTERVAL_LOW_BUFFER < POLLING_INTERVAL_MEDIUM_BUFFER);
|
||||||
|
assert!(POLLING_INTERVAL_MEDIUM_BUFFER < POLLING_INTERVAL_HIGH_BUFFER);
|
||||||
|
|
||||||
|
// Backoff should be reasonable
|
||||||
|
assert!(BACKOFF_INITIAL_SECONDS < BACKOFF_MAX_SECONDS);
|
||||||
|
assert!(BACKOFF_MULTIPLIER > 1.0);
|
||||||
|
|
||||||
|
// Cache limits should be positive
|
||||||
|
assert!(MAX_BLOCKS_REMEMBERED > 0);
|
||||||
|
assert!(TRACK_ID_HASH_BYTES > 0);
|
||||||
|
|
||||||
|
// History should be reasonable
|
||||||
|
assert!(HISTORY_DEFAULT_MAX_TRACKS > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,12 @@
|
|||||||
//! The worker pushes every completed track into the history backend while
|
//! The worker pushes every completed track into the history backend while
|
||||||
//! keeping the latest entries available for UPnP browsing. We use SQLite
|
//! keeping the latest entries available for UPnP browsing. We use SQLite
|
||||||
//! for persistent storage with an abstract trait for testability.
|
//! for persistent storage with an abstract trait for testability.
|
||||||
|
|
||||||
use super::config::HistoryConfig;
|
|
||||||
use crate::models::Song;
|
use crate::models::Song;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, Mutex as StdMutex};
|
use std::sync::{Arc, Mutex as StdMutex};
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
|
|
||||||
/// Serializable record describing a played track.
|
/// Serializable record describing a played track.
|
||||||
@@ -59,46 +56,6 @@ pub trait HistoryBackend: Send + Sync {
|
|||||||
async fn truncate(&self, keep: usize) -> anyhow::Result<()>;
|
async fn truncate(&self, keep: usize) -> anyhow::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a history backend from configuration.
|
|
||||||
///
|
|
||||||
/// This always creates a SQLite-based backend using the configured database path.
|
|
||||||
pub fn history_backend_from_config(
|
|
||||||
config: &HistoryConfig,
|
|
||||||
) -> anyhow::Result<Arc<dyn HistoryBackend>> {
|
|
||||||
let backend = SqliteHistoryBackend::new(&config.database_path)?;
|
|
||||||
Ok(Arc::new(backend))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl HistoryBackend for MemoryHistoryBackend {
|
|
||||||
async fn append(&self, entry: HistoryEntry) -> anyhow::Result<()> {
|
|
||||||
let mut entries = self.entries.lock().await;
|
|
||||||
entries.push(entry);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recent(&self, limit: usize) -> anyhow::Result<Vec<HistoryEntry>> {
|
|
||||||
let entries = self.entries.lock().await;
|
|
||||||
let total = entries.len();
|
|
||||||
let start = total.saturating_sub(limit);
|
|
||||||
Ok(entries[start..].to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn len(&self) -> anyhow::Result<usize> {
|
|
||||||
Ok(self.entries.lock().await.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn truncate(&self, keep: usize) -> anyhow::Result<()> {
|
|
||||||
let mut entries = self.entries.lock().await;
|
|
||||||
if entries.len() > keep {
|
|
||||||
let drop_count = entries.len() - keep;
|
|
||||||
entries.drain(0..drop_count);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SqliteHistoryBackend {
|
pub struct SqliteHistoryBackend {
|
||||||
conn: Arc<StdMutex<rusqlite::Connection>>,
|
conn: Arc<StdMutex<rusqlite::Connection>>,
|
||||||
}
|
}
|
||||||
@@ -137,6 +94,7 @@ impl SqliteHistoryBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl HistoryBackend for SqliteHistoryBackend {
|
impl HistoryBackend for SqliteHistoryBackend {
|
||||||
async fn append(&self, entry: HistoryEntry) -> anyhow::Result<()> {
|
async fn append(&self, entry: HistoryEntry) -> anyhow::Result<()> {
|
||||||
@@ -240,3 +198,21 @@ impl HistoryBackend for SqliteHistoryBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a SQLite history backend with the given database path.
|
||||||
|
///
|
||||||
|
/// The database file and parent directories will be created if they don't exist.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `database_path` - Path to the SQLite database file
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let backend = create_history_backend("/var/lib/pmo/history.db")?;
|
||||||
|
/// ```
|
||||||
|
pub fn create_history_backend(database_path: &str) -> anyhow::Result<Arc<dyn HistoryBackend>> {
|
||||||
|
let backend = SqliteHistoryBackend::new(database_path)?;
|
||||||
|
Ok(Arc::new(backend))
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
//! - Shared caches and history storage hooked into existing PMO components.
|
//! - Shared caches and history storage hooked into existing PMO components.
|
||||||
//!
|
//!
|
||||||
//! The implementation is split across several submodules to keep concerns
|
//! The implementation is split across several submodules to keep concerns
|
||||||
//! isolated (configuration, playlist management, history persistence, etc.).
|
//! isolated (constants, playlist management, history persistence, etc.).
|
||||||
//! The goal of this scaffolding is to provide a clear, testable surface for
|
//! The goal of this scaffolding is to provide a clear, testable surface for
|
||||||
//! the eventual end-to-end integration with the UPnP server and HTTP routes.
|
//! the eventual end-to-end integration with the UPnP server and HTTP routes.
|
||||||
|
|
||||||
mod channel;
|
mod channel;
|
||||||
mod config;
|
pub mod constants;
|
||||||
mod history;
|
mod history;
|
||||||
mod playlist;
|
mod playlist;
|
||||||
mod worker;
|
mod worker;
|
||||||
@@ -22,10 +22,7 @@ pub use channel::{
|
|||||||
max_channel_id, ChannelDescriptor, ParadiseChannel, ParadiseChannelKind, ParadiseClientStream,
|
max_channel_id, ChannelDescriptor, ParadiseChannel, ParadiseChannelKind, ParadiseClientStream,
|
||||||
ALL_CHANNELS,
|
ALL_CHANNELS,
|
||||||
};
|
};
|
||||||
pub use config::{
|
pub use constants::*; // Export all constants
|
||||||
ActivityConfig, ApiConfig, CacheConfig, HistoryConfig, PollingConfig, RadioParadiseConfig,
|
pub use history::{create_history_backend, HistoryBackend, HistoryEntry};
|
||||||
StreamConfig,
|
|
||||||
};
|
|
||||||
pub use history::{history_backend_from_config, HistoryBackend, HistoryEntry};
|
|
||||||
pub use playlist::PlaylistEntry;
|
pub use playlist::PlaylistEntry;
|
||||||
pub use worker::{ParadiseWorker, WorkerCommand};
|
pub use worker::{ParadiseWorker, WorkerCommand};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! and ensures fresh content is available according to the specification.
|
//! and ensures fresh content is available according to the specification.
|
||||||
|
|
||||||
use super::channel::ChannelDescriptor;
|
use super::channel::ChannelDescriptor;
|
||||||
use super::config::RadioParadiseConfig;
|
use super::constants::*;
|
||||||
use super::history::HistoryBackend;
|
use super::history::HistoryBackend;
|
||||||
use super::playlist::{PlaylistEntry, SharedPlaylist};
|
use super::playlist::{PlaylistEntry, SharedPlaylist};
|
||||||
use crate::client::RadioParadiseClient;
|
use crate::client::RadioParadiseClient;
|
||||||
@@ -46,7 +46,7 @@ impl ParadiseWorker {
|
|||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
descriptor: ChannelDescriptor,
|
descriptor: ChannelDescriptor,
|
||||||
client: RadioParadiseClient,
|
client: RadioParadiseClient,
|
||||||
config: Arc<RadioParadiseConfig>,
|
history_max_tracks: usize,
|
||||||
playlist: SharedPlaylist,
|
playlist: SharedPlaylist,
|
||||||
history: Arc<dyn HistoryBackend>,
|
history: Arc<dyn HistoryBackend>,
|
||||||
cache_manager: Arc<SourceCacheManager>,
|
cache_manager: Arc<SourceCacheManager>,
|
||||||
@@ -57,7 +57,7 @@ impl ParadiseWorker {
|
|||||||
info!(channel = descriptor.slug, "Starting Radio Paradise worker");
|
info!(channel = descriptor.slug, "Starting Radio Paradise worker");
|
||||||
|
|
||||||
let mut state =
|
let mut state =
|
||||||
WorkerState::new(descriptor, client, config, playlist, history, cache_manager);
|
WorkerState::new(descriptor, client, history_max_tracks, playlist, history, cache_manager);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(task) = state.scheduled_task.as_mut() {
|
if let Some(task) = state.scheduled_task.as_mut() {
|
||||||
@@ -141,7 +141,6 @@ impl ParadiseWorker {
|
|||||||
struct WorkerState {
|
struct WorkerState {
|
||||||
descriptor: ChannelDescriptor,
|
descriptor: ChannelDescriptor,
|
||||||
client: RadioParadiseClient,
|
client: RadioParadiseClient,
|
||||||
config: Arc<RadioParadiseConfig>,
|
|
||||||
playlist: SharedPlaylist,
|
playlist: SharedPlaylist,
|
||||||
history: Arc<dyn HistoryBackend>,
|
history: Arc<dyn HistoryBackend>,
|
||||||
cache_manager: Arc<SourceCacheManager>,
|
cache_manager: Arc<SourceCacheManager>,
|
||||||
@@ -160,7 +159,7 @@ impl WorkerState {
|
|||||||
fn new(
|
fn new(
|
||||||
descriptor: ChannelDescriptor,
|
descriptor: ChannelDescriptor,
|
||||||
client: RadioParadiseClient,
|
client: RadioParadiseClient,
|
||||||
config: Arc<RadioParadiseConfig>,
|
_history_max_tracks: usize,
|
||||||
playlist: SharedPlaylist,
|
playlist: SharedPlaylist,
|
||||||
history: Arc<dyn HistoryBackend>,
|
history: Arc<dyn HistoryBackend>,
|
||||||
cache_manager: Arc<SourceCacheManager>,
|
cache_manager: Arc<SourceCacheManager>,
|
||||||
@@ -168,7 +167,6 @@ impl WorkerState {
|
|||||||
Self {
|
Self {
|
||||||
descriptor,
|
descriptor,
|
||||||
client,
|
client,
|
||||||
config,
|
|
||||||
playlist,
|
playlist,
|
||||||
history,
|
history,
|
||||||
cache_manager,
|
cache_manager,
|
||||||
@@ -241,9 +239,7 @@ impl WorkerState {
|
|||||||
|
|
||||||
fn on_error(&mut self, err: anyhow::Error) {
|
fn on_error(&mut self, err: anyhow::Error) {
|
||||||
warn!(channel = self.descriptor.slug, "Worker error: {err:?}");
|
warn!(channel = self.descriptor.slug, "Worker error: {err:?}");
|
||||||
let delay = self
|
let delay = self.backoff.next_delay();
|
||||||
.backoff
|
|
||||||
.next_delay(&self.config.polling.backoff_on_error);
|
|
||||||
self.schedule_task(ScheduledTaskKind::Poll, delay);
|
self.schedule_task(ScheduledTaskKind::Poll, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +266,7 @@ impl WorkerState {
|
|||||||
"Channel entering Cooling state"
|
"Channel entering Cooling state"
|
||||||
);
|
);
|
||||||
self.status = ChannelLifecycle::Cooling;
|
self.status = ChannelLifecycle::Cooling;
|
||||||
let duration = Duration::from_secs(self.config.activity.cooling_timeout_seconds.max(1));
|
let duration = Duration::from_secs(COOLING_TIMEOUT_SECONDS.max(1));
|
||||||
self.schedule_task(ScheduledTaskKind::Cooling, duration);
|
self.schedule_task(ScheduledTaskKind::Cooling, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,11 +737,11 @@ impl WorkerState {
|
|||||||
let buffer_len = self.playlist.active_len().await;
|
let buffer_len = self.playlist.active_len().await;
|
||||||
|
|
||||||
let interval = if buffer_len > 3 {
|
let interval = if buffer_len > 3 {
|
||||||
self.config.polling.high_interval()
|
polling_high_interval()
|
||||||
} else if buffer_len >= 2 {
|
} else if buffer_len >= 2 {
|
||||||
self.config.polling.medium_interval()
|
polling_medium_interval()
|
||||||
} else {
|
} else {
|
||||||
self.config.polling.low_interval()
|
polling_low_interval()
|
||||||
};
|
};
|
||||||
|
|
||||||
self.schedule_task(ScheduledTaskKind::Poll, interval);
|
self.schedule_task(ScheduledTaskKind::Poll, interval);
|
||||||
@@ -769,7 +765,7 @@ impl WorkerState {
|
|||||||
fn record_processed_block(&mut self, event: u64) {
|
fn record_processed_block(&mut self, event: u64) {
|
||||||
self.processed_blocks.insert(event);
|
self.processed_blocks.insert(event);
|
||||||
self.recent_blocks.push_back(event);
|
self.recent_blocks.push_back(event);
|
||||||
let max = self.config.cache.max_blocks_remembered.max(1);
|
let max = MAX_BLOCKS_REMEMBERED.max(1);
|
||||||
while self.recent_blocks.len() > max {
|
while self.recent_blocks.len() > max {
|
||||||
if let Some(ev) = self.recent_blocks.pop_front() {
|
if let Some(ev) = self.recent_blocks.pop_front() {
|
||||||
self.processed_blocks.remove(&ev);
|
self.processed_blocks.remove(&ev);
|
||||||
@@ -966,13 +962,13 @@ impl BackoffState {
|
|||||||
self.current = None;
|
self.current = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_delay(&mut self, config: &super::config::PollingBackoffConfig) -> Duration {
|
fn next_delay(&mut self) -> Duration {
|
||||||
let next = match self.current {
|
let next = match self.current {
|
||||||
Some(current) => {
|
Some(current) => {
|
||||||
let multiplied = (current.as_secs_f32() * config.multiplier).round() as u64;
|
let multiplied = (current.as_secs_f32() * BACKOFF_MULTIPLIER).round() as u64;
|
||||||
Duration::from_secs(multiplied.min(config.max))
|
Duration::from_secs(multiplied.min(BACKOFF_MAX_SECONDS))
|
||||||
}
|
}
|
||||||
None => Duration::from_secs(config.initial),
|
None => Duration::from_secs(BACKOFF_INITIAL_SECONDS),
|
||||||
};
|
};
|
||||||
self.current = Some(next);
|
self.current = Some(next);
|
||||||
next
|
next
|
||||||
|
|||||||
@@ -543,8 +543,6 @@ async fn get_channel_status(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let cache_stats = channel.cache_manager().statistics().await;
|
let cache_stats = channel.cache_manager().statistics().await;
|
||||||
let config = channel.config().clone();
|
|
||||||
let configured = config.channels.iter().any(|slug| slug == descriptor.slug);
|
|
||||||
|
|
||||||
let status = ChannelStatusResponse {
|
let status = ChannelStatusResponse {
|
||||||
channel_id,
|
channel_id,
|
||||||
@@ -556,8 +554,8 @@ async fn get_channel_status(
|
|||||||
update_id,
|
update_id,
|
||||||
last_change,
|
last_change,
|
||||||
history_entries: history_len,
|
history_entries: history_len,
|
||||||
history_max_tracks: config.history.max_tracks,
|
history_max_tracks: channel.history_max_tracks(),
|
||||||
configured,
|
configured: true, // All channels are always available
|
||||||
cache_collection_id: cache_stats.collection_id,
|
cache_collection_id: cache_stats.collection_id,
|
||||||
cache_total_tracks: cache_stats.total_tracks,
|
cache_total_tracks: cache_stats.total_tracks,
|
||||||
cache_cached_tracks: cache_stats.cached_tracks,
|
cache_cached_tracks: cache_stats.cached_tracks,
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
use crate::client::RadioParadiseClient;
|
use crate::client::RadioParadiseClient;
|
||||||
use crate::paradise::{
|
use crate::paradise::{
|
||||||
history_backend_from_config, ChannelDescriptor, ParadiseChannel, PlaylistEntry,
|
create_history_backend, ChannelDescriptor, ParadiseChannel, PlaylistEntry, ALL_CHANNELS,
|
||||||
RadioParadiseConfig, ALL_CHANNELS,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "pmoconfig"))]
|
||||||
|
use crate::paradise::HISTORY_DEFAULT_MAX_TRACKS;
|
||||||
use anyhow::Result as AnyhowResult;
|
use anyhow::Result as AnyhowResult;
|
||||||
use pmoaudiocache::Cache as AudioCache;
|
use pmoaudiocache::Cache as AudioCache;
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
@@ -76,11 +78,9 @@ impl std::fmt::Debug for RadioParadiseSource {
|
|||||||
impl RadioParadiseSource {
|
impl RadioParadiseSource {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn from_registry(client: RadioParadiseClient) -> Result<Self> {
|
pub fn from_registry(client: RadioParadiseClient) -> Result<Self> {
|
||||||
let config = Arc::new(RadioParadiseConfig::load_from_pmoconfig().unwrap_or_default());
|
|
||||||
|
|
||||||
// Load history configuration from pmoconfig using the config extension trait
|
// Load history configuration from pmoconfig using the config extension trait
|
||||||
#[cfg(feature = "pmoconfig")]
|
#[cfg(feature = "pmoconfig")]
|
||||||
let history_config = {
|
let (database_path, history_max_tracks) = {
|
||||||
use crate::config_ext::RadioParadiseConfigExt;
|
use crate::config_ext::RadioParadiseConfigExt;
|
||||||
let cfg = pmoconfig::get_config();
|
let cfg = pmoconfig::get_config();
|
||||||
let database_path = cfg.get_paradise_history_database().map_err(|e| {
|
let database_path = cfg.get_paradise_history_database().map_err(|e| {
|
||||||
@@ -92,16 +92,22 @@ impl RadioParadiseSource {
|
|||||||
let max_tracks = cfg.get_paradise_history_size().map_err(|e| {
|
let max_tracks = cfg.get_paradise_history_size().map_err(|e| {
|
||||||
MusicSourceError::SourceUnavailable(format!("Failed to get history size: {}", e))
|
MusicSourceError::SourceUnavailable(format!("Failed to get history size: {}", e))
|
||||||
})?;
|
})?;
|
||||||
crate::paradise::HistoryConfig {
|
(database_path, max_tracks)
|
||||||
database_path,
|
|
||||||
max_tracks,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "pmoconfig"))]
|
#[cfg(not(feature = "pmoconfig"))]
|
||||||
let history_config = config.history.clone();
|
let (database_path, history_max_tracks) = {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
let mut path = PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()));
|
||||||
|
path.push(".config");
|
||||||
|
path.push("pmo");
|
||||||
|
path.push("paradise");
|
||||||
|
std::fs::create_dir_all(&path).ok();
|
||||||
|
path.push("history.db");
|
||||||
|
(path.to_string_lossy().to_string(), HISTORY_DEFAULT_MAX_TRACKS)
|
||||||
|
};
|
||||||
|
|
||||||
let history_backend = history_backend_from_config(&history_config).map_err(|e| {
|
let history_backend = create_history_backend(&database_path).map_err(|e| {
|
||||||
MusicSourceError::SourceUnavailable(format!(
|
MusicSourceError::SourceUnavailable(format!(
|
||||||
"Failed to initialize history backend: {}",
|
"Failed to initialize history backend: {}",
|
||||||
e
|
e
|
||||||
@@ -117,7 +123,7 @@ impl RadioParadiseSource {
|
|||||||
ParadiseChannel::new(
|
ParadiseChannel::new(
|
||||||
*descriptor,
|
*descriptor,
|
||||||
client.clone(),
|
client.clone(),
|
||||||
config.clone(),
|
history_max_tracks,
|
||||||
history_backend.clone(),
|
history_backend.clone(),
|
||||||
cache_manager,
|
cache_manager,
|
||||||
)
|
)
|
||||||
@@ -146,9 +152,34 @@ impl RadioParadiseSource {
|
|||||||
cover_cache: Arc<CoverCache>,
|
cover_cache: Arc<CoverCache>,
|
||||||
audio_cache: Arc<AudioCache>,
|
audio_cache: Arc<AudioCache>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let config = Arc::new(RadioParadiseConfig::load_from_pmoconfig().unwrap_or_default());
|
// Load history configuration from pmoconfig using the config extension trait
|
||||||
|
#[cfg(feature = "pmoconfig")]
|
||||||
|
let (database_path, history_max_tracks) = {
|
||||||
|
use crate::config_ext::RadioParadiseConfigExt;
|
||||||
|
let cfg = pmoconfig::get_config();
|
||||||
|
let database_path = cfg.get_paradise_history_database().unwrap_or_else(|e| {
|
||||||
|
panic!("Failed to get history database path: {e}");
|
||||||
|
});
|
||||||
|
let max_tracks = cfg.get_paradise_history_size().unwrap_or_else(|e| {
|
||||||
|
panic!("Failed to get history size: {e}");
|
||||||
|
});
|
||||||
|
(database_path, max_tracks)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "pmoconfig"))]
|
||||||
|
let (database_path, history_max_tracks) = {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
let mut path = PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()));
|
||||||
|
path.push(".config");
|
||||||
|
path.push("pmo");
|
||||||
|
path.push("paradise");
|
||||||
|
std::fs::create_dir_all(&path).ok();
|
||||||
|
path.push("history.db");
|
||||||
|
(path.to_string_lossy().to_string(), HISTORY_DEFAULT_MAX_TRACKS)
|
||||||
|
};
|
||||||
|
|
||||||
let history_backend: Arc<dyn crate::paradise::HistoryBackend> =
|
let history_backend: Arc<dyn crate::paradise::HistoryBackend> =
|
||||||
history_backend_from_config(&config.history).unwrap_or_else(|err| {
|
create_history_backend(&database_path).unwrap_or_else(|err| {
|
||||||
panic!("Failed to initialize history backend: {err}");
|
panic!("Failed to initialize history backend: {err}");
|
||||||
});
|
});
|
||||||
let mut channels = HashMap::new();
|
let mut channels = HashMap::new();
|
||||||
@@ -162,7 +193,7 @@ impl RadioParadiseSource {
|
|||||||
match ParadiseChannel::new(
|
match ParadiseChannel::new(
|
||||||
*descriptor,
|
*descriptor,
|
||||||
client.clone(),
|
client.clone(),
|
||||||
config.clone(),
|
history_max_tracks,
|
||||||
history_backend.clone(),
|
history_backend.clone(),
|
||||||
cache_manager,
|
cache_manager,
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user