mirror of
https://github.com/Dangoware/dango-music-player.git
synced 2025-04-19 10:02:53 -05:00
Added Playlist imports, refactored part of the controller 🍄
This commit is contained in:
parent
87965ef6da
commit
417666e21f
11 changed files with 295 additions and 172 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -29,3 +29,5 @@ dist-ssr
|
|||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
|
||||
test-config
|
|
@ -37,7 +37,7 @@ crossbeam = "0.8.2"
|
|||
quick-xml = "0.31.0"
|
||||
leb128 = "0.2.5"
|
||||
urlencoding = "2.1.3"
|
||||
m3u8-rs = "5.0.5"
|
||||
m3u8-rs = "6.0.0"
|
||||
thiserror = "1.0.56"
|
||||
uuid = { version = "1.6.1", features = ["v4", "serde"] }
|
||||
serde_json = "1.0.111"
|
||||
|
@ -54,3 +54,5 @@ text_io = "0.1.12"
|
|||
tokio = { version = "1.40.0", features = ["macros", "rt"] }
|
||||
async-channel = "2.3.1"
|
||||
ciborium = "0.2.2"
|
||||
itertools = "0.13.0"
|
||||
directories = "5.0.1"
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
//! other functions
|
||||
#![allow(while_true)]
|
||||
|
||||
use itertools::Itertools;
|
||||
use kushi::{Queue, QueueItemType};
|
||||
use kushi::{QueueError, QueueItem};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::error::Error;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
@ -15,6 +17,7 @@ use uuid::Uuid;
|
|||
use crate::config::ConfigError;
|
||||
use crate::music_player::player::{Player, PlayerError};
|
||||
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};
|
||||
|
@ -87,28 +90,20 @@ pub enum LibraryCommand {
|
|||
Song(Uuid),
|
||||
AllSongs,
|
||||
GetLibrary,
|
||||
Playlist(Uuid),
|
||||
ImportM3UPlayList(PathBuf)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LibraryResponse {
|
||||
Song(Song),
|
||||
Ok,
|
||||
Song(Song, usize),
|
||||
AllSongs(Vec<Song>),
|
||||
Library(MusicLibrary),
|
||||
Playlist(ExternalPlaylist),
|
||||
ImportM3UPlayList(Uuid, String)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd, Clone)]
|
||||
enum InnerLibraryCommand {
|
||||
Song(Uuid),
|
||||
AllSongs,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum InnerLibraryResponse<'a> {
|
||||
Song(&'a Song, usize),
|
||||
AllSongs(&'a Vec<Song>),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum QueueCommand {
|
||||
Append(QueueItem<QueueSong, QueueAlbum>, bool),
|
||||
|
@ -134,7 +129,10 @@ pub struct ControllerInput {
|
|||
MailMan<PlayerCommand, PlayerResponse>,
|
||||
MailMan<PlayerResponse, PlayerCommand>,
|
||||
),
|
||||
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
|
||||
lib_mail: (
|
||||
MailMan<LibraryCommand, LibraryResponse>,
|
||||
MailMan<LibraryResponse, LibraryCommand>
|
||||
),
|
||||
queue_mail: (
|
||||
MailMan<QueueCommand, QueueResponse>,
|
||||
MailMan<QueueResponse, QueueCommand>
|
||||
|
@ -157,13 +155,13 @@ impl ControllerHandle {
|
|||
|
||||
(
|
||||
ControllerHandle {
|
||||
lib_mail: lib_mail.0,
|
||||
lib_mail: lib_mail.0.clone(),
|
||||
player_mail: player_mail.0.clone(),
|
||||
queue_mail: queue_mail.0.clone()
|
||||
},
|
||||
ControllerInput {
|
||||
player_mail,
|
||||
lib_mail: lib_mail.1,
|
||||
lib_mail: lib_mail,
|
||||
queue_mail: queue_mail,
|
||||
library,
|
||||
config
|
||||
|
@ -203,7 +201,6 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
// true,
|
||||
// );
|
||||
// }
|
||||
let inner_lib_mail = MailMan::double();
|
||||
let queue = queue;
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
|
@ -215,14 +212,14 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
let player = Arc::new(RwLock::new(P::new().unwrap()));
|
||||
|
||||
let _player = player.clone();
|
||||
let _inner_lib_mail = inner_lib_mail.0.clone();
|
||||
let _lib_mail = lib_mail.0.clone();
|
||||
scope
|
||||
.spawn(async move {
|
||||
Controller::<P>::player_command_loop(
|
||||
_player,
|
||||
player_mail.1,
|
||||
queue_mail.0,
|
||||
_inner_lib_mail
|
||||
_lib_mail,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -235,12 +232,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
});
|
||||
scope
|
||||
.spawn(async {
|
||||
Controller::<P>::inner_library_loop(inner_lib_mail.1, &mut library).await
|
||||
.unwrap()
|
||||
});
|
||||
scope
|
||||
.spawn(async {
|
||||
Controller::<P>::outer_library_loop(lib_mail, inner_lib_mail.0)
|
||||
Controller::<P>::library_loop(lib_mail.1, &mut library)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
@ -265,7 +257,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
player: Arc<RwLock<P>>,
|
||||
player_mail: MailMan<PlayerResponse, PlayerCommand>,
|
||||
queue_mail: MailMan<QueueCommand, QueueResponse>,
|
||||
inner_lib_mail: MailMan<InnerLibraryCommand, InnerLibraryResponse<'c>>
|
||||
lib_mail: MailMan<LibraryCommand, LibraryResponse>,
|
||||
) -> Result<(), ()> {
|
||||
let mut first = true;
|
||||
while true {
|
||||
|
@ -306,13 +298,13 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
|
||||
|
||||
// Append next song in library
|
||||
inner_lib_mail.send(InnerLibraryCommand::AllSongs).await.unwrap();
|
||||
lib_mail.send(LibraryCommand::AllSongs).await.unwrap();
|
||||
|
||||
let InnerLibraryResponse::AllSongs(songs) = inner_lib_mail.recv().await.unwrap() else {
|
||||
let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
inner_lib_mail.send(InnerLibraryCommand::Song(np_song.song.uuid.clone())).await.unwrap();
|
||||
let InnerLibraryResponse::Song(_, i) = inner_lib_mail.recv().await.unwrap() else {
|
||||
lib_mail.send(LibraryCommand::Song(np_song.song.uuid.clone())).await.unwrap();
|
||||
let LibraryResponse::Song(_, i) = lib_mail.recv().await.unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
if let Some(song) = songs.get(i + 49) {
|
||||
|
@ -373,8 +365,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
}
|
||||
PlayerCommand::PlayNow(uuid, location) => {
|
||||
// TODO: This assumes the uuid doesn't point to an album. we've been over this.
|
||||
inner_lib_mail.send(InnerLibraryCommand::Song(uuid)).await.unwrap();
|
||||
let InnerLibraryResponse::Song(song, index) = inner_lib_mail.recv().await.unwrap() else {
|
||||
lib_mail.send(LibraryCommand::Song(uuid)).await.unwrap();
|
||||
let LibraryResponse::Song(song, index) = lib_mail.recv().await.unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
queue_mail.send(QueueCommand::Clear).await.unwrap();
|
||||
|
@ -392,8 +384,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
// ...
|
||||
// let's just pretend I figured that out already
|
||||
|
||||
inner_lib_mail.send(InnerLibraryCommand::AllSongs).await.unwrap();
|
||||
let InnerLibraryResponse::AllSongs(songs) = inner_lib_mail.recv().await.unwrap() else {
|
||||
lib_mail.send(LibraryCommand::AllSongs).await.unwrap();
|
||||
let LibraryResponse::AllSongs(songs) = lib_mail.recv().await.unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
|
@ -419,59 +411,32 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn outer_library_loop(
|
||||
async fn library_loop(
|
||||
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
|
||||
inner_lib_mail: MailMan<InnerLibraryCommand, InnerLibraryResponse<'c>>,
|
||||
) -> Result<(), ()> {
|
||||
while true {
|
||||
match lib_mail.recv().await.unwrap() {
|
||||
LibraryCommand::Song(uuid) => {
|
||||
inner_lib_mail
|
||||
.send(InnerLibraryCommand::Song(uuid))
|
||||
.await
|
||||
.unwrap();
|
||||
let InnerLibraryResponse::Song(song, i) = inner_lib_mail.recv().await.unwrap() else {
|
||||
unimplemented!();
|
||||
};
|
||||
lib_mail.send(LibraryResponse::Song(song.clone())).await.unwrap();
|
||||
}
|
||||
LibraryCommand::AllSongs => {
|
||||
inner_lib_mail
|
||||
.send(InnerLibraryCommand::AllSongs)
|
||||
.await
|
||||
.unwrap();
|
||||
let x = inner_lib_mail.recv().await.unwrap();
|
||||
if let InnerLibraryResponse::AllSongs(songs) = x {
|
||||
lib_mail.send(LibraryResponse::AllSongs(songs.clone())).await.unwrap();
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
},
|
||||
_ => { todo!() }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn inner_library_loop(
|
||||
lib_mail: MailMan<InnerLibraryResponse<'c>, InnerLibraryCommand>,
|
||||
library: &'c mut MusicLibrary,
|
||||
) -> Result<(), ()> {
|
||||
while true {
|
||||
match lib_mail.recv().await.unwrap() {
|
||||
InnerLibraryCommand::Song(uuid) => {
|
||||
let (song, i): (&'c Song, usize) = library.query_uuid(&uuid).unwrap();
|
||||
lib_mail
|
||||
.send(InnerLibraryResponse::Song(song, i))
|
||||
.await
|
||||
.unwrap();
|
||||
LibraryCommand::Song(uuid) => {
|
||||
let (song, i) = library.query_uuid(&uuid).unwrap();
|
||||
lib_mail.send(LibraryResponse::Song(song.clone(), i)).await.unwrap();
|
||||
}
|
||||
InnerLibraryCommand::AllSongs => {
|
||||
let songs: &'c Vec<Song> = &library.library;
|
||||
lib_mail.send(InnerLibraryResponse::AllSongs(songs))
|
||||
.await
|
||||
.unwrap();
|
||||
LibraryCommand::AllSongs => {
|
||||
lib_mail.send(LibraryResponse::AllSongs(library.library.clone())).await.unwrap();
|
||||
},
|
||||
LibraryCommand::Playlist(uuid) => {
|
||||
let playlist = library.query_playlist_uuid(&uuid).unwrap();
|
||||
lib_mail.send(LibraryResponse::Playlist(ExternalPlaylist::from_playlist(playlist, &library))).await.unwrap();
|
||||
}
|
||||
LibraryCommand::ImportM3UPlayList(path) => {
|
||||
let playlist = Playlist::from_m3u(path, library).unwrap();
|
||||
let uuid = playlist.uuid.clone();
|
||||
let name = playlist.title.clone();
|
||||
library.playlists.items.push(PlaylistFolderItem::List(playlist));
|
||||
|
||||
lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap();
|
||||
}
|
||||
_ => { todo!() }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -2,6 +2,7 @@ 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
|
||||
|
@ -687,7 +688,7 @@ impl AlbumTrack {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MusicLibrary {
|
||||
pub name: String,
|
||||
pub uuid: Uuid,
|
||||
|
@ -1237,8 +1238,12 @@ impl MusicLibrary {
|
|||
Ok(albums)
|
||||
}
|
||||
|
||||
pub fn query_playlist_uuid(&self, uuid: &Uuid) -> Result<Playlist, Box<dyn Error>> {
|
||||
todo!()
|
||||
pub fn query_playlist_uuid(&self, uuid: &Uuid) -> Option<&Playlist> {
|
||||
self.playlists.query_uuid(uuid)
|
||||
}
|
||||
|
||||
pub fn push_playlist(&mut self, playlist: PlaylistFolderItem) {
|
||||
self.playlists.items.push(playlist);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
|
@ -10,6 +11,7 @@ use std::time::Duration;
|
|||
|
||||
// use chrono::Duration;
|
||||
use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -28,8 +30,8 @@ nest! {
|
|||
#[derive(Debug, Clone, Deserialize, Serialize)]*
|
||||
#[derive(Default)]
|
||||
pub struct PlaylistFolder {
|
||||
name: String,
|
||||
items: Vec<
|
||||
pub(crate) name: String,
|
||||
pub(crate) items: Vec<
|
||||
pub enum PlaylistFolderItem {
|
||||
Folder(PlaylistFolder),
|
||||
List(Playlist)
|
||||
|
@ -38,15 +40,29 @@ nest! {
|
|||
}
|
||||
}
|
||||
|
||||
impl PlaylistFolder {
|
||||
pub fn query_uuid(&self, uuid: &Uuid) -> Option<&Playlist> {
|
||||
for item in &self.items {
|
||||
match item {
|
||||
PlaylistFolderItem::Folder(folder) => return folder.query_uuid(uuid),
|
||||
PlaylistFolderItem::List(ref playlist) => if &playlist.uuid == uuid {
|
||||
return Some(&playlist);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Playlist {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
cover: Option<AlbumArt>,
|
||||
tracks: Vec<Uuid>,
|
||||
sort_order: SortOrder,
|
||||
play_count: i32,
|
||||
play_time: Duration,
|
||||
pub(crate) uuid: Uuid,
|
||||
pub(crate) title: String,
|
||||
pub(crate) cover: Option<AlbumArt>,
|
||||
pub(crate) tracks: Vec<Uuid>,
|
||||
pub(crate) sort_order: SortOrder,
|
||||
pub(crate) play_count: i32,
|
||||
pub(crate) play_time: Duration,
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
|
@ -115,7 +131,7 @@ impl Playlist {
|
|||
super::utils::read_file(PathBuf::from(path))
|
||||
}
|
||||
|
||||
pub fn to_m3u8(
|
||||
pub fn to_m3u(
|
||||
&mut self,
|
||||
lib: Arc<RwLock<MusicLibrary>>,
|
||||
location: &str,
|
||||
|
@ -146,9 +162,9 @@ impl Playlist {
|
|||
})
|
||||
.collect::<Vec<MediaSegment>>();
|
||||
|
||||
let m3u8 = MediaPlaylist {
|
||||
let m3u = MediaPlaylist {
|
||||
version: Some(6),
|
||||
target_duration: 3.0,
|
||||
target_duration: 3,
|
||||
media_sequence: 338559,
|
||||
discontinuity_sequence: 1234,
|
||||
end_list: true,
|
||||
|
@ -163,18 +179,15 @@ impl Playlist {
|
|||
.truncate(true)
|
||||
.write(true)
|
||||
.open(location)?;
|
||||
m3u8.write_to(&mut file)?;
|
||||
m3u.write_to(&mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn from_m3u8(
|
||||
path: &str,
|
||||
lib: Arc<RwLock<MusicLibrary>>,
|
||||
pub fn from_m3u(
|
||||
m3u_path: impl AsRef<Path>,
|
||||
lib: &mut MusicLibrary,
|
||||
) -> Result<Playlist, Box<dyn Error>> {
|
||||
let mut file = match File::open(path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let mut file = File::open(&m3u_path)?;
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes).unwrap();
|
||||
|
||||
|
@ -182,7 +195,7 @@ impl Playlist {
|
|||
|
||||
let playlist = match parsed {
|
||||
Result::Ok((_, playlist)) => playlist,
|
||||
Result::Err(e) => panic!("Parsing error: \n{}", e),
|
||||
Result::Err(e) => panic!("Parsing error\n{e}"),
|
||||
};
|
||||
|
||||
match playlist {
|
||||
|
@ -192,28 +205,39 @@ impl Playlist {
|
|||
List2::MediaPlaylist(playlist_) => {
|
||||
let mut uuids = Vec::new();
|
||||
for seg in playlist_.segments {
|
||||
let path_ = PathBuf::from(seg.uri.to_owned());
|
||||
let mut lib = lib.write().unwrap();
|
||||
let seg_path = seg.uri.to_owned();
|
||||
|
||||
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) {
|
||||
let song_path = if let Ok(path) = PathBuf::from(&seg_path).canonicalize() {
|
||||
path
|
||||
} else {
|
||||
println!("{seg_path}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(song_path.clone())) {
|
||||
song.uuid
|
||||
} else {
|
||||
let song_ = Song::from_file(&path_)?;
|
||||
let song_: Song = match Song::from_file(&song_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => panic!("{e}\npath: {}", song_path.display())
|
||||
};
|
||||
let uuid = song_.uuid.to_owned();
|
||||
lib.add_song(song_)?;
|
||||
_ = lib.add_song(song_); // TODO: Add proper error handling with Library
|
||||
uuid
|
||||
};
|
||||
uuids.push(uuid);
|
||||
}
|
||||
let mut playlist = Playlist::new();
|
||||
|
||||
let path: &str = m3u_path.as_ref().to_str().unwrap();
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
{
|
||||
playlist.title = path
|
||||
.split('\\')
|
||||
.last()
|
||||
.unwrap_or_default()
|
||||
.strip_suffix(".m3u8")
|
||||
.strip_suffix(".m3u")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
}
|
||||
|
@ -223,7 +247,7 @@ impl Playlist {
|
|||
.split("/")
|
||||
.last()
|
||||
.unwrap_or_default()
|
||||
.strip_suffix(".m3u8")
|
||||
.strip_suffix(".m3u")
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
}
|
||||
|
@ -313,42 +337,72 @@ impl Default for Playlist {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ExternalPlaylist {
|
||||
pub uuid: Uuid,
|
||||
pub title: String,
|
||||
pub tracks: Vec<Song>,
|
||||
pub sort_order: SortOrder,
|
||||
pub play_count: i32,
|
||||
pub play_time: Duration,
|
||||
}
|
||||
|
||||
impl ExternalPlaylist {
|
||||
pub(crate) fn from_playlist(playlist: &Playlist, library: &MusicLibrary) -> Self {
|
||||
let tracks: Vec<Song> = playlist.tracks.iter().filter_map(|uuid| {
|
||||
library.query_uuid(uuid).map(|res| res.0.clone())
|
||||
}).collect_vec();
|
||||
|
||||
Self {
|
||||
uuid: playlist.uuid.clone(),
|
||||
title: playlist.title.clone(),
|
||||
tracks,
|
||||
sort_order: playlist.sort_order.clone(),
|
||||
play_count: playlist.play_count,
|
||||
play_time: playlist.play_time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_super {
|
||||
use super::*;
|
||||
use crate::config::tests::read_config_lib;
|
||||
use crate::config::tests::{new_config_lib, read_config_lib};
|
||||
|
||||
#[test]
|
||||
fn list_to_m3u8() {
|
||||
fn list_to_m3u() {
|
||||
let (_, lib) = read_config_lib();
|
||||
let mut playlist = Playlist::new();
|
||||
let tracks = lib.library.iter().map(|track| track.uuid).collect();
|
||||
playlist.set_tracks(tracks);
|
||||
|
||||
_ = playlist.to_m3u8(
|
||||
playlist.to_m3u(
|
||||
Arc::new(RwLock::from(lib)),
|
||||
".\\test-config\\playlists\\playlist.m3u8",
|
||||
);
|
||||
}
|
||||
|
||||
fn m3u8_to_list() -> Playlist {
|
||||
let (_, lib) = read_config_lib();
|
||||
let arc = Arc::new(RwLock::from(lib));
|
||||
let playlist =
|
||||
Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap();
|
||||
|
||||
_ = playlist.to_file(".\\test-config\\playlists\\playlist");
|
||||
dbg!(playlist)
|
||||
".\\test-config\\playlists\\playlist.m3u",
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_queue_sort() {
|
||||
let (_, lib) = read_config_lib();
|
||||
let mut list = m3u8_to_list();
|
||||
list.sort_order = SortOrder::Tag(vec![Tag::Album]);
|
||||
fn m3u_to_list() {
|
||||
let (_, mut lib) = read_config_lib();
|
||||
|
||||
let songs = &list.out_tracks(Arc::new(RwLock::from(lib)));
|
||||
let playlist =
|
||||
Playlist::from_m3u("F:\\Music\\Mp3\\Music Main\\Playlists\\Nanahira.m3u", &mut lib).unwrap();
|
||||
|
||||
dbg!(songs);
|
||||
_ = playlist.to_file(".\\test-config\\playlists\\playlist");
|
||||
dbg!(&playlist, playlist.tracks.len());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn out_queue_sort() {
|
||||
// let (_, lib) = read_config_lib();
|
||||
// let mut list = m3u_to_list();
|
||||
// list.sort_order = SortOrder::Tag(vec![Tag::Album]);
|
||||
|
||||
// let songs = &list.out_tracks(Arc::new(RwLock::from(lib)));
|
||||
|
||||
// dbg!(songs);
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -450,7 +450,7 @@ impl<
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq, PartialOrd, Clone)]
|
||||
#[derive(Error, Debug, PartialEq, Eq, PartialOrd, Clone)]
|
||||
pub enum QueueError {
|
||||
#[error("Index out of bounds! Index {index} is over len {len}")]
|
||||
OutOfBounds { index: usize, len: usize },
|
||||
|
|
|
@ -33,6 +33,7 @@ mime = "0.3.17"
|
|||
file-format = "0.26.0"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
itertools = "0.13.0"
|
||||
rfd = "0.15.1"
|
||||
|
||||
[features]
|
||||
default = [ "custom-protocol" ]
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::wrappers::_Song;
|
|||
#[tauri::command]
|
||||
pub async fn add_song_to_queue(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid, location: PlayerLocation) -> Result<(), String> {
|
||||
ctrl_handle.lib_mail.send(dmp_core::music_controller::controller::LibraryCommand::Song(uuid)).await.unwrap();
|
||||
let LibraryResponse::Song(song) = ctrl_handle.lib_mail.recv().await.unwrap() else {
|
||||
let LibraryResponse::Song(song, _) = ctrl_handle.lib_mail.recv().await.unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
ctrl_handle.queue_mail.send(dmp_core::music_controller::controller::QueueCommand::Append(QueueItem::from_item_type(kushi::QueueItemType::Single(QueueSong { song, location })), true)).await.unwrap();
|
||||
|
|
|
@ -6,7 +6,7 @@ use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{C
|
|||
use tauri::{http::Response, Emitter, Manager, State, Url, WebviewWindowBuilder, Wry};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue};
|
||||
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist};
|
||||
|
||||
pub mod wrappers;
|
||||
pub mod commands;
|
||||
|
@ -74,6 +74,8 @@ pub fn run() {
|
|||
get_queue,
|
||||
add_song_to_queue,
|
||||
play_now,
|
||||
import_playlist,
|
||||
get_playlist,
|
||||
]).manage(ConfigRx(rx))
|
||||
.manage(LibRx(lib_rx))
|
||||
.manage(HandleTx(handle_tx))
|
||||
|
@ -94,7 +96,7 @@ pub fn run() {
|
|||
futures::executor::block_on(async move {
|
||||
let controller = ctx.app_handle().state::<ControllerHandle>();
|
||||
controller.lib_mail.send(dmp_core::music_controller::controller::LibraryCommand::Song(Uuid::parse_str(query.as_str()).unwrap())).await.unwrap();
|
||||
let LibraryResponse::Song(song) = controller.lib_mail.recv().await.unwrap() else { unreachable!() };
|
||||
let LibraryResponse::Song(song, _) = controller.lib_mail.recv().await.unwrap() else { unreachable!() };
|
||||
song.album_art(0).unwrap_or_else(|_| None).unwrap_or(DEFAULT_IMAGE.to_vec())
|
||||
})};
|
||||
res.respond(
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use chrono::{DateTime, Utc, serde::ts_milliseconds_option};
|
||||
use crossbeam::channel::Sender;
|
||||
use dmp_core::{music_controller::controller::{ControllerHandle, LibraryCommand, LibraryResponse, PlayerResponse, QueueCommand, QueueResponse}, music_storage::library::{BannedType, Song, URI}};
|
||||
use dmp_core::{music_controller::controller::{ControllerHandle, LibraryCommand, LibraryResponse, PlayerResponse, QueueCommand, QueueResponse}, music_storage::library::{BannedType, Song, Tag, URI}};
|
||||
use itertools::Itertools;
|
||||
use kushi::QueueItemType;
|
||||
use serde::Serialize;
|
||||
use tauri::{ipc::Response, AppHandle, Emitter, State, Wry};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands;
|
||||
|
||||
pub struct ArtworkRx(pub Sender<Vec<u8>>);
|
||||
|
||||
#[tauri::command]
|
||||
|
@ -130,6 +132,7 @@ impl From<&Song> for _Song {
|
|||
#[tauri::command]
|
||||
pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec<_Song>, String> {
|
||||
ctrl_handle.lib_mail.send(LibraryCommand::AllSongs).await.unwrap();
|
||||
println!("getting library");
|
||||
let LibraryResponse::AllSongs(songs) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
|
||||
|
||||
let _songs = songs.iter().map(|song| _Song::from(song)).collect::<Vec<_>>();
|
||||
|
@ -138,9 +141,40 @@ pub async fn get_library(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_song(ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> {
|
||||
ctrl_handle.lib_mail.send(LibraryCommand::Song(Uuid::default())).await.unwrap();
|
||||
let LibraryResponse::Song(_) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
|
||||
println!("got songs");
|
||||
Ok(())
|
||||
pub async fn get_playlist(ctrl_handle: State<'_, ControllerHandle>, uuid: Uuid) -> Result<Vec<_Song>, String> {
|
||||
ctrl_handle.lib_mail.send(LibraryCommand::Playlist(uuid)).await.unwrap();
|
||||
let LibraryResponse::Playlist(playlist) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
|
||||
|
||||
let songs = playlist.tracks.iter().map(|song| _Song::from(song)).collect::<Vec<_>>();
|
||||
println!("Got Playlist {}, len {}", playlist.title, playlist.tracks.len());
|
||||
Ok(songs)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_playlist(ctrl_handle: State<'_, ControllerHandle>) -> Result<PlaylistPayload, String> {
|
||||
let file = rfd::AsyncFileDialog::new()
|
||||
.add_filter("m3u8 Playlist", &["m3u8", "m3u"])
|
||||
.set_title("Import a Playlist")
|
||||
.pick_file()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctrl_handle.lib_mail.send(LibraryCommand::ImportM3UPlayList(PathBuf::from(file.path()))).await.unwrap();
|
||||
let LibraryResponse::ImportM3UPlayList(uuid, name) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
|
||||
println!("Imported Playlist {name}");
|
||||
Ok(PlaylistPayload {uuid, name})
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PlaylistPayload {
|
||||
uuid: Uuid,
|
||||
name: String
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_song(ctrl_handle: State<'_, ControllerHandle>) -> Result<_Song, String> {
|
||||
ctrl_handle.lib_mail.send(LibraryCommand::Song(Uuid::default())).await.unwrap();
|
||||
let LibraryResponse::Song(song, _) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
|
||||
println!("got song {}", &song.tags.get(&Tag::Title).unwrap_or(&String::new()));
|
||||
Ok(_Song::from(&song))
|
||||
}
|
112
src/App.tsx
112
src/App.tsx
|
@ -13,6 +13,8 @@ function App() {
|
|||
const library = useState<JSX.Element[]>([]);
|
||||
const [queue, setQueue] = useState<JSX.Element[]>([]);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [playlists, setPlaylists] = useState<JSX.Element[]>([]);
|
||||
const [viewName, setViewName] = useState("Library");
|
||||
|
||||
const [nowPlaying, setNowPlaying] = useState<JSX.Element>(
|
||||
<NowPlaying
|
||||
|
@ -72,13 +74,13 @@ function App() {
|
|||
return (
|
||||
<main className="container">
|
||||
<div className="leftSide">
|
||||
<PlaylistHead />
|
||||
<MainView lib_ref={ library } />
|
||||
<PlaylistHead playlists={ playlists } setPlaylists={ setPlaylists } setViewName={ setViewName } setLibrary={ library[1] } />
|
||||
<MainView lib_ref={ library } viewName={ viewName } />
|
||||
<PlayBar playing={ playing } setPlaying={ setPlaying } />
|
||||
</div>
|
||||
<div className="rightSide">
|
||||
{ nowPlaying }
|
||||
<Queue songs={queue} setSongs={ setQueue } />
|
||||
<Queue songs={ queue } setSongs={ setQueue } />
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
@ -87,47 +89,86 @@ function App() {
|
|||
|
||||
export default App;
|
||||
|
||||
function getConfig(): any {
|
||||
invoke('get_config').then( (_config) => {
|
||||
let config = _config as Config;
|
||||
if (config.libraries.libraries.length == 0) {
|
||||
newWindow()
|
||||
} else {
|
||||
// console.log("else");
|
||||
invoke('lib_already_created').then(() => {})
|
||||
}
|
||||
})
|
||||
interface PlaylistHeadProps {
|
||||
playlists: JSX.Element[]
|
||||
setPlaylists: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
|
||||
setViewName: React.Dispatch<React.SetStateAction<string>>,
|
||||
setLibrary: React.Dispatch<React.SetStateAction<JSX.Element[]>>,
|
||||
}
|
||||
|
||||
function newWindow() {
|
||||
invoke('new_library_window').then(() => {})
|
||||
}
|
||||
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary }: PlaylistHeadProps) {
|
||||
|
||||
function PlaylistHead() {
|
||||
let handle_import = () => {
|
||||
invoke('import_playlist').then((_res) => {
|
||||
let res = _res as any;
|
||||
|
||||
setPlaylists([
|
||||
...playlists,
|
||||
<button onClick={ () => {
|
||||
invoke('get_playlist', { uuid: res.uuid }).then((list) => {
|
||||
console.log((list as any[]).length);
|
||||
|
||||
setLibrary([...(list as any[]).map((song) => {
|
||||
// console.log(song);
|
||||
return (
|
||||
<Song
|
||||
key={ song.uuid }
|
||||
location={ song.location }
|
||||
uuid={ song.uuid }
|
||||
plays={ song.plays }
|
||||
duration={ song.duration }
|
||||
tags={ song.tags }
|
||||
/>
|
||||
)
|
||||
})])
|
||||
})
|
||||
setViewName( res.name )
|
||||
} } key={ 'playlist_' + res.uuid }>{ res.name }</button>
|
||||
])
|
||||
console.log(res.name);
|
||||
})
|
||||
}
|
||||
return (
|
||||
<section className="playlistHead">
|
||||
<button>Library</button>
|
||||
<button>Playlist 1</button>
|
||||
<button>Playlist 2</button>
|
||||
<button>Playlist 3</button>
|
||||
<button>Playlist 4</button>
|
||||
<button>Playlist 5</button>
|
||||
<button>Playlist 6</button>
|
||||
<button onClick={() => {
|
||||
setViewName("Library");
|
||||
invoke('get_library').then((lib) => {
|
||||
setLibrary([...(lib as any[]).map((song) => {
|
||||
console.log(song);
|
||||
|
||||
return (
|
||||
<Song
|
||||
key={ song.uuid }
|
||||
location={ song.location }
|
||||
uuid={ song.uuid }
|
||||
plays={ song.plays }
|
||||
duration={ song.duration }
|
||||
tags={ song.tags }
|
||||
/>
|
||||
)
|
||||
})])
|
||||
})
|
||||
} }>Library</button>
|
||||
{ playlists }
|
||||
<button onClick={ handle_import }>Import .m3u8 Playlist</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
interface MainViewProps {
|
||||
lib_ref: [JSX.Element[], React.Dispatch<React.SetStateAction<JSX.Element[]>>],
|
||||
viewName: string
|
||||
}
|
||||
|
||||
function MainView({ lib_ref }: MainViewProps) {
|
||||
function MainView({ lib_ref, viewName }: MainViewProps) {
|
||||
const [library, setLibrary] = lib_ref;
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = appWindow.listen<any>("library_loaded", (_) => {
|
||||
console.log("library_loaded");
|
||||
|
||||
invoke('get_library').then((lib) => {
|
||||
setLibrary([...(lib as any[]).map((song, i) => {
|
||||
setLibrary([...(lib as any[]).map((song) => {
|
||||
console.log(song);
|
||||
|
||||
return (
|
||||
|
@ -146,9 +187,10 @@ function MainView({ lib_ref }: MainViewProps) {
|
|||
return () => { unlisten.then((f) => f()) }
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className="mainView">
|
||||
<h1>Library</h1>
|
||||
<h1>{ viewName }</h1>
|
||||
<div>{ library }</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -263,3 +305,19 @@ function QueueSong({ song }: QueueSongProps) {
|
|||
// </button>
|
||||
)
|
||||
}
|
||||
|
||||
function getConfig(): any {
|
||||
invoke('get_config').then( (_config) => {
|
||||
let config = _config as Config;
|
||||
if (config.libraries.libraries.length == 0) {
|
||||
newWindow()
|
||||
} else {
|
||||
// console.log("else");
|
||||
invoke('lib_already_created').then(() => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function newWindow() {
|
||||
invoke('new_library_window').then(() => {})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue