NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @namespace https://openuserjs.org/users/SB100
// @name GGn IGDb Game Recommendations
// @description Show game recommendations from IGDb on Game pages
// @updateURL https://openuserjs.org/meta/SB100/GGn_IGDb_Game_Recommendations.meta.js
// @version 1.0.1
// @author SB100
// @copyright 2021, SB100 (https://openuserjs.org/users/SB100)
// @license MIT
// @include https://gazellegames.net/torrents.php?id=*
// @grant GM_xmlhttpRequest
// @connect www.igdb.com
// ==/UserScript==
// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==
/* jshint esversion: 6 */
const URL_IGDB_BASE = 'https://www.igdb.com';
const URL_IGDB_SEARCH = 'https://www.igdb.com/search?type=1&q=';
const URL_GGN_SEARCH = 'https://gazellegames.net/torrents.php?order_by=relevance&order_way=desc&artistname=Games&searchstr=';
/**
* Turn a HTML string into a HTML element so that we can run querySelector calls against it
*/
function htmlToElement(html) {
var template = document.createElement('template');
html = html.trim();
template.innerHTML = html;
return template.content;
}
function query(url) {
let resolver;
let rejecter;
const p = new Promise((resolveFn, rejectFn) => {
resolver = resolveFn;
rejecter = rejectFn;
});
GM_xmlhttpRequest({
method: 'get',
url: url,
timeout: 5000,
onloadstart: function () {},
onload: result => resolver(result),
onerror: result => rejecter(result),
ontimeout: result => rejecter(result),
});
return p;
}
function searchIGDb(gameName, year) {
const url = `${URL_IGDB_SEARCH}${encodeURIComponent(gameName)}`;
return query(url).then(result => {
if (result.status !== 200) {
return;
}
return htmlToElement(result.response);
}).then(doc => {
const jsonElem = doc.querySelector('#results_json');
const json = jsonElem && JSON.parse(jsonElem.dataset.json);
if (!json || json.length === 0) {
return [];
}
return json.filter(item => item.type === 'game').map(item => ({
name: item.data.name,
nameFormatted: item.data.name.toLowerCase().replace(/[^\w\s]/g, ''),
url: item.data.url,
year: item.data.release_year,
}));
});
}
function findRecommendationsFromIGDbPage(bestMatch) {
const url = `${URL_IGDB_BASE}${bestMatch.url}`;
return query(url).then(result => {
if (result.status !== 200) {
return;
}
return htmlToElement(result.response);
}).then(doc => {
const recElems = Array.from(doc.querySelectorAll('.col-md-9 > .game-list-inline .game-list-inline-item a'));
return recElems.map(a => ({
image: a.querySelector('img').src,
name: a.querySelector('span').innerText
}));
});
}
function getGameNameAndYearFromGGn() {
const elem = document.getElementById('display_name').cloneNode(true);
const groupPlatform = elem.querySelector('#groupplatform');
elem.removeChild(groupPlatform);
const gameMatch = elem.innerText.match(/\-\s([^\(]+)/);
const gameName = gameMatch && gameMatch[1].trim().toLowerCase().replace(/[^\w\s]/g, '');
const yearMatch = elem.innerText.match(/\(([\d]+)\)/);
const year = yearMatch && yearMatch[1].length === 4 && parseInt(yearMatch[1], 10);
return {
gameName,
year
}
}
function findBestMatch(json, gameName, year) {
// match both name and year
for (let i = 0, len = json.length; i < len; i += 1) {
if (json[i].nameFormatted === gameName && json[i].year === year) {
return json[i];
}
}
// just match name
for (let i = 0, len = json.length; i < len; i += 1) {
if (json[i].nameFormatted === gameName) {
return json[i];
}
}
return null;
}
function createRecommendationsBox(recommendations) {
const builtRecs = recommendations.map(rec => {
return `<li class='recommendation__item' title='${rec.name}'>
<a href='${URL_GGN_SEARCH}${encodeURIComponent(rec.name)}'>
<img src='${rec.image}' alt='${rec.name} Cover' />
<div class='recommendation__name'>${rec.name}</div>
</a>
</li>`
}).join('')
const div = document.createElement('div');
div.classList.add('box');
div.innerHTML = `<div class='head'>Recommendations from IGDb</div>
<div class='body similar'>
<ul class='nobullet recommendation__cont'>
${builtRecs}
</ul>
</div>
`;
const allBoxes = Array.from(document.querySelectorAll('.main_column > .box'));
const lastBox = allBoxes[allBoxes.length - 1];
// append after
lastBox.parentNode.insertBefore(div, lastBox.nextSibling);
}
function createStyleTag() {
const css = `.recommendation__cont {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.recommendation__cont::after {
content: "";
flex: auto;
flex-basis: 130px;
flex-grow: 0
}
.recommendation__item a {
display: inline-block;
position: relative;
}
.recommendation__name {
display: none;
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 7px;
background: rgba(0,0,0,0.8);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.recommendation__item:hover .recommendation__name {
display: block;
}`;
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
(async function () {
'use strict';
const {
gameName,
year
} = getGameNameAndYearFromGGn();
const results = await searchIGDb(gameName);
if (!results || results.length === 0) {
console.log('[GGn IGDb Game Recommendations] No IGDb results');
return;
}
const bestMatch = findBestMatch(results, gameName, year);
if (!bestMatch) {
console.log('[GGn IGDb Game Recommendations] No game match found');
return;
}
const recommendations = await findRecommendationsFromIGDbPage(bestMatch);
if (!recommendations) {
console.log('[GGn IGDb Game Recommendations] No recommendations found');
return;
}
createStyleTag();
createRecommendationsBox(recommendations);
})();