Fix Hydrabase search types, ID routing, and plugin passthrough

- Use correct server request types: 'tracks', 'albums', 'artists',
  'artist.albums', 'album.tracks' (were singular, caused timeouts)
- Normalize artists to strings (server may send dicts)
- Use native plugin IDs (iTunes/Spotify) instead of soul_id for
  album/artist/track IDs so downstream endpoints can resolve them
- Carry soul_id and plugin_id in external_urls for routing
- Pass plugin param from frontend to server for correct client routing
  (iTunes vs Deezer vs Spotify) with isdigit() fallback
- Route source=hydrabase to iTunes client for artist images
- Include external_urls in enhanced search API response
- Reduce WebSocket timeout from 15s to 8s
This commit is contained in:
Broque Thomas 2026-03-20 21:04:56 -07:00
parent a4f0745547
commit ee3500242e
3 changed files with 123 additions and 31 deletions

View file

@ -32,7 +32,7 @@ class HydrabaseClient:
Same callable used by HydrabaseWorker. Same callable used by HydrabaseWorker.
""" """
self.get_ws_and_lock = get_ws_and_lock self.get_ws_and_lock = get_ws_and_lock
self.timeout = 15 # seconds self.timeout = 8 # seconds
self.last_peer_count = None self.last_peer_count = None
self.last_peer_count_time = None self.last_peer_count_time = None
@ -121,19 +121,21 @@ class HydrabaseClient:
# Response has our nonce — definitely ours # Response has our nonce — definitely ours
if data.get('nonce') == nonce: if data.get('nonce') == nonce:
results = self._extract_results(data) results = self._extract_results(data)
logger.debug(f"Hydrabase matched nonce for ({request_type}, '{query}'): {len(results) if results else 0} results")
return results if results is not None else [] return results if results is not None else []
# Response has results but no nonce (server doesn't echo nonces) # Response has results but no nonce (server doesn't echo nonces)
if 'nonce' not in data: if 'nonce' not in data:
results = self._extract_results(data) results = self._extract_results(data)
if results is not None: if results is not None:
logger.debug(f"Hydrabase no-nonce results for ({request_type}, '{query}'): {len(results)} results")
return results return results
# Stats-only message with no nonce — skip and recv again # Stats-only message with no nonce — skip and recv again
logger.debug(f"Hydrabase draining non-result message for ({request_type}, '{query}')") logger.debug(f"Hydrabase draining non-result message for ({request_type}, '{query}')")
continue continue
# Has a nonce but not ours — stale response, skip it # Has a nonce but not ours — stale response, skip it
logger.debug(f"Hydrabase draining stale nonce response for ({request_type}, '{query}')") logger.debug(f"Hydrabase draining stale nonce {data.get('nonce')} (ours={nonce}) for ({request_type}, '{query}')")
except Exception as e: except Exception as e:
logger.error(f"Hydrabase query failed ({request_type}, '{query}'): {e}") logger.error(f"Hydrabase query failed ({request_type}, '{query}'): {e}")
@ -166,13 +168,18 @@ class HydrabaseClient:
# ==================== Track Methods ==================== # ==================== Track Methods ====================
def search_tracks(self, query: str, limit: int = 20) -> List[Track]: def search_tracks(self, query: str, limit: int = 20) -> List[Track]:
results = self._send_and_recv('track', query) results = self._send_and_recv('tracks', query)
if not results: if not results:
return [] return []
tracks = [] tracks = []
for item in results[:limit]: for item in results[:limit]:
try: try:
ext_urls = dict(item.get('external_urls', {}) or {})
if item.get('soul_id'):
ext_urls['hydrabase_soul_id'] = str(item['soul_id'])
if item.get('plugin_id'):
ext_urls['hydrabase_plugin'] = item['plugin_id'].lower()
tracks.append(Track( tracks.append(Track(
id=str(item.get('id', '')), id=str(item.get('id', '')),
name=item.get('name', ''), name=item.get('name', ''),
@ -181,7 +188,7 @@ class HydrabaseClient:
duration_ms=item.get('duration_ms', 0), duration_ms=item.get('duration_ms', 0),
popularity=item.get('popularity', 0), popularity=item.get('popularity', 0),
preview_url=item.get('preview_url'), preview_url=item.get('preview_url'),
external_urls=item.get('external_urls'), external_urls=ext_urls,
image_url=item.get('image_url'), image_url=item.get('image_url'),
release_date=self._normalize_release_date(item.get('release_date', '')) release_date=self._normalize_release_date(item.get('release_date', ''))
)) ))
@ -199,6 +206,11 @@ class HydrabaseClient:
artists = [] artists = []
for item in results[:limit]: for item in results[:limit]:
try: try:
ext_urls = dict(item.get('external_urls', {}) or {})
if item.get('soul_id'):
ext_urls['hydrabase_soul_id'] = str(item['soul_id'])
if item.get('plugin_id'):
ext_urls['hydrabase_plugin'] = item['plugin_id'].lower()
artists.append(Artist( artists.append(Artist(
id=str(item.get('id', '')), id=str(item.get('id', '')),
name=item.get('name', ''), name=item.get('name', ''),
@ -206,7 +218,7 @@ class HydrabaseClient:
genres=item.get('genres', []), genres=item.get('genres', []),
followers=item.get('followers', 0), followers=item.get('followers', 0),
image_url=item.get('image_url'), image_url=item.get('image_url'),
external_urls=item.get('external_urls') external_urls=ext_urls
)) ))
except Exception as e: except Exception as e:
logger.debug(f"Skipping malformed Hydrabase artist: {e}") logger.debug(f"Skipping malformed Hydrabase artist: {e}")
@ -215,22 +227,29 @@ class HydrabaseClient:
# ==================== Album Methods ==================== # ==================== Album Methods ====================
def search_albums(self, query: str, limit: int = 20) -> List[Album]: def search_albums(self, query: str, limit: int = 20) -> List[Album]:
results = self._send_and_recv('album', query) results = self._send_and_recv('albums', query)
if not results: if not results:
return [] return []
albums = [] albums = []
for item in results[:limit]: for item in results[:limit]:
try: try:
# Use the plugin's native ID (iTunes/Spotify) so downstream
# endpoints can look it up. Carry soul_id in external_urls
# for Hydrabase-specific lookups (album.tracks).
ext_urls = dict(item.get('external_urls', {}) or {})
soul_id = item.get('soul_id', '')
if soul_id:
ext_urls['hydrabase_soul_id'] = str(soul_id)
albums.append(Album( albums.append(Album(
id=str(item.get('soul_id', item.get('id', ''))), id=str(item.get('id', soul_id)),
name=item.get('name', ''), name=item.get('name', ''),
artists=self._normalize_artists(item.get('artists', [])), artists=self._normalize_artists(item.get('artists', [])),
release_date=self._normalize_release_date(item.get('release_date', '')), release_date=self._normalize_release_date(item.get('release_date', '')),
total_tracks=item.get('total_tracks', 0), total_tracks=item.get('total_tracks', 0),
album_type=item.get('album_type', 'album'), album_type=item.get('album_type', 'album'),
image_url=item.get('image_url'), image_url=item.get('image_url'),
external_urls=item.get('external_urls') external_urls=ext_urls
)) ))
except Exception as e: except Exception as e:
logger.debug(f"Skipping malformed Hydrabase album: {e}") logger.debug(f"Skipping malformed Hydrabase album: {e}")
@ -240,22 +259,28 @@ class HydrabaseClient:
def search_discography(self, artist_name: str, limit: int = 50) -> List[Album]: def search_discography(self, artist_name: str, limit: int = 50) -> List[Album]:
"""Fetch an artist's discography (albums + singles) from Hydrabase.""" """Fetch an artist's discography (albums + singles) from Hydrabase."""
results = self._send_and_recv('discography', artist_name) results = self._send_and_recv('artist.albums', artist_name)
if not results:
results = self._send_and_recv('discography', artist_name)
if not results: if not results:
return [] return []
albums = [] albums = []
for item in results[:limit]: for item in results[:limit]:
try: try:
ext_urls = dict(item.get('external_urls', {}) or {})
soul_id = item.get('soul_id', '')
if soul_id:
ext_urls['hydrabase_soul_id'] = str(soul_id)
albums.append(Album( albums.append(Album(
id=str(item.get('soul_id', item.get('id', ''))), id=str(item.get('id', soul_id)),
name=item.get('name', ''), name=item.get('name', ''),
artists=self._normalize_artists(item.get('artists', [])), artists=self._normalize_artists(item.get('artists', [])),
release_date=self._normalize_release_date(item.get('release_date', '')), release_date=self._normalize_release_date(item.get('release_date', '')),
total_tracks=item.get('total_tracks', 0), total_tracks=item.get('total_tracks', 0),
album_type=item.get('album_type', 'album'), album_type=item.get('album_type', 'album'),
image_url=item.get('image_url'), image_url=item.get('image_url'),
external_urls=item.get('external_urls') external_urls=ext_urls
)) ))
except Exception as e: except Exception as e:
logger.debug(f"Skipping malformed Hydrabase discography album: {e}") logger.debug(f"Skipping malformed Hydrabase discography album: {e}")
@ -272,8 +297,7 @@ class HydrabaseClient:
""" """
results = self._send_and_recv('track.details', track_id) results = self._send_and_recv('track.details', track_id)
if not results: if not results:
# Fallback: search for the track ID directly results = self._send_and_recv('tracks', track_id)
results = self._send_and_recv('track', track_id)
if not results: if not results:
return None return None
@ -320,7 +344,7 @@ class HydrabaseClient:
""" """
results = self._send_and_recv('album.get', album_id) results = self._send_and_recv('album.get', album_id)
if not results: if not results:
results = self._send_and_recv('album', album_id) results = self._send_and_recv('albums', album_id)
if not results: if not results:
return None return None
@ -516,15 +540,19 @@ class HydrabaseClient:
item_type = item.get('album_type', 'album') item_type = item.get('album_type', 'album')
if type_filter and item_type not in type_filter: if type_filter and item_type not in type_filter:
continue continue
ext_urls = dict(item.get('external_urls', {}) or {})
soul_id = item.get('soul_id', '')
if soul_id:
ext_urls['hydrabase_soul_id'] = str(soul_id)
albums.append(Album( albums.append(Album(
id=str(item.get('soul_id', item.get('id', ''))), id=str(item.get('id', soul_id)),
name=item.get('name', ''), name=item.get('name', ''),
artists=self._normalize_artists(item.get('artists', [])), artists=self._normalize_artists(item.get('artists', [])),
release_date=self._normalize_release_date(item.get('release_date', '')), release_date=self._normalize_release_date(item.get('release_date', '')),
total_tracks=item.get('total_tracks', 0), total_tracks=item.get('total_tracks', 0),
album_type=item_type, album_type=item_type,
image_url=item.get('image_url'), image_url=item.get('image_url'),
external_urls=item.get('external_urls'), external_urls=ext_urls,
)) ))
except Exception as e: except Exception as e:
logger.debug(f"Skipping malformed Hydrabase artist album: {e}") logger.debug(f"Skipping malformed Hydrabase artist album: {e}")

View file

@ -6982,8 +6982,8 @@ def enhanced_search():
if primary_source != "hydrabase": if primary_source != "hydrabase":
# Mirror to Hydrabase worker (fire-and-forget) # Mirror to Hydrabase worker (fire-and-forget)
if hydrabase_worker and dev_mode_enabled: if hydrabase_worker and dev_mode_enabled:
hydrabase_worker.enqueue(query, 'track') hydrabase_worker.enqueue(query, 'tracks')
hydrabase_worker.enqueue(query, 'album') hydrabase_worker.enqueue(query, 'albums')
hydrabase_worker.enqueue(query, 'artists') hydrabase_worker.enqueue(query, 'artists')
# Search primary source synchronously — use is_spotify_authenticated() # Search primary source synchronously — use is_spotify_authenticated()
@ -7051,7 +7051,8 @@ def _enhanced_search_source(query, client):
artists.append({ artists.append({
"id": artist.id, "id": artist.id,
"name": artist.name, "name": artist.name,
"image_url": artist.image_url "image_url": artist.image_url,
"external_urls": artist.external_urls or {},
}) })
except Exception as e: except Exception as e:
logger.debug(f"Artist search failed for {type(client).__name__}: {e}") logger.debug(f"Artist search failed for {type(client).__name__}: {e}")
@ -7067,10 +7068,11 @@ def _enhanced_search_source(query, client):
"image_url": album.image_url, "image_url": album.image_url,
"release_date": album.release_date, "release_date": album.release_date,
"total_tracks": album.total_tracks, "total_tracks": album.total_tracks,
"album_type": album.album_type "album_type": album.album_type,
"external_urls": album.external_urls or {},
}) })
except Exception as e: except Exception as e:
logger.debug(f"Album search failed for {type(client).__name__}: {e}") logger.warning(f"Album search failed for {type(client).__name__}: {e}", exc_info=True)
try: try:
track_objs = client.search_tracks(query, limit=10) track_objs = client.search_tracks(query, limit=10)
@ -7083,10 +7085,11 @@ def _enhanced_search_source(query, client):
"album": track.album, "album": track.album,
"duration_ms": track.duration_ms, "duration_ms": track.duration_ms,
"image_url": track.image_url, "image_url": track.image_url,
"release_date": track.release_date "release_date": track.release_date,
"external_urls": track.external_urls or {},
}) })
except Exception as e: except Exception as e:
logger.debug(f"Track search failed for {type(client).__name__}: {e}") logger.warning(f"Track search failed for {type(client).__name__}: {e}", exc_info=True)
return {"artists": artists, "albums": albums, "tracks": tracks, "available": True} return {"artists": artists, "albums": albums, "tracks": tracks, "available": True}
@ -8936,6 +8939,19 @@ def get_artist_image(artist_id):
client = _get_deezer_client() client = _get_deezer_client()
image_url = client._get_artist_image_from_albums(artist_id) image_url = client._get_artist_image_from_albums(artist_id)
return jsonify({"success": True, "image_url": image_url}) return jsonify({"success": True, "image_url": image_url})
elif source_override == 'hydrabase':
# Route to the plugin that sourced the data
plugin = request.args.get('plugin', '').lower()
if plugin == 'deezer':
client = _get_deezer_client()
image_url = client._get_artist_image_from_albums(artist_id)
elif plugin == 'itunes' or artist_id.isdigit():
from core.itunes_client import iTunesClient
client = iTunesClient()
image_url = client._get_artist_image_from_albums(artist_id)
else:
image_url = None
return jsonify({"success": True, "image_url": image_url})
elif spotify_client and spotify_client.is_spotify_authenticated() and source_override != 'itunes': elif spotify_client and spotify_client.is_spotify_authenticated() and source_override != 'itunes':
# Use Spotify directly # Use Spotify directly
artist_data = spotify_client.sp.artist(artist_id) artist_data = spotify_client.sp.artist(artist_id)
@ -8963,7 +8979,7 @@ def get_artist_discography(artist_id):
# Mirror to Hydrabase P2P network # Mirror to Hydrabase P2P network
if hydrabase_worker and dev_mode_enabled and artist_name: if hydrabase_worker and dev_mode_enabled and artist_name:
hydrabase_worker.enqueue(artist_name, 'discography') hydrabase_worker.enqueue(artist_name, 'artist.albums')
# Determine which source to use # Determine which source to use
spotify_available = spotify_client and spotify_client.is_spotify_authenticated() spotify_available = spotify_client and spotify_client.is_spotify_authenticated()
@ -8995,12 +9011,31 @@ def get_artist_discography(artist_id):
albums = deezer_cl.get_artist_albums(artist_id, album_type='album,single', limit=50) albums = deezer_cl.get_artist_albums(artist_id, album_type='album,single', limit=50)
if albums: if albums:
active_source = 'deezer' active_source = 'deezer'
elif source_override == 'hydrabase':
plugin = request.args.get('plugin', '').lower()
if plugin == 'deezer':
hb_cl = _get_deezer_client()
elif plugin == 'itunes' or artist_id.isdigit():
from core.itunes_client import iTunesClient
hb_cl = iTunesClient()
else:
hb_cl = spotify_client
albums = hb_cl.get_artist_albums(artist_id, album_type='album,single', limit=50)
if albums:
active_source = plugin or 'hydrabase'
# If direct ID lookup failed but we have artist name, search by name # If direct ID lookup failed but we have artist name, search by name
if not albums and artist_name: if not albums and artist_name:
if source_override == 'itunes': if source_override == 'itunes':
from core.itunes_client import iTunesClient from core.itunes_client import iTunesClient
cl = iTunesClient() cl = iTunesClient()
elif source_override == 'hydrabase':
plugin = request.args.get('plugin', '').lower()
if plugin == 'deezer':
cl = _get_deezer_client()
else:
from core.itunes_client import iTunesClient
cl = iTunesClient()
elif source_override == 'deezer': elif source_override == 'deezer':
cl = _get_deezer_client() cl = _get_deezer_client()
elif source_override == 'spotify' and spotify_available: elif source_override == 'spotify' and spotify_available:
@ -9263,6 +9298,13 @@ def get_artist_album_tracks(artist_id, album_id):
if source_override == 'itunes': if source_override == 'itunes':
from core.itunes_client import iTunesClient from core.itunes_client import iTunesClient
client = iTunesClient() client = iTunesClient()
elif source_override == 'hydrabase':
plugin = request.args.get('plugin', '').lower()
if plugin == 'deezer':
client = _get_deezer_client()
elif plugin == 'itunes' or album_id.isdigit():
from core.itunes_client import iTunesClient
client = iTunesClient()
elif source_override == 'deezer': elif source_override == 'deezer':
client = _get_deezer_client() client = _get_deezer_client()
@ -25939,6 +25981,16 @@ def get_album_tracks(album_id):
if source_override == 'itunes': if source_override == 'itunes':
from core.itunes_client import iTunesClient from core.itunes_client import iTunesClient
client = iTunesClient() client = iTunesClient()
elif source_override == 'hydrabase':
# Hydrabase IDs originate from whichever plugin the peer runs.
# 'plugin' param is authoritative; fall back to ID format detection.
plugin = request.args.get('plugin', '').lower()
if plugin == 'itunes' or (not plugin and album_id.isdigit()):
from core.itunes_client import iTunesClient
client = iTunesClient()
elif plugin == 'deezer':
client = _get_deezer_client()
# else: spotify (default)
elif source_override == 'deezer': elif source_override == 'deezer':
client = _get_deezer_client() client = _get_deezer_client()
@ -26082,7 +26134,7 @@ def search_spotify_tracks():
tracks = hydrabase_client.search_tracks(query, limit=limit) tracks = hydrabase_client.search_tracks(query, limit=limit)
else: else:
if hydrabase_worker and dev_mode_enabled: if hydrabase_worker and dev_mode_enabled:
hydrabase_worker.enqueue(query, 'track') hydrabase_worker.enqueue(query, 'tracks')
tracks = spotify_client.search_tracks(query, limit=limit) tracks = spotify_client.search_tracks(query, limit=limit)
tracks_dict = [{ tracks_dict = [{
@ -26128,7 +26180,7 @@ def search_itunes_tracks():
source = 'hydrabase' source = 'hydrabase'
else: else:
if hydrabase_worker and dev_mode_enabled: if hydrabase_worker and dev_mode_enabled:
hydrabase_worker.enqueue(query, 'track') hydrabase_worker.enqueue(query, 'tracks')
fallback_client = _get_metadata_fallback_client() fallback_client = _get_metadata_fallback_client()
tracks = fallback_client.search_tracks(query, limit=limit) tracks = fallback_client.search_tracks(query, limit=limit)
source = _get_metadata_fallback_source() source = _get_metadata_fallback_source()
@ -41789,7 +41841,7 @@ def import_search_albums():
albums = hydrabase_client.search_albums(query, limit=limit) albums = hydrabase_client.search_albums(query, limit=limit)
else: else:
if hydrabase_worker and dev_mode_enabled: if hydrabase_worker and dev_mode_enabled:
hydrabase_worker.enqueue(query, 'album') hydrabase_worker.enqueue(query, 'albums')
albums = spotify_client.search_albums(query, limit=limit) albums = spotify_client.search_albums(query, limit=limit)
results = [] results = []
@ -42148,7 +42200,7 @@ def import_search_tracks():
tracks = hydrabase_client.search_tracks(query, limit=limit) tracks = hydrabase_client.search_tracks(query, limit=limit)
else: else:
if hydrabase_worker and dev_mode_enabled: if hydrabase_worker and dev_mode_enabled:
hydrabase_worker.enqueue(query, 'track') hydrabase_worker.enqueue(query, 'tracks')
tracks = spotify_client.search_tracks(query, limit=limit) tracks = spotify_client.search_tracks(query, limit=limit)
results = [] results = []

View file

@ -7681,7 +7681,10 @@ function initializeSearchModeToggle() {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
// Load the artist details with source context // Load the artist details with source context
await selectArtistForDetail(artist, { source: sourceOverride }); await selectArtistForDetail(artist, {
source: sourceOverride,
plugin: artist.external_urls?.hydrabase_plugin,
});
} }
}) })
); );
@ -8015,6 +8018,10 @@ function initializeSearchModeToggle() {
if (_activeSearchSource && _activeSearchSource !== 'spotify') { if (_activeSearchSource && _activeSearchSource !== 'spotify') {
albumParams.set('source', _activeSearchSource); albumParams.set('source', _activeSearchSource);
} }
// Pass Hydrabase plugin origin so server routes to correct client
if (album.external_urls?.hydrabase_plugin) {
albumParams.set('plugin', album.external_urls.hydrabase_plugin);
}
const response = await fetch(`/api/spotify/album/${album.id}?${albumParams}`); const response = await fetch(`/api/spotify/album/${album.id}?${albumParams}`);
if (!response.ok) { if (!response.ok) {
@ -31699,6 +31706,7 @@ async function selectArtistForDetail(artist, options = {}) {
artistsPageState.selectedArtist = artist; artistsPageState.selectedArtist = artist;
artistsPageState.currentView = 'detail'; artistsPageState.currentView = 'detail';
artistsPageState.sourceOverride = options.source || null; artistsPageState.sourceOverride = options.source || null;
artistsPageState.pluginOverride = options.plugin || null;
// Show detail state // Show detail state
showArtistDetailState(); showArtistDetailState();
@ -31707,7 +31715,7 @@ async function selectArtistForDetail(artist, options = {}) {
updateArtistDetailHeader(artist); updateArtistDetailHeader(artist);
// Load discography (pass artist name for cross-source fallback) // Load discography (pass artist name for cross-source fallback)
await loadArtistDiscography(artist.id, artist.name, options.source); await loadArtistDiscography(artist.id, artist.name, options.source, options.plugin);
} }
/** /**
@ -31715,7 +31723,7 @@ async function selectArtistForDetail(artist, options = {}) {
* @param {string} artistId - Artist ID (Spotify or iTunes format) * @param {string} artistId - Artist ID (Spotify or iTunes format)
* @param {string} [artistName] - Optional artist name for fallback searches * @param {string} [artistName] - Optional artist name for fallback searches
*/ */
async function loadArtistDiscography(artistId, artistName = null, sourceOverride = null) { async function loadArtistDiscography(artistId, artistName = null, sourceOverride = null, pluginOverride = null) {
console.log(`💿 Loading discography for artist: ${artistId} (name: ${artistName}, source: ${sourceOverride || 'auto'})`); console.log(`💿 Loading discography for artist: ${artistId} (name: ${artistName}, source: ${sourceOverride || 'auto'})`);
// Use source-prefixed cache key to avoid ID collisions between sources // Use source-prefixed cache key to avoid ID collisions between sources
@ -31746,6 +31754,7 @@ async function loadArtistDiscography(artistId, artistName = null, sourceOverride
const params = new URLSearchParams(); const params = new URLSearchParams();
if (artistName) params.set('artist_name', artistName); if (artistName) params.set('artist_name', artistName);
if (sourceOverride) params.set('source', sourceOverride); if (sourceOverride) params.set('source', sourceOverride);
if (pluginOverride) params.set('plugin', pluginOverride);
if (params.toString()) url += `?${params.toString()}`; if (params.toString()) url += `?${params.toString()}`;
// Call the real API endpoint // Call the real API endpoint
@ -33196,6 +33205,9 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) {
if (artistsPageState.sourceOverride) { if (artistsPageState.sourceOverride) {
_aat1.set('source', artistsPageState.sourceOverride); _aat1.set('source', artistsPageState.sourceOverride);
} }
if (artistsPageState.pluginOverride) {
_aat1.set('plugin', artistsPageState.pluginOverride);
}
const response = await fetch(`/api/artist/${artist.id}/album/${album.id}/tracks?${_aat1}`); const response = await fetch(`/api/artist/${artist.id}/album/${album.id}/tracks?${_aat1}`);
if (!response.ok) { if (!response.ok) {