Add links-mode embed thumbnails, item metadata, and media filters
This commit is contained in:
@@ -829,12 +829,37 @@ function buildServer(): McpServer {
|
||||
.describe("Where the item came from"),
|
||||
scrapeId: z.string().optional().describe("Restrict to one scrape"),
|
||||
channelId: z.string().optional().describe("Restrict to one source channel"),
|
||||
authorId: z.string().optional().describe("Restrict to one poster"),
|
||||
aspect: z
|
||||
.enum(["vertical", "square", "wide43", "wide169", "ultrawide", "other"])
|
||||
.optional()
|
||||
.describe("Aspect-ratio bucket"),
|
||||
resolution: z
|
||||
.enum(["sd", "hd", "fhd", "qhd", "uhd", "unknown"])
|
||||
.optional()
|
||||
.describe("Resolution bucket by shortest side"),
|
||||
after: z.string().optional().describe("Only items posted at or after this ISO time"),
|
||||
before: z.string().optional().describe("Only items posted at or before this ISO time"),
|
||||
limit: z.number().int().min(1).max(500).optional(),
|
||||
offset: z.number().int().min(0).optional()
|
||||
}
|
||||
},
|
||||
async ({ kind, source, scrapeId, channelId, limit, offset }) =>
|
||||
jsonText(media.queryMedia({ kind, source, scrapeId, channelId, limit, offset }))
|
||||
async ({ kind, source, scrapeId, channelId, authorId, aspect, resolution, after, before, limit, offset }) =>
|
||||
jsonText(
|
||||
media.queryMedia({
|
||||
kind,
|
||||
source,
|
||||
scrapeId,
|
||||
channelId,
|
||||
authorId,
|
||||
aspect,
|
||||
resolution,
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
|
||||
@@ -93,6 +93,12 @@ export interface MediaItemResult {
|
||||
channelId: string;
|
||||
kind: MediaKind;
|
||||
source: string | null;
|
||||
authorId: string | null;
|
||||
authorName: string | null;
|
||||
postedAt: string | null;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
aspectRatio: number | null;
|
||||
url: string;
|
||||
localUrl: string | null;
|
||||
thumbUrl: string | null;
|
||||
@@ -337,6 +343,29 @@ export class MediaService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Distinct posters across every scrape, biggest first. */
|
||||
listAuthors(): Array<{ id: string | null; name: string | null; count: number }> {
|
||||
const totals = new Map<string, { id: string | null; name: string | null; count: number }>();
|
||||
|
||||
for (const meta of this.engine.list()) {
|
||||
if (meta.state === "deleted") continue;
|
||||
|
||||
const store = new ScrapeStore(scrapeDir(this.rootDir, meta.id));
|
||||
try {
|
||||
for (const author of store.authors()) {
|
||||
const key = author.id ?? author.name ?? "unknown";
|
||||
const current = totals.get(key);
|
||||
if (current) current.count += author.count;
|
||||
else totals.set(key, { ...author });
|
||||
}
|
||||
} finally {
|
||||
store.close();
|
||||
}
|
||||
}
|
||||
|
||||
return [...totals.values()].sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
/** One media row inside a scrape, with its scrape and channel for context. */
|
||||
getMediaItem(scrapeId: string, itemId: number): (MediaRow & { scrapeId: string; channelId: string }) | null {
|
||||
const meta = this.engine.get(scrapeId);
|
||||
@@ -358,6 +387,11 @@ export class MediaService {
|
||||
source?: string;
|
||||
scrapeId?: string;
|
||||
channelId?: string;
|
||||
authorId?: string;
|
||||
aspect?: string;
|
||||
resolution?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {}
|
||||
@@ -377,6 +411,11 @@ export class MediaService {
|
||||
const rows = store.queryMedia({
|
||||
kind: options.kind,
|
||||
source: options.source,
|
||||
authorId: options.authorId,
|
||||
aspect: options.aspect,
|
||||
resolution: options.resolution,
|
||||
after: options.after,
|
||||
before: options.before,
|
||||
limit: limit + offset
|
||||
});
|
||||
for (const row of rows) {
|
||||
@@ -388,6 +427,12 @@ export class MediaService {
|
||||
channelId: meta.channelId,
|
||||
kind: row.kind,
|
||||
source: row.source,
|
||||
authorId: row.author_id,
|
||||
authorName: row.author_name,
|
||||
postedAt: row.posted_at,
|
||||
width: row.width,
|
||||
height: row.height,
|
||||
aspectRatio: row.aspect_ratio,
|
||||
url: row.url,
|
||||
localUrl: row.file_path ? `/api/media/${meta.id}/${row.id}/file` : null,
|
||||
thumbUrl: row.preview_hash ? `/api/media/${meta.id}/${row.id}/thumb` : null,
|
||||
|
||||
+27
-1
@@ -799,10 +799,36 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
|
||||
? String(req.query.source)
|
||||
: undefined;
|
||||
const channelId = req.query.channelId ? String(req.query.channelId) : undefined;
|
||||
const authorId = req.query.authorId ? String(req.query.authorId) : undefined;
|
||||
const aspect = req.query.aspect ? String(req.query.aspect) : undefined;
|
||||
const resolution = req.query.resolution ? String(req.query.resolution) : undefined;
|
||||
const after = req.query.after ? String(req.query.after) : undefined;
|
||||
const before = req.query.before ? String(req.query.before) : undefined;
|
||||
const limit = clampLimit(req.query.limit, 60, 500);
|
||||
const offset = Math.max(0, Number(req.query.offset ?? 0) || 0);
|
||||
|
||||
res.json({ ok: true, result: deps.media.queryMedia({ kind, source, channelId, limit, offset }) });
|
||||
res.json({
|
||||
ok: true,
|
||||
result: deps.media.queryMedia({
|
||||
kind,
|
||||
source,
|
||||
channelId,
|
||||
authorId,
|
||||
aspect,
|
||||
resolution,
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/media/authors",
|
||||
wrap(async (_req, res) => {
|
||||
res.json({ ok: true, result: deps.media.listAuthors() });
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user