mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-09 16:19:25 -07:00
fix(ci): restore stats-server fallback and unblock coverage tests
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: TASK-238.1
|
id: TASK-238.1
|
||||||
title: Extract main-window and overlay-window composition from src/main.ts
|
title: Extract main-window and overlay-window composition from src/main.ts
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-03-26 20:49'
|
created_date: '2026-03-26 20:49'
|
||||||
labels:
|
labels:
|
||||||
@@ -43,3 +43,11 @@ priority: high
|
|||||||
3. Update the composition root to consume the new modules and keep side effects/app state ownership explicit.
|
3. Update the composition root to consume the new modules and keep side effects/app state ownership explicit.
|
||||||
4. Verify with focused runtime/window tests plus `bun run typecheck`.
|
4. Verify with focused runtime/window tests plus `bun run typecheck`.
|
||||||
<!-- SECTION:PLAN:END -->
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Completion Notes
|
||||||
|
|
||||||
|
- Window composition now flows through `src/main/runtime/setup-window-factory.ts` and `src/main/runtime/overlay-window-factory.ts`, with `src/main/runtime/overlay-window-runtime-handlers.ts` composing the main/modal overlay entrypoints.
|
||||||
|
- `src/main.ts` keeps dependency wiring and state ownership, while the named runtime helpers own the reusable window-creation surfaces.
|
||||||
|
- Verification:
|
||||||
|
- `bun test src/main/runtime/overlay-window-factory.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts`
|
||||||
|
- `bun run typecheck` failed on unrelated existing errors in `src/core/services/immersion-tracker/lifetime.ts`, `src/core/services/immersion-tracker/maintenance.ts`, and `src/core/services/stats-server.ts`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: TASK-238.2
|
id: TASK-238.2
|
||||||
title: Extract CLI and headless command wiring from src/main.ts
|
title: Extract CLI and headless command wiring from src/main.ts
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: '2026-03-26 20:49'
|
created_date: '2026-03-26 20:49'
|
||||||
labels:
|
labels:
|
||||||
@@ -44,3 +44,11 @@ priority: high
|
|||||||
3. Keep Electron app ownership in `src/main.ts`; move only CLI orchestration and context assembly.
|
3. Keep Electron app ownership in `src/main.ts`; move only CLI orchestration and context assembly.
|
||||||
4. Verify with CLI-focused tests plus `bun run typecheck`.
|
4. Verify with CLI-focused tests plus `bun run typecheck`.
|
||||||
<!-- SECTION:PLAN:END -->
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Completion Notes
|
||||||
|
|
||||||
|
- CLI and headless startup wiring now lives behind `src/main/runtime/composers/cli-startup-composer.ts`, `src/main/runtime/cli-command-runtime-handler.ts`, `src/main/runtime/initial-args-handler.ts`, and `src/main/runtime/composers/headless-startup-composer.ts`.
|
||||||
|
- `src/main.ts` now passes CLI/context dependencies into those runtime surfaces instead of holding the full orchestration inline.
|
||||||
|
- Verification:
|
||||||
|
- `bun test src/main/runtime/composers/cli-startup-composer.test.ts src/main/runtime/initial-args-handler.test.ts src/main/runtime/cli-command-runtime-handler.test.ts`
|
||||||
|
- `bun run typecheck` failed on unrelated existing errors in `src/core/services/immersion-tracker/lifetime.ts`, `src/core/services/immersion-tracker/maintenance.ts`, and `src/core/services/stats-server.ts`
|
||||||
|
|||||||
@@ -41,28 +41,6 @@ The update flow:
|
|||||||
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
|
3. **Progress check** -- SubMiner fetches your current list entry for the matched media. If your recorded progress already meets or exceeds the detected episode, the update is skipped.
|
||||||
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
|
4. **Mutation** -- A `SaveMediaListEntry` mutation sets the new progress and marks the entry as `CURRENT`.
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef ext fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
|
|
||||||
Play["Media Plays"]:::step
|
|
||||||
Detect["Episode Detected"]:::action
|
|
||||||
Queue["Update Queue"]:::action
|
|
||||||
Rate["Rate Limiter"]:::enrich
|
|
||||||
GQL["GraphQL Mutation"]:::ext
|
|
||||||
Done["Progress Updated"]:::result
|
|
||||||
|
|
||||||
Play --> Detect
|
|
||||||
Detect --> Queue
|
|
||||||
Queue --> Rate
|
|
||||||
Rate --> GQL
|
|
||||||
GQL --> Done
|
|
||||||
```
|
|
||||||
|
|
||||||
## Update Queue and Retry
|
## Update Queue and Retry
|
||||||
|
|
||||||
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
|
Failed AniList updates are persisted to a retry queue on disk and retried with exponential backoff.
|
||||||
|
|||||||
@@ -31,30 +31,6 @@ The feature has three stages: **snapshot**, **merge**, and **match**.
|
|||||||
|
|
||||||
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color.
|
3. **Match** — During subtitle rendering, Yomitan scans subtitle text against all loaded dictionaries including the character dictionary. Tokens that match a character entry are flagged with `isNameMatch` and highlighted in the overlay with a distinct color.
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
classDef api fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef store fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef build fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef dict fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef render fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
|
|
||||||
AL["AniList API"]:::api
|
|
||||||
Snap["Snapshot JSON"]:::store
|
|
||||||
Merge["Merge"]:::build
|
|
||||||
ZIP["Yomitan ZIP"]:::dict
|
|
||||||
Yomi["Yomitan Import"]:::dict
|
|
||||||
Sub["Subtitle Scan"]:::render
|
|
||||||
HL["Name Highlight"]:::render
|
|
||||||
|
|
||||||
AL -->|"GraphQL"| Snap
|
|
||||||
Snap --> Merge
|
|
||||||
Merge --> ZIP
|
|
||||||
ZIP --> Yomi
|
|
||||||
Yomi --> Sub
|
|
||||||
Sub --> HL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Enabling the Feature
|
## Enabling the Feature
|
||||||
|
|
||||||
Character dictionary sync is disabled by default. To turn it on:
|
Character dictionary sync is disabled by default. To turn it on:
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ For internal architecture/workflow guidance, use `docs/README.md` at the repo ro
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- [Bun](https://bun.sh)
|
- Required for all contributor workflows:
|
||||||
|
- [Bun](https://bun.sh)
|
||||||
|
- `git` with submodule support
|
||||||
|
- Required by commands used on this page:
|
||||||
|
- `bash` for helper scripts such as `make dev-watch`, `bun run format:check:src`, and `bash scripts/verify-generated-launcher.sh`
|
||||||
|
- `unzip` on macOS/Linux for the bundled Yomitan build step inside `bun run build`
|
||||||
|
- `lua` for plugin/environment test lanes such as `bun run test:env` and `bun run test:launcher`
|
||||||
|
- Platform-specific / conditional:
|
||||||
|
- `swiftc` on macOS is optional. If absent, the build falls back to staging the Swift helper source instead of compiling the helper binary.
|
||||||
|
- Windows uses `powershell.exe` during the bundled Yomitan extraction step. A normal Windows install already provides it.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -21,6 +30,8 @@ bun install
|
|||||||
|
|
||||||
`make deps` is still available as a convenience wrapper around the same dependency install flow.
|
`make deps` is still available as a convenience wrapper around the same dependency install flow.
|
||||||
|
|
||||||
|
If you only need the default TypeScript/unit lanes, Bun plus the checked-in dependencies is enough after install. The extra tools above are only needed when you run the commands that invoke them.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -40,6 +51,8 @@ make build-launcher
|
|||||||
|
|
||||||
`bun run build` includes the Yomitan build step. It builds the bundled Chrome extension directly from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun.
|
`bun run build` includes the Yomitan build step. It builds the bundled Chrome extension directly from the `vendor/subminer-yomitan` submodule into `build/yomitan` using Bun.
|
||||||
|
|
||||||
|
On macOS/Linux, that build also shells out to `unzip` while extracting the Yomitan artifact. On macOS, the asset staging step will compile the helper with `swiftc` when available, then fall back to copying the `.swift` source if not.
|
||||||
|
|
||||||
## Launcher Artifact Workflow
|
## Launcher Artifact Workflow
|
||||||
|
|
||||||
- Source of truth: `launcher/*.ts`
|
- Source of truth: `launcher/*.ts`
|
||||||
@@ -94,6 +107,11 @@ bun run test:subtitle # maintained alass/ffsubsync subtitle surface
|
|||||||
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
|
- `bun run test:env` covers environment-sensitive checks: launcher smoke/plugin verification plus the Bun source SQLite lane.
|
||||||
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
|
- `bun run test:immersion:sqlite` is the reproducible persistence lane when you need real DB-backed SQLite coverage under Bun.
|
||||||
|
|
||||||
|
Command-specific test deps:
|
||||||
|
|
||||||
|
- `bun run test:env` and `bun run test:launcher` invoke Lua-based plugin checks, so `lua` must be installed.
|
||||||
|
- `bun run format:src` and `bun run format:check:src` invoke `bash scripts/prettier-scope.sh`.
|
||||||
|
|
||||||
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
|
The Bun-managed discovery lanes intentionally exclude a small compiled/runtime-focused set: `src/core/services/ipc.test.ts`, `src/core/services/anki-jimaku-ipc.test.ts`, `src/core/services/overlay-manager.test.ts`, `src/main/config-validation.test.ts`, `src/main/runtime/startup-config.test.ts`, and `src/main/runtime/registry.test.ts`. `bun run test:runtime:compat` keeps them in the standard workflow via `dist/**`.
|
||||||
|
|
||||||
Suggested local gate before handoff:
|
Suggested local gate before handoff:
|
||||||
|
|||||||
@@ -26,31 +26,6 @@ If no files match the current episode filter, a "Show all files" button lets you
|
|||||||
| `Arrow Up` / `Arrow Down` | Navigate entries or files |
|
| `Arrow Up` / `Arrow Down` | Navigate entries or files |
|
||||||
| `Escape` | Close modal |
|
| `Escape` | Close modal |
|
||||||
|
|
||||||
### Flow
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef result fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
|
|
||||||
|
|
||||||
Open["Open Jimaku modal (Ctrl+Shift+J)"]:::step
|
|
||||||
Parse["Auto-fill title, season, episode from filename"]:::enrich
|
|
||||||
Search["Search Jimaku API"]:::action
|
|
||||||
Entries["Browse matching entries"]:::action
|
|
||||||
Files["Browse subtitle files"]:::action
|
|
||||||
Download["Download selected file"]:::action
|
|
||||||
Load["Load subtitle into mpv"]:::result
|
|
||||||
|
|
||||||
Open --> Parse
|
|
||||||
Parse --> Search
|
|
||||||
Search --> Entries
|
|
||||||
Entries --> Files
|
|
||||||
Files --> Download
|
|
||||||
Download --> Load
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Add a `jimaku` section to your `config.jsonc`:
|
Add a `jimaku` section to your `config.jsonc`:
|
||||||
|
|||||||
@@ -17,41 +17,6 @@ When SubMiner detects a YouTube URL (or `ytsearch:` target), it pauses mpv at st
|
|||||||
4. **Download** --- Selected tracks are fetched via direct URL when available, falling back to `yt-dlp --write-subs` / `--write-auto-subs`. YouTube TimedText XML formats (`srv1`/`srv2`/`srv3`) are converted to VTT on the fly. Auto-generated VTT captions are normalized to remove rolling-caption duplication.
|
4. **Download** --- Selected tracks are fetched via direct URL when available, falling back to `yt-dlp --write-subs` / `--write-auto-subs`. YouTube TimedText XML formats (`srv1`/`srv2`/`srv3`) are converted to VTT on the fly. Auto-generated VTT captions are normalized to remove rolling-caption duplication.
|
||||||
5. **Load** --- Subtitle files are injected into mpv via `sub-add`. Playback resumes once the primary track is ready; secondary failures do not block.
|
5. **Load** --- Subtitle files are injected into mpv via `sub-add`. Playback resumes once the primary track is ready; secondary failures do not block.
|
||||||
|
|
||||||
## Pipeline Diagram
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
classDef step fill:#c6a0f6,stroke:#494d64,color:#24273a
|
|
||||||
classDef action fill:#8aadf4,stroke:#494d64,color:#24273a
|
|
||||||
classDef result fill:#a6da95,stroke:#494d64,color:#24273a
|
|
||||||
classDef enrich fill:#8bd5ca,stroke:#494d64,color:#24273a
|
|
||||||
classDef ext fill:#eed49f,stroke:#494d64,color:#24273a
|
|
||||||
|
|
||||||
A[YouTube URL detected]:::step
|
|
||||||
B[yt-dlp probe]:::ext
|
|
||||||
C[Track discovery]:::action
|
|
||||||
D{Auto or manual selection?}:::step
|
|
||||||
E[Auto-select best tracks]:::action
|
|
||||||
F[Manual picker — Ctrl+Alt+C]:::action
|
|
||||||
G[Download subtitle files]:::action
|
|
||||||
H[Convert TimedText to VTT]:::enrich
|
|
||||||
I[Normalize auto-caption duplicates]:::enrich
|
|
||||||
K[sub-add into mpv]:::action
|
|
||||||
L[Overlay renders subtitles]:::result
|
|
||||||
|
|
||||||
A --> B
|
|
||||||
B --> C
|
|
||||||
C --> D
|
|
||||||
D -- startup --> E
|
|
||||||
D -- user request --> F
|
|
||||||
E --> G
|
|
||||||
F --> G
|
|
||||||
G --> H
|
|
||||||
H --> I
|
|
||||||
I --> K
|
|
||||||
K --> L
|
|
||||||
```
|
|
||||||
|
|
||||||
## Auto-Load Flow
|
## Auto-Load Flow
|
||||||
|
|
||||||
On startup with a YouTube URL:
|
On startup with a YouTube URL:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { DatabaseSync } from './sqlite';
|
import type { DatabaseSync } from './sqlite';
|
||||||
import { finalizeSessionRecord } from './session';
|
import { finalizeSessionRecord } from './session';
|
||||||
import { nowMs } from './time';
|
import { nowMs } from './time';
|
||||||
|
import { toDbMs } from './query-shared';
|
||||||
import type { LifetimeRebuildSummary, SessionState } from './types';
|
import type { LifetimeRebuildSummary, SessionState } from './types';
|
||||||
|
|
||||||
interface TelemetryRow {
|
interface TelemetryRow {
|
||||||
@@ -19,11 +20,12 @@ interface AnimeRow {
|
|||||||
episodes_total: number | null;
|
episodes_total: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asPositiveNumber(value: number | null, fallback: number): number {
|
function asPositiveNumber(value: number | string | null, fallback: number): number {
|
||||||
if (value === null || !Number.isFinite(value)) {
|
const numericValue = typeof value === 'number' ? value : Number(value);
|
||||||
|
if (!Number.isFinite(numericValue)) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
return Math.max(0, Math.floor(value));
|
return Math.max(0, Math.floor(numericValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExistenceRow {
|
interface ExistenceRow {
|
||||||
@@ -41,22 +43,22 @@ interface LifetimeAnimeStateRow {
|
|||||||
interface RetainedSessionRow {
|
interface RetainedSessionRow {
|
||||||
sessionId: number;
|
sessionId: number;
|
||||||
videoId: number;
|
videoId: number;
|
||||||
startedAtMs: number;
|
startedAtMs: number | string;
|
||||||
endedAtMs: number;
|
endedAtMs: number | string;
|
||||||
lastMediaMs: number | null;
|
lastMediaMs: number | string | null;
|
||||||
totalWatchedMs: number;
|
totalWatchedMs: number | string;
|
||||||
activeWatchedMs: number;
|
activeWatchedMs: number | string;
|
||||||
linesSeen: number;
|
linesSeen: number | string;
|
||||||
tokensSeen: number;
|
tokensSeen: number | string;
|
||||||
cardsMined: number;
|
cardsMined: number | string;
|
||||||
lookupCount: number;
|
lookupCount: number | string;
|
||||||
lookupHits: number;
|
lookupHits: number | string;
|
||||||
yomitanLookupCount: number;
|
yomitanLookupCount: number | string;
|
||||||
pauseCount: number;
|
pauseCount: number | string;
|
||||||
pauseMs: number;
|
pauseMs: number | string;
|
||||||
seekForwardCount: number;
|
seekForwardCount: number | string;
|
||||||
seekBackwardCount: number;
|
seekBackwardCount: number | string;
|
||||||
mediaBufferEvents: number;
|
mediaBufferEvents: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasRetainedPriorSession(
|
function hasRetainedPriorSession(
|
||||||
@@ -110,7 +112,7 @@ function isFirstSessionForLocalDay(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetLifetimeSummaries(db: DatabaseSync, nowMs: number): void {
|
function resetLifetimeSummaries(db: DatabaseSync, nowMs: string): void {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
DELETE FROM imm_lifetime_anime;
|
DELETE FROM imm_lifetime_anime;
|
||||||
DELETE FROM imm_lifetime_media;
|
DELETE FROM imm_lifetime_media;
|
||||||
@@ -136,7 +138,7 @@ function resetLifetimeSummaries(db: DatabaseSync, nowMs: number): void {
|
|||||||
|
|
||||||
function rebuildLifetimeSummariesInternal(
|
function rebuildLifetimeSummariesInternal(
|
||||||
db: DatabaseSync,
|
db: DatabaseSync,
|
||||||
rebuiltAtMs: number,
|
rebuiltAtMs: string,
|
||||||
): LifetimeRebuildSummary {
|
): LifetimeRebuildSummary {
|
||||||
const sessions = db
|
const sessions = db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -178,30 +180,33 @@ function rebuildLifetimeSummariesInternal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toRebuildSessionState(row: RetainedSessionRow): SessionState {
|
function toRebuildSessionState(row: RetainedSessionRow): SessionState {
|
||||||
|
const startedAtMs = Number(row.startedAtMs);
|
||||||
|
const endedAtMs = Number(row.endedAtMs);
|
||||||
|
const lastMediaMs = row.lastMediaMs === null ? null : Number(row.lastMediaMs);
|
||||||
return {
|
return {
|
||||||
sessionId: row.sessionId,
|
sessionId: row.sessionId,
|
||||||
videoId: row.videoId,
|
videoId: row.videoId,
|
||||||
startedAtMs: row.startedAtMs,
|
startedAtMs,
|
||||||
currentLineIndex: 0,
|
currentLineIndex: 0,
|
||||||
lastWallClockMs: row.endedAtMs,
|
lastWallClockMs: endedAtMs,
|
||||||
lastMediaMs: row.lastMediaMs,
|
lastMediaMs,
|
||||||
lastPauseStartMs: null,
|
lastPauseStartMs: null,
|
||||||
isPaused: false,
|
isPaused: false,
|
||||||
pendingTelemetry: false,
|
pendingTelemetry: false,
|
||||||
markedWatched: false,
|
markedWatched: false,
|
||||||
totalWatchedMs: Math.max(0, row.totalWatchedMs),
|
totalWatchedMs: asPositiveNumber(row.totalWatchedMs, 0),
|
||||||
activeWatchedMs: Math.max(0, row.activeWatchedMs),
|
activeWatchedMs: asPositiveNumber(row.activeWatchedMs, 0),
|
||||||
linesSeen: Math.max(0, row.linesSeen),
|
linesSeen: asPositiveNumber(row.linesSeen, 0),
|
||||||
tokensSeen: Math.max(0, row.tokensSeen),
|
tokensSeen: asPositiveNumber(row.tokensSeen, 0),
|
||||||
cardsMined: Math.max(0, row.cardsMined),
|
cardsMined: asPositiveNumber(row.cardsMined, 0),
|
||||||
lookupCount: Math.max(0, row.lookupCount),
|
lookupCount: asPositiveNumber(row.lookupCount, 0),
|
||||||
lookupHits: Math.max(0, row.lookupHits),
|
lookupHits: asPositiveNumber(row.lookupHits, 0),
|
||||||
yomitanLookupCount: Math.max(0, row.yomitanLookupCount),
|
yomitanLookupCount: asPositiveNumber(row.yomitanLookupCount, 0),
|
||||||
pauseCount: Math.max(0, row.pauseCount),
|
pauseCount: asPositiveNumber(row.pauseCount, 0),
|
||||||
pauseMs: Math.max(0, row.pauseMs),
|
pauseMs: asPositiveNumber(row.pauseMs, 0),
|
||||||
seekForwardCount: Math.max(0, row.seekForwardCount),
|
seekForwardCount: asPositiveNumber(row.seekForwardCount, 0),
|
||||||
seekBackwardCount: Math.max(0, row.seekBackwardCount),
|
seekBackwardCount: asPositiveNumber(row.seekBackwardCount, 0),
|
||||||
mediaBufferEvents: Math.max(0, row.mediaBufferEvents),
|
mediaBufferEvents: asPositiveNumber(row.mediaBufferEvents, 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,14 +252,14 @@ function getRetainedStaleActiveSessions(db: DatabaseSync): RetainedSessionRow[]
|
|||||||
function upsertLifetimeMedia(
|
function upsertLifetimeMedia(
|
||||||
db: DatabaseSync,
|
db: DatabaseSync,
|
||||||
videoId: number,
|
videoId: number,
|
||||||
nowMs: number,
|
nowMs: string,
|
||||||
activeMs: number,
|
activeMs: number,
|
||||||
cardsMined: number,
|
cardsMined: number,
|
||||||
linesSeen: number,
|
linesSeen: number,
|
||||||
tokensSeen: number,
|
tokensSeen: number,
|
||||||
completed: number,
|
completed: number,
|
||||||
startedAtMs: number,
|
startedAtMs: number | string,
|
||||||
endedAtMs: number,
|
endedAtMs: number | string,
|
||||||
): void {
|
): void {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
@@ -310,15 +315,15 @@ function upsertLifetimeMedia(
|
|||||||
function upsertLifetimeAnime(
|
function upsertLifetimeAnime(
|
||||||
db: DatabaseSync,
|
db: DatabaseSync,
|
||||||
animeId: number,
|
animeId: number,
|
||||||
nowMs: number,
|
nowMs: string,
|
||||||
activeMs: number,
|
activeMs: number,
|
||||||
cardsMined: number,
|
cardsMined: number,
|
||||||
linesSeen: number,
|
linesSeen: number,
|
||||||
tokensSeen: number,
|
tokensSeen: number,
|
||||||
episodesStartedDelta: number,
|
episodesStartedDelta: number,
|
||||||
episodesCompletedDelta: number,
|
episodesCompletedDelta: number,
|
||||||
startedAtMs: number,
|
startedAtMs: number | string,
|
||||||
endedAtMs: number,
|
endedAtMs: number | string,
|
||||||
): void {
|
): void {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
@@ -377,7 +382,7 @@ function upsertLifetimeAnime(
|
|||||||
export function applySessionLifetimeSummary(
|
export function applySessionLifetimeSummary(
|
||||||
db: DatabaseSync,
|
db: DatabaseSync,
|
||||||
session: SessionState,
|
session: SessionState,
|
||||||
endedAtMs: number,
|
endedAtMs: number | string,
|
||||||
): void {
|
): void {
|
||||||
const applyResult = db
|
const applyResult = db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -392,8 +397,8 @@ export function applySessionLifetimeSummary(
|
|||||||
)
|
)
|
||||||
ON CONFLICT(session_id) DO NOTHING
|
ON CONFLICT(session_id) DO NOTHING
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.run(session.sessionId, endedAtMs, nowMs(), nowMs());
|
.run(session.sessionId, toDbMs(endedAtMs), toDbMs(nowMs()), toDbMs(nowMs()));
|
||||||
|
|
||||||
if ((applyResult.changes ?? 0) <= 0) {
|
if ((applyResult.changes ?? 0) <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -468,7 +473,7 @@ export function applySessionLifetimeSummary(
|
|||||||
? 1
|
? 1
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const updatedAtMs = nowMs();
|
const updatedAtMs = toDbMs(nowMs());
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
UPDATE imm_lifetime_global
|
UPDATE imm_lifetime_global
|
||||||
@@ -524,7 +529,7 @@ export function applySessionLifetimeSummary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function rebuildLifetimeSummaries(db: DatabaseSync): LifetimeRebuildSummary {
|
export function rebuildLifetimeSummaries(db: DatabaseSync): LifetimeRebuildSummary {
|
||||||
const rebuiltAtMs = nowMs();
|
const rebuiltAtMs = toDbMs(nowMs());
|
||||||
db.exec('BEGIN');
|
db.exec('BEGIN');
|
||||||
try {
|
try {
|
||||||
const summary = rebuildLifetimeSummariesInTransaction(db, rebuiltAtMs);
|
const summary = rebuildLifetimeSummariesInTransaction(db, rebuiltAtMs);
|
||||||
@@ -538,7 +543,7 @@ export function rebuildLifetimeSummaries(db: DatabaseSync): LifetimeRebuildSumma
|
|||||||
|
|
||||||
export function rebuildLifetimeSummariesInTransaction(
|
export function rebuildLifetimeSummariesInTransaction(
|
||||||
db: DatabaseSync,
|
db: DatabaseSync,
|
||||||
rebuiltAtMs = nowMs(),
|
rebuiltAtMs = toDbMs(nowMs()),
|
||||||
): LifetimeRebuildSummary {
|
): LifetimeRebuildSummary {
|
||||||
return rebuildLifetimeSummariesInternal(db, rebuiltAtMs);
|
return rebuildLifetimeSummariesInternal(db, rebuiltAtMs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const DAILY_MS = 86_400_000;
|
|||||||
const ZERO_ID = 0;
|
const ZERO_ID = 0;
|
||||||
|
|
||||||
interface RollupStateRow {
|
interface RollupStateRow {
|
||||||
state_value: number;
|
state_value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RollupGroupRow {
|
interface RollupGroupRow {
|
||||||
@@ -125,7 +125,7 @@ function setLastRollupSampleMs(db: DatabaseSync, sampleMs: number | bigint): voi
|
|||||||
`INSERT INTO imm_rollup_state (state_key, state_value)
|
`INSERT INTO imm_rollup_state (state_key, state_value)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
ON CONFLICT(state_key) DO UPDATE SET state_value = excluded.state_value`,
|
ON CONFLICT(state_key) DO UPDATE SET state_value = excluded.state_value`,
|
||||||
).run(ROLLUP_STATE_KEY, sampleMs);
|
).run(ROLLUP_STATE_KEY, toDbMs(sampleMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetRollups(db: DatabaseSync): void {
|
function resetRollups(db: DatabaseSync): void {
|
||||||
|
|||||||
@@ -271,6 +271,19 @@ export function deleteSessionsByIds(db: DatabaseSync, sessionIds: number[]): voi
|
|||||||
db.prepare(`DELETE FROM imm_sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
|
db.prepare(`DELETE FROM imm_sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toDbMs(ms: number | bigint): bigint {
|
export function toDbMs(ms: number | bigint | string): string {
|
||||||
return BigInt(Math.trunc(Number(ms)));
|
if (typeof ms === 'bigint') {
|
||||||
|
return ms.toString();
|
||||||
|
}
|
||||||
|
if (typeof ms === 'string') {
|
||||||
|
const parsedMs = Number(ms);
|
||||||
|
if (!Number.isFinite(parsedMs)) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return String(Math.trunc(parsedMs));
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(ms)) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return String(Math.trunc(ms));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,9 +287,9 @@ function ensureLifetimeSummaryTables(db: DatabaseSync): void {
|
|||||||
episodes_started INTEGER NOT NULL DEFAULT 0,
|
episodes_started INTEGER NOT NULL DEFAULT 0,
|
||||||
episodes_completed INTEGER NOT NULL DEFAULT 0,
|
episodes_completed INTEGER NOT NULL DEFAULT 0,
|
||||||
anime_completed INTEGER NOT NULL DEFAULT 0,
|
anime_completed INTEGER NOT NULL DEFAULT 0,
|
||||||
last_rebuilt_ms INTEGER,
|
last_rebuilt_ms TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER
|
LAST_UPDATE_DATE TEXT
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -332,10 +332,10 @@ function ensureLifetimeSummaryTables(db: DatabaseSync): void {
|
|||||||
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
||||||
episodes_started INTEGER NOT NULL DEFAULT 0,
|
episodes_started INTEGER NOT NULL DEFAULT 0,
|
||||||
episodes_completed INTEGER NOT NULL DEFAULT 0,
|
episodes_completed INTEGER NOT NULL DEFAULT 0,
|
||||||
first_watched_ms INTEGER,
|
first_watched_ms TEXT,
|
||||||
last_watched_ms INTEGER,
|
last_watched_ms TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE CASCADE
|
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -349,10 +349,10 @@ function ensureLifetimeSummaryTables(db: DatabaseSync): void {
|
|||||||
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
||||||
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
||||||
completed INTEGER NOT NULL DEFAULT 0,
|
completed INTEGER NOT NULL DEFAULT 0,
|
||||||
first_watched_ms INTEGER,
|
first_watched_ms TEXT,
|
||||||
last_watched_ms INTEGER,
|
last_watched_ms TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -360,9 +360,9 @@ function ensureLifetimeSummaryTables(db: DatabaseSync): void {
|
|||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS imm_lifetime_applied_sessions(
|
CREATE TABLE IF NOT EXISTS imm_lifetime_applied_sessions(
|
||||||
session_id INTEGER PRIMARY KEY,
|
session_id INTEGER PRIMARY KEY,
|
||||||
applied_at_ms INTEGER NOT NULL,
|
applied_at_ms TEXT NOT NULL,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -562,18 +562,18 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS imm_schema_version (
|
CREATE TABLE IF NOT EXISTS imm_schema_version (
|
||||||
schema_version INTEGER PRIMARY KEY,
|
schema_version INTEGER PRIMARY KEY,
|
||||||
applied_at_ms INTEGER NOT NULL
|
applied_at_ms TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS imm_rollup_state(
|
CREATE TABLE IF NOT EXISTS imm_rollup_state(
|
||||||
state_key TEXT PRIMARY KEY,
|
state_key TEXT PRIMARY KEY,
|
||||||
state_value INTEGER NOT NULL
|
state_value TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
db.exec(`
|
db.exec(`
|
||||||
INSERT INTO imm_rollup_state(state_key, state_value)
|
INSERT INTO imm_rollup_state(state_key, state_value)
|
||||||
VALUES ('last_rollup_sample_ms', 0)
|
VALUES ('last_rollup_sample_ms', '0')
|
||||||
ON CONFLICT(state_key) DO NOTHING
|
ON CONFLICT(state_key) DO NOTHING
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -597,8 +597,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
episodes_total INTEGER,
|
episodes_total INTEGER,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
metadata_json TEXT,
|
metadata_json TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER
|
LAST_UPDATE_DATE TEXT
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
db.exec(`
|
db.exec(`
|
||||||
@@ -625,8 +625,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
bitrate_kbps INTEGER, audio_codec_id INTEGER,
|
bitrate_kbps INTEGER, audio_codec_id INTEGER,
|
||||||
hash_sha256 TEXT, screenshot_path TEXT,
|
hash_sha256 TEXT, screenshot_path TEXT,
|
||||||
metadata_json TEXT,
|
metadata_json TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE SET NULL
|
FOREIGN KEY(anime_id) REFERENCES imm_anime(anime_id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -635,7 +635,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
session_uuid TEXT NOT NULL UNIQUE,
|
session_uuid TEXT NOT NULL UNIQUE,
|
||||||
video_id INTEGER NOT NULL,
|
video_id INTEGER NOT NULL,
|
||||||
started_at_ms INTEGER NOT NULL, ended_at_ms INTEGER,
|
started_at_ms TEXT NOT NULL,
|
||||||
|
ended_at_ms TEXT,
|
||||||
status INTEGER NOT NULL,
|
status INTEGER NOT NULL,
|
||||||
locale_id INTEGER, target_lang_id INTEGER,
|
locale_id INTEGER, target_lang_id INTEGER,
|
||||||
difficulty_tier INTEGER, subtitle_mode INTEGER,
|
difficulty_tier INTEGER, subtitle_mode INTEGER,
|
||||||
@@ -653,8 +654,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
seek_forward_count INTEGER NOT NULL DEFAULT 0,
|
seek_forward_count INTEGER NOT NULL DEFAULT 0,
|
||||||
seek_backward_count INTEGER NOT NULL DEFAULT 0,
|
seek_backward_count INTEGER NOT NULL DEFAULT 0,
|
||||||
media_buffer_events INTEGER NOT NULL DEFAULT 0,
|
media_buffer_events INTEGER NOT NULL DEFAULT 0,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id)
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -662,7 +663,7 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
CREATE TABLE IF NOT EXISTS imm_session_telemetry(
|
CREATE TABLE IF NOT EXISTS imm_session_telemetry(
|
||||||
telemetry_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
telemetry_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
session_id INTEGER NOT NULL,
|
session_id INTEGER NOT NULL,
|
||||||
sample_ms INTEGER NOT NULL,
|
sample_ms TEXT NOT NULL,
|
||||||
total_watched_ms INTEGER NOT NULL DEFAULT 0,
|
total_watched_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
active_watched_ms INTEGER NOT NULL DEFAULT 0,
|
active_watched_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
lines_seen INTEGER NOT NULL DEFAULT 0,
|
lines_seen INTEGER NOT NULL DEFAULT 0,
|
||||||
@@ -676,8 +677,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
seek_forward_count INTEGER NOT NULL DEFAULT 0,
|
seek_forward_count INTEGER NOT NULL DEFAULT 0,
|
||||||
seek_backward_count INTEGER NOT NULL DEFAULT 0,
|
seek_backward_count INTEGER NOT NULL DEFAULT 0,
|
||||||
media_buffer_events INTEGER NOT NULL DEFAULT 0,
|
media_buffer_events INTEGER NOT NULL DEFAULT 0,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -685,7 +686,7 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
CREATE TABLE IF NOT EXISTS imm_session_events(
|
CREATE TABLE IF NOT EXISTS imm_session_events(
|
||||||
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
session_id INTEGER NOT NULL,
|
session_id INTEGER NOT NULL,
|
||||||
ts_ms INTEGER NOT NULL,
|
ts_ms TEXT NOT NULL,
|
||||||
event_type INTEGER NOT NULL,
|
event_type INTEGER NOT NULL,
|
||||||
line_index INTEGER,
|
line_index INTEGER,
|
||||||
segment_start_ms INTEGER,
|
segment_start_ms INTEGER,
|
||||||
@@ -693,8 +694,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
tokens_delta INTEGER NOT NULL DEFAULT 0,
|
tokens_delta INTEGER NOT NULL DEFAULT 0,
|
||||||
cards_delta INTEGER NOT NULL DEFAULT 0,
|
cards_delta INTEGER NOT NULL DEFAULT 0,
|
||||||
payload_json TEXT,
|
payload_json TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -710,8 +711,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
cards_per_hour REAL,
|
cards_per_hour REAL,
|
||||||
tokens_per_min REAL,
|
tokens_per_min REAL,
|
||||||
lookup_hit_rate REAL,
|
lookup_hit_rate REAL,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
PRIMARY KEY (rollup_day, video_id)
|
PRIMARY KEY (rollup_day, video_id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -724,8 +725,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
total_lines_seen INTEGER NOT NULL DEFAULT 0,
|
||||||
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
total_tokens_seen INTEGER NOT NULL DEFAULT 0,
|
||||||
total_cards INTEGER NOT NULL DEFAULT 0,
|
total_cards INTEGER NOT NULL DEFAULT 0,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
PRIMARY KEY (rollup_month, video_id)
|
PRIMARY KEY (rollup_month, video_id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -768,8 +769,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
segment_end_ms INTEGER,
|
segment_end_ms INTEGER,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
secondary_text TEXT,
|
secondary_text TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE,
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY(event_id) REFERENCES imm_session_events(event_id) ON DELETE SET NULL,
|
FOREIGN KEY(event_id) REFERENCES imm_session_events(event_id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE,
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE,
|
||||||
@@ -806,9 +807,9 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
title_romaji TEXT,
|
title_romaji TEXT,
|
||||||
title_english TEXT,
|
title_english TEXT,
|
||||||
episodes_total INTEGER,
|
episodes_total INTEGER,
|
||||||
fetched_at_ms INTEGER NOT NULL,
|
fetched_at_ms TEXT NOT NULL,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -827,9 +828,9 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
uploader_url TEXT,
|
uploader_url TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
metadata_json TEXT,
|
metadata_json TEXT,
|
||||||
fetched_at_ms INTEGER NOT NULL,
|
fetched_at_ms TEXT NOT NULL,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
@@ -837,24 +838,24 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
CREATE TABLE IF NOT EXISTS imm_cover_art_blobs(
|
CREATE TABLE IF NOT EXISTS imm_cover_art_blobs(
|
||||||
blob_hash TEXT PRIMARY KEY,
|
blob_hash TEXT PRIMARY KEY,
|
||||||
cover_blob BLOB NOT NULL,
|
cover_blob BLOB NOT NULL,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER
|
LAST_UPDATE_DATE TEXT
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (currentVersion?.schema_version === 1) {
|
if (currentVersion?.schema_version === 1) {
|
||||||
addColumnIfMissing(db, 'imm_videos', 'CREATED_DATE');
|
addColumnIfMissing(db, 'imm_videos', 'CREATED_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_videos', 'LAST_UPDATE_DATE');
|
addColumnIfMissing(db, 'imm_videos', 'LAST_UPDATE_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_sessions', 'CREATED_DATE');
|
addColumnIfMissing(db, 'imm_sessions', 'CREATED_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_sessions', 'LAST_UPDATE_DATE');
|
addColumnIfMissing(db, 'imm_sessions', 'LAST_UPDATE_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_session_telemetry', 'CREATED_DATE');
|
addColumnIfMissing(db, 'imm_session_telemetry', 'CREATED_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_session_telemetry', 'LAST_UPDATE_DATE');
|
addColumnIfMissing(db, 'imm_session_telemetry', 'LAST_UPDATE_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_session_events', 'CREATED_DATE');
|
addColumnIfMissing(db, 'imm_session_events', 'CREATED_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_session_events', 'LAST_UPDATE_DATE');
|
addColumnIfMissing(db, 'imm_session_events', 'LAST_UPDATE_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_daily_rollups', 'CREATED_DATE');
|
addColumnIfMissing(db, 'imm_daily_rollups', 'CREATED_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_daily_rollups', 'LAST_UPDATE_DATE');
|
addColumnIfMissing(db, 'imm_daily_rollups', 'LAST_UPDATE_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_monthly_rollups', 'CREATED_DATE');
|
addColumnIfMissing(db, 'imm_monthly_rollups', 'CREATED_DATE', 'TEXT');
|
||||||
addColumnIfMissing(db, 'imm_monthly_rollups', 'LAST_UPDATE_DATE');
|
addColumnIfMissing(db, 'imm_monthly_rollups', 'LAST_UPDATE_DATE', 'TEXT');
|
||||||
|
|
||||||
const migratedAtMs = toDbMs(nowMs());
|
const migratedAtMs = toDbMs(nowMs());
|
||||||
db.prepare(
|
db.prepare(
|
||||||
@@ -938,8 +939,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
segment_end_ms INTEGER,
|
segment_end_ms INTEGER,
|
||||||
text TEXT NOT NULL,
|
text TEXT NOT NULL,
|
||||||
secondary_text TEXT,
|
secondary_text TEXT,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER,
|
LAST_UPDATE_DATE TEXT,
|
||||||
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE,
|
FOREIGN KEY(session_id) REFERENCES imm_sessions(session_id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY(event_id) REFERENCES imm_session_events(event_id) ON DELETE SET NULL,
|
FOREIGN KEY(event_id) REFERENCES imm_session_events(event_id) ON DELETE SET NULL,
|
||||||
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE,
|
FOREIGN KEY(video_id) REFERENCES imm_videos(video_id) ON DELETE CASCADE,
|
||||||
@@ -1088,8 +1089,8 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
CREATE TABLE IF NOT EXISTS imm_cover_art_blobs(
|
CREATE TABLE IF NOT EXISTS imm_cover_art_blobs(
|
||||||
blob_hash TEXT PRIMARY KEY,
|
blob_hash TEXT PRIMARY KEY,
|
||||||
cover_blob BLOB NOT NULL,
|
cover_blob BLOB NOT NULL,
|
||||||
CREATED_DATE INTEGER,
|
CREATED_DATE TEXT,
|
||||||
LAST_UPDATE_DATE INTEGER
|
LAST_UPDATE_DATE TEXT
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
deduplicateExistingCoverArtRows(db);
|
deduplicateExistingCoverArtRows(db);
|
||||||
@@ -1237,7 +1238,7 @@ export function ensureSchema(db: DatabaseSync): void {
|
|||||||
db.exec('DELETE FROM imm_daily_rollups');
|
db.exec('DELETE FROM imm_daily_rollups');
|
||||||
db.exec('DELETE FROM imm_monthly_rollups');
|
db.exec('DELETE FROM imm_monthly_rollups');
|
||||||
db.exec(
|
db.exec(
|
||||||
`UPDATE imm_rollup_state SET state_value = 0 WHERE state_key = 'last_rollup_sample_ms'`,
|
`UPDATE imm_rollup_state SET state_value = '0' WHERE state_key = 'last_rollup_sample_ms'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import type { ImmersionTrackerService } from './immersion-tracker-service.js';
|
import type { ImmersionTrackerService } from './immersion-tracker-service.js';
|
||||||
|
import http from 'node:http';
|
||||||
import { basename, extname, resolve, sep } from 'node:path';
|
import { basename, extname, resolve, sep } from 'node:path';
|
||||||
import { readFileSync, existsSync, statSync } from 'node:fs';
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
import { MediaGenerator } from '../../media-generator.js';
|
import { MediaGenerator } from '../../media-generator.js';
|
||||||
import { AnkiConnectClient } from '../../anki-connect.js';
|
import { AnkiConnectClient } from '../../anki-connect.js';
|
||||||
import type { AnkiConnectConfig } from '../../types.js';
|
import type { AnkiConnectConfig } from '../../types.js';
|
||||||
@@ -1006,27 +1008,76 @@ export function startStatsServer(config: StatsServerConfig): { close: () => void
|
|||||||
resolveAnkiNoteId: config.resolveAnkiNoteId,
|
resolveAnkiNoteId: config.resolveAnkiNoteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bunServe = (
|
const bunServe =
|
||||||
globalThis as typeof globalThis & {
|
(
|
||||||
Bun: {
|
globalThis as typeof globalThis & {
|
||||||
serve: (options: {
|
Bun?: {
|
||||||
fetch: (typeof app)['fetch'];
|
serve?: (options: {
|
||||||
port: number;
|
fetch: (typeof app)['fetch'];
|
||||||
hostname: string;
|
port: number;
|
||||||
}) => { stop: () => void };
|
hostname: string;
|
||||||
};
|
}) => { stop: () => void };
|
||||||
}
|
};
|
||||||
).Bun.serve;
|
}
|
||||||
|
).Bun?.serve;
|
||||||
|
|
||||||
const server = bunServe({
|
if (typeof bunServe === 'function') {
|
||||||
fetch: app.fetch,
|
const server = bunServe({
|
||||||
port: config.port,
|
fetch: app.fetch,
|
||||||
hostname: '127.0.0.1',
|
port: config.port,
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: () => {
|
||||||
|
server.stop();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
const url = new URL(`http://127.0.0.1:${config.port}${req.url}`);
|
||||||
|
const headers = new Headers();
|
||||||
|
for (const [name, value] of Object.entries(req.headers)) {
|
||||||
|
if (value === undefined) continue;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const entry of value) {
|
||||||
|
headers.append(name, entry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers.set(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = req.method === 'GET' || req.method === 'HEAD' ? undefined : Readable.toWeb(req);
|
||||||
|
|
||||||
|
const response = await app.fetch(
|
||||||
|
new Request(url.toString(), {
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
res.statusCode = response.status;
|
||||||
|
for (const [name, value] of response.headers) {
|
||||||
|
res.setHeader(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody = await response.arrayBuffer();
|
||||||
|
if (responseBody.byteLength > 0) {
|
||||||
|
res.end(Buffer.from(responseBody));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.listen(config.port, '127.0.0.1');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
close: () => {
|
close: () => {
|
||||||
server.stop();
|
server.close();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user