refactor: split immersion tracker query modules

This commit is contained in:
2026-03-27 00:33:43 -07:00
parent 8c633f7e48
commit ac857e932e
13 changed files with 2687 additions and 2634 deletions

View File

@@ -0,0 +1,273 @@
import type { DatabaseSync } from './sqlite';
export const ACTIVE_SESSION_METRICS_CTE = `
WITH active_session_metrics AS (
SELECT
t.session_id AS sessionId,
MAX(t.total_watched_ms) AS totalWatchedMs,
MAX(t.active_watched_ms) AS activeWatchedMs,
MAX(t.lines_seen) AS linesSeen,
MAX(t.tokens_seen) AS tokensSeen,
MAX(t.cards_mined) AS cardsMined,
MAX(t.lookup_count) AS lookupCount,
MAX(t.lookup_hits) AS lookupHits,
MAX(t.yomitan_lookup_count) AS yomitanLookupCount
FROM imm_session_telemetry t
JOIN imm_sessions s ON s.session_id = t.session_id
WHERE s.ended_at_ms IS NULL
GROUP BY t.session_id
)
`;
export function makePlaceholders(values: number[]): string {
return values.map(() => '?').join(',');
}
export function resolvedCoverBlobExpr(mediaAlias: string, blobStoreAlias: string): string {
return `COALESCE(${blobStoreAlias}.cover_blob, CASE WHEN ${mediaAlias}.cover_blob_hash IS NULL THEN ${mediaAlias}.cover_blob ELSE NULL END)`;
}
export function cleanupUnusedCoverArtBlobHash(db: DatabaseSync, blobHash: string | null): void {
if (!blobHash) {
return;
}
db.prepare(
`
DELETE FROM imm_cover_art_blobs
WHERE blob_hash = ?
AND NOT EXISTS (
SELECT 1
FROM imm_media_art
WHERE cover_blob_hash = ?
)
`,
).run(blobHash, blobHash);
}
export function findSharedCoverBlobHash(
db: DatabaseSync,
videoId: number,
anilistId: number | null,
coverUrl: string | null,
): string | null {
if (anilistId !== null) {
const byAnilist = db
.prepare(
`
SELECT cover_blob_hash AS coverBlobHash
FROM imm_media_art
WHERE video_id != ?
AND anilist_id = ?
AND cover_blob_hash IS NOT NULL
ORDER BY fetched_at_ms DESC, video_id DESC
LIMIT 1
`,
)
.get(videoId, anilistId) as { coverBlobHash: string | null } | undefined;
if (byAnilist?.coverBlobHash) {
return byAnilist.coverBlobHash;
}
}
if (coverUrl) {
const byUrl = db
.prepare(
`
SELECT cover_blob_hash AS coverBlobHash
FROM imm_media_art
WHERE video_id != ?
AND cover_url = ?
AND cover_blob_hash IS NOT NULL
ORDER BY fetched_at_ms DESC, video_id DESC
LIMIT 1
`,
)
.get(videoId, coverUrl) as { coverBlobHash: string | null } | undefined;
return byUrl?.coverBlobHash ?? null;
}
return null;
}
export function getAffectedWordIdsForSessions(db: DatabaseSync, sessionIds: number[]): number[] {
if (sessionIds.length === 0) {
return [];
}
return (
db
.prepare(
`
SELECT DISTINCT o.word_id AS wordId
FROM imm_word_line_occurrences o
JOIN imm_subtitle_lines sl ON sl.line_id = o.line_id
WHERE sl.session_id IN (${makePlaceholders(sessionIds)})
`,
)
.all(...sessionIds) as Array<{ wordId: number }>
).map((row) => row.wordId);
}
export function getAffectedKanjiIdsForSessions(db: DatabaseSync, sessionIds: number[]): number[] {
if (sessionIds.length === 0) {
return [];
}
return (
db
.prepare(
`
SELECT DISTINCT o.kanji_id AS kanjiId
FROM imm_kanji_line_occurrences o
JOIN imm_subtitle_lines sl ON sl.line_id = o.line_id
WHERE sl.session_id IN (${makePlaceholders(sessionIds)})
`,
)
.all(...sessionIds) as Array<{ kanjiId: number }>
).map((row) => row.kanjiId);
}
export function getAffectedWordIdsForVideo(db: DatabaseSync, videoId: number): number[] {
return (
db
.prepare(
`
SELECT DISTINCT o.word_id AS wordId
FROM imm_word_line_occurrences o
JOIN imm_subtitle_lines sl ON sl.line_id = o.line_id
WHERE sl.video_id = ?
`,
)
.all(videoId) as Array<{ wordId: number }>
).map((row) => row.wordId);
}
export function getAffectedKanjiIdsForVideo(db: DatabaseSync, videoId: number): number[] {
return (
db
.prepare(
`
SELECT DISTINCT o.kanji_id AS kanjiId
FROM imm_kanji_line_occurrences o
JOIN imm_subtitle_lines sl ON sl.line_id = o.line_id
WHERE sl.video_id = ?
`,
)
.all(videoId) as Array<{ kanjiId: number }>
).map((row) => row.kanjiId);
}
function refreshWordAggregates(db: DatabaseSync, wordIds: number[]): void {
if (wordIds.length === 0) {
return;
}
const rows = db
.prepare(
`
SELECT
w.id AS wordId,
COALESCE(SUM(o.occurrence_count), 0) AS frequency,
MIN(COALESCE(sl.CREATED_DATE, sl.LAST_UPDATE_DATE)) AS firstSeen,
MAX(COALESCE(sl.LAST_UPDATE_DATE, sl.CREATED_DATE)) AS lastSeen
FROM imm_words w
LEFT JOIN imm_word_line_occurrences o ON o.word_id = w.id
LEFT JOIN imm_subtitle_lines sl ON sl.line_id = o.line_id
WHERE w.id IN (${makePlaceholders(wordIds)})
GROUP BY w.id
`,
)
.all(...wordIds) as Array<{
wordId: number;
frequency: number;
firstSeen: number | null;
lastSeen: number | null;
}>;
const updateStmt = db.prepare(
`
UPDATE imm_words
SET frequency = ?, first_seen = ?, last_seen = ?
WHERE id = ?
`,
);
const deleteStmt = db.prepare('DELETE FROM imm_words WHERE id = ?');
for (const row of rows) {
if (row.frequency <= 0 || row.firstSeen === null || row.lastSeen === null) {
deleteStmt.run(row.wordId);
continue;
}
updateStmt.run(row.frequency, row.firstSeen, row.lastSeen, row.wordId);
}
}
function refreshKanjiAggregates(db: DatabaseSync, kanjiIds: number[]): void {
if (kanjiIds.length === 0) {
return;
}
const rows = db
.prepare(
`
SELECT
k.id AS kanjiId,
COALESCE(SUM(o.occurrence_count), 0) AS frequency,
MIN(COALESCE(sl.CREATED_DATE, sl.LAST_UPDATE_DATE)) AS firstSeen,
MAX(COALESCE(sl.LAST_UPDATE_DATE, sl.CREATED_DATE)) AS lastSeen
FROM imm_kanji k
LEFT JOIN imm_kanji_line_occurrences o ON o.kanji_id = k.id
LEFT JOIN imm_subtitle_lines sl ON sl.line_id = o.line_id
WHERE k.id IN (${makePlaceholders(kanjiIds)})
GROUP BY k.id
`,
)
.all(...kanjiIds) as Array<{
kanjiId: number;
frequency: number;
firstSeen: number | null;
lastSeen: number | null;
}>;
const updateStmt = db.prepare(
`
UPDATE imm_kanji
SET frequency = ?, first_seen = ?, last_seen = ?
WHERE id = ?
`,
);
const deleteStmt = db.prepare('DELETE FROM imm_kanji WHERE id = ?');
for (const row of rows) {
if (row.frequency <= 0 || row.firstSeen === null || row.lastSeen === null) {
deleteStmt.run(row.kanjiId);
continue;
}
updateStmt.run(row.frequency, row.firstSeen, row.lastSeen, row.kanjiId);
}
}
export function refreshLexicalAggregates(
db: DatabaseSync,
wordIds: number[],
kanjiIds: number[],
): void {
refreshWordAggregates(db, [...new Set(wordIds)]);
refreshKanjiAggregates(db, [...new Set(kanjiIds)]);
}
export function deleteSessionsByIds(db: DatabaseSync, sessionIds: number[]): void {
if (sessionIds.length === 0) {
return;
}
const placeholders = makePlaceholders(sessionIds);
db.prepare(`DELETE FROM imm_subtitle_lines WHERE session_id IN (${placeholders})`).run(
...sessionIds,
);
db.prepare(`DELETE FROM imm_session_telemetry WHERE session_id IN (${placeholders})`).run(
...sessionIds,
);
db.prepare(`DELETE FROM imm_session_events WHERE session_id IN (${placeholders})`).run(
...sessionIds,
);
db.prepare(`DELETE FROM imm_sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
}