use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::{Duration, Instant};
use crate::persistence::ServerStore;
use mineral_audio::AudioHandle;
use mineral_model::SourceKind;
use mineral_task::Scheduler;
use parking_lot::Mutex;
use super::{Inner, PlayerCore};
use crate::download;
use crate::media_cache::MediaCache;
use crate::server::SourceBackends;
use crate::state::State;
pub(crate) struct SpawnConfig<'a> {
pub(crate) slices: &'a crate::config::ServerConfig,
pub(crate) tree: serde_json::Value,
}
pub(crate) struct Sinks {
pub(crate) notify: crate::notify::Notifier,
pub(crate) stats: crate::StatsRecorder,
}
pub(crate) struct PlayerStorage {
pub(crate) persist: ServerStore,
pub(crate) media_cache: MediaCache,
pub(crate) music_dir: Option<std::path::PathBuf>,
}
impl PlayerCore {
pub(crate) fn spawn(
audio: AudioHandle,
scheduler: Scheduler,
sources: SourceBackends,
storage: PlayerStorage,
spawn_config: SpawnConfig<'_>,
sinks: Sinks,
) -> Result<Self, mineral_config::ConfigWarning> {
let SpawnConfig {
slices: config,
tree: config_tree,
} = spawn_config;
let Sinks { notify, stats } = sinks;
let config_host = crate::config::ConfigHost::new(
config_tree,
notify
.script_sender()
.is_some_and(|sender| sender.is_attached()),
)?;
let PlayerStorage {
persist,
media_cache,
music_dir,
} = storage;
let SourceBackends { channels, playback } = sources;
let tagging_http = reqwest::Client::builder().build().ok();
if tagging_http.is_none() {
mineral_log::warn!(target: "download", "HTTP client 构建失败,封面打标不可用");
}
let tagging = crate::tagging::TaggingQueue::spawn(
*config.download().tagging(),
&channels,
tagging_http.as_ref(),
*config.download().tagging_workers(),
);
let library = crate::library::Library::new(
channels
.iter()
.map(|ch| ch.source())
.collect::<Vec<SourceKind>>(),
);
let downloads = download::DownloadManager::spawn(
download::DownloadRuntime {
music_dir: music_dir.clone(),
channels: channels.clone(),
playback: playback.clone(),
hooks: crate::hook_bridge::HookGate::new(
notify.script_sender(),
Duration::from_millis(*config.hook_timeout_ms()),
),
tagging: tagging.clone(),
notify: notify.clone(),
stats: stats.clone(),
speed_tick: Duration::from_millis(*config.timing().download_speed_tick_ms()),
},
*config.download().quality(),
*config.download().max_concurrent(),
);
let (publisher, state_changes) = crate::state::StatePublisher::new();
let inner = Arc::new(Inner {
audio,
scheduler,
channels,
playback,
persist,
media_cache: Arc::new(media_cache),
music_dir,
downloads,
tagging,
notify,
stats,
props: crate::props::PropsWatch::default(),
ui_state: Mutex::new(crate::props::TerminalStates::default()),
config_host,
state: Mutex::new(State::empty()),
state_changes,
last_seen_finished_seq: AtomicU64::new(0),
envelope_inflight: Mutex::new(rustc_hash::FxHashSet::default()),
library,
favorites_lock: tokio::sync::Mutex::new(()),
last_session_save: Mutex::new(Instant::now()),
playback_quality: *config.playback_quality(),
playback_prefetch_bytes: *config.engine().prefetch_bytes(),
envelope_params: config.envelope().clone(),
gapless_prefetch_ms: *config.timing().gapless_prefetch_ms(),
prev_restart_threshold_ms: *config.timing().prev_restart_threshold_ms(),
player_tick_ms: *config.timing().player_tick_ms(),
session_save: Duration::from_secs(*config.timing().session_save_secs()),
media_report_interval_ms: *config.timing().report_interval_ms(),
media_seek_threshold_ms: *config.timing().seek_threshold_ms(),
hook_timeout: Duration::from_millis(*config.hook_timeout_ms()),
backfill: crate::favorites::Backfill::new(
*config.favorites_backfill_chunk_size(),
*config.favorites_backfill_max_concurrent(),
),
});
inner.state.lock().publisher = Some(publisher);
let me = Self { inner };
let bg = me.clone();
tokio::spawn(async move { bg.background_loop().await });
Ok(me)
}
}