mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 16:49:51 -07:00
fix(character-dictionary): scope manual overrides to season directory
- Match/replace AniList overrides by directory scope instead of exact series key, so later episodes keep the override even when filename guesses differ - Add tests covering differing-guess matches and same-directory override replacement - Update docs-site/character-dictionary.md and add changelog entry
This commit is contained in:
@@ -82,6 +82,73 @@ test('manual selection store persists overrides and matches later episodes in th
|
||||
});
|
||||
});
|
||||
|
||||
test('manual selection store applies a season-directory override when later episode guesses differ', async () => {
|
||||
const userDataPath = makeTempDir();
|
||||
const store = createCharacterDictionaryManualSelectionStore({ userDataPath });
|
||||
const directory = '/Volumes/jellyfin/anime/toaru-kagaku-no-railgun/Season-2';
|
||||
const selectedKey = buildCharacterDictionarySeriesKey({
|
||||
mediaPath: `${directory}/A Certain Scientific Railgun S - 10 - Critical.mkv`,
|
||||
mediaTitle: 'A Certain Scientific Railgun S - 10 - Critical.mkv',
|
||||
guess: {
|
||||
title: 'Critical',
|
||||
season: null,
|
||||
episode: 10,
|
||||
source: 'guessit',
|
||||
},
|
||||
});
|
||||
await store.setOverride({
|
||||
seriesKey: selectedKey,
|
||||
mediaId: 16049,
|
||||
mediaTitle: 'A Certain Scientific Railgun S',
|
||||
staleMediaIds: [1057],
|
||||
});
|
||||
|
||||
const laterEpisodeKey = buildCharacterDictionarySeriesKey({
|
||||
mediaPath: `${directory}/A Certain Scientific Railgun S - 16 - Sisters.mkv`,
|
||||
mediaTitle: 'A Certain Scientific Railgun S - 16 - Sisters.mkv',
|
||||
guess: {
|
||||
title: 'Sisters',
|
||||
season: null,
|
||||
episode: 16,
|
||||
source: 'guessit',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(await store.getOverride(laterEpisodeKey), {
|
||||
seriesKey: selectedKey,
|
||||
mediaId: 16049,
|
||||
mediaTitle: 'A Certain Scientific Railgun S',
|
||||
staleMediaIds: [1057],
|
||||
});
|
||||
});
|
||||
|
||||
test('manual selection store replaces older guessed keys for the same season directory', async () => {
|
||||
const userDataPath = makeTempDir();
|
||||
const store = createCharacterDictionaryManualSelectionStore({ userDataPath });
|
||||
const directoryKey = 'volumes-jellyfin-anime-toaru-kagaku-no-railgun-season-2';
|
||||
const firstKey = `${directoryKey}--critical`;
|
||||
const laterKey = `${directoryKey}--sisters`;
|
||||
await store.setOverride({
|
||||
seriesKey: firstKey,
|
||||
mediaId: 1057,
|
||||
mediaTitle: 'Ippatsu Kiki Musume',
|
||||
staleMediaIds: [],
|
||||
});
|
||||
await store.setOverride({
|
||||
seriesKey: laterKey,
|
||||
mediaId: 16049,
|
||||
mediaTitle: 'A Certain Scientific Railgun S',
|
||||
staleMediaIds: [1057],
|
||||
});
|
||||
|
||||
assert.deepEqual(await store.getOverride(firstKey), {
|
||||
seriesKey: laterKey,
|
||||
mediaId: 16049,
|
||||
mediaTitle: 'A Certain Scientific Railgun S',
|
||||
staleMediaIds: [1057],
|
||||
});
|
||||
});
|
||||
|
||||
test('manual selection store resolves legacy unscoped override keys', async () => {
|
||||
const userDataPath = makeTempDir();
|
||||
const overrideDir = path.join(userDataPath, 'character-dictionaries');
|
||||
|
||||
@@ -107,6 +107,11 @@ function getLegacySeriesKeyCandidates(seriesKey: string): string[] {
|
||||
return [seriesKey, seriesKey.slice(scopedSeparatorIndex + 2)];
|
||||
}
|
||||
|
||||
function getDirectoryScope(seriesKey: string): string | null {
|
||||
const scopedSeparatorIndex = seriesKey.indexOf('--');
|
||||
return scopedSeparatorIndex < 0 ? null : seriesKey.slice(0, scopedSeparatorIndex);
|
||||
}
|
||||
|
||||
export function buildCharacterDictionarySeriesKey(input: {
|
||||
mediaPath: string | null;
|
||||
mediaTitle: string | null;
|
||||
@@ -139,6 +144,20 @@ export function createCharacterDictionaryManualSelectionStore(deps: { userDataPa
|
||||
const match = overrides.find((entry) => entry.seriesKey === candidate);
|
||||
if (match) return match;
|
||||
}
|
||||
const directoryScope = getDirectoryScope(seriesKey);
|
||||
if (directoryScope) {
|
||||
const scopedMatches = overrides.filter(
|
||||
(entry) => getDirectoryScope(entry.seriesKey) === directoryScope,
|
||||
);
|
||||
const selectedMediaIds = new Set(scopedMatches.map((entry) => entry.mediaId));
|
||||
if (scopedMatches.length > 0 && selectedMediaIds.size === 1) {
|
||||
const latest = scopedMatches.at(-1)!;
|
||||
return {
|
||||
...latest,
|
||||
staleMediaIds: dedupeNumbers(scopedMatches.flatMap((entry) => entry.staleMediaIds)),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setOverride: async (selection: CharacterDictionaryManualSelection): Promise<void> => {
|
||||
@@ -146,8 +165,11 @@ export function createCharacterDictionaryManualSelectionStore(deps: { userDataPa
|
||||
if (!normalized) {
|
||||
throw new Error('Invalid character dictionary manual selection.');
|
||||
}
|
||||
const remaining = readOverrides(filePath).filter(
|
||||
(entry) => entry.seriesKey !== normalized.seriesKey,
|
||||
const directoryScope = getDirectoryScope(normalized.seriesKey);
|
||||
const remaining = readOverrides(filePath).filter((entry) =>
|
||||
directoryScope
|
||||
? getDirectoryScope(entry.seriesKey) !== directoryScope
|
||||
: entry.seriesKey !== normalized.seriesKey,
|
||||
);
|
||||
writeOverrides(filePath, [...remaining, normalized]);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user