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",
|
||||
"serde",
|
||||
] }
|
||||
lofty = "0.18.2"
|
||||
lofty = "0.21"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
walkdir = "2.4.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
rayon = "1.8.0"
|
||||
log = "0.4"
|
||||
base64 = "0.21.5"
|
||||
rcue = "0.1.3"
|
||||
gstreamer = "0.21.3"
|
||||
glib = "0.18.5"
|
||||
crossbeam-channel = "0.5.8"
|
||||
crossbeam = "0.8.2"
|
||||
quick-xml = "0.31.0"
|
||||
|
@ -44,8 +41,6 @@ serde_json = "1.0.111"
|
|||
deunicode = "1.4.2"
|
||||
opener = { version = "0.7.0", features = ["reveal"] }
|
||||
tempfile = "3.10.1"
|
||||
listenbrainz = "0.7.0"
|
||||
discord-rpc-client = "0.4.0"
|
||||
nestify = "0.3.3"
|
||||
moro = "0.4.0"
|
||||
moro-local = "0.4.0"
|
||||
|
@ -56,3 +51,4 @@ async-channel = "2.3.1"
|
|||
ciborium = "0.2.2"
|
||||
itertools = "0.13.0"
|
||||
directories = "5.0.1"
|
||||
prismriver = { git = "https://github.com/Dangoware/prismriver.git" }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{Error, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -189,9 +189,7 @@ pub enum ConfigError {
|
|||
pub mod tests {
|
||||
use super::{Config, ConfigLibrary};
|
||||
use crate::music_storage::library::MusicLibrary;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn new_config_lib() -> (Config, MusicLibrary) {
|
||||
_ = std::fs::create_dir_all("test-config/music/");
|
||||
|
|
|
@ -14,9 +14,4 @@ pub mod music_controller {
|
|||
pub mod queue;
|
||||
}
|
||||
|
||||
pub mod music_player {
|
||||
pub mod gstreamer;
|
||||
pub mod player;
|
||||
}
|
||||
|
||||
pub mod config;
|
||||
|
|
|
@ -3,37 +3,35 @@
|
|||
//! other functions
|
||||
#![allow(while_true)]
|
||||
|
||||
use itertools::Itertools;
|
||||
use kushi::{Queue, QueueItemType};
|
||||
use kushi::{QueueError, QueueItem};
|
||||
use prismriver::{Prismriver, Volume};
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string_pretty;
|
||||
use std::error::Error;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::{self, ConfigError};
|
||||
use crate::music_player::player::{Player, PlayerError};
|
||||
use crate::config::ConfigError;
|
||||
use crate::music_storage::library::Song;
|
||||
use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem};
|
||||
use crate::{config::Config, music_storage::library::MusicLibrary};
|
||||
|
||||
use super::queue::{QueueAlbum, QueueSong};
|
||||
|
||||
pub struct Controller<'a, P>(&'a PhantomData<P>);
|
||||
pub struct Controller();
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ControllerError {
|
||||
#[error("{0:?}")]
|
||||
QueueError(#[from] QueueError),
|
||||
#[error("{0:?}")]
|
||||
PlayerError(#[from] PlayerError),
|
||||
PlayerError(#[from] prismriver::Error),
|
||||
#[error("{0:?}")]
|
||||
ConfigError(#[from] ConfigError),
|
||||
}
|
||||
|
@ -213,7 +211,7 @@ impl ControllerState {
|
|||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
||||
impl Controller {
|
||||
pub async fn start(
|
||||
ControllerInput {
|
||||
player_mail,
|
||||
|
@ -222,10 +220,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
mut library,
|
||||
config
|
||||
}: ControllerInput
|
||||
) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
P: Player,
|
||||
{
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let queue: Queue<QueueSong, QueueAlbum> = Queue {
|
||||
items: Vec::new(),
|
||||
played: Vec::new(),
|
||||
|
@ -250,13 +245,13 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
futures::executor::block_on(async {
|
||||
moro::async_scope!(|scope| {
|
||||
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 _lib_mail = lib_mail.0.clone();
|
||||
scope
|
||||
.spawn(async move {
|
||||
Controller::<P>::player_command_loop(
|
||||
Controller::player_command_loop(
|
||||
_player,
|
||||
player_mail.1,
|
||||
queue_mail.0,
|
||||
|
@ -268,7 +263,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
});
|
||||
scope
|
||||
.spawn(async move {
|
||||
Controller::<P>::player_event_loop(
|
||||
Controller::player_event_loop(
|
||||
player,
|
||||
player_mail.0
|
||||
)
|
||||
|
@ -277,7 +272,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
});
|
||||
scope
|
||||
.spawn(async {
|
||||
Controller::<P>::library_loop(
|
||||
Controller::library_loop(
|
||||
lib_mail.1,
|
||||
&mut library,
|
||||
config,
|
||||
|
@ -292,7 +287,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
|
||||
let b = scope.spawn(|| {
|
||||
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();
|
||||
|
@ -303,7 +298,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
}
|
||||
|
||||
async fn player_command_loop(
|
||||
player: Arc<RwLock<P>>,
|
||||
player: Arc<RwLock<Prismriver>>,
|
||||
player_mail: MailMan<PlayerResponse, PlayerCommand>,
|
||||
queue_mail: MailMan<QueueCommand, QueueResponse>,
|
||||
lib_mail: MailMan<LibraryCommand, LibraryResponse>,
|
||||
|
@ -311,9 +306,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
) -> Result<(), ()> {
|
||||
let mut first = true;
|
||||
{
|
||||
let volume = state.volume as f64;
|
||||
player.write().unwrap().set_volume(volume);
|
||||
println!("volume set to {volume}");
|
||||
player.write().unwrap().set_volume(Volume::new(state.volume));
|
||||
println!("volume set to {}", state.volume);
|
||||
}
|
||||
while true {
|
||||
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();
|
||||
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") };
|
||||
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();
|
||||
first = false
|
||||
} else {
|
||||
player.write().unwrap().play().unwrap();
|
||||
player.write().unwrap().play();
|
||||
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
||||
}
|
||||
}
|
||||
PlayerCommand::Pause => {
|
||||
player.write().unwrap().pause().unwrap();
|
||||
player.write().unwrap().pause();
|
||||
player_mail.send(PlayerResponse::Empty).await.unwrap();
|
||||
}
|
||||
PlayerCommand::SetVolume(volume) => {
|
||||
player.write().unwrap().set_volume(volume as f64);
|
||||
player.write().unwrap().set_volume(Volume::new(volume));
|
||||
println!("volume set to {volume}");
|
||||
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,
|
||||
_ => 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")};
|
||||
|
||||
// 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,
|
||||
_ => 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")};
|
||||
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() {
|
||||
match item.item {
|
||||
QueueItemType::Single(song) => {
|
||||
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();
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
|
@ -436,7 +441,10 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
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?
|
||||
// ...
|
||||
|
@ -486,7 +494,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
|
||||
async fn library_loop(
|
||||
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
|
||||
library: &'c mut MusicLibrary,
|
||||
library: &mut MusicLibrary,
|
||||
config: Arc<RwLock<Config>>,
|
||||
) -> Result<(), ()> {
|
||||
while true {
|
||||
|
@ -511,7 +519,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap();
|
||||
}
|
||||
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();
|
||||
}
|
||||
LibraryCommand::Playlists => {
|
||||
|
@ -527,7 +535,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
}
|
||||
|
||||
async fn player_event_loop(
|
||||
player: Arc<RwLock<P>>,
|
||||
player: Arc<RwLock<Prismriver>>,
|
||||
player_mail: MailMan<PlayerCommand, PlayerResponse>,
|
||||
) -> Result<(), ()> {
|
||||
// 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 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::reader::Reader;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
@ -221,7 +223,7 @@ fn to_tag(string: String) -> Tag {
|
|||
_ => 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() {
|
||||
Ok(tagged_file) => tagged_file.properties().duration(),
|
||||
|
||||
|
@ -229,12 +231,12 @@ fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> {
|
|||
};
|
||||
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 blank_tag = &lofty::Tag::new(TagType::Id3v2);
|
||||
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
|
||||
let tagged_file: lofty::TaggedFile;
|
||||
let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2);
|
||||
let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed);
|
||||
let tagged_file: lofty::file::TaggedFile;
|
||||
|
||||
let tag = match Probe::open(file)?.options(normal_options).read() {
|
||||
Ok(e) => {
|
||||
|
@ -288,7 +290,7 @@ impl ITunesSong {
|
|||
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();
|
||||
//get the path with the first bit chopped off
|
||||
let path_: String = map.get_key_value("Location").unwrap().1.clone();
|
||||
|
@ -339,10 +341,7 @@ impl ITunesSong {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{
|
||||
config::{Config, ConfigLibrary},
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use super::playlist::{Playlist, PlaylistFolder};
|
||||
// Crate things
|
||||
use super::utils::{find_images, normalize, read_file, write_file};
|
||||
use crate::config::Config;
|
||||
use crate::music_storage::playlist::PlaylistFolderItem;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
// Various std things
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::BTreeMap;
|
||||
use std::error::Error;
|
||||
use std::io::Read;
|
||||
use std::ops::ControlFlow::{Break, Continue};
|
||||
|
@ -14,9 +13,10 @@ use std::vec::IntoIter;
|
|||
|
||||
// Files
|
||||
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 std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -28,12 +28,11 @@ use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
|
|||
use std::time::Duration;
|
||||
|
||||
// Serialization/Compression
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Fun parallel stuff
|
||||
use rayon::prelude::*;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub enum AlbumArt {
|
||||
|
@ -225,10 +224,10 @@ impl Song {
|
|||
|
||||
/// Creates a `Song` from a music file
|
||||
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 tagged_file: lofty::TaggedFile;
|
||||
let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2);
|
||||
let tagged_file: lofty::file::TaggedFile;
|
||||
let mut duration = Duration::from_secs(0);
|
||||
let tag = match Probe::open(target_file)?.options(normal_options).read() {
|
||||
Ok(file) => {
|
||||
|
@ -273,7 +272,7 @@ impl Song {
|
|||
let value = match item.value() {
|
||||
ItemValue::Text(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);
|
||||
|
@ -548,10 +547,10 @@ impl URI {
|
|||
|
||||
pub fn as_uri(&self) -> String {
|
||||
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")
|
||||
.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")
|
||||
.to_string(),
|
||||
URI::Remote(_, location) => location.clone(),
|
||||
|
|
|
@ -28,12 +28,12 @@ futures = "0.3.31"
|
|||
crossbeam = "0.8.4"
|
||||
directories = "5.0.1"
|
||||
uuid = { version = "1.11.0", features = ["v4", "serde"] }
|
||||
ciborium = "0.2.2"
|
||||
mime = "0.3.17"
|
||||
file-format = "0.26.0"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
itertools = "0.13.0"
|
||||
rfd = "0.15.1"
|
||||
colog = "1.3.0"
|
||||
|
||||
[features]
|
||||
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 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 tauri::{http::Response, Emitter, Manager, State, Url, WebviewWindowBuilder, Wry};
|
||||
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_storage::library::MusicLibrary};
|
||||
use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry};
|
||||
use uuid::Uuid;
|
||||
|
||||
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();
|
||||
|
||||
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()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
|
|
|
@ -3,5 +3,7 @@
|
|||
|
||||
|
||||
fn main() {
|
||||
colog::init();
|
||||
|
||||
dango_music_player_lib::run()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue