mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-06 19:57:26 -08:00
Deduplicate voice actor image downloads per AniList person ID
- Avoid repeated downloads when multiple characters share the same voice actor - Add coverage for shared voice actor image download behavior
This commit is contained in:
@@ -160,8 +160,19 @@ test('generateForCurrentMedia emits structured-content glossary so image stays w
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await runtime.generateForCurrentMedia();
|
const result = await runtime.generateForCurrentMedia();
|
||||||
const termBank = JSON.parse(readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8')) as Array<
|
const termBank = JSON.parse(
|
||||||
[string, string, string, string, number, Array<string | Record<string, unknown>>, number, string]
|
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
|
||||||
|
) as Array<
|
||||||
|
[
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
Array<string | Record<string, unknown>>,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
]
|
||||||
>;
|
>;
|
||||||
const alexia = termBank.find(([term]) => term === 'アレクシア');
|
const alexia = termBank.find(([term]) => term === 'アレクシア');
|
||||||
|
|
||||||
@@ -205,8 +216,10 @@ test('generateForCurrentMedia emits structured-content glossary so image stays w
|
|||||||
assert.equal(badge.content, 'Side Character');
|
assert.equal(badge.content, 'Side Character');
|
||||||
|
|
||||||
const descSection = children.find(
|
const descSection = children.find(
|
||||||
(c) => (c as { tag?: string }).tag === 'details' && Array.isArray((c as { content?: unknown[] }).content) &&
|
(c) =>
|
||||||
((c as { content: Array<{ content?: string }> }).content[0]?.content === 'Description'),
|
(c as { tag?: string }).tag === 'details' &&
|
||||||
|
Array.isArray((c as { content?: unknown[] }).content) &&
|
||||||
|
(c as { content: Array<{ content?: string }> }).content[0]?.content === 'Description',
|
||||||
) as { tag: string; content: Array<Record<string, unknown>> } | undefined;
|
) as { tag: string; content: Array<Record<string, unknown>> } | undefined;
|
||||||
assert.ok(descSection, 'expected Description collapsible section');
|
assert.ok(descSection, 'expected Description collapsible section');
|
||||||
const descBody = descSection.content[1] as { content: string };
|
const descBody = descSection.content[1] as { content: string };
|
||||||
@@ -215,13 +228,20 @@ test('generateForCurrentMedia emits structured-content glossary so image stays w
|
|||||||
);
|
);
|
||||||
|
|
||||||
const infoSection = children.find(
|
const infoSection = children.find(
|
||||||
(c) => (c as { tag?: string }).tag === 'details' && Array.isArray((c as { content?: unknown[] }).content) &&
|
(c) =>
|
||||||
((c as { content: Array<{ content?: string }> }).content[0]?.content === 'Character Information'),
|
(c as { tag?: string }).tag === 'details' &&
|
||||||
|
Array.isArray((c as { content?: unknown[] }).content) &&
|
||||||
|
(c as { content: Array<{ content?: string }> }).content[0]?.content ===
|
||||||
|
'Character Information',
|
||||||
) as { tag: string; content: Array<Record<string, unknown>> } | undefined;
|
) as { tag: string; content: Array<Record<string, unknown>> } | undefined;
|
||||||
assert.ok(infoSection, 'expected Character Information collapsible section with parsed __Race:__ field');
|
assert.ok(
|
||||||
|
infoSection,
|
||||||
|
'expected Character Information collapsible section with parsed __Race:__ field',
|
||||||
|
);
|
||||||
|
|
||||||
const topLevelImageGlossaryEntry = glossary.find(
|
const topLevelImageGlossaryEntry = glossary.find(
|
||||||
(item) => typeof item === 'object' && item !== null && (item as { type?: string }).type === 'image',
|
(item) =>
|
||||||
|
typeof item === 'object' && item !== null && (item as { type?: string }).type === 'image',
|
||||||
);
|
);
|
||||||
assert.equal(topLevelImageGlossaryEntry, undefined);
|
assert.equal(topLevelImageGlossaryEntry, undefined);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -322,8 +342,19 @@ test('generateForCurrentMedia adds kana aliases for romanized names when native
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await runtime.generateForCurrentMedia();
|
const result = await runtime.generateForCurrentMedia();
|
||||||
const termBank = JSON.parse(readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8')) as Array<
|
const termBank = JSON.parse(
|
||||||
[string, string, string, string, number, Array<string | Record<string, unknown>>, number, string]
|
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
|
||||||
|
) as Array<
|
||||||
|
[
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
Array<string | Record<string, unknown>>,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const kazuma = termBank.find(([term]) => term === 'カズマ');
|
const kazuma = termBank.find(([term]) => term === 'カズマ');
|
||||||
@@ -466,7 +497,16 @@ test('getOrCreateCurrentSnapshot persists and reuses normalized snapshot data',
|
|||||||
mediaId: number;
|
mediaId: number;
|
||||||
entryCount: number;
|
entryCount: number;
|
||||||
termEntries: Array<
|
termEntries: Array<
|
||||||
[string, string, string, string, number, Array<string | Record<string, unknown>>, number, string]
|
[
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
Array<string | Record<string, unknown>>,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
]
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
assert.equal(snapshot.mediaId, 130298);
|
assert.equal(snapshot.mediaId, 130298);
|
||||||
@@ -600,12 +640,27 @@ test('getOrCreateCurrentSnapshot rebuilds snapshots written with an older format
|
|||||||
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) as {
|
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) as {
|
||||||
formatVersion: number;
|
formatVersion: number;
|
||||||
termEntries: Array<
|
termEntries: Array<
|
||||||
[string, string, string, string, number, Array<string | Record<string, unknown>>, number, string]
|
[
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
Array<string | Record<string, unknown>>,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
]
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
assert.equal(snapshot.formatVersion > 9, true);
|
assert.equal(snapshot.formatVersion > 9, true);
|
||||||
assert.equal(snapshot.termEntries.some(([term]) => term === 'アルファ'), true);
|
assert.equal(
|
||||||
assert.equal(snapshot.termEntries.some(([term]) => term === 'stale'), false);
|
snapshot.termEntries.some(([term]) => term === 'アルファ'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
snapshot.termEntries.some(([term]) => term === 'stale'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
@@ -737,6 +792,168 @@ test('generateForCurrentMedia logs progress while resolving and rebuilding snaps
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('generateForCurrentMedia downloads shared voice actor images once per AniList person id', async () => {
|
||||||
|
const userDataPath = makeTempDir();
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
const fetchedImageUrls: string[] = [];
|
||||||
|
|
||||||
|
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||||
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
||||||
|
if (url === GRAPHQL_URL) {
|
||||||
|
const body = JSON.parse(String(init?.body ?? '{}')) as {
|
||||||
|
query?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.query?.includes('Page(perPage: 10)')) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
data: {
|
||||||
|
Page: {
|
||||||
|
media: [
|
||||||
|
{
|
||||||
|
id: 130298,
|
||||||
|
episodes: 20,
|
||||||
|
title: {
|
||||||
|
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
|
||||||
|
english: 'The Eminence in Shadow',
|
||||||
|
native: '陰の実力者になりたくて!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.query?.includes('characters(page: $page')) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
data: {
|
||||||
|
Media: {
|
||||||
|
title: {
|
||||||
|
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
|
||||||
|
english: 'The Eminence in Shadow',
|
||||||
|
native: '陰の実力者になりたくて!',
|
||||||
|
},
|
||||||
|
characters: {
|
||||||
|
pageInfo: { hasNextPage: false },
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
role: 'MAIN',
|
||||||
|
voiceActors: [
|
||||||
|
{
|
||||||
|
id: 9001,
|
||||||
|
name: {
|
||||||
|
full: 'Kana Hanazawa',
|
||||||
|
native: '花澤香菜',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
large: null,
|
||||||
|
medium: 'https://example.com/kana.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
node: {
|
||||||
|
id: 321,
|
||||||
|
description: 'Alpha is the second-in-command of Shadow Garden.',
|
||||||
|
image: {
|
||||||
|
large: 'https://example.com/alpha.png',
|
||||||
|
medium: null,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
full: 'Alpha',
|
||||||
|
native: 'アルファ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'SUPPORTING',
|
||||||
|
voiceActors: [
|
||||||
|
{
|
||||||
|
id: 9001,
|
||||||
|
name: {
|
||||||
|
full: 'Kana Hanazawa',
|
||||||
|
native: '花澤香菜',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
large: null,
|
||||||
|
medium: 'https://example.com/kana.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
node: {
|
||||||
|
id: 654,
|
||||||
|
description: 'Beta documents Shadow Garden operations.',
|
||||||
|
image: {
|
||||||
|
large: 'https://example.com/beta.png',
|
||||||
|
medium: null,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
full: 'Beta',
|
||||||
|
native: 'ベータ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
url === 'https://example.com/alpha.png' ||
|
||||||
|
url === 'https://example.com/beta.png' ||
|
||||||
|
url === 'https://example.com/kana.png'
|
||||||
|
) {
|
||||||
|
fetchedImageUrls.push(url);
|
||||||
|
return new Response(PNG_1X1, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': 'image/png' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected fetch URL: ${url}`);
|
||||||
|
}) as typeof globalThis.fetch;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const runtime = createCharacterDictionaryRuntimeService({
|
||||||
|
userDataPath,
|
||||||
|
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
|
||||||
|
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
|
||||||
|
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
|
||||||
|
guessAnilistMediaInfo: async () => ({
|
||||||
|
title: 'The Eminence in Shadow',
|
||||||
|
episode: 5,
|
||||||
|
source: 'fallback',
|
||||||
|
}),
|
||||||
|
now: () => 1_700_000_000_100,
|
||||||
|
sleep: async () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.generateForCurrentMedia();
|
||||||
|
|
||||||
|
assert.deepEqual(fetchedImageUrls, [
|
||||||
|
'https://example.com/alpha.png',
|
||||||
|
'https://example.com/kana.png',
|
||||||
|
'https://example.com/beta.png',
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('buildMergedDictionary combines stored snapshots into one stable dictionary', async () => {
|
test('buildMergedDictionary combines stored snapshots into one stable dictionary', async () => {
|
||||||
const userDataPath = makeTempDir();
|
const userDataPath = makeTempDir();
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
@@ -913,8 +1130,19 @@ test('buildMergedDictionary combines stored snapshots into one stable dictionary
|
|||||||
const index = JSON.parse(readStoredZipEntry(merged.zipPath, 'index.json').toString('utf8')) as {
|
const index = JSON.parse(readStoredZipEntry(merged.zipPath, 'index.json').toString('utf8')) as {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
const termBank = JSON.parse(readStoredZipEntry(merged.zipPath, 'term_bank_1.json').toString('utf8')) as Array<
|
const termBank = JSON.parse(
|
||||||
[string, string, string, string, number, Array<string | Record<string, unknown>>, number, string]
|
readStoredZipEntry(merged.zipPath, 'term_bank_1.json').toString('utf8'),
|
||||||
|
) as Array<
|
||||||
|
[
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
number,
|
||||||
|
Array<string | Record<string, unknown>>,
|
||||||
|
number,
|
||||||
|
string,
|
||||||
|
]
|
||||||
>;
|
>;
|
||||||
const frieren = termBank.find(([term]) => term === 'フリーレン');
|
const frieren = termBank.find(([term]) => term === 'フリーレン');
|
||||||
const alpha = termBank.find(([term]) => term === 'アルファ');
|
const alpha = termBank.find(([term]) => term === 'アルファ');
|
||||||
@@ -1064,7 +1292,10 @@ test('generateForCurrentMedia paces AniList requests and character image downloa
|
|||||||
await runtime.generateForCurrentMedia();
|
await runtime.generateForCurrentMedia();
|
||||||
|
|
||||||
assert.deepEqual(sleepCalls, [2000, 250]);
|
assert.deepEqual(sleepCalls, [2000, 250]);
|
||||||
assert.deepEqual(imageRequests, ['https://example.com/alpha.png', 'https://example.com/beta.png']);
|
assert.deepEqual(imageRequests, [
|
||||||
|
'https://example.com/alpha.png',
|
||||||
|
'https://example.com/beta.png',
|
||||||
|
]);
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -449,20 +449,13 @@ function romanizedTokenToKatakana(token: string): string | null {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (current === 'n' && next.length > 0 && next !== 'y' && !'aeiou'.includes(next)) {
|
||||||
current === 'n' &&
|
|
||||||
next.length > 0 &&
|
|
||||||
next !== 'y' &&
|
|
||||||
!'aeiou'.includes(next)
|
|
||||||
) {
|
|
||||||
output += 'ン';
|
output += 'ン';
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const digraph = ROMANIZED_KANA_DIGRAPHS.find(([romaji]) =>
|
const digraph = ROMANIZED_KANA_DIGRAPHS.find(([romaji]) => normalized.startsWith(romaji, i));
|
||||||
normalized.startsWith(romaji, i),
|
|
||||||
);
|
|
||||||
if (digraph) {
|
if (digraph) {
|
||||||
output += digraph[1];
|
output += digraph[1];
|
||||||
i += digraph[0].length;
|
i += digraph[0].length;
|
||||||
@@ -750,7 +743,13 @@ function writeSnapshot(snapshotPath: string, snapshot: CharacterDictionarySnapsh
|
|||||||
}
|
}
|
||||||
|
|
||||||
function roleBadgeStyle(role: CharacterDictionaryRole): Record<string, string> {
|
function roleBadgeStyle(role: CharacterDictionaryRole): Record<string, string> {
|
||||||
const base = { borderRadius: '4px', padding: '0.15em 0.5em', fontSize: '0.8em', fontWeight: 'bold', color: '#fff' };
|
const base = {
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '0.15em 0.5em',
|
||||||
|
fontSize: '0.8em',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#fff',
|
||||||
|
};
|
||||||
if (role === 'main') return { ...base, backgroundColor: '#4a8c3f' };
|
if (role === 'main') return { ...base, backgroundColor: '#4a8c3f' };
|
||||||
if (role === 'primary') return { ...base, backgroundColor: '#5c82b0' };
|
if (role === 'primary') return { ...base, backgroundColor: '#5c82b0' };
|
||||||
if (role === 'side') return { ...base, backgroundColor: '#7889a0' };
|
if (role === 'side') return { ...base, backgroundColor: '#7889a0' };
|
||||||
@@ -788,7 +787,9 @@ function buildVoicedByContent(
|
|||||||
const va = voiceActors[0]!;
|
const va = voiceActors[0]!;
|
||||||
const vaImgPath = vaImagePaths.get(va.id);
|
const vaImgPath = vaImagePaths.get(va.id);
|
||||||
const vaLabel = va.nativeName
|
const vaLabel = va.nativeName
|
||||||
? va.fullName ? `${va.nativeName} (${va.fullName})` : va.nativeName
|
? va.fullName
|
||||||
|
? `${va.nativeName} (${va.fullName})`
|
||||||
|
: va.nativeName
|
||||||
: va.fullName;
|
: va.fullName;
|
||||||
|
|
||||||
if (vaImgPath) {
|
if (vaImgPath) {
|
||||||
@@ -799,7 +800,12 @@ function buildVoicedByContent(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'td',
|
tag: 'td',
|
||||||
style: { verticalAlign: 'top', padding: '0', paddingRight: '0.4em', borderWidth: '0' },
|
style: {
|
||||||
|
verticalAlign: 'top',
|
||||||
|
padding: '0',
|
||||||
|
paddingRight: '0.4em',
|
||||||
|
borderWidth: '0',
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
tag: 'img',
|
tag: 'img',
|
||||||
path: vaImgPath,
|
path: vaImgPath,
|
||||||
@@ -829,7 +835,9 @@ function buildVoicedByContent(
|
|||||||
const items: Array<Record<string, unknown>> = [];
|
const items: Array<Record<string, unknown>> = [];
|
||||||
for (const va of voiceActors) {
|
for (const va of voiceActors) {
|
||||||
const vaLabel = va.nativeName
|
const vaLabel = va.nativeName
|
||||||
? va.fullName ? `${va.nativeName} (${va.fullName})` : va.nativeName
|
? va.fullName
|
||||||
|
? `${va.nativeName} (${va.fullName})`
|
||||||
|
: va.nativeName
|
||||||
: va.fullName;
|
: va.fullName;
|
||||||
items.push({ tag: 'li', content: vaLabel });
|
items.push({ tag: 'li', content: vaLabel });
|
||||||
}
|
}
|
||||||
@@ -844,9 +852,7 @@ function createDefinitionGlossary(
|
|||||||
): CharacterDictionaryGlossaryEntry[] {
|
): CharacterDictionaryGlossaryEntry[] {
|
||||||
const displayName = character.nativeName || character.fullName || `Character ${character.id}`;
|
const displayName = character.nativeName || character.fullName || `Character ${character.id}`;
|
||||||
const secondaryName =
|
const secondaryName =
|
||||||
character.nativeName &&
|
character.nativeName && character.fullName && character.fullName !== character.nativeName
|
||||||
character.fullName &&
|
|
||||||
character.fullName !== character.nativeName
|
|
||||||
? character.fullName
|
? character.fullName
|
||||||
: null;
|
: null;
|
||||||
const { fields, text: descriptionText } = parseCharacterDescription(character.description);
|
const { fields, text: descriptionText } = parseCharacterDescription(character.description);
|
||||||
@@ -1275,7 +1281,10 @@ async function fetchCharactersForMedia(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadCharacterImage(imageUrl: string, charId: number): Promise<{
|
async function downloadCharacterImage(
|
||||||
|
imageUrl: string,
|
||||||
|
charId: number,
|
||||||
|
): Promise<{
|
||||||
filename: string;
|
filename: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
bytes: Buffer;
|
bytes: Buffer;
|
||||||
@@ -1379,7 +1388,10 @@ function buildDictionaryZip(
|
|||||||
const zipFiles: Array<{ name: string; data: Buffer }> = [
|
const zipFiles: Array<{ name: string; data: Buffer }> = [
|
||||||
{
|
{
|
||||||
name: 'index.json',
|
name: 'index.json',
|
||||||
data: Buffer.from(JSON.stringify(createIndex(dictionaryTitle, description, revision), null, 2), 'utf8'),
|
data: Buffer.from(
|
||||||
|
JSON.stringify(createIndex(dictionaryTitle, description, revision), null, 2),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tag_bank_1.json',
|
name: 'tag_bank_1.json',
|
||||||
@@ -1454,7 +1466,9 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
|||||||
}
|
}
|
||||||
deps.logInfo?.(
|
deps.logInfo?.(
|
||||||
`[dictionary] current anime guess: ${guessed.title.trim()}${
|
`[dictionary] current anime guess: ${guessed.title.trim()}${
|
||||||
typeof guessed.episode === 'number' && guessed.episode > 0 ? ` (episode ${guessed.episode})` : ''
|
typeof guessed.episode === 'number' && guessed.episode > 0
|
||||||
|
? ` (episode ${guessed.episode})`
|
||||||
|
: ''
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
const resolved = await resolveAniListMediaIdFromGuess(guessed, beforeRequest);
|
const resolved = await resolveAniListMediaIdFromGuess(guessed, beforeRequest);
|
||||||
@@ -1486,7 +1500,9 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
|||||||
mediaId,
|
mediaId,
|
||||||
beforeRequest,
|
beforeRequest,
|
||||||
(page) => {
|
(page) => {
|
||||||
deps.logInfo?.(`[dictionary] downloaded AniList character page ${page} for AniList ${mediaId}`);
|
deps.logInfo?.(
|
||||||
|
`[dictionary] downloaded AniList character page ${page} for AniList ${mediaId}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (characters.length === 0) {
|
if (characters.length === 0) {
|
||||||
@@ -1496,12 +1512,14 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
|||||||
const imagesByCharacterId = new Map<number, CharacterDictionarySnapshotImage>();
|
const imagesByCharacterId = new Map<number, CharacterDictionarySnapshotImage>();
|
||||||
const imagesByVaId = new Map<number, CharacterDictionarySnapshotImage>();
|
const imagesByVaId = new Map<number, CharacterDictionarySnapshotImage>();
|
||||||
const allImageUrls: Array<{ id: number; url: string; kind: 'character' | 'va' }> = [];
|
const allImageUrls: Array<{ id: number; url: string; kind: 'character' | 'va' }> = [];
|
||||||
|
const seenVaIds = new Set<number>();
|
||||||
for (const character of characters) {
|
for (const character of characters) {
|
||||||
if (character.imageUrl) {
|
if (character.imageUrl) {
|
||||||
allImageUrls.push({ id: character.id, url: character.imageUrl, kind: 'character' });
|
allImageUrls.push({ id: character.id, url: character.imageUrl, kind: 'character' });
|
||||||
}
|
}
|
||||||
for (const va of character.voiceActors) {
|
for (const va of character.voiceActors) {
|
||||||
if (va.imageUrl && !allImageUrls.some((u) => u.kind === 'va' && u.id === va.id)) {
|
if (va.imageUrl && !seenVaIds.has(va.id)) {
|
||||||
|
seenVaIds.add(va.id);
|
||||||
allImageUrls.push({ id: va.id, url: va.imageUrl, kind: 'va' });
|
allImageUrls.push({ id: va.id, url: va.imageUrl, kind: 'va' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1601,7 +1619,10 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
|||||||
entryCount,
|
entryCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
generateForCurrentMedia: async (targetPath?: string, _options?: CharacterDictionaryGenerateOptions) => {
|
generateForCurrentMedia: async (
|
||||||
|
targetPath?: string,
|
||||||
|
_options?: CharacterDictionaryGenerateOptions,
|
||||||
|
) => {
|
||||||
let hasAniListRequest = false;
|
let hasAniListRequest = false;
|
||||||
const waitForAniListRequestSlot = async (): Promise<void> => {
|
const waitForAniListRequestSlot = async (): Promise<void> => {
|
||||||
if (!hasAniListRequest) {
|
if (!hasAniListRequest) {
|
||||||
|
|||||||
Reference in New Issue
Block a user