Greasy Fork

Greasy Fork is available in English.

Popmundo Fixed Itinerary Booker

Sets the tour's starting point from the itinerary and prevents booking the same club in the same week. Includes selector fixes, a simplified settings form, a stop button, and complete storage cleanup. Now with dynamic start/end dates.

当前为 2025-09-03 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Popmundo Fixed Itinerary Booker
// @namespace    http://tampermonkey.net/
// @version      4.9
// @description  Sets the tour's starting point from the itinerary and prevents booking the same club in the same week. Includes selector fixes, a simplified settings form, a stop button, and complete storage cleanup. Now with dynamic start/end dates.
// @author       Gemini
// @match        https://*.popmundo.com/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- ⚙️ DYNAMIC DATE CONFIGURATION ---
    const getFormattedDate = (date) => {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
    };

    const today = new Date();
    const sevenDaysFromNow = new Date();
    sevenDaysFromNow.setDate(today.getDate() + 7);

    // --- ⚙️ DEFAULT CONFIGURATION ---
    const DEFAULTS = {
        INITIAL_CITY: "são paulo",
        SHOW_TIMES: ["14:00:00", "22:00:00"],
        SHOWS_PER_CITY: 1,
        BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE: true,
        REQUIRE_5_STARS: true,
        TARGET_CLUB_RANGE: { min: 80, max: 1500 },
        INITIAL_DATE: getFormattedDate(today),
        FINAL_DATE: getFormattedDate(sevenDaysFromNow),
        ARTIST_ID: "xxxxxxxx", // replace if artist ID
    };

    const BOOK_SHOW_PATH = `/World/Popmundo.aspx/Artist/BookShow/${DEFAULTS.ARTIST_ID}`;

    const TOUR_ITINERARY = [
        { city: "rio de janeiro", travelHours: 3 }, { city: "são paulo", travelHours: 3 },
        { city: "buenos aires", travelHours: 6 },   { city: "são paulo", travelHours: 6 },
        { city: "mexico city", travelHours: 12 },  { city: "los angeles", travelHours: 6 },
        { city: "seattle", travelHours: 8 },       { city: "chicago", travelHours: 8 },
        { city: "nashville", travelHours: 2 },     { city: "chicago", travelHours: 2 },
        { city: "toronto", travelHours: 3 },       { city: "montreal", travelHours: 6 },
        { city: "new york", travelHours: 6 },      { city: "london", travelHours: 18 },
        { city: "brussels", travelHours: 2 },      { city: "paris", travelHours: 3 },
        { city: "barcelona", travelHours: 6 },     { city: "madrid", travelHours: 3 },
        { city: "porto", travelHours: 3 },         { city: "madrid", travelHours: 3 },
        { city: "milan", travelHours: 4 },         { city: "rome", travelHours: 2 },
        { city: "budapest", travelHours: 3 },      { city: "belgrade", travelHours: 2 },
        { city: "dubrovnik", travelHours: 2 },     { city: "sarajevo", travelHours: 2 },
        { city: "belgrade", travelHours: 2 },      { city: "bucharest", travelHours: 3 },
        { city: "sofia", travelHours: 2 },         { city: "istanbul", travelHours: 3 },
        { city: "izmir", travelHours: 2 },         { city: "antalya", travelHours: 2 },
        { city: "ankara", travelHours: 2 },        { city: "baku", travelHours: 2 },
        { city: "kyiv", travelHours: 5 },          { city: "moscow", travelHours: 2 },
        { city: "tallinn", travelHours: 4 },       { city: "stockholm", travelHours: 2 },
        { city: "vilnius", travelHours: 2 },       { city: "warsaw", travelHours: 2 },
        { city: "berlin", travelHours: 3 },        { city: "copenhagen", travelHours: 3 },
        { city: "tromsø", travelHours: 4 },        { city: "copenhagen", travelHours: 4 },
        { city: "tallinn", travelHours: 3 },       { city: "helsinki", travelHours: 2 },
        { city: "tallinn", travelHours: 2 },       { city: "tromsø", travelHours: 3 },
        { city: "berlin", travelHours: 5 },        { city: "glasgow", travelHours: 4 },
        { city: "london", travelHours: 4 },        { city: "amsterdam", travelHours: 5 },
        { city: "istanbul", travelHours: 8 },      { city: "ankara", travelHours: 3 },
        { city: "singapore", travelHours: 16 },    { city: "jakarta", travelHours: 3 },
        { city: "singapore", travelHours: 3 },     { city: "shanghai", travelHours: 6 },
        { city: "manila", travelHours: 4 },        { city: "singapore", travelHours: 7 },
        { city: "melbourne", travelHours: 9 },     { city: "johannesburg", travelHours: 34 },
    ];
    // --- END OF CONFIGURATION ---

    // --- Helper Functions ---
    const SELECTORS = {
        city: '#ctl00_cphLeftColumn_ctl01_ddlCities', day: '#ctl00_cphLeftColumn_ctl01_ddlDays', hour: '#ctl00_cphLeftColumn_ctl01_ddlHours',
        findClubsBtn: '#ctl00_cphLeftColumn_ctl01_btnFindClubs', clubsTable: '#tableclubs', bookShowBtn: '#ctl00_cphLeftColumn_ctl01_btnBookShow',
        dialogButtons: 'body > div:nth-child(4) > div.ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix > div > button:nth-child(1)'
    };
    const delay = ms => new Promise(res => setTimeout(res, ms));
    const formatDate = (date) => date.toISOString().split('T')[0];
    const getWeekStartDate = (dateStr) => {
        const date = new Date(dateStr);
        const day = date.getUTCDay(); // Use UTC to avoid timezone shifts; Sunday - 0, Monday - 1
        const diff = date.getUTCDate() - day + (day === 0 ? -6 : 1); // adjust when day is Sunday
        const monday = new Date(date.setUTCDate(diff));
        return monday.toISOString().split('T')[0];
    };
    const skipToShow = (c, r) => {
        const BOOK_SHOW_URL_FULL = `https://${window.location.hostname}${BOOK_SHOW_PATH}`;
        console.error(`${r}. Skipping.`);
        localStorage.setItem('popmundo_show_index', c + 1);
        window.location.href = BOOK_SHOW_URL_FULL;
    };

    /**
     * Creates and injects a "Stop" button onto the page.
     */
    function addStopButton() {
        if (document.getElementById('pm-stop-script-btn')) return;

        GM_addStyle(`
            #pm-stop-script-btn {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 10001;
                padding: 10px 20px;
                background-color: #d9534f;
                color: white;
                border: none;
                border-radius: 5px;
                cursor: pointer;
                font-size: 14px;
                font-weight: bold;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            }
            #pm-stop-script-btn:hover {
                background-color: #c9302c;
            }
            #pm-stop-script-btn:disabled {
                background-color: #999;
                cursor: not-allowed;
            }
        `);

        const stopButton = document.createElement('button');
        stopButton.id = 'pm-stop-script-btn';
        stopButton.textContent = 'Stop Booker';
        stopButton.onclick = () => {
            sessionStorage.setItem('popmundo_script_stopped', 'true');
            stopButton.textContent = 'Stopping...';
            stopButton.disabled = true;
            console.log('🛑 Script stop requested. It will halt and clean up on the next step/page load.');
            alert('The script will stop and clear its data.');
        };
        document.body.appendChild(stopButton);
    }


    /**
     * Creates and displays a settings form overlay.
     * @param {object} defaults - The default values for the form fields.
     * @returns {Promise<object>} A promise that resolves with the new settings or rejects if cancelled.
     */
    function showSettingsForm(defaults) {
        return new Promise((resolve, reject) => {
            GM_addStyle(`
                #pm-settings-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center; }
                #pm-settings-form { background: #f0f0f0; padding: 25px; border-radius: 8px; font-family: sans-serif; display: grid; grid-template-columns: auto 1fr; gap: 12px; border: 1px solid #ccc; color: #333; }
                #pm-settings-form h2 { grid-column: 1 / -1; margin: 0 0 10px; text-align: center; }
                #pm-settings-form label { font-weight: bold; padding-right: 10px; text-align: right; align-self: center;}
                #pm-settings-form input, #pm-settings-form select { padding: 5px; border-radius: 4px; border: 1px solid #ccc; }
                #pm-settings-form .buttons { grid-column: 1 / -1; text-align: center; margin-top: 15px; }
                #pm-settings-form button { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; margin: 0 10px; color: white; }
                #tm-save-settings { background: #4CAF50; } #tm-cancel-settings { background: #f44336; }
            `);

            const overlay = document.createElement('div');
            overlay.id = 'pm-settings-overlay';

            const form = document.createElement('div');
            form.id = 'pm-settings-form';

            const createInputRow = (labelText, inputId, inputType, value, attributes = {}) => {
                const label = document.createElement('label');
                label.htmlFor = inputId;
                label.textContent = labelText;

                const input = document.createElement('input');
                input.id = inputId;
                input.type = inputType;
                input.value = value;

                for (const [key, val] of Object.entries(attributes)) {
                    input.setAttribute(key, val);
                }
                form.append(label, input);
            };

            form.innerHTML = '<h2>Tour Booker Settings</h2>';

            // --- Create Start City Select Dropdown ---
            const uniqueCities = [...new Set(TOUR_ITINERARY.map(leg => leg.city))].sort((a, b) => a.localeCompare(b));
            const cityLabel = document.createElement('label');
            cityLabel.htmlFor = 'tm-initial-city';
            cityLabel.textContent = 'Start City:';
            const citySelect = document.createElement('select');
            citySelect.id = 'tm-initial-city';
            uniqueCities.forEach(city => {
                const option = document.createElement('option');
                const cleanCity = city.toLowerCase();
                option.value = cleanCity;
                option.textContent = city.charAt(0).toUpperCase() + city.slice(1);
                if (cleanCity === defaults.INITIAL_CITY.toLowerCase()) {
                    option.selected = true;
                }
                citySelect.appendChild(option);
            });
            form.append(cityLabel, citySelect);

            createInputRow('Start Date:', 'tm-initial-date', 'date', defaults.INITIAL_DATE);
            createInputRow('Final Date:', 'tm-final-date', 'date', defaults.FINAL_DATE);

            // --- Create Show Times Multi-Select ---
            const availableShowTimes = ["14:00:00", "16:00:00", "18:00:00", "20:00:00", "22:00:00"];
            const showTimesLabel = document.createElement('label');
            showTimesLabel.htmlFor = 'tm-show-times';
            showTimesLabel.textContent = 'Show Times:';
            showTimesLabel.style.alignSelf = 'start';
            const showTimesSelect = document.createElement('select');
            showTimesSelect.id = 'tm-show-times';
            showTimesSelect.multiple = true;
            showTimesSelect.style.height = '100px';
            availableShowTimes.forEach(time => {
                const option = document.createElement('option');
                option.value = time;
                option.textContent = time;
                if (defaults.SHOW_TIMES.includes(time)) {
                    option.selected = true;
                }
                showTimesSelect.appendChild(option);
            });
            form.append(showTimesLabel, showTimesSelect);


            createInputRow('Shows Per City:', 'tm-shows-per-city', 'number', defaults.SHOWS_PER_CITY, { min: '1', style: 'width: 80px;' });
            createInputRow('Club Price Min:', 'tm-club-min', 'number', defaults.TARGET_CLUB_RANGE.min, { min: '0', style: 'width: 80px;' });
            createInputRow('Club Price Max:', 'tm-club-max', 'number', defaults.TARGET_CLUB_RANGE.max, { min: '0', style: 'width: 80px;' });

            const blockSameDayLabel = document.createElement('label');
            blockSameDayLabel.htmlFor = 'tm-block-same-day';
            blockSameDayLabel.textContent = 'Block Multiple Shows in Same Day:';
            const blockSameDayCheckbox = document.createElement('input');
            blockSameDayCheckbox.id = 'tm-block-same-day';
            blockSameDayCheckbox.type = 'checkbox';
            blockSameDayCheckbox.checked = defaults.BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE;
            blockSameDayCheckbox.style.justifySelf = 'start';
            form.append(blockSameDayLabel, blockSameDayCheckbox);

            const starLabel = document.createElement('label');
            starLabel.htmlFor = 'tm-5star';
            starLabel.textContent = 'Require 5 Stars:';
            const starCheckbox = document.createElement('input');
            starCheckbox.id = 'tm-5star';
            starCheckbox.type = 'checkbox';
            starCheckbox.checked = defaults.REQUIRE_5_STARS;
            starCheckbox.style.justifySelf = 'start';
            form.append(starLabel, starCheckbox);

            const buttonsDiv = document.createElement('div');
            buttonsDiv.className = 'buttons';
            buttonsDiv.innerHTML = '<button id="tm-save-settings">Save and Start</button><button id="tm-cancel-settings">Cancel</button>';
            form.appendChild(buttonsDiv);

            overlay.appendChild(form);
            document.body.appendChild(overlay);

            document.getElementById('tm-save-settings').onclick = () => {
                const selectedShowTimes = Array.from(document.getElementById('tm-show-times').selectedOptions).map(option => option.value);

                const newSettings = {
                    INITIAL_CITY: document.getElementById('tm-initial-city').value,
                    INITIAL_DATE: document.getElementById('tm-initial-date').value,
                    FINAL_DATE: document.getElementById('tm-final-date').value,
                    SHOW_TIMES: selectedShowTimes,
                    SHOWS_PER_CITY: parseInt(document.getElementById('tm-shows-per-city').value, 10),
                    BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE: document.getElementById('tm-block-same-day').checked,
                    TARGET_CLUB_RANGE: {
                        min: parseInt(document.getElementById('tm-club-min').value, 10),
                        max: parseInt(document.getElementById('tm-club-max').value, 10)
                    },
                    REQUIRE_5_STARS: document.getElementById('tm-5star').checked,
                    ARTIST_ID: defaults.ARTIST_ID
                };
                document.body.removeChild(overlay);
                resolve(newSettings);
            };

            document.getElementById('tm-cancel-settings').onclick = () => {
                document.body.removeChild(overlay);
                reject(new Error("User cancelled settings."));
            };
        });
    }

    const handleConfirmationPopup = async () => {
        await delay(1000 + Math.random() * 500);
        for (let i = 0; i < 20; i++) {
            const yesButton = Array.from(document.querySelectorAll(SELECTORS.dialogButtons)).find(b => b.textContent.trim() === 'Yes');
            if (yesButton) {
                console.log('✅ "Yes" button found. Simulating a more realistic click...');
                await delay(800 + Math.random() * 400);
                const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: unsafeWindow });
                const mouseUpEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: unsafeWindow });
                const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: unsafeWindow });
                yesButton.dispatchEvent(mouseDownEvent); await delay(60 + Math.random() * 40); yesButton.dispatchEvent(mouseUpEvent); yesButton.dispatchEvent(clickEvent); return;
            }
            await delay(1000);
        }
        console.error('Confirmation pop-up "Yes" button not found in time.');
    };

    /**
     * ✅ CORRECTED FUNCTION
     * This function generates the full tour itinerary based on the user's settings.
     * The original problem was here: if a show's scheduled time exceeded the FINAL_DATE,
     * it would stop all tour planning immediately.
     *
     * The fix: Instead of halting the entire process (`break tourPlanningLoop`), the code
     * now uses a simple `break`. This stops adding more shows for the *current city*
     * but allows the script to continue planning for the *next city* in the itinerary.
     * This directly addresses the "go to next city" behavior you described.
     * The logic for BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE was already correct in how it
     * forced the next show to a new day; the issue was just the premature termination.
     */
    function generateAndStoreTour(settings) {
        console.log(`🗺️ Planning tour, starting itinerary at ${settings.INITIAL_CITY}...`);
        const tour = [];
        let lastActionTime = new Date(settings.INITIAL_DATE);
        const finalDate = new Date(settings.FINAL_DATE);
        finalDate.setHours(23, 59, 59, 999); // Ensure comparison includes the entire final day

        const findNextShowSlot = (startTime) => {
            let searchTime = new Date(startTime);
            const sortedShowTimes = [...new Set(settings.SHOW_TIMES)].sort((a, b) => a.localeCompare(b));

            while (true) {
                for (const showTime of sortedShowTimes) {
                    const [hour, minute, second] = showTime.split(':');
                    let potentialShowTime = new Date(searchTime);
                    potentialShowTime.setHours(hour, minute, second, 0);

                    if (potentialShowTime > searchTime) {
                        return potentialShowTime;
                    }
                }
                searchTime.setDate(searchTime.getDate() + 1);
                searchTime.setHours(0, 0, 0, 0);
            }
        };

        let startingIndex = TOUR_ITINERARY.findIndex(leg => leg.city.toLowerCase() === settings.INITIAL_CITY.toLowerCase());
        if (startingIndex === -1) {
            console.warn(`Initial city "${settings.INITIAL_CITY}" not found in itinerary. Starting from the beginning of the list.`);
            startingIndex = 0;
        }

        const activeItinerary = TOUR_ITINERARY.slice(startingIndex);

        tourPlanningLoop:
        for (let i = 0; i < activeItinerary.length; i++) {
            const leg = activeItinerary[i];
            let cityArrivalTime = new Date(lastActionTime);

            if (i > 0) {
                cityArrivalTime.setHours(cityArrivalTime.getHours() + leg.travelHours);
            }

            let lastShowTimeInCity = cityArrivalTime;

            for (let j = 0; j < settings.SHOWS_PER_CITY; j++) {
                let searchStartTime;
                // This logic correctly forces the search for the next show to start on the following day if the setting is active.
                if (tour.length > 0) {
                    console.log(leg.city.toLowerCase() ,"==", tour.at(-1).city.toLowerCase())
                    if (j > 0 && settings.BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE && leg.city.toLowerCase() == tour.at(-1).city.toLowerCase()) {
                      break;
                    }
                }

                searchStartTime = new Date(lastShowTimeInCity);

                const nextShowTime = findNextShowSlot(searchStartTime);

                // --- 🛠️ FIX APPLIED HERE ---
                // If the next show is past the final date, stop adding shows for THIS city and move to the next.
                if (nextShowTime > finalDate) {
                    console.log(`🛑 Final date ${settings.FINAL_DATE} reached while planning for ${leg.city}. Moving to the next city.`);
                    break; // This now only breaks the inner loop (for shows per city), not the 'tourPlanningLoop'.
                }

                tour.push({
                    city: leg.city,
                    date: formatDate(nextShowTime),
                    time: nextShowTime.toTimeString().split(' ')[0]
                });
                lastShowTimeInCity = new Date(nextShowTime);
            }
            lastActionTime = new Date(lastShowTimeInCity);
        }

        localStorage.setItem('popmundo_planned_tour', JSON.stringify(tour));
        console.log("✅ Tour planned and saved!", tour);
        return tour;
    }

    async function runAutoBooker() {
        if (sessionStorage.getItem('popmundo_script_stopped') === 'true') {
            console.log('Script execution halted by user. Cleaning up all tour data.');
            const stopBtn = document.getElementById('pm-stop-script-btn');
            if (stopBtn) stopBtn.remove();
            sessionStorage.removeItem('popmundo_script_stopped');
            sessionStorage.removeItem('popmundo_session_settings');
            sessionStorage.removeItem('popmundo_restore_selections');
            localStorage.removeItem('popmundo_show_index');
            localStorage.removeItem('popmundo_planned_tour');
            localStorage.removeItem('popmundo_booked_clubs');
            localStorage.removeItem('popmundo_booking_redirect');
            console.log('✅ All script data cleared.');
            return;
        }

        const BOOK_SHOW_URL_FULL = `https://${window.location.hostname}${BOOK_SHOW_PATH}`;
        if (localStorage.getItem('popmundo_booking_redirect')) { localStorage.removeItem('popmundo_booking_redirect'); window.location.href = BOOK_SHOW_URL_FULL; }
        if (!window.location.href.includes(BOOK_SHOW_PATH)) return;

        addStopButton();

        const restoreSelections = sessionStorage.getItem('popmundo_restore_selections');
        if (restoreSelections) {
            try {
                const { city, date, time } = JSON.parse(restoreSelections);
                console.log(`🔄 Restoring selections after page reload: ${city}, ${date}, ${time}`);
                const cityDropdown = document.querySelector(SELECTORS.city);
                if (cityDropdown) {
                    const cityOption = Array.from(cityDropdown.options).find(opt =>
                        opt.text.toLowerCase().localeCompare(city, undefined, { sensitivity: 'accent' }) === 0
                    );
                    if (cityOption) cityDropdown.value = cityOption.value;
                }
                const dayDropdown = document.querySelector(SELECTORS.day);
                if (dayDropdown) dayDropdown.value = date;
                const hourDropdown = document.querySelector(SELECTORS.hour);
                if (hourDropdown) hourDropdown.value = time;
                sessionStorage.removeItem('popmundo_restore_selections');
                await delay(1000);
                document.querySelector(SELECTORS.findClubsBtn).click();
                return;
            } catch (e) {
                console.error("Error restoring selections:", e);
                sessionStorage.removeItem('popmundo_restore_selections');
            }
        }

        let sessionSettings;
        const savedSettings = sessionStorage.getItem('popmundo_session_settings');
        if (savedSettings && savedSettings !== 'undefined' && savedSettings !== 'null') { try { sessionSettings = JSON.parse(savedSettings); } catch (e) { console.error("Could not parse saved settings, clearing them.", e); sessionStorage.removeItem('popmundo_session_settings'); } }
        if (!sessionSettings) { try { sessionSettings = await showSettingsForm(DEFAULTS); sessionStorage.setItem('popmundo_session_settings', JSON.stringify(sessionSettings)); console.log("Settings confirmed and saved for this session.", sessionSettings); } catch (error) { console.log(error.message); return; } }
        let tourJson = localStorage.getItem('popmundo_planned_tour');
        if (!tourJson) { tourJson = JSON.stringify(generateAndStoreTour(sessionSettings)); }
        const TOUR_LIST = JSON.parse(tourJson);
        let currentIndex = parseInt(localStorage.getItem('popmundo_show_index') || '0', 10);

        let bookedClubs = JSON.parse(localStorage.getItem('popmundo_booked_clubs') || '{}');

        if (currentIndex >= TOUR_LIST.length) {
            console.log('🎉 Tour finished! Cleaning up all tour data.');
            alert('Tour finished!');
            const stopBtn = document.getElementById('pm-stop-script-btn');
            if (stopBtn) stopBtn.remove();
            sessionStorage.removeItem('popmundo_script_stopped');
            sessionStorage.removeItem('popmundo_session_settings');
            sessionStorage.removeItem('popmundo_restore_selections');
            localStorage.removeItem('popmundo_show_index');
            localStorage.removeItem('popmundo_planned_tour');
            localStorage.removeItem('popmundo_booked_clubs');
            localStorage.removeItem('popmundo_booking_redirect');
            console.log('✅ All script data cleared.');
            return;
        }

        const currentShow = TOUR_LIST[currentIndex];
        console.log(`▶️ Processing show ${currentIndex + 1}/${TOUR_LIST.length}: ${currentShow.city} on ${currentShow.date} at ${currentShow.time}`);
        const cityDropdown = document.querySelector(SELECTORS.city);
        if (!cityDropdown) return;
        const clubsTable = document.querySelector(SELECTORS.clubsTable);
        const selectedCityText = cityDropdown.options[cityDropdown.selectedIndex].text.toLowerCase();

        if (selectedCityText.localeCompare(currentShow.city, undefined, { sensitivity: 'accent' }) !== 0) {
            const cityOption = Array.from(cityDropdown.options).find(opt =>
                opt.text.toLowerCase().localeCompare(currentShow.city, undefined, { sensitivity: 'accent' }) === 0
            );
            if (cityOption) {
                sessionStorage.setItem('popmundo_restore_selections', JSON.stringify({
                    city: currentShow.city, date: currentShow.date, time: currentShow.time
                }));
                cityDropdown.value = cityOption.value;
                cityDropdown.dispatchEvent(new Event('change', { bubbles: true }));
                return;
            } else {
                skipToShow(currentIndex, `City not found: ${currentShow.city}`);
            }
            return;
        }

        if (!clubsTable) {
            document.querySelector(SELECTORS.day).value = currentShow.date;
            document.querySelector(SELECTORS.hour).value = currentShow.time;
            await delay(300);
            document.querySelector(SELECTORS.findClubsBtn).click();
            return;
        }

        const rows = clubsTable.querySelectorAll('tbody tr');
        const validClubs = [];
        const currentShowWeekStart = getWeekStartDate(currentShow.date);

        rows.forEach(row => {
            const ratingCell = row.cells[2];
            let starRequirementMet = !sessionSettings.REQUIRE_5_STARS;
            if (sessionSettings.REQUIRE_5_STARS && ratingCell) { const sortKeySpan = ratingCell.querySelector('span.sortkey'); if (sortKeySpan && parseInt(sortKeySpan.textContent, 10) === 50) { starRequirementMet = true; } }
            const priceCell = row.cells[row.cells.length - 1];
            let priceRequirementMet = false;
            let price = 0;
            if (priceCell) { price = parseFloat(priceCell.textContent.trim().replace(/\s*M\$$/, '').replace(/\./g, '').replace(',', '.')); if (price >= sessionSettings.TARGET_CLUB_RANGE.min && price <= sessionSettings.TARGET_CLUB_RANGE.max) { priceRequirementMet = true; } }
            const clubName = row.cells[0].textContent.trim();
            const lastBookingDate = bookedClubs[clubName];
            let isAlreadyBookedThisWeek = false;
            if (lastBookingDate) {
                const lastBookingWeekStart = getWeekStartDate(lastBookingDate);
                if (lastBookingWeekStart === currentShowWeekStart) {
                    isAlreadyBookedThisWeek = true;
                    console.log(`Club "${clubName}" was already booked in the week of ${currentShowWeekStart}. Skipping.`);
                }
            }
            if (starRequirementMet && priceRequirementMet && !isAlreadyBookedThisWeek) {
                validClubs.push({ price: price, rowElement: row });
            }
        });

        let targetRow = null;
        if (validClubs.length > 0) {
            validClubs.sort((a, b) => b.price - a.price);
            const bestClub = validClubs[0];
            console.log(`Found ${validClubs.length} valid clubs. Selecting best one: ${bestClub.rowElement.cells[0].textContent.trim()} (Price: ${bestClub.price})`);
            targetRow = bestClub.rowElement;
        }

        if (targetRow) {
            const selectedClubName = targetRow.cells[0].textContent.trim();
            bookedClubs[selectedClubName] = currentShow.date;
            localStorage.setItem('popmundo_booked_clubs', JSON.stringify(bookedClubs));
            targetRow.querySelector('input[type="radio"]').click();
            await delay(400 + Math.random() * 200);
            localStorage.setItem('popmundo_show_index', currentIndex + 1);
            localStorage.setItem('popmundo_booking_redirect', 'true');
            document.querySelector(SELECTORS.bookShowBtn).click();
            await handleConfirmationPopup();
        } else {
            let reason = `No available club found`;
            if (sessionSettings.REQUIRE_5_STARS) reason += ` with 5 stars`;
            reason += ` in range [${sessionSettings.TARGET_CLUB_RANGE.min}-${sessionSettings.TARGET_CLUB_RANGE.max}] in ${currentShow.city}`;
            skipToShow(currentIndex, reason);
        }
    }

    runAutoBooker();
})();