MZ - WLMatches

Detects potentially weird match results in world leagues

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MZ - WLMatches
// @namespace    douglaskampl
// @version      3.16
// @description  Detects potentially weird match results in world leagues
// @author       Douglas
// @match        https://www.managerzone.com/?p=match&sub=livescores_overview
// @icon         https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @resource     weirdWLMatchesStyles https://mzdv.me/mz/userscript/other/worldLeagueMatches.css
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('weirdWLMatchesStyles'));

    const improvedSpinnerStyles = `
        .mz-loader {
            box-sizing: border-box;
            font-size: 0;
            width: 40px;
            height: 40px;
            position: fixed;
            top: calc(50% - 20px);
            left: calc(50% - 20px);
            z-index: 10001;
            border-radius: 50%;
            border: 4px solid rgba(150, 150, 150, 0.3);
            border-top-color: #555555;
            animation: mz-loader-spin 0.8s linear infinite;
        }

        @keyframes mz-loader-spin {
            to {
                transform: rotate(360deg);
            }
        }
    `;
    GM_addStyle(improvedSpinnerStyles);

    const CONSTANTS = {
        LEAGUE_TYPES: ['senior', 'u18_world', 'u21_world', 'u23_world'],
        LEAGUE_DISPLAY_NAMES: {
            'senior': 'Senior',
            'u18_world': 'U18',
            'u21_world': 'U21',
            'u23_world': 'U23'
        },
        LEAGUE_LIMITS: {
            div1: 1,
            div2: 13,
            div3: 40,
            div4: 121,
            div5: 291
        },
        ROUND_PAIRS: [
            { first: '11', second: '12', display: '11/12' },
            { first: '10', second: '13', display: '10/13' },
            { first: '9', second: '14', display: '9/14' },
            { first: '8', second: '15', display: '8/15' },
            { first: '7', second: '16', display: '7/16' },
            { first: '6', second: '17', display: '6/17' },
            { first: '5', second: '18', display: '5/18' },
            { first: '4', second: '19', display: '4/19' },
            { first: '3', second: '20', display: '3/20' },
            { first: '2', second: '21', display: '2/21' },
            { first: '1', second: '22', display: '1/22' }
        ],
        ERROR_MESSAGES: {
            FETCH_ERROR: 'Error fetching data from league',
            NO_DISCREPANCIES: 'No result inconsistencies found in this league.',
            INVALID_INPUT: 'Please enter a number between 1 and 10.'
        },
        DOM: {
            NAV_UL_SELECTOR: '#leftnav-wrapper ul.leftnav',
            BUTTON_CLASS: 'wl-check-button',
            BUTTON_TEXT: 'CHECK RESULTS',
            BUTTON_TITLE: 'Find inconsistent results between home and away matches'
        },
        REGEX: {
            SCORE: /<a[^>]*>([^<]+)<\/a>/,
            LINK: /<a href="([^"]+)"/,
            UNPLAYED_SCORE: /X\s*-\s*X/
        }
    };

    function getDivisionName(sid) {
        if (sid === CONSTANTS.LEAGUE_LIMITS.div1) return "Top Series";
        if (sid >= 2 && sid <= 4) return `1.${sid - 1}`;
        if (sid >= 5 && sid <= CONSTANTS.LEAGUE_LIMITS.div2) return `2.${sid - 4}`;
        if (sid >= 14 && sid <= CONSTANTS.LEAGUE_LIMITS.div3) return `3.${sid - 13}`;
        return `4.${sid - 40}`;
    }

    async function fetchLeagueData(leagueType, worldLeagueId) {
        let finalLeagueType = leagueType === 'senior' ? 'world' : leagueType;
        const url = `/ajax.php?p=league&type=${finalLeagueType}&sid=${worldLeagueId}&tid=1&sport=soccer&sub=schedule`;
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
            return await response.text();
        } catch (error) {
            console.error(`${CONSTANTS.ERROR_MESSAGES.FETCH_ERROR} ${worldLeagueId}:`, error.message);
            return null;
        }
    }

    function extractMatchesFromHTML(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const roundHeaders = doc.querySelectorAll('h2.subheader');
        const matches = [];

        roundHeaders.forEach((header, index) => {
            const roundNumber = index + 1;
            const nextElement = header.nextElementSibling;

            if (nextElement && nextElement.classList.contains('mainContent')) {
                const matchRows = nextElement.querySelectorAll('tr');

                matchRows.forEach(row => {
                    const teams = row.querySelectorAll('td');
                    const scoreLink = row.querySelector('a');

                    if (teams.length >= 3 && scoreLink) {
                        const match = {
                            teams: [
                                teams[0].textContent.trim(),
                                teams[2].textContent.trim()
                            ],
                            score: scoreLink.textContent.trim(),
                            link: scoreLink.getAttribute('href'),
                            round: roundNumber.toString()
                        };
                        matches.push(match);
                    }
                });
            }
        });

        return matches;
    }

    function checkDiscrepancy(homeScore, awayScore) {
        const homeDiff = homeScore[0] - homeScore[1];
        const awayDiff = awayScore[1] - awayScore[0];

        const getWinner = diff => diff > 0 ? 0 : diff < 0 ? 1 : 2;
        const homeWinner = getWinner(homeDiff);
        const awayWinner = getWinner(awayDiff);

        return {
            totalDiff: Math.abs(homeDiff) + Math.abs(awayDiff),
            isDiscrepancy: homeWinner !== 2 && awayWinner !== 2 && homeWinner !== awayWinner
        };
    }

    async function processLeague(leagueType, worldLeagueId, minTotalDiff, roundNumbers) {
        const html = await fetchLeagueData(leagueType, worldLeagueId);
        if (!html) return { discrepancies: [] };

        const matches = extractMatchesFromHTML(html);
        const homeResults = new Map();
        const awayResults = new Map();
        const discrepancies = [];

        matches.forEach(match => {
            if (CONSTANTS.REGEX.UNPLAYED_SCORE.test(match.score)) return;

            const matchKey = `${match.teams[0]} vs ${match.teams[1]}`;

            if (match.round === roundNumbers.first) {
                homeResults.set(matchKey, match);
            } else if (match.round === roundNumbers.second) {
                awayResults.set(matchKey, match);
            }
        });

        for (const [key, awayMatch] of awayResults) {
            const homeKey = `${awayMatch.teams[1]} vs ${awayMatch.teams[0]}`;
            const homeMatch = homeResults.get(homeKey);

            if (!homeMatch) continue;

            const homeScore = homeMatch.score.split(" - ").map(Number);
            const awayScore = awayMatch.score.split(" - ").map(Number);

            const { totalDiff, isDiscrepancy } = checkDiscrepancy(homeScore, awayScore);

            if (totalDiff >= minTotalDiff && isDiscrepancy) {
                discrepancies.push({
                    division: getDivisionName(worldLeagueId),
                    divisionId: worldLeagueId,
                    homeMatch: { ...homeMatch },
                    awayMatch: { ...awayMatch }
                });
            }
        }

        return { discrepancies };
    }

    function createModal(discrepancies, leagueType, minDiff, roundNumbers) {
        const modalHtml = `
            <div class="mz-modal-overlay mz-modern">
                <div class="mz-modal mz-modern">
                    <div class="mz-modal-header mz-modern">
                        <h2 class="mz-modal-title">
                            ${CONSTANTS.LEAGUE_DISPLAY_NAMES[leagueType]} Inconsistent Results [R${roundNumbers.first}/${roundNumbers.second} | Min Diff: ${minDiff}]
                        </h2>
                        <button class="mz-modal-close">×</button>
                    </div>
                    <div class="mz-modal-content mz-modern">
                        ${discrepancies.map(d => `
                            <div class="mz-discrepancy mz-modern">
                                <div class="mz-discrepancy-header mz-modern">
                                    DIVISION: <a href="/?p=league&type=placeholder&sid=${d.divisionId}" class="mz-div-link">${d.division}</a>
                                </div>
                                <div>
                                    R${d.homeMatch.round}: <a href="${d.homeMatch.link}" class="mz-match-link" target="_blank">
                                        ${d.homeMatch.teams[0]} vs ${d.homeMatch.teams[1]} (${d.homeMatch.score})
                                    </a>
                                </div>
                                <div>
                                    R${d.awayMatch.round}: <a href="${d.awayMatch.link}" class="mz-match-link" target="_blank">
                                        ${d.awayMatch.teams[0]} vs ${d.awayMatch.teams[1]} (${d.awayMatch.score})
                                    </a>
                                </div>
                            </div>
                        `).join('')}
                    </div>
                </div>
            </div>
        `;

        const modalElement = document.createElement('div');
        modalElement.innerHTML = modalHtml;
        document.body.appendChild(modalElement);

        const links = modalElement.querySelectorAll('.mz-div-link');
        for (const a of links) {
            const href = a.getAttribute('href');
            const replaced = href.replace('placeholder', encodeURIComponent(leagueType));
            a.setAttribute('href', replaced);
        }

        const closeButton = modalElement.querySelector('.mz-modal-close');
        const overlay = modalElement.querySelector('.mz-modal-overlay');
        const closeModal = () => modalElement.remove();

        closeButton.addEventListener('click', closeModal);
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) closeModal();
        });
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') closeModal();
        }, { once: true });
    }

    function createSpinner() {
        const spinner = document.createElement('div');
        spinner.className = 'mz-loader';
        spinner.style.display = 'none';
        document.body.appendChild(spinner);
        return spinner;
    }

    function promptForParams() {
        const modalHtml = `
            <div class="mz-modal-overlay mz-modern">
                <div class="input-modal mz-modern" onclick="event.stopPropagation()">
                    <h3>SELECT LEAGUE TYPE</h3>
                    <select id="leagueSelect">
                        ${CONSTANTS.LEAGUE_TYPES.map(l => `
                            <option value="${l}">${CONSTANTS.LEAGUE_DISPLAY_NAMES[l]}</option>
                        `).join('')}
                    </select>
                    <h3>SELECT ROUNDS TO COMPARE</h3>
                    <select id="roundSelect">
                        ${CONSTANTS.ROUND_PAIRS.map((pair, index) => `
                            <option value="${index}">${pair.display}</option>
                        `).join('')}
                    </select>
                    <h3>MINIMUM GOAL DIFFERENCE</h3>
                    <input type="text" id="diffInput" placeholder="1-10">
                    <div>
                        <button class="confirm">SEARCH</button>
                        <button class="cancel">CANCEL</button>
                    </div>
                </div>
            </div>
        `;

        return new Promise((resolve, reject) => {
            const modalElement = document.createElement('div');
            modalElement.innerHTML = modalHtml;
            document.body.appendChild(modalElement);

            const leagueSelect = modalElement.querySelector('#leagueSelect');
            const roundSelect = modalElement.querySelector('#roundSelect');
            const input = modalElement.querySelector('#diffInput');
            const confirmBtn = modalElement.querySelector('.confirm');
            const cancelBtn = modalElement.querySelector('.cancel');
            const overlay = modalElement.querySelector('.mz-modal-overlay');

            const cleanup = () => modalElement.remove();

            [leagueSelect, roundSelect, input].forEach(el => {
                el.addEventListener('click', e => e.stopPropagation());
                el.addEventListener('keydown', e => { if (e.key === 'Enter') e.preventDefault(); });
            });

            const handleConfirm = () => {
                const val = parseInt(input.value);
                if (val >= 1 && val <= 10) {
                    const selectedPair = CONSTANTS.ROUND_PAIRS[parseInt(roundSelect.value)];
                    cleanup();
                    resolve({
                        leagueType: leagueSelect.value,
                        diff: val,
                        roundNumbers: {
                            first: selectedPair.first,
                            second: selectedPair.second
                        }
                    });
                } else {
                    alert(CONSTANTS.ERROR_MESSAGES.INVALID_INPUT);
                }
            };

            input.addEventListener('keydown', e => {
                if (e.key === 'Enter') handleConfirm();
            });

            confirmBtn.addEventListener('click', handleConfirm);
            cancelBtn.addEventListener('click', () => {
                cleanup();
                reject('Cancelled');
            });

            overlay.addEventListener('click', (e) => {
                if (e.target === overlay) {
                    cleanup();
                    reject('Cancelled');
                }
            });

            setTimeout(() => leagueSelect.focus(), 0);
        });
    }

    let selectedLeagueType;

    async function init() {
        const navList = document.querySelector(CONSTANTS.DOM.NAV_UL_SELECTOR);
        if (!navList) return;

        const buttonContainer = document.createElement('li');
        const button = document.createElement('button');
        button.className = CONSTANTS.DOM.BUTTON_CLASS;
        button.innerHTML = CONSTANTS.DOM.BUTTON_TEXT;
        button.title = CONSTANTS.DOM.BUTTON_TITLE;

        buttonContainer.appendChild(button);
        navList.appendChild(buttonContainer);

        const spinner = createSpinner();

        button.addEventListener('click', async () => {
            try {
                const params = await promptForParams();
                selectedLeagueType = params.leagueType;
                const minTotalDiff = params.diff;
                const roundNumbers = params.roundNumbers;
                const allDiscrepancies = [];

                spinner.style.display = 'block';
                button.style.cursor = 'wait';
                button.disabled = true;

                for (let worldLeagueId = CONSTANTS.LEAGUE_LIMITS.div1; worldLeagueId <= CONSTANTS.LEAGUE_LIMITS.div2; worldLeagueId++) {
                    const result = await processLeague(selectedLeagueType, worldLeagueId, minTotalDiff, roundNumbers);
                    allDiscrepancies.push(...result.discrepancies);
                }

                spinner.style.display = 'none';
                if (allDiscrepancies.length) {
                    createModal(allDiscrepancies, selectedLeagueType, minTotalDiff, roundNumbers);
                } else {
                    alert(CONSTANTS.ERROR_MESSAGES.NO_DISCREPANCIES);
                }
            } catch (error) {
                spinner.style.display = 'none';
                if (error !== 'Cancelled') {
                    console.error('Error:', error);
                }
            } finally {
                button.style.cursor = 'pointer';
                button.disabled = false;
            }
        });
    }

    init();
})();