mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +01:00
.
This commit is contained in:
parent
f414b849b7
commit
c67dc9c7b1
10
musicdb-filldb/Cargo.toml
Executable file
10
musicdb-filldb/Cargo.toml
Executable file
@ -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" }
|
BIN
musicdb-filldb/dbfile
Executable file
BIN
musicdb-filldb/dbfile
Executable file
Binary file not shown.
202
musicdb-filldb/src/main.rs
Executable file
202
musicdb-filldb/src/main.rs
Executable file
@ -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 <library root>");
|
||||||
|
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<Path>) -> Vec<PathBuf> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
_ = all_files_in_dir(&dir, &mut files);
|
||||||
|
files
|
||||||
|
}
|
||||||
|
fn all_files_in_dir(dir: impl AsRef<Path>, vec: &mut Vec<PathBuf>) -> 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!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
musicdb-lib/src/server/get.rs
Executable file
113
musicdb-lib/src/server/get.rs
Executable file
@ -0,0 +1,113 @@
|
|||||||
|
use std::{
|
||||||
|
io::BufRead,
|
||||||
|
io::{BufReader, Read, Write},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::data::{database::Database, CoverId};
|
||||||
|
|
||||||
|
pub struct Client<T: Write + Read>(BufReader<T>);
|
||||||
|
impl<T: Write + Read> Client<T> {
|
||||||
|
pub fn new(mut con: BufReader<T>) -> std::io::Result<Self> {
|
||||||
|
writeln!(con.get_mut(), "get")?;
|
||||||
|
Ok(Self(con))
|
||||||
|
}
|
||||||
|
pub fn cover_bytes(&mut self, id: CoverId) -> Result<Result<Vec<u8>, 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<Mutex<Database>>,
|
||||||
|
connection: &mut BufReader<impl Read + Write>,
|
||||||
|
) -> 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
|
||||||
|
}
|
10
musicdb-server/assets/queue_loop.html
Executable file
10
musicdb-server/assets/queue_loop.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small>repeat \:total times</small>
|
||||||
|
</div>
|
||||||
|
\:inner
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_loop_current.html
Executable file
10
musicdb-server/assets/queue_loop_current.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small><b>repeat \:total times</b></small>
|
||||||
|
</div>
|
||||||
|
\:inner
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_loopinf.html
Executable file
10
musicdb-server/assets/queue_loopinf.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small>repeat forever</small>
|
||||||
|
</div>
|
||||||
|
\:inner
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_loopinf_current.html
Executable file
10
musicdb-server/assets/queue_loopinf_current.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small><b>repeat forever</b></small>
|
||||||
|
</div>
|
||||||
|
\:inner
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_random.html
Executable file
10
musicdb-server/assets/queue_random.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small>random</small>
|
||||||
|
</div>
|
||||||
|
\:content
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_random_current.html
Executable file
10
musicdb-server/assets/queue_random_current.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small><b>random</b></small>
|
||||||
|
</div>
|
||||||
|
\:content
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_shuffle.html
Executable file
10
musicdb-server/assets/queue_shuffle.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small>shuffle</small>
|
||||||
|
</div>
|
||||||
|
\:content
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
10
musicdb-server/assets/queue_shuffle_current.html
Executable file
10
musicdb-server/assets/queue_shuffle_current.html
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<small>>></small>
|
||||||
|
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||||
|
<small><b>shuffle</b></small>
|
||||||
|
</div>
|
||||||
|
\:content
|
||||||
|
<div>
|
||||||
|
<small><<</small>
|
||||||
|
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user