use std::{
fs,
io::{self, Write},
path,
result::Result as StdResult,
thread,
};
use crate::{app::App, config::Config, error::Result, events::Event, m3u};
pub fn add_song(app: &mut App, playlist: &str, song_path: String) {
app.notify_info(format!("Adding {}...", song_path));
if surely_invalid_path(&song_path) {
app.notify_err(format!("Failed to add song path '{}'. Doesn't look like a URL and is not a valid path in your filesystem.", song_path));
return;
}
let sender = app.channel.sender.clone();
let playlist = playlist.to_string();
thread::spawn(move || {
add_song_recursively(&song_path, &playlist);
let mut rsplit = song_path.trim_end_matches('/').rsplit('/');
let song = rsplit.next().unwrap_or(&song_path).to_string();
let event = Event::SongAdded { playlist, song };
sender.send(event).expect("Failed to send internal event");
});
}
fn add_song_recursively(path: &str, playlist_name: &str) {
let file = std::path::Path::new(&path);
if file.is_dir() && !file.is_symlink() {
let mut entries: Vec<_> = fs::read_dir(path)
.unwrap_or_else(|e| panic!("Failed to read directory '{}'. Error: {}", path, e))
.map(|entry| entry.expect("Failed to read entry").path())
.collect();
entries.sort();
for path in entries {
let path = path.to_str().unwrap_or_else(|| {
panic!(
"Failed to add '{}' to playlist. Path is not valid UTF-8",
path.display()
)
});
add_song_recursively(path, playlist_name);
}
} else if !image_file(file) {
let song = m3u::Song::from_path(path)
.unwrap_or_else(|e| panic!("Failed to add '{}' to playlist. Error: {}", path, e));
song.add_to_playlist(playlist_name)
.unwrap_or_else(|e| panic!("Failed to add '{}' to playlist. Error: {}", path, e));
}
}
fn surely_invalid_path(path: &str) -> bool {
let file = std::path::Path::new(&path);
!file.is_dir() && !file.exists() && !path.starts_with("http://") && !path.starts_with("https://")
&& !path.starts_with("ytdl://")
}
fn image_file(file: &std::path::Path) -> bool {
matches!(
file.extension().and_then(|s| s.to_str()),
Some("png") | Some("jpg") | Some("jpeg") | Some("webp") | Some("svg")
)
}
#[derive(Debug)]
pub enum CreatePlaylistError {
PlaylistAlreadyExists,
InvalidChar(char),
IOError(io::Error),
}
#[derive(Debug)]
pub enum RenamePlaylistError {
PlaylistAlreadyExists,
EmptyPlaylistName,
InvalidChar(char),
IOError(io::Error),
}
impl From<io::Error> for CreatePlaylistError {
fn from(value: io::Error) -> Self {
Self::IOError(value)
}
}
pub fn create_playlist(playlist_name: &str) -> StdResult<(), CreatePlaylistError> {
if playlist_name.contains('/') {
return Err(CreatePlaylistError::InvalidChar('/'));
}
if playlist_name.contains('\\') {
return Err(CreatePlaylistError::InvalidChar('\\'));
}
let path = Config::playlist_path(playlist_name);
if path.try_exists()? {
Err(CreatePlaylistError::PlaylistAlreadyExists)
} else {
fs::File::create(path)?;
Ok(())
}
}
pub fn delete_song(playlist_name: &str, index: usize) -> Result<()> {
let path = Config::playlist_path(playlist_name);
let content = fs::read_to_string(&path)?;
let mut parser = m3u::Parser::from_string(&content);
parser.next_header()?;
for _ in 0..index {
parser.next_song()?;
}
let start_pos = parser.cursor();
let _song = parser.next_song()?;
let end_pos = parser.cursor();
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&path)?;
file.write_all(content[..start_pos].as_bytes())?;
file.write_all(content[end_pos..].as_bytes())?;
Ok(())
}
pub fn rename_song(playlist_name: &str, index: usize, new_name: &str) -> Result<()> {
let path = Config::playlist_path(playlist_name);
let content = fs::read_to_string(&path)?;
let mut parser = m3u::Parser::from_string(&content);
parser.next_header()?;
for _ in 0..index {
parser.next_song()?;
}
let start_pos = parser.cursor();
let song = parser.next_song()?;
let end_pos = parser.cursor();
if let Some(mut song) = song {
song.title = new_name.to_string();
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&path)?;
file.write_all(content[..start_pos].as_bytes())?;
file.write_all(song.serialize().as_bytes())?;
file.write_all(content[end_pos..].as_bytes())?;
}
Ok(())
}
pub fn swap_song(playlist_name: &str, index: usize) -> Result<()> {
let path = Config::playlist_path(playlist_name);
let content = fs::read_to_string(&path)?;
let mut parser = m3u::Parser::from_string(&content);
parser.next_header()?;
for _ in 0..index {
parser.next_song()?;
}
let start_pos = parser.cursor();
let song1 = parser.next_song()?;
let song2 = parser.next_song()?;
let end_pos = parser.cursor();
if let (Some(song1), Some(song2)) = (song1, song2) {
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&path)?;
file.write_all(content[..start_pos].as_bytes())?;
file.write_all(song2.serialize().as_bytes())?;
file.write_all(song1.serialize().as_bytes())?;
file.write_all(content[end_pos..].as_bytes())?;
}
Ok(())
}
pub fn rename_playlist(playlist_name: &str, new_name: &str) -> StdResult<(), RenamePlaylistError> {
if new_name.is_empty() {
return Err(RenamePlaylistError::EmptyPlaylistName);
}
if new_name.contains('/') {
return Err(RenamePlaylistError::InvalidChar('/'));
}
if new_name.contains('\\') {
return Err(RenamePlaylistError::InvalidChar('\\'));
}
let old_path: path::PathBuf = Config::playlist_path(playlist_name);
let new_path: path::PathBuf = Config::playlist_path(new_name);
if let Ok(metadata) = fs::metadata(new_path.clone()) {
if metadata.is_file() || metadata.is_dir() {
return Err(RenamePlaylistError::PlaylistAlreadyExists);
}
}
match fs::rename(&old_path, &new_path) {
Err(e) => Err(RenamePlaylistError::IOError(e)),
Ok(_) => Ok(()),
}
}
pub fn delete_playlist(playlist_name: &str) -> Result<()> {
let path = Config::playlist_path(playlist_name);
fs::remove_file(path)?;
Ok(())
}