Added basic Queue functionality

This commit is contained in:
MrDulfin 2024-12-15 12:27:24 -05:00
parent 93d6b059a0
commit d8cf8eeb2b
10 changed files with 223 additions and 93 deletions

View file

@ -5,7 +5,7 @@
use kushi::{Queue, QueueItemType};
use kushi::{QueueError, QueueItem};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::marker::PhantomData;
use std::sync::{Arc, RwLock};
@ -32,7 +32,7 @@ pub enum ControllerError {
}
// TODO: move this to a different location to be used elsewhere
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PlayerLocation {
Test,
@ -103,17 +103,21 @@ enum InnerLibraryResponse<'a> {
AllSongs(&'a Vec<Song>),
}
#[derive(Debug, Clone)]
pub enum QueueCommand {
Append(QueueItem<QueueSong, QueueAlbum>),
Next,
Prev,
GetIndex(usize),
NowPlaying,
Get
}
#[derive(Debug, Clone)]
pub enum QueueResponse {
Ok,
Item(QueueItem<QueueSong, QueueAlbum>),
Get(Vec<QueueItem<QueueSong, QueueAlbum>>)
}
@ -123,6 +127,10 @@ pub struct ControllerInput {
MailMan<PlayerResponse, PlayerCommand>,
),
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
queue_mail: (
MailMan<QueueCommand, QueueResponse>,
MailMan<QueueResponse, QueueCommand>
),
library: MusicLibrary,
config: Arc<RwLock<Config>>,
}
@ -130,21 +138,25 @@ pub struct ControllerInput {
pub struct ControllerHandle {
pub lib_mail: MailMan<LibraryCommand, LibraryResponse>,
pub player_mail: MailMan<PlayerCommand, PlayerResponse>,
pub queue_mail: MailMan<QueueCommand, QueueResponse>,
}
impl ControllerHandle {
pub fn new(library: MusicLibrary, config: Arc<RwLock<Config>>) -> (Self, ControllerInput) {
let lib_mail = MailMan::double();
let player_mail = MailMan::double();
let queue_mail = MailMan::double();
(
ControllerHandle {
lib_mail: lib_mail.0,
player_mail: player_mail.0.clone()
player_mail: player_mail.0.clone(),
queue_mail: queue_mail.0.clone()
},
ControllerInput {
player_mail,
lib_mail: lib_mail.1,
queue_mail: queue_mail,
library,
config
}
@ -158,6 +170,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
ControllerInput {
player_mail,
lib_mail,
queue_mail,
mut library,
config
}: ControllerInput
@ -173,20 +186,20 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
shuffle: None,
};
for song in &library.library {
queue.add_item(
QueueSong {
song: song.clone(),
location: PlayerLocation::Test,
},
true,
);
}
// for song in &library.library {
// queue.add_item(
// QueueSong {
// song: song.clone(),
// location: PlayerLocation::Test,
// },
// true,
// );
// }
let inner_lib_mail = MailMan::double();
let queue = queue;
std::thread::scope(|scope| {
let queue_mail = MailMan::double();
let queue_mail = queue_mail;
let a = scope.spawn(|| {
futures::executor::block_on(async {
moro::async_scope!(|scope| {
@ -243,14 +256,19 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
player_mail: MailMan<PlayerResponse, PlayerCommand>,
queue_mail: MailMan<QueueCommand, QueueResponse>,
) -> Result<(), ()> {
{
player.write().unwrap().set_volume(0.05);
}
let mut first = true;
while true {
let _mail = player_mail.recv().await;
if let Ok(mail) = _mail {
match mail {
PlayerCommand::Play => {
if first {
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();
first = false
}
player.write().unwrap().play().unwrap();
player_mail.send(PlayerResponse::Empty).await.unwrap();
}
@ -272,8 +290,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
_ => unimplemented!(),
};
player.write().unwrap().enqueue_next(uri).unwrap();
let QueueItemType::Single(x) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
player_mail.send(PlayerResponse::NowPlaying(x.song.clone())).await.unwrap();
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();
}
}
PlayerCommand::PrevSong => {
@ -284,8 +302,8 @@ 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();
player_mail.send(PlayerResponse::Empty).await.unwrap();
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();;
}
}
PlayerCommand::Enqueue(index) => {
@ -319,16 +337,17 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
lib_mail: MailMan<LibraryResponse, LibraryCommand>,
inner_lib_mail: MailMan<InnerLibraryCommand, InnerLibraryResponse<'c>>,
) -> Result<(), ()> {
println!("outer lib loop");
while true {
match lib_mail.recv().await.unwrap() {
LibraryCommand::Song(uuid) => {
println!("got song commandf");
inner_lib_mail
.send(InnerLibraryCommand::Song(uuid))
.await
.unwrap();
let x = inner_lib_mail.recv().await.unwrap();
let InnerLibraryResponse::Song(song) = inner_lib_mail.recv().await.unwrap() else {
unimplemented!();
};
lib_mail.send(LibraryResponse::Song(song.clone())).await.unwrap();
}
LibraryCommand::AllSongs => {
inner_lib_mail
@ -386,9 +405,15 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
) {
while true {
match queue_mail.recv().await.unwrap() {
QueueCommand::Append(item) => match item.item {
QueueItemType::Single(song) => queue.add_item(song, true),
_ => unimplemented!(),
QueueCommand::Append(item) => {
match item.item {
QueueItemType::Single(song) => queue.add_item(song, true),
_ => unimplemented!(),
}
queue_mail
.send(QueueResponse::Ok)
.await
.unwrap();
},
QueueCommand::Next => {
let next = queue.next().unwrap();
@ -405,7 +430,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
.unwrap();
}
QueueCommand::GetIndex(index) => {
let item = queue.items[index].clone();
let item = queue.items.get(index).expect("No item in the queue at index {index}").clone();
queue_mail.send(QueueResponse::Item(item)).await.unwrap();
}
QueueCommand::NowPlaying => {
@ -415,6 +440,9 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
.await
.unwrap();
}
QueueCommand::Get => {
queue_mail.send(QueueResponse::Get(queue.items.clone())).await.unwrap();
}
}
}
}
@ -445,7 +473,7 @@ mod test_super {
new_config_lib();
let config = Config::read_file(PathBuf::from(std::env!("CONFIG-PATH"))).unwrap();
let mut library = {
let library = {
MusicLibrary::init(
config.libraries.get_default().unwrap().path.clone(),
config.libraries.get_default().unwrap().uuid,

View file

@ -475,18 +475,23 @@ impl Song {
}
}
pub fn album_art(&self, i: usize) -> Result<Vec<u8>, Box<dyn Error>> {
match self.album_art.get(i).unwrap() {
AlbumArt::Embedded(j) => {
let file = lofty::read_from_path(self.primary_uri()?.0.path())?;
Ok(file.tag(file.primary_tag_type()).unwrap().pictures()[*j].data().to_vec())
},
AlbumArt::External(ref path) => {
let mut buf = vec![];
std::fs::File::open(path.path())?.read_to_end(&mut buf)?;
Ok(buf)
pub fn album_art(&self, i: usize) -> Result<Option<Vec<u8>>, Box<dyn Error>> {
if let Some(art) = self.album_art.get(i) {
match art {
AlbumArt::Embedded(j) => {
let file = lofty::read_from_path(self.primary_uri()?.0.path())?;
Ok(Some(file.tag(file.primary_tag_type()).unwrap().pictures()[*j].data().to_vec()))
},
AlbumArt::External(ref path) => {
let mut buf = vec![];
std::fs::File::open(path.path())?.read_to_end(&mut buf)?;
Ok(Some(buf))
}
}
} else {
Ok(None)
}
}
}

View file

@ -57,7 +57,7 @@ QueueItem<T, U> {
}
}
#[derive(Debug, Default)]
#[derive(Debug, Clone, Default)]
pub struct Queue<
T: Debug + Clone + PartialEq, // T: The Singular Item Type
U: Debug + PartialEq + Clone + IntoIterator, // U: The Multi-Item Type. Needs to be tracked as multiple items

View file

@ -19,6 +19,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
dmp-core = { path = "../dmp-core" }
kushi = { path = "../kushi-queue" }
tauri = { version = "2", features = [ "protocol-asset", "unstable"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
@ -26,11 +27,12 @@ serde_json = "1"
futures = "0.3.31"
crossbeam = "0.8.4"
directories = "5.0.1"
uuid = { version = "1.11.0", features = ["v4"] }
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"
[features]
default = [ "custom-protocol" ]

20
src-tauri/src/commands.rs Normal file
View file

@ -0,0 +1,20 @@
use dmp_core::music_controller::{controller::{ControllerHandle, LibraryResponse, PlayerLocation, QueueResponse}, queue::QueueSong};
use kushi::QueueItem;
use tauri::{AppHandle, Emitter, State, Wry};
use uuid::Uuid;
#[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 {
unreachable!()
};
ctrl_handle.queue_mail.send(dmp_core::music_controller::controller::QueueCommand::Append(QueueItem::from_item_type(kushi::QueueItemType::Single(QueueSong { song, location })))).await.unwrap();
let QueueResponse::Ok = ctrl_handle.queue_mail.recv().await.unwrap() else {
panic!()
};
app.emit("queue_updated", ()).unwrap();
Ok(())
}

View file

@ -1,21 +1,21 @@
use std::{fs, io::Read, path::PathBuf, str::FromStr, thread::spawn, time::Duration};
use std::{fs, path::PathBuf, str::FromStr, thread::spawn};
use commands::add_song_to_queue;
use crossbeam::channel::{unbounded, Receiver, Sender};
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle}, music_player::gstreamer::GStreamer, music_storage::library::{AlbumArt, MusicLibrary}};
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, Manager, State, Url, WebviewWindowBuilder, Wry};
use uuid::Uuid;
use wrappers::ArtworkRx;
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next};
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue};
pub mod wrappers;
pub mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let (rx, tx) = unbounded::<Config>();
let (lib_rx, lib_tx) = unbounded::<Option<PathBuf>>();
let (handle_rx, handle_tx) = unbounded::<ControllerHandle>();
let (art_rx, art_tx) = unbounded::<Vec<u8>>();
let controller_thread = spawn(move || {
let mut config = { tx.recv().unwrap() } ;
@ -32,10 +32,10 @@ pub fn run() {
).unwrap();
let scan_path = scan_path.unwrap_or_else(|| config.libraries.get_default().unwrap().scan_folders.as_ref().unwrap()[0].clone());
library.scan_folder(&scan_path).unwrap();
// library.scan_folder(&scan_path).unwrap();
if config.libraries.get_default().is_err() {
config.push_library( ConfigLibrary::new(save_path, String::from("Library"), Some(vec![scan_path.clone()])));
config.push_library( ConfigLibrary::new(save_path.clone(), String::from("Library"), Some(vec![scan_path.clone()])));
}
if library.library.is_empty() {
println!("library is empty");
@ -44,6 +44,8 @@ pub fn run() {
}
println!("scan_path: {}", scan_path.display());
library.save(save_path).unwrap();
let (handle, input) = ControllerHandle::new(
library,
std::sync::Arc::new(std::sync::RwLock::new(config))
@ -51,9 +53,8 @@ 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::<GStreamer>::start(input)).unwrap();
});
let app = tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
@ -68,20 +69,37 @@ pub fn run() {
prev,
get_song,
lib_already_created,
get_queue,
add_song_to_queue,
]).manage(ConfigRx(rx))
.manage(LibRx(lib_rx))
.manage(HandleTx(handle_tx))
.manage(ArtworkRx(art_rx))
.register_asynchronous_uri_scheme_protocol("asset", move |_, req, res| {
dbg!(req);
let buf = art_tx.recv().unwrap_or_else(|_| Vec::new());
.register_asynchronous_uri_scheme_protocol("asset", move |ctx, req, res| {
let query = req
.clone()
.uri()
.clone()
.into_parts()
.path_and_query
.unwrap()
.query()
.unwrap()
.to_string();
let bytes = 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!() };
song.album_art(0).unwrap_or_else(|_| None).unwrap_or_default()
});
res.respond(
Response::builder()
.header("Origin", "*")
.header("Content-Length", buf.len())
.header("Content-Length", bytes.len())
.status(200)
.body(buf)
.body(bytes)
.unwrap()
);
println!("res sent")
@ -96,7 +114,7 @@ pub fn run() {
}
_ => {}
});
// controller_thread.join().unwrap();
std::mem::drop(controller_thread)
}
struct ConfigRx(Sender<Config>);

View file

@ -1,8 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod wrappers;
fn main() {
dango_music_player_lib::run()

View file

@ -2,7 +2,9 @@ use std::collections::BTreeMap;
use chrono::{DateTime, Utc, serde::ts_milliseconds_option};
use crossbeam::channel::Sender;
use dmp_core::{music_controller::controller::{ControllerHandle, LibraryCommand, LibraryResponse, PlayerResponse}, music_storage::library::{BannedType, Song, URI}};
use dmp_core::{music_controller::controller::{ControllerHandle, LibraryCommand, LibraryResponse, PlayerResponse, QueueCommand, QueueResponse}, music_storage::library::{BannedType, Song, URI}};
use itertools::Itertools;
use kushi::QueueItemType;
use serde::Serialize;
use tauri::{ipc::Response, AppHandle, Emitter, State, Wry};
use uuid::Uuid;
@ -44,24 +46,26 @@ pub async fn get_volume(ctrl_handle: State<'_, ControllerHandle>) -> Result<(),
}
#[tauri::command]
pub async fn next(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>, art_rx: State<'_, ArtworkRx>) -> Result<(), String> {
pub async fn next(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> {
ctrl_handle.player_mail.send(dmp_core::music_controller::controller::PlayerCommand::NextSong).await.unwrap();
let PlayerResponse::NowPlaying(song) = ctrl_handle.player_mail.recv().await.unwrap() else {
unreachable!()
};
let _song = _Song::from(&song);
art_rx.0.send(song.album_art(0).unwrap()).unwrap();
println!("next");
app.emit("now_playing_change", _song).unwrap();
app.emit("now_playing_change", _Song::from(&song)).unwrap();
app.emit("queue_updated", ()).unwrap();
Ok(())
}
#[tauri::command]
pub async fn prev(ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> {
pub async fn prev(app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>) -> Result<(), String> {
ctrl_handle.player_mail.send(dmp_core::music_controller::controller::PlayerCommand::PrevSong).await.unwrap();
let PlayerResponse::Empty = ctrl_handle.player_mail.recv().await.unwrap() else {
let PlayerResponse::NowPlaying(song) = ctrl_handle.player_mail.recv().await.unwrap() else {
unreachable!()
};
println!("prev");
app.emit("now_playing_change", _Song::from(&song)).unwrap();
app.emit("queue_updated", ()).unwrap();
Ok(())
}
@ -71,6 +75,17 @@ pub async fn now_playing(ctrl_handle: State<'_, ControllerHandle>) -> Result<(),
Ok(())
}
#[tauri::command]
pub async fn get_queue(ctrl_handle: State<'_, ControllerHandle>) -> Result<Vec<_Song>, String> {
ctrl_handle.queue_mail.send(QueueCommand::Get).await.unwrap();
let QueueResponse::Get(queue) = ctrl_handle.queue_mail.recv().await.unwrap() else {
unreachable!()
};
Ok(queue.into_iter().map(|item| {
let QueueItemType::Single(song) = item.item else { unreachable!("There should be no albums in the queue right now") };
_Song::from(&song.song)
}).collect_vec())
}
//Grab Album art from custom protocol
#[derive(Serialize, Debug, Clone)]
@ -121,11 +136,4 @@ pub async fn get_song(ctrl_handle: State<'_, ControllerHandle>) -> Result<(), St
let LibraryResponse::Song(_) = ctrl_handle.lib_mail.recv().await.unwrap() else { unreachable!("It has been reached") };
println!("got songs");
Ok(())
}
#[derive(Serialize, Debug)]
pub struct NowPlaying {
title: String,
artist: String,
album: String,
}

View file

@ -11,7 +11,7 @@
}
.leftSide {
width: 85%;
width: 80%;
height: 100%;
display: flex;
flex-direction: column;
@ -19,7 +19,7 @@
.rightSide {
position: relative;
align-self:flex-end;
width: 15%;
width: 20%;
height: 100%;
background-color: #c1bcd1;
display: flex;
@ -71,6 +71,31 @@
bottom: -25%;
background-color: burlywood;
height: 50%;
display: flex;
flex-direction: column;
overflow-y: scroll;
}
.queueSongButton {
height: 15%;
padding: 0%;
margin: 0%;
}
.queueSong {
height: 15%;
width: 90%;
display: flex;
}
.queueSongCoverArt {
width: 25%;
height: 100%;
}
.queueSongTags {
display: flex;
flex-direction: column;
}
.song {

View file

@ -10,7 +10,8 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
const appWindow = getCurrentWebviewWindow();
function App() {
const library = useState<JSX.Element[]>();
const library = useState<JSX.Element[]>([]);
const [queue, setQueue] = useState<JSX.Element[]>([]);
const [nowPlaying, setNowPlaying] = useState<JSX.Element>(
<NowPlaying
@ -24,7 +25,6 @@ function App() {
useEffect(() => {
const unlisten = appWindow.listen<any>("now_playing_change", ({ event, payload }) => {
// console.log(event);
setNowPlaying(
<NowPlaying
title={ payload.tags.TrackTitle }
@ -35,7 +35,19 @@ function App() {
)
})
return () => { unlisten.then((f) => f()) }
}, []);
useEffect(() => {
const unlisten = appWindow.listen<any>("queue_updated", (_) => {
// console.log(event);
invoke('get_queue').then((_songs) => {
let songs = _songs as any[]
setQueue(
songs.filter((_, i) => i != 0).map((song) => <QueueSong song={ song } key={ song.uuid + '_' + Math.floor((Math.random() * 100_000) + 1) + '_' + Date.now() } />)
)
})
})
return () => { unlisten.then((f) => f()) }
}, []);
@ -52,20 +64,13 @@ function App() {
</div>
<div className="rightSide">
{ nowPlaying }
<Queue />
<Queue songs={queue} setSongs={ setQueue } />
</div>
</main>
);
}
interface L {
uuid: number,
}
function LI({uuid}: L) {
return ( <img src={convertFileSrc("abc") + "?" + uuid } id="nowPlayingArtwork" alt="Some Image" key={uuid} /> )
}
export default App;
function getConfig(): any {
@ -99,7 +104,7 @@ function PlaylistHead() {
}
interface MainViewProps {
lib_ref: [JSX.Element[] | undefined, React.Dispatch<React.SetStateAction<JSX.Element[] | undefined>>],
lib_ref: [JSX.Element[], React.Dispatch<React.SetStateAction<JSX.Element[]>>],
}
function MainView({ lib_ref }: MainViewProps) {
@ -145,7 +150,7 @@ interface SongProps {
}
function Song(props: SongProps) {
console.log(props.tags);
// console.log(props.tags);
return(
<div className="song">
@ -153,6 +158,10 @@ function Song(props: SongProps) {
<p className="album">{ props.tags.Album }</p>
<p className="artist">{ props.tags.AlbumArtist }</p>
<p className="duration">{ props.duration }</p>
<button onClick={(_) => {
invoke('add_song_to_queue', { uuid: props.uuid, location: 'Library' }).then(() => {} )
}}
>Add to Queue</button>
</div>
)
}
@ -208,17 +217,34 @@ function NowPlaying({ title, artist, album, artwork }: NowPlayingProps) {
)
}
function Queue() {
interface QueueProps {
songs: JSX.Element[],
setSongs: React.Dispatch<React.SetStateAction<JSX.Element[]>>
}
function Queue({ songs, setSongs }: QueueProps) {
return (
<section className="Queue">
This is where the Queue be
{ songs }
</section>
)
}
interface CurrentArtProps {
uuid: number,
interface QueueSongProps {
song: any
}
function CurrentArt({uuid}: CurrentArtProps) {
return <img src={convertFileSrc("abc") + "?" + uuid } id="nowPlayingArtwork" alt="Now Playing Artwork" key={uuid} />
function QueueSong({ song }: QueueSongProps) {
console.log(song.tags);
return (
// <button className="queueSongButton">
<div className="queueSong">
<img className="queueSongCoverArt" src={ convertFileSrc('abc') + '?' + song.uuid } key={ 'coverArt_' + song.uuid }/>
<div className="queueSongTags">
<h3 className="queueSongTitle">{ song.tags.TrackTitle }</h3>
<h4 className="queueSongArtist">{ song.tags.TrackArtist }</h4>
</div>
</div>
// </button>
)
}