make first runnable version

This commit is contained in:
2026-06-06 18:01:59 +02:00
parent 6a3817d835
commit 94e4de8051
21 changed files with 1664 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
use rodio::{Decoder, OutputStream, Sink};
use std::fs::File;
use std::io::BufReader;
pub struct AudioEngine {
sink: Option<Sink>,
_stream: Option<OutputStream>,
}
impl AudioEngine {
pub fn new() -> Self {
Self {
sink: None,
_stream: None,
}
}
pub fn play(&mut self, path: &str) {
let (_stream, handle) = OutputStream::try_default().unwrap();
let sink = Sink::try_new(&handle).unwrap();
let file = File::open(path).unwrap();
let source = Decoder::new(BufReader::new(file)).unwrap();
sink.append(source);
self.sink = Some(sink);
self._stream = Some(_stream);
}
pub fn is_playing(&self) -> bool {
match &self.sink {
Some(s) => !s.empty(),
None => false,
}
}
}
View File
View File
View File
View File
View File
+36
View File
@@ -0,0 +1,36 @@
use std::sync::{Arc, Mutex};
use std::thread;
use crate::{
state::DaemonState,
socket::start_socket,
db::init_db,
audio::AudioEngine,
};
pub fn run() {
let state = Arc::new(Mutex::new(DaemonState::new()));
init_db();
let socket_state = state.clone();
thread::spawn(move || {
start_socket(socket_state);
});
let mut audio = AudioEngine::new();
loop {
{
let mut state = state.lock().unwrap();
if !state.queue.is_empty() && !audio.is_playing() {
let song = state.queue.remove(0);
audio.play(&song.path);
state.current = Some(song);
}
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
+16
View File
@@ -0,0 +1,16 @@
use rusqlite::Connection;
use crate::paths::db_path;
pub fn init_db() {
let conn = Connection::open(db_path()).unwrap();
conn.execute(
"CREATE TABLE IF NOT EXISTS songs (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE,
path TEXT
)",
[],
).unwrap();
}
View File
View File
View File
View File
+11
View File
@@ -0,0 +1,11 @@
mod daemon;
mod socket;
mod protocol;
mod state;
mod db;
mod audio;
mod paths;
fn main() {
daemon::run();
}
+36
View File
@@ -0,0 +1,36 @@
use std::path::PathBuf;
pub fn base_dir() -> PathBuf {
let mut p = dirs::home_dir().unwrap();
p.push(".sjn/music");
p
}
pub fn songs_dir() -> PathBuf {
let mut p = base_dir();
p.push("songs");
p
}
pub fn song_path(name: &str) -> String {
let safe: String = name
.chars()
.map(|c| {
if c.is_alphanumeric() || c == ' ' {
c
} else {
'_'
}
})
.collect();
let mut p = songs_dir();
p.push(format!("{safe}.mp3"));
p.to_string_lossy().to_string()
}
pub fn db_path() -> String {
let mut p = base_dir();
p.push("songs.db");
p.to_string_lossy().to_string()
}
+9
View File
@@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum Command {
Play(String),
Add(String),
Del(String),
Init(String),
}
View File
View File
+42
View File
@@ -0,0 +1,42 @@
use std::io::{Read, Write};
use std::os::unix::net::UnixListener;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::{protocol::Command, state::DaemonState};
pub fn start_socket(state: Arc<Mutex<DaemonState>>) {
let _ = std::fs::remove_file("/tmp/sjnmusic.sock");
let listener = UnixListener::bind("/tmp/sjnmusic.sock").unwrap();
for stream in listener.incoming() {
let state = state.clone();
thread::spawn(move || {
let mut stream = stream.unwrap();
let mut buf = [0; 2048];
let size = stream.read(&mut buf).unwrap();
let msg = String::from_utf8_lossy(&buf[..size]);
if let Ok(cmd) = serde_json::from_str::<Command>(&msg) {
handle(cmd, state);
}
let _ = stream.write_all(b"OK");
});
}
}
fn handle(cmd: Command, state: Arc<Mutex<DaemonState>>) {
let mut state = state.lock().unwrap();
match cmd {
Command::Play(song) => state.play(song),
Command::Add(song) => state.add(song),
Command::Del(song) => state.delete(song),
Command::Init(song) => state.init(song),
}
}
+66
View File
@@ -0,0 +1,66 @@
use std::process::Command as SysCommand;
#[derive(Clone)]
pub struct Song {
pub name: String,
pub path: String,
}
pub struct DaemonState {
pub queue: Vec<Song>,
pub current: Option<Song>,
}
impl DaemonState {
pub fn new() -> Self {
Self {
queue: Vec::new(),
current: None,
}
}
pub fn play(&mut self, name: String) {
let path = crate::paths::song_path(&name);
self.queue.insert(0, Song { name, path });
}
pub fn add(&mut self, name: String) {
let path = crate::paths::song_path(&name);
self.queue.push(Song { name, path });
}
pub fn delete(&mut self, name: String) {
let path = crate::paths::song_path(&name);
let _ = std::fs::remove_file(&path);
self.queue.retain(|s| s.name != name);
}
pub fn init(&mut self, name: String) {
let path = crate::paths::song_path(&name);
let result = SysCommand::new("yt-dlp")
.args([
"-x",
"--audio-format",
"mp3",
"-o",
&path,
&format!("ytsearch1:{}", name),
])
.output();
if result.is_err() {
eprintln!("INIT failed: yt-dlp not available or download error");
return;
}
self.queue.push(Song {
name,
path,
});
}
}