Support wildcards in the title search
This commit is contained in:
@@ -187,6 +187,20 @@ export function normalizeTitleKey(title: string): string {
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
/** Lowercase a title query and collapse punctuation, but keep `*` as a wildcard marker. */
|
||||
export function normalizeTitleQuery(query: string): string {
|
||||
return query
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9*]+/g, " ")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
/** Escape LIKE metacharacters, then turn `*` into `%`. */
|
||||
function titleLikePattern(value: string): string {
|
||||
return value.replace(/[\\%_]/g, match => `\\${match}`).replace(/\*/g, "%");
|
||||
}
|
||||
|
||||
const SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -803,14 +817,22 @@ export class ScrapeStore {
|
||||
values.push(options.lengthGreaterThan);
|
||||
}
|
||||
if (options.titleQuery?.trim()) {
|
||||
const query = normalizeTitleKey(options.titleQuery);
|
||||
const query = normalizeTitleQuery(options.titleQuery);
|
||||
if (options.titleExact) {
|
||||
conditions.push("title_key LIKE ?");
|
||||
values.push(`%${query}%`);
|
||||
// One phrase, substring of the title; `*` still acts as a wildcard.
|
||||
conditions.push("title_key LIKE ? ESCAPE '\\'");
|
||||
values.push(`%${titleLikePattern(query)}%`);
|
||||
} else {
|
||||
for (const token of query.split(" ").filter(Boolean)) {
|
||||
conditions.push("(' ' || COALESCE(title_key, '')) LIKE ?");
|
||||
values.push(`% ${token}%`);
|
||||
const pattern = titleLikePattern(token);
|
||||
if (token.startsWith("*")) {
|
||||
// Leading `*`: anywhere in a word, not just the start.
|
||||
conditions.push("COALESCE(title_key, '') LIKE ? ESCAPE '\\'");
|
||||
values.push(`%${pattern}%`);
|
||||
} else {
|
||||
conditions.push("(' ' || COALESCE(title_key, '')) LIKE ? ESCAPE '\\'");
|
||||
values.push(`% ${pattern}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
dirSizeBytes,
|
||||
listScrapes,
|
||||
normalizeTitleKey,
|
||||
normalizeTitleQuery,
|
||||
readScrapeMeta,
|
||||
ScrapeStore,
|
||||
writeScrapeMeta
|
||||
@@ -1619,6 +1620,56 @@ void (async () => {
|
||||
store.close();
|
||||
});
|
||||
|
||||
await test("title wildcards match anywhere and stay literal for % and _", () => {
|
||||
const dir = join(root, "title-wildcards");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const store = new ScrapeStore(dir);
|
||||
const embed = (id: string, title: string) => ({
|
||||
type: "video",
|
||||
url: `https://www.youtube.com/watch?v=${id}`,
|
||||
title,
|
||||
provider: "YouTube",
|
||||
videoUrl: `https://www.youtube.com/watch?v=${id}`,
|
||||
imageUrl: null,
|
||||
thumbnailUrl: null
|
||||
});
|
||||
|
||||
store.insertPage([
|
||||
message("1300", {
|
||||
embeds: [
|
||||
embed("w1", "Scar Tissue"),
|
||||
embed("w2", "Discard the rest"),
|
||||
embed("w3", "Car wash"),
|
||||
embed("w4", "Cartoon physics"),
|
||||
embed("w5", "Carly and the r")
|
||||
]
|
||||
})
|
||||
]);
|
||||
|
||||
const ids = (options: Record<string, unknown>) =>
|
||||
store
|
||||
.queryMedia({ limit: 50, ...options })
|
||||
.map(row => row.url.split("v=")[1])
|
||||
.sort();
|
||||
|
||||
// Word-prefix default: car starts a word.
|
||||
assert.deepEqual(ids({ titleQuery: "car" }), ["w3", "w4", "w5"]);
|
||||
// Leading *: anywhere, so scar and discard join in.
|
||||
assert.deepEqual(ids({ titleQuery: "*car" }), ["w1", "w2", "w3", "w4", "w5"]);
|
||||
// Trailing * behaves like the prefix form.
|
||||
assert.deepEqual(ids({ titleQuery: "car*" }), ["w3", "w4", "w5"]);
|
||||
// In-word wildcard after a word start.
|
||||
assert.deepEqual(ids({ titleQuery: "c*r" }), ["w3", "w4", "w5"]);
|
||||
assert.deepEqual(ids({ titleQuery: "c*r*" }), ["w3", "w4", "w5"]);
|
||||
assert.deepEqual(ids({ titleQuery: "*c*r*" }), ["w1", "w2", "w3", "w4", "w5"]);
|
||||
store.close();
|
||||
});
|
||||
|
||||
await test("normalizeTitleQuery keeps stars and drops punctuation", () => {
|
||||
assert.equal(normalizeTitleQuery(" *CaR, C*r* "), "*car c*r*");
|
||||
assert.equal(normalizeTitleQuery("Boat"), "boat");
|
||||
});
|
||||
|
||||
await test("normalizeTitleKey collapses punctuation and case", () => {
|
||||
assert.equal(normalizeTitleKey("Woman's Day: 3 Things!"), "woman s day 3 things");
|
||||
assert.equal(normalizeTitleKey(" A---B "), "a b");
|
||||
|
||||
Reference in New Issue
Block a user