update: function to add frames into encoding queue

add: scripts to migrate to V3 schema
TODO: fix processEncodingTasks() in `encoding.ts`
TODO: write docs for V3 schema
This commit is contained in:
alikia2x (寒寒) 2024-12-09 00:23:05 +08:00
parent 37e0fbfc89
commit 1f9b8ea124
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
11 changed files with 503 additions and 76 deletions

View File

@ -1,6 +1,6 @@
{
"name": "openrewind",
"version": "0.4.0",
"version": "0.5.0",
"type": "module",
"description": "Your second brain, superpowered.",
"main": "dist/electron/index.js",
@ -32,15 +32,16 @@
"i18next-electron-fs-backend": "^3.0.2",
"i18next-fs-backend": "^2.6.0",
"i18next-icu": "^2.3.0",
"image-size": "^1.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.1.2",
"react-router": "^7.0.1",
"react-router-dom": "^7.0.1",
"screenshot-desktop": "^1.15.0",
"sqlite3": "^5.1.7",
"sqlstring": "^2.3.3",
"vite-tsconfig-paths": "^5.1.3",
"screenshot-desktop": "^1.15.0"
"vite-tsconfig-paths": "^5.1.3"
},
"devDependencies": {
"@electron/rebuild": "^3.7.1",

View File

@ -16,7 +16,7 @@ importers:
version: 2.0.3
better-sqlite3:
specifier: ^11.6.0
version: 11.6.0
version: 11.7.0
electron-context-menu:
specifier: ^4.0.4
version: 4.0.4
@ -37,13 +37,13 @@ importers:
version: 5.0.3
execa:
specifier: ^9.5.1
version: 9.5.1
version: 9.5.2
i18next:
specifier: ^24.0.2
version: 24.0.5(typescript@5.6.3)
i18next-browser-languagedetector:
specifier: ^8.0.0
version: 8.0.0
version: 8.0.1
i18next-electron-fs-backend:
specifier: ^3.0.2
version: 3.0.2
@ -52,7 +52,10 @@ importers:
version: 2.6.0
i18next-icu:
specifier: ^2.3.0
version: 2.3.0(intl-messageformat@10.7.7)
version: 2.3.0(intl-messageformat@10.7.8)
image-size:
specifier: ^1.1.1
version: 1.1.1
react:
specifier: ^18.3.1
version: 18.3.1
@ -131,7 +134,7 @@ importers:
version: 0.0.3
electron-builder:
specifier: ^25.1.8
version: 25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
eslint:
specifier: ^9.13.0
version: 9.16.0(jiti@1.21.6)
@ -487,17 +490,17 @@ packages:
resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@formatjs/ecma402-abstract@2.2.4':
resolution: {integrity: sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==}
'@formatjs/ecma402-abstract@2.2.5':
resolution: {integrity: sha512-ep/5vGkyZvMSi6s8nQG8k7vTcKjuXs402fgGIWixj0AWRgKbeaZeLuYc32NIPXexgBjWepMeZGgHLuZXkuD2Gg==}
'@formatjs/fast-memoize@2.2.3':
resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==}
'@formatjs/fast-memoize@2.2.4':
resolution: {integrity: sha512-8SzI0cBADgbLOYsoQW/IqVHljCH964CrOdESFQ07wMkRLP90+MfV7k6gZPiGD88ubqET9igJV5c292rT28B7xQ==}
'@formatjs/icu-messageformat-parser@2.9.4':
resolution: {integrity: sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==}
'@formatjs/icu-messageformat-parser@2.9.5':
resolution: {integrity: sha512-mHauC9wuVXtnshAIoAYjlNrh6+OFOT6cC4fpK+AG+DHkVWwIPFVQE28hLQ/KptuvQ8VMfG/zYx6rRjtaeFPkSQ==}
'@formatjs/icu-skeleton-parser@1.8.8':
resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==}
'@formatjs/icu-skeleton-parser@1.8.9':
resolution: {integrity: sha512-1KSSlU7ywsU5E5v7xr6VTlBzLGszMi3GOu7EVINjkfA501GN5OkeNSbd5q6ie1wIknZJGBlqkvXPYYdp3YXjpw==}
'@formatjs/intl-localematcher@0.5.8':
resolution: {integrity: sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==}
@ -929,8 +932,8 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agent-base@7.1.2:
resolution: {integrity: sha512-JVzqkCNRT+VfqzzgPWDPnwvDheSAUdiMUn3NoLXpDJF5lRqeJqyC9iGsAxIOAW+mzIdq+uP1TvcX6bMtrH0agg==}
agent-base@7.1.3:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
engines: {node: '>= 14'}
agentkeepalive@4.5.0:
@ -1173,8 +1176,8 @@ packages:
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
better-sqlite3@11.6.0:
resolution: {integrity: sha512-2J6k/eVxcFYY2SsTxsXrj6XylzHWPxveCn4fKPKZFv/Vqn/Cd7lOuX4d7rGQXT5zL+97MkNL3nSbCrIoe3LkgA==}
better-sqlite3@11.7.0:
resolution: {integrity: sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
@ -1407,8 +1410,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
conf@13.0.1:
resolution: {integrity: sha512-l9Uwc9eOnz39oADzGO2cSBDi7siv8lwO+31ocQ2nOJijnDiW3pxqm9VV10DPYUO28wW83DjABoUqY1nfHRR2hQ==}
conf@13.1.0:
resolution: {integrity: sha512-Bi6v586cy1CoTFViVO4lGTtx780lfF96fUmS1lSX6wpZf6330NvHUu6fReVuDP1de8Mg0nkZb01c8tAQdz1o3w==}
engines: {node: '>=18'}
config-file-ts@0.2.8-rc1:
@ -1766,8 +1769,8 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
execa@9.5.1:
resolution: {integrity: sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==}
execa@9.5.2:
resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==}
engines: {node: ^18.19.0 || >=20.5.0}
expand-template@2.0.3:
@ -2208,8 +2211,8 @@ packages:
i18next-browser-languagedetector@4.0.1:
resolution: {integrity: sha512-RxSoX6mB8cab0CTIQ+klCS764vYRj+Jk621cnFVsINvcdlb/cdi3vQFyrPwmnowB7ReUadjHovgZX+RPIzHVQQ==}
i18next-browser-languagedetector@8.0.0:
resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==}
i18next-browser-languagedetector@8.0.1:
resolution: {integrity: sha512-z9ZuWA7qxbww+cPtdJTgV0O2H9+qlLpQnb37RpnwfsWnUmrO+q92gbVKVtfBL7jRvxfmVMOUKxKGg6VBqO49Pg==}
i18next-electron-fs-backend@3.0.2:
resolution: {integrity: sha512-KRP+4ORx0WG31qHvMNUpI4CytEQFAkFtXnrZ9/NBXH6k/DcKU3IdB573Zl+L+lR4GA6PCKyBX89mqrUhStoItA==}
@ -2252,6 +2255,11 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
image-size@1.1.1:
resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==}
engines: {node: '>=16.x'}
hasBin: true
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@ -2287,8 +2295,8 @@ packages:
resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==}
engines: {node: '>=10.13.0'}
intl-messageformat@10.7.7:
resolution: {integrity: sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==}
intl-messageformat@10.7.8:
resolution: {integrity: sha512-XnFFzJnTfdaDqeiF/ZAUjpkoKEM8UKwHijQXuqpLiM42kuJCawytP/rYAMDYNNaWww/PTaI0rIoG4oUjRrRlnA==}
ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
@ -3167,6 +3175,9 @@ packages:
queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
queue@6.0.2:
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'}
@ -4382,25 +4393,25 @@ snapshots:
dependencies:
levn: 0.4.1
'@formatjs/ecma402-abstract@2.2.4':
'@formatjs/ecma402-abstract@2.2.5':
dependencies:
'@formatjs/fast-memoize': 2.2.3
'@formatjs/fast-memoize': 2.2.4
'@formatjs/intl-localematcher': 0.5.8
tslib: 2.8.1
'@formatjs/fast-memoize@2.2.3':
'@formatjs/fast-memoize@2.2.4':
dependencies:
tslib: 2.8.1
'@formatjs/icu-messageformat-parser@2.9.4':
'@formatjs/icu-messageformat-parser@2.9.5':
dependencies:
'@formatjs/ecma402-abstract': 2.2.4
'@formatjs/icu-skeleton-parser': 1.8.8
'@formatjs/ecma402-abstract': 2.2.5
'@formatjs/icu-skeleton-parser': 1.8.9
tslib: 2.8.1
'@formatjs/icu-skeleton-parser@1.8.8':
'@formatjs/icu-skeleton-parser@1.8.9':
dependencies:
'@formatjs/ecma402-abstract': 2.2.4
'@formatjs/ecma402-abstract': 2.2.5
tslib: 2.8.1
'@formatjs/intl-localematcher@0.5.8':
@ -4847,11 +4858,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
agent-base@7.1.2:
dependencies:
debug: 4.4.0
transitivePeerDependencies:
- supports-color
agent-base@7.1.3: {}
agentkeepalive@4.5.0:
dependencies:
@ -4927,7 +4934,7 @@ snapshots:
app-builder-bin@5.0.0-alpha.10: {}
app-builder-lib@25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)):
app-builder-lib@25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8):
dependencies:
'@develar/schema-utils': 2.6.5
'@electron/notarize': 2.5.0
@ -5119,7 +5126,7 @@ snapshots:
dependencies:
tweetnacl: 0.14.5
better-sqlite3@11.6.0:
better-sqlite3@11.7.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.2
@ -5431,7 +5438,7 @@ snapshots:
tree-kill: 1.2.2
yargs: 17.7.2
conf@13.0.1:
conf@13.1.0:
dependencies:
ajv: 8.17.1
ajv-formats: 3.0.1(ajv@8.17.1)
@ -5568,7 +5575,7 @@ snapshots:
dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8):
dependencies:
app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
builder-util: 25.1.7
builder-util-runtime: 9.2.10
fs-extra: 10.1.0
@ -5646,7 +5653,7 @@ snapshots:
electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8):
dependencies:
app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
archiver: 5.3.2
builder-util: 25.1.7
fs-extra: 10.1.0
@ -5655,9 +5662,9 @@ snapshots:
- dmg-builder
- supports-color
electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8)):
electron-builder@25.1.8(electron-builder-squirrel-windows@25.1.8):
dependencies:
app-builder-lib: 25.1.8(dmg-builder@25.1.8(electron-builder-squirrel-windows@25.1.8))(electron-builder-squirrel-windows@25.1.8(dmg-builder@25.1.8))
app-builder-lib: 25.1.8(dmg-builder@25.1.8)(electron-builder-squirrel-windows@25.1.8)
builder-util: 25.1.7
builder-util-runtime: 9.2.10
chalk: 4.1.2
@ -5718,7 +5725,7 @@ snapshots:
electron-store@10.0.0:
dependencies:
conf: 13.0.1
conf: 13.1.0
type-fest: 4.30.0
electron-to-chromium@1.5.71: {}
@ -5878,7 +5885,7 @@ snapshots:
esutils@2.0.3: {}
execa@9.5.1:
execa@9.5.2:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
cross-spawn: 7.0.6
@ -5933,7 +5940,8 @@ snapshots:
extsprintf@1.3.0: {}
extsprintf@1.4.1: {}
extsprintf@1.4.1:
optional: true
fancy-log@1.3.3:
dependencies:
@ -6404,7 +6412,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.2
agent-base: 7.1.3
debug: 4.4.0
transitivePeerDependencies:
- supports-color
@ -6429,7 +6437,7 @@ snapshots:
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.2
agent-base: 7.1.3
debug: 4.4.0
transitivePeerDependencies:
- supports-color
@ -6444,7 +6452,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.26.0
i18next-browser-languagedetector@8.0.0:
i18next-browser-languagedetector@8.0.1:
dependencies:
'@babel/runtime': 7.26.0
@ -6455,9 +6463,9 @@ snapshots:
i18next-fs-backend@2.6.0: {}
i18next-icu@2.3.0(intl-messageformat@10.7.7):
i18next-icu@2.3.0(intl-messageformat@10.7.8):
dependencies:
intl-messageformat: 10.7.7
intl-messageformat: 10.7.8
i18next@19.0.2:
dependencies:
@ -6487,6 +6495,10 @@ snapshots:
ignore@5.3.2: {}
image-size@1.1.1:
dependencies:
queue: 6.0.2
import-fresh@3.3.0:
dependencies:
parent-module: 1.0.1
@ -6513,11 +6525,11 @@ snapshots:
interpret@3.1.1: {}
intl-messageformat@10.7.7:
intl-messageformat@10.7.8:
dependencies:
'@formatjs/ecma402-abstract': 2.2.4
'@formatjs/fast-memoize': 2.2.3
'@formatjs/icu-messageformat-parser': 2.9.4
'@formatjs/ecma402-abstract': 2.2.5
'@formatjs/fast-memoize': 2.2.4
'@formatjs/icu-messageformat-parser': 2.9.5
tslib: 2.8.1
ip-address@9.0.5:
@ -7335,6 +7347,10 @@ snapshots:
queue-tick@1.0.1: {}
queue@6.0.2:
dependencies:
inherits: 2.0.4
quick-lru@5.1.1: {}
rc@1.2.8:
@ -8104,7 +8120,7 @@ snapshots:
dependencies:
assert-plus: 1.0.0
core-util-is: 1.0.2
extsprintf: 1.4.1
extsprintf: 1.3.0
verror@1.10.1:
dependencies:

View File

@ -0,0 +1,118 @@
import { Database } from 'better-sqlite3';
import { exec } from 'child_process';
import fs from 'fs';
import path, { join } from "path";
import type { EncodingTask, Frame } from "./schema";
import sizeOf from "image-size";
import { getScreenshotsDir } from "../utils/backend.js";
const ENCODING_INTERVAL = 10000; // 10 sec
const CHECK_TASK_INTERVAL = 5000; // 5 sec
const MIN_FRAMES_TO_ENCODE = 300; // At least 10 mins (0.5fps)
const CONCURRENCY = 1; // Number of concurrent encoding tasks
// Detect and insert encoding tasks
export function checkFramesForEncoding(db: Database) {
const stmt = db.prepare(`
SELECT id, imgFilename, createdAt
FROM frame
WHERE encodeStatus = 0
ORDER BY createdAt ASC;
`);
const frames = stmt.all() as Frame[];
const buffer: Frame[] = [];
if (frames.length < MIN_FRAMES_TO_ENCODE) return;
for (let i = 1; i < frames.length; i++) {
const frame = frames[i];
const lastFrame = frames[i - 1];
const currentFrameSize = sizeOf(join(getScreenshotsDir(), frame.imgFilename));
const lastFrameSize = sizeOf(join(getScreenshotsDir(), lastFrame.imgFilename));
const twoFramesHaveSameSize =
currentFrameSize.width === lastFrameSize.width
&& currentFrameSize.height === lastFrameSize.height;
const bufferIsBigEnough = buffer.length >= MIN_FRAMES_TO_ENCODE;
const chunkConditionSatisfied = !twoFramesHaveSameSize || bufferIsBigEnough;
buffer.push(lastFrame);
if (chunkConditionSatisfied) {
// Create new encoding task
const taskStmt = db.prepare(`
INSERT INTO encoding_task (status) VALUES (0);
`);
const taskId = taskStmt.run().lastInsertRowid;
// Insert frames into encoding_task_data
const insertStmt = db.prepare(`
INSERT INTO encoding_task_data (encodingTaskID, frame) VALUES (?, ?);
`);
for (const frame of buffer) {
insertStmt.run(taskId, frame.id);
db.prepare(`
UPDATE frame SET encodeStatus = 1 WHERE id = ?;
`).run(frame.id);
}
console.log(`Created encoding task ${taskId} with ${buffer.length} frames`);
buffer.length = 0;
}
}
}
// TODO: Fix this function
// Check and process encoding task
function processEncodingTasks(db: Database) {
const stmt = db.prepare(`
SELECT id, status
FROM encoding_task
WHERE status = 0
LIMIT ?
`);
const tasks = stmt.all(CONCURRENCY) as EncodingTask[];
for (const task of tasks) {
const taskId = task.id;
// Update task status as processing (1)
const updateStmt = db.prepare(`
UPDATE encoding_task SET status = 1 WHERE id = ?
`);
updateStmt.run(taskId);
const framesStmt = db.prepare(`
SELECT frame.imgFilename
FROM encoding_task_data
JOIN frame ON encoding_task_data.frame = frame.id
WHERE encoding_task_data.encodingTaskID = ?
ORDER BY frame.createAt ASC
`);
const frames = framesStmt.all(taskId) as Frame[];
const metaFilePath = path.join(__dirname, `${taskId}_meta.txt`);
const metaContent = frames.map(frame => `file '${frame.imgFilename}'`).join('\n');
fs.writeFileSync(metaFilePath, metaContent);
const videoName = `video_${taskId}.mp4`;
const ffmpegCommand = `ffmpeg -f concat -safe 0 -i ${metaFilePath} -c:v libx264 -r 30 ${videoName}`;
exec(ffmpegCommand, (error, stdout, stderr) => {
if (error) {
console.error(`FFmpeg error: ${error.message}`);
// Set task status to unprocessed (0)
const failStmt = db.prepare(`
UPDATE encoding_task SET status = 0 WHERE id = ?
`);
failStmt.run(taskId);
} else {
console.log(`Video ${videoName} created successfully`);
// Update task status to complete (2)
const completeStmt = db.prepare(`
UPDATE encoding_task SET status = 2 WHERE id = ?
`);
completeStmt.run(taskId);
}
fs.unlinkSync(metaFilePath);
});
}
}

View File

@ -4,7 +4,6 @@ import DB from "better-sqlite3";
import { __dirname } from "../dirname.js";
import { getDatabaseDir } from "../utils/backend.js";
import { migrate } from "./migrate/index.js";
import { initSchemaInV2 } from "./migrate/migrateToV2";
function getLibSimpleExtensionPath() {
switch (process.platform) {
@ -28,14 +27,14 @@ function init(db: Database) {
db.exec(`
CREATE TABLE IF NOT EXISTS frame (
id INTEGER PRIMARY KEY AUTOINCREMENT,
createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
createdAt REAL,
imgFilename TEXT,
segmentID INTEGER NULL,
videoPath TEXT NULL,
videoFrameIndex INTEGER NULL,
collectionID INTEGER NULL,
encoded BOOLEAN DEFAULT 0,
FOREIGN KEY (segmentID) REFERENCES segements (id)
encodeStatus INTEGER DEFAULT 0,
FOREIGN KEY (segmentID) REFERENCES segments (id)
);
`);
@ -50,10 +49,10 @@ function init(db: Database) {
`);
db.exec(`
CREATE TABLE IF NOT EXISTS segements(
CREATE TABLE IF NOT EXISTS segments(
id INTEGER PRIMARY KEY AUTOINCREMENT,
startAt TIMESTAMP,
endAt TIMESTAMP,
startedAt REAL,
endedAt REAL,
title TEXT,
appName TEXT,
appPath TEXT,
@ -97,10 +96,49 @@ function init(db: Database) {
END;
`);
initSchemaInV2(db);
db.exec(`
CREATE TABLE config (
key TEXT PRIMARY KEY,
value TEXT
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS encoding_task (
id INTEGER PRIMARY KEY AUTOINCREMENT,
createdAt REAL,
status INT DEFAULT 0
);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS encoding_task_data (
encodingTaskID INTEGER,
frame ID INTEGER PRIMARY KEY,
FOREIGN KEY (encodingTaskID) REFERENCES encoding_task(id),
FOREIGN KEY (frame) REFERENCES frame(id)
);
`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS delete_encoding_task
AFTER UPDATE OF status
ON encoding_task
BEGIN
DELETE FROM encoding_task_data
WHERE encodingTaskID = OLD.id AND NEW.status = 2;
DELETE FROM encoding_task
WHERE id = OLD.id AND NEW.status = 2;
END;
`);
db.exec(`
INSERT INTO config (key, value) VALUES ('version', '3');
`);
}
export function initDatabase() {
export async function initDatabase() {
const dbPath = getDatabaseDir();
const db = new DB(dbPath, { verbose: console.log });
const libSimpleExtensionPath = getLibSimpleExtensionPath();
@ -114,5 +152,7 @@ export function initDatabase() {
migrate(db);
}
db.exec("PRAGMA journal_mode=WAL;");
return db;
}

View File

@ -1,5 +1,16 @@
import { Database } from "better-sqlite3";
import { migrateToV2 } from "./migrateToV2.js";
import { migrateToV3 } from "./migrateToV3.js";
const CURRENT_VERSION = 3;
function migrateTo(version: number, db: Database) {
switch (version) {
case 2:
migrateToV3(db);
break;
}
}
export function migrate(db: Database) {
const configTableExists =
@ -8,4 +19,19 @@ export function migrate(db: Database) {
if (!configTableExists) {
migrateToV2(db);
}
let databaseVersion = parseInt(
(
db.prepare(`SELECT value FROM config WHERE key = 'version';`).get() as
{ value: any }
).value
);
while (databaseVersion < CURRENT_VERSION) {
migrateTo(databaseVersion, db);
databaseVersion = parseInt(
(
db.prepare(`SELECT value FROM config WHERE key = 'version';`).get() as
{ value: any }
).value
);
}
}

View File

@ -11,7 +11,7 @@ interface OldFrame {
encoded: number;
}
export function initSchemaInV2(db: Database) {
function initSchemaInV2(db: Database) {
db.exec(`
CREATE TABLE config (
key TEXT PRIMARY KEY,

View File

@ -0,0 +1,191 @@
import { Database } from "better-sqlite3";
function convertTimestampToUnix(timestamp: string): number {
const date = new Date(timestamp);
const now = new Date();
const offsetInMinutes = now.getTimezoneOffset();
const offsetInSeconds = offsetInMinutes * 60;
return date.getTime() / 1000 - offsetInSeconds;
}
function transformEncodingTask(db: Database) {
const createTableSql = `
CREATE TABLE IF NOT EXISTS encoding_task_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status INT DEFAULT 0
);
INSERT INTO encoding_task_new (id, createdAt, status)
SELECT id, createdAt, status FROM encoding_task;
DROP TABLE encoding_task;
ALTER TABLE encoding_task_new RENAME TO encoding_task;
ALTER TABLE encoding_task ADD COLUMN createdAt_new REAL;
`;
db.exec(createTableSql);
const rows = db.prepare(`SELECT id, createdAt FROM encoding_task`).all() as { [x: string]: unknown; id: unknown; }[];
const updateStmt = db.prepare(`UPDATE encoding_task SET createdAt_new = ? WHERE id = ?`);
rows.forEach((row) => {
const unixTimestamp = convertTimestampToUnix(row.createdAt as string);
updateStmt.run(unixTimestamp, row.id);
});
db.exec(`
ALTER TABLE encoding_task DROP COLUMN createdAt;
ALTER TABLE encoding_task RENAME COLUMN createdAt_new TO createdAt;
`);
}
function transformFrame(db: Database) {
const createTableSql = `
CREATE TABLE frame_new(
id INTEGER PRIMARY KEY AUTOINCREMENT,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
imgFilename TEXT,
segmentID INTEGER NULL,
videoPath TEXT NULL,
videoFrameIndex INTEGER NULL,
collectionID INTEGER NULL,
encodeStatus INT DEFAULT 0,
FOREIGN KEY (segmentID) REFERENCES segments (id)
);
INSERT INTO frame_new (id, createdAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus)
SELECT id, createdAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus FROM frame;
DROP TABLE frame;
ALTER TABLE frame_new RENAME TO frame;
ALTER TABLE frame ADD COLUMN createdAt_new REAL;
`
db.exec(createTableSql);
const rows = db.prepare(`SELECT id, createdAt FROM frame`).all() as { [x: string]: unknown; id: unknown; }[];
const updateStmt = db.prepare(`UPDATE frame SET createdAt_new = ? WHERE id = ?`);
rows.forEach((row) => {
const unixTimestamp = convertTimestampToUnix(row.createdAt as string);
updateStmt.run(unixTimestamp, row.id);
});
db.exec(`
ALTER TABLE frame DROP COLUMN createdAt;
ALTER TABLE frame RENAME COLUMN createdAt_new TO createdAt;
`);
}
function transformSegments(db: Database) {
db.exec(`
CREATE TABLE IF NOT EXISTS segments_new(
id INTEGER PRIMARY KEY AUTOINCREMENT,
startedAt REAL,
endedAt REAL,
title TEXT,
appName TEXT,
appPath TEXT,
text TEXT,
type TEXT,
appBundleID TEXT NULL,
url TEXT NULL
);
INSERT INTO segments_new (id, startedAt, endedAt, title, appName, appPath, text, type, appBundleID, url)
SELECT id, startedAt, endedAt, title, appName, appPath, text, type, appBundleID, url FROM segments;
DROP TABLE segments;
ALTER TABLE segments_new RENAME TO segments;
ALTER TABLE segments ADD COLUMN startedAt_new REAL;
ALTER TABLE segments ADD COLUMN endedAt_new REAL;
`);
const rows = db.prepare(`SELECT id, startedAt, endedAt FROM segments`).all() as { [x: string]: unknown; id: unknown; }[];
const updateStart = db.prepare(`UPDATE segments SET startedAt_new = ? WHERE id = ?`);
const updateEnd = db.prepare(`UPDATE segments SET endedAt_new = ? WHERE id = ?`);
rows.forEach((row) => {
updateStart.run(convertTimestampToUnix(row.startedAt as string), row.id);
updateEnd.run(convertTimestampToUnix(row.endedAt as string), row.id);
});
db.exec(`
ALTER TABLE segments DROP COLUMN startedAt;
ALTER TABLE segments DROP COLUMN endedAt;
ALTER TABLE segments RENAME COLUMN startedAt_new TO startedAt;
ALTER TABLE segments RENAME COLUMN endedAt_new TO endedAt;
`);
}
function renameColumn(tableName: string, oldColumnName: string, newColumnName: string, db: Database) {
if (db.prepare(`SELECT 1 FROM pragma_table_info(?) WHERE name=?`).get([tableName, oldColumnName])) {
db.prepare(`ALTER TABLE ? RENAME COLUMN ? TO ?`).run([tableName, oldColumnName, newColumnName]);
}
}
export function migrateToV3(db: Database) {
db.prepare(`ALTER TABLE segements RENAME TO segments`).run();
db.exec(`
PRAGMA foreign_keys = OFF;
CREATE TABLE frame_new(
id INTEGER PRIMARY KEY AUTOINCREMENT,
createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
imgFilename TEXT,
segmentID INTEGER NULL,
videoPath TEXT NULL,
videoFrameIndex INTEGER NULL,
collectionID INTEGER NULL,
encodeStatus INT DEFAULT 0,
FOREIGN KEY (segmentID) REFERENCES segments (id)
);
INSERT INTO frame_new (id, createAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus)
SELECT id, createAt, imgFilename, segmentID, videoPath, videoFrameIndex, collectionID, encodeStatus FROM frame;
DROP TABLE frame;
ALTER TABLE frame_new RENAME TO frame;
CREATE TABLE encoding_task_data_new (
encodingTaskID INTEGER,
frame ID INTEGER PRIMARY KEY,
FOREIGN KEY (encodingTaskID) REFERENCES encoding_task(id),
FOREIGN KEY (frame) REFERENCES frame(id)
);
INSERT INTO encoding_task_data SELECT * FROM encoding_task_data_new;
DROP TRIGGER delete_encoding_task;
DROP TABLE encoding_task_data;
ALTER TABLE encoding_task_data_new RENAME TO encoding_task_data;
CREATE TRIGGER IF NOT EXISTS delete_encoding_task
AFTER UPDATE OF status
ON encoding_task
BEGIN
DELETE FROM encoding_task_data
WHERE encodingTaskID = OLD.id AND NEW.status = 2;
DELETE FROM encoding_task
WHERE id = OLD.id AND NEW.status = 2;
END;
CREATE TABLE recognition_data_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
frameID INTEGER,
data TEXT,
text TEXT,
FOREIGN KEY (frameID) REFERENCES frame (id)
);
INSERT INTO recognition_data SELECT * FROM recognition_data_new;
DROP TABLE recognition_data;
ALTER TABLE recognition_data_new RENAME TO recognition_data;
PRAGMA foreign_keys = ON;
`);
renameColumn('encoding_task', 'createAt', 'createdAt', db);
renameColumn('frame', 'createAt', 'createdAt', db);
renameColumn('segments', 'startAt', 'startedAt', db);
renameColumn('segments', 'endAt', 'endedAt', db);
if (db.prepare(`SELECT 1 FROM pragma_table_info('frame') WHERE name='encoded'`).get()) {
db.prepare(`ALTER TABLE frame DROP COLUMN encoded`).run();
}
transformSegments(db);
transformFrame(db);
transformEncodingTask(db);
db.exec(`
UPDATE config SET value = '3' WHERE key = 'version';
`);
}

22
src/electron/backend/schema.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
export interface Frame {
id: number;
createdAt: number;
imgFilename: string;
segmentID: number | null;
videoPath: string | null;
videoFrameIndex: number | null;
collectionID: number | null;
encodeStatus: number;
}
export interface EncodingTask {
id: number;
createdAt: number;
status: number;
}
export interface EncodingTaskData {
encodingTaskID: number;
frame: number;
}

View File

@ -12,8 +12,8 @@ export function startScreenshotLoop(db: Database) {
const screenshotPath = join(screenshotDir, filename);
screenshot({filename: screenshotPath, format: "png"}).then((absolutePath) => {
const SQL = SqlString.format(
"INSERT INTO frame (imgFilename) VALUES (?)",
[filename]
"INSERT INTO frame (imgFilename, createdAt) VALUES (?, ?)",
[filename, new Date().getTime() / 1000]
);
db.exec(SQL);
}).catch((err) => {

View File

@ -8,6 +8,7 @@ import { Database } from "better-sqlite3";
import { startScreenshotLoop } from "./backend/screenshot.js";
import { __dirname } from "./dirname.js";
import { hideDock } from "./utils/electron.js";
import { checkFramesForEncoding } from "./backend/encoding.js";
const i18n = initI18n();
@ -73,8 +74,11 @@ app.on("activate", () => {});
app.on("ready", () => {
createTray();
dbConnection = initDatabase();
screenshotInterval = startScreenshotLoop(dbConnection);
initDatabase().then((db) => {
screenshotInterval = startScreenshotLoop(db);
setInterval(checkFramesForEncoding, 10000, db);
dbConnection = db;
});
mainWindow = createMainWindow(port, () => (mainWindow = null));
settingsWindow = createSettingsWindow(port, () => (settingsWindow = null));
globalShortcut.register("Escape", () => {

View File

@ -49,3 +49,12 @@ export function getRecordingsDir() {
const dataDir = createDataDir();
return path.join(dataDir, "recordings");
}
export function getEncodingTempDir() {
const tempDir = createTempDir();
const encodingTempDir = path.join(tempDir, "encoding");
if (!fs.existsSync(encodingTempDir)) {
fs.mkdirSync(encodingTempDir, { recursive: true });
}
return encodingTempDir;
}