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-05-31 14:47:54 -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-05-31 17:30:49 -06:00
let updateMatches = process . env . FORCE _METADATA _RESYNC == "1" ? true : false
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 ;
}
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-05-31 16:44:37 -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-08-29 04:06:40 -03:00
aiEnabled : process . env . AI _ENABLED === 'true' ,
aiConfig : {
apiUrl : process . env . AI _API _URL || 'https://example.com' ,
model : process . env . AI _MODEL || 'default' ,
} ,
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 {
const response = await fetch ( romFile . path ) ;
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 ;
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 :
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
const { tools , executeToolCall } = await import ( './lib/ai/tools.js' ) ;
// Build conversation history
let messages = [
{ role : 'system' , content : systemPrompt }
] ;
// Add conversation history if provided
if ( req . body . conversation && Array . isArray ( req . body . conversation ) ) {
messages = messages . concat ( req . body . conversation ) ;
}
// Add current user message
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-08-29 03:27:00 -03:00
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Authorization' : ` Bearer ${ apiKey } ` ,
'User-Agent' : 'Myrient-Search-Engine/1.0'
} ,
body : JSON . stringify ( {
model : model ,
2025-10-18 04:52:58 -03:00
messages : messages ,
tools : tools ,
tool _choice : 'auto' ,
max _tokens : 1000 ,
2025-08-29 03:27:00 -03:00
temperature : 0.7 ,
stream : false
} )
} ) ;
if ( ! aiResponse . ok ) {
const errorData = await aiResponse . json ( ) . catch ( ( ) => ( { } ) ) ;
2025-10-18 04:52:58 -03: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 ( {
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.'
} ) ;
}
}
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 ( {
error : 'AI service returned an unexpected response.'
} ) ;
}
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
console . log ( 'Initial AI request successful' ) ;
// Handle multiple rounds of tool calls
let maxToolRounds = 3 ; // Prevent infinite loops and token exhaustion
let currentRound = 0 ;
while ( assistantMessage . tool _calls && assistantMessage . tool _calls . length > 0 && currentRound < maxToolRounds ) {
currentRound ++ ;
const roundToolCalls = assistantMessage . tool _calls . length ;
const roundToolsUsed = assistantMessage . tool _calls . map ( tc => tc . function . name ) ;
console . log ( ` Round ${ currentRound } : AI wants to use ${ roundToolCalls } tools: ${ roundToolsUsed . join ( ', ' ) } ` ) ;
// 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 ( {
role : 'tool' ,
tool _call _id : toolCall . id ,
content : JSON . stringify ( toolResult )
} ) ;
} catch ( error ) {
console . error ( 'Tool execution error:' , error ) ;
// Add error result
messages . push ( {
role : 'tool' ,
tool _call _id : toolCall . id ,
content : JSON . stringify ( { error : error . message } )
} ) ;
}
}
// Get AI response after this round of tool execution
console . log ( ` Making AI request after round ${ currentRound } tool execution... ` ) ;
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 : messages ,
tools : tools ,
tool _choice : 'auto' ,
max _tokens : 1000 ,
temperature : 0.7 ,
stream : false
} )
} ) ;
if ( ! aiResponse . ok ) {
const errorData = await aiResponse . json ( ) . catch ( ( ) => ( { } ) ) ;
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 ) ;
// 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 ) {
const waitMatch = errorData . error . message . match ( /Please try again in ([\d.]+)s/ ) ;
if ( waitMatch ) {
waitTime = Math . ceil ( parseFloat ( waitMatch [ 1 ] ) * 1000 ) + 1000 ; // Add 1 extra second
}
}
console . error ( ` Rate limit hit after tool execution. Waiting ${ waitTime / 1000 } s and retrying once... ` ) ;
await new Promise ( resolve => setTimeout ( resolve , waitTime ) ) ;
const retryResponse = 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 : messages ,
tools : tools ,
tool _choice : 'auto' ,
max _tokens : 1000 ,
temperature : 0.7 ,
stream : false
} )
} ) ;
if ( retryResponse . ok ) {
console . log ( 'Retry successful after rate limit' ) ;
aiData = await retryResponse . json ( ) ;
assistantMessage = aiData . choices [ 0 ] . message ;
} else {
console . error ( 'Retry also failed with status:' , retryResponse . status ) ;
return res . status ( 429 ) . json ( {
error : 'AI service is currently busy processing your request. Please try again in a moment.'
} ) ;
}
} else if ( aiResponse . status === 401 ) {
return res . status ( 503 ) . json ( {
error : 'AI service authentication failed. Please contact the administrator.'
} ) ;
} else {
return res . status ( 503 ) . json ( {
error : 'AI service encountered an error while processing your request. Please try again later.'
} ) ;
}
} else {
console . log ( ` AI request after round ${ currentRound } tool execution successful ` ) ;
aiData = await aiResponse . json ( ) ;
assistantMessage = aiData . choices [ 0 ] . message ;
console . log ( ` Round ${ currentRound } response - has tool_calls: ` , ! ! assistantMessage . tool _calls ) ;
console . log ( ` Round ${ currentRound } response - has content: ` , ! ! assistantMessage . content ) ;
}
}
if ( currentRound >= maxToolRounds && assistantMessage . tool _calls ) {
console . warn ( 'Maximum tool rounds reached, AI still wants to use tools. Stopping.' ) ;
}
if ( currentRound === 0 ) {
console . log ( 'No tool calls needed, using initial response' ) ;
} else {
console . log ( ` Total rounds completed: ${ currentRound } ` ) ;
}
2025-08-29 03:27:00 -03:00
2025-10-18 04:52:58 -03:00
console . log ( 'Final tool calls check - has tool_calls:' , ! ! assistantMessage . tool _calls ) ;
console . log ( 'Final tool calls check - has content:' , ! ! assistantMessage . content ) ;
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 ) ) ;
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 ) ;
// Return the response along with updated conversation
res . json ( {
response ,
conversation : messages . slice ( 1 ) , // Exclude system message from returned conversation
tool _calls _made : toolCallsCount ,
tools _used : toolsUsed
} ) ;
2025-08-29 03:27:00 -03:00
} catch ( error ) {
console . error ( 'AI Chat Error:' , error ) ;
res . status ( 500 ) . json ( {
error : 'An unexpected error occurred. Please try again later.'
} ) ;
}
} ) ;
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 ( ) ;