store file last modified date for each song

and add find-songs-with-changed-files to get api
This commit is contained in:
Mark 2024-08-26 20:16:01 +02:00
parent 1f39fe0cae
commit dd2cd8551d
4 changed files with 268 additions and 18 deletions

View File

@ -4,7 +4,7 @@ use std::{
fs, fs,
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex}, time::SystemTime,
}; };
use id3::TagLike; use id3::TagLike;
@ -12,7 +12,7 @@ use musicdb_lib::data::{
album::Album, album::Album,
artist::Artist, artist::Artist,
database::{Cover, Database}, database::{Cover, Database},
song::{CachedData, Song}, song::Song,
CoverId, DatabaseLocation, GeneralData, CoverId, DatabaseLocation, GeneralData,
}; };
@ -218,18 +218,45 @@ fn main() {
.to_string_lossy() .to_string_lossy()
.into_owned() .into_owned()
}); });
database.add_song_new(Song { database.add_song_new(Song::new(
id: 0, DatabaseLocation {
title: title.clone(),
location: DatabaseLocation {
rel_path: path.to_path_buf(), rel_path: path.to_path_buf(),
}, },
album: album_id, match path.metadata() {
artist: artist_id, Ok(v) => match v.modified() {
more_artists: vec![], Ok(v) => if let Ok(time) = v.duration_since(SystemTime::UNIX_EPOCH) {
cover: None, Some(time.as_secs())
file_size: song_file_metadata.len(), } else {
duration_millis: if let Some(dur) = song_tags.duration() { eprintln!(
"LastModified time of song {:?} is before the UNIX-EPOCH, setting `None`.",
song_path
);
None
},
Err(e) => {
eprintln!(
"LastModified time of song {:?} not available: {e}.",
song_path
);
None
}
}
Err(e) => {
eprintln!(
"LastModified time of song {:?} could not be read: {e}.",
song_path
);
None
}
},
title.clone(),
album_id,
artist_id,
vec![],
None,
song_file_metadata.len(),
if let Some(dur) = song_tags.duration() {
dur as u64 dur as u64
} else { } else {
if skip_duration { if skip_duration {
@ -249,8 +276,7 @@ fn main() {
} }
}, },
general, general,
cached_data: CachedData(Arc::new(Mutex::new((None, None)))), ));
});
} }
eprintln!("searching for covers..."); eprintln!("searching for covers...");
let mut multiple_cover_options = vec![]; let mut multiple_cover_options = vec![];

View File

@ -3,7 +3,7 @@ use std::{
collections::{BTreeSet, HashMap}, collections::{BTreeSet, HashMap},
fs::{self, File}, fs::{self, File},
io::{BufReader, Read, Write}, io::{BufReader, Read, Write},
path::PathBuf, path::{Path, PathBuf},
sync::{mpsc, Arc, Mutex}, sync::{mpsc, Arc, Mutex},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -73,7 +73,10 @@ impl Database {
self.client_is_init self.client_is_init
} }
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf { pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
self.lib_directory.join(&location.rel_path) Self::get_path_nodb(&self.lib_directory, location)
}
pub fn get_path_nodb(lib_directory: &impl AsRef<Path>, location: &DatabaseLocation) -> PathBuf {
lib_directory.as_ref().join(&location.rel_path)
} }
fn modified_data(&mut self) { fn modified_data(&mut self) {
let now = Instant::now(); let now = Instant::now();

View File

@ -21,6 +21,7 @@ use super::{
pub struct Song { pub struct Song {
pub id: SongId, pub id: SongId,
pub location: DatabaseLocation, pub location: DatabaseLocation,
pub file_last_modified_unix_timestamp: Option<u64>,
pub title: String, pub title: String,
pub album: Option<AlbumId>, pub album: Option<AlbumId>,
pub artist: ArtistId, pub artist: ArtistId,
@ -38,6 +39,7 @@ pub struct Song {
impl Song { impl Song {
pub fn new( pub fn new(
location: DatabaseLocation, location: DatabaseLocation,
file_last_modified_unix_timestamp: Option<u64>,
title: String, title: String,
album: Option<AlbumId>, album: Option<AlbumId>,
artist: ArtistId, artist: ArtistId,
@ -45,10 +47,12 @@ impl Song {
cover: Option<CoverId>, cover: Option<CoverId>,
file_size: u64, file_size: u64,
duration_millis: u64, duration_millis: u64,
general: GeneralData,
) -> Self { ) -> Self {
Self { Self {
id: 0, id: 0,
location, location,
file_last_modified_unix_timestamp,
title, title,
album, album,
artist, artist,
@ -56,7 +60,7 @@ impl Song {
cover, cover,
file_size, file_size,
duration_millis, duration_millis,
general: GeneralData::default(), general,
cached_data: CachedData(Arc::new(Mutex::new((Err(None), None)))), cached_data: CachedData(Arc::new(Mutex::new((Err(None), None)))),
} }
} }
@ -258,6 +262,7 @@ impl ToFromBytes for Song {
{ {
self.id.to_bytes(s)?; self.id.to_bytes(s)?;
self.location.to_bytes(s)?; self.location.to_bytes(s)?;
self.file_last_modified_unix_timestamp.to_bytes(s)?;
self.title.to_bytes(s)?; self.title.to_bytes(s)?;
self.album.to_bytes(s)?; self.album.to_bytes(s)?;
self.artist.to_bytes(s)?; self.artist.to_bytes(s)?;
@ -275,6 +280,7 @@ impl ToFromBytes for Song {
Ok(Self { Ok(Self {
id: ToFromBytes::from_bytes(s)?, id: ToFromBytes::from_bytes(s)?,
location: ToFromBytes::from_bytes(s)?, location: ToFromBytes::from_bytes(s)?,
file_last_modified_unix_timestamp: ToFromBytes::from_bytes(s)?,
title: ToFromBytes::from_bytes(s)?, title: ToFromBytes::from_bytes(s)?,
album: ToFromBytes::from_bytes(s)?, album: ToFromBytes::from_bytes(s)?,
artist: ToFromBytes::from_bytes(s)?, artist: ToFromBytes::from_bytes(s)?,

View File

@ -3,7 +3,7 @@ use std::{
io::{BufRead, BufReader, Read, Write}, io::{BufRead, BufReader, Read, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::{Instant, SystemTime},
}; };
use crate::data::{database::Database, CoverId, SongId}; use crate::data::{database::Database, CoverId, SongId};
@ -102,6 +102,134 @@ impl<T: Write + Read> Client<T> {
Ok(Err(response)) Ok(Err(response))
} }
} }
/// find songs which have no last-modified time, whose time has changed, whose files have been removed or whose files could not be read due to another error.
pub fn find_songs_with_changed_files(
&mut self,
) -> Result<
Result<
(
Vec<SongId>,
Vec<(SongId, u64)>,
Vec<SongId>,
Vec<(SongId, String)>,
),
String,
>,
std::io::Error,
> {
writeln!(
self.0.get_mut(),
"{}",
con_get_encode_string("find-songs-with-changed-files")
)?;
self.0.get_mut().flush()?;
loop {
let mut response = String::new();
self.0.read_line(&mut response)?;
let len_line = response.trim();
if len_line.starts_with('%') {
eprintln!(
"[Find Songs With Changed Files] Status: {}",
len_line[1..].trim()
);
} else {
let mut read_list = || -> std::io::Result<Result<Vec<String>, String>> {
if len_line.starts_with("len: ") {
if let Ok(len) = len_line[4..].trim().parse() {
let mut out = Vec::with_capacity(len);
for _ in 0..len {
let mut line = String::new();
self.0.read_line(&mut line)?;
let line = line.trim_end_matches(['\n', '\r']);
out.push(line.trim().to_owned());
}
Ok(Ok(out))
} else {
Ok(Err(format!("bad len in len-line: {len_line}")))
}
} else {
Ok(Err(format!("bad len-line: {len_line}")))
}
};
break Ok(Ok((
match read_list()? {
Ok(v) => match v
.into_iter()
.map(|v| v.trim().parse::<SongId>().map_err(|e| (v, e.to_string())))
.collect()
{
Ok(v) => v,
Err((s, e)) => {
return Ok(Err(format!("error parsing songid(notime) '{s}': {e}")))
}
},
Err(e) => return Ok(Err(e)),
},
match read_list()? {
Ok(v) => match v
.into_iter()
.map(|v| {
v.trim()
.split_once(':')
.ok_or_else(|| format!("missing colon"))
.and_then(|(i, t)| {
Ok((
i.parse::<SongId>().map_err(|e| e.to_string())?,
t.parse::<u64>().map_err(|e| e.to_string())?,
))
})
.map_err(|e| (v, e))
})
.collect()
{
Ok(v) => v,
Err((s, e)) => {
return Ok(Err(format!("error parsing songid+time '{s}': {e}")))
}
},
Err(e) => return Ok(Err(e)),
},
match read_list()? {
Ok(v) => match v
.into_iter()
.map(|v| v.trim().parse::<SongId>().map_err(|e| (v, e)))
.collect()
{
Ok(v) => v,
Err((s, e)) => {
return Ok(Err(format!("error parsing songid(deleted) '{s}': {e}")))
}
},
Err(e) => return Ok(Err(e)),
},
match read_list()? {
Ok(v) => match v
.into_iter()
.map(|v| {
v.trim()
.split_once(':')
.ok_or_else(|| format!("missing colon"))
.and_then(|(i, t)| {
Ok((
i.parse::<SongId>().map_err(|e| e.to_string())?,
t.to_owned(),
))
})
.map_err(|e| (v, e))
})
.collect()
{
Ok(v) => v,
Err((s, e)) => {
return Ok(Err(format!("error parsing songid+error '{s}': {e}")))
}
},
Err(e) => return Ok(Err(e)),
},
)));
};
}
}
/// tell the server to search for files that are not in its song database. /// tell the server to search for files that are not in its song database.
/// ///
/// ## `extensions`: /// ## `extensions`:
@ -109,6 +237,8 @@ impl<T: Write + Read> Client<T> {
/// If `Some([])`, allow all extensions, even ones like `.jpg` and files without extensions. /// If `Some([])`, allow all extensions, even ones like `.jpg` and files without extensions.
/// If `Some(...)`, only allow the specified extensions. Note: These are actually suffixes, for example `mp3` would allow a file named `test_mp3`, while `.mp3` would only allow `test.mp3`. /// If `Some(...)`, only allow the specified extensions. Note: These are actually suffixes, for example `mp3` would allow a file named `test_mp3`, while `.mp3` would only allow `test.mp3`.
/// Because of this, you usually want to include the `.` before the extension, and double extensions like `.tar.gz` are also supported. /// Because of this, you usually want to include the `.` before the extension, and double extensions like `.tar.gz` are also supported.
///
/// For each file, returns a boolean error flag indicating, if `true`, that the path was invalid (not UTF-8 or contained a newline).
pub fn find_unused_song_files( pub fn find_unused_song_files(
&mut self, &mut self,
extensions: Option<&[&str]>, extensions: Option<&[&str]>,
@ -317,6 +447,91 @@ pub fn handle_one_connection_as_get(
writeln!(connection.get_mut(), "no data")?; writeln!(connection.get_mut(), "no data")?;
} }
} }
"find-songs-with-changed-files" => {
let db_lock = db.lock().unwrap();
let lib_directory = db_lock.lib_directory.clone();
let all_songs = db_lock
.songs()
.iter()
.map(|(id, song)| {
(
*id,
song.location.clone(),
song.file_last_modified_unix_timestamp.clone(),
)
})
.collect::<Vec<_>>();
drop(db_lock);
let (
mut songs_no_time,
mut songs_new_time,
mut songs_removed,
mut songs_err,
) = (vec![], vec![], vec![], vec![]);
for (id, location, last_modified) in all_songs {
let path = Database::get_path_nodb(&lib_directory, &location);
match path.try_exists() {
Ok(true) => match path.metadata() {
Ok(metadata) => {
let time = metadata.modified().ok().and_then(|time| {
time.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.map(|v| v.as_secs())
});
if last_modified.is_none() || time != last_modified {
if let Some(time) = time {
songs_new_time.push((id, time));
} else {
songs_no_time.push(id);
}
}
}
Err(e) => songs_err.push((id, e.to_string())),
},
Ok(false) => songs_removed.push(id),
Err(e) => songs_err.push((id, e.to_string())),
}
}
write_list(
connection.get_mut(),
songs_no_time.len(),
songs_no_time.into_iter().map(|id| (id, None)),
)?;
write_list(
connection.get_mut(),
songs_new_time.len(),
songs_new_time
.into_iter()
.map(|(id, t)| (id, Some(format!("{t}")))),
)?;
write_list(
connection.get_mut(),
songs_removed.len(),
songs_removed.into_iter().map(|id| (id, None)),
)?;
write_list(
connection.get_mut(),
songs_err.len(),
songs_err
.into_iter()
.map(|(id, e)| (id, Some(format!("{e}")))),
)?;
fn write_list(
connection: &mut impl Write,
len: usize,
list: impl IntoIterator<Item = (u64, Option<String>)>,
) -> std::io::Result<()> {
writeln!(connection, "len: {}", len)?;
for (song, data) in list {
if let Some(data) = data {
writeln!(connection, "{song}:{data}")?;
} else {
writeln!(connection, "{song}")?;
}
}
Ok(())
}
}
"find-unused-song-files" => { "find-unused-song-files" => {
// configure search // configure search
let mut extensions = None; let mut extensions = None;