Compare commits
7 Commits
3494de1133
...
11a8d3a88e
| Author | SHA1 | Date | |
|---|---|---|---|
| 11a8d3a88e | |||
| ed8175b1d0 | |||
| 4f4e6e794a | |||
| 6057ffa158 | |||
| 494de64f0a | |||
| 3cba8f88c1 | |||
| 9d1366f266 |
+43
-2
@@ -65,9 +65,10 @@ async fn process_file(
|
|||||||
|
|
||||||
// Upsert artist (use album_artist if available, fall back to artist)
|
// Upsert artist (use album_artist if available, fall back to artist)
|
||||||
let artist_name = meta.album_artist.as_deref().or(meta.artist.as_deref());
|
let artist_name = meta.album_artist.as_deref().or(meta.artist.as_deref());
|
||||||
|
let artist_mbid = meta.musicbrainz_artist_id.as_deref();
|
||||||
let artist_id = match artist_name {
|
let artist_id = match artist_name {
|
||||||
Some(name) if !name.is_empty() => {
|
Some(name) if !name.is_empty() => {
|
||||||
Some(queries::artists::upsert(conn, name, None).await?.id)
|
Some(queries::artists::upsert(conn, name, artist_mbid).await?.id)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -106,18 +107,58 @@ async fn process_file(
|
|||||||
bitrate: Set(meta.bitrate),
|
bitrate: Set(meta.bitrate),
|
||||||
file_size: Set(scanned.file_size),
|
file_size: Set(scanned.file_size),
|
||||||
fingerprint: NotSet,
|
fingerprint: NotSet,
|
||||||
musicbrainz_id: NotSet,
|
musicbrainz_id: if meta.musicbrainz_recording_id.is_some() {
|
||||||
|
Set(meta.musicbrainz_recording_id)
|
||||||
|
} else {
|
||||||
|
NotSet // Don't overwrite existing DB MBID when file tag is missing
|
||||||
|
},
|
||||||
artist_id: Set(artist_id),
|
artist_id: Set(artist_id),
|
||||||
album_id: Set(album_id),
|
album_id: Set(album_id),
|
||||||
file_mtime: Set(Some(scanned.mtime)),
|
file_mtime: Set(Some(scanned.mtime)),
|
||||||
added_at: NotSet,
|
added_at: NotSet,
|
||||||
updated_at: NotSet,
|
updated_at: NotSet,
|
||||||
|
tagged: NotSet,
|
||||||
};
|
};
|
||||||
queries::tracks::upsert(conn, active).await?;
|
queries::tracks::upsert(conn, active).await?;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Index a single file by path. Returns `Ok(Some(track_id))` if indexed,
|
||||||
|
/// `Ok(None)` if skipped (unchanged mtime), or an error.
|
||||||
|
pub async fn index_file(
|
||||||
|
conn: &DatabaseConnection,
|
||||||
|
file_path: &std::path::Path,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> IndexResult<Option<i32>> {
|
||||||
|
let metadata = std::fs::metadata(file_path)?;
|
||||||
|
let mtime = metadata
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| {
|
||||||
|
let duration = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
|
||||||
|
chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0)
|
||||||
|
})
|
||||||
|
.map(|dt| dt.naive_utc())
|
||||||
|
.unwrap_or_else(|| chrono::Utc::now().naive_utc());
|
||||||
|
|
||||||
|
let scanned = ScannedFile {
|
||||||
|
path: file_path.to_owned(),
|
||||||
|
file_size: metadata.len() as i64,
|
||||||
|
mtime,
|
||||||
|
};
|
||||||
|
|
||||||
|
match process_file(conn, &scanned, dry_run).await? {
|
||||||
|
true => {
|
||||||
|
// Look up the track we just created to return its ID
|
||||||
|
let path_str = file_path.to_string_lossy().to_string();
|
||||||
|
let track = shanty_db::queries::tracks::get_by_path(conn, &path_str).await?;
|
||||||
|
Ok(track.map(|t| t.id))
|
||||||
|
}
|
||||||
|
false => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the full indexing pipeline: scan directory, extract metadata, upsert to DB.
|
/// Run the full indexing pipeline: scan directory, extract metadata, upsert to DB.
|
||||||
pub async fn index_directory(
|
pub async fn index_directory(
|
||||||
conn: &DatabaseConnection,
|
conn: &DatabaseConnection,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub mod metadata;
|
|||||||
pub mod scanner;
|
pub mod scanner;
|
||||||
|
|
||||||
pub use error::{IndexError, IndexResult};
|
pub use error::{IndexError, IndexResult};
|
||||||
|
pub use indexer::index_file;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pub struct MusicMetadata {
|
|||||||
pub duration: Option<f64>,
|
pub duration: Option<f64>,
|
||||||
pub codec: Option<String>,
|
pub codec: Option<String>,
|
||||||
pub bitrate: Option<i32>,
|
pub bitrate: Option<i32>,
|
||||||
|
pub musicbrainz_recording_id: Option<String>,
|
||||||
|
pub musicbrainz_artist_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map lofty FileType to a human-readable codec string.
|
/// Map lofty FileType to a human-readable codec string.
|
||||||
@@ -74,6 +76,14 @@ pub fn extract_metadata(path: &Path) -> IndexResult<MusicMetadata> {
|
|||||||
meta.album_artist = tag
|
meta.album_artist = tag
|
||||||
.get_string(&lofty::tag::ItemKey::AlbumArtist)
|
.get_string(&lofty::tag::ItemKey::AlbumArtist)
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
meta.musicbrainz_recording_id = tag
|
||||||
|
.get_string(&lofty::tag::ItemKey::MusicBrainzRecordingId)
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
meta.musicbrainz_artist_id = tag
|
||||||
|
.get_string(&lofty::tag::ItemKey::MusicBrainzArtistId)
|
||||||
|
.map(|s| s.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(meta)
|
Ok(meta)
|
||||||
|
|||||||
@@ -58,10 +58,12 @@ async fn test_scan_indexes_music_files() {
|
|||||||
create_test_mp3(dir.path(), "song1.mp3", "Time", "Pink Floyd", "DSOTM");
|
create_test_mp3(dir.path(), "song1.mp3", "Time", "Pink Floyd", "DSOTM");
|
||||||
create_test_mp3(dir.path(), "song2.mp3", "Money", "Pink Floyd", "DSOTM");
|
create_test_mp3(dir.path(), "song2.mp3", "Money", "Pink Floyd", "DSOTM");
|
||||||
|
|
||||||
|
// Use concurrency=1 to avoid race in album upsert when two files
|
||||||
|
// share the same artist+album and are processed simultaneously.
|
||||||
let config = ScanConfig {
|
let config = ScanConfig {
|
||||||
root: dir.path().to_owned(),
|
root: dir.path().to_owned(),
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
concurrency: 2,
|
concurrency: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let stats = run_scan(db.conn(), &config).await.unwrap();
|
let stats = run_scan(db.conn(), &config).await.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user