From 1c3369c61fb5eba4e173be60cb476a83ed7561ef Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 6 Jan 2026 16:40:47 +0100 Subject: [PATCH] feat: search for artists, albums and songs --- app/(auth)/(tabs)/(search)/index.tsx | 262 +++++++++++++++++++++- components/common/TouchableItemRouter.tsx | 38 +++- translations/en.json | 4 + 3 files changed, 299 insertions(+), 5 deletions(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index d124e0eb..fd449cab 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -5,6 +5,7 @@ import type { import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; +import { Image } from "expo-image"; import { router, useLocalSearchParams, useNavigation } from "expo-router"; import { useAtom } from "jotai"; import { @@ -39,6 +40,7 @@ import { useJellyseerr } from "@/hooks/useJellyseerr"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { createStreamystatsApi } from "@/utils/streamystats"; type SearchType = "Library" | "Discover"; @@ -197,6 +199,36 @@ export default function search() { [api, searchEngine, settings, user?.Id], ); + // Separate search function for music types - always uses Jellyfin since Streamystats doesn't support music + const jellyfinSearchFn = useCallback( + async ({ + types, + query, + }: { + types: BaseItemKind[]; + query: string; + }): Promise => { + if (!api || !query) { + return []; + } + + try { + const searchApi = await getItemsApi(api).getItems({ + searchTerm: query, + limit: 10, + includeItemTypes: types, + recursive: true, + userId: user?.Id, + }); + + return (searchApi.data.Items as BaseItemDto[]) || []; + } catch (_error) { + return []; + } + }, + [api, user?.Id], + ); + type HeaderSearchBarRef = { focus: () => void; blur: () => void; @@ -287,19 +319,74 @@ export default function search() { enabled: searchType === "Library" && debouncedSearch.length > 0, }); + // Music search queries - always use Jellyfin since Streamystats doesn't support music + const { data: artists, isFetching: l9 } = useQuery({ + queryKey: ["search", "artists", debouncedSearch], + queryFn: () => + jellyfinSearchFn({ + query: debouncedSearch, + types: ["MusicArtist"], + }), + enabled: searchType === "Library" && debouncedSearch.length > 0, + }); + + const { data: albums, isFetching: l10 } = useQuery({ + queryKey: ["search", "albums", debouncedSearch], + queryFn: () => + jellyfinSearchFn({ + query: debouncedSearch, + types: ["MusicAlbum"], + }), + enabled: searchType === "Library" && debouncedSearch.length > 0, + }); + + const { data: songs, isFetching: l11 } = useQuery({ + queryKey: ["search", "songs", debouncedSearch], + queryFn: () => + jellyfinSearchFn({ + query: debouncedSearch, + types: ["Audio"], + }), + enabled: searchType === "Library" && debouncedSearch.length > 0, + }); + + const { data: playlists, isFetching: l12 } = useQuery({ + queryKey: ["search", "playlists", debouncedSearch], + queryFn: () => + jellyfinSearchFn({ + query: debouncedSearch, + types: ["Playlist"], + }), + enabled: searchType === "Library" && debouncedSearch.length > 0, + }); + const noResults = useMemo(() => { return !( movies?.length || episodes?.length || series?.length || collections?.length || - actors?.length + actors?.length || + artists?.length || + albums?.length || + songs?.length || + playlists?.length ); - }, [episodes, movies, series, collections, actors]); + }, [ + episodes, + movies, + series, + collections, + actors, + artists, + albums, + songs, + playlists, + ]); const loading = useMemo(() => { - return l1 || l2 || l3 || l7 || l8; - }, [l1, l2, l3, l7, l8]); + return l1 || l2 || l3 || l7 || l8 || l9 || l10 || l11 || l12; + }, [l1, l2, l3, l7, l8, l9, l10, l11, l12]); return ( {/* )} /> + {/* Music search results */} + { + const imageUrl = getPrimaryImageUrl({ api, item }); + return ( + + + {imageUrl ? ( + + ) : ( + + 👤 + + )} + + + {item.Name} + + + ); + }} + /> + { + const imageUrl = getPrimaryImageUrl({ api, item }); + return ( + + + {imageUrl ? ( + + ) : ( + + 🎵 + + )} + + + {item.Name} + + + {item.AlbumArtist || item.Artists?.join(", ")} + + + ); + }} + /> + { + const imageUrl = getPrimaryImageUrl({ api, item }); + return ( + + + {imageUrl ? ( + + ) : ( + + 🎵 + + )} + + + {item.Name} + + + {item.Artists?.join(", ") || item.AlbumArtist} + + + ); + }} + /> + { + const imageUrl = getPrimaryImageUrl({ api, item }); + return ( + + + {imageUrl ? ( + + ) : ( + + 🎶 + + )} + + + {item.Name} + + + {item.ChildCount} tracks + + + ); + }} + /> ) : ( { }; } - if (item.Type === "CollectionFolder" || item.Type === "Playlist") { + if (item.Type === "CollectionFolder") { return { pathname: "/[libraryId]" as const, params: { libraryId: item.Id! }, }; } + // Music types - use shared routes for proper back navigation + if (item.Type === "MusicArtist") { + return { + pathname: "/music/artist/[artistId]" as const, + params: { artistId: item.Id! }, + }; + } + + if (item.Type === "MusicAlbum") { + return { + pathname: "/music/album/[albumId]" as const, + params: { albumId: item.Id! }, + }; + } + + if (item.Type === "Audio") { + // Navigate to the album if available, otherwise to the item page + if (item.AlbumId) { + return { + pathname: "/music/album/[albumId]" as const, + params: { albumId: item.AlbumId }, + }; + } + return { + pathname: "/items/page" as const, + params: { id: item.Id! }, + }; + } + + if (item.Type === "Playlist") { + return { + pathname: "/music/playlist/[playlistId]" as const, + params: { playlistId: item.Id! }, + }; + } + // Default case - items page return { pathname: "/items/page" as const, diff --git a/translations/en.json b/translations/en.json index 6f00ba87..6fe0dcac 100644 --- a/translations/en.json +++ b/translations/en.json @@ -435,6 +435,10 @@ "episodes": "Episodes", "collections": "Collections", "actors": "Actors", + "artists": "Artists", + "albums": "Albums", + "songs": "Songs", + "playlists": "Playlists", "request_movies": "Request Movies", "request_series": "Request Series", "recently_added": "Recently Added",