support for multiple users
This commit is contained in:
@@ -8,9 +8,70 @@ const databases = new Map();
|
||||
const DEFAULT_PATHS = {
|
||||
anilist: path.join(process.cwd(), 'src', 'metadata', 'anilist_anime.db'),
|
||||
favorites: path.join(os.homedir(), "WaifuBoards", "favorites.db"),
|
||||
cache: path.join(os.homedir(), "WaifuBoards", "cache.db")
|
||||
cache: path.join(os.homedir(), "WaifuBoards", "cache.db"),
|
||||
userdata: path.join(os.homedir(), "WaifuBoards", "user_data.db")
|
||||
};
|
||||
|
||||
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 = `
|
||||
-- Tabla 1: User
|
||||
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
|
||||
);
|
||||
|
||||
-- Tabla 2: UserIntegration (✅ ACTUALIZADA)
|
||||
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
|
||||
);
|
||||
|
||||
-- Tabla 3: ListEntry
|
||||
CREATE TABLE IF NOT EXISTS ListEntry (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
anime_id INTEGER NOT NULL,
|
||||
external_id INTEGER,
|
||||
source TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
progress INTEGER NOT NULL DEFAULT 0,
|
||||
score INTEGER,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (user_id, anime_id),
|
||||
FOREIGN KEY (user_id) REFERENCES User(id) ON DELETE CASCADE
|
||||
);
|
||||
`;
|
||||
|
||||
db.exec(schema, (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureExtensionsTable(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.exec(`
|
||||
@@ -128,6 +189,11 @@ function initDatabase(name = 'anilist', dbPath = null, readOnly = false) {
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -10,8 +10,6 @@ const BLOCK_LIST = [
|
||||
"map", "cdn.ampproject.org", "googletagmanager"
|
||||
];
|
||||
|
||||
const ALLOWED_SCRIPTS = [];
|
||||
|
||||
async function initHeadless() {
|
||||
if (browser && browser.isConnected()) return;
|
||||
|
||||
@@ -30,7 +28,6 @@ async function initHeadless() {
|
||||
"--mute-audio",
|
||||
"--no-first-run",
|
||||
"--no-zygote",
|
||||
"--single-process",
|
||||
"--disable-software-rasterizer",
|
||||
"--disable-client-side-phishing-detection",
|
||||
"--no-default-browser-check",
|
||||
@@ -43,42 +40,103 @@ async function initHeadless() {
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error al inicializar browser:", error);
|
||||
console.error("Error initializing browser:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function safeEvaluate(page, fn, ...args) {
|
||||
const maxAttempts = 3;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
// Checkeo de estado de página antes de intentar evaluar
|
||||
if (page.isClosed()) {
|
||||
throw new Error("Page is closed before evaluation");
|
||||
}
|
||||
|
||||
return await Promise.race([
|
||||
page.evaluate(fn, ...args),
|
||||
new Promise((_, reject) =>
|
||||
// Timeout más corto podría ser más seguro, e.g., 20000ms
|
||||
setTimeout(() => reject(new Error("Evaluate timeout")), 30000)
|
||||
)
|
||||
]);
|
||||
} catch (error) {
|
||||
const errorMsg = (error.message || "").toLowerCase();
|
||||
const isLastAttempt = i === maxAttempts - 1;
|
||||
|
||||
// Priorizar errores irrecuperables de contexto/página cerrada
|
||||
if (
|
||||
page.isClosed() ||
|
||||
errorMsg.includes("closed") ||
|
||||
errorMsg.includes("target closed") ||
|
||||
errorMsg.includes("session closed")
|
||||
) {
|
||||
console.error("Page context lost or closed, throwing fatal error.");
|
||||
throw error; // Lanzar inmediatamente, no tiene sentido reintentar
|
||||
}
|
||||
|
||||
// Reintentar solo por errores transitorios de ejecución
|
||||
if (!isLastAttempt && (
|
||||
errorMsg.includes("execution context was destroyed") ||
|
||||
errorMsg.includes("cannot find context") ||
|
||||
errorMsg.includes("timeout")
|
||||
)) {
|
||||
console.warn(`Evaluate attempt ${i + 1} failed, retrying...`, error.message);
|
||||
await new Promise(r => setTimeout(r, 500 * (i + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function turboScroll(page) {
|
||||
try {
|
||||
await page.evaluate(() => {
|
||||
|
||||
if (page.isClosed()) {
|
||||
console.warn("Page closed, skipping scroll");
|
||||
return;
|
||||
}
|
||||
|
||||
await safeEvaluate(page, () => {
|
||||
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();
|
||||
let lastHeight = 0;
|
||||
let sameCount = 0;
|
||||
|
||||
const scrollInterval = setInterval(() => {
|
||||
try {
|
||||
const currentHeight = document.body.scrollHeight;
|
||||
window.scrollTo(0, currentHeight);
|
||||
|
||||
if (currentHeight === lastHeight) {
|
||||
sameCount++;
|
||||
if (sameCount >= 5) {
|
||||
clearInterval(scrollInterval);
|
||||
resolve();
|
||||
}
|
||||
} else {
|
||||
sameCount = 0;
|
||||
lastHeight = currentHeight;
|
||||
}
|
||||
} else {
|
||||
same = 0;
|
||||
last = h;
|
||||
} catch (err) {
|
||||
clearInterval(scrollInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 20);
|
||||
|
||||
// Safety timeout
|
||||
setTimeout(() => {
|
||||
clearInterval(timer);
|
||||
clearInterval(scrollInterval);
|
||||
resolve();
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error en turboScroll:", error.message);
|
||||
// No lanzamos el error, continuamos
|
||||
console.error("Error in turboScroll:", error.message);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +148,6 @@ async function scrape(url, handler, options = {}) {
|
||||
scrollToBottom = false,
|
||||
renderWaitTime = 0,
|
||||
loadImages = true,
|
||||
blockScripts = true,
|
||||
retries = 3,
|
||||
retryDelay = 1000
|
||||
} = options;
|
||||
@@ -101,7 +158,7 @@ async function scrape(url, handler, options = {}) {
|
||||
let page = null;
|
||||
|
||||
try {
|
||||
// Verificar que el browser esté activo
|
||||
|
||||
if (!browser || !browser.isConnected()) {
|
||||
await initHeadless();
|
||||
}
|
||||
@@ -109,7 +166,10 @@ async function scrape(url, handler, options = {}) {
|
||||
page = await context.newPage();
|
||||
const requests = [];
|
||||
|
||||
// Listener para requests
|
||||
page.on("close", () => {
|
||||
console.warn("Page closed unexpectedly");
|
||||
});
|
||||
|
||||
page.on("request", req => {
|
||||
requests.push({
|
||||
url: req.url(),
|
||||
@@ -118,7 +178,6 @@ async function scrape(url, handler, options = {}) {
|
||||
});
|
||||
});
|
||||
|
||||
// Route para bloquear recursos
|
||||
await page.route("**/*", (route) => {
|
||||
const req = route.request();
|
||||
const resUrl = req.url().toLowerCase();
|
||||
@@ -128,9 +187,7 @@ async function scrape(url, handler, options = {}) {
|
||||
type === "font" ||
|
||||
type === "stylesheet" ||
|
||||
type === "media" ||
|
||||
type === "manifest" ||
|
||||
type === "other" ||
|
||||
(blockScripts && type === "script" && !ALLOWED_SCRIPTS.some(k => resUrl.includes(k)))
|
||||
type === "other"
|
||||
) {
|
||||
return route.abort("blockedbyclient");
|
||||
}
|
||||
@@ -148,66 +205,71 @@ async function scrape(url, handler, options = {}) {
|
||||
route.continue();
|
||||
});
|
||||
|
||||
// Navegar a la URL
|
||||
await page.goto(url, { waitUntil, timeout });
|
||||
|
||||
// Esperar selector si se especifica
|
||||
if (!page.isClosed()) {
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
if (waitSelector) {
|
||||
try {
|
||||
await page.waitForSelector(waitSelector, { timeout: Math.min(timeout, 5000) });
|
||||
await page.waitForSelector(waitSelector, {
|
||||
timeout: Math.min(timeout, 5000)
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Selector '${waitSelector}' no encontrado, continuando...`);
|
||||
console.warn(`Selector '${waitSelector}' not found, continuing...`);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll si es necesario
|
||||
if (scrollToBottom) {
|
||||
await turboScroll(page);
|
||||
}
|
||||
|
||||
// Tiempo de espera adicional para renderizado
|
||||
if (renderWaitTime > 0) {
|
||||
await page.waitForTimeout(renderWaitTime);
|
||||
}
|
||||
|
||||
// Ejecutar el handler personalizado
|
||||
const result = await handler(page);
|
||||
if (page.isClosed()) {
|
||||
throw new Error("Page closed before handler execution");
|
||||
}
|
||||
|
||||
const result = await handler(page, safeEvaluate);
|
||||
|
||||
// Cerrar la página antes de retornar
|
||||
await page.close();
|
||||
|
||||
return { result, requests };
|
||||
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`[Intento ${attempt}/${retries}] Error durante el scraping de ${url}:`, error.message);
|
||||
console.error(`[Attempt ${attempt}/${retries}] Error scraping ${url}:`, error.message);
|
||||
|
||||
// Cerrar página si está abierta
|
||||
if (page && !page.isClosed()) {
|
||||
try {
|
||||
await page.close();
|
||||
} catch (closeError) {
|
||||
console.error("Error al cerrar página:", closeError.message);
|
||||
console.error("Error closing page:", closeError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Si el browser está cerrado, limpiar referencias
|
||||
if (error.message.includes("closed") || error.message.includes("Target closed")) {
|
||||
console.log("Browser cerrado detectado, reiniciando...");
|
||||
if (
|
||||
error.message.includes("closed") ||
|
||||
error.message.includes("Target closed") ||
|
||||
error.message.includes("Session closed")
|
||||
) {
|
||||
console.log("Browser closure detected, reinitializing...");
|
||||
await closeScraper();
|
||||
}
|
||||
|
||||
// Si no es el último intento, esperar antes de reintentar
|
||||
if (attempt < retries) {
|
||||
const delay = retryDelay * attempt; // Backoff exponencial
|
||||
console.log(`Reintentando en ${delay}ms...`);
|
||||
const delay = retryDelay * attempt;
|
||||
|
||||
console.log(`Retrying in ${delay}ms...`);
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si llegamos aquí, todos los intentos fallaron
|
||||
console.error(`Todos los intentos fallaron para ${url}`);
|
||||
console.error(`All attempts failed for ${url}`);
|
||||
throw lastError || new Error("Scraping failed after all retries");
|
||||
}
|
||||
|
||||
@@ -218,7 +280,7 @@ async function closeScraper() {
|
||||
context = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cerrando context:", error.message);
|
||||
console.error("Error closing context:", error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -227,12 +289,13 @@ async function closeScraper() {
|
||||
browser = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cerrando browser:", error.message);
|
||||
console.error("Error closing browser:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initHeadless,
|
||||
scrape,
|
||||
closeScraper
|
||||
closeScraper,
|
||||
safeEvaluate
|
||||
};
|
||||
Reference in New Issue
Block a user