Add source/qobuz_track_id to LocalTrack model and implement filtering for Artists/Tracks

- Added source and qobuz_track_id fields to LocalTrack model for proper filtering
- Updated all database row mapping functions to include new fields
- Implemented network folder and download filtering for library_get_artists command
- Implemented network folder and download filtering for library_search (Tracks) command
- Made tab counters (Albums, Artists, Tracks) reactive to filtered content
- Counters now update based on current filtered data instead of total stats

Fixes offline mode filtering for Artists and Tracks tabs. When offline or when
downloads are hidden, Artists/Tracks now properly filter out inaccessible content,
matching the behavior already implemented for Albums.
This commit is contained in:
vicrodh committed 2026-01-20 01:26:27 -06:00
1 parent 1347c11fcc
commit 2270d80139
6 files changed
+152 -14

No files matched your search

+98 -6
View File
@@ -848,24 +848,116 @@ pub async fn library_get_album_tracks(
} }
#[tauri::command] #[tauri::command]
pub async fn library_get_artists(state: State<'_, LibraryState>) -> Result<Vec<LocalArtist>, String> { pub async fn library_get_artists(
log::info!("Command: library_get_artists"); exclude_network_folders: Option<bool>,
state: State<'_, LibraryState>,
download_settings_state: State<'_, crate::config::DownloadSettingsState>,
) -> Result<Vec<LocalArtist>, String> {
log::info!("Command: library_get_artists (exclude_network: {:?})", exclude_network_folders);
// Get download settings
let include_qobuz = download_settings_state
.lock()
.map_err(|e| format!("Failed to lock download settings: {}", e))?
.get_settings()
.map(|s| s.show_in_library)
.unwrap_or(false);
let db = state.db.lock().await; let db = state.db.lock().await;
db.get_artists().map_err(|e| e.to_string()) let mut artists = db.get_artists().map_err(|e| e.to_string())?;
// Filter artists based on their tracks
if exclude_network_folders.unwrap_or(false) || !include_qobuz {
let network_folders = if exclude_network_folders.unwrap_or(false) {
db.get_network_folder_paths().map_err(|e| e.to_string())?
} else {
Vec::new()
};
artists.retain(|artist| {
// Get all tracks for this artist
if let Ok(tracks) = db.search(&artist.name, 10000) {
// Check if artist has at least one valid track
tracks.iter().any(|track| {
// Check download filter
let passes_download_filter = if include_qobuz {
true
} else {
// Only include if NOT a qobuz download
track.source.as_deref() != Some("qobuz_download")
};
// Check network folder filter
let passes_network_filter = if exclude_network_folders.unwrap_or(false) && !network_folders.is_empty() {
// Only include if NOT in network folder
!network_folders.iter().any(|folder_path| track.file_path.starts_with(folder_path))
} else {
true
};
passes_download_filter && passes_network_filter
})
} else {
false
}
});
}
Ok(artists)
} }
#[tauri::command] #[tauri::command]
pub async fn library_search( pub async fn library_search(
query: String, query: String,
limit: Option<u32>, limit: Option<u32>,
exclude_network_folders: Option<bool>,
state: State<'_, LibraryState>, state: State<'_, LibraryState>,
download_settings_state: State<'_, crate::config::DownloadSettingsState>,
) -> Result<Vec<LocalTrack>, String> { ) -> Result<Vec<LocalTrack>, String> {
log::info!("Command: library_search \"{}\"", query); log::info!("Command: library_search \"{}\" (exclude_network: {:?})", query, exclude_network_folders);
// Get download settings
let include_qobuz = download_settings_state
.lock()
.map_err(|e| format!("Failed to lock download settings: {}", e))?
.get_settings()
.map(|s| s.show_in_library)
.unwrap_or(false);
let db = state.db.lock().await; let db = state.db.lock().await;
db.search(&query, limit.unwrap_or(50)) let mut tracks = db.search(&query, limit.unwrap_or(50))
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())?;
// Filter tracks
if exclude_network_folders.unwrap_or(false) || !include_qobuz {
let network_folders = if exclude_network_folders.unwrap_or(false) {
db.get_network_folder_paths().map_err(|e| e.to_string())?
} else {
Vec::new()
};
tracks.retain(|track| {
// Check download filter
let passes_download_filter = if include_qobuz {
true
} else {
// Only include if NOT a qobuz download
track.source.as_deref() != Some("qobuz_download")
};
// Check network folder filter
let passes_network_filter = if exclude_network_folders.unwrap_or(false) && !network_folders.is_empty() {
// Only include if NOT in network folder
!network_folders.iter().any(|folder_path| track.file_path.starts_with(folder_path))
} else {
true
};
passes_download_filter && passes_network_filter
});
}
Ok(tracks)
} }
#[tauri::command] #[tauri::command]
+2
View File
@@ -270,6 +270,8 @@ pub fn cue_to_tracks(
artwork_path: None, artwork_path: None,
last_modified: 0, last_modified: 0,
indexed_at: now, indexed_at: now,
source: None,
qobuz_track_id: None,
}); });
} }
+11 -3
View File
@@ -1218,6 +1218,8 @@ impl LibraryDatabase {
artwork_path: row.get(19)?, artwork_path: row.get(19)?,
last_modified: row.get(20)?, last_modified: row.get(20)?,
indexed_at: row.get(21)?, indexed_at: row.get(21)?,
source: row.get(24).ok().flatten(),
qobuz_track_id: row.get(25).ok().flatten(),
}) })
} }
@@ -1788,7 +1790,8 @@ impl LibraryDatabase {
t.album_group_key, t.album_group_title, t.track_number, t.disc_number, t.album_group_key, t.album_group_title, t.track_number, t.disc_number,
t.year, t.genre, t.duration_secs, t.format, t.bit_depth, t.sample_rate, t.year, t.genre, t.duration_secs, t.format, t.bit_depth, t.sample_rate,
t.channels, t.file_size_bytes, t.cue_file_path, t.cue_start_secs, t.channels, t.file_size_bytes, t.cue_file_path, t.cue_start_secs,
t.cue_end_secs, t.artwork_path, t.last_modified, t.indexed_at, plt.position t.cue_end_secs, t.artwork_path, t.last_modified, t.indexed_at, t.source,
t.qobuz_track_id, plt.position
FROM playlist_local_tracks plt FROM playlist_local_tracks plt
JOIN local_tracks t ON plt.local_track_id = t.id JOIN local_tracks t ON plt.local_track_id = t.id
WHERE plt.qobuz_playlist_id = ?1 WHERE plt.qobuz_playlist_id = ?1
@@ -1822,6 +1825,8 @@ impl LibraryDatabase {
artwork_path: row.get(21)?, artwork_path: row.get(21)?,
last_modified: row.get(22)?, last_modified: row.get(22)?,
indexed_at: row.get(23)?, indexed_at: row.get(23)?,
source: row.get(24)?,
qobuz_track_id: row.get(25)?,
}) })
}).map_err(|e| LibraryError::Database(format!("Failed to query playlist local tracks: {}", e)))?; }).map_err(|e| LibraryError::Database(format!("Failed to query playlist local tracks: {}", e)))?;
@@ -1836,7 +1841,8 @@ impl LibraryDatabase {
t.album_group_key, t.album_group_title, t.track_number, t.disc_number, t.album_group_key, t.album_group_title, t.track_number, t.disc_number,
t.year, t.genre, t.duration_secs, t.format, t.bit_depth, t.sample_rate, t.year, t.genre, t.duration_secs, t.format, t.bit_depth, t.sample_rate,
t.channels, t.file_size_bytes, t.cue_file_path, t.cue_start_secs, t.channels, t.file_size_bytes, t.cue_file_path, t.cue_start_secs,
t.cue_end_secs, t.artwork_path, t.last_modified, t.indexed_at, plt.position t.cue_end_secs, t.artwork_path, t.last_modified, t.indexed_at, t.source,
t.qobuz_track_id, plt.position
FROM playlist_local_tracks plt FROM playlist_local_tracks plt
JOIN local_tracks t ON plt.local_track_id = t.id JOIN local_tracks t ON plt.local_track_id = t.id
WHERE plt.qobuz_playlist_id = ?1 WHERE plt.qobuz_playlist_id = ?1
@@ -1871,8 +1877,10 @@ impl LibraryDatabase {
artwork_path: row.get(21)?, artwork_path: row.get(21)?,
last_modified: row.get(22)?, last_modified: row.get(22)?,
indexed_at: row.get(23)?, indexed_at: row.get(23)?,
source: row.get(24)?,
qobuz_track_id: row.get(25)?,
}, },
playlist_position: row.get(24)?, playlist_position: row.get(26)?,
}) })
}).map_err(|e| LibraryError::Database(format!("Failed to query playlist local tracks with position: {}", e)))?; }).map_err(|e| LibraryError::Database(format!("Failed to query playlist local tracks with position: {}", e)))?;
+4
View File
@@ -341,6 +341,8 @@ impl MetadataExtractor {
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64) .map(|d| d.as_secs() as i64)
.unwrap_or(0), .unwrap_or(0),
source: None,
qobuz_track_id: None,
} }
} else { } else {
// No tag found, use defaults // No tag found, use defaults
@@ -380,6 +382,8 @@ impl MetadataExtractor {
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64) .map(|d| d.as_secs() as i64)
.unwrap_or(0), .unwrap_or(0),
source: None,
qobuz_track_id: None,
} }
}; };
+6
View File
@@ -72,6 +72,10 @@ pub struct LocalTrack {
// Indexing // Indexing
pub last_modified: i64, pub last_modified: i64,
pub indexed_at: i64, pub indexed_at: i64,
// Download tracking
pub source: Option<String>,
pub qobuz_track_id: Option<i64>,
} }
impl Default for LocalTrack { impl Default for LocalTrack {
@@ -102,6 +106,8 @@ impl Default for LocalTrack {
artwork_path: None, artwork_path: None,
last_modified: 0, last_modified: 0,
indexed_at: 0, indexed_at: 0,
source: None,
qobuz_track_id: None,
} }
} }
} }
@@ -181,6 +181,26 @@
let folders = $state<LibraryFolder[]>([]); let folders = $state<LibraryFolder[]>([]);
let scanProgress = $state<ScanProgress | null>(null); let scanProgress = $state<ScanProgress | null>(null);
// Reactive counters based on filtered data
let filteredAlbumCount = $derived(albums.length);
let filteredArtistCount = $derived(artists.length);
let filteredTrackCount = $derived(() => {
// When in tracks view with search results, use actual filtered count
if (currentView === 'tracks' && tracks.length > 0) {
return tracks.length;
}
// When in albums view, calculate from filtered albums
if (currentView === 'albums' && albums.length > 0) {
return albums.reduce((sum, album) => sum + album.track_count, 0);
}
// When in artists view, calculate from filtered artists
if (currentView === 'artists' && artists.length > 0) {
return artists.reduce((sum, artist) => sum + artist.track_count, 0);
}
// Fallback to stats if available
return stats ? stats.track_count : 0;
});
// Loading state // Loading state
let loading = $state(false); let loading = $state(false);
let scanning = $state(false); let scanning = $state(false);
@@ -409,7 +429,9 @@
async function loadArtists() { async function loadArtists() {
loading = true; loading = true;
try { try {
artists = await invoke<LocalArtist[]>('library_get_artists'); artists = await invoke<LocalArtist[]>('library_get_artists', {
excludeNetworkFolders: shouldExcludeNetworkFolders()
});
// Load cached artist images from database // Load cached artist images from database
await loadCachedArtistImages(); await loadCachedArtistImages();
// Fetch missing images in background if enabled // Fetch missing images in background if enabled
@@ -428,7 +450,11 @@
async function loadTracks(query = '') { async function loadTracks(query = '') {
loading = true; loading = true;
try { try {
tracks = await invoke<LocalTrack[]>('library_search', { query, limit: 1000 }); tracks = await invoke<LocalTrack[]>('library_search', {
query,
limit: 1000,
excludeNetworkFolders: shouldExcludeNetworkFolders()
});
} catch (err) { } catch (err) {
console.error('Failed to load tracks:', err); console.error('Failed to load tracks:', err);
error = String(err); error = String(err);
@@ -1849,7 +1875,7 @@
> >
<Disc3 size={16} /> <Disc3 size={16} />
<span>Albums</span> <span>Albums</span>
{#if stats}<span class="count">({stats.album_count})</span>{/if} <span class="count">({filteredAlbumCount})</span>
</button> </button>
<button <button
class="tab" class="tab"
@@ -1858,7 +1884,7 @@
> >
<Mic2 size={16} /> <Mic2 size={16} />
<span>Artists</span> <span>Artists</span>
{#if stats}<span class="count">({stats.artist_count})</span>{/if} <span class="count">({filteredArtistCount})</span>
</button> </button>
<button <button
class="tab" class="tab"
@@ -1867,7 +1893,7 @@
> >
<Music size={16} /> <Music size={16} />
<span>Tracks</span> <span>Tracks</span>
{#if stats}<span class="count">({stats.track_count})</span>{/if} <span class="count">({filteredTrackCount})</span>
</button> </button>
</div> </div>