mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 07:21:32 -07:00
feat: show mining frame toast on OSD and speed up stats session deletes
- Flash mined-frame screenshot as in-overlay image toast for OSD card notifications - Batch cover art from stored DB blobs via POST /api/stats/covers (no extra AniList fetches) - Show delete progress toast on stats home and sessions pages - Refresh only affected rollups on session delete instead of full rebuild - Rebuild lifetime summaries with aggregate SQL CTEs instead of per-session loop
This commit is contained in:
@@ -27,6 +27,16 @@ type StatsExcludedWordPayload = {
|
||||
reading: string;
|
||||
};
|
||||
|
||||
type StatsCoverImagePayload = {
|
||||
contentType: 'image/jpeg';
|
||||
dataUrl: string;
|
||||
} | null;
|
||||
|
||||
type StatsCoverBatchBody = {
|
||||
animeIds?: unknown;
|
||||
videoIds?: unknown;
|
||||
};
|
||||
|
||||
function parseIntQuery(raw: string | undefined, fallback: number, maxLimit?: number): number {
|
||||
if (raw === undefined) return fallback;
|
||||
const n = Number(raw);
|
||||
@@ -73,6 +83,31 @@ function parseExcludedWordsBody(body: unknown): StatsExcludedWordPayload[] | nul
|
||||
return words;
|
||||
}
|
||||
|
||||
function parsePositiveIdList(raw: unknown, maxItems = 100): number[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
const ids = new Set<number>();
|
||||
for (const rawId of raw) {
|
||||
const id = typeof rawId === 'number' ? rawId : typeof rawId === 'string' ? Number(rawId) : NaN;
|
||||
if (Number.isFinite(id) && id > 0) {
|
||||
ids.add(Math.floor(id));
|
||||
if (ids.size >= maxItems) break;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function coverImagePayload(
|
||||
art: { coverBlob?: Uint8Array | null } | null | undefined,
|
||||
): StatsCoverImagePayload {
|
||||
if (!art?.coverBlob) return null;
|
||||
return {
|
||||
contentType: 'image/jpeg',
|
||||
dataUrl: `data:image/jpeg;base64,${Buffer.from(art.coverBlob).toString('base64')}`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveStatsNoteFieldName(
|
||||
noteInfo: StatsServerNoteInfo,
|
||||
...preferredNames: (string | undefined)[]
|
||||
@@ -707,6 +742,27 @@ export function createStatsApp(
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.post('/api/stats/covers', async (c) => {
|
||||
const body = (await c.req.json().catch(() => null)) as StatsCoverBatchBody | null;
|
||||
const animeIds = parsePositiveIdList(body?.animeIds);
|
||||
const videoIds = parsePositiveIdList(body?.videoIds);
|
||||
const anime: Record<string, StatsCoverImagePayload> = {};
|
||||
const media: Record<string, StatsCoverImagePayload> = {};
|
||||
|
||||
await Promise.all(
|
||||
animeIds.map(async (animeId) => {
|
||||
anime[String(animeId)] = coverImagePayload(await tracker.getAnimeCoverArt(animeId));
|
||||
}),
|
||||
);
|
||||
await Promise.all(
|
||||
videoIds.map(async (videoId) => {
|
||||
media[String(videoId)] = coverImagePayload(await tracker.getCoverArt(videoId));
|
||||
}),
|
||||
);
|
||||
|
||||
return c.json({ anime, media });
|
||||
});
|
||||
|
||||
app.get('/api/stats/anime/:animeId/cover', async (c) => {
|
||||
const animeId = parseIntQuery(c.req.param('animeId'), 0);
|
||||
if (animeId <= 0) return c.body(null, 404);
|
||||
|
||||
Reference in New Issue
Block a user