feat(library): read ISRC and MusicBrainz ids from tags; project them to the catalog
The scanner now keeps the identity tags Picard writes (ISRC/TSRC and the MusicBrainz recording, track, release, release-group and artist ids — the same lofty ItemKeys the tag writer already knows) in six additive local_tracks columns, migrated idempotently (also after the sample_rate table rebuild) and indexed on isrc and the recording id. Rows fill on the next scan. The catalog projection (legacy.rs) stops hardcoding isrc / musicbrainz_recording_id to None for local, remote and Plex rows (the upsert already bound both), treating an empty string as no id. qbz-source stamps both keys on local queue tracks. cc
This commit is contained in:
6 files changed
+161
-8
No files matched your search
@@ -322,6 +322,12 @@ pub fn cue_to_tracks(
|
||||
indexed_at: now,
|
||||
source: None,
|
||||
qobuz_track_id: None,
|
||||
isrc: None,
|
||||
musicbrainz_recording_id: None,
|
||||
musicbrainz_track_id: None,
|
||||
musicbrainz_release_id: None,
|
||||
musicbrainz_release_group_id: None,
|
||||
musicbrainz_artist_id: None,
|
||||
is_network_mount: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -641,6 +641,10 @@ impl LibraryDatabase {
|
||||
.map_err(|e| LibraryError::Database(format!("Migration failed: {}", e)))?;
|
||||
}
|
||||
|
||||
// Migration: cross-source identity tags (ISRC + MusicBrainz ids).
|
||||
// Additive; NULL until the next scan re-reads the file's tags.
|
||||
self.ensure_identity_columns()?;
|
||||
|
||||
// Migration: Change sample_rate from INTEGER to REAL for decimal precision (44.1kHz, 88.2kHz, etc.)
|
||||
// Check if sample_rate is currently INTEGER
|
||||
let sample_rate_type: String = self
|
||||
@@ -791,6 +795,8 @@ impl LibraryDatabase {
|
||||
LibraryError::Database(format!("Failed to re-add catalog_number: {}", e))
|
||||
})?;
|
||||
}
|
||||
// Same for the identity columns (they post-date the rebuild too).
|
||||
self.ensure_identity_columns()?;
|
||||
|
||||
log::info!("Migration completed: sample_rate is now REAL");
|
||||
}
|
||||
@@ -1393,8 +1399,11 @@ impl LibraryDatabase {
|
||||
disc_number, year, genre, genres_json, catalog_number, duration_secs, format, bit_depth,
|
||||
sample_rate, channels, file_size_bytes, cue_file_path,
|
||||
cue_start_secs, cue_end_secs, artwork_path, last_modified, indexed_at,
|
||||
album_group_key, album_group_title, source, is_network_mount)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||
album_group_key, album_group_title, source, is_network_mount,
|
||||
isrc, musicbrainz_recording_id, musicbrainz_track_id,
|
||||
musicbrainz_release_id, musicbrainz_release_group_id, musicbrainz_artist_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?)"#,
|
||||
params![
|
||||
track.file_path,
|
||||
track.title,
|
||||
@@ -1423,6 +1432,12 @@ impl LibraryDatabase {
|
||||
track.album_group_title,
|
||||
source,
|
||||
is_network_mount as i64,
|
||||
track.isrc,
|
||||
track.musicbrainz_recording_id,
|
||||
track.musicbrainz_track_id,
|
||||
track.musicbrainz_release_id,
|
||||
track.musicbrainz_release_group_id,
|
||||
track.musicbrainz_artist_id,
|
||||
],
|
||||
)
|
||||
.map_err(|e| LibraryError::Database(e.to_string()))?;
|
||||
@@ -1430,6 +1445,38 @@ impl LibraryDatabase {
|
||||
Ok(self.conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
/// Add the ISRC + MusicBrainz id columns when absent (idempotent, cheap:
|
||||
/// one `pragma_table_info` probe). Called from the migration chain AND
|
||||
/// after the sample_rate table rebuild, which recreates `local_tracks`
|
||||
/// without any column added by a later ALTER.
|
||||
fn ensure_identity_columns(&self) -> Result<(), LibraryError> {
|
||||
let has_isrc: bool = self
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('local_tracks') WHERE name = 'isrc'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.map(|count| count > 0)
|
||||
.unwrap_or(false);
|
||||
if !has_isrc {
|
||||
log::info!("Running migration: adding identity columns (isrc, musicbrainz_*) to local_tracks");
|
||||
self.conn
|
||||
.execute_batch(
|
||||
"ALTER TABLE local_tracks ADD COLUMN isrc TEXT;
|
||||
ALTER TABLE local_tracks ADD COLUMN musicbrainz_recording_id TEXT;
|
||||
ALTER TABLE local_tracks ADD COLUMN musicbrainz_track_id TEXT;
|
||||
ALTER TABLE local_tracks ADD COLUMN musicbrainz_release_id TEXT;
|
||||
ALTER TABLE local_tracks ADD COLUMN musicbrainz_release_group_id TEXT;
|
||||
ALTER TABLE local_tracks ADD COLUMN musicbrainz_artist_id TEXT;
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_isrc ON local_tracks(isrc);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_mb_recording ON local_tracks(musicbrainz_recording_id);",
|
||||
)
|
||||
.map_err(|e| LibraryError::Database(format!("Migration failed: {}", e)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a track by ID
|
||||
pub fn get_track(&self, id: i64) -> Result<Option<LocalTrack>, LibraryError> {
|
||||
let mut stmt = self
|
||||
@@ -3747,7 +3794,9 @@ impl LibraryDatabase {
|
||||
bit_depth, sample_rate, channels, file_size_bytes, \
|
||||
cue_file_path, cue_start_secs, cue_end_secs, artwork_path, \
|
||||
last_modified, indexed_at, album_group_key, album_group_title, \
|
||||
source, qobuz_track_id, catalog_number, is_network_mount";
|
||||
source, qobuz_track_id, catalog_number, is_network_mount, \
|
||||
isrc, musicbrainz_recording_id, musicbrainz_track_id, \
|
||||
musicbrainz_release_id, musicbrainz_release_group_id, musicbrainz_artist_id";
|
||||
|
||||
fn track_genres_json(track: &LocalTrack) -> String {
|
||||
let mut genres = Vec::<String>::new();
|
||||
@@ -3831,6 +3880,12 @@ impl LibraryDatabase {
|
||||
source: row.get(25).ok().flatten(), // source
|
||||
qobuz_track_id: row.get(26).ok().flatten(), // qobuz_track_id
|
||||
catalog_number: row.get(27).ok().flatten(), // catalog_number
|
||||
isrc: row.get(29).ok().flatten(),
|
||||
musicbrainz_recording_id: row.get(30).ok().flatten(),
|
||||
musicbrainz_track_id: row.get(31).ok().flatten(),
|
||||
musicbrainz_release_id: row.get(32).ok().flatten(),
|
||||
musicbrainz_release_group_id: row.get(33).ok().flatten(),
|
||||
musicbrainz_artist_id: row.get(34).ok().flatten(),
|
||||
is_network_mount: row
|
||||
.get::<_, Option<i64>>(28)
|
||||
.ok()
|
||||
@@ -5064,6 +5119,12 @@ impl LibraryDatabase {
|
||||
indexed_at: row.get(24)?,
|
||||
source: row.get(25)?,
|
||||
qobuz_track_id: row.get(26)?,
|
||||
isrc: None,
|
||||
musicbrainz_recording_id: None,
|
||||
musicbrainz_track_id: None,
|
||||
musicbrainz_release_id: None,
|
||||
musicbrainz_release_group_id: None,
|
||||
musicbrainz_artist_id: None,
|
||||
is_network_mount: row.get::<_, i64>(27)? != 0,
|
||||
})
|
||||
})
|
||||
@@ -5133,6 +5194,12 @@ impl LibraryDatabase {
|
||||
indexed_at: row.get(24)?,
|
||||
source: row.get(25)?,
|
||||
qobuz_track_id: row.get(26)?,
|
||||
isrc: None,
|
||||
musicbrainz_recording_id: None,
|
||||
musicbrainz_track_id: None,
|
||||
musicbrainz_release_id: None,
|
||||
musicbrainz_release_group_id: None,
|
||||
musicbrainz_artist_id: None,
|
||||
is_network_mount: row.get::<_, i64>(27)? != 0,
|
||||
},
|
||||
playlist_position: row.get(28)?,
|
||||
|
||||
@@ -733,6 +733,31 @@ impl MetadataExtractor {
|
||||
.unwrap_or(0),
|
||||
source: None,
|
||||
qobuz_track_id: None,
|
||||
// Identity tags as Picard writes them (lofty maps
|
||||
// MUSICBRAINZ_TRACKID -> MusicBrainzRecordingId and
|
||||
// MUSICBRAINZ_RELEASETRACKID -> MusicBrainzTrackId; ISRC/TSRC
|
||||
// -> Isrc). Same cross-tag fallback as every other field.
|
||||
isrc: Self::string_across_tags(&tagged_file, &ItemKey::Isrc),
|
||||
musicbrainz_recording_id: Self::string_across_tags(
|
||||
&tagged_file,
|
||||
&ItemKey::MusicBrainzRecordingId,
|
||||
),
|
||||
musicbrainz_track_id: Self::string_across_tags(
|
||||
&tagged_file,
|
||||
&ItemKey::MusicBrainzTrackId,
|
||||
),
|
||||
musicbrainz_release_id: Self::string_across_tags(
|
||||
&tagged_file,
|
||||
&ItemKey::MusicBrainzReleaseId,
|
||||
),
|
||||
musicbrainz_release_group_id: Self::string_across_tags(
|
||||
&tagged_file,
|
||||
&ItemKey::MusicBrainzReleaseGroupId,
|
||||
),
|
||||
musicbrainz_artist_id: Self::string_across_tags(
|
||||
&tagged_file,
|
||||
&ItemKey::MusicBrainzArtistId,
|
||||
),
|
||||
is_network_mount: false,
|
||||
}
|
||||
} else {
|
||||
@@ -776,6 +801,12 @@ impl MetadataExtractor {
|
||||
.unwrap_or(0),
|
||||
source: None,
|
||||
qobuz_track_id: None,
|
||||
isrc: None,
|
||||
musicbrainz_recording_id: None,
|
||||
musicbrainz_track_id: None,
|
||||
musicbrainz_release_id: None,
|
||||
musicbrainz_release_group_id: None,
|
||||
musicbrainz_artist_id: None,
|
||||
is_network_mount: false,
|
||||
}
|
||||
};
|
||||
@@ -858,6 +889,12 @@ impl MetadataExtractor {
|
||||
.unwrap_or(0),
|
||||
source: None,
|
||||
qobuz_track_id: None,
|
||||
isrc: None,
|
||||
musicbrainz_recording_id: None,
|
||||
musicbrainz_track_id: None,
|
||||
musicbrainz_release_id: None,
|
||||
musicbrainz_release_group_id: None,
|
||||
musicbrainz_artist_id: None,
|
||||
is_network_mount: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,6 +95,23 @@ pub struct LocalTrack {
|
||||
/// which is common under Flatpak / Snap sandboxes.
|
||||
#[serde(default)]
|
||||
pub is_network_mount: bool,
|
||||
|
||||
// Cross-source identity, straight from the file's tags (Picard writes
|
||||
// them; lofty reads them). `None` when the file is untagged. The
|
||||
// recording id + ISRC are the JOIN keys the listen log stamps so a play
|
||||
// of a local file and a play of the same recording on Qobuz can meet.
|
||||
#[serde(default)]
|
||||
pub isrc: Option<String>,
|
||||
#[serde(default)]
|
||||
pub musicbrainz_recording_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub musicbrainz_track_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub musicbrainz_release_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub musicbrainz_release_group_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub musicbrainz_artist_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for LocalTrack {
|
||||
@@ -129,6 +146,12 @@ impl Default for LocalTrack {
|
||||
indexed_at: 0,
|
||||
source: None,
|
||||
qobuz_track_id: None,
|
||||
isrc: None,
|
||||
musicbrainz_recording_id: None,
|
||||
musicbrainz_track_id: None,
|
||||
musicbrainz_release_id: None,
|
||||
musicbrainz_release_group_id: None,
|
||||
musicbrainz_artist_id: None,
|
||||
is_network_mount: false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user