mirror of
https://github.com/Dummi26/musicdb.git
synced 2026-04-28 09:39:58 +02:00
feat: playback-via-mpv, rust 2024
This commit is contained in:
80
README.md
80
README.md
@@ -46,6 +46,77 @@ https://github.com/Dummi26/musicdb/assets/67615357/afb0c9fa-3cf0-414a-a59f-7e462
|
||||
|
||||
# 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:
|
||||
|
||||
```sh
|
||||
@@ -64,8 +135,11 @@ font = '/usr/share/fonts/...'
|
||||
...
|
||||
```
|
||||
|
||||
The script will start a server and client.
|
||||
After closing the client, the server may still be running, so you may have to `pkill musicdb-server` if you want to stop it.
|
||||
## manually
|
||||
|
||||
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:
|
||||
|
||||
@@ -73,7 +147,7 @@ To open the player again:
|
||||
musicdb-client 0.0.0.0:26002 gui
|
||||
```
|
||||
|
||||
To start the server:
|
||||
To start the server without `setup.sh`:
|
||||
|
||||
```sh
|
||||
musicdb-server --tcp 0.0.0.0:26002 --play-audio local ~/my_dbdir ~/music
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "musicdb-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -32,5 +32,7 @@ merscfg = []
|
||||
mers = []
|
||||
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-rodio = ["playback", "musicdb-lib/playback-via-rodio"]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use musicdb_lib::data::ArtistId;
|
||||
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
|
||||
|
||||
use crate::{
|
||||
gui::{rect_from_rel, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage},
|
||||
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage, rect_from_rel},
|
||||
gui_anim::AnimationController,
|
||||
gui_base::Button,
|
||||
gui_playback::{get_right_x, image_display, CurrentInfo},
|
||||
gui_playback::{CurrentInfo, get_right_x, image_display},
|
||||
gui_playpause::PlayPause,
|
||||
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 {
|
||||
info.gui_config
|
||||
.idle_top_text
|
||||
.gen(&info.database, info.database.get_song(&song))
|
||||
.gen_new(&info.database, info.database.get_song(&song))
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -134,7 +134,7 @@ impl GuiElem for IdleDisplay {
|
||||
self.c_side1_label.content = if let Some(song) = self.current_info.current_song {
|
||||
info.gui_config
|
||||
.idle_side1_text
|
||||
.gen(&info.database, info.database.get_song(&song))
|
||||
.gen_new(&info.database, info.database.get_song(&song))
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -142,7 +142,7 @@ impl GuiElem for IdleDisplay {
|
||||
self.c_side2_label.content = if let Some(song) = self.current_info.current_song {
|
||||
info.gui_config
|
||||
.idle_side2_text
|
||||
.gen(&info.database, info.database.get_song(&song))
|
||||
.gen_new(&info.database, info.database.get_song(&song))
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use speedy2d::{dimen::Vec2, shape::Rectangle};
|
||||
|
||||
use crate::{
|
||||
gui::{DrawInfo, GuiElem, GuiElemCfg},
|
||||
gui_anim::AnimationController,
|
||||
gui_playback::{image_display, CurrentInfo},
|
||||
gui_playback::{CurrentInfo, image_display},
|
||||
gui_playpause::PlayPause,
|
||||
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 {
|
||||
info.gui_config
|
||||
.status_bar_text
|
||||
.gen(&info.database, info.database.get_song(&song))
|
||||
.gen_new(&info.database, info.database.get_song(&song))
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
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 crate::gui_text::{AdvancedContent, Content, ImageSource};
|
||||
@@ -38,7 +38,7 @@ pub enum TextPart {
|
||||
ImgCustom(TextBuilder),
|
||||
}
|
||||
impl TextBuilder {
|
||||
pub fn gen(
|
||||
pub fn gen_new(
|
||||
&self,
|
||||
db: &Database,
|
||||
current_song: Option<&Song>,
|
||||
@@ -151,40 +151,39 @@ impl TextBuilder {
|
||||
}
|
||||
}
|
||||
TextPart::TagEq(p) => {
|
||||
for (i, gen) in all_general(db, ¤t_song).into_iter().enumerate() {
|
||||
if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) {
|
||||
push!(match i {
|
||||
for (i, g) in all_general(db, ¤t_song).into_iter().enumerate() {
|
||||
if let Some(_) = g.and_then(|g| g.tags.iter().find(|t| *t == p)) {
|
||||
push!(
|
||||
match i {
|
||||
0 => 's',
|
||||
1 => 'a',
|
||||
2 => 'A',
|
||||
_ => unreachable!("array length should be 3"),
|
||||
}
|
||||
.to_string());
|
||||
.to_string()
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
TextPart::TagEnd(p) => {
|
||||
for gen in all_general(db, ¤t_song) {
|
||||
if let Some(t) =
|
||||
gen.and_then(|gen| gen.tags.iter().find(|t| t.starts_with(p)))
|
||||
{
|
||||
for g in all_general(db, ¤t_song) {
|
||||
if let Some(t) = g.and_then(|g| g.tags.iter().find(|t| t.starts_with(p))) {
|
||||
push!(t[p.len()..].to_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
TextPart::TagContains(p) => {
|
||||
for gen in all_general(db, ¤t_song) {
|
||||
if let Some(t) = gen.and_then(|gen| gen.tags.iter().find(|t| t.contains(p)))
|
||||
{
|
||||
for g in all_general(db, ¤t_song) {
|
||||
if let Some(t) = g.and_then(|g| g.tags.iter().find(|t| t.contains(p))) {
|
||||
push!(t.to_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
no.gen_to(db, current_song, out, line, scale, align, color);
|
||||
@@ -195,7 +194,7 @@ impl TextBuilder {
|
||||
}
|
||||
TextPart::ImgCustom(path) => {
|
||||
push_img!(ImageSource::CustomFile(
|
||||
path.gen(db, current_song)
|
||||
path.gen_new(db, current_song)
|
||||
.into_iter()
|
||||
.flat_map(|v| v.into_iter().map(|(v, _, _)| v.to_string()))
|
||||
.collect()
|
||||
@@ -326,7 +325,7 @@ impl TextBuilder {
|
||||
None => {
|
||||
return Err(TextBuilderParseError::InvalidImageSourceName(
|
||||
src,
|
||||
))
|
||||
));
|
||||
}
|
||||
Some(':') => break,
|
||||
Some(c) => src.push(c),
|
||||
@@ -349,7 +348,7 @@ impl TextBuilder {
|
||||
}
|
||||
"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 =.")
|
||||
}
|
||||
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::InvalidImageSourceName(name) => write!(f, "Invalid image source name: '{name}'."),
|
||||
Self::InvalidImageCoverId(id) => write!(f, "Invalid image cover id: '{id}'."),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "musicdb-lib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
@@ -18,5 +18,6 @@ playback = []
|
||||
default-playback = ["playback-via-playback-rs"]
|
||||
# default-playback = ["playback-via-rodio"]
|
||||
playback-via-sleep = ["playback"]
|
||||
playback-via-mpv = ["playback"]
|
||||
playback-via-playback-rs = ["playback", "dep:playback-rs"]
|
||||
playback-via-rodio = ["playback", "dep:rodio"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{BTreeMap, HashMap, VecDeque},
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
@@ -164,6 +164,34 @@ where
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "playback-via-mpv")]
|
||||
pub mod mpv;
|
||||
#[cfg(feature = "playback-via-playback-rs")]
|
||||
pub mod playback_rs;
|
||||
#[cfg(feature = "playback-via-rodio")]
|
||||
@@ -8,14 +10,18 @@ pub mod sleep;
|
||||
pub type PlayerBackendFeat<T> = playback_rs::PlayerBackendPlaybackRs<T>;
|
||||
#[cfg(feature = "playback-via-rodio")]
|
||||
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 crate::{
|
||||
data::{
|
||||
SongId,
|
||||
database::Database,
|
||||
song::{CachedData, Song},
|
||||
SongId,
|
||||
},
|
||||
server::Action,
|
||||
};
|
||||
|
||||
252
musicdb-lib/src/player/mpv.rs
Normal file
252
musicdb-lib/src/player/mpv.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
data::{song::Song, SongId},
|
||||
data::{SongId, song::Song},
|
||||
server::Command,
|
||||
};
|
||||
|
||||
@@ -28,6 +28,14 @@ enum SongFinished {
|
||||
|
||||
impl<T> PlayerBackendSleep<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 {
|
||||
|
||||
@@ -3,7 +3,7 @@ pub mod get;
|
||||
use std::{
|
||||
io::{BufRead as _, BufReader, Read, Write},
|
||||
net::TcpListener,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -15,12 +15,12 @@ use crate::player::Player;
|
||||
use crate::server::get::handle_one_connection_as_get;
|
||||
use crate::{
|
||||
data::{
|
||||
AlbumId, ArtistId, SongId,
|
||||
album::Album,
|
||||
artist::Artist,
|
||||
database::{Cover, Database, UpdateEndpoint},
|
||||
queue::Queue,
|
||||
song::Song,
|
||||
AlbumId, ArtistId, SongId,
|
||||
},
|
||||
load::ToFromBytes,
|
||||
};
|
||||
@@ -284,23 +284,15 @@ pub fn run_server_caching_thread_opt(
|
||||
) {
|
||||
#[cfg(not(feature = "playback"))]
|
||||
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 crate::data::cache_manager::CacheManager;
|
||||
#[cfg(feature = "playback-via-playback-rs")]
|
||||
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"
|
||||
))]
|
||||
#[cfg(any(feature = "playback"))]
|
||||
use crate::player::PlayerBackend;
|
||||
|
||||
// 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")]
|
||||
let mut player = if play_audio {
|
||||
#[cfg(feature = "playback-via-sleep")]
|
||||
let backend = PlayerBackendSleep::new(Some(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();
|
||||
use crate::player::PlayerBackendFeat;
|
||||
let backend = PlayerBackendFeat::new(command_sender.clone()).unwrap();
|
||||
Some(Player::new(backend))
|
||||
} else {
|
||||
None
|
||||
@@ -336,7 +324,8 @@ pub fn run_server_caching_thread_opt(
|
||||
Ok(v) => {
|
||||
let command_sender = command_sender.clone();
|
||||
let db = Arc::clone(&database);
|
||||
thread::spawn(move || loop {
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
if let Ok((connection, _con_addr)) = v.accept() {
|
||||
let command_sender = command_sender.clone();
|
||||
let db = Arc::clone(&db);
|
||||
@@ -363,7 +352,9 @@ pub fn run_server_caching_thread_opt(
|
||||
&command_sender,
|
||||
None,
|
||||
),
|
||||
"get" => _ = handle_one_connection_as_get(db, &mut connection),
|
||||
"get" => {
|
||||
_ = handle_one_connection_as_get(db, &mut connection)
|
||||
}
|
||||
_ => {
|
||||
_ = connection
|
||||
.into_inner()
|
||||
@@ -373,6 +364,7 @@ pub fn run_server_caching_thread_opt(
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "musicdb-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# 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 = []
|
||||
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-rodio = ["playback", "musicdb-lib/playback-via-rodio"]
|
||||
|
||||
Reference in New Issue
Block a user