feat: enhance search functionality with improved LIKE pattern matching for non-ASCII characters

This commit is contained in:
4gray committed 2026-02-08 20:24:18 +01:00
1 parent 8900ee1834
commit e04ae0e738
1 file changed
+38 -11
@@ -3,7 +3,7 @@
* Operations for managing and searching content (streams, movies, series)
*/
import { and, desc, eq, inArray, sql } from 'drizzle-orm';
import { and, desc, eq, inArray, or, sql } from 'drizzle-orm';
import { ipcMain } from 'electron';
import { getDatabase } from '../../database/connection';
import * as schema from '../../database/schema';
@@ -16,6 +16,25 @@ function escapeLikePattern(term: string): string {
return term.replace(/[%_\\]/g, '\\$&');
}
/**
* Build LIKE patterns for multiple case variants to improve non-ASCII matching
* in SQLite (default LIKE case-insensitive behavior is ASCII-only).
*/
function buildLikePatterns(term: string): string[] {
const variants = new Set<string>();
const titleCase =
term.length > 0
? term.charAt(0).toLocaleUpperCase() + term.slice(1).toLocaleLowerCase()
: term;
variants.add(term);
variants.add(term.toLocaleLowerCase());
variants.add(term.toLocaleUpperCase());
variants.add(titleCase);
return [...variants].map(value => `%${escapeLikePattern(value)}%`);
}
/**
* Check if content exists
*/
@@ -243,9 +262,11 @@ ipcMain.handle(
async (event, playlistId: string, searchTerm: string, types: string[]) => {
try {
const db = await getDatabase();
const searchTermLower = searchTerm.toLowerCase();
const escapedTerm = escapeLikePattern(searchTerm);
const likePattern = `%${escapedTerm}%`;
const searchTermLower = searchTerm.toLocaleLowerCase();
const likePatterns = buildLikePatterns(searchTerm);
const likeConditions = likePatterns.map(
pattern => sql`${schema.content.title} LIKE ${pattern} ESCAPE '\\'`
);
// Pre-filter with SQL LIKE (case-insensitive for ASCII, handled in native C code).
// This avoids loading the entire content table into JS memory.
@@ -272,7 +293,7 @@ ipcMain.handle(
schema.content.type,
types as Array<'live' | 'movie' | 'series'>
),
sql`${schema.content.title} LIKE ${likePattern}`
or(...likeConditions)
)
)
.limit(200);
@@ -280,7 +301,9 @@ ipcMain.handle(
// Post-filter for proper Unicode case-insensitive matching
// (SQLite LIKE is only case-insensitive for ASCII)
const result = candidates
.filter(item => item.title?.toLowerCase().includes(searchTermLower))
.filter(item =>
item.title?.toLocaleLowerCase().includes(searchTermLower)
)
.slice(0, 50);
return result;
@@ -299,9 +322,11 @@ ipcMain.handle(
async (event, searchTerm: string, types: string[]) => {
try {
const db = await getDatabase();
const searchTermLower = searchTerm.toLowerCase();
const escapedTerm = escapeLikePattern(searchTerm);
const likePattern = `%${escapedTerm}%`;
const searchTermLower = searchTerm.toLocaleLowerCase();
const likePatterns = buildLikePatterns(searchTerm);
const likeConditions = likePatterns.map(
pattern => sql`${schema.content.title} LIKE ${pattern} ESCAPE '\\'`
);
// Pre-filter with SQL LIKE (case-insensitive for ASCII, handled in native C code).
// This avoids loading the entire content table into JS memory.
@@ -335,7 +360,7 @@ ipcMain.handle(
schema.content.type,
types as Array<'live' | 'movie' | 'series'>
),
sql`${schema.content.title} LIKE ${likePattern}`
or(...likeConditions)
)
)
.orderBy(schema.content.title)
@@ -344,7 +369,9 @@ ipcMain.handle(
// Post-filter for proper Unicode case-insensitive matching
// (SQLite LIKE is only case-insensitive for ASCII)
const result = candidates
.filter(item => item.title?.toLowerCase().includes(searchTermLower))
.filter(item =>
item.title?.toLocaleLowerCase().includes(searchTermLower)
)
.slice(0, 50);
return result;