Add catalog number support for precise Discogs searches

- Added catalog_number field to LocalTrack and LocalAlbum models
- Extract catalog_number tag from audio files using lofty
- Database migration to add catalog_number column to local_tracks table
- Modified Discogs search to prefer catalog number over artist/album
- Updated UI to display catalog number in album details
- Pass catalog_number to discogs_search_artwork command

For audiophile users with well-tagged libraries, catalog numbers
provide more precise Discogs search results than artist + album.
This commit is contained in:
vicrodh committed 2026-01-17 00:34:27 -06:00
1 parent 2a5c181618
commit 279d433b77
7 files changed
+72 -17

No files matched your search

+14 -3
View File
@@ -147,20 +147,31 @@ impl DiscogsClient {
/// Search for album artwork options
/// Returns up to 10 image options from the first matching release
/// If catalog_number is provided, searches by that first, then falls back to artist + album
pub async fn search_artwork_options(
&self,
artist: &str,
album: &str,
catalog_number: Option<&str>,
) -> Result<Vec<DiscogsImageOption>, String> {
// Build search query
let query = format!("{} {}", artist, album);
// Build search query - prefer catalog number if available
let query = if let Some(catno) = catalog_number.filter(|s| !s.trim().is_empty()) {
catno.to_string()
} else {
format!("{} {}", artist, album)
};
let url = format!(
"{}/search?q={}&type=release",
DISCOGS_PROXY_URL,
urlencoding::encode(&query)
);
log::debug!("Searching Discogs artwork options for: {} - {}", artist, album);
log::debug!(
"Searching Discogs artwork options for: {} - {} (catalog: {:?})",
artist,
album,
catalog_number
);
let response = self.client.get(&url).send().await
.map_err(|e| format!("Failed to search Discogs: {}", e))?;
+10 -2
View File
@@ -1720,11 +1720,19 @@ pub async fn playlist_get_offline_available(
pub async fn discogs_search_artwork(
artist: String,
album: String,
catalog_number: Option<String>,
) -> Result<Vec<crate::discogs::DiscogsImageOption>, String> {
log::info!("Command: discogs_search_artwork {} - {}", artist, album);
log::info!(
"Command: discogs_search_artwork {} - {} (catalog: {:?})",
artist,
album,
catalog_number
);
let client = DiscogsClient::new();
client.search_artwork_options(&artist, &album).await
client
.search_artwork_options(&artist, &album, catalog_number.as_deref())
.await
}
/// Download and save Discogs artwork
+1
View File
@@ -257,6 +257,7 @@ pub fn cue_to_tracks(
disc_number: inferred_disc,
year: None,
genre: None,
catalog_number: None,
duration_secs: duration,
format: format.clone(),
bit_depth: properties.bit_depth,
+38 -11
View File
@@ -309,6 +309,24 @@ impl LibraryDatabase {
).map_err(|e| LibraryError::Database(format!("Migration failed: {}", e)))?;
}
// Check if catalog_number column exists
let has_catalog_number: bool = self
.conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('local_tracks') WHERE name = 'catalog_number'",
[],
|row| row.get::<_, i64>(0),
)
.map(|count| count > 0)
.unwrap_or(false);
if !has_catalog_number {
log::info!("Running migration: adding catalog_number to local_tracks");
self.conn
.execute_batch("ALTER TABLE local_tracks ADD COLUMN catalog_number TEXT;")
.map_err(|e| LibraryError::Database(format!("Migration failed: {}", e)))?;
}
Ok(())
}
@@ -513,11 +531,11 @@ impl LibraryDatabase {
.execute(
r#"INSERT OR REPLACE INTO local_tracks
(file_path, title, artist, album, album_artist, track_number,
disc_number, year, genre, duration_secs, format, bit_depth,
disc_number, year, genre, 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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
params![
track.file_path,
track.title,
@@ -528,6 +546,7 @@ impl LibraryDatabase {
track.disc_number,
track.year,
track.genre,
track.catalog_number,
track.duration_secs,
track.format.to_string(),
track.bit_depth,
@@ -627,6 +646,7 @@ impl LibraryDatabase {
ELSE MIN(artist)
END as artist,
MIN(year) as year,
MIN(catalog_number) as catalog_number,
MAX(CASE WHEN artwork_path IS NOT NULL THEN artwork_path END) as artwork,
COUNT(*) as track_count,
SUM(duration_secs) as total_duration,
@@ -640,6 +660,7 @@ impl LibraryDatabase {
COALESCE(album_group_title, album) as title,
COALESCE(album_artist, artist) as artist,
year,
catalog_number,
artwork_path,
duration_secs,
format,
@@ -664,6 +685,7 @@ impl LibraryDatabase {
ELSE MIN(artist)
END as artist,
MIN(year) as year,
MIN(catalog_number) as catalog_number,
MAX(CASE WHEN artwork_path IS NOT NULL THEN artwork_path END) as artwork,
COUNT(*) as track_count,
SUM(duration_secs) as total_duration,
@@ -677,6 +699,7 @@ impl LibraryDatabase {
COALESCE(album_group_title, album) as title,
COALESCE(album_artist, artist) as artist,
year,
catalog_number,
artwork_path,
duration_secs,
format,
@@ -705,25 +728,26 @@ impl LibraryDatabase {
let group_key: String = row.get(0)?;
let album: String = row.get(1)?;
let artist: String = row.get(2)?;
let artwork_path: Option<String> = row.get(4)?;
let artwork_path: Option<String> = row.get(5)?;
log::info!("Album {} by {}: artwork_path = {:?}", album, artist, artwork_path);
Ok(LocalAlbum {
id: group_key.clone(),
title: album,
artist,
year: row.get(3)?,
catalog_number: row.get(4)?,
artwork_path,
track_count: row.get(5)?,
total_duration_secs: row.get(6)?,
track_count: row.get(6)?,
total_duration_secs: row.get(7)?,
format: Self::parse_format(
&row.get::<_, Option<String>>(7)?.unwrap_or_default(),
&row.get::<_, Option<String>>(8)?.unwrap_or_default(),
),
bit_depth: row.get(8)?,
sample_rate: row.get::<_, Option<u32>>(9)?.unwrap_or(44100),
bit_depth: row.get(9)?,
sample_rate: row.get::<_, Option<u32>>(10)?.unwrap_or(44100),
directory_path: row
.get::<_, Option<String>>(10)?
.get::<_, Option<String>>(11)?
.unwrap_or_else(|| group_key.clone()),
})
})
@@ -973,6 +997,7 @@ impl LibraryDatabase {
disc_number: row.get(7)?,
year: row.get(8)?,
genre: row.get(9)?,
catalog_number: row.get(26).ok().flatten(),
duration_secs: row.get(10)?,
format: Self::parse_format(&row.get::<_, String>(11)?),
bit_depth: row.get(12)?,
@@ -1531,6 +1556,7 @@ impl LibraryDatabase {
disc_number: row.get(9)?,
year: row.get(10)?,
genre: row.get(11)?,
catalog_number: None,
duration_secs: row.get(12)?,
format: Self::parse_format(&row.get::<_, String>(13)?),
bit_depth: row.get(14)?,
@@ -1579,6 +1605,7 @@ impl LibraryDatabase {
disc_number: row.get(9)?,
year: row.get(10)?,
genre: row.get(11)?,
catalog_number: None,
duration_secs: row.get(12)?,
format: Self::parse_format(&row.get::<_, String>(13)?),
bit_depth: row.get(14)?,
+2
View File
@@ -325,6 +325,7 @@ impl MetadataExtractor {
.or(inferred_disc),
year: tag.year().map(|y| y as u32),
genre: tag.genre().map(|s| s.to_string()),
catalog_number: tag.get_string(&ItemKey::CatalogNumber).map(|s| s.to_string()),
duration_secs,
format,
bit_depth,
@@ -363,6 +364,7 @@ impl MetadataExtractor {
disc_number: inferred_disc,
year: None,
genre: None,
catalog_number: None,
duration_secs,
format,
bit_depth,
+3
View File
@@ -51,6 +51,7 @@ pub struct LocalTrack {
pub disc_number: Option<u32>,
pub year: Option<u32>,
pub genre: Option<String>,
pub catalog_number: Option<String>,
// Audio properties
pub duration_secs: u64,
@@ -88,6 +89,7 @@ impl Default for LocalTrack {
disc_number: None,
year: None,
genre: None,
catalog_number: None,
duration_secs: 0,
format: AudioFormat::Unknown,
bit_depth: None,
@@ -120,6 +122,7 @@ pub struct LocalAlbum {
pub title: String,
pub artist: String,
pub year: Option<u32>,
pub catalog_number: Option<String>,
pub artwork_path: Option<String>,
pub track_count: u32,
pub total_duration_secs: u64,
@@ -58,6 +58,7 @@
title: string;
artist: string;
year?: number;
catalog_number?: string;
artwork_path?: string;
track_count: number;
total_duration_secs: number;
@@ -834,7 +835,8 @@
const options = await invoke<DiscogsImageOption[]>('discogs_search_artwork', {
artist: selectedAlbum.artist,
album: selectedAlbum.title
album: selectedAlbum.title,
catalogNumber: selectedAlbum.catalog_number || null
});
discogsImageOptions = options;
@@ -1419,6 +1421,7 @@
<p class="artist">{selectedAlbum.artist}</p>
{/if}
<p class="meta">
{#if selectedAlbum.catalog_number}Cat# {selectedAlbum.catalog_number} &bull; {/if}
{#if selectedAlbum.year}{selectedAlbum.year} &bull; {/if}
{selectedAlbum.track_count} tracks &bull;
{formatTotalDuration(selectedAlbum.total_duration_secs)}