How Long To Beat To Playnite

Download/Copy HLTB Information for the HLTB Playnite Addon. Improved navigation support.

// ==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
        });
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址