mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
fix: correct guessit title parsing for character dictionary sync
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
id: TASK-140
|
||||||
|
title: Fix guessit title parsing for character dictionary sync
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-09 00:00'
|
||||||
|
updated_date: '2026-03-09 00:25'
|
||||||
|
labels:
|
||||||
|
- dictionary
|
||||||
|
- anilist
|
||||||
|
- bug
|
||||||
|
- guessit
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/src/core/services/anilist/anilist-updater.ts
|
||||||
|
- /home/sudacode/projects/japanese/SubMiner/src/core/services/anilist/anilist-updater.test.ts
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Fix AniList character dictionary auto-sync for filenames where `guessit` misparses the full path and our title extraction keeps only the first array segment, causing AniList resolution to match the wrong anime and abort merged dictionary refresh.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 AniList media guessing passes basename-only targets to `guessit` so parent folder names do not corrupt series title detection.
|
||||||
|
- [x] #2 Guessit title arrays are combined into one usable title instead of truncating to the first segment.
|
||||||
|
- [x] #3 Regression coverage includes the Bunny Girl Senpai filename shape that previously resolved to the wrong AniList entry.
|
||||||
|
- [x] #4 Verification confirms the targeted AniList guessing tests pass.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Root repro: `guessit` parsed the Bunny Girl Senpai full path as `title: ["Rascal", "Does-not-Dream-of-Bunny-Girl-Senapi"]`, and our `firstString` helper kept only `Rascal`, which resolved to AniList 3490 (`rayca`) and produced zero character results. Fixed by sending basename-only input to `guessit` and joining multi-part guessit title arrays.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
4
changes/task-140.md
Normal file
4
changes/task-140.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: dictionary
|
||||||
|
|
||||||
|
- Fixed AniList media guessing for character dictionary auto-sync by using filename-only `guessit` input and preserving multi-part guessit titles instead of truncating them to the first segment.
|
||||||
@@ -34,6 +34,44 @@ test('guessAnilistMediaInfo falls back to parser when guessit fails', async () =
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('guessAnilistMediaInfo uses basename for guessit input', async () => {
|
||||||
|
const mediaPath =
|
||||||
|
'/truenas/jellyfin/anime/Rascal-Does-not-Dream-of-Bunny-Girl-Senapi/Season-1/Rascal Does Not Dream of Bunny Girl Senpai (2018) - S01E01 - 001 - My Senpai Is a Bunny Girl [Bluray-1080p][10bit][x265][Opus 2.0][JA]-Subs.mkv';
|
||||||
|
const seenTargets: string[] = [];
|
||||||
|
const result = await guessAnilistMediaInfo(mediaPath, null, {
|
||||||
|
runGuessit: async (target) => {
|
||||||
|
seenTargets.push(target);
|
||||||
|
return JSON.stringify({
|
||||||
|
title: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
||||||
|
episode: 1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.deepEqual(seenTargets, [
|
||||||
|
'Rascal Does Not Dream of Bunny Girl Senpai (2018) - S01E01 - 001 - My Senpai Is a Bunny Girl [Bluray-1080p][10bit][x265][Opus 2.0][JA]-Subs.mkv',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
title: 'Rascal Does Not Dream of Bunny Girl Senpai',
|
||||||
|
episode: 1,
|
||||||
|
source: 'guessit',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guessAnilistMediaInfo joins multi-part guessit titles', async () => {
|
||||||
|
const result = await guessAnilistMediaInfo('/tmp/demo.mkv', null, {
|
||||||
|
runGuessit: async () =>
|
||||||
|
JSON.stringify({
|
||||||
|
title: ['Rascal', 'Does-not-Dream-of-Bunny-Girl-Senpai'],
|
||||||
|
episode: 1,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
title: 'Rascal Does not Dream of Bunny Girl Senpai',
|
||||||
|
episode: 1,
|
||||||
|
source: 'guessit',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('updateAnilistPostWatchProgress updates progress when behind', async () => {
|
test('updateAnilistPostWatchProgress updates progress when behind', async () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
let call = 0;
|
let call = 0;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
import { parseMediaInfo } from '../../../jimaku/utils';
|
import { parseMediaInfo } from '../../../jimaku/utils';
|
||||||
|
|
||||||
@@ -90,6 +91,32 @@ function firstString(value: unknown): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeGuessitTitlePart(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/[._]+/g, ' ')
|
||||||
|
.replace(/-/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readGuessitTitle(value: unknown): string | null {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = normalizeGuessitTitlePart(value);
|
||||||
|
return normalized.length > 0 ? normalized : null;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const parts = value
|
||||||
|
.filter((item): item is string => typeof item === 'string')
|
||||||
|
.map((item) => normalizeGuessitTitlePart(item))
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function firstPositiveInteger(value: unknown): number | null {
|
function firstPositiveInteger(value: unknown): number | null {
|
||||||
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
||||||
return value;
|
return value;
|
||||||
@@ -184,12 +211,13 @@ export async function guessAnilistMediaInfo(
|
|||||||
deps: GuessAnilistMediaInfoDeps = { runGuessit },
|
deps: GuessAnilistMediaInfoDeps = { runGuessit },
|
||||||
): Promise<AnilistMediaGuess | null> {
|
): Promise<AnilistMediaGuess | null> {
|
||||||
const target = mediaPath ?? mediaTitle;
|
const target = mediaPath ?? mediaTitle;
|
||||||
|
const guessitTarget = mediaPath ? path.basename(mediaPath) : mediaTitle;
|
||||||
|
|
||||||
if (target && target.trim().length > 0) {
|
if (guessitTarget && guessitTarget.trim().length > 0) {
|
||||||
try {
|
try {
|
||||||
const stdout = await deps.runGuessit(target);
|
const stdout = await deps.runGuessit(guessitTarget);
|
||||||
const parsed = JSON.parse(stdout) as Record<string, unknown>;
|
const parsed = JSON.parse(stdout) as Record<string, unknown>;
|
||||||
const title = firstString(parsed.title);
|
const title = readGuessitTitle(parsed.title);
|
||||||
const episode = firstPositiveInteger(parsed.episode);
|
const episode = firstPositiveInteger(parsed.episode);
|
||||||
if (title) {
|
if (title) {
|
||||||
return { title, episode, source: 'guessit' };
|
return { title, episode, source: 'guessit' };
|
||||||
|
|||||||
Reference in New Issue
Block a user