use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
use crate::persistence::ServerStore;
use mineral_audio::{AudioHandle, AudioMode};
use mineral_channel_core::MusicChannel;
use mineral_model::{SongId, SourceKind};
use mineral_playback::PlaybackRegistry;
use mineral_task::Scheduler;
use parking_lot::Mutex;
use super::backends::{RecordingChannel, test_playback_registry};
use crate::media_cache::MediaCache;
use crate::player::{Inner, PlayerCore};
use crate::state::State;
pub(super) fn core_with(
calls: Arc<Mutex<Vec<(SongId, bool, u64)>>>,
) -> color_eyre::Result<PlayerCore> {
core_with_persist(calls, ServerStore::disabled())
}
pub(super) fn core_with_persist(
calls: Arc<Mutex<Vec<(SongId, bool, u64)>>>,
persist: ServerStore,
) -> color_eyre::Result<PlayerCore> {
let channels: Vec<Arc<dyn MusicChannel>> = vec![Arc::new(RecordingChannel {
calls,
liked_ids: None,
playlists: None,
})];
core_with_channels(
channels,
persist,
None,
MediaCache::disabled(),
)
}
pub(super) fn core_with_channels(
channels: Vec<Arc<dyn MusicChannel>>,
persist: ServerStore,
music_dir: Option<&Path>,
media_cache: MediaCache,
) -> color_eyre::Result<PlayerCore> {
core_with_events(
channels,
persist,
music_dir,
media_cache,
tokio::sync::broadcast::channel( 8).0,
None,
)
}
pub(super) fn core_with_channels_music_dir(
channels: Vec<Arc<dyn MusicChannel>>,
persist: ServerStore,
media_cache: MediaCache,
music_dir: &Path,
) -> color_eyre::Result<PlayerCore> {
let playback = test_playback_registry(
&channels,
Duration::ZERO,
false,
true,
)?;
core_with_events_stats_playback(
channels,
playback,
persist,
Some(music_dir),
media_cache,
tokio::sync::broadcast::channel( 8).0,
None,
crate::StatsRecorder::disabled(),
)
}
pub(super) fn core_with_script(
script: &str,
) -> color_eyre::Result<(PlayerCore, mineral_script::ScriptRuntime)> {
core_with_script_stats(script, crate::StatsRecorder::disabled())
}
pub(super) fn core_with_script_stats(
script: &str,
stats: crate::StatsRecorder,
) -> color_eyre::Result<(PlayerCore, mineral_script::ScriptRuntime)> {
core_with_script_playback(
script,
stats,
Duration::ZERO,
false,
true,
)
}
pub(super) fn core_with_script_playback(
script: &str,
stats: crate::StatsRecorder,
delay: Duration,
fail: bool,
direct: bool,
) -> color_eyre::Result<(PlayerCore, mineral_script::ScriptRuntime)> {
core_with_script_playback_persist(script, stats, delay, fail, direct, ServerStore::disabled())
}
pub(super) fn core_with_script_persist(
script: &str,
persist: ServerStore,
) -> color_eyre::Result<(PlayerCore, mineral_script::ScriptRuntime)> {
core_with_script_playback_persist(
script,
crate::StatsRecorder::disabled(),
Duration::ZERO,
false,
true,
persist,
)
}
fn core_with_script_playback_persist(
script: &str,
stats: crate::StatsRecorder,
delay: Duration,
fail: bool,
direct: bool,
persist: ServerStore,
) -> color_eyre::Result<(PlayerCore, mineral_script::ScriptRuntime)> {
use mineral_script::{ScriptHost, ScriptRuntime, ScriptSender, install_daemon_api};
let (cmd_tx, _cmd_rx) = tokio::sync::mpsc::unbounded_channel();
let (push_tx, _push_rx) = tokio::sync::mpsc::unbounded_channel();
let host = ScriptHost::new(cmd_tx, push_tx);
let lua = mineral_script::mlua::Lua::new();
install_daemon_api(&lua, &host)?;
lua.load(format!(
"local mineral = require(\"mineral.daemon\")\n{script}"
))
.exec()?;
let sender = ScriptSender::detached();
let watchdog = mineral_script::WatchdogConfig::builder()
.instruction_interval(10_000)
.soft_wall(Duration::from_millis(200))
.hard_wall(Duration::from_secs(1))
.build();
let runtime = ScriptRuntime::spawn(lua, host, watchdog, &sender)?;
let channels: Vec<Arc<dyn MusicChannel>> = vec![Arc::new(RecordingChannel {
calls: Arc::default(),
liked_ids: None,
playlists: None,
})];
let playback = test_playback_registry(&channels, delay, fail, direct)?;
let core = core_with_events_stats_playback(
channels,
playback,
persist,
None,
MediaCache::disabled(),
tokio::sync::broadcast::channel( 8).0,
Some(sender),
stats,
)?;
Ok((core, runtime))
}
pub(super) fn core_with_events(
channels: Vec<Arc<dyn MusicChannel>>,
persist: ServerStore,
music_dir: Option<&Path>,
media_cache: MediaCache,
events: tokio::sync::broadcast::Sender<mineral_protocol::Event>,
script: Option<mineral_script::ScriptSender>,
) -> color_eyre::Result<PlayerCore> {
core_with_events_stats(
channels,
persist,
music_dir,
media_cache,
events,
script,
crate::StatsRecorder::disabled(),
)
}
#[allow(clippy::too_many_arguments)] pub(super) fn core_with_events_stats(
channels: Vec<Arc<dyn MusicChannel>>,
persist: ServerStore,
music_dir: Option<&Path>,
media_cache: MediaCache,
events: tokio::sync::broadcast::Sender<mineral_protocol::Event>,
script: Option<mineral_script::ScriptSender>,
stats: crate::StatsRecorder,
) -> color_eyre::Result<PlayerCore> {
let playback = test_playback_registry(
&channels,
Duration::ZERO,
false,
true,
)?;
core_with_events_stats_playback(
channels,
playback,
persist,
music_dir,
media_cache,
events,
script,
stats,
)
}
#[allow(clippy::too_many_arguments)] pub(super) fn core_with_events_stats_playback(
channels: Vec<Arc<dyn MusicChannel>>,
playback: PlaybackRegistry,
persist: ServerStore,
music_dir: Option<&Path>,
media_cache: MediaCache,
events: tokio::sync::broadcast::Sender<mineral_protocol::Event>,
script: Option<mineral_script::ScriptSender>,
stats: crate::StatsRecorder,
) -> color_eyre::Result<PlayerCore> {
let cfg = crate::config::ServerConfig::from_config(&crate::config::DaemonConfig::defaults()?);
let scheduler = Scheduler::new(&channels, *cfg.channel_workers_per());
let (audio, _tap) = AudioHandle::spawn(AudioMode::ForceNull, cfg.engine().clone())?;
let library = crate::library::Library::new(
channels
.iter()
.map(|ch| ch.source())
.collect::<Vec<SourceKind>>(),
);
let tagging =
crate::tagging::TaggingQueue::spawn( false, &[], None, 1);
let notify = crate::notify::Notifier::new(events, script);
let music_dir = music_dir.map(Path::to_path_buf);
let downloads = crate::download::DownloadManager::spawn(
crate::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(*cfg.hook_timeout_ms()),
),
tagging: tagging.clone(),
notify: notify.clone(),
stats: stats.clone(),
speed_tick: Duration::from_millis(*cfg.timing().download_speed_tick_ms()),
},
*cfg.download().quality(),
*cfg.download().max_concurrent(),
);
let inner = Arc::new(Inner {
state_changes: crate::state::StatePublisher::new().1,
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: crate::config::ConfigHost::new(crate::config::default_daemon_tree()?, false)?,
state: Mutex::new(State::empty()),
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(std::time::Instant::now()),
playback_quality: *cfg.playback_quality(),
playback_prefetch_bytes: *cfg.engine().prefetch_bytes(),
envelope_params: cfg.envelope().clone(),
gapless_prefetch_ms: *cfg.timing().gapless_prefetch_ms(),
prev_restart_threshold_ms: *cfg.timing().prev_restart_threshold_ms(),
player_tick_ms: *cfg.timing().player_tick_ms(),
session_save: Duration::from_secs(*cfg.timing().session_save_secs()),
media_report_interval_ms: *cfg.timing().report_interval_ms(),
media_seek_threshold_ms: *cfg.timing().seek_threshold_ms(),
hook_timeout: Duration::from_millis(*cfg.hook_timeout_ms()),
backfill: crate::favorites::Backfill::new(
*cfg.favorites_backfill_chunk_size(),
*cfg.favorites_backfill_max_concurrent(),
),
});
Ok(PlayerCore { inner })
}