code organisation & refactor

This commit is contained in:
2025-11-27 20:23:00 +01:00
parent 03636fee99
commit 2b29beeeb5
33 changed files with 2099 additions and 1947 deletions

48
src/shared/database.js Normal file
View File

@@ -0,0 +1,48 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const DB_PATH = path.join(__dirname, '..', 'metadata', 'anilist_anime.db');
let db = null;
function initDatabase() {
db = new sqlite3.Database(DB_PATH, sqlite3.OPEN_READONLY, (err) => {
if (err) {
console.error("Database Error:", err.message);
} else {
console.log("Connected to local AniList database.");
}
});
return db;
}
function getDatabase() {
if (!db) {
throw new Error("Database not initialized. Call initDatabase() first.");
}
return db;
}
function queryOne(sql, params = []) {
return new Promise((resolve, reject) => {
getDatabase().get(sql, params, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
}
function queryAll(sql, params = []) {
return new Promise((resolve, reject) => {
getDatabase().all(sql, params, (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
}
module.exports = {
initDatabase,
getDatabase,
queryOne,
queryAll
};

65
src/shared/extensions.js Normal file
View File

@@ -0,0 +1,65 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
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, skipping...");
return;
}
try {
const files = await fs.promises.readdir(extensionsDir);
for (const file of files) {
if (file.endsWith('.js')) {
const filePath = path.join(extensionsDir, file);
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 && (instance.type === "anime-board" || instance.type === "book-board")) {
const name = instance.constructor.name;
extensions.set(name, instance);
console.log(`📦 Loaded Extension: ${name}`);
}
} catch (e) {
console.error(`❌ Failed to load extension ${file}:`, e.message);
}
}
}
console.log(`✅ Loaded ${extensions.size} extensions`);
} catch (err) {
console.error("❌ Extension Scan Error:", err);
}
}
function getExtension(name) {
return extensions.get(name);
}
function getAllExtensions() {
return extensions;
}
function getExtensionsList() {
return Array.from(extensions.keys());
}
module.exports = {
loadExtensions,
getExtension,
getAllExtensions,
getExtensionsList
};

View File

@@ -0,0 +1,47 @@
const { proxyRequest, processM3U8Content, streamToReadable } = require('./proxy.service');
async function proxyRoutes(fastify, options) {
fastify.get('/proxy', async (req, reply) => {
const { url, referer, origin, userAgent } = req.query;
if (!url) {
return reply.code(400).send({ error: "No URL provided" });
}
try {
const { response, contentType, isM3U8 } = await proxyRequest(url, {
referer,
origin,
userAgent
});
reply.header('Access-Control-Allow-Origin', '*');
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
if (contentType) {
reply.header('Content-Type', contentType);
}
if (isM3U8) {
const text = await response.text();
const baseUrl = new URL(response.url);
const processedContent = processM3U8Content(text, baseUrl, {
referer,
origin,
userAgent
});
return processedContent;
} else {
return reply.send(streamToReadable(response.body));
}
} catch (err) {
fastify.log.error(err);
return reply.code(500).send({ error: "Internal Server Error" });
}
});
}
module.exports = proxyRoutes;

View File

@@ -0,0 +1,58 @@
const { Readable } = require('stream');
async function proxyRequest(url, { referer, origin, userAgent }) {
const headers = {
'User-Agent': userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9'
};
if (referer) headers['Referer'] = referer;
if (origin) headers['Origin'] = origin;
const response = await fetch(url, { headers, redirect: 'follow' });
if (!response.ok) {
throw new Error(`Proxy Error: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
const isM3U8 = (contentType && contentType.includes('mpegurl')) || url.includes('.m3u8');
return {
response,
contentType,
isM3U8
};
}
function processM3U8Content(text, baseUrl, { referer, origin, userAgent }) {
return text.replace(/^(?!#)(?!\s*$).+/gm, (line) => {
line = line.trim();
let absoluteUrl;
try {
absoluteUrl = new URL(line, baseUrl).href;
} catch(e) {
return line;
}
const proxyParams = new URLSearchParams();
proxyParams.set('url', absoluteUrl);
if (referer) proxyParams.set('referer', referer);
if (origin) proxyParams.set('origin', origin);
if (userAgent) proxyParams.set('userAgent', userAgent);
return `/api/proxy?${proxyParams.toString()}`;
});
}
function streamToReadable(webStream) {
return Readable.fromWeb(webStream);
}
module.exports = {
proxyRequest,
processM3U8Content,
streamToReadable
};