Greasy Fork

Greasy Fork is available in English.

Popmundo Itinerary Booker (Improved)

Refactored for readability and maintainability with enhanced logging.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Popmundo Itinerary Booker (Improved)
// @namespace    http://tampermonkey.net/
// @version      9.1
// @description  Refactored for readability and maintainability with enhanced logging.
// @author       Gemini & You
// @match        https://*.popmundo.com/*
// @grant        GM_addStyle
// @grant        unsafeWindow
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ---  UTILITIES ---
    /**
     * Logs messages to the console with a consistent prefix.
     * @param {...any} args - The messages to log.
     */
    const log = (...args) => console.log('[Itinerary Booker]', ...args);

    /**
     * Logs error messages to the console.
     * @param {...any} args - The error messages to log.
     */
    const logError = (...args) => console.error('[Itinerary Booker]', ...args);

    /**
     * Creates a promise that resolves after a specified number of milliseconds.
     * @param {number} ms - The number of milliseconds to wait.
     * @returns {Promise<void>}
     */
    const delay = ms => new Promise(res => setTimeout(res, ms));


    // --- ⚙️ CONFIGURATION ---
    const SCRIPT_CONFIG = {
        storage: {
            status: 'pm_booker_status',
            settings: 'pm_booker_settings',
            tour: 'pm_booker_planned_tour',
            bookedClubs: 'pm_booker_booked_clubs',
            showIndex: 'pm_booker_show_index',
            restore: 'pm_booker_restore_selections'
        },
        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',
            dialogConfirm: 'body > div:nth-child(4) > div.ui-dialog-buttonpane.ui-widget-content.ui-helper-clearfix > div > button:nth-child(1)'
        },
        STATE: {
            RUNNING: 'RUNNING',
            IDLE: 'IDLE'
        }
    };

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

    const DEFAULTS = {
        INITIAL_CITY: "são paulo",
        SHOW_TIMES: ["14:00:00", "22:00:00"],
        SHOWS_PER_CITY: 1,
        SHOWS_PER_DATE: 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: "2786249",
    };

    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 },
    ];

    // --- UI INJECTION ---
    /**
     * Injects the control panel UI into the page.
     */
    function injectUi() {
        if (document.getElementById('pmBookerPanel')) return;

        const referenceElement = document.querySelector('#ppm-content > div:nth-child(6)');
        const fallbackContainer = document.querySelector('#ppm-content');
        if (!referenceElement && !fallbackContainer) return;

        const uniqueCities = [...new Set(TOUR_ITINERARY.map(leg => leg.city))].sort((a, b) => a.localeCompare(b));
        const cityOptions = uniqueCities.map(city => {
            const cleanCity = city.toLowerCase();
            const selected = cleanCity === DEFAULTS.INITIAL_CITY ? 'selected' : '';
            return `<option value="${cleanCity}" ${selected}>${city.charAt(0).toUpperCase() + city.slice(1)}</option>`;
        }).join('');

        const availableShowTimes = ["14:00:00", "16:00:00", "18:00:00", "20:00:00", "22:00:00"];
        const timeOptions = availableShowTimes.map(time => {
            const selected = DEFAULTS.SHOW_TIMES.includes(time) ? 'selected' : '';
            return `<option value="${time}" ${selected}>${time}</option>`;
        }).join('');

        const controlPanel = document.createElement('div');
        controlPanel.innerHTML = `
            <div id="pmBookerPanel" style="padding: 15px; margin-bottom: 20px; border: 2px solid #4CAF50; background-color: #e8f5e9; text-align: center;">
                <h3 style="margin: 0 0 10px 0;">Itinerary Booker</h3>
                <div id="pmBookerForm" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); justify-content: center; align-items: flex-start; gap: 15px 25px;">
                    <span><label>Artist ID:</label><br><input type="text" id="pm_artist_id" value="${DEFAULTS.ARTIST_ID}" style="padding: 5px; width: 100px;"></span>
                    <span><label>Start City:</label><br><select id="pm_initial_city" style="padding: 5px; width: 150px;">${cityOptions}</select></span>
                    <span><label>Shows Per City:</label><br><input type="number" id="pm_shows_per_city" value="${DEFAULTS.SHOWS_PER_CITY}" min="1" style="padding: 5px; width: 80px;"></span>
                    <span><label>Shows Per Date:</label><br><input type="number" id="pm_shows_per_date" value="${DEFAULTS.SHOWS_PER_DATE}" min="1" style="padding: 5px; width: 80px;"></span>
                    <span><label>Start Date:</label><br><input type="date" id="pm_initial_date" value="${DEFAULTS.INITIAL_DATE}" style="padding: 5px;"></span>
                    <span><label>Final Date:</label><br><input type="date" id="pm_final_date" value="${DEFAULTS.FINAL_DATE}" style="padding: 5px;"></span>
                    <span style="grid-column: 1 / -1;"><label>Club Price (Min/Max):</label><br><input type="number" id="pm_club_min" value="${DEFAULTS.TARGET_CLUB_RANGE.min}" min="0" style="width: 60px; padding: 5px;"> <input type="number" id="pm_club_max" value="${DEFAULTS.TARGET_CLUB_RANGE.max}" min="0" style="width: 60px; padding: 5px;"></span>
                    <span style="grid-column: 1 / -1; display:flex; justify-content:center; gap: 20px;">
                        <span><label>Show Times (Ctrl+Click):</label><br><select id="pm_show_times" multiple style="padding: 5px; height: 100px; width: 120px;">${timeOptions}</select></span>
                        <div style="text-align: left;">
                           <input type="checkbox" id="pm_5star" ${DEFAULTS.REQUIRE_5_STARS ? 'checked' : ''}> <label for="pm_5star">Require 5 Stars</label><br>
                           <input type="checkbox" id="pm_block_same_day" ${DEFAULTS.BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE ? 'checked' : ''}> <label for="pm_block_same_day">Block Two Shows in same City on same Day</label>
                        </div>
                    </span>
                </div>
                <div style="margin-top: 15px;">
                    <button id="startBookerBtn" style="padding: 8px 15px; background-color: #4CAF50; color: white; border: none; cursor: pointer; font-size: 14px;">Start Booker</button>
                    <button id="stopBookerBtn" style="padding: 8px 15px; background-color: #f44336; color: white; border: none; cursor: pointer; margin-left: 10px; font-size: 14px;">Stop Booker</button>
                </div>
                <p id="pmBookerStatus" style="margin-top: 10px; font-weight: bold; min-height: 1.2em;">Status: Idle.</p>
            </div>
        `;

        if (referenceElement) {
            referenceElement.parentNode.insertBefore(controlPanel, referenceElement);
        } else {
            fallbackContainer.prepend(controlPanel);
        }

        document.getElementById('startBookerBtn').addEventListener('click', startProcess);
        document.getElementById('stopBookerBtn').addEventListener('click', stopProcess);
    }

    // --- SCRIPT CONTROL ---
    /**
     * Gathers settings from the UI, saves them, and starts the booking process.
     */
    function startProcess() {
        const selectedShowTimes = Array.from(document.querySelectorAll('#pm_show_times option:checked')).map(el => el.value);
        if (!document.getElementById('pm_initial_date').value || !document.getElementById('pm_final_date').value || selectedShowTimes.length === 0) {
            alert('Please select a Start Date, Final Date, and at least one Show Time.');
            return;
        }

        const newSettings = {
            ARTIST_ID: document.getElementById('pm_artist_id').value.trim(),
            INITIAL_CITY: document.getElementById('pm_initial_city').value,
            INITIAL_DATE: document.getElementById('pm_initial_date').value,
            FINAL_DATE: document.getElementById('pm_final_date').value,
            SHOW_TIMES: selectedShowTimes,
            SHOWS_PER_CITY: parseInt(document.getElementById('pm_shows_per_city').value, 10),
            SHOWS_PER_DATE: parseInt(document.getElementById('pm_shows_per_date').value, 10),
            BLOCK_TWO_SHOWS_IN_CITY_AT_SAME_DATE: document.getElementById('pm_block_same_day').checked,
            TARGET_CLUB_RANGE: {
                min: parseInt(document.getElementById('pm_club_min').value, 10),
                max: parseInt(document.getElementById('pm_club_max').value, 10)
            },
            REQUIRE_5_STARS: document.getElementById('pm_5star').checked,
        };

        log('Starting booker with settings:', newSettings);
        sessionStorage.setItem(SCRIPT_CONFIG.storage.status, SCRIPT_CONFIG.STATE.RUNNING);
        sessionStorage.setItem(SCRIPT_CONFIG.storage.settings, JSON.stringify(newSettings));

        document.getElementById('pmBookerForm').style.display = 'none';
        document.getElementById('startBookerBtn').disabled = true;

        processNextShow(newSettings);
    }

    /**
     * Stops the booking process and clears all stored data.
     */
    function stopProcess() {
        log('Stopping booker and clearing all data.');
        sessionStorage.removeItem(SCRIPT_CONFIG.storage.status);
        sessionStorage.removeItem(SCRIPT_CONFIG.storage.settings);
        sessionStorage.removeItem(SCRIPT_CONFIG.storage.restore);
        localStorage.removeItem(SCRIPT_CONFIG.storage.showIndex);
        localStorage.removeItem(SCRIPT_CONFIG.storage.tour);
        localStorage.removeItem(SCRIPT_CONFIG.storage.bookedClubs);
        alert('Process stopped and all data cleared.');
        location.reload();
    }


    // --- CORE LOGIC ---

    /**
     * Generates the full tour itinerary based on user settings.
     * @param {object} settings - The user-defined settings for the tour.
     * @returns {Array<object>} The generated tour itinerary.
     */
    function generateTour(settings) {
        log('No tour found in storage. Generating new tour itinerary...');
        const tour = [];
        let showsPerDateCount = {};
        let lastActionTime = new Date(settings.INITIAL_DATE);
        const finalDate = new Date(settings.FINAL_DATE);
        finalDate.setHours(23, 59, 59, 999);

        const findNextShowSlot = (startTime) => {
            let searchTime = new Date(startTime);
            const sortedShowTimes = [...new Set(settings.SHOW_TIMES)].sort();
            while (true) {
                for (const showTime of sortedShowTimes) {
                    const [h, m, s] = showTime.split(':');
                    let potentialShowTime = new Date(searchTime);
                    potentialShowTime.setHours(h, m, s, 0);
                    if (potentialShowTime > startTime) {
                        const dateStr = potentialShowTime.toISOString().split('T')[0];
                        if ((showsPerDateCount[dateStr] || 0) < settings.SHOWS_PER_DATE) {
                            return potentialShowTime;
                        }
                    }
                }
                searchTime.setDate(searchTime.getDate() + 1);
                searchTime.setHours(0, 0, 0, 0);
                startTime = new Date(searchTime.getTime() - 1);
            }
        };

        let startingIndex = TOUR_ITINERARY.findIndex(l => l.city.toLowerCase() === settings.INITIAL_CITY.toLowerCase());
        if (startingIndex === -1) startingIndex = 0;
        const activeItinerary = TOUR_ITINERARY.slice(startingIndex);

        for (const leg of activeItinerary) {
            if (lastActionTime > finalDate) break;
            let cityArrivalTime = new Date(lastActionTime);
            if (tour.length > 0) cityArrivalTime.setHours(cityArrivalTime.getHours() + leg.travelHours);
            let lastShowTimeInCity = cityArrivalTime;

            for (let j = 0; j < settings.SHOWS_PER_CITY; j++) {
                const nextShowTime = findNextShowSlot(lastShowTimeInCity);
                if (nextShowTime > finalDate) break;

                const nextShowDateStr = nextShowTime.toISOString().split('T')[0];
                tour.push({ city: leg.city, date: nextShowDateStr, time: nextShowTime.toTimeString().split(' ')[0] });
                showsPerDateCount[nextShowDateStr] = (showsPerDateCount[nextShowDateStr] || 0) + 1;
                lastShowTimeInCity = nextShowTime;
            }
            lastActionTime = lastShowTimeInCity;
        }
        localStorage.setItem(SCRIPT_CONFIG.storage.tour, JSON.stringify(tour));
        log(`Tour generation complete. ${tour.length} shows planned.`, tour);
        return tour;
    }

    /**
     * Retrieves the tour from localStorage or generates a new one.
     * @param {object} settings - The user-defined settings.
     * @returns {Array<object>} The tour itinerary.
     */
    function getTour(settings) {
        const tourJson = localStorage.getItem(SCRIPT_CONFIG.storage.tour);
        if (tourJson) {
            log('Loaded existing tour from storage.');
            return JSON.parse(tourJson);
        }
        return generateTour(settings);
    }

    /**
     * Finds the best available club from the list, books it, and confirms.
     * @param {object} settings - The user-defined settings.
     * @param {object} currentShow - The current show object from the tour.
     * @returns {Promise<boolean>} True if a club was successfully booked, false otherwise.
     */
    async function findAndBookBestClub(settings, currentShow) {
        const getWeekStartDate = (dateStr) => {
            const date = new Date(dateStr);
            const day = date.getUTCDay();
            const diff = date.getUTCDate() - day + (day === 0 ? -6 : 1);
            return new Date(date.setUTCDate(diff)).toISOString().split('T')[0];
        };

        const clubsTable = document.querySelector(SCRIPT_CONFIG.selectors.clubsTable);
        const bookedClubs = JSON.parse(localStorage.getItem(SCRIPT_CONFIG.storage.bookedClubs) || '{}');
        const validClubs = [];
        const rows = clubsTable.querySelectorAll('tbody tr');
        log(`Found ${rows.length} clubs. Filtering based on settings...`);

        for (const row of rows) {
            const clubName = row.cells[0].textContent.trim();
            const priceText = row.cells[row.cells.length - 1].textContent;
            const price = parseFloat(priceText.trim().replace(/\s*M\$$/, '').replace(/\./g, '').replace(',', '.'));
            const starRatingKey = row.cells[2].querySelector('span.sortkey')?.textContent;

            if (settings.REQUIRE_5_STARS && starRatingKey !== '50') {
                log(`- Skipping ${clubName}: Does not have 5 stars.`);
                continue;
            }
            if (price < settings.TARGET_CLUB_RANGE.min || price > settings.TARGET_CLUB_RANGE.max) {
                log(`- Skipping ${clubName}: Price ${price}M$ is outside target range (${settings.TARGET_CLUB_RANGE.min}-${settings.TARGET_CLUB_RANGE.max}M$).`);
                continue;
            }
            const bookedDate = bookedClubs[clubName];
            const currentShowWeekStart = getWeekStartDate(currentShow.date);
            if (bookedDate && getWeekStartDate(bookedDate) === currentShowWeekStart) {
                log(`- Skipping ${clubName}: Already booked a show in this club for the week of ${currentShowWeekStart}.`);
                continue;
            }
            log(`+ Found valid club: ${clubName} (Price: ${price}M$)`);
            validClubs.push({ price, row, name: clubName });
        }

        if (validClubs.length > 0) {
            validClubs.sort((a, b) => b.price - a.price);
            const bestClub = validClubs[0];
            log(`Best club found: ${bestClub.name} for ${bestClub.price}M$. Attempting to book...`);

            bookedClubs[bestClub.name] = currentShow.date;
            localStorage.setItem(SCRIPT_CONFIG.storage.bookedClubs, JSON.stringify(bookedClubs));

            bestClub.row.querySelector('input[type="radio"]').click();
            await delay(500);
            document.querySelector(SCRIPT_CONFIG.selectors.bookShowBtn).click();
            log('Clicked "Book Show". Waiting for confirmation dialog...');

            await delay(1500);
            const confirmBtn = document.querySelector(SCRIPT_CONFIG.selectors.dialogConfirm);
            if (confirmBtn && (confirmBtn.textContent.includes('Yes') || confirmBtn.textContent.includes('OK'))) {
                confirmBtn.click();
                log('Confirmation dialog found and clicked. Show booked successfully.');
                return true;
            } else {
                logError("Could not find the confirmation 'Yes'/'OK' button. Stopping script.");
                stopProcess();
                return false;
            }
        } else {
            log('No valid clubs found for this slot.');
            return false;
        }
    }

    /**
     * Main processing function that orchestrates the booking of the next show.
     * @param {object} settings - The user-defined settings.
     */
    async function processNextShow(settings) {
        const statusEl = document.getElementById('pmBookerStatus');
        statusEl.style.color = 'orange';

        const tour = getTour(settings);
        let currentIndex = parseInt(localStorage.getItem(SCRIPT_CONFIG.storage.showIndex) || '0', 10);

        if (currentIndex >= tour.length) {
            statusEl.textContent = 'Tour Finished! All shows booked.';
            statusEl.style.color = 'green';
            log('Tour finished!');
            alert('Tour finished!');
            stopProcess();
            return;
        }

        const currentShow = tour[currentIndex];
        statusEl.textContent = `Processing show ${currentIndex + 1}/${tour.length}: ${currentShow.city} on ${currentShow.date} at ${currentShow.time}`;
        log(`Processing show ${currentIndex + 1}/${tour.length}: ${currentShow.city} @ ${currentShow.date} ${currentShow.time}`);

        const cityDropdown = document.querySelector(SCRIPT_CONFIG.selectors.city);
        const selectedCityText = cityDropdown.options[cityDropdown.selectedIndex].text.toLowerCase();

        if (selectedCityText.localeCompare(currentShow.city, undefined, { sensitivity: 'accent' }) !== 0) {
            const cityOption = [...cityDropdown.options].find(opt => opt.text.toLowerCase().localeCompare(currentShow.city, undefined, { sensitivity: 'accent' }) === 0);
            if (cityOption) {
                log(`Changing city from ${selectedCityText} to ${currentShow.city}.`);
                sessionStorage.setItem(SCRIPT_CONFIG.storage.restore, JSON.stringify(currentShow));
                cityDropdown.value = cityOption.value;
                cityDropdown.dispatchEvent(new Event('change', { bubbles: true }));
            } else {
                logError(`City ${currentShow.city} not found in dropdown. Skipping.`);
                localStorage.setItem(SCRIPT_CONFIG.storage.showIndex, currentIndex + 1);
                await delay(2000);
                processNextShow(settings);
            }
            return;
        }

        if (!document.querySelector(SCRIPT_CONFIG.selectors.clubsTable)) {
            log('Club table not found. Setting date/time and clicking "Find Clubs".');
            document.querySelector(SCRIPT_CONFIG.selectors.day).value = currentShow.date;
            document.querySelector(SCRIPT_CONFIG.selectors.hour).value = currentShow.time;
            await delay(300);
            document.querySelector(SCRIPT_CONFIG.selectors.findClubsBtn).click();
            return;
        }

        const booked = await findAndBookBestClub(settings, currentShow);
        localStorage.setItem(SCRIPT_CONFIG.storage.showIndex, currentIndex + 1);

        if (!booked) {
            log('No club was booked. Moving to the next show in the itinerary.');
            await delay(2000);
            window.location.reload();
        }
    }


    // --- SCRIPT ROUTER ---
    /**
     * Entry point of the script. Determines the current state and acts accordingly.
     */
    function run() {
        const status = sessionStorage.getItem(SCRIPT_CONFIG.storage.status);
        const settings = JSON.parse(sessionStorage.getItem(SCRIPT_CONFIG.storage.settings));
        const onBookShowPage = window.location.pathname.includes('/Artist/BookShow/');

        if (status === SCRIPT_CONFIG.STATE.RUNNING) {
            log(`Script is RUNNING. On book show page: ${onBookShowPage}.`);
            const bookShowPath = `/World/Popmundo.aspx/Artist/BookShow/${settings.ARTIST_ID}`;
            if (window.location.pathname !== bookShowPath) {
                log(`Incorrect page. Redirecting to ${bookShowPath}`);
                window.location.href = `https://${window.location.hostname}${bookShowPath}`;
                return;
            }
            injectUi();
            document.getElementById('pmBookerForm').style.display = 'none';
            document.getElementById('startBookerBtn').disabled = true;

            const restore = JSON.parse(sessionStorage.getItem(SCRIPT_CONFIG.storage.restore));
            if (restore) {
                log('Restoring date/time after city change.');
                sessionStorage.removeItem(SCRIPT_CONFIG.storage.restore);
                document.querySelector(SCRIPT_CONFIG.selectors.day).value = restore.date;
                document.querySelector(SCRIPT_CONFIG.selectors.hour).value = restore.time;
                document.querySelector(SCRIPT_CONFIG.selectors.findClubsBtn).click();
            } else {
                processNextShow(settings);
            }
        } else {
            log(`Script is IDLE. On book show page: ${onBookShowPage}.`);
            if (onBookShowPage) {
                injectUi();
            }
        }
    }

    run();

})();