fix(search): resolve notification loops causing multiple API requests
- Make auth_token() public for catalog/search endpoint authentication - Fix setSearchQuery to skip notification when value unchanged - Fix setSearchState to only notify listeners when query actually changed - Add title case transformation for better most_popular API results - Add stale result detection to ignore outdated search responses - Preserve most_popular when API returns inconsistent results - Clean up debug logging
This commit is contained in:
1 parent
d8bd93374f
commit
f699cf0e63
4 files changed
+90
-19
No files matched your search
@@ -187,8 +187,8 @@ impl QobuzClient {
|
||||
self.session.read().await.as_ref().map(|s| s.user_id)
|
||||
}
|
||||
|
||||
/// Get user auth token header value
|
||||
async fn auth_token(&self) -> Result<String> {
|
||||
/// Get user auth token header value (public for catalog search)
|
||||
pub async fn auth_token(&self) -> Result<String> {
|
||||
self.session
|
||||
.read()
|
||||
.await
|
||||
|
||||
@@ -75,14 +75,17 @@ pub async fn search_all(
|
||||
query: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<SearchAllResults, String> {
|
||||
log::debug!("search_all called with query: {}", query);
|
||||
let client = state.client.lock().await;
|
||||
|
||||
// Use catalog/search endpoint which returns everything including most_popular
|
||||
let url = endpoints::build_url(paths::CATALOG_SEARCH);
|
||||
|
||||
let response: Value = client
|
||||
.get_http()
|
||||
.get(&url)
|
||||
.header("X-App-Id", client.app_id().await.map_err(|e| e.to_string())?)
|
||||
.header("X-User-Auth-Token", client.auth_token().await.map_err(|e| e.to_string())?)
|
||||
.query(&[
|
||||
("query", query.as_str()),
|
||||
("limit", "30"),
|
||||
@@ -119,7 +122,28 @@ pub async fn search_all(
|
||||
.and_then(|mp| mp.get("items"))
|
||||
.and_then(|items| items.as_array())
|
||||
.and_then(|arr| arr.first())
|
||||
.and_then(|item| serde_json::from_value(item.clone()).ok());
|
||||
.and_then(|item| {
|
||||
let item_type = item.get("type")?.as_str()?;
|
||||
let content = item.get("content")?;
|
||||
|
||||
log::debug!("most_popular type: {}, content keys: {:?}",
|
||||
item_type,
|
||||
content.as_object().map(|o| o.keys().collect::<Vec<_>>())
|
||||
);
|
||||
|
||||
match item_type {
|
||||
"tracks" => serde_json::from_value::<Track>(content.clone())
|
||||
.ok()
|
||||
.map(MostPopularItem::Tracks),
|
||||
"albums" => serde_json::from_value::<Album>(content.clone())
|
||||
.ok()
|
||||
.map(MostPopularItem::Albums),
|
||||
"artists" => serde_json::from_value::<Artist>(content.clone())
|
||||
.ok()
|
||||
.map(MostPopularItem::Artists),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
Ok(SearchAllResults {
|
||||
albums,
|
||||
|
||||
@@ -213,6 +213,8 @@
|
||||
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let isLoadingMore = $state(false);
|
||||
let searchVersion = $state(0); // Track search version to ignore stale results
|
||||
let currentSearchQuery = $state(''); // Track current in-flight search
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
// Check if there are more results to load
|
||||
@@ -313,7 +315,19 @@
|
||||
});
|
||||
|
||||
async function performSearch() {
|
||||
if (!query.trim()) return;
|
||||
const searchQuery = query.trim();
|
||||
if (!searchQuery) return;
|
||||
|
||||
// Prevent duplicate concurrent searches for the same query
|
||||
if (currentSearchQuery === searchQuery && isSearching) {
|
||||
console.log(`Skipping duplicate search for "${searchQuery}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment version to invalidate any in-flight requests
|
||||
searchVersion++;
|
||||
const thisSearchVersion = searchVersion;
|
||||
currentSearchQuery = searchQuery;
|
||||
|
||||
isSearching = true;
|
||||
searchError = null;
|
||||
@@ -321,47 +335,71 @@
|
||||
try {
|
||||
// Search based on active tab - reset to first page
|
||||
if (activeTab === 'all') {
|
||||
allResults = await invoke<SearchAllResults<Album, Track, Artist>>('search_all', {
|
||||
query: query.trim(),
|
||||
searchType: filterType
|
||||
// Use title case for better most_popular results from Qobuz API
|
||||
const results = await invoke<SearchAllResults<Album, Track, Artist>>('search_all', {
|
||||
query: toTitleCase(searchQuery)
|
||||
});
|
||||
console.log('All results:', allResults);
|
||||
// Only apply if query hasn't changed while we were fetching
|
||||
const currentQuery = query.trim();
|
||||
if (currentQuery !== searchQuery) {
|
||||
console.log(`[Search] Ignoring stale result for "${searchQuery}"`);
|
||||
return;
|
||||
}
|
||||
// Preserve most_popular if new result doesn't have it but old one does
|
||||
// (API can be inconsistent for the same query)
|
||||
if (!results.most_popular && allResults?.most_popular) {
|
||||
results.most_popular = allResults.most_popular;
|
||||
}
|
||||
allResults = results;
|
||||
if (allResults && allResults.albums.items) {
|
||||
await loadAllAlbumDownloadStatuses(allResults.albums.items);
|
||||
}
|
||||
} else if (activeTab === 'albums') {
|
||||
albumResults = await invoke<SearchResults<Album>>('search_albums', {
|
||||
query: query.trim(),
|
||||
const results = await invoke<SearchResults<Album>>('search_albums', {
|
||||
query: searchQuery,
|
||||
limit: PAGE_SIZE,
|
||||
offset: 0,
|
||||
searchType: filterType
|
||||
});
|
||||
if (query.trim() !== searchQuery) return;
|
||||
albumResults = results;
|
||||
console.log('Album results:', albumResults);
|
||||
if (albumResults && albumResults.items) {
|
||||
await loadAllAlbumDownloadStatuses(albumResults.items);
|
||||
}
|
||||
} else if (activeTab === 'tracks') {
|
||||
trackResults = await invoke<SearchResults<Track>>('search_tracks', {
|
||||
query: query.trim(),
|
||||
const results = await invoke<SearchResults<Track>>('search_tracks', {
|
||||
query: searchQuery,
|
||||
limit: PAGE_SIZE,
|
||||
offset: 0,
|
||||
searchType: filterType
|
||||
});
|
||||
if (query.trim() !== searchQuery) return;
|
||||
trackResults = results;
|
||||
console.log('Track results:', trackResults);
|
||||
} else if (activeTab === 'artists') {
|
||||
artistResults = await invoke<SearchResults<Artist>>('search_artists', {
|
||||
query: query.trim(),
|
||||
const results = await invoke<SearchResults<Artist>>('search_artists', {
|
||||
query: searchQuery,
|
||||
limit: PAGE_SIZE,
|
||||
offset: 0,
|
||||
searchType: filterType
|
||||
});
|
||||
if (query.trim() !== searchQuery) return;
|
||||
artistResults = results;
|
||||
console.log('Artist results:', artistResults);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Search error:', err);
|
||||
searchError = String(err);
|
||||
// Only show error if this is still the latest search
|
||||
if (thisSearchVersion === searchVersion) {
|
||||
console.error('Search error:', err);
|
||||
searchError = String(err);
|
||||
}
|
||||
} finally {
|
||||
isSearching = false;
|
||||
// Only update loading state if this is still the latest search
|
||||
if (thisSearchVersion === searchVersion) {
|
||||
isSearching = false;
|
||||
currentSearchQuery = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +566,11 @@
|
||||
return artist.image?.large || artist.image?.thumbnail || artist.image?.small || '';
|
||||
}
|
||||
|
||||
// Title case query for better API results (Qobuz returns most_popular more consistently with proper capitalization)
|
||||
function toTitleCase(str: string): string {
|
||||
return str.replace(/\b\w/g, char => char.toUpperCase());
|
||||
}
|
||||
|
||||
function scrollAlbumsCarousel(direction: 'left' | 'right') {
|
||||
if (direction === 'left') {
|
||||
currentAlbumPage = Math.max(0, currentAlbumPage - 1);
|
||||
|
||||
@@ -48,9 +48,12 @@ export function getSearchState<Album, Track, Artist>(): SearchState<Album, Track
|
||||
}
|
||||
|
||||
export function setSearchState<Album, Track, Artist>(next: SearchState<Album, Track, Artist>): void {
|
||||
const prevQuery = searchState.query;
|
||||
searchState = next as SearchState<unknown, unknown, unknown>;
|
||||
// Notify query listeners when query changes
|
||||
queryListeners.forEach(fn => fn(searchState.query));
|
||||
// Only notify query listeners when query actually changes
|
||||
if (prevQuery !== searchState.query) {
|
||||
queryListeners.forEach(fn => fn(searchState.query));
|
||||
}
|
||||
}
|
||||
|
||||
// Signal to trigger scroll-to-top and focus on search input
|
||||
@@ -75,6 +78,7 @@ export function getSearchQuery(): string {
|
||||
}
|
||||
|
||||
export function setSearchQuery(query: string): void {
|
||||
if (searchState.query === query) return; // No change, don't notify
|
||||
searchState.query = query;
|
||||
queryListeners.forEach(fn => fn(query));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user