feat: playback-via-mpv, rust 2024

This commit is contained in:
Mark
2026-04-15 22:07:49 +02:00
committed by Mark
parent 8d4d418166
commit ab17477285
12 changed files with 460 additions and 94 deletions

View File

@@ -46,6 +46,77 @@ https://github.com/Dummi26/musicdb/assets/67615357/afb0c9fa-3cf0-414a-a59f-7e462
# Setup # Setup
## building
Run `cargo build --release` in `musicdb-filldb`, `musicdb-server`, and `musicdb-client`.
The executable files will be `musicdb-*/target/release/musicdb-*`.
### building the server
You may need to install `libasound2-dev` or similar packages,
as the default audio backend likely requires it.
If this is the case, you will likely see `error: ld returned 1 exit status`.
Multiple backends are available for playing audio,
but using `default-playback` should be fine most of the time.
If compilation fails or you experience an audio related issue,
try compiling with a different backend and see if the issue persists:
```sh
# in musicdb-server
cargo build --release --no-default-features --features website,playback-via-$backend
```
#### backends
The `rodio` and `playback-rs` backends are named after the libraries they use.
These pull in dependencies and may try to link to system libraries.
The `mpv` backend executes the `mpv` media player to play audio.
If you can't compile other backends, use this.
This backend will spawn up to two `mpv` processes at a time
and control them using unix sockets in `/tmp/`.
The `sleep` backend doesn't play any audio. Instead,
it waits for the duration of the song and then moves
on to the next one. If you want to connect
(audio-playing) servers or (web) clients
to one server where the audio files are stored but which
doesn't play any audio itself, you can use this backend.
If the duration of a song is unknown, the song will be played
for a duration of zero seconds, meaning it will just get skipped
(although connected servers or clients may still load the song
and may even succeed in playing it).
#### other features
The `website` feature is required for `--web <addr>` to work.
It allows the server to handle http requests so that it
can be controlled without using `musicdb-client`.
Turning this off improves compile times and stuff,
but leaving the feature on is quite useful
(e.g. to pause/resume playback from a phone).
### building the client
The client can play audio in sync with the server.
If you don't need this, you don't need to compile it:
```sh
# in musicdb-client
cargo build --release --no-default-features --features gui
```
Instead of disabling playback, you can also choose
from the backends available to the server.
Read "compiling the server" for more information.
In the future, it may make sense to disable the `gui` feature too
(e.g. the client may act as a cli tool for scripts), but not yet.
## setup.sh
Review, then run the `setup.sh` script: Review, then run the `setup.sh` script:
```sh ```sh
@@ -64,8 +135,11 @@ font = '/usr/share/fonts/...'
... ...
``` ```
The script will start a server and client. ## manually
After closing the client, the server may still be running, so you may have to `pkill musicdb-server` if you want to stop it.
The `setup.sh` script will start a server and client.
After closing the client, the server remains active.
You have to `pkill musicdb-server` if you want to stop it.
To open the player again: To open the player again:
@@ -73,7 +147,7 @@ To open the player again:
musicdb-client 0.0.0.0:26002 gui musicdb-client 0.0.0.0:26002 gui
``` ```
To start the server: To start the server without `setup.sh`:
```sh ```sh
musicdb-server --tcp 0.0.0.0:26002 --play-audio local ~/my_dbdir ~/music musicdb-server --tcp 0.0.0.0:26002 --play-audio local ~/my_dbdir ~/music

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "musicdb-client" name = "musicdb-client"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -32,5 +32,7 @@ merscfg = []
mers = [] mers = []
playback = [] playback = []
default-playback = ["playback", "musicdb-lib/default-playback"] default-playback = ["playback", "musicdb-lib/default-playback"]
playback-via-sleep = ["playback", "musicdb-lib/playback-via-sleep"]
playback-via-mpv = ["playback", "musicdb-lib/playback-via-mpv"]
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"] playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"] playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"]

View File

@@ -1,13 +1,13 @@
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{Arc, atomic::AtomicBool};
use musicdb_lib::data::ArtistId; use musicdb_lib::data::ArtistId;
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
use crate::{ use crate::{
gui::{rect_from_rel, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage}, gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage, rect_from_rel},
gui_anim::AnimationController, gui_anim::AnimationController,
gui_base::Button, gui_base::Button,
gui_playback::{get_right_x, image_display, CurrentInfo}, gui_playback::{CurrentInfo, get_right_x, image_display},
gui_playpause::PlayPause, gui_playpause::PlayPause,
gui_text::{AdvancedLabel, Label}, gui_text::{AdvancedLabel, Label},
}; };
@@ -126,7 +126,7 @@ impl GuiElem for IdleDisplay {
self.c_top_label.content = if let Some(song) = self.current_info.current_song { self.c_top_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
.idle_top_text .idle_top_text
.gen(&info.database, info.database.get_song(&song)) .gen_new(&info.database, info.database.get_song(&song))
} else { } else {
vec![] vec![]
}; };
@@ -134,7 +134,7 @@ impl GuiElem for IdleDisplay {
self.c_side1_label.content = if let Some(song) = self.current_info.current_song { self.c_side1_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
.idle_side1_text .idle_side1_text
.gen(&info.database, info.database.get_song(&song)) .gen_new(&info.database, info.database.get_song(&song))
} else { } else {
vec![] vec![]
}; };
@@ -142,7 +142,7 @@ impl GuiElem for IdleDisplay {
self.c_side2_label.content = if let Some(song) = self.current_info.current_song { self.c_side2_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
.idle_side2_text .idle_side2_text
.gen(&info.database, info.database.get_song(&song)) .gen_new(&info.database, info.database.get_song(&song))
} else { } else {
vec![] vec![]
}; };

View File

@@ -1,11 +1,11 @@
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{Arc, atomic::AtomicBool};
use speedy2d::{dimen::Vec2, shape::Rectangle}; use speedy2d::{dimen::Vec2, shape::Rectangle};
use crate::{ use crate::{
gui::{DrawInfo, GuiElem, GuiElemCfg}, gui::{DrawInfo, GuiElem, GuiElemCfg},
gui_anim::AnimationController, gui_anim::AnimationController,
gui_playback::{image_display, CurrentInfo}, gui_playback::{CurrentInfo, image_display},
gui_playpause::PlayPause, gui_playpause::PlayPause,
gui_text::AdvancedLabel, gui_text::AdvancedLabel,
}; };
@@ -61,7 +61,7 @@ impl GuiElem for StatusBar {
self.c_song_label.content = if let Some(song) = self.current_info.current_song { self.c_song_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
.status_bar_text .status_bar_text
.gen(&info.database, info.database.get_song(&song)) .gen_new(&info.database, info.database.get_song(&song))
} else { } else {
vec![] vec![]
}; };

View File

@@ -3,7 +3,7 @@ use std::{
str::{Chars, FromStr}, str::{Chars, FromStr},
}; };
use musicdb_lib::data::{database::Database, song::Song, CoverId, GeneralData}; use musicdb_lib::data::{CoverId, GeneralData, database::Database, song::Song};
use speedy2d::color::Color; use speedy2d::color::Color;
use crate::gui_text::{AdvancedContent, Content, ImageSource}; use crate::gui_text::{AdvancedContent, Content, ImageSource};
@@ -38,7 +38,7 @@ pub enum TextPart {
ImgCustom(TextBuilder), ImgCustom(TextBuilder),
} }
impl TextBuilder { impl TextBuilder {
pub fn gen( pub fn gen_new(
&self, &self,
db: &Database, db: &Database,
current_song: Option<&Song>, current_song: Option<&Song>,
@@ -151,40 +151,39 @@ impl TextBuilder {
} }
} }
TextPart::TagEq(p) => { TextPart::TagEq(p) => {
for (i, gen) in all_general(db, &current_song).into_iter().enumerate() { for (i, g) in all_general(db, &current_song).into_iter().enumerate() {
if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) { if let Some(_) = g.and_then(|g| g.tags.iter().find(|t| *t == p)) {
push!(match i { push!(
match i {
0 => 's', 0 => 's',
1 => 'a', 1 => 'a',
2 => 'A', 2 => 'A',
_ => unreachable!("array length should be 3"), _ => unreachable!("array length should be 3"),
} }
.to_string()); .to_string()
);
break; break;
} }
} }
} }
TextPart::TagEnd(p) => { TextPart::TagEnd(p) => {
for gen in all_general(db, &current_song) { for g in all_general(db, &current_song) {
if let Some(t) = if let Some(t) = g.and_then(|g| g.tags.iter().find(|t| t.starts_with(p))) {
gen.and_then(|gen| gen.tags.iter().find(|t| t.starts_with(p)))
{
push!(t[p.len()..].to_owned()); push!(t[p.len()..].to_owned());
break; break;
} }
} }
} }
TextPart::TagContains(p) => { TextPart::TagContains(p) => {
for gen in all_general(db, &current_song) { for g in all_general(db, &current_song) {
if let Some(t) = gen.and_then(|gen| gen.tags.iter().find(|t| t.contains(p))) if let Some(t) = g.and_then(|g| g.tags.iter().find(|t| t.contains(p))) {
{
push!(t.to_owned()); push!(t.to_owned());
break; break;
} }
} }
} }
TextPart::If(condition, yes, no) => { TextPart::If(condition, yes, no) => {
if !condition.gen(db, current_song).is_empty() { if !condition.gen_new(db, current_song).is_empty() {
yes.gen_to(db, current_song, out, line, scale, align, color); yes.gen_to(db, current_song, out, line, scale, align, color);
} else { } else {
no.gen_to(db, current_song, out, line, scale, align, color); no.gen_to(db, current_song, out, line, scale, align, color);
@@ -195,7 +194,7 @@ impl TextBuilder {
} }
TextPart::ImgCustom(path) => { TextPart::ImgCustom(path) => {
push_img!(ImageSource::CustomFile( push_img!(ImageSource::CustomFile(
path.gen(db, current_song) path.gen_new(db, current_song)
.into_iter() .into_iter()
.flat_map(|v| v.into_iter().map(|(v, _, _)| v.to_string())) .flat_map(|v| v.into_iter().map(|(v, _, _)| v.to_string()))
.collect() .collect()
@@ -326,7 +325,7 @@ impl TextBuilder {
None => { None => {
return Err(TextBuilderParseError::InvalidImageSourceName( return Err(TextBuilderParseError::InvalidImageSourceName(
src, src,
)) ));
} }
Some(':') => break, Some(':') => break,
Some(c) => src.push(c), Some(c) => src.push(c),
@@ -349,7 +348,7 @@ impl TextBuilder {
} }
"CustomFile" => TextPart::ImgCustom(Self::from_chars(chars)?), "CustomFile" => TextPart::ImgCustom(Self::from_chars(chars)?),
_ => { _ => {
return Err(TextBuilderParseError::InvalidImageSourceName(src)) return Err(TextBuilderParseError::InvalidImageSourceName(src));
} }
}); });
} }
@@ -419,7 +418,10 @@ impl Display for TextBuilderParseError {
write!(f, "Unknown tag mode '{mode}': Allowed are only _, > or =.") write!(f, "Unknown tag mode '{mode}': Allowed are only _, > or =.")
} }
Self::TooFewCharsForColor => write!(f, "Too few chars for color: Syntax is \\cRRGGBB."), Self::TooFewCharsForColor => write!(f, "Too few chars for color: Syntax is \\cRRGGBB."),
Self::ColorNotHex => write!(f, "Color value wasn't a hex number! Syntax is \\cRRGGBB, where R, G, and B are values from 0-9 and A-F (hex 0-F)."), Self::ColorNotHex => write!(
f,
"Color value wasn't a hex number! Syntax is \\cRRGGBB, where R, G, and B are values from 0-9 and A-F (hex 0-F)."
),
Self::CouldntParse(v, t) => write!(f, "Couldn't parse value '{v}' to type '{t}'."), Self::CouldntParse(v, t) => write!(f, "Couldn't parse value '{v}' to type '{t}'."),
Self::InvalidImageSourceName(name) => write!(f, "Invalid image source name: '{name}'."), Self::InvalidImageSourceName(name) => write!(f, "Invalid image source name: '{name}'."),
Self::InvalidImageCoverId(id) => write!(f, "Invalid image cover id: '{id}'."), Self::InvalidImageCoverId(id) => write!(f, "Invalid image cover id: '{id}'."),

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "musicdb-lib" name = "musicdb-lib"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
base64 = "0.22.1" base64 = "0.22.1"
@@ -18,5 +18,6 @@ playback = []
default-playback = ["playback-via-playback-rs"] default-playback = ["playback-via-playback-rs"]
# default-playback = ["playback-via-rodio"] # default-playback = ["playback-via-rodio"]
playback-via-sleep = ["playback"] playback-via-sleep = ["playback"]
playback-via-mpv = ["playback"]
playback-via-playback-rs = ["playback", "dep:playback-rs"] playback-via-playback-rs = ["playback", "dep:playback-rs"]
playback-via-rodio = ["playback", "dep:rodio"] playback-via-rodio = ["playback", "dep:rodio"]

View File

@@ -1,5 +1,5 @@
use std::{ use std::{
collections::{HashMap, VecDeque}, collections::{BTreeMap, HashMap, VecDeque},
io::{Read, Write}, io::{Read, Write},
path::PathBuf, path::PathBuf,
}; };
@@ -164,6 +164,34 @@ where
Ok(o) Ok(o)
} }
} }
impl<K, V> ToFromBytes for BTreeMap<K, V>
where
K: ToFromBytes + std::cmp::Ord,
V: ToFromBytes,
{
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.len().to_bytes(s)?;
for (key, val) in self.iter() {
key.to_bytes(s)?;
val.to_bytes(s)?;
}
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let len = ToFromBytes::from_bytes(s)?;
let mut o = Self::new();
for _ in 0..len {
o.insert(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?);
}
Ok(o)
}
}
// - for (i/u)(size/8/16/32/64/128) // - for (i/u)(size/8/16/32/64/128)

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "playback-via-mpv")]
pub mod mpv;
#[cfg(feature = "playback-via-playback-rs")] #[cfg(feature = "playback-via-playback-rs")]
pub mod playback_rs; pub mod playback_rs;
#[cfg(feature = "playback-via-rodio")] #[cfg(feature = "playback-via-rodio")]
@@ -8,14 +10,18 @@ pub mod sleep;
pub type PlayerBackendFeat<T> = playback_rs::PlayerBackendPlaybackRs<T>; pub type PlayerBackendFeat<T> = playback_rs::PlayerBackendPlaybackRs<T>;
#[cfg(feature = "playback-via-rodio")] #[cfg(feature = "playback-via-rodio")]
pub type PlayerBackendFeat<T> = rodio::PlayerBackendRodio<T>; pub type PlayerBackendFeat<T> = rodio::PlayerBackendRodio<T>;
#[cfg(feature = "playback-via-sleep")]
pub type PlayerBackendFeat<T> = sleep::PlayerBackendSleep<T>;
#[cfg(feature = "playback-via-mpv")]
pub type PlayerBackendFeat<T> = mpv::PlayerBackendMpv<T>;
use std::{collections::HashMap, ffi::OsStr, sync::Arc}; use std::{collections::HashMap, ffi::OsStr, sync::Arc};
use crate::{ use crate::{
data::{ data::{
SongId,
database::Database, database::Database,
song::{CachedData, Song}, song::{CachedData, Song},
SongId,
}, },
server::Action, server::Action,
}; };

View File

@@ -0,0 +1,252 @@
use std::{
ffi::OsStr,
io::Write,
os::unix::net::UnixStream,
process::Stdio,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use crate::{
data::{SongId, song::Song},
server::{Action, Command},
};
use super::PlayerBackend;
const IPC_QUIT: &[u8] = b"{\"command\":[\"quit\"]}\n";
const IPC_PAUSE: &[u8] = b"{\"command\":[\"set_property\",\"pause\",true]}\n";
const IPC_RESUME: &[u8] = b"{\"command\":[\"set_property\",\"pause\",false]}\n";
const IPC_STOP: &[u8] =
b"{\"command\":[\"set_property\",\"pause\",true]}\n{\"command\":[\"seek\",0,\"absolute\"]}\n";
pub struct PlayerBackendMpv<T> {
id: (u32, u8),
current: Option<(SongId, Option<(UnixStream, bool, Arc<AtomicBool>)>, T)>,
next: Option<(SongId, Option<(UnixStream, bool, Arc<AtomicBool>)>, T)>,
/// unused, but could be used to do something smarter than polling at some point
#[allow(unused)]
command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
}
impl<T> PlayerBackendMpv<T> {
pub fn new(
command_sender: std::sync::mpsc::Sender<(Command, Option<u64>)>,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(Some(command_sender))
}
pub fn new_without_command_sending() -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(None)
}
pub fn new_with_optional_command_sending(
command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
id: (std::process::id(), 0),
current: None,
next: None,
command_sender,
})
}
}
impl<T> PlayerBackend<T> for PlayerBackendMpv<T> {
fn load_next_song(
&mut self,
id: SongId,
_song: &Song,
_filename: &OsStr,
bytes: Arc<Vec<u8>>,
_load_duration: bool,
custom_data: T,
) {
if let Some((_, Some((mut ipc, _, quit)), _)) = self.next.take() {
quit.store(true, Ordering::Release);
ipc.write_all(IPC_QUIT).ok();
}
self.id.1 = 1 + (self.id.1 % 9);
let ipc_path = format!("/tmp/musicdb-server-mpv-ipc-{:X}-{}", self.id.0, self.id.1);
match std::process::Command::new("mpv")
.args(["--no-config", "--no-video", "--pause"])
.arg(format!("--input-ipc-server={ipc_path}"))
.args(["--no-terminal", "--cache=yes", "--cache-on-disk=no", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(mut proc) => {
let quit = Arc::new(AtomicBool::new(false));
for i in 1..=34 {
std::thread::sleep(Duration::from_millis(100 * i));
if let Ok(ipc) = UnixStream::connect(&ipc_path) {
self.next = Some((id, Some((ipc, false, Arc::clone(&quit))), custom_data));
break;
}
}
if self.next.is_some()
&& let Some(mut stdin) = proc.stdin.take()
{
let s = self.command_sender.clone();
std::thread::spawn(move || {
stdin.write_all(&bytes).ok();
drop(stdin);
match proc.wait() {
Ok(status) => {
if quit.load(Ordering::Acquire) {
return;
}
quit.store(true, Ordering::Release);
if let Some(s) = &s {
if status.success() {
eprintln!("mpv exited, success");
s.send((Action::NextSong.cmd(0xFFu8), None)).unwrap();
} else {
s.send((
Action::ErrorInfo(
"mpv process crashed!".to_owned(),
format!(
"Exit code: {}",
status
.code()
.map(|n| n.to_string())
.unwrap_or("unknown".to_owned())
),
)
.cmd(0xFFu8),
None,
))
.unwrap();
}
}
}
Err(e) => {
if quit.load(Ordering::Acquire) {
return;
}
quit.store(true, Ordering::Release);
if let Some(s) = &s {
s.send((
Action::ErrorInfo(
"Error waiting for mpv to exit!".to_owned(),
format!("Error: {e}"),
)
.cmd(0xFFu8),
None,
))
.unwrap();
}
}
}
});
} else {
proc.kill().ok();
if let Some(s) = &self.command_sender {
s.send((
Action::ErrorInfo(
"Error waiting for mpv to start!".to_owned(),
"Could not get process' stdin or could not connect to ipc socket."
.to_owned(),
)
.cmd(0xFFu8),
None,
))
.unwrap();
}
}
}
Err(e) => {
if let Some(s) = &self.command_sender {
s.send((
Action::ErrorInfo(
"Error starting mpv process!".to_owned(),
format!("Error: {e}"),
)
.cmd(0xFFu8),
None,
))
.unwrap();
}
}
}
}
fn pause(&mut self) {
if let Some((_, Some((ipc, playing, _)), _)) = &mut self.current {
ipc.write_all(IPC_PAUSE).ok();
ipc.flush().ok();
*playing = false;
}
}
fn stop(&mut self) {
if let Some((_, Some((ipc, playing, _)), _)) = &mut self.current {
ipc.write_all(IPC_STOP).ok();
ipc.flush().ok();
*playing = false;
}
}
fn resume(&mut self) {
if let Some((_, Some((ipc, playing, _)), _)) = &mut self.current {
ipc.write_all(IPC_RESUME).ok();
ipc.flush().ok();
*playing = true;
}
}
fn next(&mut self, play: bool, _load_duration: bool) {
if let Some((_, Some((mut ipc, _, quit)), _)) = self.current.take() {
quit.store(true, Ordering::Release);
ipc.write_all(IPC_QUIT).ok();
}
self.current = self.next.take();
if play {
self.resume();
}
}
fn clear(&mut self) {
self.next(false, false);
self.next(false, false);
}
fn playing(&self) -> bool {
if let Some((_, Some((_, playing, _)), _)) = self.current {
playing
} else {
false
}
}
fn current_song(&self) -> Option<(SongId, bool, &T)> {
self.current
.as_ref()
.map(|(id, _, custom)| (*id, true, custom))
}
fn next_song(&self) -> Option<(SongId, bool, &T)> {
self.next
.as_ref()
.map(|(id, _, custom)| (*id, true, custom))
}
fn gen_data_mut(&mut self) -> (Option<&mut T>, Option<&mut T>) {
(
self.current.as_mut().map(|(_, _, t)| t),
self.next.as_mut().map(|(_, _, t)| t),
)
}
fn song_finished_polling(&self) -> bool {
self.command_sender.is_none()
}
fn song_finished(&self) -> bool {
if self.command_sender.is_none()
&& let Some((_, Some((_, _, quit)), _)) = &self.current
{
quit.load(Ordering::Relaxed)
} else {
false
}
}
fn current_song_duration(&self) -> Option<u64> {
None
}
fn current_song_playback_position(&self) -> Option<u64> {
None
}
}

View File

@@ -5,7 +5,7 @@ use std::{
}; };
use crate::{ use crate::{
data::{song::Song, SongId}, data::{SongId, song::Song},
server::Command, server::Command,
}; };
@@ -28,6 +28,14 @@ enum SongFinished {
impl<T> PlayerBackendSleep<T> { impl<T> PlayerBackendSleep<T> {
pub fn new( pub fn new(
command_sender: std::sync::mpsc::Sender<(Command, Option<u64>)>,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(Some(command_sender))
}
pub fn new_without_command_sending() -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(None)
}
pub fn new_with_optional_command_sending(
command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>, command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { Ok(Self {

View File

@@ -3,7 +3,7 @@ pub mod get;
use std::{ use std::{
io::{BufRead as _, BufReader, Read, Write}, io::{BufRead as _, BufReader, Read, Write},
net::TcpListener, net::TcpListener,
sync::{mpsc, Arc, Mutex}, sync::{Arc, Mutex, mpsc},
thread, thread,
time::Duration, time::Duration,
}; };
@@ -15,12 +15,12 @@ use crate::player::Player;
use crate::server::get::handle_one_connection_as_get; use crate::server::get::handle_one_connection_as_get;
use crate::{ use crate::{
data::{ data::{
AlbumId, ArtistId, SongId,
album::Album, album::Album,
artist::Artist, artist::Artist,
database::{Cover, Database, UpdateEndpoint}, database::{Cover, Database, UpdateEndpoint},
queue::Queue, queue::Queue,
song::Song, song::Song,
AlbumId, ArtistId, SongId,
}, },
load::ToFromBytes, load::ToFromBytes,
}; };
@@ -284,23 +284,15 @@ pub fn run_server_caching_thread_opt(
) { ) {
#[cfg(not(feature = "playback"))] #[cfg(not(feature = "playback"))]
if play_audio { if play_audio {
panic!("Can't run the server: cannot play audio because the `playback` feature was disabled when compiling, but `play_audio` was set to `true`!"); panic!(
"Can't run the server: cannot play audio because the `playback` feature was disabled when compiling, but `play_audio` was set to `true`!"
);
} }
use std::time::Instant; use std::time::Instant;
use crate::data::cache_manager::CacheManager; use crate::data::cache_manager::CacheManager;
#[cfg(feature = "playback-via-playback-rs")] #[cfg(any(feature = "playback"))]
use crate::player::playback_rs::PlayerBackendPlaybackRs;
#[cfg(feature = "playback-via-rodio")]
use crate::player::rodio::PlayerBackendRodio;
#[cfg(feature = "playback-via-sleep")]
use crate::player::sleep::PlayerBackendSleep;
#[cfg(any(
feature = "playback",
feature = "playback-via-playback-rs",
feature = "playback-via-rodio"
))]
use crate::player::PlayerBackend; use crate::player::PlayerBackend;
// commands sent to this will be handeled later in this function in an infinite loop. // commands sent to this will be handeled later in this function in an infinite loop.
@@ -309,12 +301,8 @@ pub fn run_server_caching_thread_opt(
#[cfg(feature = "playback")] #[cfg(feature = "playback")]
let mut player = if play_audio { let mut player = if play_audio {
#[cfg(feature = "playback-via-sleep")] use crate::player::PlayerBackendFeat;
let backend = PlayerBackendSleep::new(Some(command_sender.clone())).unwrap(); let backend = PlayerBackendFeat::new(command_sender.clone()).unwrap();
#[cfg(feature = "playback-via-playback-rs")]
let backend = PlayerBackendPlaybackRs::new(command_sender.clone()).unwrap();
#[cfg(feature = "playback-via-rodio")]
let backend = PlayerBackendRodio::new(command_sender.clone()).unwrap();
Some(Player::new(backend)) Some(Player::new(backend))
} else { } else {
None None
@@ -336,7 +324,8 @@ pub fn run_server_caching_thread_opt(
Ok(v) => { Ok(v) => {
let command_sender = command_sender.clone(); let command_sender = command_sender.clone();
let db = Arc::clone(&database); let db = Arc::clone(&database);
thread::spawn(move || loop { thread::spawn(move || {
loop {
if let Ok((connection, _con_addr)) = v.accept() { if let Ok((connection, _con_addr)) = v.accept() {
let command_sender = command_sender.clone(); let command_sender = command_sender.clone();
let db = Arc::clone(&db); let db = Arc::clone(&db);
@@ -363,7 +352,9 @@ pub fn run_server_caching_thread_opt(
&command_sender, &command_sender,
None, None,
), ),
"get" => _ = handle_one_connection_as_get(db, &mut connection), "get" => {
_ = handle_one_connection_as_get(db, &mut connection)
}
_ => { _ => {
_ = connection _ = connection
.into_inner() .into_inner()
@@ -373,6 +364,7 @@ pub fn run_server_caching_thread_opt(
} }
}); });
} }
}
}); });
} }
Err(e) => { Err(e) => {

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "musicdb-server" name = "musicdb-server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -27,5 +27,6 @@ website = ["dep:tokio", "dep:rocket", "dep:html-escape"]
playback = [] playback = []
default-playback = ["playback", "musicdb-lib/default-playback"] default-playback = ["playback", "musicdb-lib/default-playback"]
playback-via-sleep = ["playback", "musicdb-lib/playback-via-sleep"] playback-via-sleep = ["playback", "musicdb-lib/playback-via-sleep"]
playback-via-mpv = ["playback", "musicdb-lib/playback-via-mpv"]
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"] playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"] playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"]