mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 01:52:53 -05:00
Implemented prismriver
backend, fixed a bunch of small issues, removed unused deps
This commit is contained in:
parent
4f2d5ab64a
commit
69c3cd7427
13 changed files with 74 additions and 776 deletions
|
@ -22,16 +22,13 @@ file-format = { version = "0.23.0", features = [
|
||||||
"reader-xml",
|
"reader-xml",
|
||||||
"serde",
|
"serde",
|
||||||
] }
|
] }
|
||||||
lofty = "0.18.2"
|
lofty = "0.21"
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.4.0"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
rayon = "1.8.0"
|
rayon = "1.8.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
base64 = "0.21.5"
|
|
||||||
rcue = "0.1.3"
|
rcue = "0.1.3"
|
||||||
gstreamer = "0.21.3"
|
|
||||||
glib = "0.18.5"
|
|
||||||
crossbeam-channel = "0.5.8"
|
crossbeam-channel = "0.5.8"
|
||||||
crossbeam = "0.8.2"
|
crossbeam = "0.8.2"
|
||||||
quick-xml = "0.31.0"
|
quick-xml = "0.31.0"
|
||||||
|
@ -44,8 +41,6 @@ serde_json = "1.0.111"
|
||||||
deunicode = "1.4.2"
|
deunicode = "1.4.2"
|
||||||
opener = { version = "0.7.0", features = ["reveal"] }
|
opener = { version = "0.7.0", features = ["reveal"] }
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
listenbrainz = "0.7.0"
|
|
||||||
discord-rpc-client = "0.4.0"
|
|
||||||
nestify = "0.3.3"
|
nestify = "0.3.3"
|
||||||
moro = "0.4.0"
|
moro = "0.4.0"
|
||||||
moro-local = "0.4.0"
|
moro-local = "0.4.0"
|
||||||
|
@ -56,3 +51,4 @@ async-channel = "2.3.1"
|
||||||
ciborium = "0.2.2"
|
ciborium = "0.2.2"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
|
prismriver = { git = "https://github.com/Dangoware/prismriver.git" }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File, OpenOptions},
|
fs::{self, File, OpenOptions},
|
||||||
io::{Error, Read, Write},
|
io::{Error, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -189,9 +189,7 @@ pub enum ConfigError {
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::{Config, ConfigLibrary};
|
use super::{Config, ConfigLibrary};
|
||||||
use crate::music_storage::library::MusicLibrary;
|
use crate::music_storage::library::MusicLibrary;
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn new_config_lib() -> (Config, MusicLibrary) {
|
pub fn new_config_lib() -> (Config, MusicLibrary) {
|
||||||
_ = std::fs::create_dir_all("test-config/music/");
|
_ = std::fs::create_dir_all("test-config/music/");
|
||||||
|
|
|
@ -14,9 +14,4 @@ pub mod music_controller {
|
||||||
pub mod queue;
|
pub mod queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod music_player {
|
|
||||||
pub mod gstreamer;
|
|
||||||
pub mod player;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
|
@ -3,37 +3,35 @@
|
||||||
//! other functions
|
//! other functions
|
||||||
#![allow(while_true)]
|
#![allow(while_true)]
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use kushi::{Queue, QueueItemType};
|
use kushi::{Queue, QueueItemType};
|
||||||
use kushi::{QueueError, QueueItem};
|
use kushi::{QueueError, QueueItem};
|
||||||
|
use prismriver::{Prismriver, Volume};
|
||||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::to_string_pretty;
|
use serde_json::to_string_pretty;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::config::{self, ConfigError};
|
use crate::config::ConfigError;
|
||||||
use crate::music_player::player::{Player, PlayerError};
|
|
||||||
use crate::music_storage::library::Song;
|
use crate::music_storage::library::Song;
|
||||||
use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem};
|
use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem};
|
||||||
use crate::{config::Config, music_storage::library::MusicLibrary};
|
use crate::{config::Config, music_storage::library::MusicLibrary};
|
||||||
|
|
||||||
use super::queue::{QueueAlbum, QueueSong};
|
use super::queue::{QueueAlbum, QueueSong};
|
||||||
|
|
||||||
pub struct Controller<'a, P>(&'a PhantomData<P>);
|
pub struct Controller();
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ControllerError {
|
pub enum ControllerError {
|
||||||
#[error("{0:?}")]
|
#[error("{0:?}")]
|
||||||
QueueError(#[from] QueueError),
|
QueueError(#[from] QueueError),
|
||||||
#[error("{0:?}")]
|
#[error("{0:?}")]
|
||||||
PlayerError(#[from] PlayerError),
|
PlayerError(#[from] prismriver::Error),
|
||||||
#[error("{0:?}")]
|
#[error("{0:?}")]
|
||||||
ConfigError(#[from] ConfigError),
|
ConfigError(#[from] ConfigError),
|
||||||
}
|
}
|
||||||
|
@ -213,7 +211,7 @@ impl ControllerState {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
impl Controller {
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
ControllerInput {
|
ControllerInput {
|
||||||
player_mail,
|
player_mail,
|
||||||
|
@ -222,10 +220,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
mut library,
|
mut library,
|
||||||
config
|
config
|
||||||
}: ControllerInput
|
}: ControllerInput
|
||||||
) -> Result<(), Box<dyn Error>>
|
) -> Result<(), Box<dyn Error>> {
|
||||||
where
|
|
||||||
P: Player,
|
|
||||||
{
|
|
||||||
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
played: Vec::new(),
|
played: Vec::new(),
|
||||||
|
@ -250,13 +245,13 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
moro::async_scope!(|scope| {
|
moro::async_scope!(|scope| {
|
||||||
println!("async scope created");
|
println!("async scope created");
|
||||||
let player = Arc::new(RwLock::new(P::new().unwrap()));
|
let player = Arc::new(RwLock::new(Prismriver::new()));
|
||||||
|
|
||||||
let _player = player.clone();
|
let _player = player.clone();
|
||||||
let _lib_mail = lib_mail.0.clone();
|
let _lib_mail = lib_mail.0.clone();
|
||||||
scope
|
scope
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
Controller::<P>::player_command_loop(
|
Controller::player_command_loop(
|
||||||
_player,
|
_player,
|
||||||
player_mail.1,
|
player_mail.1,
|
||||||
queue_mail.0,
|
queue_mail.0,
|
||||||
|
@ -268,7 +263,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
});
|
});
|
||||||
scope
|
scope
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
Controller::<P>::player_event_loop(
|
Controller::player_event_loop(
|
||||||
player,
|
player,
|
||||||
player_mail.0
|
player_mail.0
|
||||||
)
|
)
|
||||||
|
@ -277,7 +272,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
});
|
});
|
||||||
scope
|
scope
|
||||||
.spawn(async {
|
.spawn(async {
|
||||||
Controller::<P>::library_loop(
|
Controller::library_loop(
|
||||||
lib_mail.1,
|
lib_mail.1,
|
||||||
&mut library,
|
&mut library,
|
||||||
config,
|
config,
|
||||||
|
@ -292,7 +287,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
|
|
||||||
let b = scope.spawn(|| {
|
let b = scope.spawn(|| {
|
||||||
futures::executor::block_on(async {
|
futures::executor::block_on(async {
|
||||||
Controller::<P>::queue_loop(queue, queue_mail.1).await;
|
Controller::queue_loop(queue, queue_mail.1).await;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
a.join().unwrap();
|
a.join().unwrap();
|
||||||
|
@ -303,7 +298,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn player_command_loop(
|
async fn player_command_loop(
|
||||||
player: Arc<RwLock<P>>,
|
player: Arc<RwLock<Prismriver>>,
|
||||||
player_mail: MailMan<PlayerResponse, PlayerCommand>,
|
player_mail: MailMan<PlayerResponse, PlayerCommand>,
|
||||||
queue_mail: MailMan<QueueCommand, QueueResponse>,
|
queue_mail: MailMan<QueueCommand, QueueResponse>,
|
||||||
lib_mail: MailMan<LibraryCommand, LibraryResponse>,
|
lib_mail: MailMan<LibraryCommand, LibraryResponse>,
|
||||||
|
@ -311,9 +306,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
let mut first = true;
|
let mut first = true;
|
||||||
{
|
{
|
||||||
let volume = state.volume as f64;
|
player.write().unwrap().set_volume(Volume::new(state.volume));
|
||||||
player.write().unwrap().set_volume(volume);
|
println!("volume set to {}", state.volume);
|
||||||
println!("volume set to {volume}");
|
|
||||||
}
|
}
|
||||||
while true {
|
while true {
|
||||||
let _mail = player_mail.recv().await;
|
let _mail = player_mail.recv().await;
|
||||||
|
@ -324,20 +318,24 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
queue_mail.send(QueueCommand::NowPlaying).await.unwrap();
|
queue_mail.send(QueueCommand::NowPlaying).await.unwrap();
|
||||||
let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() else { unimplemented!() };
|
let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() else { unimplemented!() };
|
||||||
let QueueItemType::Single(song) = item.item else { unimplemented!("This is temporary, handle queueItemTypes at some point") };
|
let QueueItemType::Single(song) = item.item else { unimplemented!("This is temporary, handle queueItemTypes at some point") };
|
||||||
player.write().unwrap().enqueue_next(song.song.primary_uri().unwrap().0).unwrap();
|
|
||||||
|
let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
|
||||||
|
player.write().unwrap().load_new(&prism_uri).unwrap();
|
||||||
|
player.write().unwrap().play();
|
||||||
|
|
||||||
player_mail.send(PlayerResponse::NowPlaying(song.song)).await.unwrap();
|
player_mail.send(PlayerResponse::NowPlaying(song.song)).await.unwrap();
|
||||||
first = false
|
first = false
|
||||||
} else {
|
} else {
|
||||||
player.write().unwrap().play().unwrap();
|
player.write().unwrap().play();
|
||||||
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlayerCommand::Pause => {
|
PlayerCommand::Pause => {
|
||||||
player.write().unwrap().pause().unwrap();
|
player.write().unwrap().pause();
|
||||||
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
||||||
}
|
}
|
||||||
PlayerCommand::SetVolume(volume) => {
|
PlayerCommand::SetVolume(volume) => {
|
||||||
player.write().unwrap().set_volume(volume as f64);
|
player.write().unwrap().set_volume(Volume::new(volume));
|
||||||
println!("volume set to {volume}");
|
println!("volume set to {volume}");
|
||||||
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
||||||
|
|
||||||
|
@ -352,7 +350,11 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
|
QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
player.write().unwrap().enqueue_next(uri).unwrap();
|
|
||||||
|
let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap();
|
||||||
|
player.write().unwrap().load_new(&prism_uri).unwrap();
|
||||||
|
player.write().unwrap().play();
|
||||||
|
|
||||||
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
||||||
|
|
||||||
// Append next song in library
|
// Append next song in library
|
||||||
|
@ -398,6 +400,11 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
|
QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap();
|
||||||
|
player.write().unwrap().load_new(&prism_uri).unwrap();
|
||||||
|
player.write().unwrap().play();
|
||||||
|
|
||||||
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
||||||
player_mail.send(PlayerResponse::NowPlaying(np_song.song.clone())).await.unwrap();
|
player_mail.send(PlayerResponse::NowPlaying(np_song.song.clone())).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -410,11 +417,9 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() {
|
if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() {
|
||||||
match item.item {
|
match item.item {
|
||||||
QueueItemType::Single(song) => {
|
QueueItemType::Single(song) => {
|
||||||
player
|
let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
|
||||||
.write()
|
player.write().unwrap().load_new(&prism_uri).unwrap();
|
||||||
.unwrap()
|
player.write().unwrap().play();
|
||||||
.enqueue_next(song.song.primary_uri().unwrap().0)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
|
@ -436,7 +441,10 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
player.write().unwrap().enqueue_next(song.primary_uri().unwrap().0).unwrap();
|
// TODO: Handle non Local URIs here, and whenever `load_new()` or `load_gapless()` is called
|
||||||
|
let prism_uri = prismriver::utils::path_to_uri(&song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
|
||||||
|
player.write().unwrap().load_new(&prism_uri).unwrap();
|
||||||
|
player.write().unwrap().play();
|
||||||
|
|
||||||
// how grab all the songs in a certain subset of the library, I reckon?
|
// how grab all the songs in a certain subset of the library, I reckon?
|
||||||
// ...
|
// ...
|
||||||
|
@ -486,7 +494,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
|
|
||||||
async fn library_loop(
|
async fn library_loop(
|
||||||
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
|
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
|
||||||
library: &'c mut MusicLibrary,
|
library: &mut MusicLibrary,
|
||||||
config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
while true {
|
while true {
|
||||||
|
@ -511,7 +519,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap();
|
lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap();
|
||||||
}
|
}
|
||||||
LibraryCommand::Save => {
|
LibraryCommand::Save => {
|
||||||
library.save({config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()}).unwrap();
|
library.save(config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap();
|
||||||
lib_mail.send(LibraryResponse::Ok).await.unwrap();
|
lib_mail.send(LibraryResponse::Ok).await.unwrap();
|
||||||
}
|
}
|
||||||
LibraryCommand::Playlists => {
|
LibraryCommand::Playlists => {
|
||||||
|
@ -527,7 +535,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn player_event_loop(
|
async fn player_event_loop(
|
||||||
player: Arc<RwLock<P>>,
|
player: Arc<RwLock<Prismriver>>,
|
||||||
player_mail: MailMan<PlayerCommand, PlayerResponse>,
|
player_mail: MailMan<PlayerCommand, PlayerResponse>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
// just pretend this does something
|
// just pretend this does something
|
||||||
|
@ -586,82 +594,3 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test_super {
|
|
||||||
use std::{
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
thread::spawn,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::{tests::new_config_lib, Config},
|
|
||||||
music_controller::controller::{
|
|
||||||
LibraryCommand, LibraryResponse, MailMan, PlayerCommand, PlayerResponse, ControllerHandle
|
|
||||||
},
|
|
||||||
music_player::gstreamer::GStreamer,
|
|
||||||
music_storage::library::MusicLibrary,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::Controller;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn construct_controller() {
|
|
||||||
// use if you don't have a config setup and add music to the music folder
|
|
||||||
new_config_lib();
|
|
||||||
|
|
||||||
let config = Config::read_file(PathBuf::from(std::env!("CONFIG-PATH"))).unwrap();
|
|
||||||
let library = {
|
|
||||||
MusicLibrary::init(
|
|
||||||
config.libraries.get_default().unwrap().path.clone(),
|
|
||||||
config.libraries.get_default().unwrap().uuid,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (handle, input) = ControllerHandle::new(library, Arc::new(RwLock::new(config)));
|
|
||||||
|
|
||||||
let b = spawn(move || {
|
|
||||||
futures::executor::block_on(async {
|
|
||||||
handle.player_mail
|
|
||||||
.send(PlayerCommand::SetVolume(0.01))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
loop {
|
|
||||||
let buf: String = text_io::read!();
|
|
||||||
dbg!(&buf);
|
|
||||||
handle.player_mail
|
|
||||||
.send(match buf.to_lowercase().as_str() {
|
|
||||||
"next" => PlayerCommand::NextSong,
|
|
||||||
"prev" => PlayerCommand::PrevSong,
|
|
||||||
"pause" => PlayerCommand::Pause,
|
|
||||||
"play" => PlayerCommand::Play,
|
|
||||||
x if x.parse::<usize>().is_ok() => {
|
|
||||||
PlayerCommand::Enqueue(x.parse::<usize>().unwrap())
|
|
||||||
}
|
|
||||||
_ => continue,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
println!("sent it");
|
|
||||||
println!("{:?}", handle.player_mail.recv().await.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let a = spawn(move || {
|
|
||||||
futures::executor::block_on(async {
|
|
||||||
|
|
||||||
|
|
||||||
Controller::<GStreamer>::start(input)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
b.join().unwrap();
|
|
||||||
a.join().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,521 +0,0 @@
|
||||||
// Crate things
|
|
||||||
use crate::music_storage::library::URI;
|
|
||||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
|
||||||
|
|
||||||
// GStreamer things
|
|
||||||
use glib::FlagsClass;
|
|
||||||
use gst::{ClockTime, Element};
|
|
||||||
use gstreamer as gst;
|
|
||||||
use gstreamer::prelude::*;
|
|
||||||
|
|
||||||
// Extra things
|
|
||||||
use chrono::Duration;
|
|
||||||
|
|
||||||
use super::player::{Player, PlayerCommand, PlayerError, PlayerState};
|
|
||||||
|
|
||||||
impl From<gst::State> for PlayerState {
|
|
||||||
fn from(value: gst::State) -> Self {
|
|
||||||
match value {
|
|
||||||
gst::State::VoidPending => Self::VoidPending,
|
|
||||||
gst::State::Playing => Self::Playing,
|
|
||||||
gst::State::Paused => Self::Paused,
|
|
||||||
gst::State::Ready => Self::Ready,
|
|
||||||
gst::State::Null => Self::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryInto<gst::State> for PlayerState {
|
|
||||||
fn try_into(self) -> Result<gst::State, Box<dyn Error>> {
|
|
||||||
match self {
|
|
||||||
Self::VoidPending => Ok(gst::State::VoidPending),
|
|
||||||
Self::Playing => Ok(gst::State::Playing),
|
|
||||||
Self::Paused => Ok(gst::State::Paused),
|
|
||||||
Self::Ready => Ok(gst::State::Ready),
|
|
||||||
Self::Null => Ok(gst::State::Null),
|
|
||||||
state => Err(format!("Invalid gst::State: {:?}", state).into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Error = Box<dyn Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
enum PlaybackInfo {
|
|
||||||
Idle,
|
|
||||||
Switching,
|
|
||||||
Playing {
|
|
||||||
start: Duration,
|
|
||||||
end: Duration,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// When this is sent, the thread will die! Use it when the [Player] is
|
|
||||||
/// done playing
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An instance of a music player with a GStreamer backend
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GStreamer {
|
|
||||||
source: Option<URI>,
|
|
||||||
|
|
||||||
message_rx: crossbeam::channel::Receiver<PlayerCommand>,
|
|
||||||
playback_tx: crossbeam::channel::Sender<PlaybackInfo>,
|
|
||||||
|
|
||||||
playbin: Arc<RwLock<Element>>,
|
|
||||||
volume: f64,
|
|
||||||
start: Option<Duration>,
|
|
||||||
end: Option<Duration>,
|
|
||||||
paused: Arc<RwLock<bool>>,
|
|
||||||
position: Arc<RwLock<Option<Duration>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<gst::StateChangeError> for PlayerError {
|
|
||||||
fn from(value: gst::StateChangeError) -> Self {
|
|
||||||
PlayerError::StateChange(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<glib::BoolError> for PlayerError {
|
|
||||||
fn from(value: glib::BoolError) -> Self {
|
|
||||||
PlayerError::General(value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GStreamer {
|
|
||||||
/// Set the playback URI
|
|
||||||
fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> {
|
|
||||||
if !source.exists().is_ok_and(|x| x) {
|
|
||||||
// If the source doesn't exist, gstreamer will crash!
|
|
||||||
return Err(PlayerError::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the playback tracker knows the stuff is stopped
|
|
||||||
println!("Beginning switch");
|
|
||||||
self.playback_tx.send(PlaybackInfo::Switching).unwrap();
|
|
||||||
|
|
||||||
let uri = self.playbin.read().unwrap().property_value("current-uri");
|
|
||||||
self.source = Some(source.clone());
|
|
||||||
match source {
|
|
||||||
URI::Cue { start, end, .. } => {
|
|
||||||
self.playbin
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.set_property("uri", source.as_uri());
|
|
||||||
|
|
||||||
// Set the start and end positions of the CUE file
|
|
||||||
self.start = Some(Duration::from_std(*start).unwrap());
|
|
||||||
self.end = Some(Duration::from_std(*end).unwrap());
|
|
||||||
|
|
||||||
// Send the updated position to the tracker
|
|
||||||
self.playback_tx
|
|
||||||
.send(PlaybackInfo::Playing {
|
|
||||||
start: self.start.unwrap(),
|
|
||||||
end: self.end.unwrap(),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Wait for it to be ready, and then move to the proper position
|
|
||||||
self.play().unwrap();
|
|
||||||
let now = std::time::Instant::now();
|
|
||||||
while now.elapsed() < std::time::Duration::from_millis(20) {
|
|
||||||
if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
||||||
}
|
|
||||||
//panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)");
|
|
||||||
return Err(PlayerError::StateChange(
|
|
||||||
"Could not seek to beginning of CUE track".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.playbin
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.set_property("uri", source.as_uri());
|
|
||||||
|
|
||||||
if self.state() != PlayerState::Playing {
|
|
||||||
self.play().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
while self.raw_duration().is_none() {
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.start = Some(Duration::seconds(0));
|
|
||||||
self.end = self.raw_duration();
|
|
||||||
|
|
||||||
// Send the updated position to the tracker
|
|
||||||
self.playback_tx
|
|
||||||
.send(PlaybackInfo::Playing {
|
|
||||||
start: self.start.unwrap(),
|
|
||||||
end: self.end.unwrap(),
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a mutable reference to the playbin element
|
|
||||||
fn playbin_mut(
|
|
||||||
&mut self,
|
|
||||||
) -> Result<RwLockWriteGuard<gst::Element>, std::sync::PoisonError<RwLockWriteGuard<'_, Element>>>
|
|
||||||
{
|
|
||||||
let element = match self.playbin.write() {
|
|
||||||
Ok(element) => element,
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
|
||||||
Ok(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a read-only reference to the playbin element
|
|
||||||
fn playbin(
|
|
||||||
&self,
|
|
||||||
) -> Result<RwLockReadGuard<gst::Element>, std::sync::PoisonError<RwLockReadGuard<'_, Element>>>
|
|
||||||
{
|
|
||||||
let element = match self.playbin.read() {
|
|
||||||
Ok(element) => element,
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
|
||||||
Ok(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set volume of the internal playbin player, can be
|
|
||||||
/// used to bypass the main volume control for seeking
|
|
||||||
fn set_gstreamer_volume(&mut self, volume: f64) {
|
|
||||||
self.playbin_mut().unwrap().set_property("volume", volume)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> {
|
|
||||||
self.playbin_mut().unwrap().set_state(state)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raw_duration(&self) -> Option<Duration> {
|
|
||||||
self.playbin()
|
|
||||||
.unwrap()
|
|
||||||
.query_duration::<ClockTime>()
|
|
||||||
.map(|pos| Duration::nanoseconds(pos.nseconds() as i64))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current state of the playback
|
|
||||||
fn state(&mut self) -> PlayerState {
|
|
||||||
self.playbin().unwrap().current_state().into()
|
|
||||||
/*
|
|
||||||
match *self.buffer.read().unwrap() {
|
|
||||||
None => self.playbin().unwrap().current_state().into(),
|
|
||||||
Some(value) => PlayerState::Buffering(value),
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
fn property(&self, property: &str) -> glib::Value {
|
|
||||||
self.playbin().unwrap().property_value(property)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ready(&mut self) -> Result<(), PlayerError> {
|
|
||||||
self.set_state(gst::State::Ready)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Player for GStreamer {
|
|
||||||
fn new() -> Result<Self, PlayerError> {
|
|
||||||
// Initialize GStreamer, maybe figure out how to nicely fail here
|
|
||||||
if let Err(err) = gst::init() {
|
|
||||||
return Err(PlayerError::Init(err.to_string()));
|
|
||||||
};
|
|
||||||
let ctx = glib::MainContext::default();
|
|
||||||
let _guard = ctx.acquire();
|
|
||||||
let mainloop = glib::MainLoop::new(Some(&ctx), false);
|
|
||||||
|
|
||||||
let playbin_arc = Arc::new(RwLock::new(
|
|
||||||
match gst::ElementFactory::make("playbin3").build() {
|
|
||||||
Ok(playbin) => playbin,
|
|
||||||
Err(error) => return Err(PlayerError::Init(error.to_string())),
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
let playbin = playbin_arc.clone();
|
|
||||||
|
|
||||||
let flags = playbin.read().unwrap().property_value("flags");
|
|
||||||
let flags_class = FlagsClass::with_type(flags.type_()).unwrap();
|
|
||||||
|
|
||||||
// Set up the Playbin flags to only play audio
|
|
||||||
let flags = flags_class
|
|
||||||
.builder_with_value(flags)
|
|
||||||
.ok_or(PlayerError::Build)?
|
|
||||||
.set_by_nick("audio")
|
|
||||||
.set_by_nick("download")
|
|
||||||
.unset_by_nick("video")
|
|
||||||
.unset_by_nick("text")
|
|
||||||
.build()
|
|
||||||
.ok_or(PlayerError::Build)?;
|
|
||||||
|
|
||||||
playbin
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.set_property_from_value("flags", &flags);
|
|
||||||
//playbin.write().unwrap().set_property("instant-uri", true);
|
|
||||||
|
|
||||||
let position = Arc::new(RwLock::new(None));
|
|
||||||
|
|
||||||
// Set up the thread to monitor the position
|
|
||||||
let (playback_tx, playback_rx) = unbounded();
|
|
||||||
let (status_tx, status_rx) = unbounded::<PlaybackInfo>();
|
|
||||||
let position_update = Arc::clone(&position);
|
|
||||||
|
|
||||||
std::thread::spawn(|| {
|
|
||||||
playback_monitor(playbin_arc, status_rx, playback_tx, position_update)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up the thread to monitor bus messages
|
|
||||||
let playbin_bus_ctrl = Arc::clone(&playbin);
|
|
||||||
let paused = Arc::new(RwLock::new(false));
|
|
||||||
let bus_paused = Arc::clone(&paused);
|
|
||||||
let bus_watch = playbin
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.bus()
|
|
||||||
.expect("Failed to get GStreamer message bus")
|
|
||||||
.add_watch(move |_bus, msg| {
|
|
||||||
match msg.view() {
|
|
||||||
gst::MessageView::Eos(_) => println!("End of stream"),
|
|
||||||
gst::MessageView::StreamStart(_) => println!("Stream start"),
|
|
||||||
gst::MessageView::Error(err) => {
|
|
||||||
println!("Error recieved: {}", err);
|
|
||||||
return glib::ControlFlow::Break;
|
|
||||||
}
|
|
||||||
gst::MessageView::Buffering(buffering) => {
|
|
||||||
if *bus_paused.read().unwrap() == true {
|
|
||||||
return glib::ControlFlow::Continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the player is not paused, pause it
|
|
||||||
let percent = buffering.percent();
|
|
||||||
if percent < 100 {
|
|
||||||
playbin_bus_ctrl
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.set_state(gst::State::Paused)
|
|
||||||
.unwrap();
|
|
||||||
} else if percent >= 100 {
|
|
||||||
println!("Finished buffering");
|
|
||||||
playbin_bus_ctrl
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.set_state(gst::State::Playing)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
})
|
|
||||||
.expect("Failed to connect to GStreamer message bus");
|
|
||||||
|
|
||||||
// Set up a thread to watch the messages
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let _watch = bus_watch;
|
|
||||||
mainloop.run()
|
|
||||||
});
|
|
||||||
|
|
||||||
let source = None;
|
|
||||||
Ok(Self {
|
|
||||||
source,
|
|
||||||
playbin,
|
|
||||||
message_rx: playback_rx,
|
|
||||||
playback_tx: status_tx,
|
|
||||||
volume: 1.0,
|
|
||||||
start: None,
|
|
||||||
end: None,
|
|
||||||
paused,
|
|
||||||
position,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source(&self) -> &Option<URI> {
|
|
||||||
&self.source
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> {
|
|
||||||
println!("enqueuing in fn");
|
|
||||||
self.set_source(next_track)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: f64) {
|
|
||||||
self.volume = volume.clamp(0.0, 1.0);
|
|
||||||
self.set_gstreamer_volume(self.volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn volume(&self) -> f64 {
|
|
||||||
self.volume
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play(&mut self) -> Result<(), PlayerError> {
|
|
||||||
if self.state() == PlayerState::Playing {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
*self.paused.write().unwrap() = false;
|
|
||||||
self.set_state(gst::State::Playing)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pause(&mut self) -> Result<(), PlayerError> {
|
|
||||||
if self.state() == PlayerState::Paused || *self.paused.read().unwrap() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
*self.paused.write().unwrap() = true;
|
|
||||||
self.set_state(gst::State::Paused)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_paused(&self) -> bool {
|
|
||||||
self.playbin().unwrap().current_state() == gst::State::Paused
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position(&self) -> Option<Duration> {
|
|
||||||
*self.position.read().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn duration(&self) -> Option<Duration> {
|
|
||||||
if self.end.is_some() && self.start.is_some() {
|
|
||||||
Some(self.end.unwrap() - self.start.unwrap())
|
|
||||||
} else {
|
|
||||||
self.raw_duration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> {
|
|
||||||
let time_pos = match *self.position.read().unwrap() {
|
|
||||||
Some(pos) => pos,
|
|
||||||
None => return Err(PlayerError::Seek("No position".into())),
|
|
||||||
};
|
|
||||||
let seek_pos = time_pos + seek_amount;
|
|
||||||
|
|
||||||
self.seek_to(seek_pos)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> {
|
|
||||||
let start = if self.start.is_none() {
|
|
||||||
return Err(PlayerError::Seek("No START time".into()));
|
|
||||||
} else {
|
|
||||||
self.start.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let end = if self.end.is_none() {
|
|
||||||
return Err(PlayerError::Seek("No END time".into()));
|
|
||||||
} else {
|
|
||||||
self.end.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let adjusted_target = target_pos + start;
|
|
||||||
let clamped_target = adjusted_target.clamp(start, end);
|
|
||||||
|
|
||||||
let seek_pos_clock =
|
|
||||||
ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64);
|
|
||||||
|
|
||||||
self.set_gstreamer_volume(0.0);
|
|
||||||
self.playbin_mut()
|
|
||||||
.unwrap()
|
|
||||||
.seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?;
|
|
||||||
self.set_gstreamer_volume(self.volume);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop(&mut self) -> Result<(), PlayerError> {
|
|
||||||
self.pause()?;
|
|
||||||
self.ready()?;
|
|
||||||
|
|
||||||
// Send the updated position to the tracker
|
|
||||||
self.playback_tx.send(PlaybackInfo::Idle).unwrap();
|
|
||||||
|
|
||||||
// Set all positions to none
|
|
||||||
*self.position.write().unwrap() = None;
|
|
||||||
self.start = None;
|
|
||||||
self.end = None;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand> {
|
|
||||||
&self.message_rx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for GStreamer {
|
|
||||||
/// Cleans up the `GStreamer` pipeline and the monitoring
|
|
||||||
/// thread when [Player] is dropped.
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.playbin_mut()
|
|
||||||
.unwrap()
|
|
||||||
.set_state(gst::State::Null)
|
|
||||||
.expect("Unable to set the pipeline to the `Null` state");
|
|
||||||
let _ = self.playback_tx.send(PlaybackInfo::Finished);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn playback_monitor(
|
|
||||||
playbin: Arc<RwLock<Element>>,
|
|
||||||
status_rx: Receiver<PlaybackInfo>,
|
|
||||||
playback_tx: Sender<PlayerCommand>,
|
|
||||||
position: Arc<RwLock<Option<Duration>>>,
|
|
||||||
) {
|
|
||||||
let mut stats = PlaybackInfo::Idle;
|
|
||||||
let mut pos_temp;
|
|
||||||
let mut sent_atf = false;
|
|
||||||
loop {
|
|
||||||
// Check for new messages to decide how to proceed
|
|
||||||
if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(50)) {
|
|
||||||
stats = result
|
|
||||||
}
|
|
||||||
|
|
||||||
pos_temp = playbin
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.query_position::<ClockTime>()
|
|
||||||
.map(|pos| Duration::nanoseconds(pos.nseconds() as i64));
|
|
||||||
|
|
||||||
match stats {
|
|
||||||
PlaybackInfo::Playing { start, end } if pos_temp.is_some() => {
|
|
||||||
// Check if the current playback position is close to the end
|
|
||||||
let finish_point = end - Duration::milliseconds(2000);
|
|
||||||
if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() {
|
|
||||||
println!("MONITOR: End of stream");
|
|
||||||
let _ = playback_tx.try_send(PlayerCommand::EndOfStream);
|
|
||||||
playbin
|
|
||||||
.write()
|
|
||||||
.unwrap()
|
|
||||||
.set_state(gst::State::Ready)
|
|
||||||
.expect("Unable to set the pipeline state");
|
|
||||||
sent_atf = false
|
|
||||||
} else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds()
|
|
||||||
&& !sent_atf
|
|
||||||
{
|
|
||||||
println!("MONITOR: About to finish");
|
|
||||||
let _ = playback_tx.try_send(PlayerCommand::AboutToFinish);
|
|
||||||
sent_atf = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This has to be done AFTER the current time in the file
|
|
||||||
// is calculated, or everything else is wrong
|
|
||||||
pos_temp = Some(pos_temp.unwrap() - start)
|
|
||||||
}
|
|
||||||
PlaybackInfo::Finished => {
|
|
||||||
println!("MONITOR: Shutting down");
|
|
||||||
*position.write().unwrap() = None;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
PlaybackInfo::Idle | PlaybackInfo::Switching => sent_atf = false,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
*position.write().unwrap() = pos_temp;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
use chrono::Duration;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::music_storage::library::URI;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum PlayerError {
|
|
||||||
#[error("player initialization failed: {0}")]
|
|
||||||
Init(String),
|
|
||||||
#[error("could not change playback state")]
|
|
||||||
StateChange(String),
|
|
||||||
#[error("seeking failed: {0}")]
|
|
||||||
Seek(String),
|
|
||||||
#[error("the file or source is not found")]
|
|
||||||
NotFound,
|
|
||||||
#[error("failed to build gstreamer item")]
|
|
||||||
Build,
|
|
||||||
#[error("poison error")]
|
|
||||||
Poison,
|
|
||||||
#[error("general player error")]
|
|
||||||
General(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum PlayerState {
|
|
||||||
Playing,
|
|
||||||
Paused,
|
|
||||||
Ready,
|
|
||||||
Buffering(u8),
|
|
||||||
Null,
|
|
||||||
VoidPending,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum PlayerCommand {
|
|
||||||
Play,
|
|
||||||
Pause,
|
|
||||||
EndOfStream,
|
|
||||||
AboutToFinish,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Player {
|
|
||||||
/// Create a new player.
|
|
||||||
fn new() -> Result<Self, PlayerError>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
/// Get the currently playing [URI] from the player.
|
|
||||||
fn source(&self) -> &Option<URI>;
|
|
||||||
|
|
||||||
/// Insert a new [`URI`] to be played. This method should be called at the
|
|
||||||
/// beginning to start playback of something, and once the [`PlayerCommand`]
|
|
||||||
/// indicates the track is about to finish to enqueue gaplessly.
|
|
||||||
///
|
|
||||||
/// For backends which do not support gapless playback, `AboutToFinish`
|
|
||||||
/// will not be called, and the next [`URI`] should be enqueued once `Eos`
|
|
||||||
/// occurs.
|
|
||||||
fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>;
|
|
||||||
|
|
||||||
/// Set the playback volume, accepts a float from `0` to `1`.
|
|
||||||
///
|
|
||||||
/// Values outside the range of `0` to `1` will be capped.
|
|
||||||
fn set_volume(&mut self, volume: f64);
|
|
||||||
|
|
||||||
/// Returns the current volume level, a float from `0` to `1`.
|
|
||||||
fn volume(&self) -> f64;
|
|
||||||
|
|
||||||
/// If the player is paused or stopped, starts playback.
|
|
||||||
fn play(&mut self) -> Result<(), PlayerError>;
|
|
||||||
|
|
||||||
/// If the player is playing, pause playback.
|
|
||||||
fn pause(&mut self) -> Result<(), PlayerError>;
|
|
||||||
|
|
||||||
/// Stop the playback entirely, removing the current [`URI`] from the player.
|
|
||||||
fn stop(&mut self) -> Result<(), PlayerError>;
|
|
||||||
|
|
||||||
/// Convenience function to check if playback is paused.
|
|
||||||
fn is_paused(&self) -> bool;
|
|
||||||
|
|
||||||
/// Get the current playback position of the player.
|
|
||||||
fn position(&self) -> Option<Duration>;
|
|
||||||
|
|
||||||
/// Get the duration of the currently playing track.
|
|
||||||
fn duration(&self) -> Option<Duration>;
|
|
||||||
|
|
||||||
/// Seek relative to the current position.
|
|
||||||
///
|
|
||||||
/// The position is capped at the duration of the song, and zero.
|
|
||||||
fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>;
|
|
||||||
|
|
||||||
/// Seek absolutely within the song.
|
|
||||||
///
|
|
||||||
/// The position is capped at the duration of the song, and zero.
|
|
||||||
fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>;
|
|
||||||
|
|
||||||
/// Return a reference to the player message channel, which can be cloned
|
|
||||||
/// in order to monitor messages from the player.
|
|
||||||
fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand>;
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
use file_format::FileFormat;
|
use file_format::FileFormat;
|
||||||
use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt};
|
use lofty::file::{AudioFile as _, TaggedFileExt as _};
|
||||||
|
use lofty::probe::Probe;
|
||||||
|
use lofty::tag::TagType;
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
use quick_xml::reader::Reader;
|
use quick_xml::reader::Reader;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
@ -221,7 +223,7 @@ fn to_tag(string: String) -> Tag {
|
||||||
_ => Tag::Key(string),
|
_ => Tag::Key(string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> {
|
fn get_duration(file: &Path) -> Result<StdDur, lofty::error::LoftyError> {
|
||||||
let dur = match Probe::open(file)?.read() {
|
let dur = match Probe::open(file)?.read() {
|
||||||
Ok(tagged_file) => tagged_file.properties().duration(),
|
Ok(tagged_file) => tagged_file.properties().duration(),
|
||||||
|
|
||||||
|
@ -229,12 +231,12 @@ fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> {
|
||||||
};
|
};
|
||||||
Ok(dur)
|
Ok(dur)
|
||||||
}
|
}
|
||||||
fn get_art(file: &Path) -> Result<Vec<AlbumArt>, LoftyError> {
|
fn get_art(file: &Path) -> Result<Vec<AlbumArt>, lofty::error::LoftyError> {
|
||||||
let mut album_art: Vec<AlbumArt> = Vec::new();
|
let mut album_art: Vec<AlbumArt> = Vec::new();
|
||||||
|
|
||||||
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
|
let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2);
|
||||||
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
|
let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed);
|
||||||
let tagged_file: lofty::TaggedFile;
|
let tagged_file: lofty::file::TaggedFile;
|
||||||
|
|
||||||
let tag = match Probe::open(file)?.options(normal_options).read() {
|
let tag = match Probe::open(file)?.options(normal_options).read() {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
|
@ -288,7 +290,7 @@ impl ITunesSong {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_hashmap(map: &mut HashMap<String, String>) -> Result<ITunesSong, LoftyError> {
|
fn from_hashmap(map: &mut HashMap<String, String>) -> Result<ITunesSong, lofty::error::LoftyError> {
|
||||||
let mut song = ITunesSong::new();
|
let mut song = ITunesSong::new();
|
||||||
//get the path with the first bit chopped off
|
//get the path with the first bit chopped off
|
||||||
let path_: String = map.get_key_value("Location").unwrap().1.clone();
|
let path_: String = map.get_key_value("Location").unwrap().1.clone();
|
||||||
|
@ -339,10 +341,7 @@ impl ITunesSong {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::path::{Path, PathBuf};
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, ConfigLibrary},
|
config::{Config, ConfigLibrary},
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use super::playlist::{Playlist, PlaylistFolder};
|
use super::playlist::{Playlist, PlaylistFolder};
|
||||||
// Crate things
|
// Crate things
|
||||||
use super::utils::{find_images, normalize, read_file, write_file};
|
use super::utils::{find_images, normalize, read_file, write_file};
|
||||||
use crate::config::Config;
|
|
||||||
use crate::music_storage::playlist::PlaylistFolderItem;
|
use crate::music_storage::playlist::PlaylistFolderItem;
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
// Various std things
|
// Various std things
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::BTreeMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::ops::ControlFlow::{Break, Continue};
|
use std::ops::ControlFlow::{Break, Continue};
|
||||||
|
@ -14,9 +13,10 @@ use std::vec::IntoIter;
|
||||||
|
|
||||||
// Files
|
// Files
|
||||||
use file_format::{FileFormat, Kind};
|
use file_format::{FileFormat, Kind};
|
||||||
use glib::filename_to_uri;
|
|
||||||
|
|
||||||
use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt};
|
use lofty::file::{AudioFile as _, TaggedFileExt as _};
|
||||||
|
use lofty::probe::Probe;
|
||||||
|
use lofty::tag::{ItemKey, ItemValue, TagType};
|
||||||
use rcue::parser::parse_from_file;
|
use rcue::parser::parse_from_file;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -28,12 +28,11 @@ use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
// Serialization/Compression
|
// Serialization/Compression
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// Fun parallel stuff
|
// Fun parallel stuff
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
pub enum AlbumArt {
|
pub enum AlbumArt {
|
||||||
|
@ -225,10 +224,10 @@ impl Song {
|
||||||
|
|
||||||
/// Creates a `Song` from a music file
|
/// Creates a `Song` from a music file
|
||||||
pub fn from_file<P: ?Sized + AsRef<Path>>(target_file: &P) -> Result<Self, Box<dyn Error>> {
|
pub fn from_file<P: ?Sized + AsRef<Path>>(target_file: &P) -> Result<Self, Box<dyn Error>> {
|
||||||
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
|
let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed);
|
||||||
|
|
||||||
let blank_tag = &lofty::Tag::new(TagType::Id3v2);
|
let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2);
|
||||||
let tagged_file: lofty::TaggedFile;
|
let tagged_file: lofty::file::TaggedFile;
|
||||||
let mut duration = Duration::from_secs(0);
|
let mut duration = Duration::from_secs(0);
|
||||||
let tag = match Probe::open(target_file)?.options(normal_options).read() {
|
let tag = match Probe::open(target_file)?.options(normal_options).read() {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
|
@ -273,7 +272,7 @@ impl Song {
|
||||||
let value = match item.value() {
|
let value = match item.value() {
|
||||||
ItemValue::Text(value) => value.clone(),
|
ItemValue::Text(value) => value.clone(),
|
||||||
ItemValue::Locator(value) => value.clone(),
|
ItemValue::Locator(value) => value.clone(),
|
||||||
ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)),
|
ItemValue::Binary(_) => continue, // TODO: Ignoring binary values for now
|
||||||
};
|
};
|
||||||
|
|
||||||
tags.insert(key, value);
|
tags.insert(key, value);
|
||||||
|
@ -548,10 +547,10 @@ impl URI {
|
||||||
|
|
||||||
pub fn as_uri(&self) -> String {
|
pub fn as_uri(&self) -> String {
|
||||||
let path_str = match self {
|
let path_str = match self {
|
||||||
URI::Local(location) => filename_to_uri(location, None)
|
URI::Local(location) => prismriver::utils::path_to_uri(location)
|
||||||
.expect("couldn't convert path to URI")
|
.expect("couldn't convert path to URI")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
URI::Cue { location, .. } => filename_to_uri(location, None)
|
URI::Cue { location, .. } => prismriver::utils::path_to_uri(location)
|
||||||
.expect("couldn't convert path to URI")
|
.expect("couldn't convert path to URI")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
URI::Remote(_, location) => location.clone(),
|
URI::Remote(_, location) => location.clone(),
|
||||||
|
|
|
@ -94,4 +94,4 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(images)
|
Ok(images)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,12 @@ futures = "0.3.31"
|
||||||
crossbeam = "0.8.4"
|
crossbeam = "0.8.4"
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
||||||
ciborium = "0.2.2"
|
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
file-format = "0.26.0"
|
file-format = "0.26.0"
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
rfd = "0.15.1"
|
rfd = "0.15.1"
|
||||||
|
colog = "1.3.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "custom-protocol" ]
|
default = [ "custom-protocol" ]
|
||||||
|
|
|
@ -2,8 +2,8 @@ use std::{fs, path::PathBuf, str::FromStr, thread::spawn};
|
||||||
|
|
||||||
use commands::{add_song_to_queue, play_now};
|
use commands::{add_song_to_queue, play_now};
|
||||||
use crossbeam::channel::{unbounded, Receiver, Sender};
|
use crossbeam::channel::{unbounded, Receiver, Sender};
|
||||||
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_player::gstreamer::GStreamer, music_storage::library::{AlbumArt, MusicLibrary}};
|
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_storage::library::MusicLibrary};
|
||||||
use tauri::{http::Response, Emitter, Manager, State, Url, WebviewWindowBuilder, Wry};
|
use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists};
|
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists};
|
||||||
|
@ -55,7 +55,7 @@ pub fn run() {
|
||||||
|
|
||||||
handle_rx.send(handle).unwrap();
|
handle_rx.send(handle).unwrap();
|
||||||
|
|
||||||
let _controller = futures::executor::block_on(Controller::<GStreamer>::start(input)).unwrap();
|
let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
|
||||||
});
|
});
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
|
|
@ -3,5 +3,7 @@
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
colog::init();
|
||||||
|
|
||||||
dango_music_player_lib::run()
|
dango_music_player_lib::run()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue