mirror of
https://github.com/alexankitty/Myrient-Search-Engine.git
synced 2026-01-15 08:23:18 -03:00
728 lines
21 KiB
JavaScript
728 lines
21 KiB
JavaScript
import getAllFiles from "./lib/crawler/dircrawl.js";
|
|
import { optimizeDatabaseKws } from "./lib/database/dboptimize.js";
|
|
import FileHandler from "./lib/crawler/filehandler.js";
|
|
import Searcher from "./lib/search/search.js";
|
|
import cron from "node-cron";
|
|
import "dotenv/config";
|
|
import express from "express";
|
|
import http from "http";
|
|
import sanitize from "sanitize";
|
|
import debugPrint from "./lib/utility/printutils.js";
|
|
import compression from "compression";
|
|
import cookieParser from "cookie-parser";
|
|
import { generateAsciiArt } from "./lib/utility/asciiart.js";
|
|
import {
|
|
getEmulatorConfig,
|
|
isEmulatorCompatible,
|
|
isNonGameContent,
|
|
} from "./lib/emulator/emulatorConfig.js";
|
|
import fetch from "node-fetch";
|
|
import { initDB, File, QueryCount, Metadata } from "./lib/database/database.js";
|
|
import { initElasticsearch } from "./lib/services/elasticsearch.js";
|
|
import i18n, { locales } from "./config/i18n.js";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import Flag from "./lib/images/flag.js";
|
|
import ConsoleIcons from "./lib/images/consoleicons.js";
|
|
import MetadataManager from "./lib/crawler/metadatamanager.js";
|
|
|
|
let categoryListPath = "./lib/json/terms/categories.json";
|
|
let nonGameTermsPath = "./lib/json/terms/nonGameTerms.json";
|
|
let emulatorsPath = "./lib/json/dynamic_content/emulators.json";
|
|
let localeNamePath = "./lib/json/maps/name_localization.json";
|
|
let categoryList = await FileHandler.parseJsonFile(categoryListPath);
|
|
let nonGameTerms = await FileHandler.parseJsonFile(nonGameTermsPath);
|
|
let emulatorsData = await FileHandler.parseJsonFile(emulatorsPath);
|
|
let localeNames = await FileHandler.parseJsonFile(localeNamePath);
|
|
let crawlTime = 0;
|
|
let queryCount = 0;
|
|
let fileCount = 0;
|
|
let metadataMatchCount = 0;
|
|
let indexPage = "pages/index";
|
|
let flags = new Flag();
|
|
let consoleIcons = new ConsoleIcons(emulatorsData);
|
|
let updatingFiles = false;
|
|
import { Op } from "sequelize";
|
|
|
|
// Initialize databases
|
|
await initDB();
|
|
await initElasticsearch();
|
|
|
|
// Get initial counts
|
|
fileCount = await File.count();
|
|
crawlTime = (await File.max("updatedAt"))?.getTime() || 0;
|
|
queryCount = (await QueryCount.findOne())?.count || 0;
|
|
metadataMatchCount = await File.count({
|
|
where: { detailsId: { [Op.ne]: null } },
|
|
});
|
|
|
|
let searchFields = ["filename", "category", "type", "region"];
|
|
|
|
let defaultSettings = {
|
|
boost: {},
|
|
combineWith: "AND",
|
|
fields: searchFields,
|
|
fuzzy: 0,
|
|
prefix: true,
|
|
hideNonGame: true,
|
|
useOldResults: false,
|
|
};
|
|
|
|
//programmatically set the default boosts while reducing overhead when adding another search field
|
|
for (let field in searchFields) {
|
|
let fieldName = searchFields[field];
|
|
if (searchFields[field] == "filename") {
|
|
defaultSettings.boost[fieldName] = 2;
|
|
} else {
|
|
defaultSettings.boost[fieldName] = 1;
|
|
}
|
|
}
|
|
|
|
let search = new Searcher(searchFields);
|
|
let metadataManager = new MetadataManager();
|
|
|
|
async function getFilesJob() {
|
|
updatingFiles = true;
|
|
console.log("Updating the file list.");
|
|
let oldFileCount = fileCount || 0;
|
|
fileCount = await getAllFiles(categoryList);
|
|
if (!fileCount) {
|
|
console.log("File update failed");
|
|
return;
|
|
}
|
|
crawlTime = Date.now();
|
|
console.log(`Finished updating file list. ${fileCount} found.`);
|
|
if (fileCount > oldFileCount) {
|
|
if (
|
|
(await Metadata.count()) < (await metadataManager.getIGDBGamesCount())
|
|
) {
|
|
await metadataManager.syncAllMetadata();
|
|
}
|
|
await metadataManager.matchAllMetadata();
|
|
metadataMatchCount = await File.count({
|
|
where: { detailsId: { [Op.ne]: null } },
|
|
});
|
|
await optimizeDatabaseKws();
|
|
}
|
|
//this is less important and needs to run last.
|
|
if (fileCount > oldFileCount && (await Metadata.count())) {
|
|
metadataManager.matchAllMetadata(true);
|
|
}
|
|
metadataMatchCount = await File.count({
|
|
where: { detailsId: { [Op.ne]: null } },
|
|
});
|
|
updatingFiles = false;
|
|
}
|
|
|
|
async function updateMetadata() {
|
|
if (updatingFiles) return;
|
|
let updateMatches = process.env.FORCE_METADATA_RESYNC == "1" ? true : false
|
|
if ((await Metadata.count()) < (await metadataManager.getIGDBGamesCount())) {
|
|
await metadataManager.syncAllMetadata();
|
|
updateMatches = true;
|
|
}
|
|
if(updateMatches){
|
|
if (await Metadata.count()) {
|
|
await metadataManager.matchAllMetadata();
|
|
}
|
|
metadataMatchCount = await File.count({
|
|
where: { detailsId: { [Op.ne]: null } },
|
|
});
|
|
}
|
|
}
|
|
|
|
async function updateKws() {
|
|
if (updatingFiles) return;
|
|
if (!(await File.count({ where: { filenamekws: { [Op.ne]: null } } })) || process.env.FORCE_DB_OPTIMIZE == "1") {
|
|
await optimizeDatabaseKws();
|
|
}
|
|
}
|
|
|
|
function buildOptions(page, options) {
|
|
return { page: page, ...options, ...defaultOptions };
|
|
}
|
|
|
|
let defaultOptions = {
|
|
crawlTime: crawlTime,
|
|
queryCount: queryCount,
|
|
fileCount: fileCount,
|
|
metadataMatchCount: metadataMatchCount,
|
|
generateAsciiArt: generateAsciiArt,
|
|
isEmulatorCompatible: isEmulatorCompatible,
|
|
isNonGameContent: isNonGameContent,
|
|
nonGameTerms: nonGameTerms,
|
|
};
|
|
|
|
function updateDefaults() {
|
|
defaultOptions.crawlTime = crawlTime;
|
|
defaultOptions.queryCount = queryCount;
|
|
defaultOptions.fileCount = fileCount;
|
|
defaultOptions.metadataMatchCount = metadataMatchCount;
|
|
}
|
|
|
|
let app = express();
|
|
let server = http.createServer(app);
|
|
|
|
app.use((req, res, next) => {
|
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
next();
|
|
});
|
|
|
|
//static files
|
|
app.use("/public", express.static("views/public"));
|
|
|
|
//middleware
|
|
app.use(sanitize.middleware);
|
|
app.use(compression());
|
|
app.use(express.json());
|
|
app.use(cookieParser());
|
|
app.set("view engine", "ejs");
|
|
|
|
app.use((req, res, next) => {
|
|
req.requestId = uuidv4();
|
|
next();
|
|
});
|
|
|
|
app.use(i18n.init);
|
|
|
|
// Add language detection middleware
|
|
app.use((req, res, next) => {
|
|
// check query parameter (dropdown)
|
|
let lang = null;
|
|
if (req.query.lang) {
|
|
lang = locales.includes(req.query.lang) ? req.query.lang : null;
|
|
}
|
|
|
|
// check cookie
|
|
if (!lang && req.cookies.lang) {
|
|
// Verify the cookie language is available
|
|
lang = locales.includes(req.cookies.lang) ? req.cookies.lang : null;
|
|
}
|
|
|
|
// check browser locale
|
|
if (!lang) {
|
|
lang = req.acceptsLanguages(locales);
|
|
}
|
|
|
|
// Fallback to English
|
|
if (!lang) {
|
|
lang = "en";
|
|
}
|
|
req.setLocale(lang);
|
|
res.locals.locale = lang;
|
|
|
|
res.locals.availableLocales = locales;
|
|
|
|
res.cookie("lang", lang, { maxAge: 365 * 24 * 60 * 60 * 1000 }); // 1 year
|
|
|
|
next();
|
|
});
|
|
|
|
// Add helper function to all templates
|
|
app.locals.__ = function () {
|
|
return i18n.__.apply(this, arguments);
|
|
};
|
|
|
|
app.get("/", function (req, res) {
|
|
let page = "search";
|
|
res.render(indexPage, buildOptions(page));
|
|
});
|
|
|
|
app.get("/search", async function (req, res) {
|
|
let loadOldResults =
|
|
req.query.old === "true" || !(await Metadata.count()) ? true : false;
|
|
let query = req.query.q ? req.query.q : "";
|
|
let pageNum = parseInt(req.query.p);
|
|
let urlPrefix = encodeURI(
|
|
`/search?s=${req.query.s}&q=${req.query.q}${
|
|
req.query.o ? "&o=" + req.query.o : ""
|
|
}${loadOldResults ? "&old=true" : ""}&p=`
|
|
);
|
|
pageNum = pageNum ? pageNum : 1;
|
|
let settings = {};
|
|
try {
|
|
settings = req.query.s ? JSON.parse(atob(req.query.s)) : defaultSettings;
|
|
} catch {
|
|
debugPrint("Search settings corrupt, forcing default.");
|
|
settings = defaultSettings;
|
|
}
|
|
for (let key in defaultSettings) {
|
|
let failed = false;
|
|
if (typeof settings[key] != "undefined") {
|
|
if (typeof settings[key] != typeof defaultSettings[key]) {
|
|
debugPrint("Search settings corrupt, forcing default.");
|
|
failed = true;
|
|
break;
|
|
}
|
|
}
|
|
if (failed) {
|
|
settings = defaultSettings;
|
|
}
|
|
}
|
|
if (settings.combineWith != "AND") {
|
|
delete settings.combineWith;
|
|
}
|
|
settings.pageSize = loadOldResults ? 100 : 10;
|
|
settings.page = pageNum - 1;
|
|
settings.sort = req.query.o || "";
|
|
let results = await search.findAllMatches(query.trim(), settings);
|
|
debugPrint(results);
|
|
if (results.count && pageNum == 1) {
|
|
queryCount += 1;
|
|
await QueryCount.update({ count: queryCount }, { where: { id: 1 } });
|
|
updateDefaults();
|
|
}
|
|
let options = {
|
|
query: query,
|
|
results: results.items,
|
|
count: results.count,
|
|
elapsed: results.elapsed,
|
|
pageNum: pageNum,
|
|
pageCount: Math.ceil(results.count / settings.pageSize),
|
|
indexing: search.indexing,
|
|
urlPrefix: urlPrefix,
|
|
settings: settings,
|
|
flags: flags,
|
|
consoleIcons: consoleIcons,
|
|
localeNames: localeNames,
|
|
};
|
|
let page = loadOldResults ? "resultsold" : "results";
|
|
options = buildOptions(page, options);
|
|
res.render(indexPage, options);
|
|
});
|
|
|
|
app.get("/lucky", async function (req, res) {
|
|
let results = { items: [] };
|
|
if (req.query.q) {
|
|
let settings = req.query.s
|
|
? JSON.parse(atob(req.query.s))
|
|
: defaultSettings;
|
|
results = await search.findAllMatches(req.query.q, settings);
|
|
debugPrint(results);
|
|
}
|
|
if (results?.items?.length) {
|
|
res.redirect(results.items[0].path);
|
|
} else {
|
|
const count = await File.count();
|
|
const randomId = Math.floor(Math.random() * count);
|
|
const luckyFile = await File.findOne({
|
|
offset: randomId,
|
|
});
|
|
debugPrint(`${randomId}: ${luckyFile?.path}`);
|
|
res.redirect(luckyFile?.path || "/");
|
|
}
|
|
queryCount += 1;
|
|
await QueryCount.update({ count: queryCount }, { where: { id: 1 } });
|
|
updateDefaults();
|
|
});
|
|
|
|
app.get("/settings", async function (req, res) {
|
|
let options = { defaultSettings: defaultSettings };
|
|
let page = "settings";
|
|
options.oldSettingsAvailable = (await Metadata.count()) ? true : false;
|
|
options = buildOptions(page, options);
|
|
res.render(indexPage, options);
|
|
});
|
|
|
|
app.post("/suggest", async function (req, res) {
|
|
if (!req.body) {
|
|
return;
|
|
}
|
|
if (typeof req.body.query == "undefined") {
|
|
return;
|
|
}
|
|
let suggestions = await search.getSuggestions(
|
|
req.body.query,
|
|
defaultSettings
|
|
);
|
|
debugPrint(suggestions);
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.end(JSON.stringify({ suggestions }));
|
|
});
|
|
|
|
app.get("/about", function (req, res) {
|
|
let page = "about";
|
|
res.render(indexPage, buildOptions(page));
|
|
});
|
|
|
|
app.get("/play/:id", async function (req, res) {
|
|
// Block access if emulator is disabled
|
|
if (process.env.EMULATOR_ENABLED !== "true") {
|
|
res.redirect("/");
|
|
return;
|
|
}
|
|
|
|
let fileId = parseInt(req.params.id);
|
|
let romFile = await search.findIndex(fileId);
|
|
|
|
if (!romFile) {
|
|
res.redirect("/");
|
|
return;
|
|
}
|
|
|
|
let options = {
|
|
romFile: romFile,
|
|
emulatorConfig: getEmulatorConfig(romFile.category),
|
|
isNonGame: isNonGameContent(romFile.filename, nonGameTerms),
|
|
};
|
|
|
|
let page = "emulator";
|
|
options = buildOptions(page, options);
|
|
res.render(indexPage, options);
|
|
});
|
|
|
|
app.get("/info/:id", async function (req, res) {
|
|
//set header to allow video embed
|
|
res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-non");
|
|
let romId = parseInt(req.params.id);
|
|
let romFile = await search.findIndex(romId);
|
|
if (!romFile) {
|
|
res.redirect("/");
|
|
return;
|
|
}
|
|
let options = {
|
|
file: {
|
|
...romFile.dataValues,
|
|
},
|
|
metadata: {
|
|
...romFile?.details?.dataValues,
|
|
},
|
|
flags: flags,
|
|
consoleIcons: consoleIcons,
|
|
localeNames: localeNames,
|
|
};
|
|
let page = "info";
|
|
options = buildOptions(page, options);
|
|
res.render(indexPage, options);
|
|
});
|
|
|
|
app.get("/proxy-rom/:id", async function (req, res, next) {
|
|
// Block access if emulator is disabled
|
|
if (process.env.EMULATOR_ENABLED !== "true") {
|
|
return next(new Error("Emulator feature is disabled"));
|
|
}
|
|
|
|
let fileId = parseInt(req.params.id);
|
|
let romFile = await search.findIndex(fileId);
|
|
|
|
if (!romFile) {
|
|
return next(new Error("ROM not found"));
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(romFile.path);
|
|
const contentLength = response.headers.get("content-length");
|
|
|
|
res.setHeader("Content-Type", "application/zip");
|
|
res.setHeader("Content-Length", contentLength);
|
|
res.setHeader(
|
|
"Content-Disposition",
|
|
`attachment; filename="${romFile.filename}"`
|
|
);
|
|
|
|
// Add all required cross-origin headers
|
|
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
|
|
response.body.pipe(res);
|
|
} catch (error) {
|
|
console.error("Error proxying ROM:", error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get("/proxy-bios", async function (req, res, next) {
|
|
// Block access if emulator is disabled
|
|
if (process.env.EMULATOR_ENABLED !== "true") {
|
|
return next(new Error("Emulator feature is disabled"));
|
|
}
|
|
|
|
const biosUrl = req.query.url;
|
|
|
|
// Validate that URL is from GitHub
|
|
if (!biosUrl || !biosUrl.startsWith("https://github.com")) {
|
|
return next(new Error("Invalid BIOS URL - only GitHub URLs are allowed"));
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(biosUrl);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const contentLength = response.headers.get("content-length");
|
|
const contentType = response.headers.get("content-type");
|
|
|
|
res.setHeader("Content-Type", contentType || "application/octet-stream");
|
|
res.setHeader("Content-Length", contentLength);
|
|
|
|
// Add all required cross-origin headers
|
|
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
|
|
response.body.pipe(res);
|
|
} catch (error) {
|
|
console.error("Error proxying BIOS:", error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Proxy route for EmulatorJS content
|
|
app.get("/emulatorjs/*", async function (req, res, next) {
|
|
try {
|
|
// Extract the path after /emulatorjs/
|
|
const filePath = req.path.replace(/^\/emulatorjs\//, "");
|
|
|
|
// Support both stable and latest paths
|
|
const emulatorJsUrl = `https://cdn.emulatorjs.org/stable/${filePath}`;
|
|
|
|
const response = await fetch(emulatorJsUrl, {
|
|
headers: {
|
|
"User-Agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Copy content type and length
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType) {
|
|
res.setHeader("Content-Type", contentType);
|
|
}
|
|
|
|
const contentLength = response.headers.get("content-length");
|
|
if (contentLength) {
|
|
res.setHeader("Content-Length", contentLength);
|
|
}
|
|
|
|
// Set special headers for WASM files
|
|
if (filePath.endsWith(".wasm")) {
|
|
res.setHeader("Content-Type", "application/wasm");
|
|
}
|
|
|
|
// Special handling for JavaScript files
|
|
if (filePath.endsWith(".js")) {
|
|
res.setHeader("Content-Type", "application/javascript");
|
|
}
|
|
|
|
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
|
|
response.body.pipe(res);
|
|
} catch (error) {
|
|
console.error("Error proxying EmulatorJS content:", error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get("/emulators", function (req, res) {
|
|
let page = "emulators";
|
|
let options = { emulators: emulatorsData };
|
|
res.render(indexPage, buildOptions(page, options));
|
|
});
|
|
|
|
app.get("/api/emulators", function (req, res) {
|
|
res.json(emulatorsData);
|
|
});
|
|
|
|
app.post("/api/ai-chat", async function (req, res) {
|
|
try {
|
|
const { message } = req.body;
|
|
|
|
if (!message || typeof message !== 'string') {
|
|
return res.status(400).json({ error: 'Message is required' });
|
|
}
|
|
|
|
// Check if AI is enabled and configured
|
|
const aiEnabled = process.env.AI_ENABLED === 'true';
|
|
const apiKey = process.env.AI_API_KEY;
|
|
const apiUrl = process.env.AI_API_URL || 'https://api.openai.com/v1/chat/completions';
|
|
const model = process.env.AI_MODEL || 'gpt-3.5-turbo';
|
|
|
|
if (!aiEnabled) {
|
|
return res.status(503).json({
|
|
error: 'AI chat is currently disabled. Please contact the administrator.'
|
|
});
|
|
}
|
|
|
|
if (!apiKey) {
|
|
return res.status(503).json({
|
|
error: 'AI service is not configured. Please contact the administrator.'
|
|
});
|
|
}
|
|
|
|
// Create system prompt with context about Myrient
|
|
const systemPrompt = `You are a helpful AI assistant for the Myrient Search Engine, a website that helps users find and search through retro games and ROMs.
|
|
|
|
About Myrient:
|
|
- Myrient is a preservation project that offers a comprehensive collection of retro games
|
|
- Users can search for games by filename, category, type, and region
|
|
- The site includes an emulator feature for playing games directly in the browser
|
|
- The search engine indexes thousands of games from various gaming systems and regions
|
|
|
|
Your role:
|
|
- Help users find games they're looking for
|
|
- Provide information about gaming history, consoles, and game recommendations
|
|
- Answer questions about how to use the search features
|
|
- Be knowledgeable about retro gaming but stay focused on being helpful
|
|
- Keep responses concise and friendly
|
|
- If users ask about downloading or legal issues, remind them that Myrient focuses on preservation
|
|
|
|
Current search context: The user is on a retro gaming search website and may be looking for specific games or gaming information.`;
|
|
|
|
const aiResponse = await fetch(apiUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
'User-Agent': 'Myrient-Search-Engine/1.0'
|
|
},
|
|
body: JSON.stringify({
|
|
model: model,
|
|
messages: [
|
|
{ role: 'system', content: systemPrompt },
|
|
{ role: 'user', content: message }
|
|
],
|
|
max_tokens: 500,
|
|
temperature: 0.7,
|
|
stream: false
|
|
})
|
|
});
|
|
|
|
if (!aiResponse.ok) {
|
|
const errorData = await aiResponse.json().catch(() => ({}));
|
|
console.error('AI API Error:', aiResponse.status, errorData);
|
|
|
|
// Handle specific error cases
|
|
if (aiResponse.status === 401) {
|
|
return res.status(503).json({
|
|
error: 'AI service authentication failed. Please contact the administrator.'
|
|
});
|
|
} else if (aiResponse.status === 429) {
|
|
return res.status(429).json({
|
|
error: 'AI service is currently busy. Please try again in a moment.'
|
|
});
|
|
} else {
|
|
return res.status(503).json({
|
|
error: 'AI service is temporarily unavailable. Please try again later.'
|
|
});
|
|
}
|
|
}
|
|
|
|
const aiData = await aiResponse.json();
|
|
|
|
if (!aiData.choices || aiData.choices.length === 0) {
|
|
return res.status(503).json({
|
|
error: 'AI service returned an unexpected response.'
|
|
});
|
|
}
|
|
|
|
const response = aiData.choices[0].message.content.trim();
|
|
|
|
res.json({ response });
|
|
|
|
} catch (error) {
|
|
console.error('AI Chat Error:', error);
|
|
res.status(500).json({
|
|
error: 'An unexpected error occurred. Please try again later.'
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/proxy-image", async function (req, res, next) {
|
|
const imageUrl = req.query.url;
|
|
|
|
if (!imageUrl) {
|
|
return next(new Error("No image URL provided"));
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(imageUrl, {
|
|
headers: {
|
|
"User-Agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Copy content type
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType) {
|
|
res.setHeader("Content-Type", contentType);
|
|
}
|
|
|
|
const contentLength = response.headers.get("content-length");
|
|
if (contentLength) {
|
|
res.setHeader("Content-Length", contentLength);
|
|
}
|
|
|
|
response.body.pipe(res);
|
|
} catch (error) {
|
|
console.error("Error proxying image:", error);
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// 404 handler
|
|
app.use((req, res, next) => {
|
|
const err = new Error("Page Not Found");
|
|
err.status = 404;
|
|
next(err);
|
|
});
|
|
|
|
// Error handling middleware
|
|
app.use((err, req, res, next) => {
|
|
const status = err.status || 500;
|
|
const message = err.message || "An unexpected error occurred";
|
|
|
|
if (process.env.NODE_ENV !== "production") {
|
|
console.error(err);
|
|
}
|
|
|
|
res.status(status);
|
|
|
|
res.render("pages/error", {
|
|
status,
|
|
message,
|
|
stack: process.env.NODE_ENV !== "production" ? err.stack : null,
|
|
req,
|
|
requestId: req.requestId,
|
|
});
|
|
});
|
|
|
|
server.listen(process.env.PORT, process.env.BIND_ADDRESS);
|
|
server.on("listening", function () {
|
|
console.log(
|
|
"Server started on %s:%s.",
|
|
server.address().address,
|
|
server.address().port
|
|
);
|
|
});
|
|
console.log(`Loaded ${fileCount} known files.`);
|
|
console.log(`${metadataMatchCount} files contain matched metadata.`);
|
|
|
|
// Run file update job if needed
|
|
if (
|
|
process.env.FORCE_FILE_REBUILD == "1" ||
|
|
!fileCount ||
|
|
(crawlTime && Date.now() - crawlTime > 7 * 24 * 60 * 60 * 1000) // 1 week
|
|
) {
|
|
await getFilesJob();
|
|
}
|
|
|
|
cron.schedule("0 30 2 * * *", getFilesJob);
|
|
|
|
//run these tasks after to add new functions
|
|
await updateMetadata();
|
|
await updateKws();
|