// ==UserScript==
// @name Kleinanzeigen Mietdashboard mit Preisanalyse
// @description A new userscript
// @version 8.3
// @match https://*.kleinanzeigen.de/*
// @icon https://www.kleinanzeigen.de/favicon.svg
// @grant GM.getValue
// @grant GM.setValue
// @license MIT
// @grant GM.deleteValue
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
DASHBOARD_ANCHOR_SELECTOR: '.srp-header.l-container-row',
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 },
async init() {
this.state.plzLength = await GM.getValue('plz_length', CONFIG.PLZ_PREFIX_LENGTH);
this.injectStyles();
this.blockScripts();
setTimeout(() => this.run(), 250);
this.observeSPA();
window.addEventListener('error', e => console.error('[KA-SCRIPT] Globaler JS-Fehler:', e.error, e));
},
async run() {
console.log('[KA-SCRIPT] Starte Analyse für:', location.href);
try {
this.removeAdElements();
await this.processAdItems();
this.setupImgZoom();
} catch(e) {
console.error('[KA-SCRIPT] Ein schwerwiegender Fehler ist im run() aufgetreten:', e);
}
},
injectStyles() {
const style = document.createElement('style');
style.textContent = `
: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 rgba(0,0,0,0.13);
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: 0.9em 0.4em;
box-shadow: 0 1px 8px rgba(0,0,0,0.08);
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 0.3em 0;
display: block;
font-weight: bold;
}
.ka-dash-sub {
color: #777;
font-size: 0.97em;
line-height: 1.12;
}
#ka-dash-meta {
font-size: 0.94em;
color: #444;
margin-top: 6px;
flex-basis: 100%;
text-align: center;
}
#ka-dash-clear-btn {
margin-left: 0.7em;
font-size: 0.98em;
padding: 0.07em 0.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: 0.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(0.19, 1, 0.22, 1);
}
`;
document.head.appendChild(style);
},
injectDashboard(stats) {
console.log('[KA-SCRIPT] Injiziere Dashboard mit folgenden Daten:', stats);
document.getElementById('ka-main-dashboard')?.remove();
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();
}
});
},
async processAdItems() {
this.state.regionalPrices = await 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) {
await 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);
const maxP = Math.max(...prices);
const range = maxP - minP;
const lower = minP + range / 3;
const 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: {
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) {
const match = article.textContent.match(/\b(\d{5})\b/);
return match ? match[1].substring(0, plzLength) : null;
},
getArea(article) {
// Versuche zuerst im Tags-Container
const tagsContainer = article.querySelector('p.aditem-main--middle--tags');
if (tagsContainer) {
const match = tagsContainer.textContent.match(/([0-9.,]+)\s*m²/);
if (match) {
return parseFloat(match[1].replace(',', '.'));
}
}
// Fallback: Suche im Bottom-Bereich (für neue Struktur)
const bottomContainer = article.querySelector('.aditem-main--bottom p');
if (bottomContainer && bottomContainer.innerHTML.includes('m²')) {
const match = bottomContainer.innerHTML.match(/([0-9.,]+)\s*m²/);
if (match) {
return parseFloat(match[1].replace(',', '.'));
}
}
// Letzter Fallback: Gesamter Text
const match = article.textContent.match(/\b([\d.,]+)\s*m²\b/);
return match ? parseFloat(match[1].replace(',', '.')) : null;
},
getPrice(article) {
const priceEl = article.querySelector('.aditem-main--middle--price-shipping--price');
if (priceEl) {
// FIX: Extrahiere nur den ersten Preis (vor Rabatt)
const priceMatch = priceEl.textContent.match(/([\d\.,]+)\s*€/i);
if (priceMatch) {
const cleaned = priceMatch[1].replace(/\./g, '').replace(',', '.');
const value = parseFloat(cleaned);
if (!isNaN(value)) {
return value;
}
}
}
// Fallback auf gesamten Text
const match = article.textContent.match(/(\d{1,3}(?:\.\d{3})*(?:,\d{2})?|\d+)\s*€/);
if (match) {
return parseFloat(match[1].replace(/\./g, '').replace(',', '.'));
}
return null;
},
getAdId(article) {
return article.dataset.adid || 'ad_' + btoa(article.textContent.substring(0, 32)).replace(/[^a-zA-Z0-9]/g, '').substring(0, 10);
}
},
storage: {
getStorageKey() {
return `${CONFIG.STORAGE_KEY_PREFIX}_${KleinanzeigenOptimizer.state.plzLength}`;
},
async load() {
return await GM.getValue(this.getStorageKey(), {});
},
async save(data) {
await GM.setValue(this.getStorageKey(), data);
},
async clear() {
if (confirm('Alle regionalen m²-Preis-Daten für die aktuelle PLZ-Länge löschen?')) {
await GM.deleteValue(this.getStorageKey());
KleinanzeigenOptimizer.state.regionalPrices = {};
KleinanzeigenOptimizer.run();
}
}
},
setupImgZoom() {
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() {
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 });
},
utils: {
debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
}
};
KleinanzeigenOptimizer.init();
})();