add fuse search utils

This commit is contained in:
jeffvli
2025-11-19 02:10:22 -08:00
parent ec135e30ed
commit 4fc346ac90
3 changed files with 158 additions and 22 deletions
@@ -14,6 +14,7 @@ import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { eventEmitter } from '/@/renderer/events/event-emitter';
import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events';
import { useIsPlayerFetching, usePlayer } from '/@/renderer/features/player/context/player-context';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import {
subscribeCurrentTrack,
@@ -26,7 +27,6 @@ import {
usePlayerQueueType,
usePlayerSong,
} from '/@/renderer/store';
import { searchSongs } from '/@/renderer/utils/search-songs';
import { Flex } from '/@/shared/components/flex/flex';
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
import { Text } from '/@/shared/components/text/text';
@@ -140,7 +140,7 @@ export const PlayQueue = forwardRef<ItemListHandle, QueueProps>(({ listKey, sear
const filteredData: QueueSong[] = useMemo(() => {
if (debouncedSearchTerm) {
const searched = searchSongs(data, debouncedSearchTerm);
const searched = searchLibraryItems(data, debouncedSearchTerm, LibraryItem.SONG);
return searched;
}
+156
View File
@@ -1,6 +1,17 @@
import Fuse from 'fuse.js';
import z from 'zod';
import i18n from '/@/i18n/i18n';
import {
Album,
AlbumArtist,
Artist,
Genre,
LibraryItem,
Playlist,
QueueSong,
Song,
} from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const PLAY_TYPES = [
@@ -69,3 +80,148 @@ export const FILTER_KEYS = {
SHARED: SharedFilterKeys,
SONG: SongFilterKeys,
};
interface CreateFuseOptions {
fieldNormWeight?: number;
ignoreLocation?: boolean;
threshold?: number;
}
type FuseSearchableItem = Album | AlbumArtist | Artist | Genre | Playlist | QueueSong | Song;
export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
items: T[],
itemType: LibraryItem,
options: CreateFuseOptions = {},
): Fuse<T> => {
const { fieldNormWeight = 1, ignoreLocation = true, threshold = 0.3 } = options;
if (items.length === 0) {
return new Fuse(items, {
fieldNormWeight,
ignoreLocation,
keys: [],
threshold,
});
}
const sampleItem = items[0];
const stringKeys = Object.keys(sampleItem).filter(
(key) =>
typeof sampleItem[key as keyof T] === 'string' &&
!key.startsWith('_') &&
key !== 'id' &&
key !== 'albumId' &&
key !== 'streamUrl' &&
key !== 'serverId' &&
key !== 'ownerId',
) as string[];
const nestedKeys: Array<{ getFn: (item: T) => string; name: string }> = [];
switch (itemType) {
case LibraryItem.ALBUM: {
nestedKeys.push(
{
getFn: (item) => {
const a = item as Album;
return a.artists?.map((artist) => artist.name).join(' ') || '';
},
name: 'artists',
},
{
getFn: (item) => {
const a = item as Album;
return a.albumArtists?.map((artist) => artist.name).join(' ') || '';
},
name: 'albumArtists',
},
{
getFn: (item) => {
const a = item as Album;
return a.genres?.map((genre) => genre.name).join(' ') || '';
},
name: 'genres',
},
);
break;
}
case LibraryItem.ALBUM_ARTIST: {
nestedKeys.push({
getFn: (item) => {
const aa = item as AlbumArtist;
return aa.genres?.map((genre) => genre.name).join(' ') || '';
},
name: 'genres',
});
break;
}
case LibraryItem.ARTIST:
case LibraryItem.GENRE:
break;
case LibraryItem.PLAYLIST: {
nestedKeys.push({
getFn: (item) => {
const p = item as Playlist;
return p.genres?.map((genre) => genre.name).join(' ') || '';
},
name: 'genres',
});
break;
}
case LibraryItem.PLAYLIST_SONG:
case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG: {
nestedKeys.push(
{
getFn: (item) => {
const s = item as QueueSong | Song;
return s.artists?.map((artist) => artist.name).join(' ') || '';
},
name: 'artists',
},
{
getFn: (item) => {
const s = item as QueueSong | Song;
return s.albumArtists?.map((artist) => artist.name).join(' ') || '';
},
name: 'albumArtists',
},
{
getFn: (item) => {
const s = item as QueueSong | Song;
return s.genres?.map((genre) => genre.name).join(' ') || '';
},
name: 'genres',
},
);
break;
}
}
return new Fuse(items, {
fieldNormWeight,
ignoreLocation,
keys: [...stringKeys, ...nestedKeys],
threshold,
});
};
export const searchLibraryItems = <T extends FuseSearchableItem>(
items: T[],
searchTerm: string,
itemType: LibraryItem,
options?: CreateFuseOptions,
): T[] => {
if (!searchTerm.trim()) {
return items;
}
const fuse = createFuseForLibraryItem(items, itemType, options);
return fuse.search(searchTerm).map((result) => result.item);
};
-20
View File
@@ -1,20 +0,0 @@
import Fuse from 'fuse.js';
import { QueueSong } from '/@/shared/types/domain-types';
export const searchSongs = (songs: QueueSong[], searchTerm: string) => {
const fuse = new Fuse(songs, {
fieldNormWeight: 1,
ignoreLocation: true,
keys: [
'name',
'album',
{
getFn: (song) => song.artists.map((artist) => artist.name),
name: 'artist',
},
],
threshold: 0,
});
return fuse.search(searchTerm).map((item) => item.item);
};