diff --git a/config/locales/en.json b/config/locales/en.json index b51bb02..d878da2 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -21,7 +21,12 @@ "in_seconds": "in {{seconds}} seconds", "indexing": "Indexing in progress, if the list is missing something please try reloading in a few minutes", "non_game_filter": "Non-game content filter is active", - "displaying_results": "Displaying results {{start}} through {{end}}." + "no_description": "No descrption found.", + "no_metadata": "No metadata found.", + "displaying_results": "Displaying results {{start}} through {{end}}.", + "released": "Released", + "region": "Region", + "platform": "Platform" }, "about": { "title": "About", @@ -40,6 +45,10 @@ "credits": { "created_by": "Search engine created by", "view_github": "View project on GitHub" + }, + "metadata": { + "title": "Metadata Information", + "description": "This website pulls metadata information about games from {{metadata_source}}. This information is pulled on search. Search queries may be slow until metadata has been cached in the database." } }, "settings": { @@ -69,6 +78,10 @@ "hide_non_game": { "label": "Hide Non-Game Content", "tooltip": "Filters out ROM hacks, patches, artwork, and other non-game content from search results." + }, + "use_old_results": { + "label": "Old Search", + "tooltip": "Redirects your search to the old table formatted results. These will load faster as they will ignore pulling metadata." } }, "save": "Save Settings" diff --git a/lib/metadatasearch.js b/lib/metadatasearch.js index 610ff13..8f9b434 100644 --- a/lib/metadatasearch.js +++ b/lib/metadatasearch.js @@ -48,6 +48,8 @@ export default class MetadataSearch { "game_localizations.region.name", "platforms.name", "game_type.type", + "screenshots.image_id", + "videos.video_id" ]; async setupClient() { @@ -143,6 +145,8 @@ export default class MetadataSearch { string = string.replace("Nintendo Satellaview", "Satellaview"); string = string.replace("Sony PlayStation", "PlayStation"); string = string.replace("Microsoft Xbox", "Xbox"); + string = string.replace("Commodore 64", "Commodore C64"); + string = string.replace("Commodore Amiga", "Amiga"); return [where("platforms.name", op, string, WhereFlags.CONTAINS)]; } @@ -208,16 +212,13 @@ export default class MetadataSearch { return combined; } catch (error) { console.error("Error getting metadata:", error); + return [] } } async addMetadataToDb(metadata, game) { try { - let md = await Metadata.findOne({ - where: { - id: metadata.id, - }, - }); + let md = await Metadata.findByPk(metadata.id); if (!md) { md = await Metadata.build( { @@ -237,6 +238,8 @@ export default class MetadataSearch { platforms: JSON.stringify( metadata.platforms?.map((platform) => platform.name) ), + screenshots: JSON.stringify(metadata.screenshots?.map((ss) => ss.image_id)), + videos: JSON.stringify(metadata.videos?.map((v) => v.video_id)) }, { returning: true, diff --git a/lib/models/metadata.js b/lib/models/metadata.js index 7cc42f4..cfeaded 100644 --- a/lib/models/metadata.js +++ b/lib/models/metadata.js @@ -39,6 +39,12 @@ export default function (sequelize) { }, platforms: { type: DataTypes.STRING + }, + screenshots: { + type: DataTypes.STRING + }, + videos:{ + type: DataTypes.STRING } }, { indexes: [ diff --git a/lib/services/elasticsearch.js b/lib/services/elasticsearch.js index a46f50e..f63858f 100644 --- a/lib/services/elasticsearch.js +++ b/lib/services/elasticsearch.js @@ -102,7 +102,7 @@ export async function bulkIndexFiles(files) { filenamekws: file.filenamekws, categorykws: file.categorykws, regionkws: file.regionkws, - nongame: file.nongame + nongame: file.nongame, }, ]); @@ -168,7 +168,7 @@ export async function search(query, options) { // Fetch full records from PostgreSQL for the search results const ids = response.hits.hits.map((hit) => hit._id); const fullRecords = await File.findAll({ - where: { id: ids }, + where: { id: ids } }); // Create a map of full records by id @@ -179,7 +179,7 @@ export async function search(query, options) { // Build results with full PostgreSQL records let results = response.hits.hits.map((hit) => ({ - file:{ + file: { ...recordMap[hit._id]?.dataValues, }, score: hit._score, @@ -187,7 +187,7 @@ export async function search(query, options) { })); //Filter out anything that couldn't be found in postgres - results = results.filter(result => result.file.filename) + results = results.filter((result) => result.file.filename); const elapsed = timer.elapsedSeconds(); return { diff --git a/lib/taskqueue.js b/lib/taskqueue.js index 49d70a0..98664af 100644 --- a/lib/taskqueue.js +++ b/lib/taskqueue.js @@ -1,6 +1,11 @@ export default class TaskQueue { - constructor(maxTasksPerSecond = 4, maxSimultaneousTasks = 8) { + constructor( + maxTasksPerSecond = 4, + maxSimultaneousTasks = 8, + maxQueueLength = 20 + ) { this.maxTasksPerSecond = maxTasksPerSecond; + this.maxQueueLength = maxQueueLength; this.maxSimultaneousTasks = maxSimultaneousTasks; this.queue = []; this.processing = false; @@ -9,8 +14,12 @@ export default class TaskQueue { this.tasksWaiting = 0; } - async enqueue(taskFunction, that=this, ...args) { + async enqueue(taskFunction, that = this, ...args) { return new Promise((resolve, reject) => { + if (this.queue.length >= this.maxQueueLength) { + reject(new Error("Queue is full. Maximum queue size exceeded.")); + return; + } this.queue.push({ taskFunction, that, diff --git a/server.js b/server.js index cdf3de0..a8acf59 100644 --- a/server.js +++ b/server.js @@ -171,7 +171,11 @@ app.get("/", function (req, res) { app.get("/search", async function (req, res) { 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}&p=`); + let urlPrefix = encodeURI( + `/search?s=${req.query.s}&q=${req.query.q}${ + req.query.o ? "&o=" + req.query.o : "" + }&p=` + ); pageNum = pageNum ? pageNum : 1; let settings = {}; try { @@ -198,9 +202,13 @@ app.get("/search", async function (req, res) { } settings.pageSize = settings.useOldResults ? 100 : 10; settings.page = pageNum - 1; + settings.sort = req.query.o || ""; let results = await search.findAllMatches(query, settings); debugPrint(results); - let metas = await metadataSearch.queueGetGamesMetadata(results.db); + let metas = []; + if (!settings.useOldResults) { + metas = await metadataSearch.queueGetGamesMetadata(results.db); + } if (results.count && pageNum == 1) { queryCount += 1; await QueryCount.update({ count: queryCount }, { where: { id: 1 } }); @@ -301,6 +309,29 @@ app.get("/play/:id", async function (req, res) { 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"); + if (!metadataSearch.authorized) { + res.redirect("/"); + return; + } + let romId = parseInt(req.params.id); + let romFile = await search.findIndex(romId); + let romInfo = await metadataSearch.queueGetGamesMetadata([romFile]); + + if (!romInfo.length) { + res.redirect("/"); + return; + } + let options = { + romFile: romInfo[0], + }; + 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") { diff --git a/views/pages/about.ejs b/views/pages/about.ejs index 2084c88..7961ed5 100644 --- a/views/pages/about.ejs +++ b/views/pages/about.ejs @@ -16,6 +16,11 @@ <%= __('about.donate') %> +
+
<%= __('about.metadata.title') %>
+

<%= __('about.metadata.description', {metadata_source: 'IGDB'}) %>

+
+
<%= __('about.emulator.title') %>
<% if (process.env.EMULATOR_ENABLED === 'true') { %> diff --git a/views/pages/info.ejs b/views/pages/info.ejs new file mode 100644 index 0000000..52975f9 --- /dev/null +++ b/views/pages/info.ejs @@ -0,0 +1,99 @@ +<% + const metadata = romFile.metadata || new Object() + const file = romFile.file || new Object() + const coverUrl = metadata.coverartid ? `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_cover_big/${metadata.coverartid}.webp` : "/public/images/coverart/nocoverart.png" + let images = [] + if(metadata.screenshots){ + images = JSON.parse(metadata.screenshots).map((im) => `/proxy-image?url=https://images.igdb.com/igdb/image/upload/t_720p/${im}.webp`) + } + let videos = [] + if(metadata.videos){ + videos = JSON.parse(metadata.videos) + } +%> + +
+
+
+
+

<%= metadata.title %>

+

<%= file.category %>

+
+
+ +
+ <% if(metadata.rating) {%> +
+ <% + const fullstars = Math.floor(metadata.rating / 20) + const halfstars = Math.floor(metadata.rating % 20 / 10) + const nostars = 5 - fullstars - halfstars + const nostarstring = '' + const fullstarstring = '' + const halfstarstring = '' + let stars = '' + for(let x = 0; x < fullstars ; x++){ + stars += fullstarstring + } + if(halfstars){ + stars += halfstarstring + } + for(let x = 0; x < nostars; x++){ + stars += nostarstring + } + %> +

<%- stars%> (<%= Math.floor(metadata.rating) %>%)

+
+ <% } %> + <% if(metadata.developers) {%> +
+

Developed by: <%= JSON.parse(metadata.developers).join(", ") %>

+
+ <% } %> + <% if(metadata.publishers) {%> +
+

Published by: <%= JSON.parse(metadata.publishers).join(", ") %>

+
+ <% } %> + <% if(metadata.releasedate) {%> +
+

Release date: <%= metadata.releasedate %>

+
+ <% } %> + <% if(file.region) {%> +
+

Region: <%= file.region %>

+
+ <% } %> + <% if(metadata.genre) {%> +
+

Genre: <%= JSON.parse(metadata.genre).join(", ") %>

+
+ <% } %> + <% if(metadata.gamemodes) {%> +
+

Gameplay modes: <%= JSON.parse(metadata.gamemodes).join(", ") %>

+
+ <% } %> +
+

<%= metadata.description %>

+
+
+

+ Download + <% if (process.env.EMULATOR_ENABLED === 'true') { %> + <% if (isEmulatorCompatible(file.category)) { %> + <%= __('emulator.play')%> <% } else { %> + + <% } + }%> +

+
+
+
+ <%- include("../partials/carousel", {images: images, videos: videos})%> +
+
+
+ +
\ No newline at end of file diff --git a/views/pages/results.ejs b/views/pages/results.ejs index 83151ae..3b3218c 100644 --- a/views/pages/results.ejs +++ b/views/pages/results.ejs @@ -4,12 +4,6 @@ pageNum = 1 } %> - - - - - -
diff --git a/views/pages/resultsold.ejs b/views/pages/resultsold.ejs index 74efa8e..a436ef2 100644 --- a/views/pages/resultsold.ejs +++ b/views/pages/resultsold.ejs @@ -1,12 +1,8 @@ <% - let pageCount = Math.ceil(results.items.length / 100) pageCount = pageCount ? pageCount : 1 //always ensure 1 page if(pageNum > pageCount){ pageNum = 1 } - let entryStart = Math.floor((pageNum - 1) * 100) - let entryEnd = entryStart + 100 - entryEnd = entryEnd > results.items.length ? results.items.length : entryEnd %> @@ -14,6 +10,7 @@ +
@@ -31,7 +28,7 @@

    - <%= __('search.found_plural', { count: results.items.length }) %> <%= __('search.in_seconds', { seconds: results.elapsed }) %>. + <%= __('search.found_plural', { count: count }) %> <%= __('search.in_seconds', { seconds: elapsed }) %>. <%= indexing ? __('search.indexing') : "" %> <% if (settings.hideNonGame) { %> @@ -43,55 +40,55 @@

    -

    <%= __('search.displaying_results', { start: entryStart, end: entryEnd }) %>

    +

    <%= __('search.displaying_results', { start: ((pageNum - 1) * 100), end: pageNum * 100 < count ? pageNum * 100 : count }) %>

    - - - - - - - - + + + + + + + + <% if (process.env.EMULATOR_ENABLED === 'true') { %> <% } %> - <% for (let x = entryStart; x < entryEnd; x++) { %> + <% for (let x = 0; x < results.length; x++) { %> <% if (process.env.EMULATOR_ENABLED === 'true') { %>
    <%= __('results.table.name') %><%= __('results.table.group') %><%= __('results.table.category') %><%= __('results.table.region') %><%= __('results.table.type') %><%= __('results.table.size') %><%= __('results.table.date') %><%= __('results.table.score') %><%= __('results.table.name') %><%= __('results.table.group') %><%= __('results.table.category') %><%= __('results.table.region') %><%= __('results.table.type') %><%= __('results.table.size') %><%= __('results.table.date') %><%= __('results.table.score') %><%= __('results.table.play') %>
    - - <%= results.items[x].filename %> + + <%= results[x].file.filename %> - <%= results.items[x].group %> + <%= results[x].file.group %> - <%= results.items[x].category %> + <%= results[x].file.category %> - <%= results.items[x].region %> + <%= results[x].file.region %> - <%= results.items[x].type %> + <%= results[x].file.type %> - <%= results.items[x].size %> + <%= results[x].file.size %> - <%= results.items[x].date %> + <%= results[x].file.date %> - <%= results.items[x].score.toFixed(2) %> + <%= results[x].score.toFixed(2) %> - <% if (isEmulatorCompatible(results.items[x].category)) { %> - <%= __('emulator.play') %> + <% if (isEmulatorCompatible(results[x].category)) { %> + <%= __('emulator.play') %> <% } else { %> <% } %> @@ -137,7 +134,9 @@
  • class="page-link previous" aria-controls="results" aria-disabled="true" aria-label="Previous" data-dt-idx="previous" tabindex="-1">‹
  • 1
  • <%- pageNum >= 5 ? ellipsesElem : '' %> - <% for(let x = pageRange.lower; x <= pageRange.upper; x++){ %> + <% for(let x = pageRange.lower; x <= pageRange.upper; x++){ + if(x == pageCount) break; + %>
  • <%= x %>
  • <% } %> <%- pageNum <= pageCount - 5 ? ellipsesElem : '' %> @@ -152,31 +151,28 @@ \ No newline at end of file diff --git a/views/pages/settings.ejs b/views/pages/settings.ejs index 24aa745..17959cf 100644 --- a/views/pages/settings.ejs +++ b/views/pages/settings.ejs @@ -48,11 +48,15 @@ +
    + diff --git a/views/partials/carousel.ejs b/views/partials/carousel.ejs new file mode 100644 index 0000000..1f34143 --- /dev/null +++ b/views/partials/carousel.ejs @@ -0,0 +1,45 @@ +<% + if(typeof videos === 'undefined'){ + let videos = [] + } + if(typeof images === 'undefined'){ + let images = [] + } +%> + + \ No newline at end of file diff --git a/views/partials/result.ejs b/views/partials/result.ejs index 77a5677..db7ae44 100644 --- a/views/partials/result.ejs +++ b/views/partials/result.ejs @@ -9,21 +9,31 @@

    <%= metadata.title || file.filename %>

    -

    Released: <%= metadata.releasedate || file.date %> - Region: <%= file.region %> - Platform: <%= file.category %> +

    <%= __('search.released') %>: <%= metadata.releasedate || file.date %> + <%= __('search.region') %>: <%= file.region %> + <%= __('search.platform') %>: <%= file.category %> <% if(metadata.genre){ %> Genres: <%= JSON.parse(metadata.genre).join(' / ') %> <% } %>

    -

    <%= metadata.description || "No description was found."//todo: localize %>

    + <% if(metadata.title) {%> +

    <%= metadata.description || __('search.no_description') %>

    + <% } else { %> +

    <%= __('search.no_metadata') %>

    + <% } %> <% if(metadata.title) {%>

    Filename: <%= file.filename %>

    <% } %>

    Release Group: <%= file.group %>

    - More Info - Download - Play In Browser + More Info + Download + <% if (process.env.EMULATOR_ENABLED === 'true') { %> + <% if (isEmulatorCompatible(file.category)) { %> + <%= __('emulator.play')%> <% } else { %> + + <% } + }%> +

    \ No newline at end of file diff --git a/views/public/css/info.css b/views/public/css/info.css new file mode 100644 index 0000000..1cc0c99 --- /dev/null +++ b/views/public/css/info.css @@ -0,0 +1,46 @@ +.info { + font-weight: bold; + color: #f0a400; +} +.stars { + color: #f0a400; +} + +.embed-responsive iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.embed-responsive { + position: relative; + overflow: hidden; + width: 100%; + +} +.slideshow-container { + position: relative; + width: 100%; + margin: auto; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} +.container { + + background-color: none; + margin-bottom: 50px; + margin-top: 10px; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.coverart{ + object-fit: contain; + height: auto; + width: 240px; +} \ No newline at end of file diff --git a/views/public/css/resultsold.css b/views/public/css/resultsold.css new file mode 100644 index 0000000..592a9dd --- /dev/null +++ b/views/public/css/resultsold.css @@ -0,0 +1,42 @@ +.dt-orderable { + position: absolute; + + +} +.dt-column-order { + position: absolute; + width: 12px; + color: #ffffff20; + line-height: 9px; + font-size: 0.8em; + right: 12px; + top: 0; + bottom: 0; +} +.table thead > tr > th { + position: relative; + padding-right: 30px; +} +thead > tr > th.dt-orderable span.dt-column-order::before { + position: absolute; + display: block; + bottom: 50%; + content: "▲"; + content: "▲"/""; +} +thead > tr > th.dt-orderable span.dt-column-order::after { + position: absolute; + display: block; + top: 50%; + content: "▼"; + content: "▼"/""; +} + +thead > tr > th.dt-orderable span.dt-column-order.order-asc::before{ + opacity: 1; + color: #ffffffff; +} +thead > tr > th.dt-orderable span.dt-column-order.order-desc::after{ + opacity: 1; + color: #ffffffff; +} diff --git a/views/public/css/style.css b/views/public/css/style.css index 81167f4..136009f 100644 --- a/views/public/css/style.css +++ b/views/public/css/style.css @@ -144,43 +144,3 @@ td a { position: relative; z-index: 9999 !important; } -.dt-orderable { - position: absolute; - - -} -.dt-column-order { - position: absolute; - width: 12px; - opacity: 0.125; - line-height: 9px; - font-size: 0.8em; - right: 12px; - top: 0; - bottom: 0; -} -.table thead > tr > th { - position: relative; - padding-right: 30px; -} -thead > tr > th.dt-orderable span.dt-column-order::before { - position: absolute; - display: block; - bottom: 50%; - content: "▲"; - content: "▲"/""; -} -thead > tr > th.dt-orderable span.dt-column-order::after { - position: absolute; - display: block; - top: 50%; - content: "▼"; - content: "▼"/""; -} - -thead > tr > th.dt-orderable span.dt-column-order.order-desc::before{ - opacity: 1; -} -thead > tr > th.dt-orderable span.dt-column-order.order-asc::after{ - opacity: 1; -} diff --git a/views/public/js/settings.js b/views/public/js/settings.js index d1514d7..61ca2b8 100644 --- a/views/public/js/settings.js +++ b/views/public/js/settings.js @@ -32,10 +32,12 @@ if(typeof settings.fuzzy == 'undefined') {settings.fuzzy = defaults.fuzzy} if(typeof settings.prefix == 'undefined') {settings.prefix = defaults.prefix} if(typeof settings.hideNonGame == 'undefined') {settings.hideNonGame = defaults.hideNonGame} + if(typeof settings.hideNonGame == 'undefined') {settings.useOldResults = defaults.useOldResults} document.getElementById('combineWith').checked = settings.combineWith ? true : false document.getElementById('fuzzy').value = settings.fuzzy document.getElementById('prefix').checked = settings.prefix document.getElementById('hideNonGame').checked = settings.hideNonGame + document.getElementById('useOldResults').checked = settings.useOldResults } function saveSettings(){ @@ -50,6 +52,7 @@ settings.fuzzy = parseFloat (document.getElementById('fuzzy').value) settings.prefix = document.getElementById('prefix').checked settings.hideNonGame = document.getElementById('hideNonGame').checked + settings.useOldResults = document.getElementById('useOldResults').checked localStorage.setItem('settings', JSON.stringify(settings)) window.location.href = '/' } diff --git a/views/public/js/video.js b/views/public/js/video.js new file mode 100644 index 0000000..9b5371a --- /dev/null +++ b/views/public/js/video.js @@ -0,0 +1,32 @@ +// index.js +const videos = []; +const tag = document.createElement("script"); +const firstScriptTag = document.getElementsByTagName("script")[0]; + +tag.src = "https://www.youtube.com/iframe_api"; +firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + +// YouTube wants this function, don't rename it +function onYouTubeIframeAPIReady() { + const slides = Array.from(document.querySelectorAll(".carousel-item")); + slides.forEach((slide, index) => { + // does this slide have a video? + const video = slide.querySelector(".video-player"); + if (video && video.dataset) { + const player = createPlayer({ + id: video.id, + videoId: video.dataset.videoId, + }); + videos.push({ player, index }); + } + }); +} + +function createPlayer(playerInfo) { + return new YT.Player(playerInfo.id, { + videoId: playerInfo.videoId, + playerVars: { + showinfo: 0, + }, + }); +} \ No newline at end of file