您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Erfasst sichtbare Inhalte und Metadaten als JSONL für LLM-Verarbeitung. Unterstützt kontinuierliches Sammeln über mehrere Seiten (persistenter Modus) und Seitennavigation mit A/D. Exportiert nur sichtbare/lesbare Daten + Anzeigen-ID.
// ==UserScript== // @name Kleinanzeigen-Datenexport (TO LLM) & Seitennavigation 🚀 // @namespace http://tampermonkey.net/ // @version 22.8 // Version angehoben für A/D Navigation immer aktiv // @description Erfasst sichtbare Inhalte und Metadaten als JSONL für LLM-Verarbeitung. Unterstützt kontinuierliches Sammeln über mehrere Seiten (persistenter Modus) und Seitennavigation mit A/D. Exportiert nur sichtbare/lesbare Daten + Anzeigen-ID. // @author Assistant & User // @license MIT // @match https://www.kleinanzeigen.de/* // @icon https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://kleinanzeigen.de&size=64 // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // Globale Variablen let pickerButton = null; let longPressTimer = null; const LONG_PRESS_THRESHOLD = 500; // ms, Wie lange der Klick gehalten werden muss für Langklick let isLockedMode = false; // true wenn im kontinuierlichen Sammel-Modus let allCollectedData = []; // Speichert JSONL-Objekte aus allen gesammelten Seiten let scriptErrors = []; // Speichert Skript-interne Fehler let countdownInterval = null; // Für den Countdown-Timer let lastKnownUrl = window.location.href; // Für die Erkennung von Seitenwechseln let mutationObserver = null; // Für DOM-Änderungen in SPAs let processingPage = false; // Flag, um mehrfache Verarbeitung bei schnellen DOM-Änderungen zu verhindern // Schlüssel für die Speicherung des Modus-Status const STORAGE_KEY_LOCKED_MODE = 'kleinanzeigen_picker_locked_mode'; // Startzeit für Skript-Laufzeitmessung (für Metadaten) const scriptStartTime = performance.now(); // CSS-Stile für den Button und Benachrichtigungen GM_addStyle(` #element-picker-btn { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); width: 100px; /* Länglicher */ height: 50px; /* Länglicher */ background: #007185; border: none; border-radius: 8px; /* Leicht abgerundet */ cursor: pointer; z-index: 9999; box-shadow: 0 2px 10px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; transition: background-color 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease; color: white; font-size: 16px; font-weight: bold; text-align: center; line-height: 1.2; padding: 5px; } #element-picker-btn:hover { background-color: #005a6b; opacity: 0.9; } #element-picker-btn.active { background: #dc3545; animation: pulse 1s infinite; } #element-picker-btn.locked { background: #17a2b8; animation: pulse-locked 1s infinite; } #element-picker-btn.processing { opacity: 0.6; cursor: wait; } /* Visueller Hinweis während der Datenverarbeitung */ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); } 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); } } @keyframes pulse-locked { 0% { box-shadow: 0 0 0 0 rgba(23, 162, 184, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(23, 162, 184, 0); } 100% { box-shadow: 0 0 0 0 rgba(23, 162, 184, 0); } } .picker-notification { position: fixed; top: 20px; right: 20px; background: #28a745; color: white; padding: 10px 20px; border-radius: 5px; z-index: 10000; font-family: Arial, sans-serif; font-size: 14px; animation: slideIn 0.3s ease-out; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .picker-status { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #343a40; color: white; padding: 8px 16px; border-radius: 5px; z-index: 10000; font-family: Arial, sans-serif; font-size: 12px; font-weight: bold; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } `); // --- FEHLERPROTOKOLLIERUNG --- function logScriptError(error, context) { scriptErrors.push({ timestamp: new Date().toISOString(), context: context, message: error.message, stack: error.stack ? error.stack.split('\n') : 'No stack trace available' }); console.error(`Kleinanzeigen-Skript Fehler [${context}]:`, error); } // --- HILFSFUNKTIONEN FÜR DATENEXTRAKTION --- function parseLocation(locationString) { try { if (!locationString) return { plz: null, stadt: null, entfernung_km: null }; const cleanString = locationString.replace(/\s+/g, ' ').trim(); const plzMatch = cleanString.match(/^(\d{5})/); let stadt = cleanString.replace(/^\d{5}\s*/, '').replace(/\(\d+\s*km\)/, '').trim(); // Wenn die Stadt noch den PLZ-Teil enthält, entferne ihn erneut if (plzMatch && stadt.startsWith(plzMatch[1])) { stadt = stadt.substring(plzMatch[1].length).trim(); } const distanceMatch = cleanString.match(/\((\d+)\s*km\)/); return { plz: plzMatch ? plzMatch[1] : null, stadt: stadt || null, entfernung_km: distanceMatch ? parseInt(distanceMatch[1], 10) : null }; } catch (e) { logScriptError(e, 'parseLocation'); return { plz: null, stadt: null, entfernung_km: null }; } } function parsePrice(priceString) { try { if (!priceString) return { betrag: null, zusatz: null }; const cleanString = priceString.replace(/\s+/g, ' ').trim(); const betragMatch = cleanString.match(/(\d[\d\.]*)/); let betrag = null; if (betragMatch) { betrag = parseFloat(betragMatch[1].replace(/\./g, '').replace(/,/g, '.')); // Auch Kommas für Dezimalzahlen beachten } const zusatzMatch = cleanString.match(/VB/i); return { betrag: betrag, zusatz: zusatzMatch ? 'VB' : null }; } catch (e) { logScriptError(e, 'parsePrice'); return { betrag: null, zusatz: null }; } } // Funktion zum Extrahieren von Anzeigen-spezifischen Daten function extractAdItemData(adListItem) { try { const data = {}; const adid = adListItem.getAttribute('data-adid'); if (adid) data.id_of_ad = adid; // Hier wird die Anzeigen-ID hinzugefügt // Nur die Key-Data, die ein Mensch auch lesen würde const titleElement = adListItem.querySelector('h2.text-module-begin a.ellipsis, h2.text-module-begin span.ellipsis'); if (titleElement) data.title = titleElement.textContent.trim(); const descElement = adListItem.querySelector('.aditem-main--middle--description'); if (descElement) data.description = descElement.textContent.trim().replace(/\s+/g, ' '); const priceElement = adListItem.querySelector('.aditem-main--middle--price-shipping--price'); if (priceElement) data.price = parsePrice(priceElement.textContent); const locationElement = adListItem.querySelector('.aditem-main--top--left'); if (locationElement) data.location = parseLocation(locationElement.textContent); const dateElement = adListItem.querySelector('.aditem-main--top--right'); if (dateElement) data.date = dateElement.textContent.trim(); const linkElement = adListItem.querySelector('a[href^="/s-anzeige/"]'); if(linkElement) data.link = `https://www.kleinanzeigen.de${linkElement.getAttribute('href')}`; const imageCountElement = adListItem.querySelector('.galleryimage--counter'); if (imageCountElement) data.image_count = parseInt(imageCountElement.textContent.trim(), 10); const isShippingPossible = adListItem.querySelector('.simpletag.tag-with-icon .icon-package'); if (isShippingPossible) data.shipping_possible = true; const isDirectBuy = adListItem.querySelector('.simpletag.tag-with-icon .icon-send-money'); if (isDirectBuy) data.direct_buy_possible = true; // Wenn keine ID gefunden wurde, ist es wahrscheinlich kein gültiges Anzeigen-Element if (!data.id_of_ad) { return null; } // Entferne undefined-Werte Object.keys(data).forEach(key => data[key] === undefined && delete data[key]); return data; } catch (e) { logScriptError(e, 'extractAdItemData'); return null; } } function extractPageMetadata() { try { const url = window.location.href; const title = document.title; const timestamp_utc = new Date().toISOString(); const html_lang = document.documentElement.lang || 'de'; const faviconLink = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]'); const favicon_url = faviconLink ? faviconLink.href : 'https://www.kleinanzeigen.de/favicon.ico'; const metaDescriptionTag = document.querySelector('meta[name="description"]'); const metaDescription = metaDescriptionTag ? metaDescriptionTag.content : null; const canonicalLink = document.querySelector('link[rel="canonical"]'); const canonicalUrl = canonicalLink ? canonicalLink.href : null; const urlParams = new URLSearchParams(window.location.search); const url_parameters = {}; for (const [key, value] of urlParams.entries()) { url_parameters[key] = value; } const schemaOrgData = []; document.querySelectorAll('script[type="application/ld+json"]').forEach(script => { try { const json = JSON.parse(script.textContent); if (json['@type']) { // If it's an array of types, take them all if (Array.isArray(json['@type'])) { schemaOrgData.push(...json['@type']); } else { schemaOrgData.push(json['@type']); } } } catch (e) { logScriptError(e, 'Parsing Schema.org JSON-LD'); } }); // Paginierungsinformationen werden hier direkt hinzugefügt const currentPageElement = document.querySelector('.pagination-current'); const currentPage = currentPageElement ? parseInt(currentPageElement.textContent.trim()) : 1; const totalPagesElement = document.querySelector('.pagination-pages a:last-of-type'); let totalPages = totalPagesElement ? parseInt(totalPagesElement.textContent.trim()) : null; if (!totalPages || isNaN(totalPages)) { const allPageLinks = document.querySelectorAll('.pagination-pages a.pagination-page, .pagination-current'); if (allPageLinks.length > 0) { const visiblePageNumbers = Array.from(allPageLinks) .map(el => parseInt(el.textContent.trim())) .filter(num => !isNaN(num)); totalPages = Math.max(...visiblePageNumbers, currentPage); } } const nextLinkElement = document.querySelector('a.pagination-next, span.pagination-next[data-url]'); const nextPageLink = nextLinkElement ? (nextLinkElement.tagName.toLowerCase() === 'a' ? nextLinkElement.href : nextLinkElement.dataset.url) : null; const prevLinkElement = document.querySelector('a.pagination-prev, span.pagination-prev[data-url]'); const prevPageLink = prevLinkElement ? (prevLinkElement.tagName.toLowerCase() === 'a' ? prevLinkElement.href : prevLinkElement.dataset.url) : null; return { metadata: { url: url, title: title, timestamp_utc: timestamp_utc, html_lang: html_lang, favicon_url: favicon_url, url_parameters: Object.keys(url_parameters).length > 0 ? url_parameters : null, metaDescription: metaDescription, canonicalUrl: canonicalUrl, schemaOrgData: schemaOrgData.length > 0 ? Array.from(new Set(schemaOrgData)) : null, // Unique types script_runtime: ((performance.now() - scriptStartTime) / 1000).toFixed(3) + 's', script_version: GM_info.script.version, page_load_errors: scriptErrors.length > 0 ? scriptErrors : null, pagination: { current_page: currentPage, total_pages: totalPages, next_page_link: nextPageLink, prev_page_link: prevPageLink } } }; } catch (e) { logScriptError(e, 'extractPageMetadata'); return { metadata: { url: window.location.href, error: e.message, page_load_errors: scriptErrors } }; } } // --- HAUPTFUNKTIONEN & MODUS-STEUERUNG --- function collectCurrentPageData() { scriptErrors = []; // Fehler für jede neue Seite zurücksetzen const pageData = [extractPageMetadata()]; // Beginne immer mit Metadaten // Kleinanzeigen spezifische Ad-Items immer sammeln (wenn sichtbar und mit ID) // HINWEIS: Der Viewport-Check wurde hier entfernt, um alle Anzeigen der Liste zu erfassen. document.querySelectorAll('article.aditem, li.ad-listitem > article.aditem').forEach(item => { const adid = item.getAttribute('data-adid'); if (adid) { // Nur Anzeigen mit einer ID const data = extractAdItemData(item); if (data) pageData.push(data); } }); // Aktualisiere Metadaten mit tatsächlicher Anzahl gesammelter Elemente // Beachten: pageData[0] ist die Metadaten. Der Rest sind die Anzeigen. pageData[0].metadata.selected_elements_count = pageData.length - 1; console.log(`Kleinanzeigen-Skript: Gesammelte Anzeigen auf aktueller Seite: ${pageData.length - 1}`); return pageData; } function copyDataToClipboard(data) { try { const jsonlOutput = data.map(obj => JSON.stringify(obj)).join('\n'); GM_setClipboard(jsonlOutput, 'text/plain'); showNotification(`${data.length > 0 ? data.length - 1 : 0} Objekt(e) als JSONL kopiert!`); } catch (e) { logScriptError(e, 'copyDataToClipboard'); showNotification('Fehler beim Kopieren der Daten!'); } } // Funktion zum Anzeigen von Benachrichtigungen function showNotification(message, duration = 3000) { let notification = document.querySelector('.picker-notification'); if (!notification) { notification = document.createElement('div'); notification.classList.add('picker-notification'); document.body.appendChild(notification); } notification.textContent = message; notification.style.display = 'block'; // Sicherstellen, dass es sichtbar ist clearTimeout(notification.timer); notification.timer = setTimeout(() => { notification.style.display = 'none'; }, duration); } // --- BUTTON INTERAKTION --- function handleButtonPress(e) { if (e.button !== 0) return; // Nur linke Maustaste // Verhindere Textauswahl bei Klick e.preventDefault(); if (isLockedMode) { // Wenn im Sammel-Modus, beende den Modus und kopiere alles console.log("Kleinanzeigen-Skript: Sammel-Modus beenden durch Kurz-Klick."); deactivateLockedMode(); return; } // Startet Timer für Langklick-Erkennung console.log("Kleinanzeigen-Skript: Maustaste gedrückt, starte Langklick-Timer."); longPressTimer = setTimeout(() => { console.log("Kleinanzeigen-Skript: Langklick-Schwelle erreicht, starte Countdown."); startCountdown(3); // Startet den visuellen Countdown longPressTimer = null; // Setzt Timer zurück, da Langklick erkannt }, LONG_PRESS_THRESHOLD); // Listener, um Langklick zu beenden, falls die Maustaste losgelassen wird // Oder wenn die Maus den Button verlässt, bevor der Langklick registriert wird // ACHTUNG: 'once: true' ist wichtig für den Mouseleave Listener auf dem Button pickerButton.addEventListener('mouseleave', handleButtonRelease, { once: true }); // Der globale 'mouseup' Listener wird unten in 'init' hinzugefügt } function handleButtonRelease(e) { // Nur auslösen, wenn der Event vom Picker Button oder dem Dokument selbst kommt // (oder wenn der Event-Target nicht pickerButton ist, aber pickerButton ein Child von e.target ist, was für document.mouseup relevant ist) if (e.target !== pickerButton && !pickerButton.contains(e.target)) return; console.log("Kleinanzeigen-Skript: Maustaste losgelassen oder Maus vom Button entfernt."); if (countdownInterval) { // Wenn der Countdown aktiv ist, wurde der Langklick abgebrochen console.log("Kleinanzeigen-Skript: Langklick-Countdown abgebrochen."); clearInterval(countdownInterval); countdownInterval = null; pickerButton.textContent = 'TO LLM'; // Text zurücksetzen pickerButton.classList.remove('active'); // Puls entfernen showNotification('Langklick abgebrochen.'); } else if (longPressTimer) { // Wenn der Timer noch läuft, war es ein kurzer Klick console.log("Kleinanzeigen-Skript: Kurzer Klick erkannt."); clearTimeout(longPressTimer); longPressTimer = null; // Kurzer Klick: Sammle Daten der aktuellen Seite und kopiere sie const currentPageData = collectCurrentPageData(); copyDataToClipboard(currentPageData); showNotification('Aktuelle Seite als JSONL kopiert!'); pickerButton.classList.remove('active'); // Setze Zustand zurück } // Nach jeder Button-Interaktion Timer und Listener aufräumen if (longPressTimer) { // Falls Timer noch aktiv, aber Release ausgelöst clearTimeout(longPressTimer); longPressTimer = null; } pickerButton.removeEventListener('mouseleave', handleButtonRelease); // Sicherstellen, dass nur dieser Listener entfernt wird } function startCountdown(count) { let currentCount = count; pickerButton.textContent = currentCount; pickerButton.classList.add('active'); // Visuell als "aktiv" markieren countdownInterval = setInterval(() => { currentCount--; if (currentCount > 0) { pickerButton.textContent = currentCount; } else { clearInterval(countdownInterval); countdownInterval = null; pickerButton.textContent = 'STOP'; // Text für Sammel-Modus activateLockedMode(); // Aktiviere den kontinuierlichen Sammel-Modus showNotification('Sammel-Modus aktiv. Navigieren Sie durch Seiten.'); } }, 1000); } function activateLockedMode() { console.log("Kleinanzeigen-Skript: Sammel-Modus aktiviert."); isLockedMode = true; GM_setValue(STORAGE_KEY_LOCKED_MODE, true); // Status speichern allCollectedData = []; // Alte Daten löschen processNewPage(); // Daten der Startseite sammeln pickerButton.classList.remove('active'); // Entferne "active" Puls pickerButton.classList.add('locked'); // Füge "locked" Puls hinzu pickerButton.title = 'Klicken, um Sammel-Modus zu beenden und alle gesammelten Daten zu kopieren'; // Starte den MutationObserver für Seitenwechsel in SPAs observeDOMChanges(); updateStatusDisplay(); } function deactivateLockedMode() { console.log("Kleinanzeigen-Skript: Sammel-Modus deaktiviert."); isLockedMode = false; GM_setValue(STORAGE_KEY_LOCKED_MODE, false); // Status löschen if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; console.log("Kleinanzeigen-Skript: MutationObserver gestoppt."); } // Kopiere alle gesammelten Daten vor dem Löschen copyDataToClipboard(allCollectedData); allCollectedData = []; // Gesammelte Daten leeren nach dem Kopieren/Beenden pickerButton.classList.remove('locked', 'active', 'processing'); pickerButton.textContent = 'TO LLM'; // Zurück zum Standardtext pickerButton.title = 'Daten der aktuellen Seite erfassen (Kurz-Klick) / Kontinuierlich sammeln (Langklick)'; removeStatus(); showNotification('Sammel-Modus beendet.'); } // --- SEITENWECHSEL-ERKENNUNG (Primär über MutationObserver) --- function processNewPage() { if (processingPage) { console.log("Kleinanzeigen-Skript: Seitenverarbeitung bereits im Gange, überspringe."); return; } processingPage = true; pickerButton.classList.add('processing'); // Visueller Hinweis console.log(`Kleinanzeigen-Skript: Starte Datensammlung für aktuelle Seite: ${window.location.href}`); // Kleine Verzögerung, damit die Seite vollständig gerendert werden kann // und um Doppel-Sammlungen bei schnellen DOM-Updates zu vermeiden setTimeout(() => { allCollectedData.push(...collectCurrentPageData()); updateStatusDisplay(); showNotification(`Seite gesammelt: ${window.location.pathname.split('/').pop() || 'Startseite'}`); console.log(`Kleinanzeigen-Skript: Datensammlung für ${window.location.href} abgeschlossen. Total Datenpunkte: ${allCollectedData.length}`); pickerButton.classList.remove('processing'); processingPage = false; }, 800); // Erhöhte Verzögerung für stabilere Erfassung } function observeDOMChanges() { if (mutationObserver) { mutationObserver.disconnect(); // Vorherigen Observer trennen } const targetNode = document.body; const config = { childList: true, subtree: true, attributes: false }; mutationObserver = new MutationObserver(mutationsList => { const newUrl = window.location.href; if (newUrl !== lastKnownUrl) { // URL hat sich geändert (Browser-Nav, PushState), behandle als Seitenwechsel console.log(`Kleinanzeigen-Skript: URL-Wechsel erkannt: ${lastKnownUrl} -> ${newUrl}`); if (isLockedMode) { processNewPage(); } lastKnownUrl = newUrl; // URL aktualisieren } else { // URL ist gleich, aber DOM hat sich geändert (AJAX-Load, Filter) const adListContainer = document.getElementById('srchrslt-adtable'); // Der Container, der die Anzeigenliste hält const relevantChange = mutationsList.some(mutation => { return (adListContainer && adListContainer.contains(mutation.target)) || mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node => node.nodeType === Node.ELEMENT_NODE && (node.matches('.ad-listitem') || node.matches('.pagination-page'))); }); if (relevantChange && isLockedMode && !processingPage) { console.log("Kleinanzeigen-Skript: Relevante DOM-Änderung erkannt und im Sammel-Modus."); processNewPage(); } } }); mutationObserver.observe(targetNode, config); console.log("Kleinanzeigen-Skript: MutationObserver gestartet."); } // --- Statusanzeige am unteren Rand --- function updateStatusDisplay() { let statusElement = document.getElementById('picker-status-display'); if (!statusElement) { statusElement = document.createElement('div'); statusElement.id = 'picker-status-display'; statusElement.classList.add('picker-status'); document.body.appendChild(statusElement); } // Datenpunkte = Gesamtanzahl der Objekte minus die Metadaten-Objekte (eines pro gesammelter Seite) const totalDataPoints = allCollectedData.length > 0 ? allCollectedData.filter(item => item.id_of_ad).length : 0; const totalPagesCollected = allCollectedData.length > 0 ? allCollectedData.filter(item => item.metadata).length : 0; statusElement.textContent = `Gesammelte Seiten: ${totalPagesCollected} | Anzeigen: ${totalDataPoints}`; } function removeStatus() { const statusElement = document.getElementById('picker-status-display'); if (statusElement) { statusElement.remove(); } } // Bombensichere Tastatur-Navigation (A/D) document.addEventListener('keydown', (e) => { // Die Bedingung `isLockedMode` wurde entfernt, damit die Navigation immer funktioniert. const targetTagName = e.target.tagName.toLowerCase(); if (!(targetTagName === 'input' || targetTagName === 'textarea' || e.target.isContentEditable)) { // Verhindere Standard-Scrollverhalten bei A/D if (e.key === 'a' || e.key === 'A' || e.key === 'd' || e.key === 'D') { e.preventDefault(); } let navigationElement = null; // Navigiere zur vorherigen Seite mit 'A' if (e.key === 'a' || e.key === 'A') { console.log("Kleinanzeigen-Skript: Suche nach vorheriger Seite..."); // Spezifisch für Kleinanzeigen: a.pagination-prev oder span.pagination-prev[data-url] navigationElement = document.querySelector('a.pagination-prev, span.pagination-prev[data-url]'); // Fallback für allgemeinere Selektoren, falls die spezifischen nicht gefunden werden if (!navigationElement) { navigationElement = document.querySelector( 'a[title*="vorherige" i], ' + 'a[title*="Zurück" i], ' + 'a[aria-label*="vorherige" i], ' + 'a[aria-label*="Zurück" i], ' + 'a:contains("<"), ' + 'a:contains("«"), ' + 'a[rel="prev"], ' + '.pagination-pages a:first-of-type, ' + '.pagination-nav a:first-of-type' ); } if (navigationElement) { console.log("Kleinanzeigen-Skript: Navigiere zur vorherigen Seite (gefunden)."); if (navigationElement.tagName.toLowerCase() === 'a') { navigationElement.click(); } else if (navigationElement.tagName.toLowerCase() === 'span' && navigationElement.dataset.url) { window.location.href = navigationElement.dataset.url; } else { // Fallback, wenn es ein unbekannter Typ ist aber klickbar scheint navigationElement.click(); } } else { console.log("Kleinanzeigen-Skript: Keine vorherige Seite gefunden."); } } // Navigiere zur nächsten Seite mit 'D' else if (e.key === 'd' || e.key === 'D') { console.log("Kleinanzeigen-Skript: Suche nach nächster Seite..."); // Spezifisch für Kleinanzeigen: a.pagination-next oder span.pagination-next[data-url] navigationElement = document.querySelector('a.pagination-next, span.pagination-next[data-url]'); // Fallback für allgemeinere Selektoren if (!navigationElement) { navigationElement = document.querySelector( 'a[title*="nächste" i], ' + 'a[title*="Weiter" i], ' + 'a[aria-label*="nächste" i], ' + 'a[aria-label*="Weiter" i], ' + 'a:contains(">"), ' + 'a:contains("»"), ' + 'a[rel="next"], ' + '.pagination-pages a:last-of-type, ' + '.pagination-nav a:last-of-type' ); } if (navigationElement) { console.log("Kleinanzeigen-Skript: Navigiere zur nächsten Seite (gefunden)."); if (navigationElement.tagName.toLowerCase() === 'a') { navigationElement.click(); } else if (navigationElement.tagName.toLowerCase() === 'span' && navigationElement.dataset.url) { window.location.href = navigationElement.dataset.url; } else { // Fallback, wenn es ein unbekannter Typ ist aber klickbar scheint navigationElement.click(); } } else { console.log("Kleinanzeigen-Skript: Keine nächste Seite gefunden."); } } } }); // HILFSFUNKTION FÜR :contains Pseudo-Klasse // Da :contains keine native CSS-Selektor-Funktion ist, müssen wir sie nachbilden. // Dies erweitert querySelector/querySelectorAll, um :contains zu verstehen. // Die 'i' Flag in :contains("Text" i) macht die Suche case-insensitive. (function() { // Sicherstellen, dass Element.prototype.matches vorhanden ist if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; } const originalQuerySelector = Document.prototype.querySelector; const originalQuerySelectorAll = Document.prototype.querySelectorAll; const handleContains = (selector) => { // Regex angepasst, um die 'i' Flag korrekt zu verarbeiten const containsRegex = /:contains\(['"]([^'"]+)['"]\s*(i)?\)/g; let match; let modifiedSelector = selector; const textsToFind = []; while ((match = containsRegex.exec(selector)) !== null) { textsToFind.push({ text: match[1], caseInsensitive: !!match[2] // True, wenn 'i' Flag vorhanden ist }); // Ersetze :contains im Selektor durch '' modifiedSelector = modifiedSelector.replace(match[0], ''); } return { modifiedSelector, textsToFind }; }; Document.prototype.querySelector = function(selector) { const { modifiedSelector, textsToFind } = handleContains(selector); // Wenn der Selector nach dem Entfernen von :contains leer ist, können wir ihn nicht direkt verwenden. // In diesem Fall suchen wir einfach alle Elemente, die den Text enthalten könnten. const elementsToSearch = modifiedSelector.trim() === '' ? this.querySelectorAll('*') : originalQuerySelectorAll.call(this, modifiedSelector); if (textsToFind.length > 0) { // Finden des ersten Elements, das den Text enthält for (let i = 0; i < elementsToSearch.length; i++) { const element = elementsToSearch[i]; if (textsToFind.some(item => { const content = element.textContent; return item.caseInsensitive ? content.toLowerCase().includes(item.text.toLowerCase()) : content.includes(item.text); })) { return element; } } return null; // Kein Element mit passendem Text gefunden } // Wenn keine :contains-Klausel, nutze den ursprünglichen Selektor direkt return originalQuerySelector.call(this, selector); }; Document.prototype.querySelectorAll = function(selector) { const { modifiedSelector, textsToFind } = handleContains(selector); // Wenn der Selector nach dem Entfernen von :contains leer ist, suchen wir alle Elemente. const elements = modifiedSelector.trim() === '' ? originalQuerySelectorAll.call(this, '*') : originalQuerySelectorAll.call(this, modifiedSelector); if (textsToFind.length > 0) { return Array.from(elements).filter(element => textsToFind.some(item => { const content = element.textContent; return item.caseInsensitive ? content.toLowerCase().includes(item.text.toLowerCase()) : content.includes(item.text); }) ); } // Wenn keine :contains-Klausel, nutze den ursprünglichen Selektor direkt return originalQuerySelectorAll.call(this, selector); }; })(); // --- Skript starten --- function init() { console.log("Kleinanzeigen-Skript: Initialisiere..."); // Stelle sicher, dass der Button nur einmal erstellt wird if (!document.getElementById('element-picker-btn')) { pickerButton = document.createElement('button'); pickerButton.id = 'element-picker-btn'; pickerButton.textContent = 'TO LLM'; pickerButton.title = 'Daten der aktuellen Seite erfassen (Kurz-Klick) / Kontinuierlich sammeln (Langklick)'; document.body.appendChild(pickerButton); // Event Listener für den Button hinzufügen pickerButton.addEventListener('mousedown', handleButtonPress); // Zusätzlicher Listener auf das Dokument, um sicherzustellen, dass `handleButtonRelease` immer gefangen wird, // auch wenn der Mauszeiger den Button verlässt, bevor die Maustaste losgelassen wird. document.addEventListener('mouseup', handleButtonRelease); // Globaler Listener, kein 'once' } else { pickerButton = document.getElementById('element-picker-btn'); console.log("Kleinanzeigen-Skript: Button existiert bereits, überspringe Erstellung."); } // Lade den gespeicherten Modus-Status isLockedMode = GM_getValue(STORAGE_KEY_LOCKED_MODE, false); if (isLockedMode) { // Re-aktiviere den Locked Mode beim Laden, falls er aktiv war console.log("Kleinanzeigen-Skript: Locked Mode wurde beim Laden re-aktiviert."); pickerButton.textContent = 'STOP'; // Text anpassen pickerButton.classList.add('locked'); // Visuell anzeigen allCollectedData = []; // Sicherstellen, dass die Daten von der letzten Session nicht übernommen werden processNewPage(); // Starte die Datensammlung für die aktuelle Seite observeDOMChanges(); // Observer re-aktivieren updateStatusDisplay(); } console.log("Kleinanzeigen-Skript: Initialisierung abgeschlossen."); } // Sicherstellen, dass das Skript läuft, wenn das DOM bereit ist if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();