MZ - Ongoing Match Results

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

目前為 2024-12-10 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();