mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
Add inline character portraits and dictionary search workflow (#83)
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
buildCharacterDictionarySeriesKey,
|
||||
createCharacterDictionaryManualSelectionStore,
|
||||
} from './character-dictionary-runtime/manual-selection';
|
||||
import { snapshotHasCharacterNameImages } from './character-dictionary-runtime/image-lookup';
|
||||
import type {
|
||||
AniListMediaCandidate,
|
||||
CharacterDictionaryBuildResult,
|
||||
@@ -151,6 +152,7 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
buildMergedDictionary: (mediaIds: number[]) => Promise<MergedCharacterDictionaryBuildResult>;
|
||||
getManualSelectionSnapshot: (
|
||||
targetPath?: string,
|
||||
searchTitle?: string,
|
||||
) => Promise<CharacterDictionaryManualSelectionSnapshot>;
|
||||
setManualSelection: (request: {
|
||||
targetPath?: string;
|
||||
@@ -168,6 +170,13 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
userDataPath: deps.userDataPath,
|
||||
});
|
||||
|
||||
const shouldRefreshCachedSnapshot = (snapshot: CharacterDictionarySnapshot): boolean => {
|
||||
if (deps.getNameMatchImagesEnabled?.() !== true) {
|
||||
return false;
|
||||
}
|
||||
return !snapshotHasCharacterNameImages(snapshot);
|
||||
};
|
||||
|
||||
const createAniListRequestSlot = (): (() => Promise<void>) => {
|
||||
let hasAniListRequest = false;
|
||||
return async (): Promise<void> => {
|
||||
@@ -205,12 +214,19 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
mediaTitle: guessInput.mediaTitle,
|
||||
guess: guessed,
|
||||
}),
|
||||
unscopedSeriesKey: buildCharacterDictionarySeriesKey({
|
||||
mediaPath: null,
|
||||
mediaTitle: guessInput.mediaTitle,
|
||||
guess: guessed,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const findCachedSnapshotForSeriesKey = (
|
||||
seriesKey: string,
|
||||
fallbackSeriesKey?: string,
|
||||
): CharacterDictionarySnapshot | null => {
|
||||
const acceptedKeys = new Set([seriesKey, fallbackSeriesKey].filter(Boolean));
|
||||
return (
|
||||
readCachedSnapshots(outputDir).find((snapshot) => {
|
||||
const snapshotSeriesKey = buildCharacterDictionarySeriesKey({
|
||||
@@ -223,7 +239,7 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
source: 'fallback',
|
||||
},
|
||||
});
|
||||
return snapshotSeriesKey === seriesKey;
|
||||
return acceptedKeys.has(snapshotSeriesKey);
|
||||
}) ?? null
|
||||
);
|
||||
};
|
||||
@@ -233,7 +249,7 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
beforeRequest?: () => Promise<void>,
|
||||
): Promise<ResolvedAniListMedia> => {
|
||||
deps.logInfo?.('[dictionary] resolving current anime for character dictionary generation');
|
||||
const { guessed, seriesKey } = await guessCurrentMedia(targetPath);
|
||||
const { guessed, seriesKey, unscopedSeriesKey } = await guessCurrentMedia(targetPath);
|
||||
deps.logInfo?.(
|
||||
`[dictionary] current anime guess: ${guessed.title.trim()}${
|
||||
typeof guessed.episode === 'number' && guessed.episode > 0
|
||||
@@ -267,7 +283,7 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
}
|
||||
}
|
||||
|
||||
const cachedSnapshot = findCachedSnapshotForSeriesKey(seriesKey);
|
||||
const cachedSnapshot = findCachedSnapshotForSeriesKey(seriesKey, unscopedSeriesKey);
|
||||
if (cachedSnapshot) {
|
||||
writeCachedMediaResolution(outputDir, {
|
||||
seriesKey,
|
||||
@@ -301,7 +317,7 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
): Promise<CharacterDictionarySnapshotResult> => {
|
||||
const snapshotPath = getSnapshotPath(outputDir, mediaId);
|
||||
const cachedSnapshot = readSnapshot(snapshotPath);
|
||||
if (cachedSnapshot) {
|
||||
if (cachedSnapshot && !shouldRefreshCachedSnapshot(cachedSnapshot)) {
|
||||
deps.logInfo?.(`[dictionary] snapshot hit for AniList ${mediaId}`);
|
||||
return {
|
||||
mediaId: cachedSnapshot.mediaId,
|
||||
@@ -311,6 +327,11 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
updatedAt: cachedSnapshot.updatedAt,
|
||||
};
|
||||
}
|
||||
if (cachedSnapshot) {
|
||||
deps.logInfo?.(
|
||||
`[dictionary] snapshot stale for AniList ${mediaId}: missing cached character images`,
|
||||
);
|
||||
}
|
||||
|
||||
progress?.onGenerating?.({
|
||||
mediaId,
|
||||
@@ -455,28 +476,43 @@ export function createCharacterDictionaryRuntimeService(deps: CharacterDictionar
|
||||
entryCount,
|
||||
};
|
||||
},
|
||||
getManualSelectionSnapshot: async (targetPath?: string) => {
|
||||
getManualSelectionSnapshot: async (targetPath?: string, searchTitle?: string) => {
|
||||
const waitForAniListRequestSlot = createAniListRequestSlot();
|
||||
const { guessed, seriesKey } = await guessCurrentMedia(targetPath);
|
||||
const [candidates, override] = await Promise.all([
|
||||
searchAniListMediaCandidates(guessed.title, waitForAniListRequestSlot),
|
||||
const normalizedSearchTitle = searchTitle?.trim();
|
||||
const shouldUseExplicitSearch = searchTitle !== undefined;
|
||||
const candidateSearchTitle = shouldUseExplicitSearch ? normalizedSearchTitle : guessed.title;
|
||||
const candidates = candidateSearchTitle
|
||||
? await searchAniListMediaCandidates(candidateSearchTitle, waitForAniListRequestSlot)
|
||||
: [];
|
||||
const [override, current] = await Promise.all([
|
||||
manualSelectionStore.getOverride(seriesKey),
|
||||
shouldUseExplicitSearch
|
||||
? Promise.resolve(null)
|
||||
: resolveAniListMediaIdFromGuess(guessed, waitForAniListRequestSlot)
|
||||
.then(
|
||||
(entry): AniListMediaCandidate => ({
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
episodes:
|
||||
candidates.find((candidate) => candidate.id === entry.id)?.episodes ?? null,
|
||||
}),
|
||||
)
|
||||
.catch(() => null),
|
||||
]);
|
||||
const current = await resolveAniListMediaIdFromGuess(guessed, waitForAniListRequestSlot)
|
||||
.then(
|
||||
(entry): AniListMediaCandidate => ({
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
episodes: candidates.find((candidate) => candidate.id === entry.id)?.episodes ?? null,
|
||||
}),
|
||||
)
|
||||
.catch(() => null);
|
||||
const overrideCandidate = override
|
||||
? candidates.find((candidate) => candidate.id === override.mediaId)
|
||||
: null;
|
||||
return {
|
||||
seriesKey,
|
||||
guessTitle: guessed.title,
|
||||
current,
|
||||
override: override
|
||||
? { id: override.mediaId, title: override.mediaTitle, episodes: null }
|
||||
? {
|
||||
id: override.mediaId,
|
||||
title: override.mediaTitle,
|
||||
episodes: overrideCandidate?.episodes ?? null,
|
||||
}
|
||||
: null,
|
||||
candidates,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user