Store embed titles and search them

This commit is contained in:
Fisto
2026-09-17 13:10:16 +02:00
parent c6caa168be
commit 557ba5ca7e
13 changed files with 200 additions and 10 deletions
+37 -1
View File
@@ -136,6 +136,9 @@ export interface MediaFilterOptions {
limit?: number;
offset?: number;
sort?: MediaSort;
/** Title search: multi-word prefix tokens by default, a phrase when `titleExact`. */
titleQuery?: string;
titleExact?: boolean;
}
export interface MediaRow {
@@ -166,9 +169,22 @@ export interface MediaRow {
download_error: string | null;
preview_error: string | null;
preview_attempts: number;
/** Embed title from Discord (YouTube and friends). */
title: string | null;
/** Lowercased, punctuation-collapsed title for token-prefix search. */
title_key: string | null;
created_at: string;
}
/** Normalize a title for prefix-token search: lowercase, punctuation to spaces, collapsed. */
export function normalizeTitleKey(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, " ")
.trim()
.replace(/\s+/g, " ");
}
const SCHEMA = `
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
@@ -206,6 +222,8 @@ CREATE TABLE IF NOT EXISTS media (
download_error TEXT,
preview_error TEXT,
preview_attempts INTEGER NOT NULL DEFAULT 0,
title TEXT,
title_key TEXT,
created_at TEXT NOT NULL
);
@@ -251,6 +269,10 @@ export class ScrapeStore {
if (!names.has("preview_attempts")) {
this.db.exec("ALTER TABLE media ADD COLUMN preview_attempts INTEGER NOT NULL DEFAULT 0");
}
if (!names.has("title")) {
this.db.exec("ALTER TABLE media ADD COLUMN title TEXT");
this.db.exec("ALTER TABLE media ADD COLUMN title_key TEXT");
}
}
close(): void {
@@ -264,7 +286,7 @@ export class ScrapeStore {
"INSERT OR REPLACE INTO messages (id, author_id, author_name, timestamp, content) VALUES (?, ?, ?, ?, ?)"
);
const insertMedia = this.db.prepare(
"INSERT OR IGNORE INTO media (kind, source, author_id, author_name, posted_at, url, play_url, provider, content_type, size, width, height, aspect_ratio, duration_secs, downloaded, preview_url, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
"INSERT OR IGNORE INTO media (kind, source, author_id, author_name, posted_at, url, play_url, provider, content_type, size, width, height, aspect_ratio, duration_secs, downloaded, preview_url, title, title_key, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
const findMedia = this.db.prepare("SELECT id FROM media WHERE url = ?");
const linkSource = this.db.prepare(
@@ -301,6 +323,8 @@ export class ScrapeStore {
item.durationSecs,
item.downloaded ? 1 : 0,
item.previewUrl,
item.title,
item.title ? normalizeTitleKey(item.title) : null,
createdAt
);
if (result.changes > 0) added[item.kind]++;
@@ -640,6 +664,18 @@ export class ScrapeStore {
conditions.push("duration_secs > ?");
values.push(options.lengthGreaterThan);
}
if (options.titleQuery?.trim()) {
const query = normalizeTitleKey(options.titleQuery);
if (options.titleExact) {
conditions.push("title_key LIKE ?");
values.push(`%${query}%`);
} else {
for (const token of query.split(" ").filter(Boolean)) {
conditions.push("(' ' || COALESCE(title_key, '')) LIKE ?");
values.push(`% ${token}%`);
}
}
}
if (options.after) {
conditions.push("posted_at >= ?");
values.push(options.after);