Store embed titles and search them
This commit is contained in:
+37
-1
@@ -291,6 +291,11 @@
|
||||
if (value === "" || !Number.isFinite(number) || number < 0) return "";
|
||||
return String(Math.round(number * (unit ? sizeUnit(unit) : 1)));
|
||||
};
|
||||
const title = state.filters.title.trim()
|
||||
? `title=${encodeURIComponent(state.filters.title.trim())}&${
|
||||
state.filters.titleExact ? "titleExact=1&" : ""
|
||||
}`
|
||||
: "";
|
||||
const sizeLt = state.filters.sizeLt !== "" ? `sizeLt=${amount(state.filters.sizeLt, state.filters.sizeLtUnit)}&` : "";
|
||||
const sizeGt = state.filters.sizeGt !== "" ? `sizeGt=${amount(state.filters.sizeGt, state.filters.sizeGtUnit)}&` : "";
|
||||
const lenLt = state.filters.lenLt !== "" ? `lenLt=${amount(state.filters.lenLt)}&` : "";
|
||||
@@ -301,7 +306,7 @@
|
||||
const before = state.filters.before ? `before=${state.filters.before}T23:59:59.999Z&` : "";
|
||||
return (
|
||||
`${kind}${source}${channel}${authors}${origin}${cached}${exts}${excludeHosts}` +
|
||||
`${sort}${excludeProviders}${excludeSources}${excludeExts}` +
|
||||
`${sort}${title}${excludeProviders}${excludeSources}${excludeExts}` +
|
||||
`${includeProviders}${includeSources}${sizeLt}${sizeGt}${lenLt}${lenGt}` +
|
||||
`${aspect}${resolution}${after}${before}`
|
||||
);
|
||||
@@ -1044,6 +1049,7 @@
|
||||
|
||||
function renderCard(item) {
|
||||
const figure = el("figure", "vk-media-figure");
|
||||
if (item.title) figure.title = item.title;
|
||||
const media = el("div", "vk-media-media");
|
||||
const imageSource = item.localUrl || item.thumbUrl;
|
||||
const fallbackSource = item.source !== "embed" ? item.url : null;
|
||||
@@ -1174,6 +1180,8 @@
|
||||
exts: [],
|
||||
excludeHosts: ["4chan.org", "i.4cdn.org"],
|
||||
sort: "newest",
|
||||
title: "",
|
||||
titleExact: false,
|
||||
excludeProviders: [],
|
||||
excludeSources: [],
|
||||
includeProviders: [],
|
||||
@@ -1204,6 +1212,8 @@
|
||||
excludeSources: [...state.filters.excludeSources],
|
||||
includeProviders: [...state.filters.includeProviders],
|
||||
includeSources: [...state.filters.includeSources],
|
||||
title: state.filters.title,
|
||||
titleExact: state.filters.titleExact,
|
||||
excludeExts: [...state.filters.excludeExts]
|
||||
};
|
||||
}
|
||||
@@ -1224,6 +1234,8 @@
|
||||
excludeSources: [...state.draft.excludeSources],
|
||||
includeProviders: [...state.draft.includeProviders],
|
||||
includeSources: [...state.draft.includeSources],
|
||||
title: state.draft.title,
|
||||
titleExact: state.draft.titleExact,
|
||||
excludeExts: [...state.draft.excludeExts],
|
||||
sizeLt: state.draft.sizeLt,
|
||||
sizeGt: state.draft.sizeGt,
|
||||
@@ -1382,6 +1394,30 @@
|
||||
draft.sort = value;
|
||||
}
|
||||
),
|
||||
(() => {
|
||||
const row = el("div", "vk-media-toolbar");
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.className = "vk-media-input";
|
||||
input.placeholder = "Video title (Ma Ca Ju finds Man ... car jumps)";
|
||||
input.value = draft.title;
|
||||
input.addEventListener("input", () => {
|
||||
draft.title = input.value;
|
||||
});
|
||||
row.append(input);
|
||||
|
||||
const exact = el("label", "vk-media-check");
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.checked = draft.titleExact;
|
||||
box.title = "Match the whole phrase instead of per-word prefixes";
|
||||
box.addEventListener("change", () => {
|
||||
draft.titleExact = box.checked;
|
||||
});
|
||||
exact.append(box, document.createTextNode(" Exact match (quotes)"));
|
||||
row.append(exact);
|
||||
return row;
|
||||
})(),
|
||||
filterSelect(
|
||||
[
|
||||
["", "Any ratio"],
|
||||
|
||||
@@ -904,6 +904,11 @@ function buildServer(): McpServer {
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Keep only these row sources (attachment, sticker, embed)"),
|
||||
titleQuery: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Search embed titles (YouTube). Multi-word prefix match by default"),
|
||||
titleExact: z.boolean().optional().describe("Treat titleQuery as one exact phrase"),
|
||||
sizeLessThan: z.number().optional().describe("Only items smaller than this many bytes"),
|
||||
sizeGreaterThan: z.number().optional().describe("Only items larger than this many bytes"),
|
||||
lengthLessThan: z.number().optional().describe("Only items shorter than this many seconds"),
|
||||
@@ -941,6 +946,8 @@ function buildServer(): McpServer {
|
||||
excludeSources,
|
||||
includeProviders,
|
||||
includeSources,
|
||||
titleQuery,
|
||||
titleExact,
|
||||
sizeLessThan,
|
||||
sizeGreaterThan,
|
||||
lengthLessThan,
|
||||
@@ -969,6 +976,8 @@ function buildServer(): McpServer {
|
||||
excludeSources,
|
||||
includeProviders,
|
||||
includeSources,
|
||||
titleQuery,
|
||||
titleExact,
|
||||
sizeLessThan,
|
||||
sizeGreaterThan,
|
||||
lengthLessThan,
|
||||
|
||||
@@ -135,6 +135,8 @@ export interface MediaItemResult {
|
||||
thumbUrl: string | null;
|
||||
provider: string | null;
|
||||
downloaded: boolean;
|
||||
/** Embed title (YouTube and friends), when Discord provided one. */
|
||||
title: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -719,6 +721,8 @@ export class MediaService {
|
||||
excludeSources?: string[];
|
||||
includeProviders?: string[];
|
||||
includeSources?: string[];
|
||||
titleQuery?: string;
|
||||
titleExact?: boolean;
|
||||
sizeLessThan?: number;
|
||||
sizeGreaterThan?: number;
|
||||
lengthLessThan?: number;
|
||||
@@ -850,6 +854,8 @@ export class MediaService {
|
||||
excludeSources?: string[];
|
||||
includeProviders?: string[];
|
||||
includeSources?: string[];
|
||||
titleQuery?: string;
|
||||
titleExact?: boolean;
|
||||
sizeLessThan?: number;
|
||||
sizeGreaterThan?: number;
|
||||
lengthLessThan?: number;
|
||||
@@ -887,6 +893,8 @@ export class MediaService {
|
||||
excludeSources: options.excludeSources,
|
||||
includeProviders: options.includeProviders,
|
||||
includeSources: options.includeSources,
|
||||
titleQuery: options.titleQuery,
|
||||
titleExact: options.titleExact,
|
||||
sizeLessThan: options.sizeLessThan,
|
||||
sizeGreaterThan: options.sizeGreaterThan,
|
||||
lengthLessThan: options.lengthLessThan,
|
||||
@@ -949,6 +957,8 @@ export class MediaService {
|
||||
excludeSources?: string[];
|
||||
includeProviders?: string[];
|
||||
includeSources?: string[];
|
||||
titleQuery?: string;
|
||||
titleExact?: boolean;
|
||||
sizeLessThan?: number;
|
||||
sizeGreaterThan?: number;
|
||||
lengthLessThan?: number;
|
||||
@@ -993,6 +1003,7 @@ export class MediaService {
|
||||
thumbUrl: row.preview_hash ? `/api/media/${meta.id}/${row.id}/thumb` : null,
|
||||
provider: row.provider,
|
||||
downloaded: row.downloaded === 1,
|
||||
title: row.title,
|
||||
createdAt: row.created_at
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -213,6 +213,8 @@ function parseMediaFilters(query: Record<string, unknown>) {
|
||||
excludeSources: list(query.excludeSources),
|
||||
includeProviders: list(query.includeProviders),
|
||||
includeSources: list(query.includeSources),
|
||||
titleQuery: query.title ? String(query.title).slice(0, 200) : undefined,
|
||||
titleExact: query.titleExact === "1" || query.titleExact === "true",
|
||||
sizeLessThan: amount(query.sizeLt),
|
||||
sizeGreaterThan: amount(query.sizeGt),
|
||||
lengthLessThan: amount(query.lenLt),
|
||||
|
||||
@@ -125,6 +125,15 @@ try {
|
||||
|
||||
await page.getByRole("button", { name: "Advanced search" }).click();
|
||||
await page.waitForSelector(".vk-media-drawer:not([hidden])", { timeout: 5000 });
|
||||
check(
|
||||
"title search field present",
|
||||
(await page.locator('.vk-media-drawer input[type="text"]').count()) >= 1
|
||||
);
|
||||
check(
|
||||
"exact-match toggle present",
|
||||
(await page.locator(".vk-media-drawer .vk-media-check", { hasText: "Exact match" }).count()) === 1
|
||||
);
|
||||
|
||||
const sourceBoxes = await page
|
||||
.locator(".vk-media-exclude-group")
|
||||
.first()
|
||||
|
||||
Reference in New Issue
Block a user