From 6be5195364df39171673bdb72186de57a94d7b25 Mon Sep 17 00:00:00 2001 From: lklynet Date: Mon, 23 Feb 2026 15:22:33 -0500 Subject: [PATCH 1/2] feat(search): show library checks and speed up lookup when searching artists or #tags, results will now display a small green checkmark next to artists already in your library. to save on api costs on large libraries, this uses the discovery artist cache created on app launch / every 7 days. --- backend/routes/library/handlers/misc.js | 7 +- frontend/src/pages/SearchResultsPage.jsx | 114 +++++++++++++++++------ 2 files changed, 89 insertions(+), 32 deletions(-) diff --git a/backend/routes/library/handlers/misc.js b/backend/routes/library/handlers/misc.js index 89db9c4..2397025 100644 --- a/backend/routes/library/handlers/misc.js +++ b/backend/routes/library/handlers/misc.js @@ -76,10 +76,13 @@ export default function registerMisc(router) { return res.status(400).json({ error: "mbids must be an array" }); } + const libraryArtists = await libraryManager.getAllArtists(); + const existingArtistIds = new Set( + libraryArtists.map((artist) => artist.mbid).filter(Boolean), + ); const results = {}; for (const mbid of mbids) { - const artist = await libraryManager.getArtist(mbid); - results[mbid] = !!artist; + results[mbid] = existingArtistIds.has(mbid); } res.json(results); diff --git a/frontend/src/pages/SearchResultsPage.jsx b/frontend/src/pages/SearchResultsPage.jsx index fd2dd62..e866254 100644 --- a/frontend/src/pages/SearchResultsPage.jsx +++ b/frontend/src/pages/SearchResultsPage.jsx @@ -1,11 +1,12 @@ import { useState, useEffect, useCallback, useMemo } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { Loader, Music, ArrowLeft } from "lucide-react"; +import { Loader, Music, ArrowLeft, CheckCircle2 } from "lucide-react"; import { searchArtists, searchArtistsByTag, getDiscovery, checkHealth, + lookupArtistsInLibraryBatch, } from "../utils/api"; import ArtistImage from "../components/ArtistImage"; import PillToggle from "../components/PillToggle"; @@ -26,6 +27,7 @@ function SearchResultsPage() { const [hasMore, setHasMore] = useState(false); const [searchTotalCount, setSearchTotalCount] = useState(0); const [lastfmConfigured, setLastfmConfigured] = useState(null); + const [libraryLookup, setLibraryLookup] = useState({}); const navigate = useNavigate(); const trimmedQuery = useMemo(() => query.trim(), [query]); @@ -36,15 +38,21 @@ function SearchResultsPage() { const tagScope = searchParams.get("scope") || "recommended"; const showAllTagResults = isTagSearch && tagScope === "all"; + const getArtistId = useCallback( + (artist) => artist?.id || artist?.mbid || artist?.foreignArtistId, + [], + ); + const dedupe = useCallback((artists) => { const seen = new Set(); return artists.filter((artist) => { - if (!artist.id) return false; - if (seen.has(artist.id)) return false; - seen.add(artist.id); + const artistId = getArtistId(artist); + if (!artistId) return false; + if (seen.has(artistId)) return false; + seen.add(artistId); return true; }); - }, []); + }, [getArtistId]); const updateTagScope = useCallback( (nextScope) => { const params = new URLSearchParams(searchParams); @@ -72,6 +80,7 @@ function SearchResultsPage() { useEffect(() => { const performSearch = async () => { + setLibraryLookup({}); if (type === "recommended" || type === "trending") { setLoading(true); setError(null); @@ -88,8 +97,8 @@ function SearchResultsPage() { if (list.length > 0) { const imagesMap = {}; list.forEach((artist) => { - if (artist.image && artist.id) - imagesMap[artist.id] = artist.image; + const artistId = getArtistId(artist); + if (artist.image && artistId) imagesMap[artistId] = artist.image; }); setArtistImages(imagesMap); } @@ -143,7 +152,8 @@ function SearchResultsPage() { if (uniqueArtists.length > 0) { const imagesMap = {}; uniqueArtists.forEach((artist) => { - if (artist.image && artist.id) imagesMap[artist.id] = artist.image; + const artistId = getArtistId(artist); + if (artist.image && artistId) imagesMap[artistId] = artist.image; }); setArtistImages(imagesMap); } @@ -160,7 +170,40 @@ function SearchResultsPage() { }; performSearch(); - }, [query, type, dedupe, trimmedQuery, isTagSearch, tagScope]); + }, [query, type, dedupe, trimmedQuery, isTagSearch, tagScope, getArtistId]); + + useEffect(() => { + let cancelled = false; + const ids = results.map((artist) => getArtistId(artist)).filter(Boolean); + if (ids.length === 0) { + setLibraryLookup({}); + return () => { + cancelled = true; + }; + } + const missing = ids.filter((id) => libraryLookup[id] === undefined); + if (missing.length === 0) return () => { + cancelled = true; + }; + + const fetchLookup = async () => { + try { + const lookup = await lookupArtistsInLibraryBatch(missing); + if (!cancelled && lookup) { + setLibraryLookup((prev) => ({ ...prev, ...lookup })); + } + } catch { + if (!cancelled) { + setLibraryLookup((prev) => ({ ...prev })); + } + } + }; + + fetchLookup(); + return () => { + cancelled = true; + }; + }, [results, libraryLookup, getArtistId]); const loadMore = useCallback(async () => { if (type === "recommended" || type === "trending") { @@ -188,8 +231,9 @@ function SearchResultsPage() { setResults(combined); setHasMore(newArtists.length >= PAGE_SIZE); newArtists.forEach((artist) => { - if (artist.image && artist.id) { - setArtistImages((prev) => ({ ...prev, [artist.id]: artist.image })); + const artistId = getArtistId(artist); + if (artist.image && artistId) { + setArtistImages((prev) => ({ ...prev, [artistId]: artist.image })); } }); } finally { @@ -210,8 +254,9 @@ function SearchResultsPage() { setSearchTotalCount(total); setHasMore(total > offset + newArtists.length); newArtists.forEach((artist) => { - if (artist.image && artist.id) { - setArtistImages((prev) => ({ ...prev, [artist.id]: artist.image })); + const artistId = getArtistId(artist); + if (artist.image && artistId) { + setArtistImages((prev) => ({ ...prev, [artistId]: artist.image })); } }); } @@ -228,6 +273,7 @@ function SearchResultsPage() { trimmedQuery, isTagSearch, tagScope, + getArtistId, ]); const getArtistType = (artistType) => { @@ -398,14 +444,16 @@ function SearchResultsPage() { ) : ( <>
- {displayedArtists.map((artist, index) => ( + {displayedArtists.map((artist, index) => { + const artistId = getArtistId(artist); + return (
- navigate(`/artist/${artist.id}`, { + navigate(`/artist/${artistId}`, { state: { artistName: artist.name }, }) } @@ -414,11 +462,11 @@ function SearchResultsPage() { >
-

- navigate(`/artist/${artist.id}`, { - state: { artistName: artist.name }, - }) - } - className="font-semibold truncate hover:underline cursor-pointer" - style={{ color: "#fff" }} - > - {artist.name} -

+
+

+ navigate(`/artist/${artistId}`, { + state: { artistName: artist.name }, + }) + } + className="font-semibold truncate hover:underline cursor-pointer" + style={{ color: "#fff" }} + > + {artist.name} +

+ {libraryLookup[artistId] && ( + + )} +
- ))} + ); + })}
{showLoadMore && ( From cc4f78008c9e9f2ca44686361e603de89da3f2f2 Mon Sep 17 00:00:00 2001 From: lklynet Date: Mon, 23 Feb 2026 15:32:39 -0500 Subject: [PATCH 2/2] fix(backend): add python3, make, and g++ to Docker image Install additional build tools required by some npm dependencies. --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 7f836ac..c0e8bac 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,7 @@ FROM node:18-alpine WORKDIR /app -RUN apk add --no-cache su-exec +RUN apk add --no-cache su-exec python3 make g++ && ln -sf /usr/bin/python3 /usr/bin/python COPY package*.json ./