Add inline character portraits and dictionary search workflow (#83)

This commit is contained in:
2026-05-25 03:16:25 -07:00
committed by GitHub
parent 7e6f9672cf
commit 807c0ff3db
54 changed files with 2306 additions and 178 deletions
@@ -10,15 +10,17 @@ import {
} from './manual-selection';
const REZERO_EP1 =
'/anime/Re - ZERO, Starting Life in Another World (2016) - S01E01 - - The End of the Beginning and the Beginning of the End [v2 Bluray-1080p Proper][10bit][x265][FLAC 2.0][EN+JA]-SCY.mkv';
'/anime/ReZERO/Season 1/Re - ZERO, Starting Life in Another World (2016) - S01E01 - - The End of the Beginning and the Beginning of the End [v2 Bluray-1080p Proper][10bit][x265][FLAC 2.0][EN+JA]-SCY.mkv';
const REZERO_EP2 =
'/anime/Re - ZERO, Starting Life in Another World (2016) - S01E02 - Reunion with the Witch [Bluray-1080p][x265][JA]-SCY.mkv';
'/anime/ReZERO/Season 1/Re - ZERO, Starting Life in Another World (2016) - S01E02 - Reunion with the Witch [Bluray-1080p][x265][JA]-SCY.mkv';
const REZERO_S2_EP1 =
'/anime/ReZERO/Season 2/Re - ZERO, Starting Life in Another World (2016) - S02E01 - Each Ones Promise [Bluray-1080p][x265][JA]-SCY.mkv';
function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-manual-selection-'));
}
test('buildCharacterDictionarySeriesKey uses guessit title, alternative title, and year for Re ZERO series scope', () => {
test('buildCharacterDictionarySeriesKey scopes guessit title and year by media directory', () => {
const key = buildCharacterDictionarySeriesKey({
mediaPath: REZERO_EP1,
mediaTitle: null,
@@ -32,10 +34,10 @@ test('buildCharacterDictionarySeriesKey uses guessit title, alternative title, a
},
});
assert.equal(key, 're-zero-starting-life-in-another-world-2016');
assert.equal(key, 'anime-rezero-season-1--re-zero-starting-life-in-another-world-2016');
});
test('manual selection store persists overrides and matches later episodes in the same series', async () => {
test('manual selection store persists overrides and matches later episodes in the same directory', async () => {
const userDataPath = makeTempDir();
const store = createCharacterDictionaryManualSelectionStore({ userDataPath });
const firstKey = buildCharacterDictionarySeriesKey({
@@ -79,3 +81,131 @@ test('manual selection store persists overrides and matches later episodes in th
staleMediaIds: [10607],
});
});
test('manual selection store resolves legacy unscoped override keys', async () => {
const userDataPath = makeTempDir();
const overrideDir = path.join(userDataPath, 'character-dictionaries');
fs.mkdirSync(overrideDir, { recursive: true });
fs.writeFileSync(
path.join(overrideDir, 'anilist-overrides.json'),
JSON.stringify({
overrides: [
{
seriesKey: 're-zero-starting-life-in-another-world-2016',
mediaId: 21355,
mediaTitle: 'Re:ZERO -Starting Life in Another World-',
staleMediaIds: [10607],
},
],
}),
'utf8',
);
const scopedKey = buildCharacterDictionarySeriesKey({
mediaPath: REZERO_EP1,
mediaTitle: null,
guess: {
title: 'Re ZERO, Starting Life in Another World',
alternativeTitle: 'ZERO, Starting Life in Another World',
year: 2016,
season: 1,
episode: 1,
source: 'guessit',
},
});
const store = createCharacterDictionaryManualSelectionStore({ userDataPath });
assert.deepEqual(await store.getOverride(scopedKey), {
seriesKey: 're-zero-starting-life-in-another-world-2016',
mediaId: 21355,
mediaTitle: 'Re:ZERO -Starting Life in Another World-',
staleMediaIds: [10607],
});
});
test('manual selection store prefers exact scoped override over legacy fallback', async () => {
const userDataPath = makeTempDir();
const overrideDir = path.join(userDataPath, 'character-dictionaries');
fs.mkdirSync(overrideDir, { recursive: true });
const scopedKey = buildCharacterDictionarySeriesKey({
mediaPath: REZERO_EP1,
mediaTitle: null,
guess: {
title: 'Re ZERO, Starting Life in Another World',
alternativeTitle: 'ZERO, Starting Life in Another World',
year: 2016,
season: 1,
episode: 1,
source: 'guessit',
},
});
fs.writeFileSync(
path.join(overrideDir, 'anilist-overrides.json'),
JSON.stringify({
overrides: [
{
seriesKey: 're-zero-starting-life-in-another-world-2016',
mediaId: 10607,
mediaTitle: 'Legacy Re:ZERO',
staleMediaIds: [],
},
{
seriesKey: scopedKey,
mediaId: 21355,
mediaTitle: 'Re:ZERO -Starting Life in Another World-',
staleMediaIds: [10607],
},
],
}),
'utf8',
);
const store = createCharacterDictionaryManualSelectionStore({ userDataPath });
assert.deepEqual(await store.getOverride(scopedKey), {
seriesKey: scopedKey,
mediaId: 21355,
mediaTitle: 'Re:ZERO -Starting Life in Another World-',
staleMediaIds: [10607],
});
});
test('manual selection store keeps overrides separate for different season directories', async () => {
const userDataPath = makeTempDir();
const store = createCharacterDictionaryManualSelectionStore({ userDataPath });
const firstSeasonKey = buildCharacterDictionarySeriesKey({
mediaPath: REZERO_EP1,
mediaTitle: null,
guess: {
title: 'Re ZERO, Starting Life in Another World',
alternativeTitle: 'ZERO, Starting Life in Another World',
year: 2016,
season: 1,
episode: 1,
source: 'guessit',
},
});
await store.setOverride({
seriesKey: firstSeasonKey,
mediaId: 21355,
mediaTitle: 'Re:ZERO -Starting Life in Another World-',
staleMediaIds: [],
});
const secondSeasonKey = buildCharacterDictionarySeriesKey({
mediaPath: REZERO_S2_EP1,
mediaTitle: null,
guess: {
title: 'Re ZERO, Starting Life in Another World',
alternativeTitle: 'ZERO, Starting Life in Another World',
year: 2016,
season: 2,
episode: 1,
source: 'guessit',
},
});
assert.notEqual(secondSeasonKey, firstSeasonKey);
assert.equal(await store.getOverride(secondSeasonKey), null);
});