100 lines
3.7 KiB
Rust
100 lines
3.7 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
|
|
use log::{debug, error, info, warn};
|
|
|
|
use crate::{
|
|
audio::AudioEngine,
|
|
config::Config,
|
|
db::open_db,
|
|
server::start_server,
|
|
state::DaemonState,
|
|
};
|
|
|
|
pub fn run(cfg: Config) {
|
|
info!("Starting sjnmusicd");
|
|
|
|
let conn = open_db();
|
|
let state = Arc::new(Mutex::new(DaemonState::new(conn)));
|
|
|
|
// HTTP server thread.
|
|
let server_state = state.clone();
|
|
let host = cfg.server.host.clone();
|
|
let port = cfg.server.port;
|
|
let server_handle = thread::Builder::new()
|
|
.name("sjnmusic-http".into())
|
|
.spawn(move || {
|
|
start_server(server_state, &host, port);
|
|
});
|
|
if let Err(e) = server_handle {
|
|
error!("Failed to spawn HTTP server thread: {e}");
|
|
}
|
|
|
|
// Audio playback loop (runs on the main thread).
|
|
let mut audio = AudioEngine::new();
|
|
info!("Playback loop ready");
|
|
|
|
loop {
|
|
{
|
|
let mut state = state.lock().unwrap();
|
|
|
|
// Clear "now playing" once the audio engine reports the song has
|
|
// ended so /now-playing and /queue reflect reality while the
|
|
// next iteration decides what's up next.
|
|
if !audio.is_playing() {
|
|
if let Some(prev) = state.current.take() {
|
|
debug!("Playback finished for {}", prev.name);
|
|
}
|
|
}
|
|
|
|
if !state.queue.is_empty() && !audio.is_playing() {
|
|
// Snapshot the next song first, attempt audio.play, and only
|
|
// remove from the queue (memory + DB) when playback actually
|
|
// starts so decode/init failures don't silently lose it.
|
|
let next = state.queue[0].clone();
|
|
debug!("Starting playback: {} ({})", next.name, next.path);
|
|
match audio.play(&next.path) {
|
|
Ok(()) => match state.pop_played() {
|
|
Ok(Some(popped)) => {
|
|
let name = popped.name.clone();
|
|
state.current = Some(popped);
|
|
debug!("DB queue row removed for {name}");
|
|
info!("Now playing: {name}");
|
|
}
|
|
Ok(None) => {
|
|
// Should not happen — we peeked a non-empty queue.
|
|
state.current = Some(next.clone());
|
|
warn!(
|
|
"pop_played returned None after successful audio.play for {}",
|
|
next.name
|
|
);
|
|
}
|
|
Err(e) => {
|
|
// pop_played is DB-first; on Err the in-memory
|
|
// queue has NOT been mutated, so the song is
|
|
// already at index 0 — do not re-insert or we
|
|
// would duplicate it.
|
|
error!(
|
|
"Played {} but failed to update DB queue ({e}); \
|
|
leaving in-memory queue untouched",
|
|
next.name
|
|
);
|
|
state.current = Some(next);
|
|
}
|
|
},
|
|
Err(e) => {
|
|
// The previous "now playing" is no longer accurate.
|
|
state.current = None;
|
|
error!(
|
|
"Failed to play {} ({e}); leaving in queue",
|
|
next.path
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
thread::sleep(std::time::Duration::from_millis(200));
|
|
}
|
|
}
|