From 4c18e1c3d372f3012a681fbd8daf64f215c2d1df Mon Sep 17 00:00:00 2001 From: Xoconoch Date: Tue, 5 Aug 2025 12:27:58 -0600 Subject: [PATCH] Minor fixes --- spotizerr-ui/src/router.tsx | 20 +++++++++------ spotizerr-ui/src/routes/home.tsx | 42 ++++++++++++++++++++++++------- spotizerr-ui/src/types/spotify.ts | 10 ++++++++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/spotizerr-ui/src/router.tsx b/spotizerr-ui/src/router.tsx index c125d57..d025d2e 100644 --- a/spotizerr-ui/src/router.tsx +++ b/spotizerr-ui/src/router.tsx @@ -1,7 +1,8 @@ import { createRouter, createRootRoute, createRoute, lazyRouteComponent } from "@tanstack/react-router"; import Root from "./routes/root"; import apiClient from "./lib/api-client"; -import type { SearchResult } from "./types/spotify"; +import type { SearchResult, SearchApiResponse } from "./types/spotify"; +import { isValidSearchResult } from "./types/spotify"; // Lazy load route components for code splitting const Album = lazyRouteComponent(() => import("./routes/album").then(m => ({ default: m.Album }))); @@ -42,12 +43,17 @@ export const indexRoute = createRoute({ return { items: [{ ...response.data, model: urlType as "track" | "album" | "artist" | "playlist" }] }; } - const response = await apiClient.get<{ items: SearchResult[] }>(`/search?q=${q}&search_type=${type}&limit=50`); - const augmentedResults = response.data.items.map((item) => ({ - ...item, - model: type, - })); - return { items: augmentedResults }; + const response = await apiClient.get(`/search?q=${q}&search_type=${type}&limit=50`); + + // Filter out null values and add the model property + const validResults = response.data.items + .filter(isValidSearchResult) + .map((item) => ({ + ...item, + model: type, + })); + + return { items: validResults }; }, gcTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5 minutes diff --git a/spotizerr-ui/src/routes/home.tsx b/spotizerr-ui/src/routes/home.tsx index 4c0fe8a..9f59630 100644 --- a/spotizerr-ui/src/routes/home.tsx +++ b/spotizerr-ui/src/routes/home.tsx @@ -2,11 +2,27 @@ import { useState, useEffect, useMemo, useContext, useCallback, useRef } from "r import { useNavigate, useSearch, useRouterState } from "@tanstack/react-router"; import { useDebounce } from "use-debounce"; import { toast } from "sonner"; -import type { TrackType, AlbumType, ArtistType, PlaylistType, SearchResult } from "@/types/spotify"; +import type { TrackType, AlbumType, SearchResult } from "@/types/spotify"; import { QueueContext } from "@/contexts/queue-context"; import { SearchResultCard } from "@/components/SearchResultCard"; import { indexRoute } from "@/router"; +// Utility function to safely get properties from search results +const safelyGetProperty = (obj: any, path: string[], fallback: T): T => { + try { + let current = obj; + for (const key of path) { + if (current == null || typeof current !== 'object') { + return fallback; + } + current = current[key]; + } + return current ?? fallback; + } catch { + return fallback; + } +}; + const PAGE_SIZE = 12; export const Home = () => { @@ -127,24 +143,32 @@ export const Home = () => { return (
{displayedResults.map((item) => { + // Add safety checks for essential properties + if (!item || !item.id || !item.name || !item.model) { + return null; + } + let imageUrl; let onDownload; let subtitle; if (item.model === "track") { - imageUrl = (item as TrackType).album?.images?.[0]?.url; + imageUrl = safelyGetProperty(item, ['album', 'images', '0', 'url'], undefined); onDownload = () => handleDownloadTrack(item as TrackType); - subtitle = (item as TrackType).artists?.map((a) => a.name).join(", "); + const artists = safelyGetProperty(item, ['artists'], []); + subtitle = Array.isArray(artists) ? artists.map((a: any) => safelyGetProperty(a, ['name'], 'Unknown')).join(", ") : "Unknown Artist"; } else if (item.model === "album") { - imageUrl = (item as AlbumType).images?.[0]?.url; + imageUrl = safelyGetProperty(item, ['images', '0', 'url'], undefined); onDownload = () => handleDownloadAlbum(item as AlbumType); - subtitle = (item as AlbumType).artists?.map((a) => a.name).join(", "); + const artists = safelyGetProperty(item, ['artists'], []); + subtitle = Array.isArray(artists) ? artists.map((a: any) => safelyGetProperty(a, ['name'], 'Unknown')).join(", ") : "Unknown Artist"; } else if (item.model === "artist") { - imageUrl = (item as ArtistType).images?.[0]?.url; + imageUrl = safelyGetProperty(item, ['images', '0', 'url'], undefined); subtitle = "Artist"; } else if (item.model === "playlist") { - imageUrl = (item as PlaylistType).images?.[0]?.url; - subtitle = `By ${(item as PlaylistType).owner?.display_name || "Unknown"}`; + imageUrl = safelyGetProperty(item, ['images', '0', 'url'], undefined); + const ownerName = safelyGetProperty(item, ['owner', 'display_name'], 'Unknown'); + subtitle = `By ${ownerName}`; } return ( @@ -158,7 +182,7 @@ export const Home = () => { onDownload={onDownload} /> ); - })} + }).filter(Boolean)} {/* Filter out null components */}
); }, [displayedResults, handleDownloadTrack, handleDownloadAlbum]); diff --git a/spotizerr-ui/src/types/spotify.ts b/spotizerr-ui/src/types/spotify.ts index 59e75b6..b066a62 100644 --- a/spotizerr-ui/src/types/spotify.ts +++ b/spotizerr-ui/src/types/spotify.ts @@ -111,3 +111,13 @@ export interface PlaylistType { export type SearchResult = (TrackType | AlbumType | ArtistType | PlaylistType) & { model: "track" | "album" | "artist" | "playlist"; }; + +// API response type that can contain null values +export interface SearchApiResponse { + items: (SearchResult | null)[]; +} + +// Type guard to check if a search result is valid (not null) +export function isValidSearchResult(item: SearchResult | null): item is SearchResult { + return item !== null && typeof item === 'object' && 'id' in item; +}