Greasy Fork

MZ - Ongoing Match Results and Standings Update

Tracks ongoing matches results and updates standings

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

// ==UserScript==
// @name         MZ - Ongoing Match Results and Standings Update
// @namespace    douglaskampl
// @version      2.8
// @description  Tracks ongoing matches results and updates standings
// @author       Douglas
// @match        https://www.managerzone.com/?p=league*
// @match        https://www.managerzone.com/?p=friendlyseries*
// @match        https://www.managerzone.com/?p=private_cup*
// @match        https://www.managerzone.com/?p=cup*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js
// @license      MIT
// ==/UserScript==

GM_addStyle(`
    #nprogress {
        pointer-events: none;
    }

    #nprogress .bar {
        background: linear-gradient(to right, #ff6600, #ff9966);
        position: fixed;
        z-index: 1031;
        top: 0;
        left: 0;
        width: 100%;
        height: 2px;
        box-shadow: 0 1px 3px rgba(255, 102, 0, 0.2);
    }

    .status-message {
        position: fixed;
        top: 10px;
        right: 10px;
        background: rgba(0, 0, 0, 0.8);
        color: #ff6600;
        padding: 8px 15px;
        border-radius: 4px;
        font-size: 14px;
        z-index: 1000;
        animation: fadeInOut 0.3s ease;
    }

    .mz-fetch-button {
        background-color: #1a1a1a;
        color: #ff6600;
        border: 1px solid #ff6600;
        margin-left: 10px;
        cursor: pointer;
        padding: 5px 10px;
        border-radius: 4px;
        transition: all 0.2s ease;
    }

    .mz-fetch-button:hover {
        background-color: #333 !important;
    }

    .mz-fetch-button.disabled {
        opacity: 0.7 !important;
        cursor: not-allowed !important;
        background-color: #333 !important;
        border-color: #666 !important;
        color: #999 !important;
    }

    .mz-fetch-button.done {
        background-color: #333 !important;
        border-color: #666 !important;
        color: #999 !important;
        cursor: default !important;
    }

    .mz-modal {
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: rgba(0, 0, 0, 0.9);
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 4px 15px rgba(255, 102, 0, 0.2);
        z-index: 1100;
        max-width: 300px;
        max-height: 400px;
        overflow-y: auto;
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        opacity: 0;
        transform: translateY(20px);
        color: #ff6600;
        border: 1px solid #ff6600;
    }

    .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 #ff6600;
    }

    .mz-modal-close {
        cursor: pointer;
        padding: 5px 10px;
        background: #ff6600;
        color: black;
        border: none;
        border-radius: 4px;
        transition: all 0.2s ease;
    }

    .mz-modal-close:hover {
        background: #ff9966;
    }

    .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 rgba(255, 102, 0, 0.3);
    }

    .mz-match-result:last-child {
        border-bottom: none;
    }

    .mz-table-row-champion {
        border-bottom: 2px solid green !important;
    }

    .mz-table-row-promotion {
        border-bottom: 2px dashed #556B2F !important;
    }

    .mz-table-row-relegation {
        border-bottom: 2px solid red !important;
    }

    @keyframes fadeInOut {
        0% { opacity: 0; transform: translateY(-10px); }
        100% { opacity: 1; transform: translateY(0); }
    }
`);

(function () {
    'use strict';

    const UI = {
        BUTTON_STATES: {
            READY: 'Get match results',
            FETCHING: 'Processing…',
            DONE: 'Results updated'
        },
        LOADING_MESSAGES: {
            MATCHES: 'Fetching matches…',
            RESULTS: 'Processing results…',
            UPDATING: 'Updating standings…'
        }
    };

    const SELECTORS = {
        TRACK_BUTTONS: [
            '[id^="trackButton_series_"]',
            '[id^="trackButton_u18_series_"]',
            '[id^="trackButton_u21_series_"]',
            '[id^="trackButton_u23_series_"]',
            '[id^="trackButton_world_series_"]',
            '[id^="trackButton_u18_world_series_"]',
            '[id^="trackButton_u21_world_series_"]',
            '[id^="trackButton_u23_world_series_"]',
            '[id^="trackButton_friendlyseries_"]',
            '[id^="trackButton_cup_"]',
            '[id^="trackButton_privatecup_"]',
        ],
        MATCHES_TABLE: 'table.hitlist',
        STANDINGS_TABLE: 'table.nice_table'
    };

    class MatchTracker {
        constructor() {
            this.matchResults = new Map();
            this.isFriendlySeries = window.location.href.includes('friendlyseries');
            this.isPrivateCup = window.location.href.includes('private_cup');
            this.isCup = window.location.href.includes('p=cup');
            this.hasRun = false;
            this.init();
        }

        showStatusMessage(message) {
            const existingMessage = document.querySelector('.status-message');
            if (existingMessage) {
                existingMessage.remove();
            }

            const messageElement = document.createElement('div');
            messageElement.className = 'status-message';
            messageElement.textContent = message;
            document.body.appendChild(messageElement);

            setTimeout(() => {
                messageElement.style.opacity = '0';
                setTimeout(() => messageElement.remove(), 300);
            }, 2000);
        }

        async init() {
            const matches = await this.getMatches();
            if (!matches || !matches.length) {
                console.log('No ongoing matches found');
                return;
            }
            this.setupUI(matches);
        }

        async getMatches() {
            if (this.isFriendlySeries) {
                return await this.getFriendlySeriesMatches();
            } else if (this.isPrivateCup || this.isCup) {
                return this.getCupMatches();
            }
            return this.getLeagueMatches();
        }

        getCupMatches() {
            const groupStages = document.querySelector('#group-stages');
            if (!groupStages) return [];

            return Array.from(groupStages.querySelectorAll('table.hitlist 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
                    };
                });
        }

        setupUI(matches) {
            const trackButton = this.findTrackButton();
            if (!trackButton) return;

            const fetchButton = this.createFetchButton();
            trackButton.parentNode.insertBefore(fetchButton, trackButton.nextSibling);

            fetchButton.addEventListener('click', () => {
                if (!this.hasRun) {
                    this.handleFetchClick(fetchButton, matches);
                }
            });
        }

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

        createFetchButton() {
            const button = document.createElement('button');
            button.className = 'mz-fetch-button';
            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) {
            if (this.hasRun) return;

            this.hasRun = true;
            NProgress.configure({ showSpinner: false });
            NProgress.start();
            this.showStatusMessage(UI.LOADING_MESSAGES.MATCHES);

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

            button.classList.add('disabled');
            button.textContent = UI.BUTTON_STATES.FETCHING;

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

            if (this.isFriendlySeries || this.isPrivateCup || this.isCup) {
                this.showResultsModal(results);
            }

            this.showStatusMessage(UI.LOADING_MESSAGES.UPDATING);
            this.updateAllTeamStats();

            button.classList.remove('disabled');
            button.classList.add('done');
            button.textContent = UI.BUTTON_STATES.DONE;

            NProgress.done();
            this.showStatusMessage('All updates complete');
        }

        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 [];
            try {
                const response = await fetch(
                    `https://www.managerzone.com/ajax.php?p=friendlySeries&sub=matches&fsid=${fsidMatch[1]}&sport=soccer`
                );
                const text = await response.text();
                const doc = new DOMParser().parseFromString(text, "text/html");
                const now = new Date();
                const ongoingMatches = [];
                const rounds = doc.querySelectorAll('h2.subheader.clearfix');
                rounds.forEach(round => {
                    const headerText = round.textContent;
                    const dateTimeMatch = headerText.match(/(\d{2}\/\d{2}\/\d{4})\s+(\d{1,2}:\d{2}(?:am|pm))/i);
                    if (dateTimeMatch) {
                        const [_, dateStr, timeStr] = dateTimeMatch;
                        const matchTime = this.parseDateTime(dateStr, timeStr);
                        const matchEndTime = new Date(matchTime.getTime() + (2 * 60 * 60 * 1000));
                        const matchesDiv = round.nextElementSibling;
                        if (matchesDiv && matchesDiv.classList.contains('mainContent')) {
                            const matches = matchesDiv.querySelectorAll('tr');
                            matches.forEach(match => {
                                const link = match.querySelector('a[href*="mid="]');
                                if (link) {
                                    const score = link.textContent.trim();
                                    if (!score.match(/^\d+-\d+$/)) {
                                        if (score === 'X - X') {
                                            if (now >= matchTime && now <= matchEndTime) {
                                                const homeTeam = match.querySelector('td:first-child').textContent.trim();
                                                const awayTeam = match.querySelector('td:last-child').textContent.trim();
                                                const params = new URLSearchParams(link.href);
                                                ongoingMatches.push({
                                                    mid: params.get('mid'),
                                                    homeTeam,
                                                    awayTeam
                                                });
                                            }
                                        } else {
                                            const homeTeam = match.querySelector('td:first-child').textContent.trim();
                                            const awayTeam = match.querySelector('td:last-child').textContent.trim();
                                            const params = new URLSearchParams(link.href);
                                            ongoingMatches.push({
                                                mid: params.get('mid'),
                                                homeTeam,
                                                awayTeam
                                            });
                                        }
                                    }
                                }
                            });
                        }
                    }
                });
                return ongoingMatches;
            } catch (error) {
                console.error('Error fetching friendly series matches:', error);
                return [];
            }
        }

        parseDateTime(dateStr, timeStr) {
            const [day, month, year] = dateStr.split('/');
            const date = `${month}/${day}/${year}`;

            let [time, period] = timeStr.toLowerCase().split(/(?=[ap]m)/);
            let [hours, minutes] = time.split(':');
            hours = parseInt(hours);

            if (period === 'pm' && hours !== 12) {
                hours += 12;
            } else if (period === 'am' && hours === 12) {
                hours = 0;
            }

            return new Date(`${date} ${hours}:${minutes}`);
        }

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

            for (let i = 0; i < matches.length; i++) {
                const match = matches[i];
                try {
                    const response = await fetch(
                        `https://www.managerzone.com/xml/match_info.php?sport_id=1&match_id=${match.mid}`
                    );
                    const text = await response.text();
                    const matchData = this.parseMatchResponse({ responseText: text });

                    if (matchData) {
                        this.matchResults.set(match.mid, matchData);
                        this.updateMatchDisplay(match.mid, matchData);
                        results.push({
                            ...match,
                            score: `${matchData.homeGoals}-${matchData.awayGoals}`
                        });
                    }
                } catch (error) {
                    console.error(`Error processing match ${match.mid}:`, error);
                }

                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;
            });

            this.sortTableByPoints();
        }

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

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

            const dataRows = Array.from(tbody.querySelectorAll('tr')).filter(row => {
                const cells = row.querySelectorAll('td');
                return cells.length >= 10 && cells[2] && !isNaN(parseInt(cells[2].textContent));
            });

            dataRows.sort((a, b) => {
                const getCellValue = (row, index) => {
                    const cell = row.querySelectorAll('td')[index];
                    return parseInt(cell.textContent.trim(), 10) || 0;
                };

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

                const goalDiffA = getCellValue(a, 6) - getCellValue(a, 7);
                const goalDiffB = getCellValue(b, 6) - getCellValue(b, 7);
                if (goalDiffB !== goalDiffA) return goalDiffB - goalDiffA;

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

            const nonDataRows = Array.from(tbody.children).filter(row => !dataRows.includes(row));
            const tempContainer = document.createDocumentFragment();

            dataRows.forEach((row, index) => {
                const originalRow = row.cloneNode(true);
                const positionCell = originalRow.querySelector('td:first-child span');
                if (positionCell) {
                    const prevHelpButton = row.querySelector('td:first-child span .help_button');
                    const currentSpan = positionCell.textContent.trim();

                    positionCell.innerHTML = `${index + 1}`;
                    if (prevHelpButton) {
                        positionCell.appendChild(prevHelpButton);
                    }
                }

                originalRow.className = index % 2 === 0 ? '' : 'highlight_row';

                if (index === 0) {
                    originalRow.classList.add('mz-table-row-champion');
                } else if (index === 1) {
                    originalRow.classList.add('mz-table-row-promotion');
                } else if (index === 7) {
                    originalRow.classList.add('mz-table-row-relegation');
                }

                tempContainer.appendChild(originalRow);
            });

            nonDataRows.forEach(row => {
                tempContainer.appendChild(row.cloneNode(true));
            });

            tbody.innerHTML = '';
            tbody.appendChild(tempContainer);
        }
    }

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