2025-05-31 13:53:13 -06:00
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" ;
2024-10-22 00:41:46 -06:00
import cron from "node-cron" ;
import "dotenv/config" ;
import express from "express" ;
import http from "http" ;
import sanitize from "sanitize" ;
2025-05-31 13:53:13 -06:00
import debugPrint from "./lib/utility/printutils.js" ;
2024-10-27 01:53:57 -03:00
import compression from "compression" ;
2025-05-18 07:11:37 -06:00
import cookieParser from "cookie-parser" ;
2025-05-31 13:53:13 -06:00
import { generateAsciiArt } from "./lib/utility/asciiart.js" ;
2025-05-18 07:11:37 -06:00
import {
getEmulatorConfig ,
isEmulatorCompatible ,
isNonGameContent ,
2025-05-31 13:53:13 -06:00
} from "./lib/emulator/emulatorConfig.js" ;
2025-05-18 07:11:37 -06:00
import fetch from "node-fetch" ;
2025-05-31 13:53:13 -06:00
import { initDB , File , QueryCount , Metadata } from "./lib/database/database.js" ;
2025-05-18 07:11:37 -06:00
import { initElasticsearch } from "./lib/services/elasticsearch.js" ;
import i18n , { locales } from "./config/i18n.js" ;
import { v4 as uuidv4 } from "uuid" ;
2025-05-31 13:53:13 -06:00
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" ;
2024-10-22 00:41:46 -06:00
let categoryList = await FileHandler . parseJsonFile ( categoryListPath ) ;
2024-11-14 10:01:10 -03:00
let nonGameTerms = await FileHandler . parseJsonFile ( nonGameTermsPath ) ;
2025-03-13 15:15:38 -03:00
let emulatorsData = await FileHandler . parseJsonFile ( emulatorsPath ) ;
2025-05-31 13:53:13 -06:00
let localeNames = await FileHandler . parseJsonFile ( localeNamePath ) ;
2024-10-22 00:41:46 -06:00
let crawlTime = 0 ;
let queryCount = 0 ;
let fileCount = 0 ;
2025-05-31 14:12:47 -06:00
let metadataMatchCount = 0 ;
2024-10-22 00:41:46 -06:00
let indexPage = "pages/index" ;
2025-05-27 22:35:11 -06:00
let flags = new Flag ( ) ;
let consoleIcons = new ConsoleIcons ( emulatorsData ) ;
2025-05-31 14:47:54 -06:00
let updatingFiles = false ;
2025-05-31 14:12:47 -06:00
import { Op } from "sequelize" ;
2025-01-28 20:14:19 -03:00
// Initialize databases
await initDB ( ) ;
await initElasticsearch ( ) ;
// Get initial counts
fileCount = await File . count ( ) ;
2025-05-18 07:11:37 -06:00
crawlTime = ( await File . max ( "updatedAt" ) ) ? . getTime ( ) || 0 ;
2025-01-28 20:14:19 -03:00
queryCount = ( await QueryCount . findOne ( ) ) ? . count || 0 ;
2025-05-31 14:12:47 -06:00
metadataMatchCount = await File . count ( {
where : { detailsId : { [ Op . ne ] : null } } ,
} ) ;
2024-10-17 02:02:07 -06:00
2024-10-22 00:41:46 -06:00
let searchFields = [ "filename" , "category" , "type" , "region" ] ;
2024-10-19 00:08:34 -06:00
let defaultSettings = {
2024-10-22 00:41:46 -06:00
boost : { } ,
combineWith : "AND" ,
fields : searchFields ,
2024-10-22 03:17:20 -06:00
fuzzy : 0 ,
2024-10-19 00:08:34 -06:00
prefix : true ,
2025-03-17 02:48:34 -03:00
hideNonGame : true ,
2025-05-24 02:40:43 -06:00
useOldResults : false ,
2024-10-22 00:41:46 -06:00
} ;
2024-10-19 00:08:34 -06:00
//programmatically set the default boosts while reducing overhead when adding another search field
2024-10-22 00:41:46 -06:00
for ( let field in searchFields ) {
let fieldName = searchFields [ field ] ;
if ( searchFields [ field ] == "filename" ) {
defaultSettings . boost [ fieldName ] = 2 ;
} else {
defaultSettings . boost [ fieldName ] = 1 ;
2024-10-19 00:08:34 -06:00
}
}
2025-01-28 20:14:19 -03:00
let search = new Searcher ( searchFields ) ;
2025-05-31 13:53:13 -06:00
let metadataManager = new MetadataManager ( ) ;
2024-10-16 03:09:31 -06:00
2024-10-22 00:41:46 -06:00
async function getFilesJob ( ) {
2025-05-31 14:47:54 -06:00
updatingFiles = true ;
2024-10-22 00:41:46 -06:00
console . log ( "Updating the file list." ) ;
2025-05-31 13:53:13 -06:00
let oldFileCount = fileCount || 0 ;
2025-01-28 20:14:19 -03:00
fileCount = await getAllFiles ( categoryList ) ;
2025-05-18 07:11:37 -06:00
if ( ! fileCount ) {
2025-01-28 20:14:19 -03:00
console . log ( "File update failed" ) ;
return ;
2024-10-17 01:23:34 -06:00
}
2025-01-28 20:14:19 -03:00
crawlTime = Date . now ( ) ;
2024-10-22 00:41:46 -06:00
console . log ( ` Finished updating file list. ${ fileCount } found. ` ) ;
2025-05-31 13:53:13 -06:00
if ( fileCount > oldFileCount ) {
2025-05-31 14:47:54 -06:00
if (
( await Metadata . count ( ) ) < ( await metadataManager . getIGDBGamesCount ( ) )
) {
await metadataManager . syncAllMetadata ( ) ;
}
2025-05-31 15:14:42 -06:00
await metadataManager . matchAllMetadata ( ) ;
metadataMatchCount = await File . count ( {
where : { detailsId : { [ Op . ne ] : null } } ,
} ) ;
2025-10-21 00:04:47 -06:00
if ( process . env . DB _KEYWORD _OPTIMIZER === "1" ) {
2025-10-20 20:17:07 -06:00
await optimizeDatabaseKws ( ) ;
}
2025-05-30 05:18:19 -06:00
}
2025-05-29 17:18:01 -06:00
//this is less important and needs to run last.
2025-05-31 15:14:42 -06:00
if ( fileCount > oldFileCount && ( await Metadata . count ( ) ) ) {
2025-05-31 13:53:13 -06:00
metadataManager . matchAllMetadata ( true ) ;
2025-05-30 05:18:19 -06:00
}
2025-05-31 14:12:47 -06:00
metadataMatchCount = await File . count ( {
where : { detailsId : { [ Op . ne ] : null } } ,
} ) ;
2025-05-31 14:47:54 -06:00
updatingFiles = false ;
}
async function updateMetadata ( ) {
if ( updatingFiles ) return ;
2025-10-21 00:04:47 -06:00
let updateMatches = process . env . FORCE _METADATA _RESYNC == "1" ? true : false ;
2025-05-31 17:30:49 -06:00
if ( ( await Metadata . count ( ) ) < ( await metadataManager . getIGDBGamesCount ( ) ) ) {
2025-05-31 14:47:54 -06:00
await metadataManager . syncAllMetadata ( ) ;
2025-05-31 17:30:49 -06:00
updateMatches = true ;
}
2025-10-21 00:04:47 -06:00
if ( updateMatches ) {
2025-05-31 15:14:42 -06:00
if ( await Metadata . count ( ) ) {
2025-05-31 14:53:27 -06:00
await metadataManager . matchAllMetadata ( ) ;
}
2025-05-31 14:47:54 -06:00
metadataMatchCount = await File . count ( {
where : { detailsId : { [ Op . ne ] : null } } ,
} ) ;
}
}
async function updateKws ( ) {
if ( updatingFiles ) return ;
2025-10-20 20:17:07 -06:00
if ( process . env . DB _KEYWORD _OPTIMIZER !== "1" ) return ;
2025-10-21 00:04:47 -06:00
if (
! ( await File . count ( { where : { filenamekws : { [ Op . ne ] : null } } } ) ) ||
process . env . FORCE _DB _OPTIMIZE == "1"
) {
2025-05-31 14:47:54 -06:00
await optimizeDatabaseKws ( ) ;
}
2024-10-22 00:04:07 -06:00
}
function buildOptions ( page , options ) {
2024-10-22 00:41:46 -06:00
return { page : page , ... options , ... defaultOptions } ;
2024-10-16 03:09:31 -06:00
}
2024-10-22 00:04:07 -06:00
let defaultOptions = {
crawlTime : crawlTime ,
2024-10-22 00:41:46 -06:00
queryCount : queryCount ,
2024-10-22 03:21:37 -06:00
fileCount : fileCount ,
2025-05-31 14:12:47 -06:00
metadataMatchCount : metadataMatchCount ,
2024-11-14 10:01:10 -03:00
generateAsciiArt : generateAsciiArt ,
2025-03-17 02:48:34 -03:00
isEmulatorCompatible : isEmulatorCompatible ,
isNonGameContent : isNonGameContent ,
2025-05-18 07:11:37 -06:00
nonGameTerms : nonGameTerms ,
2025-10-21 00:04:47 -06:00
aiEnabled : process . env . AI _ENABLED === "true" ,
2025-08-29 04:06:40 -03:00
aiConfig : {
2025-10-21 00:04:47 -06:00
apiUrl : process . env . AI _API _URL || "https://example.com" ,
model : process . env . AI _MODEL || "default" ,
2025-08-29 04:06:40 -03:00
} ,
2024-10-22 00:41:46 -06:00
} ;
2024-10-22 00:04:07 -06:00
2025-05-18 07:11:37 -06:00
function updateDefaults ( ) {
2025-01-28 20:14:19 -03:00
defaultOptions . crawlTime = crawlTime ;
defaultOptions . queryCount = queryCount ;
defaultOptions . fileCount = fileCount ;
2025-05-31 14:12:47 -06:00
defaultOptions . metadataMatchCount = metadataMatchCount ;
2024-10-22 04:12:21 -06:00
}
2024-10-19 00:08:34 -06:00
let app = express ( ) ;
let server = http . createServer ( app ) ;
2025-03-31 05:16:43 -03:00
app . use ( ( req , res , next ) => {
2025-05-18 07:11:37 -06:00
res . setHeader ( "Cross-Origin-Opener-Policy" , "same-origin" ) ;
res . setHeader ( "Cross-Origin-Embedder-Policy" , "require-corp" ) ;
2025-03-31 05:16:43 -03:00
next ( ) ;
} ) ;
2025-05-20 14:52:23 -06:00
//static files
2025-05-24 02:40:43 -06:00
app . use ( "/public" , express . static ( "views/public" ) ) ;
2025-05-20 14:52:23 -06:00
//middleware
2024-10-22 00:41:46 -06:00
app . use ( sanitize . middleware ) ;
2025-05-18 07:11:37 -06:00
app . use ( compression ( ) ) ;
app . use ( express . json ( ) ) ;
app . use ( cookieParser ( ) ) ;
2024-10-27 01:53:57 -03:00
app . set ( "view engine" , "ejs" ) ;
2024-10-22 00:41:46 -06:00
2025-03-31 05:16:43 -03:00
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 ) {
2025-05-18 07:11:37 -06:00
lang = "en" ;
2025-03-31 05:16:43 -03:00
}
req . setLocale ( lang ) ;
res . locals . locale = lang ;
res . locals . availableLocales = locales ;
2025-05-18 07:11:37 -06:00
res . cookie ( "lang" , lang , { maxAge : 365 * 24 * 60 * 60 * 1000 } ) ; // 1 year
2025-03-31 05:16:43 -03:00
next ( ) ;
} ) ;
// Add helper function to all templates
2025-05-18 07:11:37 -06:00
app . locals . _ _ = function ( ) {
2025-03-31 05:16:43 -03:00
return i18n . _ _ . apply ( this , arguments ) ;
} ;
2024-10-22 00:41:46 -06:00
app . get ( "/" , function ( req , res ) {
let page = "search" ;
res . render ( indexPage , buildOptions ( page ) ) ;
} ) ;
app . get ( "/search" , async function ( req , res ) {
2025-05-31 15:10:49 -06:00
let loadOldResults =
req . query . old === "true" || ! ( await Metadata . count ( ) ) ? true : false ;
2024-10-22 00:41:46 -06:00
let query = req . query . q ? req . query . q : "" ;
2025-05-18 07:11:37 -06:00
let pageNum = parseInt ( req . query . p ) ;
2025-05-26 14:31:12 -06:00
let urlPrefix = encodeURI (
` /search?s= ${ req . query . s } &q= ${ req . query . q } ${
req . query . o ? "&o=" + req . query . o : ""
2025-05-31 15:14:42 -06:00
} $ { loadOldResults ? "&old=true" : "" } & p = `
2025-05-26 14:31:12 -06:00
) ;
2025-05-18 07:11:37 -06:00
pageNum = pageNum ? pageNum : 1 ;
2024-10-22 03:21:37 -06:00
let settings = { } ;
try {
2024-10-22 02:15:09 -06:00
settings = req . query . s ? JSON . parse ( atob ( req . query . s ) ) : defaultSettings ;
2024-10-22 03:21:37 -06:00
} catch {
debugPrint ( "Search settings corrupt, forcing default." ) ;
settings = defaultSettings ;
2024-10-22 02:15:09 -06:00
}
2024-10-22 03:21:37 -06:00
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 ;
2024-10-22 02:15:09 -06:00
}
}
2024-10-22 03:21:37 -06:00
if ( failed ) {
settings = defaultSettings ;
2024-10-22 02:15:09 -06:00
}
}
2024-10-22 03:21:37 -06:00
if ( settings . combineWith != "AND" ) {
2025-01-28 20:14:19 -03:00
delete settings . combineWith ;
2024-10-19 00:08:34 -06:00
}
2025-05-27 20:34:38 -06:00
settings . pageSize = loadOldResults ? 100 : 10 ;
2025-05-24 02:40:43 -06:00
settings . page = pageNum - 1 ;
2025-05-26 14:31:12 -06:00
settings . sort = req . query . o || "" ;
2025-05-30 04:26:40 -06:00
let results = await search . findAllMatches ( query . trim ( ) , settings ) ;
2024-10-22 03:21:37 -06:00
debugPrint ( results ) ;
2025-05-24 02:40:43 -06:00
if ( results . count && pageNum == 1 ) {
2024-10-23 02:25:12 -06:00
queryCount += 1 ;
2025-05-18 07:11:37 -06:00
await QueryCount . update ( { count : queryCount } , { where : { id : 1 } } ) ;
updateDefaults ( ) ;
2024-10-23 02:25:12 -06:00
}
2024-10-22 00:04:07 -06:00
let options = {
2024-10-18 02:23:05 -06:00
query : query ,
2025-05-29 13:07:08 -06:00
results : results . items ,
2025-05-24 02:40:43 -06:00
count : results . count ,
elapsed : results . elapsed ,
2024-10-24 01:43:11 -06:00
pageNum : pageNum ,
2025-05-24 02:40:43 -06:00
pageCount : Math . ceil ( results . count / settings . pageSize ) ,
2024-10-21 23:39:54 -06:00
indexing : search . indexing ,
2025-03-17 02:48:34 -03:00
urlPrefix : urlPrefix ,
2025-05-18 07:11:37 -06:00
settings : settings ,
2025-05-27 22:35:11 -06:00
flags : flags ,
2025-05-29 16:24:12 -06:00
consoleIcons : consoleIcons ,
2025-05-31 13:53:13 -06:00
localeNames : localeNames ,
2024-10-22 00:41:46 -06:00
} ;
2025-05-27 20:12:30 -06:00
let page = loadOldResults ? "resultsold" : "results" ;
2024-10-22 00:41:46 -06:00
options = buildOptions ( page , options ) ;
res . render ( indexPage , options ) ;
} ) ;
app . get ( "/lucky" , async function ( req , res ) {
2025-03-11 14:57:03 -03:00
let results = { items : [ ] } ;
2024-10-22 03:21:37 -06:00
if ( req . query . q ) {
2025-05-18 07:11:37 -06:00
let settings = req . query . s
? JSON . parse ( atob ( req . query . s ) )
: defaultSettings ;
2024-10-22 03:17:20 -06:00
results = await search . findAllMatches ( req . query . q , settings ) ;
2024-10-22 03:21:37 -06:00
debugPrint ( results ) ;
2024-10-22 03:17:20 -06:00
}
2025-03-11 14:57:03 -03:00
if ( results ? . items ? . length ) {
2024-10-22 00:41:46 -06:00
res . redirect ( results . items [ 0 ] . path ) ;
} else {
2025-01-28 20:14:19 -03:00
const count = await File . count ( ) ;
const randomId = Math . floor ( Math . random ( ) * count ) ;
const luckyFile = await File . findOne ( {
2025-05-18 07:11:37 -06:00
offset : randomId ,
2025-01-28 20:14:19 -03:00
} ) ;
debugPrint ( ` ${ randomId } : ${ luckyFile ? . path } ` ) ;
2025-05-18 07:11:37 -06:00
res . redirect ( luckyFile ? . path || "/" ) ;
2024-10-17 02:10:44 -06:00
}
2024-10-22 04:05:11 -06:00
queryCount += 1 ;
2025-05-18 07:11:37 -06:00
await QueryCount . update ( { count : queryCount } , { where : { id : 1 } } ) ;
updateDefaults ( ) ;
2024-10-22 00:41:46 -06:00
} ) ;
2025-05-30 04:26:40 -06:00
app . get ( "/settings" , async function ( req , res ) {
2024-10-22 00:41:46 -06:00
let options = { defaultSettings : defaultSettings } ;
let page = "settings" ;
2025-05-31 13:53:13 -06:00
options . oldSettingsAvailable = ( await Metadata . count ( ) ) ? true : false ;
2024-10-22 00:41:46 -06:00
options = buildOptions ( page , options ) ;
res . render ( indexPage , options ) ;
} ) ;
2025-05-18 07:11:37 -06:00
app . post ( "/suggest" , async function ( req , res ) {
if ( ! req . body ) {
return ;
2024-10-24 06:01:08 -06:00
}
2025-05-18 07:11:37 -06:00
if ( typeof req . body . query == "undefined" ) {
return ;
2024-10-24 06:01:08 -06:00
}
2025-05-18 07:11:37 -06:00
let suggestions = await search . getSuggestions (
req . body . query ,
defaultSettings
) ;
debugPrint ( suggestions ) ;
res . setHeader ( "Content-Type" , "application/json" ) ;
2024-10-24 06:01:08 -06:00
res . end ( JSON . stringify ( { suggestions } ) ) ;
2025-05-18 07:11:37 -06:00
} ) ;
2024-10-24 06:01:08 -06:00
2024-10-27 01:53:57 -03:00
app . get ( "/about" , function ( req , res ) {
let page = "about" ;
res . render ( indexPage , buildOptions ( page ) ) ;
} ) ;
2024-11-14 10:01:10 -03:00
app . get ( "/play/:id" , async function ( req , res ) {
// Block access if emulator is disabled
2025-05-18 07:11:37 -06:00
if ( process . env . EMULATOR _ENABLED !== "true" ) {
res . redirect ( "/" ) ;
2024-11-14 10:01:10 -03:00
return ;
}
let fileId = parseInt ( req . params . id ) ;
2025-01-28 20:14:19 -03:00
let romFile = await search . findIndex ( fileId ) ;
2024-11-14 10:01:10 -03:00
if ( ! romFile ) {
2025-05-18 07:11:37 -06:00
res . redirect ( "/" ) ;
2024-11-14 10:01:10 -03:00
return ;
}
let options = {
romFile : romFile ,
emulatorConfig : getEmulatorConfig ( romFile . category ) ,
2025-05-18 07:11:37 -06:00
isNonGame : isNonGameContent ( romFile . filename , nonGameTerms ) ,
2024-11-14 10:01:10 -03:00
} ;
let page = "emulator" ;
options = buildOptions ( page , options ) ;
res . render ( indexPage , options ) ;
} ) ;
2025-05-26 14:31:12 -06:00
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 ) ;
2025-05-30 03:21:24 -06:00
if ( ! romFile ) {
2025-05-26 14:31:12 -06:00
res . redirect ( "/" ) ;
return ;
}
let options = {
2025-05-30 03:21:24 -06:00
file : {
2025-05-31 13:53:13 -06:00
... romFile . dataValues ,
2025-05-30 03:21:24 -06:00
} ,
metadata : {
2025-05-31 13:53:13 -06:00
... romFile ? . details ? . dataValues ,
2025-05-30 03:21:24 -06:00
} ,
2025-05-27 22:35:11 -06:00
flags : flags ,
2025-05-29 16:24:12 -06:00
consoleIcons : consoleIcons ,
2025-05-31 13:53:13 -06:00
localeNames : localeNames ,
2025-05-26 14:31:12 -06:00
} ;
let page = "info" ;
options = buildOptions ( page , options ) ;
res . render ( indexPage , options ) ;
} ) ;
2025-03-31 05:16:43 -03:00
app . get ( "/proxy-rom/:id" , async function ( req , res , next ) {
2024-11-14 10:01:10 -03:00
// Block access if emulator is disabled
2025-05-18 07:11:37 -06:00
if ( process . env . EMULATOR _ENABLED !== "true" ) {
return next ( new Error ( "Emulator feature is disabled" ) ) ;
2024-11-14 10:01:10 -03:00
}
let fileId = parseInt ( req . params . id ) ;
2025-01-28 20:14:19 -03:00
let romFile = await search . findIndex ( fileId ) ;
2024-11-14 10:01:10 -03:00
if ( ! romFile ) {
2025-05-18 07:11:37 -06:00
return next ( new Error ( "ROM not found" ) ) ;
2024-11-14 10:01:10 -03:00
}
try {
2025-10-21 00:04:47 -06:00
const response = await fetch ( romFile . path , {
headers : {
"User-Agent" : "Wget/1.25.0" ,
} ,
} ) ;
2025-05-18 07:11:37 -06:00
const contentLength = response . headers . get ( "content-length" ) ;
2024-11-14 10:01:10 -03:00
2025-05-18 07:11:37 -06:00
res . setHeader ( "Content-Type" , "application/zip" ) ;
res . setHeader ( "Content-Length" , contentLength ) ;
res . setHeader (
"Content-Disposition" ,
` attachment; filename=" ${ romFile . filename } " `
) ;
2024-11-14 10:01:10 -03:00
2025-03-31 05:16:43 -03:00
// Add all required cross-origin headers
2025-05-18 07:11:37 -06:00
res . setHeader ( "Cross-Origin-Resource-Policy" , "same-origin" ) ;
res . setHeader ( "Cross-Origin-Embedder-Policy" , "require-corp" ) ;
res . setHeader ( "Cross-Origin-Opener-Policy" , "same-origin" ) ;
2025-03-31 05:16:43 -03:00
2024-11-14 10:01:10 -03:00
response . body . pipe ( res ) ;
} catch ( error ) {
2025-05-18 07:11:37 -06:00
console . error ( "Error proxying ROM:" , error ) ;
2025-03-31 05:16:43 -03:00
next ( error ) ;
2024-11-14 10:01:10 -03:00
}
} ) ;
2025-03-31 05:16:43 -03:00
app . get ( "/proxy-bios" , async function ( req , res , next ) {
2025-01-07 09:22:50 -03:00
// Block access if emulator is disabled
2025-05-18 07:11:37 -06:00
if ( process . env . EMULATOR _ENABLED !== "true" ) {
return next ( new Error ( "Emulator feature is disabled" ) ) ;
2025-01-07 09:22:50 -03:00
}
const biosUrl = req . query . url ;
// Validate that URL is from GitHub
2025-05-18 07:11:37 -06:00
if ( ! biosUrl || ! biosUrl . startsWith ( "https://github.com" ) ) {
return next ( new Error ( "Invalid BIOS URL - only GitHub URLs are allowed" ) ) ;
2025-01-07 09:22:50 -03:00
}
try {
const response = await fetch ( biosUrl ) ;
if ( ! response . ok ) {
throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
}
2025-05-18 07:11:37 -06:00
const contentLength = response . headers . get ( "content-length" ) ;
const contentType = response . headers . get ( "content-type" ) ;
2025-01-07 09:22:50 -03:00
2025-05-18 07:11:37 -06:00
res . setHeader ( "Content-Type" , contentType || "application/octet-stream" ) ;
res . setHeader ( "Content-Length" , contentLength ) ;
2025-01-07 09:22:50 -03:00
2025-03-31 05:16:43 -03:00
// Add all required cross-origin headers
2025-05-18 07:11:37 -06:00
res . setHeader ( "Cross-Origin-Resource-Policy" , "same-origin" ) ;
res . setHeader ( "Cross-Origin-Embedder-Policy" , "require-corp" ) ;
res . setHeader ( "Cross-Origin-Opener-Policy" , "same-origin" ) ;
2025-03-31 05:16:43 -03:00
2025-01-07 09:22:50 -03:00
response . body . pipe ( res ) ;
} catch ( error ) {
2025-05-18 07:11:37 -06:00
console . error ( "Error proxying BIOS:" , error ) ;
2025-03-31 05:16:43 -03:00
next ( error ) ;
}
} ) ;
// Proxy route for EmulatorJS content
2025-05-18 07:11:37 -06:00
app . get ( "/emulatorjs/*" , async function ( req , res , next ) {
2025-03-31 05:16:43 -03:00
try {
// Extract the path after /emulatorjs/
2025-05-18 07:11:37 -06:00
const filePath = req . path . replace ( /^\/emulatorjs\// , "" ) ;
2025-03-31 05:16:43 -03:00
// Support both stable and latest paths
const emulatorJsUrl = ` https://cdn.emulatorjs.org/stable/ ${ filePath } ` ;
const response = await fetch ( emulatorJsUrl , {
headers : {
2025-05-18 07:11:37 -06:00
"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" ,
} ,
2025-03-31 05:16:43 -03:00
} ) ;
if ( ! response . ok ) {
throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
}
// Copy content type and length
2025-05-18 07:11:37 -06:00
const contentType = response . headers . get ( "content-type" ) ;
2025-03-31 05:16:43 -03:00
if ( contentType ) {
2025-05-18 07:11:37 -06:00
res . setHeader ( "Content-Type" , contentType ) ;
2025-03-31 05:16:43 -03:00
}
2025-05-18 07:11:37 -06:00
const contentLength = response . headers . get ( "content-length" ) ;
2025-03-31 05:16:43 -03:00
if ( contentLength ) {
2025-05-18 07:11:37 -06:00
res . setHeader ( "Content-Length" , contentLength ) ;
2025-03-31 05:16:43 -03:00
}
// Set special headers for WASM files
2025-05-18 07:11:37 -06:00
if ( filePath . endsWith ( ".wasm" ) ) {
res . setHeader ( "Content-Type" , "application/wasm" ) ;
2025-03-31 05:16:43 -03:00
}
// Special handling for JavaScript files
2025-05-18 07:11:37 -06:00
if ( filePath . endsWith ( ".js" ) ) {
res . setHeader ( "Content-Type" , "application/javascript" ) ;
2025-03-31 05:16:43 -03:00
}
2025-05-18 07:11:37 -06:00
res . setHeader ( "Cross-Origin-Resource-Policy" , "same-origin" ) ;
res . setHeader ( "Cross-Origin-Embedder-Policy" , "require-corp" ) ;
res . setHeader ( "Cross-Origin-Opener-Policy" , "same-origin" ) ;
2025-03-31 05:16:43 -03:00
response . body . pipe ( res ) ;
} catch ( error ) {
2025-05-18 07:11:37 -06:00
console . error ( "Error proxying EmulatorJS content:" , error ) ;
2025-03-31 05:16:43 -03:00
next ( error ) ;
2025-01-07 09:22:50 -03:00
}
} ) ;
2025-03-13 15:15:38 -03:00
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 ) ;
} ) ;
2025-08-29 03:27:00 -03:00
app . post ( "/api/ai-chat" , async function ( req , res ) {
try {
const { message } = req . body ;
2025-10-21 00:04:47 -06:00
if ( ! message || typeof message !== "string" ) {
return res . status ( 400 ) . json ( { error : "Message is required" } ) ;
2025-08-29 03:27:00 -03:00
}
// Check if AI is enabled and configured
2025-10-21 00:04:47 -06:00
const aiEnabled = process . env . AI _ENABLED === "true" ;
2025-08-29 03:27:00 -03:00
const apiKey = process . env . AI _API _KEY ;
2025-10-21 00:04:47 -06:00
const apiUrl =
process . env . AI _API _URL || "https://api.openai.com/v1/chat/completions" ;
const model = process . env . AI _MODEL || "gpt-3.5-turbo" ;
2025-08-29 03:27:00 -03:00
if ( ! aiEnabled ) {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI chat is currently disabled. Please contact the administrator." ,
2025-08-29 03:27:00 -03:00
} ) ;
}
if ( ! apiKey ) {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI service is not configured. Please contact the administrator." ,
2025-08-29 03:27:00 -03:00
} ) ;
}
// 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 :
2025-10-18 04:52:58 -03:00
- Help users find games they ' re looking for by using the search tools available to you
2025-08-29 03:27:00 -03:00
- 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
2025-10-18 04:52:58 -03:00
- When users ask for games , always use the search _games tool to find them
- Keep responses SHORT , CONCISE and SIMPLE - the chat interface is small
- Present search results as simple lists , NOT tables ( tables don ' t fit in the small chat window )
- Use bullet points or numbered lists instead of tables
- Limit responses to 3 - 5 game recommendations maximum to keep it readable
2025-08-29 03:27:00 -03:00
- If users ask about downloading or legal issues , remind them that Myrient focuses on preservation
2025-10-18 04:52:58 -03:00
IMPORTANT SEARCH STRATEGY :
- When users describe a game , THINK about what the actual game title might be before searching
- Don ' t search literal descriptions - identify the likely game name first
- Use SIMPLE searches with just the game title for best results
- The search is fuzzy and will find partial matches - keep queries simple
- If first search fails or returns few results , try alternative searches with different terms
- For empty results , suggest the user try different search terms or check spelling
GAME RECOMMENDATION STRATEGY :
- When users ask for "best [console] games" , don ' t search the console name
- Instead , search for specific popular titles for that console
- EFFICIENCY : Limit to 2 - 3 searches maximum for recommendations to avoid hitting rate limits
- Stop searching when you have enough games ( 3 - 5 ) for a good recommendation
- Focus on well - known AAA titles , not obscure indie games
Available Tools :
- search _games : Fuzzy text search for games by title / name ( returns URLs for each game )
- get _search _suggestions : Get search suggestions for partial queries
CRITICAL LINKING RULES :
- NEVER make up or guess URLs - ONLY use URLs from search tool results
- When mentioning specific games found via search _games tool , ALWAYS link to them using EXACTLY the urls . info value from the search results
- Format : [ Game Title ] ( EXACT _INFO _URL _FROM _SEARCH _RESULTS )
- Do NOT create links like / info / 123 - use the EXACT urls . info field from the tool response
- If you haven ' t searched for a game using the tool , do NOT create any links for it
- Only link to games that were actually returned by the search _games tool with their provided URLs ` ;
// Import tools dynamically
2025-10-21 00:04:47 -06:00
const { tools , executeToolCall } = await import ( "./lib/ai/tools.js" ) ;
2025-10-18 04:52:58 -03:00
// Build conversation history
2025-10-21 00:04:47 -06:00
let messages = [ { role : "system" , content : systemPrompt } ] ;
2025-10-18 04:52:58 -03:00
// Add conversation history if provided
if ( req . body . conversation && Array . isArray ( req . body . conversation ) ) {
messages = messages . concat ( req . body . conversation ) ;
}
// Add current user message
2025-10-21 00:04:47 -06:00
messages . push ( { role : "user" , content : message } ) ;
2025-08-29 03:27:00 -03:00
2025-10-18 04:52:58 -03:00
let aiResponse = await fetch ( apiUrl , {
2025-10-21 00:04:47 -06:00
method : "POST" ,
2025-08-29 03:27:00 -03:00
headers : {
2025-10-21 00:04:47 -06:00
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ apiKey } ` ,
"User-Agent" : "Myrient-Search-Engine/1.0" ,
2025-08-29 03:27:00 -03:00
} ,
body : JSON . stringify ( {
model : model ,
2025-10-18 04:52:58 -03:00
messages : messages ,
tools : tools ,
2025-10-21 00:04:47 -06:00
tool _choice : "auto" ,
2025-10-18 04:52:58 -03:00
max _tokens : 1000 ,
2025-08-29 03:27:00 -03:00
temperature : 0.7 ,
2025-10-21 00:04:47 -06:00
stream : false ,
} ) ,
2025-08-29 03:27:00 -03:00
} ) ;
if ( ! aiResponse . ok ) {
const errorData = await aiResponse . json ( ) . catch ( ( ) => ( { } ) ) ;
2025-10-21 00:04:47 -06:00
console . error ( "AI API Error on initial request:" ) ;
console . error ( "Status:" , aiResponse . status ) ;
console . error ( "Error data:" , errorData ) ;
console . error ( "Request details:" ) ;
console . error ( "- Model:" , model ) ;
console . error ( "- Messages count:" , messages . length ) ;
console . error ( "- User message:" , message . substring ( 0 , 100 ) + "..." ) ;
2025-08-29 03:27:00 -03:00
// Handle specific error cases
if ( aiResponse . status === 401 ) {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI service authentication failed. Please contact the administrator." ,
2025-08-29 03:27:00 -03:00
} ) ;
} else if ( aiResponse . status === 429 ) {
return res . status ( 429 ) . json ( {
2025-10-21 00:04:47 -06:00
error : "AI service is currently busy. Please try again in a moment." ,
2025-08-29 03:27:00 -03:00
} ) ;
} else {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI service is temporarily unavailable. Please try again later." ,
2025-08-29 03:27:00 -03:00
} ) ;
}
}
2025-10-18 04:52:58 -03:00
let aiData = await aiResponse . json ( ) ;
2025-08-29 03:27:00 -03:00
if ( ! aiData . choices || aiData . choices . length === 0 ) {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error : "AI service returned an unexpected response." ,
2025-08-29 03:27:00 -03:00
} ) ;
}
2025-10-18 04:52:58 -03:00
let assistantMessage = aiData . choices [ 0 ] . message ;
let toolCallsCount = 0 ; // Track tool calls executed
let toolsUsed = [ ] ; // Track which tools were used
2025-10-21 00:04:47 -06:00
console . log ( "Initial AI request successful" ) ;
2025-10-18 04:52:58 -03:00
// Handle multiple rounds of tool calls
let maxToolRounds = 3 ; // Prevent infinite loops and token exhaustion
let currentRound = 0 ;
2025-10-21 00:04:47 -06:00
while (
assistantMessage . tool _calls &&
assistantMessage . tool _calls . length > 0 &&
currentRound < maxToolRounds
) {
2025-10-18 04:52:58 -03:00
currentRound ++ ;
const roundToolCalls = assistantMessage . tool _calls . length ;
2025-10-21 00:04:47 -06:00
const roundToolsUsed = assistantMessage . tool _calls . map (
( tc ) => tc . function . name
) ;
2025-10-18 04:52:58 -03:00
2025-10-21 00:04:47 -06:00
console . log (
` Round ${ currentRound } : AI wants to use ${ roundToolCalls } tools: ${ roundToolsUsed . join (
", "
) } `
) ;
2025-10-18 04:52:58 -03:00
// Track total tools across all rounds
toolCallsCount += roundToolCalls ;
toolsUsed = toolsUsed . concat ( roundToolsUsed ) ;
// Add assistant message with tool calls to conversation
messages . push ( assistantMessage ) ;
// Execute each tool call in this round
for ( const toolCall of assistantMessage . tool _calls ) {
try {
const toolResult = await executeToolCall ( toolCall ) ;
// Add tool result to conversation
messages . push ( {
2025-10-21 00:04:47 -06:00
role : "tool" ,
2025-10-18 04:52:58 -03:00
tool _call _id : toolCall . id ,
2025-10-21 00:04:47 -06:00
content : JSON . stringify ( toolResult ) ,
2025-10-18 04:52:58 -03:00
} ) ;
} catch ( error ) {
2025-10-21 00:04:47 -06:00
console . error ( "Tool execution error:" , error ) ;
2025-10-18 04:52:58 -03:00
// Add error result
messages . push ( {
2025-10-21 00:04:47 -06:00
role : "tool" ,
2025-10-18 04:52:58 -03:00
tool _call _id : toolCall . id ,
2025-10-21 00:04:47 -06:00
content : JSON . stringify ( { error : error . message } ) ,
2025-10-18 04:52:58 -03:00
} ) ;
}
}
// Get AI response after this round of tool execution
2025-10-21 00:04:47 -06:00
console . log (
` Making AI request after round ${ currentRound } tool execution... `
) ;
2025-10-18 04:52:58 -03:00
aiResponse = await fetch ( apiUrl , {
2025-10-21 00:04:47 -06:00
method : "POST" ,
2025-10-18 04:52:58 -03:00
headers : {
2025-10-21 00:04:47 -06:00
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ apiKey } ` ,
"User-Agent" : "Myrient-Search-Engine/1.0" ,
2025-10-18 04:52:58 -03:00
} ,
body : JSON . stringify ( {
model : model ,
messages : messages ,
tools : tools ,
2025-10-21 00:04:47 -06:00
tool _choice : "auto" ,
2025-10-18 04:52:58 -03:00
max _tokens : 1000 ,
temperature : 0.7 ,
2025-10-21 00:04:47 -06:00
stream : false ,
} ) ,
2025-10-18 04:52:58 -03:00
} ) ;
if ( ! aiResponse . ok ) {
const errorData = await aiResponse . json ( ) . catch ( ( ) => ( { } ) ) ;
2025-10-21 00:04:47 -06:00
console . error (
` AI API Error after round ${ currentRound } tool execution: `
) ;
console . error ( "Status:" , aiResponse . status ) ;
console . error ( "Error data:" , errorData ) ;
console . error ( "Request details:" ) ;
console . error ( "- Model:" , model ) ;
console . error ( "- Messages count:" , messages . length ) ;
console . error ( "- Tools used:" , toolsUsed ) ;
2025-10-18 04:52:58 -03:00
// Handle specific error cases
if ( aiResponse . status === 429 ) {
// Extract wait time from error message if available
let waitTime = 5000 ; // Default 5 seconds
if ( errorData . error ? . message ) {
2025-10-21 00:04:47 -06:00
const waitMatch = errorData . error . message . match (
/Please try again in ([\d.]+)s/
) ;
2025-10-18 04:52:58 -03:00
if ( waitMatch ) {
waitTime = Math . ceil ( parseFloat ( waitMatch [ 1 ] ) * 1000 ) + 1000 ; // Add 1 extra second
}
}
2025-10-21 00:04:47 -06:00
console . error (
` Rate limit hit after tool execution. Waiting ${
waitTime / 1000
} s and retrying once ... `
) ;
await new Promise ( ( resolve ) => setTimeout ( resolve , waitTime ) ) ;
2025-10-18 04:52:58 -03:00
const retryResponse = await fetch ( apiUrl , {
2025-10-21 00:04:47 -06:00
method : "POST" ,
2025-10-18 04:52:58 -03:00
headers : {
2025-10-21 00:04:47 -06:00
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ apiKey } ` ,
"User-Agent" : "Myrient-Search-Engine/1.0" ,
2025-10-18 04:52:58 -03:00
} ,
body : JSON . stringify ( {
model : model ,
messages : messages ,
tools : tools ,
2025-10-21 00:04:47 -06:00
tool _choice : "auto" ,
2025-10-18 04:52:58 -03:00
max _tokens : 1000 ,
temperature : 0.7 ,
2025-10-21 00:04:47 -06:00
stream : false ,
} ) ,
2025-10-18 04:52:58 -03:00
} ) ;
if ( retryResponse . ok ) {
2025-10-21 00:04:47 -06:00
console . log ( "Retry successful after rate limit" ) ;
2025-10-18 04:52:58 -03:00
aiData = await retryResponse . json ( ) ;
assistantMessage = aiData . choices [ 0 ] . message ;
} else {
2025-10-21 00:04:47 -06:00
console . error (
"Retry also failed with status:" ,
retryResponse . status
) ;
2025-10-18 04:52:58 -03:00
return res . status ( 429 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI service is currently busy processing your request. Please try again in a moment." ,
2025-10-18 04:52:58 -03:00
} ) ;
}
} else if ( aiResponse . status === 401 ) {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI service authentication failed. Please contact the administrator." ,
2025-10-18 04:52:58 -03:00
} ) ;
} else {
return res . status ( 503 ) . json ( {
2025-10-21 00:04:47 -06:00
error :
"AI service encountered an error while processing your request. Please try again later." ,
2025-10-18 04:52:58 -03:00
} ) ;
}
} else {
2025-10-21 00:04:47 -06:00
console . log (
` AI request after round ${ currentRound } tool execution successful `
) ;
2025-10-18 04:52:58 -03:00
aiData = await aiResponse . json ( ) ;
assistantMessage = aiData . choices [ 0 ] . message ;
2025-10-21 00:04:47 -06:00
console . log (
` Round ${ currentRound } response - has tool_calls: ` ,
! ! assistantMessage . tool _calls
) ;
console . log (
` Round ${ currentRound } response - has content: ` ,
! ! assistantMessage . content
) ;
2025-10-18 04:52:58 -03:00
}
}
if ( currentRound >= maxToolRounds && assistantMessage . tool _calls ) {
2025-10-21 00:04:47 -06:00
console . warn (
"Maximum tool rounds reached, AI still wants to use tools. Stopping."
) ;
2025-10-18 04:52:58 -03:00
}
if ( currentRound === 0 ) {
2025-10-21 00:04:47 -06:00
console . log ( "No tool calls needed, using initial response" ) ;
2025-10-18 04:52:58 -03:00
} else {
console . log ( ` Total rounds completed: ${ currentRound } ` ) ;
}
2025-08-29 03:27:00 -03:00
2025-10-21 00:04:47 -06:00
console . log (
"Final tool calls check - has tool_calls:" ,
! ! assistantMessage . tool _calls
) ;
console . log (
"Final tool calls check - has content:" ,
! ! assistantMessage . content
) ;
2025-10-18 04:52:58 -03:00
2025-10-21 00:04:47 -06:00
console . log (
"Final assistant message structure:" ,
JSON . stringify ( assistantMessage , null , 2 )
) ;
console . log ( "Assistant message content:" , assistantMessage . content ) ;
console . log (
"Assistant message content type:" ,
typeof assistantMessage . content
) ;
console . log ( "Assistant message keys:" , Object . keys ( assistantMessage ) ) ;
2025-10-18 04:52:58 -03:00
2025-10-21 00:04:47 -06:00
const response = assistantMessage . content ? . trim ( ) || "Something went wrong" ;
console . log (
"Final response after processing:" ,
response . substring ( 0 , 100 ) + "..."
) ;
console . log ( "Tools used in this request:" , toolsUsed ) ;
2025-10-18 04:52:58 -03:00
// Return the response along with updated conversation
res . json ( {
response ,
conversation : messages . slice ( 1 ) , // Exclude system message from returned conversation
tool _calls _made : toolCallsCount ,
2025-10-21 00:04:47 -06:00
tools _used : toolsUsed ,
2025-10-18 04:52:58 -03:00
} ) ;
2025-08-29 03:27:00 -03:00
} catch ( error ) {
2025-10-21 00:04:47 -06:00
console . error ( "AI Chat Error:" , error ) ;
2025-08-29 03:27:00 -03:00
res . status ( 500 ) . json ( {
2025-10-21 00:04:47 -06:00
error : "An unexpected error occurred. Please try again later." ,
2025-08-29 03:27:00 -03:00
} ) ;
}
} ) ;
2025-03-31 05:16:43 -03:00
app . get ( "/proxy-image" , async function ( req , res , next ) {
const imageUrl = req . query . url ;
if ( ! imageUrl ) {
2025-05-18 07:11:37 -06:00
return next ( new Error ( "No image URL provided" ) ) ;
2025-03-31 05:16:43 -03:00
}
try {
const response = await fetch ( imageUrl , {
headers : {
2025-05-18 07:11:37 -06:00
"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" ,
} ,
2025-03-31 05:16:43 -03:00
} ) ;
if ( ! response . ok ) {
throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
}
// Copy content type
2025-05-18 07:11:37 -06:00
const contentType = response . headers . get ( "content-type" ) ;
2025-03-31 05:16:43 -03:00
if ( contentType ) {
2025-05-18 07:11:37 -06:00
res . setHeader ( "Content-Type" , contentType ) ;
2025-03-31 05:16:43 -03:00
}
2025-05-18 07:11:37 -06:00
const contentLength = response . headers . get ( "content-length" ) ;
2025-03-31 05:16:43 -03:00
if ( contentLength ) {
2025-05-18 07:11:37 -06:00
res . setHeader ( "Content-Length" , contentLength ) ;
2025-03-31 05:16:43 -03:00
}
response . body . pipe ( res ) ;
} catch ( error ) {
2025-05-18 07:11:37 -06:00
console . error ( "Error proxying image:" , error ) ;
2025-03-31 05:16:43 -03:00
next ( error ) ;
}
} ) ;
// 404 handler
app . use ( ( req , res , next ) => {
2025-05-18 07:11:37 -06:00
const err = new Error ( "Page Not Found" ) ;
2025-03-31 05:16:43 -03:00
err . status = 404 ;
next ( err ) ;
} ) ;
// Error handling middleware
app . use ( ( err , req , res , next ) => {
const status = err . status || 500 ;
2025-05-18 07:11:37 -06:00
const message = err . message || "An unexpected error occurred" ;
2025-03-31 05:16:43 -03:00
2025-05-18 07:11:37 -06:00
if ( process . env . NODE _ENV !== "production" ) {
2025-03-31 05:16:43 -03:00
console . error ( err ) ;
}
res . status ( status ) ;
2025-05-18 07:11:37 -06:00
res . render ( "pages/error" , {
2025-03-31 05:16:43 -03:00
status ,
message ,
2025-05-18 07:11:37 -06:00
stack : process . env . NODE _ENV !== "production" ? err . stack : null ,
2025-03-31 05:16:43 -03:00
req ,
2025-05-18 07:11:37 -06:00
requestId : req . requestId ,
2025-03-31 05:16:43 -03:00
} ) ;
} ) ;
2024-10-22 00:41:46 -06:00
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. ` ) ;
2025-05-31 14:12:47 -06:00
console . log ( ` ${ metadataMatchCount } files contain matched metadata. ` ) ;
2024-10-22 00:41:46 -06:00
2025-01-28 20:14:19 -03:00
// 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 ( ) ;
}
2025-05-24 02:40:43 -06:00
cron . schedule ( "0 30 2 * * *" , getFilesJob ) ;
2025-05-31 14:47:54 -06:00
//run these tasks after to add new functions
2025-05-31 15:14:42 -06:00
await updateMetadata ( ) ;
await updateKws ( ) ;