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
|
# 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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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![]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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![]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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, ¤t_song).into_iter().enumerate() {
|
for (i, g) in all_general(db, ¤t_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!(
|
||||||
0 => 's',
|
match i {
|
||||||
1 => 'a',
|
0 => 's',
|
||||||
2 => 'A',
|
1 => 'a',
|
||||||
_ => unreachable!("array length should be 3"),
|
2 => 'A',
|
||||||
}
|
_ => unreachable!("array length should be 3"),
|
||||||
.to_string());
|
}
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextPart::TagEnd(p) => {
|
TextPart::TagEnd(p) => {
|
||||||
for gen in all_general(db, ¤t_song) {
|
for g in all_general(db, ¤t_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, ¤t_song) {
|
for g in all_general(db, ¤t_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}'."),
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
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::{
|
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 {
|
||||||
|
|||||||
@@ -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,42 +324,46 @@ 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 || {
|
||||||
if let Ok((connection, _con_addr)) = v.accept() {
|
loop {
|
||||||
let command_sender = command_sender.clone();
|
if let Ok((connection, _con_addr)) = v.accept() {
|
||||||
let db = Arc::clone(&db);
|
let command_sender = command_sender.clone();
|
||||||
thread::spawn(move || {
|
let db = Arc::clone(&db);
|
||||||
// each connection first has to send one line to tell us what it wants
|
thread::spawn(move || {
|
||||||
let mut connection = BufReader::new(connection);
|
// each connection first has to send one line to tell us what it wants
|
||||||
let mut line = String::new();
|
let mut connection = BufReader::new(connection);
|
||||||
if connection.read_line(&mut line).is_ok() {
|
let mut line = String::new();
|
||||||
// based on that line, we adjust behavior
|
if connection.read_line(&mut line).is_ok() {
|
||||||
match line.as_str().trim() {
|
// based on that line, we adjust behavior
|
||||||
// sends all updates to this connection and reads commands from it
|
match line.as_str().trim() {
|
||||||
"main" => {
|
// sends all updates to this connection and reads commands from it
|
||||||
let connection = connection.into_inner();
|
"main" => {
|
||||||
_ = handle_one_connection_as_main(
|
let connection = connection.into_inner();
|
||||||
db,
|
_ = handle_one_connection_as_main(
|
||||||
&mut connection.try_clone().unwrap(),
|
db,
|
||||||
connection,
|
&mut connection.try_clone().unwrap(),
|
||||||
|
connection,
|
||||||
|
&command_sender,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// reads commands from the connection, but (unlike main) doesn't send any updates
|
||||||
|
"control" => handle_one_connection_as_control(
|
||||||
|
&mut connection,
|
||||||
&command_sender,
|
&command_sender,
|
||||||
)
|
None,
|
||||||
}
|
),
|
||||||
// reads commands from the connection, but (unlike main) doesn't send any updates
|
"get" => {
|
||||||
"control" => handle_one_connection_as_control(
|
_ = handle_one_connection_as_get(db, &mut connection)
|
||||||
&mut connection,
|
}
|
||||||
&command_sender,
|
_ => {
|
||||||
None,
|
_ = connection
|
||||||
),
|
.into_inner()
|
||||||
"get" => _ = handle_one_connection_as_get(db, &mut connection),
|
.shutdown(std::net::Shutdown::Both)
|
||||||
_ => {
|
}
|
||||||
_ = connection
|
|
||||||
.into_inner()
|
|
||||||
.shutdown(std::net::Shutdown::Both)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user