// ==UserScript==
// @name How Long To Beat To Playnite
// @namespace http://vers.works/
// @version 1.4
// @icon https://styles.redditmedia.com/t5_3koqm/styles/communityIcon_xc2zfag6beo81.png
// @description Download/Copy HLTB Information for the HLTB Playnite Addon. Improved navigation support.
// @author VERS
// @match https://howlongtobeat.com/*
// @grant none
// @license MIT
// ==/UserScript==
// 1.4 - QUICK FIX A MISTAKE IN THE CODE.
// 1.3 - FIXES MORE ISSUES, BUTTONS ARE NOW ADDED PROPERLY (NO NEED TO REFRESH) MADE BETTER UI - MORE IMPROVEMENTS TO COME.
// 1.2 - FIXES ISSUES WHERE TIMES WERE NOT EXTRACTED PROPERLY.
(function() {
'use strict';
function addCopyButton() {
const statsListContainer = document.querySelector('.GameHeader_profile_details__oQTrK');
if (statsListContainer) {
const statsList = statsListContainer.querySelector('ul');
if (statsList.querySelector('.list-item')) return;
const copyButtonLi = document.createElement('li');
copyButtonLi.classList.add('list-item');
copyButtonLi.style.backgroundColor = '#2b7ab9';
copyButtonLi.style.color = '#ffffff';
copyButtonLi.style.padding = '4px';
copyButtonLi.style.borderRadius = '5px';
copyButtonLi.style.cursor = 'pointer';
copyButtonLi.style.textAlign = 'center';
copyButtonLi.style.fontWeight = 'bold';
const copyButtonText = document.createTextNode('COPY INFO');
copyButtonLi.appendChild(copyButtonText);
statsList.appendChild(copyButtonLi);
copyButtonLi.addEventListener('click', handleCopyButtonClick);
}
}
function extractTime(category) {
const liElement = [...document.querySelectorAll('li.GameStats_short__tSJ6I.time_00, li.GameStats_short__tSJ6I.time_10, li.GameStats_short__tSJ6I.time_20, li.GameStats_short__tSJ6I.time_30, li.GameStats_short__tSJ6I.time_40, li.GameStats_short__tSJ6I.time_50, li.GameStats_short__tSJ6I.time_60, li.GameStats_short__tSJ6I.time_70, li.GameStats_short__tSJ6I.time_80, li.GameStats_short__tSJ6I.time_90, li.GameStats_short__tSJ6I.time_100, li.GameStats_short__tSJ6I.time_110, li.GameStats_short__tSJ6I.time_120, li.GameStats_short__tSJ6I.time_130, li.GameStats_short__tSJ6I.time_140, li.GameStats_short__tSJ6I.time_150, li.GameStats_short__tSJ6I.time_160, li.GameStats_short__tSJ6I.time_170, li.GameStats_short__tSJ6I.time_180, li.GameStats_short__tSJ6I.time_190, li.GameStats_short__tSJ6I.time_200, li.GameStats_short__tSJ6I.time_210, li.GameStats_short__tSJ6I.time_220, li.GameStats_short__tSJ6I.time_230, li.GameStats_short__tSJ6I.time_240, li.GameStats_short__tSJ6I.time_250, li.GameStats_short__tSJ6I.time_260, li.GameStats_short__tSJ6I.time_270, li.GameStats_short__tSJ6I.time_280, li.GameStats_short__tSJ6I.time_290, li.GameStats_short__tSJ6I.time_300, li.GameStats_short__tSJ6I.time_310, li.GameStats_short__tSJ6I.time_320, li.GameStats_short__tSJ6I.time_330, li.GameStats_short__tSJ6I.time_340, li.GameStats_short__tSJ6I.time_350, li.GameStats_short__tSJ6I.time_360, li.GameStats_short__tSJ6I.time_370, li.GameStats_short__tSJ6I.time_380, li.GameStats_short__tSJ6I.time_390, li.GameStats_short__tSJ6I.time_400, li.GameStats_short__tSJ6I.time_410, li.GameStats_short__tSJ6I.time_420, li.GameStats_short__tSJ6I.time_430, li.GameStats_short__tSJ6I.time_440, li.GameStats_short__tSJ6I.time_450, li.GameStats_short__tSJ6I.time_460, li.GameStats_short__tSJ6I.time_470, li.GameStats_short__tSJ6I.time_480, li.GameStats_short__tSJ6I.time_490, li.GameStats_short__tSJ6I.time_500')].find(item =>
item.querySelector('h4')?.textContent.trim().includes(category)
);
const timeString = liElement ? liElement.querySelector('h5')?.textContent.trim() : null;
if (timeString) {
return timeString.replace('½', '.5');
}
return null;
}
function handleCopyButtonClick() {
const gameNameElement = document.querySelector('.GameHeader_profile_header__q_PID.shadow_text');
const gameName = gameNameElement ? gameNameElement.textContent.trim() : "Unknown Game";
const gameId = window.location.pathname.split('/').pop();
const gameImageElement = document.querySelector('.GameSideBar_game_image__ozUTt.mobile_hide img');
const imageUrl = gameImageElement ? gameImageElement.src.split('?')[0] : "";
const platformElement = document.querySelector('.GameSummary_profile_info__HZFQu.GameSummary_medium___r_ia');
const platformsText = platformElement ? platformElement.textContent.trim().replace('Platforms:', '').trim() : "";
const platforms = platformsText ? platformsText : "";
const convertToSeconds = (timeString) => {
let hours = 0;
if (timeString) {
timeString = timeString.replace(' Hours', '').trim();
timeString = timeString.replace('½', '.5');
const match = timeString.match(/(\d+(\.\d+)?)/);
if (match) {
hours = parseFloat(match[0]);
}
}
return Math.round(hours * 3600);
};
const mainStoryTime = extractTime("Main Story");
const mainExtraTime = extractTime("Main + Sides");
const completionistTime = extractTime("Completionist");
const mainStoryClassic = convertToSeconds(mainStoryTime);
const mainExtraClassic = convertToSeconds(mainExtraTime);
const completionistClassic = convertToSeconds(completionistTime);
const currentDate = new Date();
const formattedDate = currentDate.toISOString();
const optionsModal = document.createElement('div');
optionsModal.style.position = 'fixed';
optionsModal.style.top = '50%';
optionsModal.style.left = '50%';
optionsModal.style.transform = 'translate(-50%, -50%)';
optionsModal.style.backgroundColor = '#242424';
optionsModal.style.padding = '20px';
optionsModal.style.borderRadius = '5px';
optionsModal.style.zIndex = '9999';
optionsModal.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
optionsModal.style.display = 'flex';
optionsModal.style.flexDirection = 'column';
optionsModal.style.gap = '10px';
optionsModal.style.width = '300px';
optionsModal.innerHTML = `
<label for="playniteId">DOWNLOAD HLTB DATA</label>
<input type="text" id="playniteId" placeholder="Enter Playnite Database ID" style="padding: 5px; margin-bottom: 10px;">
<div style="display: flex; gap: 10px;">
<button id="copyToClipboard" style="flex: 1;">Copy to Clipboard</button>
<button id="downloadJsonFile" style="flex: 1;">Download JSON File</button>
</div>
`;
document.body.appendChild(optionsModal);
const playniteIdInput = document.getElementById('playniteId');
const copyButton = document.getElementById('copyToClipboard');
const downloadButton = document.getElementById('downloadJsonFile');
const processJson = () => {
const userId = playniteIdInput.value.trim();
if (!userId) {
alert('ENTER DATABASE ID');
return null;
}
const jsonData = {
"Items": [
{
"Name": gameName,
"Id": gameId,
"UrlImg": imageUrl,
"Url": window.location.href,
"Platform": platforms,
"GameType": 0,
"GameHltbData": {
"GameType": 0,
"MainStoryClassic": mainStoryClassic,
"MainStoryMedian": 0,
"MainStoryAverage": 0,
"MainStoryRushed": 0,
"MainStoryLeisure": 0,
"MainExtraClassic": mainExtraClassic,
"MainExtraMedian": 0,
"MainExtraAverage": 0,
"MainExtraRushed": 0,
"MainExtraLeisure": 0,
"CompletionistClassic": completionistClassic,
"CompletionistMedian": 0,
"CompletionistAverage": 0,
"CompletionistRushed": 0,
"CompletionistLeisure": 0,
"SoloClassic": 0,
"SoloMedian": 0,
"SoloAverage": 0,
"SoloRushed": 0,
"SoloLeisure": 0,
"CoOpClassic": 0,
"CoOpMedian": 0,
"CoOpAverage": 0,
"CoOpRushed": 0,
"CoOpLeisure": 0,
"VsClassic": 0,
"VsMedian": 0,
"VsAverage": 0,
"VsRushed": 0,
"VsLeisure": 0
},
"IsVndb": false
}
],
"DateLastRefresh": formattedDate,
"Id": userId,
"Name": gameName
};
return jsonData;
};
copyButton.addEventListener('click', function() {
const jsonData = processJson();
if (jsonData) {
const jsonString = JSON.stringify(jsonData, null, 4);
navigator.clipboard.writeText(jsonString)
.then(() => {
document.body.removeChild(optionsModal);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
}
});
downloadButton.addEventListener('click', function() {
const jsonData = processJson();
if (jsonData) {
const jsonString = JSON.stringify(jsonData, null, 4);
const blob = new Blob([jsonString], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${jsonData.Id}.json`;
link.click();
document.body.removeChild(optionsModal);
}
});
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
if (window.location.pathname.includes('/game/') &&
document.querySelector('.GameHeader_profile_details__oQTrK') &&
!document.querySelector('.list-item')) {
addCopyButton();
break;
}
}
}
});
window.addEventListener('load', function() {
addCopyButton();
observer.observe(document.body, {
childList: true,
subtree: true
});
});
})();