mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 07:21:32 -07:00
fix(stats): detect cover MIME type, fix ankiConfig reuse, skip retiming
- Cover routes detect JPEG/PNG/WebP from magic bytes instead of hardcoding image/jpeg - notesInfo builds previews with the same ankiConfig snapshot used for the fetch call - Skip alass retiming when secondary text is already present in the request body
This commit is contained in:
@@ -950,6 +950,26 @@ describe('stats server API routes', () => {
|
|||||||
assert.equal(res.headers.get('cache-control'), 'public, max-age=86400');
|
assert.equal(res.headers.get('cache-control'), 'public, max-age=86400');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('GET /api/stats/anime/:animeId/cover serves detected cover MIME type', async () => {
|
||||||
|
const app = createStatsApp(
|
||||||
|
createMockTracker({
|
||||||
|
getAnimeCoverArt: async () => ({
|
||||||
|
videoId: 1,
|
||||||
|
anilistId: 21858,
|
||||||
|
coverUrl: 'https://example.com/cover.png',
|
||||||
|
coverBlob: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||||
|
titleRomaji: 'Little Witch Academia',
|
||||||
|
titleEnglish: 'Little Witch Academia',
|
||||||
|
episodesTotal: 25,
|
||||||
|
fetchedAtMs: Date.now(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const res = await app.request('/api/stats/anime/1/cover');
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(res.headers.get('content-type'), 'image/png');
|
||||||
|
});
|
||||||
|
|
||||||
it('GET /api/stats/anime/:animeId/cover returns 404 for missing anime', async () => {
|
it('GET /api/stats/anime/:animeId/cover returns 404 for missing anime', async () => {
|
||||||
const app = createStatsApp(createMockTracker());
|
const app = createStatsApp(createMockTracker());
|
||||||
const res = await app.request('/api/stats/anime/99999/cover');
|
const res = await app.request('/api/stats/anime/99999/cover');
|
||||||
@@ -1371,8 +1391,12 @@ describe('stats server API routes', () => {
|
|||||||
fs.writeFileSync(sourcePath, 'fake media');
|
fs.writeFileSync(sourcePath, 'fake media');
|
||||||
|
|
||||||
await withFakeAnkiConnect(async (requests, url) => {
|
await withFakeAnkiConnect(async (requests, url) => {
|
||||||
|
let retimedCalls = 0;
|
||||||
const options = {
|
const options = {
|
||||||
resolveRetimedSecondarySubtitleText: async () => 'Aligned English subtitle',
|
resolveRetimedSecondarySubtitleText: async () => {
|
||||||
|
retimedCalls += 1;
|
||||||
|
return 'Aligned English subtitle';
|
||||||
|
},
|
||||||
ankiConnectConfig: {
|
ankiConnectConfig: {
|
||||||
url,
|
url,
|
||||||
deck: 'Mining',
|
deck: 'Mining',
|
||||||
@@ -1416,6 +1440,7 @@ describe('stats server API routes', () => {
|
|||||||
addNoteRequest?.params?.note?.fields?.SelectionText,
|
addNoteRequest?.params?.note?.fields?.SelectionText,
|
||||||
'Stale stored English subtitle',
|
'Stale stored English subtitle',
|
||||||
);
|
);
|
||||||
|
assert.equal(retimedCalls, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2634,6 +2659,52 @@ Aligned English subtitle
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('POST /api/stats/anki/notesInfo builds previews with the same config used for fetch', async () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = (async () =>
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
noteId: 444,
|
||||||
|
fields: {
|
||||||
|
TargetWord: { value: '一致' },
|
||||||
|
OtherWord: { value: '不一致' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
)) as typeof fetch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let configCalls = 0;
|
||||||
|
const app = createStatsApp(createMockTracker(), {
|
||||||
|
getAnkiConnectConfig: () => {
|
||||||
|
configCalls += 1;
|
||||||
|
return {
|
||||||
|
url: 'http://127.0.0.1:8765',
|
||||||
|
fields: { word: configCalls === 1 ? 'TargetWord' : 'OtherWord' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await app.request('/api/stats/anki/notesInfo', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ noteIds: [444] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(configCalls, 1);
|
||||||
|
assert.equal((await res.json())[0].preview.word, '一致');
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('serves stats index and asset files from absolute static dir paths', async () => {
|
it('serves stats index and asset files from absolute static dir paths', async () => {
|
||||||
await withTempDir(async (dir) => {
|
await withTempDir(async (dir) => {
|
||||||
const assetDir = path.join(dir, 'assets');
|
const assetDir = path.join(dir, 'assets');
|
||||||
@@ -2667,7 +2738,9 @@ Aligned English subtitle
|
|||||||
videoId: 1,
|
videoId: 1,
|
||||||
anilistId: 1,
|
anilistId: 1,
|
||||||
coverUrl: 'https://example.com/cover.jpg',
|
coverUrl: 'https://example.com/cover.jpg',
|
||||||
coverBlob: Buffer.from([0xff, 0xd8, 0xff, 0xd9]),
|
coverBlob: Buffer.from([
|
||||||
|
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
|
||||||
|
]),
|
||||||
titleRomaji: 'Test',
|
titleRomaji: 'Test',
|
||||||
titleEnglish: 'Test',
|
titleEnglish: 'Test',
|
||||||
episodesTotal: 12,
|
episodesTotal: 12,
|
||||||
@@ -2684,7 +2757,7 @@ Aligned English subtitle
|
|||||||
|
|
||||||
const res = await app.request('/api/stats/media/1/cover');
|
const res = await app.request('/api/stats/media/1/cover');
|
||||||
assert.equal(res.status, 200);
|
assert.equal(res.status, 200);
|
||||||
assert.equal(res.headers.get('content-type'), 'image/jpeg');
|
assert.equal(res.headers.get('content-type'), 'image/webp');
|
||||||
assert.equal(ensureCalls, 1);
|
assert.equal(ensureCalls, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1013,9 +1013,10 @@ export function createStatsApp(
|
|||||||
if (animeId <= 0) return c.body(null, 404);
|
if (animeId <= 0) return c.body(null, 404);
|
||||||
const art = await tracker.getAnimeCoverArt(animeId);
|
const art = await tracker.getAnimeCoverArt(animeId);
|
||||||
if (!art?.coverBlob) return c.body(null, 404);
|
if (!art?.coverBlob) return c.body(null, 404);
|
||||||
return new Response(new Uint8Array(art.coverBlob), {
|
const bytes = new Uint8Array(art.coverBlob);
|
||||||
|
return new Response(bytes, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/jpeg',
|
'Content-Type': detectImageContentType(bytes),
|
||||||
'Cache-Control': 'public, max-age=86400',
|
'Cache-Control': 'public, max-age=86400',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1030,9 +1031,10 @@ export function createStatsApp(
|
|||||||
art = await tracker.getCoverArt(videoId);
|
art = await tracker.getCoverArt(videoId);
|
||||||
}
|
}
|
||||||
if (!art?.coverBlob) return c.body(null, 404);
|
if (!art?.coverBlob) return c.body(null, 404);
|
||||||
return new Response(new Uint8Array(art.coverBlob), {
|
const bytes = new Uint8Array(art.coverBlob);
|
||||||
|
return new Response(bytes, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'image/jpeg',
|
'Content-Type': detectImageContentType(bytes),
|
||||||
'Cache-Control': 'public, max-age=604800',
|
'Cache-Control': 'public, max-age=604800',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1110,7 +1112,7 @@ export function createStatsApp(
|
|||||||
return c.json(
|
return c.json(
|
||||||
(result.result ?? []).map((note) => ({
|
(result.result ?? []).map((note) => ({
|
||||||
...note,
|
...note,
|
||||||
preview: buildAnkiNotePreview(note.fields, getAnkiConnectConfig()),
|
preview: buildAnkiNotePreview(note.fields, ankiConfig),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1148,7 +1150,7 @@ export function createStatsApp(
|
|||||||
}
|
}
|
||||||
const secondarySubtitleLanguages = getSecondarySubtitleLanguages();
|
const secondarySubtitleLanguages = getSecondarySubtitleLanguages();
|
||||||
let retimedSecondaryText = '';
|
let retimedSecondaryText = '';
|
||||||
if (mode === 'sentence') {
|
if (mode === 'sentence' && !bodySecondaryText) {
|
||||||
try {
|
try {
|
||||||
retimedSecondaryText = await (
|
retimedSecondaryText = await (
|
||||||
options?.resolveRetimedSecondarySubtitleText ??
|
options?.resolveRetimedSecondarySubtitleText ??
|
||||||
|
|||||||
Reference in New Issue
Block a user