mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-21 00:11:27 -07:00
feat(immersion): add anime metadata, occurrence tracking, and schema upgrades
- Add imm_anime table with AniList integration - Add imm_subtitle_lines, imm_word_line_occurrences, imm_kanji_line_occurrences - Add POS fields (part_of_speech, pos1, pos2, pos3) to imm_words - Add anime metadata parsing with guessit fallback - Add video duration tracking and watched status - Add episode, streak, trend, and word/kanji detail queries - Deduplicate subtitle line recording within sessions - Pass Anki note IDs through card mining callback chain
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { spawn as nodeSpawn } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseMediaInfo } from '../../../jimaku/utils';
|
||||
import {
|
||||
guessAnilistMediaInfo,
|
||||
runGuessit,
|
||||
type GuessAnilistMediaInfoDeps,
|
||||
} from '../anilist/anilist-updater';
|
||||
import {
|
||||
deriveCanonicalTitle,
|
||||
emptyMetadata,
|
||||
@@ -8,7 +15,12 @@ import {
|
||||
parseFps,
|
||||
toNullableInt,
|
||||
} from './reducer';
|
||||
import { SOURCE_TYPE_LOCAL, type ProbeMetadata, type VideoMetadata } from './types';
|
||||
import {
|
||||
SOURCE_TYPE_LOCAL,
|
||||
type ParsedAnimeVideoGuess,
|
||||
type ProbeMetadata,
|
||||
type VideoMetadata,
|
||||
} from './types';
|
||||
|
||||
type SpawnFn = typeof nodeSpawn;
|
||||
|
||||
@@ -24,6 +36,21 @@ interface MetadataDeps {
|
||||
fs?: FsDeps;
|
||||
}
|
||||
|
||||
interface GuessAnimeVideoMetadataDeps {
|
||||
runGuessit?: GuessAnilistMediaInfoDeps['runGuessit'];
|
||||
}
|
||||
|
||||
function mapParserConfidenceToScore(confidence: 'high' | 'medium' | 'low'): number {
|
||||
switch (confidence) {
|
||||
case 'high':
|
||||
return 1;
|
||||
case 'medium':
|
||||
return 0.6;
|
||||
default:
|
||||
return 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeSha256(
|
||||
mediaPath: string,
|
||||
deps: MetadataDeps = {},
|
||||
@@ -151,3 +178,48 @@ export async function getLocalVideoMetadata(
|
||||
metadataJson: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function guessAnimeVideoMetadata(
|
||||
mediaPath: string | null,
|
||||
mediaTitle: string | null,
|
||||
deps: GuessAnimeVideoMetadataDeps = {},
|
||||
): Promise<ParsedAnimeVideoGuess | null> {
|
||||
const parsed = await guessAnilistMediaInfo(mediaPath, mediaTitle, {
|
||||
runGuessit: deps.runGuessit ?? runGuessit,
|
||||
});
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedBasename = mediaPath ? path.basename(mediaPath) : null;
|
||||
if (parsed.source === 'guessit') {
|
||||
return {
|
||||
parsedBasename,
|
||||
parsedTitle: parsed.title,
|
||||
parsedSeason: parsed.season,
|
||||
parsedEpisode: parsed.episode,
|
||||
parserSource: 'guessit',
|
||||
parserConfidence: 1,
|
||||
parseMetadataJson: JSON.stringify({
|
||||
filename: parsedBasename,
|
||||
source: 'guessit',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackInfo = parseMediaInfo(mediaPath ?? mediaTitle);
|
||||
return {
|
||||
parsedBasename: parsedBasename ?? fallbackInfo.filename ?? null,
|
||||
parsedTitle: parsed.title,
|
||||
parsedSeason: parsed.season,
|
||||
parsedEpisode: parsed.episode,
|
||||
parserSource: 'fallback',
|
||||
parserConfidence: mapParserConfidenceToScore(fallbackInfo.confidence),
|
||||
parseMetadataJson: JSON.stringify({
|
||||
confidence: fallbackInfo.confidence,
|
||||
filename: fallbackInfo.filename,
|
||||
rawTitle: fallbackInfo.rawTitle,
|
||||
source: 'fallback',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user