Kleinanzeigen Mietdashboard V8.2 (Final Anchor Fix)

Dashboard mit präzisem Anker, robustere Parser, €/m², Werbeblocker #komplett refaktoriert

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

// ==UserScript==
// @name         Kleinanzeigen Mietdashboard V8.2 (Final Anchor Fix)
// @namespace    http://tampermonkey.net/
// @version      8.2
// @license MIT
// @description  Dashboard mit präzisem Anker, robustere Parser, €/m², Werbeblocker #komplett refaktoriert 
// @author       Deine KI-Assistenz
// @match        https://www.kleinanzeigen.de/s-wohnung-mieten/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        DASHBOARD_ANCHOR_SELECTOR: '.srp-header.l-container-row', // ❗ NEU: Dein exakter Ankerpunkt!
        AD_ITEM_SELECTOR: '.ad-listitem, article.aditem[data-adid]',
        STORAGE_KEY_PREFIX: 'ka_RegionalSqmPrices',
        PLZ_PREFIX_LENGTH: 3,
        DEBOUNCE_DELAY: 450,
        AD_SCRIPT_PATTERNS: [ /ads\.js/i, /advertisement\.js/i, /adservice/i, /googlesyndication\.com/i, /liberty.*\.js/i, /fbevent\.js/i, /teads.*\.js/i, /taboola.*\.js/i, /criteo.*\.js/i, /bat\.bing\.com/i, /hotjar.*\.js/i ],
        AD_ELEMENT_SELECTORS: [ '.site-base--left-banner', '.site-base--right-banner', '#banner-skyscraper', '.sticky-advertisement', 'div[id^="google_ads_iframe_"]', 'iframe[aria-label*="ad"]', '[data-liberty-position-name*="banner"]', '[aria-label*="Advertisement"]', '[aria-label*="Werbung"]', 'div[aria-label*="Gesponsert"]' ]
    };

    const KleinanzeigenOptimizer = {
        state: { lastUrl: location.href, regionalPrices: {}, plzLength: 3 },
        
        init() {
            this.state.plzLength = GM_getValue('plz_length', CONFIG.PLZ_PREFIX_LENGTH);
            this.injectStyles();
            this.blockScripts();
            this.registerMenuCommands();
            setTimeout(() => this.run(), 250); // Leicht erhöhter Start-Delay
            this.observeSPA();
            window.addEventListener('error', e => console.error('[KA-SCRIPT] Globaler JS-Fehler:', e.error, e));
        },

        run() {
            console.log('[KA-SCRIPT] Starte Analyse für:', location.href);
            try {
                this.removeAdElements();
                this.processAdItems();
                this.setupImgZoom();
            } catch(e) { console.error('[KA-SCRIPT] Ein schwerwiegender Fehler ist im run() aufgetreten:', e); }
        },

        injectStyles() {
             GM_addStyle(`
                :root { --ka-color-low: #38cb7f; --ka-color-low-bg: #ecfaed; --ka-color-mid: #ffd264; --ka-color-mid-bg: #fff8d2; --ka-color-high: #ff6363; --ka-color-high-bg: #fff1ef; --ka-price-color: #2342b2; --ka-border-color: #e3e9f1; }
                #ka-main-dashboard { margin: 0 0 16px 0; display:flex; gap:1.3em; background:#f4f7fb; border:2px solid var(--ka-border-color); border-radius:13px; box-shadow:0 2px 18px #2222; padding:1.2em 1.6em; align-items:center; flex-wrap:wrap; }
                .ka-dash-card { flex:1 1 0; text-align:center; border-radius:10px; background:#fff; margin:0 0.2em; padding:.9em .4em; box-shadow:0 1px 8px #2221; min-width:120px; }
                .ka-dash-card.ka-low { border-left:7px solid var(--ka-color-low); } .ka-dash-card.ka-mid { border-left:7px solid var(--ka-color-mid); } .ka-dash-card.ka-high { border-left:7px solid var(--ka-color-high); }
                .ka-dash-title { font-weight:700; font-size:1.13em; margin-bottom:6px; } .ka-dash-value { font-size:1.77em; margin:0 0 .3em 0; display:block; font-weight:bold; } .ka-dash-sub { color:#777;font-size:.97em;line-height:1.12 }
                #ka-dash-meta { font-size:.94em; color:#444; margin-top:6px; flex-basis:100%; text-align:center; } #ka-dash-clear-btn { margin-left:.7em;font-size:.98em;padding:.07em .55em; cursor:pointer; }
                .ka-sqm-wrap { text-align:right; } .ka-sqm-price-display { font-size:1.65em; color: var(--ka-price-color); font-weight:800; margin-left:.6em; background:#eef4fc; padding:2px 16px 2px 13px; border-radius:5px; float:none; display:inline-block; }
                article.aditem.ka-price-low, .ad-listitem.ka-price-low { background: var(--ka-color-low-bg) !important; } article.aditem.ka-price-mid, .ad-listitem.ka-price-mid { background: var(--ka-color-mid-bg) !important; } article.aditem.ka-price-high, .ad-listitem.ka-price-high { background: var(--ka-color-high-bg) !important; } article.aditem.ka-price-uniform, .ad-listitem.ka-price-uniform { background:#eee !important; }
                .ka-overlay-img { position:fixed; left:50%; top:50%; max-width:94vw; max-height:94vh; transform:translate(-50%,-50%); z-index:29999; border-radius:12px; box-shadow:0 8px 40px 0 rgba(0,0,0,0.72); background:#222; opacity:0; pointer-events:none; display:block; object-fit:contain; transition:opacity 0.16s cubic-bezier(.19,1,.22,1);}
            `);
        },

        injectDashboard(stats) {
            console.log('[KA-SCRIPT] Injiziere Dashboard mit folgenden Daten:', stats);
            document.getElementById('ka-main-dashboard')?.remove();

            // ❗ NEU: Direkter Anker, wie von dir vorgegeben, mit einem sicheren Fallback.
            const anchor = document.querySelector(CONFIG.DASHBOARD_ANCHOR_SELECTOR);

            if (!anchor) {
                console.error(`[KA-SCRIPT] Dashboard-Anker "${CONFIG.DASHBOARD_ANCHOR_SELECTOR}" nicht gefunden! Nutze Body als Fallback.`);
                document.body.prepend(this.createDashboardPanel(stats));
                return;
            }

            console.log('[KA-SCRIPT] Dashboard-Anker gefunden:', anchor);
            const panel = this.createDashboardPanel(stats);
            anchor.parentNode.insertBefore(panel, anchor.nextSibling);
        },
        
        createDashboardPanel(stats) {
            const panel = document.createElement('div');
            panel.id = 'ka-main-dashboard';
            const createCard = (className, title, value, subtext) => {
                const card = document.createElement('div');
                card.className = `ka-dash-card ${className}`;
                card.innerHTML = `<div class="ka-dash-title">${title}</div><span class="ka-dash-value">${value}</span><div class="ka-dash-sub">${subtext}</div>`;
                return card;
            };
            panel.append(
                createCard('ka-low', 'Günstig', stats.low.count, `${stats.low.min}–${stats.low.max} €/m²`),
                createCard('ka-mid', 'Mittel', stats.mid.count, `${stats.mid.min + 1}–${stats.mid.max} €/m²`),
                createCard('ka-high', 'Teuer', stats.high.count, `${stats.high.min + 1}–${stats.high.max} €/m²`)
            );
            const metaDiv = document.createElement('div');
            metaDiv.id = 'ka-dash-meta';
            metaDiv.innerHTML = `Analysiert: <b>${stats.adsOnPage}</b> · Ø-Preis: <b>${stats.avg} €/m²</b>`;
            const clearBtn = document.createElement('button');
            clearBtn.id = 'ka-dash-clear-btn';
            clearBtn.textContent = 'Preis-Cache löschen';
            clearBtn.onclick = () => this.storage.clear();
            metaDiv.appendChild(clearBtn);
            panel.appendChild(metaDiv);
            return panel;
        },

        blockScripts() {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(m => m.addedNodes.forEach(node => {
                    if (node.nodeType === 1 && node.tagName === 'SCRIPT' && node.src && CONFIG.AD_SCRIPT_PATTERNS.some(rx => rx.test(node.src))) {
                        console.warn('[KA-SCRIPT] Blockiere Werbe-Script:', node.src);
                        node.remove();
                    }
                }));
            });
            observer.observe(document.documentElement, { childList: true, subtree: true });
        },

        removeAdElements() {
            document.querySelectorAll(CONFIG.AD_ELEMENT_SELECTORS.join(', ')).forEach(el => el.remove());
            document.querySelectorAll(CONFIG.AD_ITEM_SELECTOR).forEach(ad => {
                if (/top anzeige|gesponsert|sponsored/i.test(ad.textContent)) ad.remove();
            });
        },

        processAdItems() {
            this.state.regionalPrices = this.storage.load();
            let storageChanged = false;
            const adElements = document.querySelectorAll(CONFIG.AD_ITEM_SELECTOR);
            console.log(`[KA-SCRIPT] ${adElements.length} Anzeigenelemente mit Selektor "${CONFIG.AD_ITEM_SELECTOR}" gefunden.`);
            if (adElements.length === 0) return;

            const adData = Array.from(adElements).map(article => {
                article.classList.remove('ka-price-low', 'ka-price-mid', 'ka-price-high', 'ka-price-uniform');
                article.querySelector('.ka-sqm-wrap')?.remove();
                const data = this.parser.parseArticle(article, this.state.plzLength);
                if (!data) return null;
                const roundedPrice = Math.round(data.pricePerSqm);
                const plzPrefix = data.plzPrefix;
                if (!this.state.regionalPrices[plzPrefix]) this.state.regionalPrices[plzPrefix] = {};
                if (this.state.regionalPrices[plzPrefix][data.adId] !== roundedPrice) {
                    this.state.regionalPrices[plzPrefix][data.adId] = roundedPrice;
                    storageChanged = true;
                }
                const pricebox = article.querySelector('.aditem-main--middle--price-shipping');
                if (pricebox) {
                    const wrap = document.createElement('div');
                    wrap.className = 'ka-sqm-wrap';
                    wrap.innerHTML = `<span class="ka-sqm-price-display">${data.pricePerSqm.toFixed(2).replace('.', ',')} €/m²</span>`;
                    pricebox.appendChild(wrap);
                }
                return { article, ...data };
            }).filter(Boolean);

            console.log(`[KA-SCRIPT] ${adData.length} davon konnten erfolgreich verarbeitet werden.`);
            if (storageChanged) this.storage.save(this.state.regionalPrices);
            if (!adData.length) {
                document.getElementById('ka-main-dashboard')?.remove();
                return;
            }

            const prices = adData.map(d => d.pricePerSqm);
            const minP = Math.min(...prices), maxP = Math.max(...prices), range = maxP - minP;
            const lower = minP + range / 3, upper = minP + 2 * range / 3;
            let low = 0, mid = 0, high = 0;
            adData.forEach(({ article, pricePerSqm }) => {
                if (range < 0.01) { article.classList.add('ka-price-uniform'); return; }
                if (pricePerSqm <= lower) { article.classList.add('ka-price-low'); low++; }
                else if (pricePerSqm <= upper) { article.classList.add('ka-price-mid'); mid++; }
                else { article.classList.add('ka-price-high'); high++; }
            });
            this.injectDashboard({
                low: { count: low, min: Math.round(minP), max: Math.floor(lower) },
                mid: { count: mid, min: Math.floor(lower), max: Math.floor(upper) },
                high: { count: high, min: Math.floor(upper), max: Math.round(maxP) },
                adsOnPage: adData.length,
                avg: (prices.reduce((s, x) => s + x, 0) / prices.length).toFixed(2),
            });
        },

        parser: { // ❗ VERBESSERT: Robusterer Parser mit Fallbacks
            parseArticle(article, plzLength) {
                const data = {
                    plzPrefix: this.getPLZPrefix(article, plzLength),
                    area: this.getArea(article),
                    price: this.getPrice(article),
                    adId: this.getAdId(article)
                };
                if (!data.plzPrefix || !data.area || !data.price || !data.adId) return null;
                data.pricePerSqm = data.price / data.area;
                return data;
            },
            getPLZPrefix: (article, plzLength) => (article.textContent.match(/\b(\d{5})\b/) || [])[1]?.substring(0, plzLength) || null,
            getArea(article) {
                const tags = article.querySelector('.aditem-main--middle--tags')?.textContent;
                let match = tags ? tags.match(/([0-9.,]+)\s*m²/) : null;
                if (match) return parseFloat(match[1].replace(',', '.'));
                match = article.textContent.match(/\b([\d.,]+)\s*m²\b/); // Fallback auf gesamten Text
                return match ? parseFloat(match[1].replace(',', '.')) : null;
            },
            getPrice(article) {
                const priceEl = article.querySelector('.aditem-main--middle--price-shipping--price');
                if (priceEl) {
                    const cleaned = priceEl.textContent.replace(/\s*€\s*|VB/gi, '').replace(/\./g, '').replace(',', '.');
                    const value = parseFloat(cleaned);
                    if (!isNaN(value)) return value;
                }
                const match = article.textContent.match(/(\d{1,3}(?:\.\d{3})*,\d{2}|\d+)\s*€/); // Fallback auf gesamten Text
                if(match) return parseFloat(match[1].replace(/\./g, '').replace(',', '.'));
                return null;
            },
            getAdId: (article) => article.dataset.adid || 'ad_' + btoa(article.textContent.substring(0, 32)).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10)
        },

        storage: { /* ... keine Änderung ... */ getStorageKey: () => `${CONFIG.STORAGE_KEY_PREFIX}_${KleinanzeigenOptimizer.state.plzLength}`, load: () => GM_getValue(KleinanzeigenOptimizer.storage.getStorageKey(), {}), save: (data) => GM_setValue(KleinanzeigenOptimizer.storage.getStorageKey(), data), clear: () => { if (confirm('Alle regionalen m²-Preis-Daten für die aktuelle PLZ-Länge löschen?')) { GM_deleteValue(KleinanzeigenOptimizer.storage.getStorageKey()); KleinanzeigenOptimizer.state.regionalPrices = {}; KleinanzeigenOptimizer.run(); } } },
        setupImgZoom() { /* ... keine Änderung ... */ const list = document.querySelector('#srchrslt-adtable, #srchrslt-gallery'); if (!list || list.dataset.kaZoomBound) return; list.dataset.kaZoomBound = 'y'; let overlayImg = document.querySelector('.ka-overlay-img'); if (!overlayImg) { overlayImg = document.createElement('img'); overlayImg.className = 'ka-overlay-img'; document.body.appendChild(overlayImg); overlayImg.onclick = () => { overlayImg.style.opacity = '0'; overlayImg.style.pointerEvents = 'none'; }; } list.addEventListener('mouseover', e => { const img = e.target.closest('.imagebox img, .aditem-image img'); if (img) { img.style.cursor = 'zoom-in'; overlayImg.src = img.src; overlayImg.style.opacity = '1'; overlayImg.style.pointerEvents = 'auto'; } }); list.addEventListener('mouseout', e => { if (e.target.closest('.imagebox img, .aditem-image img')) { overlayImg.style.opacity = '0'; } }); },
        observeSPA() { /* ... keine Änderung ... */ const debouncedRun = this.utils.debounce(() => this.run(), CONFIG.DEBOUNCE_DELAY); const observer = new MutationObserver(() => { if (location.href !== this.state.lastUrl) { this.state.lastUrl = location.href; debouncedRun(); } }); observer.observe(document.body, { childList: true, subtree: true }); },
        registerMenuCommands() { /* ... keine Änderung ... */ GM_registerMenuCommand('Miet-Dashboard: PLZ-Genauigkeit einstellen', () => { const newLength = prompt('Postleitzahlen-Genauigkeit (2-5 Ziffern):', this.state.plzLength); const val = parseInt(newLength); if (val >= 2 && val <= 5) { GM_setValue('plz_length', val); alert('Einstellung gespeichert. Die Seite wird neu geladen.'); location.reload(); } else if (newLength !== null) { alert('Ungültige Eingabe. Bitte eine Zahl zwischen 2 und 5 eingeben.'); } }); },
        utils: { debounce(func, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; } }
    };

    KleinanzeigenOptimizer.init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址