mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-30 07:21:32 -07:00
fix(immersion): use localtime rollup keys and guard sentence search limi
- Remove UTC-based UNION branch from getRollupGroupsForSessions; keep only localtime rollup - Add resolveSentenceSearchLimit to clamp/sanitize infinity and negative limit values - Switch onMiningImageEvent from queued to latest-value IPC listener (drop stale frames) - Add regression tests for rollup key locality and sentence search limit edge cases
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
test('getRollupGroupsForSessions uses only localtime rollup keys', () => {
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
path.join(process.cwd(), 'src/core/services/immersion-tracker/maintenance.ts'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const start = source.indexOf('export function getRollupGroupsForSessions');
|
||||||
|
const end = source.indexOf('export function refreshRollupsForGroupsInTransaction');
|
||||||
|
const functionSource = source.slice(start, end);
|
||||||
|
|
||||||
|
assert.match(functionSource, /'unixepoch', 'localtime'/);
|
||||||
|
assert.doesNotMatch(functionSource, /UNION/);
|
||||||
|
assert.doesNotMatch(functionSource, /86400000/);
|
||||||
|
});
|
||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
upsertCoverArt,
|
upsertCoverArt,
|
||||||
} from '../query.js';
|
} from '../query.js';
|
||||||
import {
|
import {
|
||||||
|
getLocalEpochDay,
|
||||||
getShiftedLocalDaySec,
|
getShiftedLocalDaySec,
|
||||||
getStartOfLocalDayTimestamp,
|
getStartOfLocalDayTimestamp,
|
||||||
toDbTimestamp,
|
toDbTimestamp,
|
||||||
@@ -3776,6 +3777,8 @@ test('searchSubtitleSentences searches known subtitle lines and returns media co
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
assert.deepEqual(searchSubtitleSentences(db, 'monsters', 10), []);
|
assert.deepEqual(searchSubtitleSentences(db, 'monsters', 10), []);
|
||||||
|
assert.doesNotThrow(() => searchSubtitleSentences(db, '魔物', Number.POSITIVE_INFINITY));
|
||||||
|
assert.equal(searchSubtitleSentences(db, '魔物', -1).length, 1);
|
||||||
} finally {
|
} finally {
|
||||||
db.close();
|
db.close();
|
||||||
cleanupDbPath(dbPath);
|
cleanupDbPath(dbPath);
|
||||||
@@ -4196,8 +4199,14 @@ test('deleteSession removes zero-session media from library and trends', () => {
|
|||||||
|
|
||||||
const startedAtMs = 9_000_000;
|
const startedAtMs = 9_000_000;
|
||||||
const endedAtMs = startedAtMs + 120_000;
|
const endedAtMs = startedAtMs + 120_000;
|
||||||
const rollupDay = Math.floor(startedAtMs / 86_400_000);
|
const rollupDay = getLocalEpochDay(db, startedAtMs);
|
||||||
const rollupMonth = 197001;
|
const rollupMonth = (
|
||||||
|
db
|
||||||
|
.prepare(
|
||||||
|
"SELECT CAST(strftime('%Y%m', CAST(? AS REAL) / 1000, 'unixepoch', 'localtime') AS INTEGER) AS rollupMonth",
|
||||||
|
)
|
||||||
|
.get(startedAtMs) as { rollupMonth: number }
|
||||||
|
).rollupMonth;
|
||||||
const { sessionId } = startSessionRecord(db, videoId, startedAtMs);
|
const { sessionId } = startSessionRecord(db, videoId, startedAtMs);
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
|
|||||||
@@ -319,16 +319,9 @@ export function getRollupGroupsForSessions(db: DatabaseSync, sessionIds: number[
|
|||||||
video_id
|
video_id
|
||||||
FROM imm_sessions
|
FROM imm_sessions
|
||||||
WHERE session_id IN (${placeholders})
|
WHERE session_id IN (${placeholders})
|
||||||
UNION
|
|
||||||
SELECT DISTINCT
|
|
||||||
CAST(CAST(started_at_ms AS REAL) / 86400000 AS INTEGER) AS rollup_day,
|
|
||||||
CAST(strftime('%Y%m', CAST(started_at_ms AS REAL) / 1000, 'unixepoch') AS INTEGER) AS rollup_month,
|
|
||||||
video_id
|
|
||||||
FROM imm_sessions
|
|
||||||
WHERE session_id IN (${placeholders})
|
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.all(...sessionIds, ...sessionIds) as RollupGroupRow[];
|
.all(...sessionIds) as RollupGroupRow[];
|
||||||
|
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
rollupDay: row.rollup_day,
|
rollupDay: row.rollup_day,
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ import { nowMs } from './time';
|
|||||||
|
|
||||||
const VOCABULARY_STATS_FILTER_OVERSAMPLE_FACTOR = 4;
|
const VOCABULARY_STATS_FILTER_OVERSAMPLE_FACTOR = 4;
|
||||||
const VOCABULARY_STATS_FILTER_OVERSAMPLE_MIN = 100;
|
const VOCABULARY_STATS_FILTER_OVERSAMPLE_MIN = 100;
|
||||||
|
const SENTENCE_SEARCH_DEFAULT_LIMIT = 50;
|
||||||
|
const SENTENCE_SEARCH_MAX_LIMIT = 100;
|
||||||
|
|
||||||
|
function resolveSentenceSearchLimit(limit: number): number {
|
||||||
|
if (!Number.isFinite(limit)) return SENTENCE_SEARCH_DEFAULT_LIMIT;
|
||||||
|
const normalized = Math.floor(limit);
|
||||||
|
if (normalized <= 0) return SENTENCE_SEARCH_DEFAULT_LIMIT;
|
||||||
|
return Math.min(normalized, SENTENCE_SEARCH_MAX_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
function splitSearchTerms(query: string): string[] {
|
function splitSearchTerms(query: string): string[] {
|
||||||
return query
|
return query
|
||||||
@@ -228,10 +237,11 @@ export function getKanjiOccurrences(
|
|||||||
export function searchSubtitleSentences(
|
export function searchSubtitleSentences(
|
||||||
db: DatabaseSync,
|
db: DatabaseSync,
|
||||||
query: string,
|
query: string,
|
||||||
limit = 50,
|
limit = SENTENCE_SEARCH_DEFAULT_LIMIT,
|
||||||
): SentenceSearchResultRow[] {
|
): SentenceSearchResultRow[] {
|
||||||
const terms = splitSearchTerms(query);
|
const terms = splitSearchTerms(query);
|
||||||
if (terms.length === 0) return [];
|
if (terms.length === 0) return [];
|
||||||
|
const resolvedLimit = resolveSentenceSearchLimit(limit);
|
||||||
|
|
||||||
const clauses: string[] = [];
|
const clauses: string[] = [];
|
||||||
const params: string[] = [];
|
const params: string[] = [];
|
||||||
@@ -270,7 +280,7 @@ export function searchSubtitleSentences(
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.all(...params, limit) as unknown as SentenceSearchResultRow[];
|
.all(...params, resolvedLimit) as unknown as SentenceSearchResultRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionEvents(
|
export function getSessionEvents(
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ test('overlay preload buffers only latest subtitle state until renderer listener
|
|||||||
assert.match(source, /onSubtitle:\s*\(callback:[\s\S]+?onSubtitleSetEvent\(callback\);/);
|
assert.match(source, /onSubtitle:\s*\(callback:[\s\S]+?onSubtitleSetEvent\(callback\);/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('overlay preload buffers only latest mining image payload before listener registration', () => {
|
||||||
|
const source = fs.readFileSync(path.join(process.cwd(), 'src', 'preload.ts'), 'utf8');
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/const onMiningImageEvent =\s*createLatestValueIpcListenerWithPayload<MiningImagePayload>\(\s*IPC_CHANNELS\.event\.miningImage,/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('overlay preload exposes queued pointer recovery requests', () => {
|
test('overlay preload exposes queued pointer recovery requests', () => {
|
||||||
const source = fs.readFileSync(path.join(process.cwd(), 'src', 'preload.ts'), 'utf8');
|
const source = fs.readFileSync(path.join(process.cwd(), 'src', 'preload.ts'), 'utf8');
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -223,7 +223,7 @@ const onSecondarySubtitleModeEvent = createLatestValueIpcListenerWithPayload<Sec
|
|||||||
IPC_CHANNELS.event.secondarySubtitleMode,
|
IPC_CHANNELS.event.secondarySubtitleMode,
|
||||||
(payload) => payload as SecondarySubMode,
|
(payload) => payload as SecondarySubMode,
|
||||||
);
|
);
|
||||||
const onMiningImageEvent = createQueuedIpcListenerWithPayload<MiningImagePayload>(
|
const onMiningImageEvent = createLatestValueIpcListenerWithPayload<MiningImagePayload>(
|
||||||
IPC_CHANNELS.event.miningImage,
|
IPC_CHANNELS.event.miningImage,
|
||||||
(payload) => payload as MiningImagePayload,
|
(payload) => payload as MiningImagePayload,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export function posColor(pos: string): string {
|
|||||||
|
|
||||||
export function PosBadge({ pos }: { pos: string }) {
|
export function PosBadge({ pos }: { pos: string }) {
|
||||||
return (
|
return (
|
||||||
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium whitespace-nowrap ${posColor(pos)}`}>
|
<span
|
||||||
|
className={`rounded-full px-2 py-0.5 text-[11px] font-medium whitespace-nowrap ${posColor(pos)}`}
|
||||||
|
>
|
||||||
{pos.replace(/_/g, ' ')}
|
{pos.replace(/_/g, ' ')}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user