Added function to remove a song from the library/ playlist

This commit is contained in:
MrDulfin 2025-06-04 20:07:18 -04:00
parent 5666581c7f
commit 624e6f2db6
9 changed files with 185 additions and 88 deletions

View file

@ -108,6 +108,7 @@ pub enum LibraryCommand {
Song(Uuid), Song(Uuid),
AllSongs, AllSongs,
GetLibrary, GetLibrary,
LibraryRemoveSong(Uuid),
ExternalPlaylist(Uuid), ExternalPlaylist(Uuid),
PlaylistSong { list_uuid: Uuid, item_uuid: Uuid }, PlaylistSong { list_uuid: Uuid, item_uuid: Uuid },
Playlist(Uuid), Playlist(Uuid),
@ -115,6 +116,7 @@ pub enum LibraryCommand {
Save, Save,
Playlists, Playlists,
PlaylistAddSong { playlist: Uuid, song: Uuid }, PlaylistAddSong { playlist: Uuid, song: Uuid },
PlaylistRemoveSong { playlist: Uuid, song: Uuid },
DeletePlaylist(Uuid), DeletePlaylist(Uuid),
} }

View file

@ -46,6 +46,14 @@ impl ControllerHandle {
}; };
} }
pub async fn lib_remove_song(&self, song: Uuid) {
let (command, tx) = LibraryCommandInput::command(LibraryCommand::LibraryRemoveSong(song));
self.lib_mail_rx.send(command).await.unwrap();
let LibraryResponse::Ok = tx.recv().await.unwrap() else {
unreachable!()
};
}
// The Playlist Section // The Playlist Section
pub async fn playlist_get(&self, uuid: Uuid) -> Result<ExternalPlaylist, ()> { pub async fn playlist_get(&self, uuid: Uuid) -> Result<ExternalPlaylist, ()> {
let (command, tx) = LibraryCommandInput::command(LibraryCommand::ExternalPlaylist(uuid)); let (command, tx) = LibraryCommandInput::command(LibraryCommand::ExternalPlaylist(uuid));
@ -92,6 +100,15 @@ impl ControllerHandle {
}; };
} }
pub async fn playlist_remove_song(&self, song: Uuid, playlist: Uuid) {
let (command, tx) =
LibraryCommandInput::command(LibraryCommand::PlaylistRemoveSong { song, playlist });
self.lib_mail_rx.send(command).await.unwrap();
let LibraryResponse::Ok = tx.recv().await.unwrap() else {
unreachable!()
};
}
// The Queue Section // The Queue Section
pub async fn queue_append( pub async fn queue_append(
&self, &self,

View file

@ -108,7 +108,18 @@ impl Controller {
LibraryCommand::PlaylistAddSong { playlist, song } => { LibraryCommand::PlaylistAddSong { playlist, song } => {
let playlist = library.query_playlist_uuid_mut(&playlist).unwrap(); let playlist = library.query_playlist_uuid_mut(&playlist).unwrap();
playlist.add_track(song); playlist.add_track(song);
library.save(config.read().path.clone()).unwrap(); let lib_uuid = library.uuid;
library
.save(
config
.read()
.libraries
.get_library(&lib_uuid)
.unwrap()
.path
.clone(),
)
.unwrap();
res_rx.send(LibraryResponse::Ok).await.unwrap(); res_rx.send(LibraryResponse::Ok).await.unwrap();
} }
LibraryCommand::DeletePlaylist(uuid) => { LibraryCommand::DeletePlaylist(uuid) => {
@ -127,6 +138,39 @@ impl Controller {
.unwrap(); .unwrap();
res_rx.send(LibraryResponse::Ok).await.unwrap(); res_rx.send(LibraryResponse::Ok).await.unwrap();
} }
LibraryCommand::LibraryRemoveSong(uuid) => {
library.remove_uuid(&uuid).unwrap();
let lib_uuid = library.uuid;
library
.save_path(
&config
.read()
.libraries
.get_library(&lib_uuid)
.unwrap()
.path
.clone(),
)
.unwrap();
res_rx.send(LibraryResponse::Ok).await.unwrap();
}
LibraryCommand::PlaylistRemoveSong { playlist, song } => {
library.playlists.delete_song(song, &playlist).unwrap();
let lib_uuid = library.uuid;
library
.save(
config
.read()
.libraries
.get_library(&lib_uuid)
.unwrap()
.path
.clone(),
)
.unwrap();
res_rx.send(LibraryResponse::Ok).await.unwrap();
}
_ => { _ => {
todo!() todo!()
} }

View file

@ -18,7 +18,6 @@ use file_format::{FileFormat, Kind};
use lofty::file::{AudioFile as _, TaggedFileExt as _}; use lofty::file::{AudioFile as _, TaggedFileExt as _};
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::{ItemKey, ItemValue, TagType}; use lofty::tag::{ItemKey, ItemValue, TagType};
use rayon::iter::plumbing::Folder;
use rcue::parser::parse_from_file; use rcue::parser::parse_from_file;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -985,6 +984,17 @@ impl MusicLibrary {
Ok(location) Ok(location)
} }
pub fn remove_uuid(&mut self, target_uuid: &Uuid) -> Result<usize, Box<dyn Error>> {
let location = match self.query_uuid(target_uuid) {
Some(value) => value.1,
None => return Err("Uuid not in database".into()),
};
self.library.remove(location);
Ok(location)
}
/// Scan the song by a location and update its tags /// Scan the song by a location and update its tags
// TODO: change this to work with multiple uris // TODO: change this to work with multiple uris
pub fn update_uri( pub fn update_uri(

View file

@ -14,6 +14,7 @@ use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI};
use chrono::format::Item; use chrono::format::Item;
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2};
@ -99,6 +100,20 @@ impl PlaylistFolder {
None None
} }
} }
pub fn delete_song(&mut self, song: Uuid, playlist: &Uuid) -> Result<(), PlaylistError> {
match self.query_uuid_mut(playlist) {
Some(list) => {
if let Some((_, index)) = list.query_uuid(&song) {
list.remove_track(index);
Ok(())
} else {
Err(PlaylistError::NoUuid(song, *playlist))
}
}
None => Err(PlaylistError::NoPlaylist(*playlist)),
}
}
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -146,9 +161,9 @@ impl Playlist {
self.tracks.push(track); self.tracks.push(track);
} }
pub fn remove_track(&mut self, index: i32) { pub fn remove_track(&mut self, index: usize) {
let index = index as usize; let len = self.tracks.len();
if (self.tracks.len() - 1) >= index { if len > 0 && (len - 1) >= index {
self.tracks.remove(index); self.tracks.remove(index);
} }
} }
@ -453,6 +468,14 @@ impl ExternalPlaylist {
} }
} }
#[derive(Debug, Error)]
pub enum PlaylistError {
#[error("No uuid {0} found in playlist {1}")]
NoUuid(Uuid, Uuid),
#[error("No Playlist found with uuid {0}")]
NoPlaylist(Uuid),
}
#[cfg(test)] #[cfg(test)]
mod test_super { mod test_super {
use super::*; use super::*;

View file

@ -11,7 +11,7 @@ use tauri::{AppHandle, Emitter, State, Wry};
use tempfile::TempDir; use tempfile::TempDir;
use uuid::Uuid; use uuid::Uuid;
use crate::{wrappers::_Song, LAST_FM_API_KEY, LAST_FM_API_SECRET}; use crate::{LAST_FM_API_KEY, LAST_FM_API_SECRET, wrappers::_Song};
#[tauri::command] #[tauri::command]
pub async fn add_song_to_queue( pub async fn add_song_to_queue(
@ -93,19 +93,23 @@ pub async fn last_fm_init_auth(ctrl_handle: State<'_, ControllerHandle>) -> Resu
Ok(()) Ok(())
} }
// #[tauri::command] #[tauri::command]
// pub async fn test_menu( pub async fn remove_from_lib_playlist(
// ctrl_handle: State<'_, ControllerHandle>, app: AppHandle<Wry>,
// app: AppHandle<Wry>, ctrl_handle: State<'_, ControllerHandle>,
// window: Window, song: Uuid,
// uuid: Uuid, location: PlayerLocation,
// ) -> Result<(), String> { ) -> Result<(), String> {
// let handle = app.app_handle(); match location {
// let menu = MenuBuilder::new(handle) PlayerLocation::Library => {
// .item(&MenuItem::new(handle, "Add to Queue", true, None::<&str>).unwrap()) ctrl_handle.lib_remove_song(song).await;
// .build() app.emit("library_loaded", ()).unwrap();
// .unwrap(); }
// window.set_menu(menu).unwrap(); PlayerLocation::Playlist(uuid) => {
// println!("Menu popup!"); ctrl_handle.playlist_remove_song(song, uuid).await;
// Ok(()) }
// } _ => unimplemented!(),
}
Ok(())
}

View file

@ -28,7 +28,9 @@ use crate::wrappers::{
get_queue, get_song, import_playlist, next, pause, play, play_next_queue, prev, get_queue, get_song, import_playlist, next, pause, play, play_next_queue, prev,
remove_from_queue, seek, set_volume, remove_from_queue, seek, set_volume,
}; };
use commands::{add_song_to_queue, display_album_art, last_fm_init_auth, play_now}; use commands::{
add_song_to_queue, display_album_art, last_fm_init_auth, play_now, remove_from_lib_playlist,
};
pub mod commands; pub mod commands;
pub mod config; pub mod config;
@ -74,6 +76,7 @@ pub fn run() {
delete_playlist, delete_playlist,
play_next_queue, play_next_queue,
clear_queue, clear_queue,
remove_from_lib_playlist,
// test_menu, // test_menu,
]) ])
.manage(tempfile::TempDir::new().unwrap()) .manage(tempfile::TempDir::new().unwrap())

View file

@ -316,6 +316,6 @@ pub async fn clear_queue(
ctrl_handle: State<'_, ControllerHandle>, ctrl_handle: State<'_, ControllerHandle>,
) -> Result<(), String> { ) -> Result<(), String> {
let res = ctrl_handle.queue_clear().await.map_err(|e| e.to_string()); let res = ctrl_handle.queue_clear().await.map_err(|e| e.to_string());
app.emit("queue_updated", ()); _ = app.emit("queue_updated", ());
res res
} }

View file

@ -116,17 +116,39 @@ interface PlaylistHeadProps {
} }
function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playlistsInfo }: PlaylistHeadProps) { function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playlistsInfo }: PlaylistHeadProps) {
function getPlaylist(playlist: PlaylistInfo) {
invoke('get_playlist', { uuid: playlist.uuid }).then((list) => {
setLibrary([...(list as any[]).map((song) => {
// console.log(song);
const reload = () => getPlaylist(playlist)
return (
<Song
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location }
playerLocation={ {"Playlist" : playlist.uuid } }
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
tags={ song.tags }
playlists={ playlistsInfo }
reload = { reload }
/>
)
})])
})
setViewName( playlist.name )
}
useEffect(() => { useEffect(() => {
const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => { const unlisten = appWindow.listen<any[]>("playlists_gotten", (_res) => {
// console.log(event); // console.log(event);
let res = _res.payload as PlaylistInfo[]; let res = _res.payload as PlaylistInfo[];
playlistsInfo.current = [...res]; playlistsInfo.current = [...res];
console.log(playlistsInfo, res); // console.log(playlistsInfo, res);
setPlaylists([ setPlaylists([
...res.map( (list) => { ...res.map( (list) => {
const _getPlaylist = () => getPlaylist(list)
const deletePlaylist = () => { const deletePlaylist = () => {
invoke('delete_playlist', { uuid: list.uuid }).then(() => {}); invoke('delete_playlist', { uuid: list.uuid }).then(() => {});
invoke('get_playlists').then(() => {}); invoke('get_playlists').then(() => {});
@ -142,27 +164,7 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
} }
return ( return (
<button onClick={ () => { <button onClick={ _getPlaylist }
invoke('get_playlist', { uuid: list.uuid }).then((list_songs) => {
setLibrary([...(list_songs as any[]).map((song) => {
// console.log(song);
return (
<Song
key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location }
playerLocation={ {"Playlist" : list.uuid } }
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
tags={ song.tags }
playlists={ playlistsInfo }
/>
)
})])
})
setViewName( list.name )
} }
onContextMenu={ menuHandler } onContextMenu={ menuHandler }
key={ 'playlist_' + list.uuid }>{ list.name }</button> key={ 'playlist_' + list.uuid }>{ list.name }</button>
) )
@ -177,31 +179,9 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
setPlaylists([ setPlaylists([
...playlists, ...playlists,
<button onClick={ () => { <button onClick={ () => getPlaylist(res) } key={ 'playlist_' + res.uuid }>{ res.name }</button>
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 }
playerLocation={ {"Playlist" : res.uuid } }
uuid={ song.uuid }
plays={ song.plays }
duration={ song.duration }
tags={ song.tags }
playlists={ playlistsInfo }
/>
)
})])
})
setViewName( res.name )
} } key={ 'playlist_' + res.uuid }>{ res.name }</button>
]) ])
console.log(res.name); // console.log(res.name);
}) })
} }
return ( return (
@ -210,11 +190,11 @@ function PlaylistHead({ playlists, setPlaylists, setViewName, setLibrary, playli
setViewName("Library"); setViewName("Library");
invoke('get_library').then((lib) => { invoke('get_library').then((lib) => {
setLibrary([...(lib as any[]).map((song) => { setLibrary([...(lib as any[]).map((song) => {
console.log(song); // console.log(song);
return ( return (
<Song <Song
key={ song.uuid } key={ song.uuid + Math.floor(Math.random() * 100_000_000_000) }
location={ song.location } location={ song.location }
playerLocation="Library" playerLocation="Library"
uuid={ song.uuid } uuid={ song.uuid }
@ -290,22 +270,37 @@ interface SongProps {
date_added?: string, date_added?: string,
date_modified?: string, date_modified?: string,
tags: any, tags: any,
playlists: MutableRefObject<PlaylistInfo[]> playlists: MutableRefObject<PlaylistInfo[]>,
reload?: () => void
} }
function Song(props: SongProps) { function Song(props: SongProps) {
// console.log(props.tags); // console.log(props.tags);
const add_to_queue_test = (_: string) => { const addToQueue = (_: string) => {
invoke('add_song_to_queue', { uuid: props.uuid, location: props.playerLocation }).then(() => {}); invoke('add_song_to_queue', { uuid: props.uuid, location: props.playerLocation }).then(() => {});
} }
const playNow = () => invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {})
const playNext = () => invoke("play_next_queue", { uuid: props.uuid, location: props.playerLocation }).then(() => {})
const removeLibPlaylist = () => {
invoke("remove_from_lib_playlist", { song: props.uuid, location: props.playerLocation }).then(() => {
if (props.reload !== undefined) {
props.reload()
}
})
}
async function clickHandler(event: React.MouseEvent) { async function clickHandler(event: React.MouseEvent) {
event.preventDefault(); event.preventDefault();
console.log(props.playlists);
const _ = await invoke('get_playlists'); const _ = await invoke('get_playlists');
let removeText = "Remove from Library";
if (props.playerLocation != "Library") {
removeText = "Remove from Playlist";
}
const menu = await Menu.new({ const menu = await Menu.new({
items: [ items: [
{ id: "add_song_to_queue" + props.uuid, text: "Add to Queue", action: add_to_queue_test }, { id: "play_now_" + props.uuid, text: "Play Now", action: playNow },
{ id: "play_next_" + props.uuid, text: "Play Next", action: playNext },
{ id: "add_song_to_queue" + props.uuid, text: "Add to Queue", action: addToQueue },
await Submenu.new( await Submenu.new(
{ {
text: "Add to Playlist...", text: "Add to Playlist...",
@ -316,7 +311,8 @@ function Song(props: SongProps) {
return { id: "add_song_to_playlists" + props.uuid + list.uuid, text: list.name, action: addToPlaylist } return { id: "add_song_to_playlists" + props.uuid + list.uuid, text: list.name, action: addToPlaylist }
})] })]
} as SubmenuOptions } as SubmenuOptions
) ),
{ id: "remove_from_lib_playlist" + props.location + props.uuid, text: removeText, action: removeLibPlaylist },
] ]
}) })
; ;
@ -339,10 +335,8 @@ function Song(props: SongProps) {
return( return(
<div <div
onDoubleClick={() => { onDoubleClick = { playNow }
invoke("play_now", { uuid: props.uuid, location: props.playerLocation }).then(() => {}) onContextMenu={ clickHandler }
}}
onContextMenu={clickHandler}
className="song"> className="song">
<p className="artist unselectable">{ props.tags.TrackArtist }</p> <p className="artist unselectable">{ props.tags.TrackArtist }</p>
<p className="title unselectable">{ props.tags.TrackTitle }</p> <p className="title unselectable">{ props.tags.TrackTitle }</p>