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:
1 parent
2a5c181618
commit
279d433b77
7 files changed
+70
-15
No files matched your search
@@ -147,20 +147,31 @@ impl DiscogsClient {
|
|||||||
|
|
||||||
/// Search for album artwork options
|
/// Search for album artwork options
|
||||||
/// Returns up to 10 image options from the first matching release
|
/// 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(
|
pub async fn search_artwork_options(
|
||||||
&self,
|
&self,
|
||||||
artist: &str,
|
artist: &str,
|
||||||
album: &str,
|
album: &str,
|
||||||
|
catalog_number: Option<&str>,
|
||||||
) -> Result<Vec<DiscogsImageOption>, String> {
|
) -> Result<Vec<DiscogsImageOption>, String> {
|
||||||
// Build search query
|
// Build search query - prefer catalog number if available
|
||||||
let query = format!("{} {}", artist, album);
|
let query = if let Some(catno) = catalog_number.filter(|s| !s.trim().is_empty()) {
|
||||||
|
catno.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} {}", artist, album)
|
||||||
|
};
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/search?q={}&type=release",
|
"{}/search?q={}&type=release",
|
||||||
DISCOGS_PROXY_URL,
|
DISCOGS_PROXY_URL,
|
||||||
urlencoding::encode(&query)
|
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
|
let response = self.client.get(&url).send().await
|
||||||
.map_err(|e| format!("Failed to search Discogs: {}", e))?;
|
.map_err(|e| format!("Failed to search Discogs: {}", e))?;
|
||||||
|
|||||||
@@ -1720,11 +1720,19 @@ pub async fn playlist_get_offline_available(
|
|||||||
pub async fn discogs_search_artwork(
|
pub async fn discogs_search_artwork(
|
||||||
artist: String,
|
artist: String,
|
||||||
album: String,
|
album: String,
|
||||||
|
catalog_number: Option<String>,
|
||||||
) -> Result<Vec<crate::discogs::DiscogsImageOption>, 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();
|
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
|
/// Download and save Discogs artwork
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ pub fn cue_to_tracks(
|
|||||||
disc_number: inferred_disc,
|
disc_number: inferred_disc,
|
||||||
year: None,
|
year: None,
|
||||||
genre: None,
|
genre: None,
|
||||||
|
catalog_number: None,
|
||||||
duration_secs: duration,
|
duration_secs: duration,
|
||||||
format: format.clone(),
|
format: format.clone(),
|
||||||
bit_depth: properties.bit_depth,
|
bit_depth: properties.bit_depth,
|
||||||
|
|||||||
@@ -309,6 +309,24 @@ impl LibraryDatabase {
|
|||||||
).map_err(|e| LibraryError::Database(format!("Migration failed: {}", e)))?;
|
).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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,11 +531,11 @@ impl LibraryDatabase {
|
|||||||
.execute(
|
.execute(
|
||||||
r#"INSERT OR REPLACE INTO local_tracks
|
r#"INSERT OR REPLACE INTO local_tracks
|
||||||
(file_path, title, artist, album, album_artist, track_number,
|
(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,
|
sample_rate, channels, file_size_bytes, cue_file_path,
|
||||||
cue_start_secs, cue_end_secs, artwork_path, last_modified, indexed_at,
|
cue_start_secs, cue_end_secs, artwork_path, last_modified, indexed_at,
|
||||||
album_group_key, album_group_title)
|
album_group_key, album_group_title)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
params![
|
params![
|
||||||
track.file_path,
|
track.file_path,
|
||||||
track.title,
|
track.title,
|
||||||
@@ -528,6 +546,7 @@ impl LibraryDatabase {
|
|||||||
track.disc_number,
|
track.disc_number,
|
||||||
track.year,
|
track.year,
|
||||||
track.genre,
|
track.genre,
|
||||||
|
track.catalog_number,
|
||||||
track.duration_secs,
|
track.duration_secs,
|
||||||
track.format.to_string(),
|
track.format.to_string(),
|
||||||
track.bit_depth,
|
track.bit_depth,
|
||||||
@@ -627,6 +646,7 @@ impl LibraryDatabase {
|
|||||||
ELSE MIN(artist)
|
ELSE MIN(artist)
|
||||||
END as artist,
|
END as artist,
|
||||||
MIN(year) as year,
|
MIN(year) as year,
|
||||||
|
MIN(catalog_number) as catalog_number,
|
||||||
MAX(CASE WHEN artwork_path IS NOT NULL THEN artwork_path END) as artwork,
|
MAX(CASE WHEN artwork_path IS NOT NULL THEN artwork_path END) as artwork,
|
||||||
COUNT(*) as track_count,
|
COUNT(*) as track_count,
|
||||||
SUM(duration_secs) as total_duration,
|
SUM(duration_secs) as total_duration,
|
||||||
@@ -640,6 +660,7 @@ impl LibraryDatabase {
|
|||||||
COALESCE(album_group_title, album) as title,
|
COALESCE(album_group_title, album) as title,
|
||||||
COALESCE(album_artist, artist) as artist,
|
COALESCE(album_artist, artist) as artist,
|
||||||
year,
|
year,
|
||||||
|
catalog_number,
|
||||||
artwork_path,
|
artwork_path,
|
||||||
duration_secs,
|
duration_secs,
|
||||||
format,
|
format,
|
||||||
@@ -664,6 +685,7 @@ impl LibraryDatabase {
|
|||||||
ELSE MIN(artist)
|
ELSE MIN(artist)
|
||||||
END as artist,
|
END as artist,
|
||||||
MIN(year) as year,
|
MIN(year) as year,
|
||||||
|
MIN(catalog_number) as catalog_number,
|
||||||
MAX(CASE WHEN artwork_path IS NOT NULL THEN artwork_path END) as artwork,
|
MAX(CASE WHEN artwork_path IS NOT NULL THEN artwork_path END) as artwork,
|
||||||
COUNT(*) as track_count,
|
COUNT(*) as track_count,
|
||||||
SUM(duration_secs) as total_duration,
|
SUM(duration_secs) as total_duration,
|
||||||
@@ -677,6 +699,7 @@ impl LibraryDatabase {
|
|||||||
COALESCE(album_group_title, album) as title,
|
COALESCE(album_group_title, album) as title,
|
||||||
COALESCE(album_artist, artist) as artist,
|
COALESCE(album_artist, artist) as artist,
|
||||||
year,
|
year,
|
||||||
|
catalog_number,
|
||||||
artwork_path,
|
artwork_path,
|
||||||
duration_secs,
|
duration_secs,
|
||||||
format,
|
format,
|
||||||
@@ -705,7 +728,7 @@ impl LibraryDatabase {
|
|||||||
let group_key: String = row.get(0)?;
|
let group_key: String = row.get(0)?;
|
||||||
let album: String = row.get(1)?;
|
let album: String = row.get(1)?;
|
||||||
let artist: String = row.get(2)?;
|
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);
|
log::info!("Album {} by {}: artwork_path = {:?}", album, artist, artwork_path);
|
||||||
|
|
||||||
@@ -714,16 +737,17 @@ impl LibraryDatabase {
|
|||||||
title: album,
|
title: album,
|
||||||
artist,
|
artist,
|
||||||
year: row.get(3)?,
|
year: row.get(3)?,
|
||||||
|
catalog_number: row.get(4)?,
|
||||||
artwork_path,
|
artwork_path,
|
||||||
track_count: row.get(5)?,
|
track_count: row.get(6)?,
|
||||||
total_duration_secs: row.get(6)?,
|
total_duration_secs: row.get(7)?,
|
||||||
format: Self::parse_format(
|
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)?,
|
bit_depth: row.get(9)?,
|
||||||
sample_rate: row.get::<_, Option<u32>>(9)?.unwrap_or(44100),
|
sample_rate: row.get::<_, Option<u32>>(10)?.unwrap_or(44100),
|
||||||
directory_path: row
|
directory_path: row
|
||||||
.get::<_, Option<String>>(10)?
|
.get::<_, Option<String>>(11)?
|
||||||
.unwrap_or_else(|| group_key.clone()),
|
.unwrap_or_else(|| group_key.clone()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -973,6 +997,7 @@ impl LibraryDatabase {
|
|||||||
disc_number: row.get(7)?,
|
disc_number: row.get(7)?,
|
||||||
year: row.get(8)?,
|
year: row.get(8)?,
|
||||||
genre: row.get(9)?,
|
genre: row.get(9)?,
|
||||||
|
catalog_number: row.get(26).ok().flatten(),
|
||||||
duration_secs: row.get(10)?,
|
duration_secs: row.get(10)?,
|
||||||
format: Self::parse_format(&row.get::<_, String>(11)?),
|
format: Self::parse_format(&row.get::<_, String>(11)?),
|
||||||
bit_depth: row.get(12)?,
|
bit_depth: row.get(12)?,
|
||||||
@@ -1531,6 +1556,7 @@ impl LibraryDatabase {
|
|||||||
disc_number: row.get(9)?,
|
disc_number: row.get(9)?,
|
||||||
year: row.get(10)?,
|
year: row.get(10)?,
|
||||||
genre: row.get(11)?,
|
genre: row.get(11)?,
|
||||||
|
catalog_number: None,
|
||||||
duration_secs: row.get(12)?,
|
duration_secs: row.get(12)?,
|
||||||
format: Self::parse_format(&row.get::<_, String>(13)?),
|
format: Self::parse_format(&row.get::<_, String>(13)?),
|
||||||
bit_depth: row.get(14)?,
|
bit_depth: row.get(14)?,
|
||||||
@@ -1579,6 +1605,7 @@ impl LibraryDatabase {
|
|||||||
disc_number: row.get(9)?,
|
disc_number: row.get(9)?,
|
||||||
year: row.get(10)?,
|
year: row.get(10)?,
|
||||||
genre: row.get(11)?,
|
genre: row.get(11)?,
|
||||||
|
catalog_number: None,
|
||||||
duration_secs: row.get(12)?,
|
duration_secs: row.get(12)?,
|
||||||
format: Self::parse_format(&row.get::<_, String>(13)?),
|
format: Self::parse_format(&row.get::<_, String>(13)?),
|
||||||
bit_depth: row.get(14)?,
|
bit_depth: row.get(14)?,
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ impl MetadataExtractor {
|
|||||||
.or(inferred_disc),
|
.or(inferred_disc),
|
||||||
year: tag.year().map(|y| y as u32),
|
year: tag.year().map(|y| y as u32),
|
||||||
genre: tag.genre().map(|s| s.to_string()),
|
genre: tag.genre().map(|s| s.to_string()),
|
||||||
|
catalog_number: tag.get_string(&ItemKey::CatalogNumber).map(|s| s.to_string()),
|
||||||
duration_secs,
|
duration_secs,
|
||||||
format,
|
format,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
@@ -363,6 +364,7 @@ impl MetadataExtractor {
|
|||||||
disc_number: inferred_disc,
|
disc_number: inferred_disc,
|
||||||
year: None,
|
year: None,
|
||||||
genre: None,
|
genre: None,
|
||||||
|
catalog_number: None,
|
||||||
duration_secs,
|
duration_secs,
|
||||||
format,
|
format,
|
||||||
bit_depth,
|
bit_depth,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ pub struct LocalTrack {
|
|||||||
pub disc_number: Option<u32>,
|
pub disc_number: Option<u32>,
|
||||||
pub year: Option<u32>,
|
pub year: Option<u32>,
|
||||||
pub genre: Option<String>,
|
pub genre: Option<String>,
|
||||||
|
pub catalog_number: Option<String>,
|
||||||
|
|
||||||
// Audio properties
|
// Audio properties
|
||||||
pub duration_secs: u64,
|
pub duration_secs: u64,
|
||||||
@@ -88,6 +89,7 @@ impl Default for LocalTrack {
|
|||||||
disc_number: None,
|
disc_number: None,
|
||||||
year: None,
|
year: None,
|
||||||
genre: None,
|
genre: None,
|
||||||
|
catalog_number: None,
|
||||||
duration_secs: 0,
|
duration_secs: 0,
|
||||||
format: AudioFormat::Unknown,
|
format: AudioFormat::Unknown,
|
||||||
bit_depth: None,
|
bit_depth: None,
|
||||||
@@ -120,6 +122,7 @@ pub struct LocalAlbum {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
pub artist: String,
|
pub artist: String,
|
||||||
pub year: Option<u32>,
|
pub year: Option<u32>,
|
||||||
|
pub catalog_number: Option<String>,
|
||||||
pub artwork_path: Option<String>,
|
pub artwork_path: Option<String>,
|
||||||
pub track_count: u32,
|
pub track_count: u32,
|
||||||
pub total_duration_secs: u64,
|
pub total_duration_secs: u64,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
|
catalog_number?: string;
|
||||||
artwork_path?: string;
|
artwork_path?: string;
|
||||||
track_count: number;
|
track_count: number;
|
||||||
total_duration_secs: number;
|
total_duration_secs: number;
|
||||||
@@ -834,7 +835,8 @@
|
|||||||
|
|
||||||
const options = await invoke<DiscogsImageOption[]>('discogs_search_artwork', {
|
const options = await invoke<DiscogsImageOption[]>('discogs_search_artwork', {
|
||||||
artist: selectedAlbum.artist,
|
artist: selectedAlbum.artist,
|
||||||
album: selectedAlbum.title
|
album: selectedAlbum.title,
|
||||||
|
catalogNumber: selectedAlbum.catalog_number || null
|
||||||
});
|
});
|
||||||
|
|
||||||
discogsImageOptions = options;
|
discogsImageOptions = options;
|
||||||
@@ -1419,6 +1421,7 @@
|
|||||||
<p class="artist">{selectedAlbum.artist}</p>
|
<p class="artist">{selectedAlbum.artist}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
|
{#if selectedAlbum.catalog_number}Cat# {selectedAlbum.catalog_number} • {/if}
|
||||||
{#if selectedAlbum.year}{selectedAlbum.year} • {/if}
|
{#if selectedAlbum.year}{selectedAlbum.year} • {/if}
|
||||||
{selectedAlbum.track_count} tracks •
|
{selectedAlbum.track_count} tracks •
|
||||||
{formatTotalDuration(selectedAlbum.total_duration_secs)}
|
{formatTotalDuration(selectedAlbum.total_duration_secs)}
|
||||||
|
|||||||
Reference in new issue
Block a user