make first runnable version
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod daemon;
|
||||
mod socket;
|
||||
mod protocol;
|
||||
mod state;
|
||||
mod db;
|
||||
mod audio;
|
||||
mod paths;
|
||||
|
||||
fn main() {
|
||||
daemon::run();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Command {
|
||||
Play(String),
|
||||
Add(String),
|
||||
Del(String),
|
||||
Init(String),
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user