MZ - Ongoing Match Results

Userscript to easily fetch results for ongoing matches in a league.

当前为 2024-12-10 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MZ - Ongoing Match Results
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Userscript to easily fetch results for ongoing matches in a league.
// @author       You
// @match        https://www.managerzone.com/?p=league&type=*
// @match        https://www.managerzone.com/?p=friendlyseries&sub=standings&fsid=*
// @grant        GM_xmlhttpRequest
// @connect      www.managerzone.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        #nprogress {
            pointer-events: none;
        }
        #nprogress .bar {
            background: #ff6600;
            position: fixed;
            z-index: 1031;
            top: 0;
            left: 0;
            width: 100%;
            height: 3px;
        }
        #nprogress .peg {
            display: block;
            position: absolute;
            right: 0px;
            width: 100px;
            height: 100%;
            box-shadow: 0 0 10px #ff6600, 0 0 5px #ff6600;
            opacity: 1.0;
            transform: rotate(3deg) translate(0px, -4px);
        }
        .status-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1000;
            display: flex;
            justify-content: center;
            align-items: center;
            color: white;
            font-size: 24px;
            font-weight: bold;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
        }
        .mz-modal {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            z-index: 1100;
            max-width: 300px;
            max-height: 400px;
            overflow-y: auto;
            transition: opacity 0.3s ease, transform 0.3s ease;
            opacity: 0;
            transform: translateY(20px);
        }
        .mz-modal.show {
            opacity: 1;
            transform: translateY(0);
        }
        .mz-modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        }
        .mz-modal-close {
            cursor: pointer;
            padding: 5px 10px;
            background: #ff6600;
            color: white;
            border: none;
            border-radius: 4px;
        }
        .mz-modal-content {
            margin-bottom: 15px;
            font-size: 14px;
        }
        .mz-match-result {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px;
            border-bottom: 1px solid #eee;
        }
        .mz-match-result:last-child {
            border-bottom: none;
        }
    `;
    document.head.appendChild(style);

    const UI = {
        BUTTON_STYLES: {
            backgroundColor: 'navy',
            color: 'orange',
            border: '2px solid lightgray',
            marginLeft: '10px',
            cursor: 'pointer'
        },
        BUTTON_STATES: {
            READY: 'Get match results',
            FETCHING: 'Fetching matches...'
        },
        LOADING_MESSAGES: {
            MATCHES: 'Fetching match data...',
            RESULTS: 'Processing results...',
            UPDATING: 'Updating standings...'
        }
    };

    const SELECTORS = {
        TRACK_BUTTONS: [
            '[id^="trackButton_u18_world_series_"]',
            '[id^="trackButton_u18_series_"]',
            '[id^="trackButton_friendlyseries_"]',
            '#trackButton_u18_world_series_3'
        ],
        MATCHES_TABLE: 'table.hitlist',
        STANDINGS_TABLE: 'table.nice_table'
    };

    class MatchTracker {
        constructor() {
            this.matchResults = new Map();
            this.isFriendlySeries = window.location.href.includes('friendlyseries');
            this.init();
        }

        async init() {
            const matches = this.isFriendlySeries ?
                await this.getFriendlySeriesMatches() :
                this.getLeagueMatches();

            if (!matches || !matches.length) {
                console.log('No ongoing matches found');
                return;
            }

            this.setupUI(matches);
        }

        setupUI(matches) {
            const trackButton = this.findTrackButton();
            if (!trackButton) {
                console.error('Track button not found');
                return;
            }

            const fetchButton = this.createFetchButton();
            trackButton.parentNode.insertBefore(fetchButton, trackButton.nextSibling);
            fetchButton.addEventListener('click', () => this.handleFetchClick(fetchButton, matches));
        }

        findTrackButton() {
            return SELECTORS.TRACK_BUTTONS.reduce((found, selector) =>
                found || document.querySelector(selector), null);
        }

        createFetchButton() {
            const button = document.createElement('button');
            Object.assign(button.style, UI.BUTTON_STYLES);
            button.textContent = UI.BUTTON_STATES.READY;
            return button;
        }

        showLoadingOverlay(message) {
            let overlay = document.querySelector('.status-overlay');
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'status-overlay';
                document.body.appendChild(overlay);
            }
            overlay.textContent = message;
        }

        hideLoadingOverlay() {
            const overlay = document.querySelector('.status-overlay');
            if (overlay) {
                overlay.remove();
            }
        }

        showResultsModal(results) {
            const modal = document.createElement('div');
            modal.className = 'mz-modal';

            const header = document.createElement('div');
            header.className = 'mz-modal-header';
            header.innerHTML = `
                <h3>Match Results</h3>
                <button class="mz-modal-close">Close</button>
            `;

            const content = document.createElement('div');
            content.className = 'mz-modal-content';

            results.forEach(result => {
                const matchDiv = document.createElement('div');
                matchDiv.className = 'mz-match-result';
                matchDiv.textContent = `${result.homeTeam} ${result.score} ${result.awayTeam}`;
                content.appendChild(matchDiv);
            });

            modal.appendChild(header);
            modal.appendChild(content);
            document.body.appendChild(modal);

            setTimeout(() => modal.classList.add('show'), 10);

            modal.querySelector('.mz-modal-close').addEventListener('click', () => {
                modal.classList.remove('show');
                setTimeout(() => modal.remove(), 300);
            });
        }

        async handleFetchClick(button, matches) {
            NProgress.configure({ showSpinner: false });
            NProgress.start();
            this.showLoadingOverlay(UI.LOADING_MESSAGES.MATCHES);

            if (!matches.length) {
                NProgress.done();
                this.hideLoadingOverlay();
                return;
            }

            button.disabled = true;
            button.textContent = UI.BUTTON_STATES.FETCHING;

            this.showLoadingOverlay(UI.LOADING_MESSAGES.RESULTS);
            const results = await this.processMatches(matches);

            if (this.isFriendlySeries) {
                this.showResultsModal(results);
            }

            this.showLoadingOverlay(UI.LOADING_MESSAGES.UPDATING);
            this.updateAllTeamStats();
            this.finalizeUpdate(button);

            NProgress.done();
            this.hideLoadingOverlay();
        }

        getLeagueMatches() {
            const matchesTable = document.querySelector(SELECTORS.MATCHES_TABLE);
            if (!matchesTable) return [];

            return Array.from(matchesTable.querySelectorAll('tr'))
                .filter(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    if (!link) return false;
                    const score = link.textContent.trim();
                    return !score.match(/^\d+\s*-\s*\d+$/) && !score.match(/^X\s*-\s*X$/);
                })
                .map(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    const homeTeam = row.querySelector('td:first-child').textContent.trim();
                    const awayTeam = row.querySelector('td:last-child').textContent.trim();
                    const params = new URLSearchParams(link.href);
                    return {
                        mid: params.get('mid'),
                        homeTeam,
                        awayTeam
                    };
                });
        }

        async getFriendlySeriesMatches() {
            const fsidMatch = window.location.href.match(/fsid=(\d+)/);
            if (!fsidMatch) return [];

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://www.managerzone.com/ajax.php?p=friendlySeries&sub=matches&fsid=${fsidMatch[1]}&sport=soccer`,
                    onload: response => {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, "text/html");
                        const matchRows = Array.from(doc.querySelectorAll('table.hitlist tr'));

                        const inProgressMatches = matchRows
                            .filter(row => {
                                const link = row.querySelector('a[href*="mid="]');
                                if (!link) return false;
                                const score = link.textContent.trim();
                                return !score.match(/^\d+\s*-\s*\d+$/) && !score.match(/^X\s*-\s*X$/);
                            })
                            .map(row => {
                                const link = row.querySelector('a[href*="mid="]');
                                const homeTeam = row.querySelector('td:first-child').textContent.trim();
                                const awayTeam = row.querySelector('td:last-child').textContent.trim();
                                const params = new URLSearchParams(link.href);
                                return {
                                    mid: params.get('mid'),
                                    homeTeam,
                                    awayTeam
                                };
                            });

                        resolve(inProgressMatches);
                    },
                    onerror: reject
                });
            });
        }

        async processMatches(matches) {
            const results = [];
            const total = matches.length;

            for (let i = 0; i < matches.length; i++) {
                const match = matches[i];
                const result = await new Promise((resolve) => {
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: `https://www.managerzone.com/xml/match_info.php?sport_id=1&match_id=${match.mid}`,
                        onload: response => {
                            try {
                                const matchData = this.parseMatchResponse(response);
                                if (matchData) {
                                    this.matchResults.set(match.mid, matchData);
                                    this.updateMatchDisplay(match.mid, matchData);
                                    resolve({
                                        ...match,
                                        score: `${matchData.homeGoals}-${matchData.awayGoals}`
                                    });
                                }
                            } catch (error) {
                                console.error(`Error processing match ${match.mid}:`, error);
                                resolve(null);
                            }
                        },
                        onerror: () => resolve(null),
                        ontimeout: () => resolve(null)
                    });
                });

                if (result) {
                    results.push(result);
                }
                NProgress.set((i + 1) / total);
            }

            return results;
        }

        parseMatchResponse(response) {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(response.responseText, "application/xml");
            const matchNode = xmlDoc.querySelector('Match');
            if (!matchNode) return null;

            const homeTeam = matchNode.querySelector('Team[field="home"]');
            const awayTeam = matchNode.querySelector('Team[field="away"]');
            if (!homeTeam || !awayTeam) return null;

            return {
                homeTid: homeTeam.getAttribute('id'),
                awayTid: awayTeam.getAttribute('id'),
                homeGoals: parseInt(homeTeam.getAttribute('goals'), 10) || 0,
                awayGoals: parseInt(awayTeam.getAttribute('goals'), 10) || 0
            };
        }

        updateMatchDisplay(mid, matchData) {
            const link = Array.from(document.links)
                .find(link => link.href.includes(`mid=${mid}`));
            if (link) {
                link.textContent = `${matchData.homeGoals}-${matchData.awayGoals}`;
            }
        }

        calculateMatchResult(matchData) {
            if (matchData.homeGoals > matchData.awayGoals) {
                return {
                    home: { points: 3, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 0, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            } else if (matchData.homeGoals < matchData.awayGoals) {
                return {
                    home: { points: 0, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 3, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            } else {
                return {
                    home: { points: 1, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 1, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            }
        }

        findTeamRows(tid) {
            const teamLinks = Array.from(document.querySelectorAll(`a[href*="tid=${tid}"]`));
            const rowsSet = new Set();

            teamLinks.forEach(link => {
                const row = link.closest('tr');
                if (row) rowsSet.add(row);
            });

            const highlightedRows = Array.from(document.querySelectorAll('.highlight_row'))
                .filter(row => row.querySelector(`a[href*="tid=${tid}"]`));
            highlightedRows.forEach(row => rowsSet.add(row));

            return Array.from(rowsSet);
        }

        updateAllTeamStats() {
            this.matchResults.forEach((matchData) => {
                const result = this.calculateMatchResult(matchData);
                this.updateTeamRow(matchData.homeTid, result.home);
                this.updateTeamRow(matchData.awayTid, result.away);
            });
        }

        updateTeamRow(tid, result) {
            const teamRows = this.findTeamRows(tid);
            if (!teamRows.length) return;

            teamRows.forEach(row => {
                const cells = row.querySelectorAll('td');
                if (cells.length < 10) return;

                const parseCell = cell => parseInt(cell.textContent, 10) || 0;

                cells[2].textContent = parseCell(cells[2]) + 1;

                if (result.points === 3) {
                    cells[3].textContent = parseCell(cells[3]) + 1;
                } else if (result.points === 1) {
                    cells[4].textContent = parseCell(cells[4]) + 1;
                } else {
                    cells[5].textContent = parseCell(cells[5]) + 1;
                }

                cells[6].textContent = parseCell(cells[6]) + result.goalsFor;
                cells[7].textContent = parseCell(cells[7]) + result.goalsAgainst;

                const goalDiff = parseCell(cells[6]) - parseCell(cells[7]);
                const goalDiffElem = cells[8].querySelector('nobr');
                if (goalDiffElem) {
                    goalDiffElem.textContent = goalDiff;
                }

                cells[9].textContent = parseCell(cells[9]) + result.points;
            });
        }

        sortTableByPoints() {
            const table = document.querySelector(SELECTORS.STANDINGS_TABLE);
            if (!table) return;

            const tbody = table.querySelector('tbody');
            if (!tbody) return;

            const headerRows = Array.from(tbody.querySelectorAll('tr.seriesHeader'));
            const dataRows = Array.from(tbody.querySelectorAll('tr')).filter(row =>
                !row.classList.contains('seriesHeader'));

            dataRows.forEach(row => {
                row.classList.remove('highlight_row');
                row.style.borderBottom = '';
                row.className = '';
            });

            dataRows.sort((a, b) => {
                const parseCell = (row, idx) => parseInt(row.querySelectorAll('td')[idx].textContent, 10) || 0;
                const parseGoalDiff = (row) => parseInt(row.querySelectorAll('td')[8].querySelector('nobr')?.textContent || '0', 10);

                const pointsA = parseCell(a, 9);
                const pointsB = parseCell(b, 9);
                if (pointsB !== pointsA) return pointsB - pointsA;

                const goalDiffA = parseGoalDiff(a);
                const goalDiffB = parseGoalDiff(b);
                if (goalDiffB !== goalDiffA) return goalDiffB - goalDiffA;

                return parseCell(b, 6) - parseCell(a, 6);
            });

            tbody.innerHTML = '';
            headerRows.forEach(row => tbody.appendChild(row));

            dataRows.forEach((row, index) => {
                const positionCell = row.querySelector('td:first-child span');
                if (positionCell) {
                    positionCell.textContent = (index + 1).toString();
                }

                row.className = index % 2 === 0 ? '' : 'highlight_row';
                if (index === 0) {
                    row.style.borderBottom = '2px solid green';
                }

                tbody.appendChild(row);
            });

            table.style.display = 'none';
            table.offsetHeight;
            table.style.display = '';
        }

        finalizeUpdate(button) {
            this.sortTableByPoints();
            button.disabled = false;
            button.textContent = UI.BUTTON_STATES.READY;
        }
    }

    setTimeout(() => new MatchTracker(), 3333);
})();