Compare commits

..

7 Commits

Author SHA1 Message Date
Connor Johnstone 11a8d3a88e fleshed out subsonic more 2026-04-01 19:36:24 -04:00
Connor Johnstone ed8175b1d0 Unified the track logic. Seems to work much better 2026-03-26 17:38:16 -04:00
Connor Johnstone 4f4e6e794a added the cleanup, which was missing. also artist lookup first rather than search on import 2026-03-25 14:04:24 -04:00
Connor Johnstone 6057ffa158 fixed some unwatch cleanup stuff 2026-03-24 20:41:13 -04:00
Connor Johnstone 494de64f0a Added the import/cleanup functionality 2026-03-24 15:58:14 -04:00
Connor Johnstone 3cba8f88c1 fixed up the featured artist thing 2026-03-24 11:38:07 -04:00
Connor Johnstone 9d1366f266 redux of the worker queue 2026-03-23 18:37:45 -04:00
4 changed files with 57 additions and 3 deletions
+43 -2
View File
@@ -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,
+1
View File
@@ -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;
+10
View File
@@ -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)
+3 -1
View File
@@ -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();