"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createImmersionDbFixture = createImmersionDbFixture; exports.insertFixtureSession = insertFixtureSession; const bun_sqlite_1 = require("bun:sqlite"); const types_js_1 = require("../../src/core/services/immersion-tracker/types.js"); const immersion_db_schema_js_1 = require("./immersion-db-schema.js"); function createImmersionDbFixture(dbPath) { const db = new bun_sqlite_1.Database(dbPath, { create: true }); try { db.run('PRAGMA foreign_keys = ON'); db.run('PRAGMA journal_mode = WAL'); for (const statement of immersion_db_schema_js_1.IMMERSION_DB_FIXTURE_DDL.split(';')) { const sql = statement.trim(); if (sql) db.run(sql); } db.prepare('INSERT INTO imm_schema_version (schema_version, applied_at_ms) VALUES (?, ?)').run(types_js_1.SCHEMA_VERSION, String(Date.now())); db.prepare(`INSERT INTO imm_rollup_state(state_key, state_value) VALUES ('last_rollup_sample_ms', 0)`).run(); db.prepare(`INSERT INTO imm_lifetime_global(global_id, CREATED_DATE, LAST_UPDATE_DATE) VALUES (1, ?, ?)`).run(String(Date.now()), String(Date.now())); } finally { db.close(); } } let uniqueCounter = 0; /** * Insert a session (with video/anime/subtitle-line/occurrence rows) the way * the app would have recorded it, optionally crediting lifetime aggregates * the way applySessionLifetimeSummary does for locally-recorded sessions. */ function insertFixtureSession(dbPath, input) { const db = new bun_sqlite_1.Database(dbPath, { readwrite: true }); const stamp = String(input.startedAtMs); try { db.run('PRAGMA foreign_keys = ON'); let animeId = null; if (input.animeTitleKey) { const existing = db .query('SELECT anime_id FROM imm_anime WHERE normalized_title_key = ?') .get(input.animeTitleKey); animeId = existing ? existing.anime_id : Number(db .prepare(`INSERT INTO imm_anime (normalized_title_key, canonical_title, episodes_total, CREATED_DATE, LAST_UPDATE_DATE) VALUES (?, ?, ?, ?, ?)`) .run(input.animeTitleKey, input.animeTitleKey, input.animeEpisodesTotal ?? null, stamp, stamp).lastInsertRowid); } const existingVideo = db .query('SELECT video_id FROM imm_videos WHERE video_key = ?') .get(input.videoKey); const videoId = existingVideo ? existingVideo.video_id : Number(db .prepare(`INSERT INTO imm_videos (video_key, anime_id, canonical_title, source_type, source_path, watched, duration_ms, CREATED_DATE, LAST_UPDATE_DATE) VALUES (?, ?, ?, 1, ?, ?, 1440000, ?, ?)`) .run(input.videoKey, animeId, input.videoKey, `/videos/${input.videoKey}.mkv`, input.watched ? 1 : 0, stamp, stamp).lastInsertRowid); if (input.watched) { db.prepare('UPDATE imm_videos SET watched = 1 WHERE video_id = ?').run(videoId); } const endedAtMs = input.endedAtMs === undefined ? input.startedAtMs + 1_500_000 : input.endedAtMs; const activeWatchedMs = input.activeWatchedMs ?? 1_200_000; const cardsMined = input.cardsMined ?? 2; const linesSeen = input.linesSeen ?? 100; const tokensSeen = input.tokensSeen ?? 800; const sessionId = Number(db .prepare(`INSERT INTO imm_sessions ( session_uuid, video_id, started_at_ms, ended_at_ms, status, total_watched_ms, active_watched_ms, lines_seen, tokens_seen, cards_mined, CREATED_DATE, LAST_UPDATE_DATE ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) .run(input.uuid, videoId, String(input.startedAtMs), endedAtMs === null ? null : String(endedAtMs), endedAtMs === null ? 1 : 2, activeWatchedMs, activeWatchedMs, linesSeen, tokensSeen, cardsMined, stamp, stamp).lastInsertRowid); db.prepare(`INSERT INTO imm_session_telemetry ( session_id, sample_ms, total_watched_ms, active_watched_ms, lines_seen, tokens_seen, cards_mined, CREATED_DATE, LAST_UPDATE_DATE ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(sessionId, String(endedAtMs ?? input.startedAtMs), activeWatchedMs, activeWatchedMs, linesSeen, tokensSeen, cardsMined, stamp, stamp); for (const word of input.words ?? []) { const existing = db .query('SELECT id FROM imm_words WHERE headword = ? AND word = ? AND reading = ?') .get(word.headword, word.word, word.reading); const wordId = existing ? existing.id : Number(db .prepare(`INSERT INTO imm_words (headword, word, reading, first_seen, last_seen, frequency) VALUES (?, ?, ?, ?, ?, 0)`) .run(word.headword, word.word, word.reading, Math.floor(input.startedAtMs / 1000), Math.floor(input.startedAtMs / 1000)).lastInsertRowid); uniqueCounter += 1; const lineId = Number(db .prepare(`INSERT INTO imm_subtitle_lines (session_id, video_id, anime_id, line_index, text, CREATED_DATE, LAST_UPDATE_DATE) VALUES (?, ?, ?, ?, ?, ?, ?)`) .run(sessionId, videoId, animeId, uniqueCounter, `line ${word.word}`, input.startedAtMs, input.startedAtMs).lastInsertRowid); db.prepare('INSERT INTO imm_word_line_occurrences (line_id, word_id, occurrence_count) VALUES (?, ?, ?)').run(lineId, wordId, word.count); db.prepare('UPDATE imm_words SET frequency = frequency + ? WHERE id = ?').run(word.count, wordId); } if (input.applyLifetime && endedAtMs !== null) { applyFixtureLifetime(db, sessionId, videoId, animeId, { endedAtMs, startedAtMs: input.startedAtMs, activeWatchedMs, cardsMined, linesSeen, tokensSeen, watched: Boolean(input.watched), episodesTotal: input.animeEpisodesTotal ?? null, }); } } finally { db.close(); } } function applyFixtureLifetime(db, sessionId, videoId, animeId, data) { const stamp = String(data.endedAtMs); db.prepare(`INSERT INTO imm_lifetime_applied_sessions (session_id, applied_at_ms, CREATED_DATE, LAST_UPDATE_DATE) VALUES (?, ?, ?, ?)`).run(sessionId, String(data.endedAtMs), stamp, stamp); const mediaLifetime = db .query('SELECT completed FROM imm_lifetime_media WHERE video_id = ?') .get(videoId); const isFirstSessionForVideo = !mediaLifetime; const isFirstCompleted = data.watched && Number(mediaLifetime?.completed ?? 0) <= 0; const dayExpr = `CAST(julianday(CAST(started_at_ms AS REAL) / 1000, 'unixepoch', 'localtime') - 2440587.5 AS INTEGER)`; const otherOnDay = db .query(`SELECT 1 FROM imm_sessions WHERE session_id != ? AND ${dayExpr} = CAST(julianday(CAST(? AS REAL) / 1000, 'unixepoch', 'localtime') - 2440587.5 AS INTEGER) LIMIT 1`) .get(sessionId, String(data.startedAtMs)); db.prepare(`UPDATE imm_lifetime_global SET total_sessions = total_sessions + 1, total_active_ms = total_active_ms + ?, total_cards = total_cards + ?, active_days = active_days + ?, episodes_started = episodes_started + ?, episodes_completed = episodes_completed + ? WHERE global_id = 1`).run(data.activeWatchedMs, data.cardsMined, otherOnDay ? 0 : 1, isFirstSessionForVideo ? 1 : 0, isFirstCompleted ? 1 : 0); db.prepare(`INSERT INTO imm_lifetime_media (video_id, total_sessions, total_active_ms, total_cards, total_lines_seen, total_tokens_seen, completed, first_watched_ms, last_watched_ms, CREATED_DATE, LAST_UPDATE_DATE) VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(video_id) DO UPDATE SET total_sessions = total_sessions + 1, total_active_ms = total_active_ms + excluded.total_active_ms, total_cards = total_cards + excluded.total_cards, total_lines_seen = total_lines_seen + excluded.total_lines_seen, total_tokens_seen = total_tokens_seen + excluded.total_tokens_seen, completed = MAX(completed, excluded.completed), last_watched_ms = excluded.last_watched_ms`).run(videoId, data.activeWatchedMs, data.cardsMined, data.linesSeen, data.tokensSeen, data.watched ? 1 : 0, String(data.startedAtMs), String(data.endedAtMs), stamp, stamp); if (animeId !== null) { db.prepare(`INSERT INTO imm_lifetime_anime (anime_id, total_sessions, total_active_ms, total_cards, total_lines_seen, total_tokens_seen, episodes_started, episodes_completed, first_watched_ms, last_watched_ms, CREATED_DATE, LAST_UPDATE_DATE) VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(anime_id) DO UPDATE SET total_sessions = total_sessions + 1, total_active_ms = total_active_ms + excluded.total_active_ms, total_cards = total_cards + excluded.total_cards, total_lines_seen = total_lines_seen + excluded.total_lines_seen, total_tokens_seen = total_tokens_seen + excluded.total_tokens_seen, episodes_started = episodes_started + excluded.episodes_started, episodes_completed = episodes_completed + excluded.episodes_completed, last_watched_ms = excluded.last_watched_ms`).run(animeId, data.activeWatchedMs, data.cardsMined, data.linesSeen, data.tokensSeen, isFirstSessionForVideo ? 1 : 0, isFirstCompleted ? 1 : 0, String(data.startedAtMs), String(data.endedAtMs), stamp, stamp); } } //# sourceMappingURL=immersion-db-fixture.js.map