Greasy Fork

MZ - Ongoing Match Results

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

目前为 2024-12-10 提交的版本。查看 最新版本

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