diff --git a/musicdb-filldb/Cargo.toml b/musicdb-filldb/Cargo.toml new file mode 100755 index 0000000..46f201a --- /dev/null +++ b/musicdb-filldb/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "musicdb-filldb" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +id3 = "1.7.0" +musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } diff --git a/musicdb-filldb/dbfile b/musicdb-filldb/dbfile new file mode 100755 index 0000000..b1665b6 Binary files /dev/null and b/musicdb-filldb/dbfile differ diff --git a/musicdb-filldb/src/main.rs b/musicdb-filldb/src/main.rs new file mode 100755 index 0000000..3735595 --- /dev/null +++ b/musicdb-filldb/src/main.rs @@ -0,0 +1,202 @@ +use std::{ + collections::{HashMap, HashSet}, + fs::{self, FileType}, + io::Write, + ops::IndexMut, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use id3::TagLike; +use musicdb_lib::data::{ + album::{self, Album}, + artist::Artist, + database::{Cover, Database}, + song::Song, + DatabaseLocation, GeneralData, +}; + +fn main() { + // arg parsing + let lib_dir = if let Some(arg) = std::env::args().nth(1) { + arg + } else { + eprintln!("usage: musicdb-filldb "); + std::process::exit(1); + }; + eprintln!("Library: {lib_dir}. press enter to start. result will be saved in 'dbfile'."); + std::io::stdin().read_line(&mut String::new()).unwrap(); + // start + eprintln!("finding files..."); + let files = get_all_files_in_dir(&lib_dir); + let files_count = files.len(); + eprintln!("found {files_count} files, reading metadata..."); + let mut songs = Vec::new(); + for (i, file) in files.into_iter().enumerate() { + let mut newline = OnceNewline::new(); + eprint!("\r{}/{}", i + 1, files_count); + _ = std::io::stderr().flush(); + if let Some("mp3") = file.extension().and_then(|ext_os| ext_os.to_str()) { + match id3::Tag::read_from_path(&file) { + Err(e) => { + newline.now(); + eprintln!("[{file:?}] error reading id3 tag: {e}"); + } + Ok(tag) => songs.push((file, tag)), + } + } + } + eprintln!("\nloaded metadata of {} files.", songs.len()); + let mut database = Database::new_empty(PathBuf::from("dbfile"), PathBuf::from(&lib_dir)); + eprintln!("searching for artists..."); + let mut artists = HashMap::new(); + for song in songs { + let (artist_id, album_id) = + if let Some(artist) = song.1.album_artist().or_else(|| song.1.artist()) { + let artist_id = if !artists.contains_key(artist) { + let artist_id = database.add_artist_new(Artist { + id: 0, + name: artist.to_string(), + cover: None, + albums: vec![], + singles: vec![], + general: GeneralData::default(), + }); + artists.insert(artist.to_string(), (artist_id, HashMap::new())); + eprintln!("Artist #{artist_id}: {artist}"); + artist_id + } else { + artists.get(artist).unwrap().0 + }; + if let Some(album) = song.1.album() { + let (_, albums) = artists.get_mut(artist).unwrap(); + let album_id = if !albums.contains_key(album) { + let album_id = database.add_album_new(Album { + id: 0, + artist: Some(artist_id), + name: album.to_string(), + cover: None, + songs: vec![], + general: GeneralData::default(), + }); + albums.insert( + album.to_string(), + (album_id, song.0.parent().map(|dir| dir.to_path_buf())), + ); + eprintln!("Album #{album_id}: {album}"); + album_id + } else { + let album = albums.get_mut(album).unwrap(); + if album + .1 + .as_ref() + .is_some_and(|dir| Some(dir.as_path()) != song.0.parent()) + { + // album directory is inconsistent + album.1 = None; + } + album.0 + }; + (Some(artist_id), Some(album_id)) + } else { + (Some(artist_id), None) + } + } else { + (None, None) + }; + let path = song.0.strip_prefix(&lib_dir).unwrap(); + let title = song + .1 + .title() + .map(|title| title.to_string()) + .unwrap_or_else(|| song.0.file_stem().unwrap().to_string_lossy().into_owned()); + let song_id = database.add_song_new(Song { + id: 0, + title: title.clone(), + location: DatabaseLocation { + rel_path: path.to_path_buf(), + }, + album: album_id, + artist: artist_id, + more_artists: vec![], + cover: None, + general: GeneralData::default(), + cached_data: Arc::new(Mutex::new(None)), + }); + eprintln!("Song #{song_id}: \"{title}\" @ {path:?}"); + } + eprintln!("searching for covers..."); + for (artist, (_artist_id, albums)) in &artists { + for (album, (album_id, album_dir)) in albums { + if let Some(album_dir) = album_dir { + let mut cover = None; + if let Ok(files) = fs::read_dir(album_dir) { + for file in files { + if let Ok(file) = file { + if let Ok(metadata) = file.metadata() { + if metadata.is_file() { + let path = file.path(); + if matches!( + path.extension().and_then(|v| v.to_str()), + Some("png" | "jpg" | "jpeg") + ) { + if cover.is_none() + || cover + .as_ref() + .is_some_and(|(_, size)| *size < metadata.len()) + { + cover = Some((path, metadata.len())); + } + } + } + } + } + } + } + if let Some((path, _)) = cover { + let rel_path = path.strip_prefix(&lib_dir).unwrap().to_path_buf(); + let cover_id = database.add_cover_new(Cover { + location: DatabaseLocation { + rel_path: rel_path.clone(), + }, + data: Arc::new(Mutex::new((false, None))), + }); + eprintln!("Cover #{cover_id}: {artist} - {album} -> {rel_path:?}"); + database.albums_mut().get_mut(album_id).unwrap().cover = Some(cover_id); + } + } + } + } + eprintln!("saving dbfile..."); + database.save_database(None).unwrap(); + eprintln!("done!"); +} + +fn get_all_files_in_dir(dir: impl AsRef) -> Vec { + let mut files = Vec::new(); + _ = all_files_in_dir(&dir, &mut files); + files +} +fn all_files_in_dir(dir: impl AsRef, vec: &mut Vec) -> Result<(), std::io::Error> { + for path in fs::read_dir(dir)? + .filter_map(|possible_entry| possible_entry.ok()) + .map(|entry| entry.path()) + { + if all_files_in_dir(&path, vec).is_err() { + vec.push(path); + } + } + Ok(()) +} + +struct OnceNewline(bool); +impl OnceNewline { + pub fn new() -> Self { + Self(true) + } + pub fn now(&mut self) { + if std::mem::replace(&mut self.0, false) { + eprintln!(); + } + } +} diff --git a/musicdb-lib/src/server/get.rs b/musicdb-lib/src/server/get.rs new file mode 100755 index 0000000..5eacc46 --- /dev/null +++ b/musicdb-lib/src/server/get.rs @@ -0,0 +1,113 @@ +use std::{ + io::BufRead, + io::{BufReader, Read, Write}, + sync::{Arc, Mutex}, +}; + +use crate::data::{database::Database, CoverId}; + +pub struct Client(BufReader); +impl Client { + pub fn new(mut con: BufReader) -> std::io::Result { + writeln!(con.get_mut(), "get")?; + Ok(Self(con)) + } + pub fn cover_bytes(&mut self, id: CoverId) -> Result, String>, std::io::Error> { + writeln!( + self.0.get_mut(), + "{}", + con_get_encode_string(&format!("cover-bytes\n{id}")) + )?; + let mut response = String::new(); + self.0.read_line(&mut response)?; + let response = con_get_decode_line(&response); + if response.starts_with("len: ") { + if let Ok(len) = response[4..].trim().parse() { + let mut bytes = vec![0; len]; + self.0.read_exact(&mut bytes)?; + Ok(Ok(bytes)) + } else { + Ok(Err(response)) + } + } else { + Ok(Err(response)) + } + } +} + +pub fn handle_one_connection_as_get( + db: Arc>, + connection: &mut BufReader, +) -> Result<(), std::io::Error> { + let mut line = String::new(); + loop { + line.clear(); + if connection.read_line(&mut line).is_ok() { + if line.is_empty() { + return Ok(()); + } + let request = con_get_decode_line(&line); + let mut request = request.lines(); + if let Some(req) = request.next() { + match req { + "cover-bytes" => { + if let Some(cover) = request + .next() + .and_then(|id| id.parse().ok()) + .and_then(|id| db.lock().unwrap().covers().get(&id).cloned()) + { + if let Some(v) = cover.get_bytes( + |p| db.lock().unwrap().get_path(p), + |bytes| { + writeln!(connection.get_mut(), "len: {}", bytes.len())?; + connection.get_mut().write_all(bytes)?; + Ok::<(), std::io::Error>(()) + }, + ) { + v?; + } else { + writeln!(connection.get_mut(), "no data")?; + } + } else { + writeln!(connection.get_mut(), "no cover")?; + } + } + _ => {} + } + } + } else { + return Ok(()); + } + } +} + +pub fn con_get_decode_line(line: &str) -> String { + let mut o = String::new(); + let mut chars = line.chars(); + loop { + match chars.next() { + Some('\\') => match chars.next() { + Some('n') => o.push('\n'), + Some('r') => o.push('\r'), + Some('\\') => o.push('\\'), + Some(ch) => o.push(ch), + None => break, + }, + Some(ch) => o.push(ch), + None => break, + } + } + o +} +pub fn con_get_encode_string(line: &str) -> String { + let mut o = String::new(); + for ch in line.chars() { + match ch { + '\\' => o.push_str("\\\\"), + '\n' => o.push_str("\\n"), + '\r' => o.push_str("\\r"), + _ => o.push(ch), + } + } + o +} diff --git a/musicdb-server/assets/queue_loop.html b/musicdb-server/assets/queue_loop.html new file mode 100755 index 0000000..d64827f --- /dev/null +++ b/musicdb-server/assets/queue_loop.html @@ -0,0 +1,10 @@ +
+ >> + + repeat \:total times +
+\:inner +
+ << + +
diff --git a/musicdb-server/assets/queue_loop_current.html b/musicdb-server/assets/queue_loop_current.html new file mode 100755 index 0000000..f4a4657 --- /dev/null +++ b/musicdb-server/assets/queue_loop_current.html @@ -0,0 +1,10 @@ +
+ >> + + repeat \:total times +
+\:inner +
+ << + +
diff --git a/musicdb-server/assets/queue_loopinf.html b/musicdb-server/assets/queue_loopinf.html new file mode 100755 index 0000000..6bc1ecf --- /dev/null +++ b/musicdb-server/assets/queue_loopinf.html @@ -0,0 +1,10 @@ +
+ >> + + repeat forever +
+\:inner +
+ << + +
diff --git a/musicdb-server/assets/queue_loopinf_current.html b/musicdb-server/assets/queue_loopinf_current.html new file mode 100755 index 0000000..ee389e7 --- /dev/null +++ b/musicdb-server/assets/queue_loopinf_current.html @@ -0,0 +1,10 @@ +
+ >> + + repeat forever +
+\:inner +
+ << + +
diff --git a/musicdb-server/assets/queue_random.html b/musicdb-server/assets/queue_random.html new file mode 100755 index 0000000..85066ce --- /dev/null +++ b/musicdb-server/assets/queue_random.html @@ -0,0 +1,10 @@ +
+ >> + + random +
+\:content +
+ << + +
diff --git a/musicdb-server/assets/queue_random_current.html b/musicdb-server/assets/queue_random_current.html new file mode 100755 index 0000000..a108899 --- /dev/null +++ b/musicdb-server/assets/queue_random_current.html @@ -0,0 +1,10 @@ +
+ >> + + random +
+\:content +
+ << + +
diff --git a/musicdb-server/assets/queue_shuffle.html b/musicdb-server/assets/queue_shuffle.html new file mode 100755 index 0000000..a59f5cb --- /dev/null +++ b/musicdb-server/assets/queue_shuffle.html @@ -0,0 +1,10 @@ +
+ >> + + shuffle +
+\:content +
+ << + +
diff --git a/musicdb-server/assets/queue_shuffle_current.html b/musicdb-server/assets/queue_shuffle_current.html new file mode 100755 index 0000000..0ae331c --- /dev/null +++ b/musicdb-server/assets/queue_shuffle_current.html @@ -0,0 +1,10 @@ +
+ >> + + shuffle +
+\:content +
+ << + +