mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 07:21:33 -07:00
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import type { AnkiConnectConfig } from '../types/anki';
|
|
import type { KnownWordMaturityTier } from '../types/subtitle';
|
|
|
|
export const DEFAULT_MATURE_INTERVAL_THRESHOLD_DAYS = 21;
|
|
|
|
// Version of the tier classification rules; part of the known-word cache
|
|
// identity so a rule change invalidates caches built under the old rules.
|
|
export const KNOWN_WORD_MATURITY_RULES_VERSION = 2;
|
|
|
|
// Ascending maturity; index order backs tier comparison.
|
|
const TIER_ORDER: readonly KnownWordMaturityTier[] = ['new', 'learning', 'young', 'mature'];
|
|
|
|
export interface KnownWordMaturityTierQueries {
|
|
mature: string;
|
|
young: string;
|
|
learning: string;
|
|
}
|
|
|
|
export interface KnownWordMaturityTierSets {
|
|
mature: ReadonlySet<number>;
|
|
young: ReadonlySet<number>;
|
|
learning: ReadonlySet<number>;
|
|
}
|
|
|
|
// Maturity tiers only affect how known-word highlights render, so both flags
|
|
// must be on before tier data is fetched or served.
|
|
export function getKnownWordMaturityEnabled(config: AnkiConnectConfig): boolean {
|
|
return (
|
|
config.knownWords?.highlightEnabled === true && config.knownWords?.maturityEnabled === true
|
|
);
|
|
}
|
|
|
|
export function getMatureIntervalThresholdDays(config: AnkiConnectConfig): number {
|
|
const threshold = config.knownWords?.matureThresholdDays;
|
|
if (typeof threshold === 'number' && Number.isFinite(threshold) && threshold >= 1) {
|
|
return Math.floor(threshold);
|
|
}
|
|
return DEFAULT_MATURE_INTERVAL_THRESHOLD_DAYS;
|
|
}
|
|
|
|
// Anki search props classify notes server-side: a note matches a tier query
|
|
// when ANY of its cards matches, which implements most-mature-card-wins for
|
|
// free once tiers are checked in mature > young > learning order.
|
|
//
|
|
// The interval tiers exclude is:learn so the per-card buckets stay disjoint and
|
|
// match Anki's own card counts, where (re)learning is its own bucket rather
|
|
// than part of young/mature. Without the exclusion a lapsed card - whose
|
|
// interval is reset to at least lapse minInt, so >= 1 - is caught by the young
|
|
// query first and the learning tier becomes unreachable in practice.
|
|
export function buildKnownWordMaturityTierQueries(
|
|
scopeQuery: string,
|
|
thresholdDays: number,
|
|
): KnownWordMaturityTierQueries {
|
|
const prefix = scopeQuery.trim().length > 0 ? `${scopeQuery.trim()} ` : '';
|
|
return {
|
|
mature: `${prefix}prop:ivl>=${thresholdDays} -is:learn`,
|
|
young: `${prefix}prop:ivl>=1 prop:ivl<${thresholdDays} -is:learn`,
|
|
learning: `${prefix}is:learn`,
|
|
};
|
|
}
|
|
|
|
export async function fetchKnownWordMaturityTierSets(
|
|
findNotes: (query: string, options?: { maxRetries?: number }) => Promise<unknown>,
|
|
scopeQueries: string[],
|
|
thresholdDays: number,
|
|
): Promise<{ mature: Set<number>; young: Set<number>; learning: Set<number> }> {
|
|
const sets = {
|
|
mature: new Set<number>(),
|
|
young: new Set<number>(),
|
|
learning: new Set<number>(),
|
|
};
|
|
for (const scopeQuery of scopeQueries) {
|
|
const queries = buildKnownWordMaturityTierQueries(scopeQuery, thresholdDays);
|
|
for (const tier of ['mature', 'young', 'learning'] as const) {
|
|
const noteIds = (await findNotes(queries[tier], { maxRetries: 0 })) as number[];
|
|
if (!Array.isArray(noteIds)) {
|
|
continue;
|
|
}
|
|
for (const noteId of noteIds) {
|
|
if (Number.isInteger(noteId) && noteId > 0) {
|
|
sets[tier].add(noteId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sets;
|
|
}
|
|
|
|
export function classifyKnownWordNoteTier(
|
|
noteId: number,
|
|
sets: KnownWordMaturityTierSets,
|
|
): KnownWordMaturityTier {
|
|
if (sets.mature.has(noteId)) return 'mature';
|
|
if (sets.young.has(noteId)) return 'young';
|
|
if (sets.learning.has(noteId)) return 'learning';
|
|
return 'new';
|
|
}
|
|
|
|
export function maxKnownWordMaturityTier(
|
|
a: KnownWordMaturityTier | null | undefined,
|
|
b: KnownWordMaturityTier | null | undefined,
|
|
): KnownWordMaturityTier | null {
|
|
if (!a) return b ?? null;
|
|
if (!b) return a;
|
|
return TIER_ORDER.indexOf(a) >= TIER_ORDER.indexOf(b) ? a : b;
|
|
}
|
|
|
|
export function sanitizeKnownWordMaturityTier(value: unknown): KnownWordMaturityTier | null {
|
|
return typeof value === 'string' && TIER_ORDER.includes(value as KnownWordMaturityTier)
|
|
? (value as KnownWordMaturityTier)
|
|
: null;
|
|
}
|