mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-12 13:55:51 -07:00
fix(stats): stop counting duplicate typeset subtitle lines
- Collapse animation-burst subtitle lines (karaoke OPs, animated signs) at ingest time using the same dedup rules the subtitle sidebar already applies, so repeated frames no longer flood "Top Repeated Words" - Add retroactive cleanup for stats already affected: a "Duplicates" scanner/cleaner in the Vocabulary tab and `subminer stats cleanup --duplicate-lines` (`--dry-run`, `--lookback-days`) on the CLI - Only subtitle lines and the vocabulary counts they feed are touched; watch time and lines-seen totals are left as recorded
This commit is contained in:
@@ -157,6 +157,15 @@ export async function runStatsCommand(
|
||||
if (args.statsCleanupLifetime) {
|
||||
forwarded.push('--stats-cleanup-lifetime');
|
||||
}
|
||||
if (args.statsCleanupDuplicateLines) {
|
||||
forwarded.push('--stats-cleanup-duplicate-lines');
|
||||
}
|
||||
if (args.statsCleanupDryRun) {
|
||||
forwarded.push('--stats-cleanup-dry-run');
|
||||
}
|
||||
if (args.statsCleanupLookbackDays) {
|
||||
forwarded.push('--stats-cleanup-lookback-days', String(args.statsCleanupLookbackDays));
|
||||
}
|
||||
if (shouldForwardLogLevel(args.logLevel)) {
|
||||
forwarded.push('--log-level', args.logLevel);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,9 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => {
|
||||
statsCleanup: false,
|
||||
statsCleanupVocab: false,
|
||||
statsCleanupLifetime: false,
|
||||
statsCleanupDuplicateLines: false,
|
||||
statsCleanupDryRun: false,
|
||||
statsCleanupLookbackDays: null,
|
||||
statsLogLevel: null,
|
||||
syncTriggered: false,
|
||||
syncCliTokens: [],
|
||||
@@ -185,6 +188,9 @@ test('applyInvocationsToArgs maps settings invocation to settings window', () =>
|
||||
statsCleanup: false,
|
||||
statsCleanupVocab: false,
|
||||
statsCleanupLifetime: false,
|
||||
statsCleanupDuplicateLines: false,
|
||||
statsCleanupDryRun: false,
|
||||
statsCleanupLookbackDays: null,
|
||||
statsLogLevel: null,
|
||||
syncTriggered: false,
|
||||
syncCliTokens: [],
|
||||
@@ -229,6 +235,9 @@ test('applyInvocationsToArgs fails when config invocation has no action', () =>
|
||||
statsCleanup: false,
|
||||
statsCleanupVocab: false,
|
||||
statsCleanupLifetime: false,
|
||||
statsCleanupDuplicateLines: false,
|
||||
statsCleanupDryRun: false,
|
||||
statsCleanupLookbackDays: null,
|
||||
statsLogLevel: null,
|
||||
syncTriggered: false,
|
||||
syncCliTokens: [],
|
||||
@@ -271,6 +280,9 @@ test('applyInvocationsToArgs maps texthooker browser-open request', () => {
|
||||
statsCleanup: false,
|
||||
statsCleanupVocab: false,
|
||||
statsCleanupLifetime: false,
|
||||
statsCleanupDuplicateLines: false,
|
||||
statsCleanupDryRun: false,
|
||||
statsCleanupLookbackDays: null,
|
||||
statsLogLevel: null,
|
||||
syncTriggered: false,
|
||||
syncCliTokens: [],
|
||||
|
||||
@@ -162,6 +162,8 @@ export function createDefaultArgs(
|
||||
statsCleanup: false,
|
||||
statsCleanupVocab: false,
|
||||
statsCleanupLifetime: false,
|
||||
statsCleanupDuplicateLines: false,
|
||||
statsCleanupDryRun: false,
|
||||
doctor: false,
|
||||
doctorRefreshKnownWords: false,
|
||||
logsExport: false,
|
||||
@@ -258,6 +260,11 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations
|
||||
if (invocations.statsCleanup) parsed.statsCleanup = true;
|
||||
if (invocations.statsCleanupVocab) parsed.statsCleanupVocab = true;
|
||||
if (invocations.statsCleanupLifetime) parsed.statsCleanupLifetime = true;
|
||||
if (invocations.statsCleanupDuplicateLines) parsed.statsCleanupDuplicateLines = true;
|
||||
if (invocations.statsCleanupDryRun) parsed.statsCleanupDryRun = true;
|
||||
if (invocations.statsCleanupLookbackDays !== null) {
|
||||
parsed.statsCleanupLookbackDays = invocations.statsCleanupLookbackDays;
|
||||
}
|
||||
if (invocations.dictionaryTarget) {
|
||||
parsed.dictionaryTarget = parseDictionaryTarget(invocations.dictionaryTarget);
|
||||
} else if (
|
||||
|
||||
@@ -37,6 +37,9 @@ export interface CliInvocations {
|
||||
statsCleanup: boolean;
|
||||
statsCleanupVocab: boolean;
|
||||
statsCleanupLifetime: boolean;
|
||||
statsCleanupDuplicateLines: boolean;
|
||||
statsCleanupDryRun: boolean;
|
||||
statsCleanupLookbackDays: number | null;
|
||||
statsLogLevel: string | null;
|
||||
syncTriggered: boolean;
|
||||
syncCliTokens: string[];
|
||||
@@ -53,6 +56,16 @@ export interface CliInvocations {
|
||||
texthookerOpenBrowser: boolean;
|
||||
}
|
||||
|
||||
/** `--lookback-days` narrows the duplicate-line cleanup; anything unusable means no limit. */
|
||||
function parseStatsLookbackDays(value: unknown): number | null {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') return null;
|
||||
const days = Number(value);
|
||||
if (!Number.isFinite(days) || days <= 0) {
|
||||
throw new Error('Stats --lookback-days must be a positive number of days.');
|
||||
}
|
||||
return Math.floor(days);
|
||||
}
|
||||
|
||||
function applyRootOptions(program: Command): void {
|
||||
program
|
||||
.option(
|
||||
@@ -169,6 +182,9 @@ export function parseCliPrograms(
|
||||
let statsCleanup = false;
|
||||
let statsCleanupVocab = false;
|
||||
let statsCleanupLifetime = false;
|
||||
let statsCleanupDuplicateLines = false;
|
||||
let statsCleanupDryRun = false;
|
||||
let statsCleanupLookbackDays: number | null = null;
|
||||
let statsLogLevel: string | null = null;
|
||||
let syncTriggered = false;
|
||||
let syncCliTokens: string[] = [];
|
||||
@@ -269,6 +285,9 @@ export function parseCliPrograms(
|
||||
.option('-s, --stop', 'Stop the background stats server')
|
||||
.option('-v, --vocab', 'Clean vocabulary rows in the stats database')
|
||||
.option('-l, --lifetime', 'Rebuild lifetime summary rows from retained data')
|
||||
.option('-d, --duplicate-lines', 'Collapse repeated subtitle lines from typeset animations')
|
||||
.option('--dry-run', 'Report what a cleanup would remove without changing anything')
|
||||
.option('--lookback-days <days>', 'Only clean lines recorded in the last N days')
|
||||
.option('--log-level <level>', 'Log level')
|
||||
.action((action: string | undefined, options: Record<string, unknown>) => {
|
||||
statsTriggered = true;
|
||||
@@ -289,13 +308,29 @@ export function parseCliPrograms(
|
||||
if (normalizedAction && (statsBackground || statsStop)) {
|
||||
throw new Error('Stats background and stop flags cannot be combined with stats actions.');
|
||||
}
|
||||
if (normalizedAction !== 'cleanup' && (options.vocab === true || options.lifetime === true)) {
|
||||
throw new Error('Stats --vocab and --lifetime flags require the cleanup action.');
|
||||
if (
|
||||
normalizedAction !== 'cleanup' &&
|
||||
(options.vocab === true || options.lifetime === true || options.duplicateLines === true)
|
||||
) {
|
||||
throw new Error(
|
||||
'Stats --vocab, --lifetime and --duplicate-lines flags require the cleanup action.',
|
||||
);
|
||||
}
|
||||
if (options.duplicateLines !== true && (options.dryRun === true || options.lookbackDays)) {
|
||||
throw new Error('Stats --dry-run and --lookback-days require --duplicate-lines.');
|
||||
}
|
||||
if (normalizedAction === 'cleanup') {
|
||||
statsCleanup = true;
|
||||
statsCleanupLifetime = options.lifetime === true;
|
||||
statsCleanupVocab = statsCleanupLifetime ? false : options.vocab !== false;
|
||||
statsCleanupDuplicateLines = options.duplicateLines === true;
|
||||
if (statsCleanupLifetime && statsCleanupDuplicateLines) {
|
||||
throw new Error('Stats cleanup runs one mode at a time.');
|
||||
}
|
||||
// Vocabulary cleanup stays the default so `stats cleanup` keeps its old meaning.
|
||||
statsCleanupVocab =
|
||||
statsCleanupLifetime || statsCleanupDuplicateLines ? false : options.vocab !== false;
|
||||
statsCleanupDryRun = options.dryRun === true;
|
||||
statsCleanupLookbackDays = parseStatsLookbackDays(options.lookbackDays);
|
||||
} else if (normalizedAction === 'rebuild' || normalizedAction === 'backfill') {
|
||||
statsCleanup = true;
|
||||
statsCleanupLifetime = true;
|
||||
@@ -483,6 +518,9 @@ export function parseCliPrograms(
|
||||
statsCleanup,
|
||||
statsCleanupVocab,
|
||||
statsCleanupLifetime,
|
||||
statsCleanupDuplicateLines,
|
||||
statsCleanupDryRun,
|
||||
statsCleanupLookbackDays,
|
||||
statsLogLevel,
|
||||
syncTriggered,
|
||||
syncCliTokens,
|
||||
|
||||
@@ -232,6 +232,29 @@ test('parseArgs maps lifetime stats cleanup flag', () => {
|
||||
assert.equal(parsed.statsCleanupLifetime, true);
|
||||
});
|
||||
|
||||
test('parseArgs maps duplicate-line stats cleanup flags', () => {
|
||||
const parsed = parseArgs(
|
||||
['stats', 'cleanup', '--duplicate-lines', '--dry-run', '--lookback-days', '30'],
|
||||
'subminer',
|
||||
{},
|
||||
);
|
||||
|
||||
assert.equal(parsed.statsCleanup, true);
|
||||
assert.equal(parsed.statsCleanupVocab, false);
|
||||
assert.equal(parsed.statsCleanupDuplicateLines, true);
|
||||
assert.equal(parsed.statsCleanupDryRun, true);
|
||||
assert.equal(parsed.statsCleanupLookbackDays, 30);
|
||||
});
|
||||
|
||||
test('parseArgs rejects duplicate-line flags without the duplicate-lines mode', () => {
|
||||
const error = withProcessExitIntercept(() => {
|
||||
parseArgs(['stats', 'cleanup', '--dry-run'], 'subminer', {});
|
||||
});
|
||||
|
||||
assert.equal(error.code, 1);
|
||||
assert.match(error.stderr, /--dry-run and --lookback-days require --duplicate-lines/);
|
||||
});
|
||||
|
||||
test('parseArgs rejects cleanup-only stats flags without cleanup action', () => {
|
||||
const error = withProcessExitIntercept(() => {
|
||||
parseArgs(['stats', '--vocab'], 'subminer', {});
|
||||
@@ -239,7 +262,10 @@ test('parseArgs rejects cleanup-only stats flags without cleanup action', () =>
|
||||
|
||||
assert.equal(error.code, 1);
|
||||
assert.match(error.message, /exit:1/);
|
||||
assert.match(error.stderr, /Stats --vocab and --lifetime flags require the cleanup action/);
|
||||
assert.match(
|
||||
error.stderr,
|
||||
/Stats --vocab, --lifetime and --duplicate-lines flags require the cleanup action/,
|
||||
);
|
||||
});
|
||||
|
||||
test('parseArgs maps stats rebuild action to cleanup lifetime mode', () => {
|
||||
|
||||
@@ -142,6 +142,9 @@ export interface Args {
|
||||
statsCleanup?: boolean;
|
||||
statsCleanupVocab?: boolean;
|
||||
statsCleanupLifetime?: boolean;
|
||||
statsCleanupDuplicateLines?: boolean;
|
||||
statsCleanupDryRun?: boolean;
|
||||
statsCleanupLookbackDays?: number;
|
||||
dictionaryTarget?: string;
|
||||
doctor: boolean;
|
||||
doctorRefreshKnownWords: boolean;
|
||||
|
||||
Reference in New Issue
Block a user