Files
psysonic/src/api/subsonicSearch.ts
T
cucadmuh 19fdba006a fix(search): local index rejects FTS syntax in live search queries (#983)
* fix(search): reject FTS syntax chars in local index live search queries

FTS5 treats `=` and similar characters as query syntax, so tokens like
1=2 produced unrelated prefix hits. Skip FTS for unsafe tokens instead.

* docs: CHANGELOG for local index FTS syntax query fix (PR #983)

* fix(search): reject syntax junk in server search3 and live search race

Share FTS-safe token guard with search3; skip network invoke for ** and
similar queries; show empty dropdown without misleading source badge.

* fix(search): allow censorship stars in queries, reject wildcard-only tokens

Block ** and **** but keep ***Flawless-style title searches working
in local FTS and search3.
2026-06-04 13:37:11 +03:00

74 lines
2.3 KiB
TypeScript

import { api, libraryFilterParams } from './subsonicClient';
import { searchQueryIsFtsSafe } from '../utils/library/searchQueryFtsSafe';
import type {
SearchResults,
SubsonicAlbum,
SubsonicArtist,
SubsonicSong,
} from './subsonicTypes';
/**
* search3 sometimes returns duplicate or junk artist rows with **zero** albums (e.g. Navidrome indexing).
* Drop only rows that explicitly report `albumCount === 0`; keep artists when the field is absent.
* Thanks to zunoz for the report on the Psysonic Discord.
*/
export function filterSearchArtistsWithNoAlbums(artists: SubsonicArtist[]): SubsonicArtist[] {
return artists.filter((a) => a.albumCount !== 0);
}
export async function search(
query: string,
options?: {
albumCount?: number;
artistCount?: number;
songCount?: number;
signal?: AbortSignal;
timeout?: number;
},
): Promise<SearchResults> {
if (!query.trim()) return { artists: [], albums: [], songs: [] };
if (!searchQueryIsFtsSafe(query)) return { artists: [], albums: [], songs: [] };
const data = await api<{
searchResult3: {
artist?: SubsonicArtist[];
album?: SubsonicAlbum[];
song?: SubsonicSong[];
};
}>(
'search3.view',
{
query,
artistCount: options?.artistCount ?? 5,
albumCount: options?.albumCount ?? 5,
songCount: options?.songCount ?? 10,
...libraryFilterParams(),
},
options?.timeout ?? 15000,
options?.signal,
);
const r = data.searchResult3 ?? {};
return {
artists: filterSearchArtistsWithNoAlbums(r.artist ?? []),
albums: r.album ?? [],
songs: r.song ?? [],
};
}
/**
* Song-only paginated search3. Tolerates empty query — Navidrome returns all songs
* ordered by title in that case; strict Subsonic implementations may return nothing.
* Caller handles empty results gracefully (Tracks page falls back to its random pool).
*/
export async function searchSongsPaged(query: string, songCount: number, songOffset: number): Promise<SubsonicSong[]> {
if (!searchQueryIsFtsSafe(query.trim())) return [];
const data = await api<{ searchResult3: { song?: SubsonicSong[] } }>('search3.view', {
query,
artistCount: 0,
albumCount: 0,
songCount,
songOffset,
...libraryFilterParams(),
});
return data.searchResult3?.song ?? [];
}