make first runnable version
This commit is contained in:
Generated
+1400
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "sjnmusicd"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rusqlite = "0.32"
|
||||||
|
rodio = "0.19"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
dirs = "5"
|
||||||
@@ -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