use std::sync::Arc;
use crate::config::DaemonConfig;
use mineral_protocol::{BusValue, PlayCountAvailability, ServiceInfo};
use mineral_script::ConfigOverrideOp;
use parking_lot::Mutex;
use crate::player::PlayerCore;
pub(crate) struct ConfigHost {
state: Mutex<HostState>,
}
struct HostState {
base: serde_json::Value,
overlay: Vec<(String, BusValue)>,
effective: serde_json::Value,
config: DaemonConfig,
queue_transforms: Vec<String>,
}
struct EvictedOverride {
path: String,
warning: Arc<mineral_config::ConfigWarning>,
}
impl ConfigHost {
pub(crate) fn new(
base: serde_json::Value,
script_available: bool,
) -> Result<Self, mineral_config::ConfigWarning> {
let config = crate::config::daemon_from_tree(&base)?;
let queue_transforms = available_transforms(&config, script_available);
Ok(Self {
state: Mutex::new(HostState {
effective: base.clone(),
base,
overlay: Vec::new(),
config,
queue_transforms,
}),
})
}
}
fn available_transforms(config: &DaemonConfig, script_available: bool) -> Vec<String> {
if script_available {
config
.queue()
.transforms()
.iter()
.map(|operation| operation.name().clone())
.collect::<Vec<String>>()
} else {
Vec::<String>::new()
}
}
fn recompute(
base: &serde_json::Value,
overlay: &mut Vec<(String, BusValue)>,
) -> Result<(serde_json::Value, DaemonConfig, Vec<EvictedOverride>), mineral_config::ConfigWarning>
{
let mut evicted = Vec::<EvictedOverride>::new();
loop {
let mut tree = base.clone();
for (path, value) in overlay.iter() {
tree = mineral_config::merge_tree(
tree,
mineral_config::nest_path(path, value.clone().into_json()),
);
}
let warning = match crate::config::daemon_from_tree(&tree) {
Ok(config) => return Ok((tree, config, evicted)),
Err(warning) => warning,
};
if overlay.is_empty() {
return Err(warning);
}
let warning = Arc::new(warning);
let err_path = match warning.as_ref() {
mineral_config::ConfigWarning::Deserialize { path, .. } => path.as_deref(),
_ => None,
};
let before = overlay.len();
overlay.retain(|(path, _value)| {
let hit = err_path.is_none_or(|err_path| covers(path, err_path));
if hit {
evicted.push(EvictedOverride {
path: path.clone(),
warning: Arc::clone(&warning),
});
}
!hit
});
if overlay.len() == before {
evicted.extend(overlay.drain(..).map(|(path, _value)| EvictedOverride {
path,
warning: Arc::clone(&warning),
}));
}
}
}
fn covers(overlay_path: &str, err_path: &str) -> bool {
segment_prefix_of(overlay_path, err_path) || segment_prefix_of(err_path, overlay_path)
}
fn segment_prefix_of(short: &str, long: &str) -> bool {
long.strip_prefix(short)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('.') || rest.starts_with('['))
}
impl PlayerCore {
pub(crate) fn set_config_base(&self, tree: serde_json::Value) {
self.inner.stats.event(mineral_stats::StatsEvent::System(
mineral_stats::SystemEvent::ConfigReload,
));
let previous_info = self.service_info();
let script_available = self
.script_sender()
.is_some_and(|sender| sender.is_attached());
let result = {
let mut guard = self.inner.config_host.state.lock();
let state = &mut *guard;
recompute(&tree, &mut state.overlay).map(|(effective, config, evicted)| {
let changed = effective != state.effective;
state.base = tree;
state.queue_transforms = available_transforms(&config, script_available);
state.effective = effective;
state.config = config.clone();
(changed, config, evicted)
})
};
self.apply_recomputed_config(result, &previous_info);
}
pub(crate) fn apply_config_overrides(&self, ops: Vec<ConfigOverrideOp>) {
let previous_info = self.service_info();
let script_available = self
.script_sender()
.is_some_and(|sender| sender.is_attached());
let result = {
let mut guard = self.inner.config_host.state.lock();
let state = &mut *guard;
let mut touched = false;
for ConfigOverrideOp { path, value } in ops {
if value.as_ref().is_some_and(|value| {
mineral_config::nest_path(&path, value.clone().into_json())
.pointer("/queue/transforms")
.is_some()
}) {
mineral_log::warn!(target: "config", path, "队列变换须随 daemon.lua 的函数定义一起加载,拒绝覆盖描述表");
self.notify()
.failure(mineral_protocol::FailureNotice::ConfigOverrideRejected { path });
continue;
}
touched |= match value {
Some(new_value) => match state.overlay.iter_mut().find(|(p, _)| *p == path) {
Some((_, existing)) if *existing == new_value => false,
Some((_, existing)) => {
*existing = new_value;
true
}
None => {
state.overlay.push((path, new_value));
true
}
},
None => {
let before = state.overlay.len();
state.overlay.retain(|(p, _)| *p != path);
state.overlay.len() != before
}
};
}
if !touched {
return;
}
recompute(&state.base, &mut state.overlay).map(|(effective, config, evicted)| {
let changed = effective != state.effective;
state.queue_transforms = available_transforms(&config, script_available);
state.effective = effective;
state.config = config.clone();
(changed, config, evicted)
})
};
self.apply_recomputed_config(result, &previous_info);
}
pub(crate) fn service_info(&self) -> ServiceInfo {
let guard = self.inner.config_host.state.lock();
ServiceInfo {
queue_transforms: guard.queue_transforms.clone(),
play_counts: PlayCountAvailability {
enabled: self.inner.stats.play_counts_enabled(),
excluded_sources: guard.config.stats().exclude_sources().clone(),
},
}
}
fn apply_recomputed_config(
&self,
result: Result<(bool, DaemonConfig, Vec<EvictedOverride>), mineral_config::ConfigWarning>,
previous_info: &ServiceInfo,
) {
let (changed, config, evicted) = match result {
Ok(result) => result,
Err(error) => {
mineral_log::error!(target: "config", error = mineral_log::chain(&error), "daemon 配置底树无效,保持旧策略");
return;
}
};
self.report_evicted_overrides(&evicted);
if changed {
self.inner
.stats
.set_params(crate::params_from_config(config.stats()));
self.inner.downloads.set_config(
*config.download().quality(),
*config.download().max_concurrent(),
);
mineral_log::info!(target: "config", "daemon 运行时配置已更新");
}
let info = self.service_info();
if &info != previous_info {
self.notify().service_info_changed(info);
}
}
fn report_evicted_overrides(&self, evicted: &[EvictedOverride]) {
for e in evicted {
mineral_log::warn!(
target: "config",
path = e.path,
error = mineral_log::chain(e.warning.as_ref()),
"daemon 配置覆盖无效,已撤销"
);
self.notify()
.failure(mineral_protocol::FailureNotice::ConfigOverrideRejected {
path: e.path.clone(),
});
}
}
}