Fixed bundling into an exe not working as intended.
This commit is contained in:
115
electron/shared/database.js
Normal file
115
electron/shared/database.js
Normal file
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { ensureUserDataDB, ensureAnilistSchema, ensureExtensionsTable, ensureCacheTable, ensureFavoritesDB } = require('./schemas');
|
||||
const databases = new Map();
|
||||
const DEFAULT_PATHS = {
|
||||
anilist: path.join(os.homedir(), "WaifuBoards", 'anilist_anime.db'),
|
||||
favorites: path.join(os.homedir(), "WaifuBoards", "favorites.db"),
|
||||
cache: path.join(os.homedir(), "WaifuBoards", "cache.db"),
|
||||
userdata: path.join(os.homedir(), "WaifuBoards", "user_data.db")
|
||||
};
|
||||
function initDatabase(name = 'anilist', dbPath = null, readOnly = false) {
|
||||
if (databases.has(name)) {
|
||||
return databases.get(name);
|
||||
}
|
||||
const finalPath = dbPath || DEFAULT_PATHS[name] || DEFAULT_PATHS.anilist;
|
||||
if (name === "favorites") {
|
||||
ensureFavoritesDB(finalPath)
|
||||
.catch(err => console.error("Error creando favorites:", err));
|
||||
}
|
||||
if (name === "cache") {
|
||||
const dir = path.dirname(finalPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
if (name === "userdata") {
|
||||
ensureUserDataDB(finalPath)
|
||||
.catch(err => console.error("Error creando userdata:", err));
|
||||
}
|
||||
const mode = readOnly ? sqlite3.OPEN_READONLY : (sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE);
|
||||
const db = new sqlite3.Database(finalPath, mode, (err) => {
|
||||
if (err) {
|
||||
console.error(`Database Error (${name}):`, err.message);
|
||||
}
|
||||
else {
|
||||
console.log(`Connected to ${name} database at ${finalPath}`);
|
||||
}
|
||||
});
|
||||
databases.set(name, db);
|
||||
if (name === "anilist") {
|
||||
ensureAnilistSchema(db)
|
||||
.then(() => ensureExtensionsTable(db))
|
||||
.catch(err => console.error("Error creating anilist schema:", err));
|
||||
}
|
||||
if (name === "cache") {
|
||||
ensureCacheTable(db)
|
||||
.catch(err => console.error("Error creating cache table:", err));
|
||||
}
|
||||
return db;
|
||||
}
|
||||
function getDatabase(name = 'anilist') {
|
||||
if (!databases.has(name)) {
|
||||
const readOnly = (name === 'anilist');
|
||||
return initDatabase(name, null, readOnly);
|
||||
}
|
||||
return databases.get(name);
|
||||
}
|
||||
function queryOne(sql, params = [], dbName = 'anilist') {
|
||||
return new Promise((resolve, reject) => {
|
||||
getDatabase(dbName).get(sql, params, (err, row) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
function queryAll(sql, params = [], dbName = 'anilist') {
|
||||
return new Promise((resolve, reject) => {
|
||||
getDatabase(dbName).all(sql, params, (err, rows) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(rows || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
function run(sql, params = [], dbName = 'anilist') {
|
||||
return new Promise((resolve, reject) => {
|
||||
getDatabase(dbName).run(sql, params, function (err) {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve({ changes: this.changes, lastID: this.lastID });
|
||||
});
|
||||
});
|
||||
}
|
||||
function closeDatabase(name = null) {
|
||||
if (name) {
|
||||
const db = databases.get(name);
|
||||
if (db) {
|
||||
db.close();
|
||||
databases.delete(name);
|
||||
console.log(`Closed ${name} database`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const [dbName, db] of databases) {
|
||||
db.close();
|
||||
console.log(`Closed ${dbName} database`);
|
||||
}
|
||||
databases.clear();
|
||||
}
|
||||
}
|
||||
module.exports = {
|
||||
initDatabase,
|
||||
getDatabase,
|
||||
queryOne,
|
||||
queryAll,
|
||||
run,
|
||||
closeDatabase
|
||||
};
|
||||
174
electron/shared/extensions.js
Normal file
174
electron/shared/extensions.js
Normal file
@@ -0,0 +1,174 @@
|
||||
"use strict";
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { queryAll, run } = require('./database');
|
||||
const { scrape } = require("./headless");
|
||||
const extensions = new Map();
|
||||
async function loadExtensions() {
|
||||
const homeDir = os.homedir();
|
||||
const extensionsDir = path.join(homeDir, 'WaifuBoards', 'extensions');
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
console.log("📁 Extensions directory not found, creating...");
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
}
|
||||
try {
|
||||
const files = await fs.promises.readdir(extensionsDir);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.js')) {
|
||||
await loadExtension(file);
|
||||
}
|
||||
}
|
||||
console.log(`✅ Loaded ${extensions.size} extensions`);
|
||||
try {
|
||||
const loaded = Array.from(extensions.keys());
|
||||
const rows = await queryAll("SELECT DISTINCT ext_name FROM extension");
|
||||
for (const row of rows) {
|
||||
if (!loaded.includes(row.ext_name)) {
|
||||
console.log(`🧹 Cleaning cached metadata for removed extension: ${row.ext_name}`);
|
||||
await run("DELETE FROM extension WHERE ext_name = ?", [row.ext_name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error("❌ Error cleaning extension cache:", err);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error("❌ Extension Scan Error:", err);
|
||||
}
|
||||
}
|
||||
async function loadExtension(fileName) {
|
||||
const homeDir = os.homedir();
|
||||
const extensionsDir = path.join(homeDir, 'WaifuBoards', 'extensions');
|
||||
const filePath = path.join(extensionsDir, fileName);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.warn(`⚠️ Extension not found: ${fileName}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
delete require.cache[require.resolve(filePath)];
|
||||
const ExtensionClass = require(filePath);
|
||||
const instance = typeof ExtensionClass === 'function'
|
||||
? new ExtensionClass()
|
||||
: (ExtensionClass.default ? new ExtensionClass.default() : null);
|
||||
if (!instance) {
|
||||
console.warn(`⚠️ Invalid extension: ${fileName}`);
|
||||
return;
|
||||
}
|
||||
if (!["anime-board", "book-board", "image-board"].includes(instance.type)) {
|
||||
console.warn(`⚠️ Invalid extension (${instance.type}): ${fileName}`);
|
||||
return;
|
||||
}
|
||||
const name = instance.constructor.name;
|
||||
instance.scrape = scrape;
|
||||
extensions.set(name, instance);
|
||||
console.log(`📦 Installed & loaded: ${name}`);
|
||||
return name;
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(`⚠️ Error loading ${fileName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
const https = require('https');
|
||||
async function saveExtensionFile(fileName, downloadUrl) {
|
||||
const homeDir = os.homedir();
|
||||
const extensionsDir = path.join(homeDir, 'WaifuBoards', 'extensions');
|
||||
const filePath = path.join(extensionsDir, fileName);
|
||||
const fullUrl = downloadUrl;
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(filePath);
|
||||
https.get(fullUrl, async (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
return reject(new Error(`Download failed: ${response.statusCode}`));
|
||||
}
|
||||
response.pipe(file);
|
||||
file.on('finish', async () => {
|
||||
file.close(async () => {
|
||||
try {
|
||||
await loadExtension(fileName);
|
||||
resolve();
|
||||
}
|
||||
catch (err) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.promises.unlink(filePath);
|
||||
}
|
||||
reject(new Error(`Load failed, file rolled back: ${err.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}).on('error', async (err) => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.promises.unlink(filePath);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function deleteExtensionFile(fileName) {
|
||||
const homeDir = os.homedir();
|
||||
const extensionsDir = path.join(homeDir, 'WaifuBoards', 'extensions');
|
||||
const filePath = path.join(extensionsDir, fileName);
|
||||
const extName = fileName.replace(".js", "");
|
||||
for (const key of extensions.keys()) {
|
||||
if (key.toLowerCase() === extName) {
|
||||
extensions.delete(key);
|
||||
console.log(`🗑️ Removed from memory: ${key}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fs.existsSync(filePath)) {
|
||||
await fs.promises.unlink(filePath);
|
||||
console.log(`🗑️ Deleted file: ${fileName}`);
|
||||
}
|
||||
}
|
||||
function getExtension(name) {
|
||||
return extensions.get(name);
|
||||
}
|
||||
function getAllExtensions() {
|
||||
return extensions;
|
||||
}
|
||||
function getExtensionsList() {
|
||||
return Array.from(extensions.keys());
|
||||
}
|
||||
function getAnimeExtensionsMap() {
|
||||
const animeExts = new Map();
|
||||
for (const [name, ext] of extensions) {
|
||||
if (ext.type === 'anime-board') {
|
||||
animeExts.set(name, ext);
|
||||
}
|
||||
}
|
||||
return animeExts;
|
||||
}
|
||||
function getBookExtensionsMap() {
|
||||
const bookExts = new Map();
|
||||
for (const [name, ext] of extensions) {
|
||||
if (ext.type === 'book-board' || ext.type === 'manga-board') {
|
||||
bookExts.set(name, ext);
|
||||
}
|
||||
}
|
||||
return bookExts;
|
||||
}
|
||||
function getGalleryExtensionsMap() {
|
||||
const galleryExts = new Map();
|
||||
for (const [name, ext] of extensions) {
|
||||
if (ext.type === 'image-board') {
|
||||
galleryExts.set(name, ext);
|
||||
}
|
||||
}
|
||||
return galleryExts;
|
||||
}
|
||||
module.exports = {
|
||||
loadExtensions,
|
||||
getExtension,
|
||||
getAllExtensions,
|
||||
getExtensionsList,
|
||||
getAnimeExtensionsMap,
|
||||
getBookExtensionsMap,
|
||||
getGalleryExtensionsMap,
|
||||
saveExtensionFile,
|
||||
deleteExtensionFile
|
||||
};
|
||||
108
electron/shared/headless.js
Normal file
108
electron/shared/headless.js
Normal file
@@ -0,0 +1,108 @@
|
||||
"use strict";
|
||||
const { chromium } = require("playwright-chromium");
|
||||
let browser;
|
||||
let context;
|
||||
const BLOCK_LIST = [
|
||||
"google-analytics", "doubleclick", "facebook", "twitter",
|
||||
"adsystem", "analytics", "tracker", "pixel", "quantserve", "newrelic"
|
||||
];
|
||||
async function initHeadless() {
|
||||
if (browser)
|
||||
return;
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--disable-extensions",
|
||||
"--disable-background-networking",
|
||||
"--disable-sync",
|
||||
"--disable-translate",
|
||||
"--mute-audio",
|
||||
"--no-first-run",
|
||||
"--no-zygote",
|
||||
"--single-process"
|
||||
]
|
||||
});
|
||||
context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
|
||||
});
|
||||
}
|
||||
async function turboScroll(page) {
|
||||
await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
let last = 0;
|
||||
let same = 0;
|
||||
const timer = setInterval(() => {
|
||||
const h = document.body.scrollHeight;
|
||||
window.scrollTo(0, h);
|
||||
if (h === last) {
|
||||
same++;
|
||||
if (same >= 5) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
else {
|
||||
same = 0;
|
||||
last = h;
|
||||
}
|
||||
}, 20);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function scrape(url, handler, options = {}) {
|
||||
const { waitUntil = "domcontentloaded", waitSelector = null, timeout = 10000, scrollToBottom = false, renderWaitTime = 0, loadImages = true } = options;
|
||||
if (!browser)
|
||||
await initHeadless();
|
||||
const page = await context.newPage();
|
||||
let collectedRequests = [];
|
||||
await page.route("**/*", (route) => {
|
||||
const req = route.request();
|
||||
const rUrl = req.url().toLowerCase();
|
||||
const type = req.resourceType();
|
||||
collectedRequests.push({
|
||||
url: req.url(),
|
||||
method: req.method(),
|
||||
resourceType: type
|
||||
});
|
||||
if (type === "font" || type === "media" || type === "manifest")
|
||||
return route.abort();
|
||||
if (BLOCK_LIST.some(k => rUrl.includes(k)))
|
||||
return route.abort();
|
||||
if (!loadImages && (type === "image" || rUrl.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)))
|
||||
return route.abort();
|
||||
route.continue();
|
||||
});
|
||||
await page.goto(url, { waitUntil, timeout });
|
||||
if (waitSelector) {
|
||||
try {
|
||||
await page.waitForSelector(waitSelector, { timeout });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
if (scrollToBottom) {
|
||||
await turboScroll(page);
|
||||
}
|
||||
if (renderWaitTime > 0) {
|
||||
await new Promise(r => setTimeout(r, renderWaitTime));
|
||||
}
|
||||
const result = await handler(page);
|
||||
await page.close();
|
||||
return { result, requests: collectedRequests };
|
||||
}
|
||||
async function closeScraper() {
|
||||
if (context)
|
||||
await context.close();
|
||||
if (browser)
|
||||
await browser.close();
|
||||
context = null;
|
||||
browser = null;
|
||||
}
|
||||
module.exports = {
|
||||
initHeadless,
|
||||
scrape,
|
||||
closeScraper
|
||||
};
|
||||
43
electron/shared/queries.js
Normal file
43
electron/shared/queries.js
Normal file
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
const { queryOne, run } = require('./database');
|
||||
async function getCachedExtension(extName, id) {
|
||||
return queryOne("SELECT metadata FROM extension WHERE ext_name = ? AND id = ?", [extName, id]);
|
||||
}
|
||||
async function cacheExtension(extName, id, title, metadata) {
|
||||
return run(`
|
||||
INSERT INTO extension (ext_name, id, title, metadata, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(ext_name, id)
|
||||
DO UPDATE SET
|
||||
title = excluded.title,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = ?
|
||||
`, [extName, id, title, JSON.stringify(metadata), Date.now(), Date.now()]);
|
||||
}
|
||||
async function getExtensionTitle(extName, id) {
|
||||
const sql = "SELECT title FROM extension WHERE ext_name = ? AND id = ?";
|
||||
const row = await queryOne(sql, [extName, id], 'anilist');
|
||||
return row ? row.title : null;
|
||||
}
|
||||
async function deleteExtension(extName) {
|
||||
return run("DELETE FROM extension WHERE ext_name = ?", [extName]);
|
||||
}
|
||||
async function getCache(key) {
|
||||
return queryOne("SELECT result, created_at, ttl_ms FROM cache WHERE key = ?", [key], "cache");
|
||||
}
|
||||
async function setCache(key, result, ttl_ms) {
|
||||
return run(`
|
||||
INSERT INTO cache (key, result, created_at, ttl_ms)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(key)
|
||||
DO UPDATE SET result = excluded.result, created_at = excluded.created_at, ttl_ms = excluded.ttl_ms
|
||||
`, [key, JSON.stringify(result), Date.now(), ttl_ms], "cache");
|
||||
}
|
||||
module.exports = {
|
||||
getCachedExtension,
|
||||
cacheExtension,
|
||||
getExtensionTitle,
|
||||
deleteExtension,
|
||||
getCache,
|
||||
setCache
|
||||
};
|
||||
217
electron/shared/schemas.js
Normal file
217
electron/shared/schemas.js
Normal file
@@ -0,0 +1,217 @@
|
||||
"use strict";
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
async function ensureUserDataDB(dbPath) {
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE);
|
||||
return new Promise((resolve, reject) => {
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS User (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
profile_picture_url TEXT,
|
||||
email TEXT UNIQUE,
|
||||
password_hash TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS UserIntegration (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
platform TEXT NOT NULL DEFAULT 'AniList',
|
||||
access_token TEXT NOT NULL,
|
||||
refresh_token TEXT NOT NULL,
|
||||
token_type TEXT NOT NULL,
|
||||
anilist_user_id INTEGER NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ListEntry (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
entry_id INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
entry_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
score INTEGER,
|
||||
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
repeat_count INTEGER NOT NULL DEFAULT 0,
|
||||
notes TEXT,
|
||||
is_private BOOLEAN NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (user_id, entry_id),
|
||||
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
db.exec(schema, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function ensureAnilistSchema(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS anime (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
updatedAt INTEGER,
|
||||
cache_created_at INTEGER DEFAULT 0,
|
||||
cache_ttl_ms INTEGER DEFAULT 0,
|
||||
full_data JSON
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trending (
|
||||
rank INTEGER PRIMARY KEY,
|
||||
id INTEGER,
|
||||
full_data JSON,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS top_airing (
|
||||
rank INTEGER PRIMARY KEY,
|
||||
id INTEGER,
|
||||
full_data JSON,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS books (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
updatedAt INTEGER,
|
||||
full_data JSON
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trending_books (
|
||||
rank INTEGER PRIMARY KEY,
|
||||
id INTEGER,
|
||||
full_data JSON,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS popular_books (
|
||||
rank INTEGER PRIMARY KEY,
|
||||
id INTEGER,
|
||||
full_data JSON,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
`;
|
||||
db.exec(schema, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function ensureExtensionsTable(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS extension (
|
||||
ext_name TEXT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
metadata TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY(ext_name, id)
|
||||
);
|
||||
`, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
async function ensureCacheTable(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
key TEXT PRIMARY KEY,
|
||||
result TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
ttl_ms INTEGER NOT NULL
|
||||
);
|
||||
`, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
function ensureFavoritesDB(dbPath) {
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const exists = fs.existsSync(dbPath);
|
||||
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!exists) {
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS favorites (
|
||||
id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
image_url TEXT NOT NULL,
|
||||
thumbnail_url TEXT NOT NULL DEFAULT "",
|
||||
tags TEXT NOT NULL DEFAULT "",
|
||||
headers TEXT NOT NULL DEFAULT "",
|
||||
provider TEXT NOT NULL DEFAULT "",
|
||||
PRIMARY KEY (id, user_id)
|
||||
);
|
||||
`;
|
||||
db.exec(schema, (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
db.all(`PRAGMA table_info(favorites)`, (err, cols) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
const hasHeaders = cols.some(c => c.name === "headers");
|
||||
const hasProvider = cols.some(c => c.name === "provider");
|
||||
const hasUserId = cols.some(c => c.name === "user_id");
|
||||
const queries = [];
|
||||
if (!hasHeaders) {
|
||||
queries.push(`ALTER TABLE favorites ADD COLUMN headers TEXT NOT NULL DEFAULT ""`);
|
||||
}
|
||||
if (!hasProvider) {
|
||||
queries.push(`ALTER TABLE favorites ADD COLUMN provider TEXT NOT NULL DEFAULT ""`);
|
||||
}
|
||||
if (!hasUserId) {
|
||||
queries.push(`ALTER TABLE favorites ADD COLUMN user_id INTEGER NOT NULL DEFAULT 1`);
|
||||
}
|
||||
if (queries.length === 0) {
|
||||
return resolve(false);
|
||||
}
|
||||
db.exec(queries.join(";"), (err) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
module.exports = {
|
||||
ensureUserDataDB,
|
||||
ensureAnilistSchema,
|
||||
ensureExtensionsTable,
|
||||
ensureCacheTable,
|
||||
ensureFavoritesDB
|
||||
};
|
||||
Reference in New Issue
Block a user