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:
1 parent
1347c11fcc
commit
2270d80139
6 files changed
+152
-14
No files matched your search
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)))?;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user