// ==UserScript==
// @name Marketplace Deal Finder
// @name:de Marketplace Deal Finder
// @namespace https://github.com/marmoris-x/tampermonkey-scripts
// @version 31.0.30
// @author marmoris
// @description Multi-provider AI deal aggregator for Willhaben & Kleinanzeigen. Supports Gemini, OpenAI, DeepSeek, Claude, OpenRouter & Portkey.
// @description:de Multi-Provider KI-Deal-Aggregator fΓΌr Willhaben und Kleinanzeigen. UnterstΓΌtzt Gemini, OpenAI, DeepSeek, Claude, OpenRouter und Portkey.
// @license MIT
// @icon https://i.imgur.com/oQmtRjQ.png
// @homepageURL https://github.com/marmoris-x/tampermonkey-scripts
// @supportURL https://github.com/marmoris-x/tampermonkey-scripts/issues
// @downloadURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Marketplace%20Deal%20Finder.user.js
// @updateURL https://raw.githubusercontent.com/marmoris-x/tampermonkey-scripts/main/dist/Marketplace%20Deal%20Finder.user.js
// @match https://www.willhaben.at/iad/kaufen-und-verkaufen/*
// @match https://www.kleinanzeigen.de/s-*
// @match https://www.kleinanzeigen.de/z-*
// @require https://raw.githubusercontent.com/Tampermonkey/utils/3b32b826e84ccc99a0a3e3d8d6e5ce0fa9834f23/requires/gh_2215_make_GM_xhr_more_parallel_again.js#sha256=qo7Imc1jg2AryApKESDzER/AO92l55w1dH7+xsxrx2I=
// @sandbox raw
// @tag search
// @connect willhaben.at
// @connect kleinanzeigen.de
// @connect generativelanguage.googleapis.com
// @connect api.openai.com
// @connect api.deepseek.com
// @connect api.anthropic.com
// @connect openrouter.ai
// @connect api.portkey.ai
// @grant GM.getValue
// @grant GM.setValue
// @grant GM_addElement
// @grant GM_xmlhttpRequest
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
function createLogger(prefix, debugMode) {
debugMode = debugMode || false;
const tag = "[" + prefix + "]";
return {
log: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.log.apply(console, args);
},
warn: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.warn.apply(console, args);
},
error: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.error.apply(console, args);
},
info: function() {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.info.apply(console, args);
},
debug: function() {
if (debugMode) {
const args = [tag];
for (let i = 0; i < arguments.length; i++) args.push(arguments[i]);
console.debug.apply(console, args);
}
}
};
}
const PROVIDER_TYPES = {
GEMINI: "gemini",
OPENAI: "openai",
DEEPSEEK: "deepseek",
CLAUDE: "claude",
OPENROUTER: "openrouter",
PORTKEY: "portkey"
};
const INITIAL_BATCH_SIZE = 4;
const SETTINGS_VERSION = 3;
const MAX_CACHE_SIZE = 100;
const REQUEST_TIMEOUT = 6e4;
const RE_RANK_MAX_DEALS = 30;
const PAUSE_POLL_INTERVAL = 500;
const SAME_PAGE_INCREMENT = 0;
const NEW_PAGE_INCREMENT = 1;
const MAX_INIT_RETRIES = 5;
const MIN_TITLE_LENGTH = 5;
const GEMINI_API_TIMEOUT = 6e4;
const RATE_LIMIT_MAX_RETRIES = 5;
const RETRY_BASE_DELAY = 2e3;
const MAX_RATE_LIMIT_DELAY = 3e5;
const JITTER_FACTOR = 0.2;
const MAX_OUTPUT_TOKENS = 32e3;
const PROVIDER_MAX_TOKENS = {
gemini: 8192,
deepseek: 8192,
openai: 16384,
claude: 32e3,
openrouter: 16384,
portkey: 16384
};
function getMaxTokensForProvider(providerType) {
return PROVIDER_MAX_TOKENS[providerType] || MAX_OUTPUT_TOKENS;
}
const DESCRIPTION_FETCH_DELAY = 1e3;
const DESCRIPTION_MAX_RETRIES = 2;
const DESCRIPTION_BACKOFF_FACTOR = 2;
const PAGE_TRANSITION_DELAY = 1500;
const MODEL_PRESETS = {
gemini: [
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite", icon: "π‘", desc: "Most economical, high thinking", options: { thinkingBudget: -1 } },
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite", icon: "β‘", desc: "Latest flash, high thinking", options: { thinkingLevel: "high" } },
{ id: "gemini-3-flash-preview", label: "Gemini 3 Flash", icon: "π₯", desc: "Preview, high thinking", options: { thinkingLevel: "high" } },
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro", icon: "π§ ", desc: "Max intelligence, preview" }
],
openai: [
{ id: "gpt-5.4-nano", label: "GPT-5.4 Nano", icon: "π‘", desc: "Cheapest, high thinking", options: { reasoning_effort: "high" } },
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini", icon: "β‘", desc: "Fast & cheap" },
{ id: "gpt-5.5", label: "GPT-5.5", icon: "π§ ", desc: "Flagship intelligence" }
],
deepseek: [
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", icon: "β‘", desc: "Fast 284B, max thinking", options: { reasoning_effort: "max", skip_response_format: true } },
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro", icon: "π§ ", desc: "1.6T params, max thinking", options: { reasoning_effort: "max", skip_response_format: true } }
],
claude: [
{ id: "claude-opus-4-8", label: "Claude Opus 4.8", icon: "π§ ", desc: "Latest flagship, adaptive thinking", options: { thinking: { type: "adaptive" } } },
{ id: "claude-opus-4-7", label: "Claude Opus 4.7", icon: "π§ ", desc: "Flagship, adaptive thinking", options: { thinking: { type: "adaptive" } } },
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", icon: "π―", desc: "Best balance, extended thinking", options: { thinking: { type: "enabled", budget_tokens: 8e3 } } },
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", icon: "β‘", desc: "Fast & cheap" }
],
openrouter: [
{ id: "google/gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite", icon: "π‘", desc: "Cheap via OR" },
{ id: "deepseek/deepseek-v4-flash", label: "DeepSeek V4 Flash", icon: "β‘", desc: "Cheap via OR" },
{ id: "openai/gpt-5.4-nano", label: "GPT-5.4 Nano", icon: "π‘", desc: "Cheapest smart model via OR" }
],
portkey: [
{ id: "gpt-5.4-nano", label: "GPT-5.4 Nano", icon: "π‘", desc: "Via Portkey config" },
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", icon: "β‘", desc: "Via Portkey config" },
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash", icon: "β‘", desc: "Via Portkey config", options: { skip_response_format: true } }
]
};
const Logger$9 = createLogger("MDF-WH");
function findAds$1() {
const adSelectors = [
'a[data-testid^="search-result-entry-header-"]',
'article[data-testid^="search-result-entry-"]',
'[data-testid*="search-result-entry"]'
];
for (let si = 0; si < adSelectors.length; si++) {
const entries = document.querySelectorAll(adSelectors[si]);
if (entries.length > 0) {
Logger$9.log("Found " + entries.length + " ads (selector: " + adSelectors[si] + ")");
return { adEntries: entries };
}
}
const uniqueUrls = new Set();
const uniqueAds = [];
const urlRegex = /\/iad\/kaufen-und-verkaufen\/.*\/\d+/;
document.querySelectorAll('a[href*="/iad/kaufen-und-verkaufen/"]').forEach(function(link) {
const url = link.href;
if (urlRegex.test(url) && !uniqueUrls.has(url)) {
uniqueUrls.add(url);
const container = link.closest('article, div[class*="box"], [data-testid*="search-result"], .ad-item, .list-item');
uniqueAds.push(container || link);
}
});
if (uniqueAds.length > 0) {
Logger$9.log("Found " + uniqueAds.length + " ads (fallback method)");
return { adEntries: uniqueAds };
}
return null;
}
function extractBasicInfo$1(ad) {
let title = "Title not available";
["h3", "h2", '[data-testid*="title"]'].forEach(function(s) {
const el = ad.querySelector(s);
if (el) {
const text = el.textContent.trim();
if (text.length > MIN_TITLE_LENGTH && !isPriceOnlyText$1(text)) title = text;
}
});
let price = "Price not available";
var priceSelectors = [
'[data-testid*="price"]',
'[class*="Price"]',
'[class*="price"]',
'[data-testid*="Price"]',
".AdSummaryI__PriceText"
];
var priceEl = null;
for (var psi = 0; psi < priceSelectors.length; psi++) {
priceEl = ad.querySelector(priceSelectors[psi]);
if (priceEl) {
var pt = priceEl.textContent.trim();
if (pt && pt.length < 20 && (pt.indexOf("β¬") !== -1 || pt.indexOf("EUR") !== -1)) {
price = pt;
break;
}
}
}
if (!priceEl) {
const spans = ad.querySelectorAll("span, div, p");
for (let pi = 0; pi < spans.length; pi++) {
const text = spans[pi].textContent.trim();
if ((text.indexOf("β¬") !== -1 || text.indexOf("EUR") !== -1) && text.length < 20 && text.indexOf("...") === -1) {
price = text;
break;
}
}
}
const url = ad.href || (ad.querySelector('a[href*="/iad/"]') ? ad.querySelector('a[href*="/iad/"]').href : "URL not available");
return { title, price, url };
}
function isPriceOnlyText$1(text) {
return /^\s*[\d.,]+\s*β¬?\s*(VB)?\s*$/i.test(text);
}
function descSelectors$1() {
return [
'[data-testid="ad-description-Beschreibung"]',
'[data-testid*="description"]',
".ad-description",
'[class*="description"]'
];
}
function goToNextPage$1(currentPage) {
let nextButton = document.querySelector('[data-testid="pagination-bottom-next-button"]');
if (!nextButton) {
const targetPage = currentPage + 1;
const paginationLinks = document.querySelectorAll('[data-testid*="pagination"] a, nav a');
for (let li = 0; li < paginationLinks.length; li++) {
const btn = paginationLinks[li];
const text = (btn.textContent || "").trim();
const href = btn.getAttribute("href");
if (text && (text === String(targetPage) || text.toLowerCase().indexOf("weiter") !== -1 || text.toLowerCase().indexOf("next") !== -1 || text === "βΊ" || text === ">")) {
if (!btn.hasAttribute("disabled") && btn.getAttribute("aria-disabled") !== "true" && href) {
nextButton = btn;
break;
}
}
}
}
if (nextButton) {
const isDisabled = nextButton.hasAttribute("disabled");
const ariaDisabled = nextButton.getAttribute("aria-disabled") === "true";
const href = nextButton.getAttribute("href");
Logger$9.log("Next button disabled:", isDisabled, "| aria-disabled:", ariaDisabled, "| href:", href);
if (!isDisabled && !ariaDisabled && !href) {
try {
var url = new URL(location.href);
var nextPage = currentPage + 1;
url.searchParams.set("page", String(nextPage));
var nextUrl = url.pathname + url.search;
Logger$9.log("Constructed next page URL from params:", nextUrl);
return nextUrl;
} catch (e) {
Logger$9.warn("Failed to construct next page URL:", e);
return false;
}
}
if (!isDisabled && !ariaDisabled && href) {
try {
if (new URL(href, location.href).href === location.href) {
Logger$9.log("Next button points to same page - skipped");
return false;
}
} catch (e) {
Logger$9.warn("Invalid URL in next button:", href, e);
return false;
}
return href;
}
Logger$9.log("Next button not usable");
}
var hasPaginationContainer = document.querySelector('[data-testid*="pagination"], nav, [class*="pagination"]');
if (!hasPaginationContainer) {
Logger$9.warn("No pagination container found β markup may have changed");
} else {
Logger$9.log("Pagination container found but no next link β likely last page");
}
return false;
}
const whScraper = Object.freeze( Object.defineProperty({
__proto__: null,
descSelectors: descSelectors$1,
extractBasicInfo: extractBasicInfo$1,
findAds: findAds$1,
goToNextPage: goToNextPage$1
}, Symbol.toStringTag, { value: "Module" }));
const Logger$8 = createLogger("MDF-KA");
function findAds() {
const adSelectors = ["article[data-adid]", "li.ad-listitem", ".aditem"];
for (let si = 0; si < adSelectors.length; si++) {
const entries = document.querySelectorAll(adSelectors[si]);
if (entries.length > 0) {
Logger$8.log("Found " + entries.length + " ads (selector: " + adSelectors[si] + ")");
return { adEntries: entries };
}
}
const uniqueUrls = new Set();
const uniqueAds = [];
const urlRegex = /\/s-anzeige\/.*\/\d+/;
document.querySelectorAll('a[href*="/s-anzeige/"]').forEach(function(link) {
const url = link.href;
if (urlRegex.test(url) && !uniqueUrls.has(url)) {
uniqueUrls.add(url);
const container = link.closest("article, li, .aditem, .ad-listitem, [data-adid]");
uniqueAds.push(container || link);
}
});
if (uniqueAds.length > 0) {
Logger$8.log("Found " + uniqueAds.length + " ads (fallback method)");
return { adEntries: uniqueAds };
}
return null;
}
function extractBasicInfo(ad) {
let title = "Title not available";
["h2", "h3", 'a[class*="ellipsis"]', '[class*="title"]'].forEach(function(s) {
const el = ad.querySelector(s);
if (el) {
const text = el.textContent.trim();
if (text.length > MIN_TITLE_LENGTH && !isPriceOnlyText(text)) title = text;
}
});
let price = "Price not available";
var priceSelectors = [
".aditem-main--middle--price-shipping--price",
'[class*="aditem-main--price"]',
".aditem-main--price",
"[data-price]",
'[class*="aditem-price"]'
];
var priceEl = null;
for (var psi = 0; psi < priceSelectors.length; psi++) {
priceEl = ad.querySelector(priceSelectors[psi]);
if (priceEl) {
var pt = priceEl.textContent.trim();
if (pt && pt.length < 30 && (pt.indexOf("β¬") !== -1 || /^(\d[\d.,]*\s*β¬?\s*)?VB$/i.test(pt))) {
price = pt;
break;
}
}
}
if (!priceEl) {
const spans = ad.querySelectorAll("span, div, p, strong");
for (let pi = 0; pi < spans.length; pi++) {
const text = spans[pi].textContent.trim();
if ((text.indexOf("β¬") !== -1 || text.indexOf("EUR") !== -1 || /^(\d[\d.,]*\s*β¬?\s*)?VB$/i.test(text.trim())) && text.length < 30 && text.indexOf("...") === -1) {
price = text;
break;
}
}
}
let url = ad.getAttribute("data-href") || ad.href || (ad.querySelector('a[href*="/s-anzeige/"]') ? ad.querySelector('a[href*="/s-anzeige/"]').href : "URL not available");
if (url && url.indexOf("/") === 0) {
url = "https://www.kleinanzeigen.de" + url;
}
return { title, price, url };
}
function isPriceOnlyText(text) {
return /^\s*[\d.,]+\s*β¬?\s*(VB)?\s*$/i.test(text);
}
function descSelectors() {
return [
"#viewad-description-text",
".ad-description",
'div[class*="description"]',
'[class*="description"]'
];
}
function goToNextPage(currentPage) {
let nextButton = document.querySelector('a[class*="pagination-next"]');
if (!nextButton) {
const paginationLinks = document.querySelectorAll('[class*="pagination"] a, nav a, .pagination a');
for (let li = 0; li < paginationLinks.length; li++) {
const linkEl = paginationLinks[li];
const text = (linkEl.textContent || "").trim().toLowerCase();
const href = linkEl.getAttribute("href");
if ((text === "weiter" || text === ">" || text === "βΊ") && href && href.indexOf("seite:") !== -1) {
nextButton = linkEl;
break;
}
}
}
if (!nextButton) {
const targetPage = currentPage + 1;
const seiteLinks = document.querySelectorAll('a[href*="seite:"]');
for (let sl = 0; sl < seiteLinks.length; sl++) {
const href = seiteLinks[sl].getAttribute("href");
if (href && href.indexOf("seite:" + targetPage) !== -1) {
nextButton = seiteLinks[sl];
break;
}
}
}
if (nextButton) {
const href = nextButton.getAttribute("href");
Logger$8.log("Next button href:", href);
if (href) {
try {
if (new URL(href, location.href).href === location.href) {
Logger$8.log("Next button points to same page - skipped");
return false;
}
} catch (e) {
Logger$8.warn(" Invalid URL in next button:", href, e);
return false;
}
return href;
}
Logger$8.log("Next button has no href");
}
var hasPaginationContainer = document.querySelector('[class*="pagination"], nav, .pagination');
if (!hasPaginationContainer) {
Logger$8.warn("No pagination container found β markup may have changed");
} else {
Logger$8.log("Pagination container found but no next link β likely last page");
}
return false;
}
const kaScraper = Object.freeze( Object.defineProperty({
__proto__: null,
descSelectors,
extractBasicInfo,
findAds,
goToNextPage
}, Symbol.toStringTag, { value: "Module" }));
function getScraper() {
const isWH = window.location.hostname.includes("willhaben.at");
return {
...isWH ? whScraper : kaScraper,
siteName: isWH ? "WILLHABEN" : "KLEINANZEIGEN",
storagePrefix: isWH ? "wh" : "ka",
buttonGradient: isWH ? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)" : "linear-gradient(135deg, #86a542 0%, #2d2d2d 100%)"
};
}
const Logger$7 = createLogger("MDF Storage");
async function loadSetting(key, defaultValue) {
try {
const raw = await GM.getValue(key);
if (raw === void 0 || raw === null) return defaultValue;
return raw;
} catch (e) {
return defaultValue;
}
}
async function saveSetting(key, value) {
try {
await GM.setValue(key, value);
} catch (e) {
Logger$7.error("Failed to save setting:", key, e.message || e);
}
}
async function saveCrawlState(state, storagePrefix) {
await saveSetting(storagePrefix + "_dealfinder_crawl_state", JSON.stringify(state));
}
async function loadCrawlState(storagePrefix) {
const saved = await loadSetting(storagePrefix + "_dealfinder_crawl_state", null);
if (!saved) return null;
try {
return typeof saved === "string" ? JSON.parse(saved) : saved;
} catch (e) {
await saveSetting(storagePrefix + "_dealfinder_crawl_state", null);
return null;
}
}
async function clearCrawlState(storagePrefix) {
await saveSetting(storagePrefix + "_dealfinder_crawl_state", null);
}
async function saveResults(results, storagePrefix) {
await saveSetting(storagePrefix + "_dealfinder_results", JSON.stringify(results));
}
async function loadResults(storagePrefix) {
const saved = await loadSetting(storagePrefix + "_dealfinder_results", null);
if (!saved) return null;
try {
return typeof saved === "string" ? JSON.parse(saved) : saved;
} catch (e) {
await saveSetting(storagePrefix + "_dealfinder_results", null);
return null;
}
}
async function clearResults(storagePrefix) {
await saveSetting(storagePrefix + "_dealfinder_results", null);
}
function deepCopySettings(settings) {
if (!settings) return settings;
const copy = { ...settings };
if (copy.provider) {
copy.provider = { ...copy.provider };
if (copy.provider.options) copy.provider.options = { ...copy.provider.options };
}
if (copy.providers) {
copy.providers = { ...copy.providers };
Object.keys(copy.providers).forEach(function(k) {
copy.providers[k] = { ...copy.providers[k] };
if (copy.providers[k].options) copy.providers[k].options = { ...copy.providers[k].options };
});
}
return copy;
}
const Logger$6 = createLogger("MDF Settings");
const DEFAULT_SETTINGS = {
version: SETTINGS_VERSION,
currentProvider: PROVIDER_TYPES.GEMINI,
providers: {
gemini: { type: "gemini", apiKey: "", modelId: "gemini-2.5-flash-lite", baseUrl: "", options: {} },
openai: { type: "openai", apiKey: "", modelId: "gpt-5.4-nano", baseUrl: "", options: {} },
deepseek: { type: "deepseek", apiKey: "", modelId: "deepseek-v4-flash", baseUrl: "", options: {} },
claude: { type: "claude", apiKey: "", modelId: "claude-sonnet-4-6", baseUrl: "", options: {} },
openrouter: { type: "openrouter", apiKey: "", modelId: "google/gemini-3.1-flash-lite", baseUrl: "", options: {} },
portkey: { type: "portkey", apiKey: "", modelId: "gpt-5.4-nano", baseUrl: "", options: {} }
},
searchContext: "",
topX: 3,
maxPages: 10
};
function isV1Settings(settings) {
if (!settings || typeof settings !== "object") return false;
return settings.version === 1 || typeof settings.apiKey === "string" && !settings.provider && !settings.providers;
}
function isV2Settings(settings) {
if (!settings || typeof settings !== "object") return false;
return settings.version === 2 || settings.provider && !settings.providers;
}
function migrateV1toV2(v1) {
const modelMap = v1.modelMapping || {};
const modelKey = v1.model || "flash";
const modelId = modelMap[modelKey] || "gemini-2.5-flash";
return {
version: 2,
provider: {
type: PROVIDER_TYPES.GEMINI,
apiKey: v1.apiKey || "",
modelId,
baseUrl: "",
options: {}
},
searchContext: v1.searchContext || "",
topX: v1.topX ?? 3,
maxPages: v1.maxPages ?? 10
};
}
function migrateV2toV3(v2) {
const type = v2.provider ? v2.provider.type : PROVIDER_TYPES.GEMINI;
const v3 = {
version: SETTINGS_VERSION,
currentProvider: type,
providers: {
gemini: { type: "gemini", apiKey: "", modelId: "gemini-2.5-flash-lite", baseUrl: "", options: {} },
openai: { type: "openai", apiKey: "", modelId: "gpt-5.4-nano", baseUrl: "", options: {} },
deepseek: { type: "deepseek", apiKey: "", modelId: "deepseek-v4-flash", baseUrl: "", options: {} },
claude: { type: "claude", apiKey: "", modelId: "claude-sonnet-4-6", baseUrl: "", options: {} },
openrouter: { type: "openrouter", apiKey: "", modelId: "google/gemini-3.1-flash-lite", baseUrl: "", options: {} },
portkey: { type: "portkey", apiKey: "", modelId: "gpt-5.4-nano", baseUrl: "", options: {} }
},
searchContext: v2.searchContext || "",
topX: v2.topX ?? 3,
maxPages: v2.maxPages ?? 10
};
if (v2.provider) {
const p = v2.provider;
if (v3.providers[p.type]) {
v3.providers[p.type] = { ...p };
}
}
return v3;
}
async function loadSettings(storagePrefix, cachedSettings) {
if (cachedSettings != null) {
const copy = deepCopySettings(cachedSettings);
if (copy.providers && copy.currentProvider) {
copy.provider = copy.providers[copy.currentProvider] || {};
}
return { settings: copy, cachedSettings };
}
const raw = await loadSetting(storagePrefix + "_dealfinder_settings", null);
if (!raw) {
const defaults = deepCopySettings(DEFAULT_SETTINGS);
if (defaults.providers && defaults.currentProvider) {
defaults.provider = defaults.providers[defaults.currentProvider] || {};
}
return { settings: deepCopySettings(defaults), cachedSettings: defaults };
}
try {
let parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
const needsMigration = isV1Settings(parsed) || isV2Settings(parsed);
if (isV1Settings(parsed)) parsed = migrateV1toV2(parsed);
if (isV2Settings(parsed)) parsed = migrateV2toV3(parsed);
const merged = {
...DEFAULT_SETTINGS,
...parsed,
providers: { ...DEFAULT_SETTINGS.providers, ...parsed.providers || {} }
};
if (needsMigration) {
const toStore = { ...merged };
delete toStore.provider;
await saveSetting(storagePrefix + "_dealfinder_settings", JSON.stringify(toStore));
}
const cs = deepCopySettings(merged);
cs.provider = cs.providers[cs.currentProvider] || {};
const copy = deepCopySettings(cs);
copy.provider = copy.providers[copy.currentProvider] || {};
return { settings: copy, cachedSettings: cs };
} catch (e) {
Logger$6.warn("Corrupted settings storage, resetting to defaults");
await saveSetting(storagePrefix + "_dealfinder_settings", null);
const defs = deepCopySettings(DEFAULT_SETTINGS);
defs.provider = defs.providers[defs.currentProvider] || {};
return { settings: deepCopySettings(defs), cachedSettings: defs };
}
}
async function saveSettings(storagePrefix, settings) {
const toStore = { ...settings, providers: { ...settings.providers } };
if (toStore.provider && toStore.currentProvider && toStore.providers) {
toStore.providers[toStore.currentProvider] = { ...toStore.provider };
}
delete toStore.provider;
await saveSetting(storagePrefix + "_dealfinder_settings", JSON.stringify(toStore));
}
const S = {
isRunning: false,
isPaused: false,
shouldStop: false,
captchaPaused: false,
finished: false,
allTopDeals: [],
currentPage: 1,
initRetries: 0,
descriptionCache: new Map(),
cachedSettings: null,
scraper: null,
abortController: null,
uiRoot: null,
uiContent: null
};
function setRunning(val) {
S.isRunning = val;
}
function setCachedSettings(val) {
S.cachedSettings = val;
}
function setScraper(val) {
S.scraper = val;
}
function waitForElement(selector, timeout, root) {
timeout = timeout || 1e4;
root = root || document.body;
return new Promise(function(resolve, reject) {
const existing = root.querySelector(selector);
if (existing) return resolve(existing);
let timer;
const observer = new MutationObserver(function() {
const found = root.querySelector(selector);
if (found) {
cleanup();
resolve(found);
}
});
function cleanup() {
observer.disconnect();
if (timer) clearTimeout(timer);
}
observer.observe(root, { childList: true, subtree: true });
if (timeout > 0) {
timer = setTimeout(function() {
cleanup();
reject(new Error("waitForElement timeout: " + selector));
}, timeout);
}
});
}
function createShadowContainer(opts) {
opts = opts || {};
const host = document.createElement(opts.tag || "div");
if (opts.id) host.id = opts.id;
if (opts.className) host.className = opts.className;
const root = host.attachShadow({ mode: "closed" });
if (opts.styles) {
const style = document.createElement("style");
style.textContent = opts.styles;
root.appendChild(style);
}
const content = document.createElement("div");
content.id = "content";
root.appendChild(content);
document.body.appendChild(host);
return { host, root, content };
}
function createToast(message, opts) {
opts = opts || {};
const duration = opts.duration || 3e3;
const type = opts.type || "info";
const colors = { info: "#2196F3", success: "#4CAF50", error: "#F44336", warn: "#FF9800" };
const toast = document.createElement("div");
const root = toast.attachShadow({ mode: "closed" });
const toastStyle = document.createElement("style");
toastStyle.textContent = [
":host { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:2147483647;",
"background:" + (colors[type] || colors.info) + "; color:#fff; padding:10px 20px; border-radius:6px;",
"font:13px/1.4 system-ui,sans-serif; box-shadow:0 4px 12px rgba(0,0,0,0.3);",
"opacity:0; transition:opacity 0.3s ease; pointer-events:none; max-width:80vw; }",
":host(.show) { opacity:1; }"
].join("");
root.appendChild(toastStyle);
const span = document.createElement("span");
span.textContent = message;
root.appendChild(span);
document.body.appendChild(toast);
requestAnimationFrame(function() {
toast.classList.add("show");
});
setTimeout(function() {
toast.classList.remove("show");
setTimeout(function() {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 300);
}, duration);
return toast;
}
function esc(str) {
if (!str) return "";
return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
}
function renderSettingsView(prefix, settings, savedResults, siteName) {
const provider = settings.provider || {};
const providerType = provider.type || PROVIDER_TYPES.GEMINI;
let displayContext = settings.searchContext || "";
if (!displayContext) {
const urlParams = new URLSearchParams(window.location.search);
const keyword = urlParams.get("keyword");
if (keyword) {
displayContext = keyword;
} else if (siteName === "KLEINANZEIGEN") {
const pathMatch = window.location.pathname.match(/\/s-([^/]+)/);
if (pathMatch) displayContext = decodeURIComponent(pathMatch[1].replace(/-/g, " "));
}
}
let savedTs = "";
if (savedResults && savedResults.timestamp) {
const ts = savedResults.timestamp;
savedTs = ts.indexOf("T") !== -1 ? new Date(ts).toLocaleString("de-DE") : ts;
}
const providerOptions = Object.values(PROVIDER_TYPES).map(function(type) {
const labels = {
gemini: "Google Gemini",
openai: "OpenAI",
deepseek: "DeepSeek",
claude: "Anthropic Claude",
openrouter: "OpenRouter",
portkey: "Portkey"
};
const selected = type === providerType ? " selected" : "";
return '";
}).join("\n");
const presets = MODEL_PRESETS[providerType] || [];
const presetButtons = presets.map(function(preset) {
const active = preset.id === provider.modelId ? " background:#6366f1; color:#fff; border-color:#6366f1;" : "";
const optionsJson = preset.options ? esc(JSON.stringify(preset.options)) : "";
return '";
}).join("");
return [
'
',
'
',
'
Deal Finder
',
'',
"",
savedResults ? '
' + savedResults.deals.length + ' gespeicherte Deals
Analysierte Seiten: ' + savedResults.pages + " | " + savedTs + '
' : "",
'
',
'',
'",
"
",
'
',
'',
'',
"
",
providerType === PROVIDER_TYPES.PORTKEY ? '
Config-ID aus dem Portkey-Dashboard (pc-...)
' : "",
'
',
'
',
'
',
presetButtons ? '
' + presetButtons + "
" : "",
"
",
'
',
'',
'',
"
",
'
',
'',
'",
"
",
'
',
'',
'',
'Anzahl der besten Deals, die die AI pro Seite auswaehlt (1-10)',
"
",
'
',
'',
'',
"
",
'
",
'
',
'
Live Top-Deals
',
'
',
"
",
'
',
'',
'',
'',
"
"
].join("\n");
}
function renderResultsView(prefix, deals) {
const sortedDeals = (deals || []).slice().sort(function(a, b) {
return (b && b.score || 0) - (a && a.score || 0);
});
const items = sortedDeals.map(function(deal, index) {
const safeUrl = deal.url && deal.url.startsWith("https://") ? deal.url : "#";
const safeScore = Number.isFinite(Number(deal.score)) ? Math.min(100, Math.max(0, Number(deal.score))) : null;
const scoreBar = safeScore !== null ? 'Score: ' + safeScore + '/100
= 40 ? "#ffc107" : "#dc3545") + ";height:100%;width:" + safeScore + '%;">
' : "";
const medal = index === 0 ? "#1" : index === 1 ? "#2" : index === 2 ? "#3" : "#" + (index + 1);
const borderColor = index === 0 ? "#ffc107" : index === 1 ? "#28a745" : index === 2 ? "#17a2b8" : "#6c757d";
const bgColor = index === 0 ? "#fff8e1" : "#f8f9fa";
return [
'',
'
',
'
' + medal + " " + esc(deal.title) + "
",
'
S.' + deal.page + "
",
"
",
'
' + esc(deal.price) + "
",
scoreBar,
'
' + esc(deal.reasoning || "Keine Begruendung verfuegbar") + "
",
deal.description ? '
' + esc(deal.description.substring(0, 150)) + (deal.description.length > 150 ? "..." : "") + "
" : "",
'
Anzeige oeffnen',
"
"
].join("\n");
}).join("\n");
return [
'',
'
',
'
Top-Deals
',
'',
"",
'
',
'
' + deals.length + "
",
'
Top-Deals gefunden
',
"
",
'
' + items + "
",
'
',
'',
'',
'',
'',
"
",
'
',
"
"
].join("\n");
}
const SIDEBAR_WIDTH = "400px";
const SHADOW_STYLES = [
":host { all: initial; contain: strict; isolation: isolate; }",
":host { display: none; position: fixed; top: 0; right: 0; width: 400px; height: 100vh;",
" background: white; z-index: 999999; box-shadow: -5px 0 20px rgba(0,0,0,0.2);",
" overflow-y: auto; transition: transform 0.3s ease;",
" font-family: system-ui, -apple-system, sans-serif;",
" pointer-events: auto; }",
":host([open]) { display: block; }"
].join("\n");
function createModal(prefix) {
var modalId = prefix + "-dealfinder-modal";
if (document.getElementById(modalId)) return;
var container = createShadowContainer({
tag: "div",
id: modalId,
styles: SHADOW_STYLES
});
S.uiRoot = container.root;
S.uiContent = container.content;
}
function openModal(prefix) {
var modal = document.getElementById(prefix + "-dealfinder-modal");
var floatBtn = document.getElementById(prefix + "-dealfinder-btn");
if (modal) modal.setAttribute("open", "");
if (floatBtn) floatBtn.style.display = "none";
document.documentElement.style.transition = "margin-right 0.3s ease";
document.documentElement.style.marginRight = SIDEBAR_WIDTH;
}
function closeModal(prefix, isRunning) {
if (isRunning) {
var closeBtn = S.uiRoot ? S.uiRoot.getElementById(prefix + "-close-btn-x") : null;
if (closeBtn) {
closeBtn.style.color = "#dc3545";
closeBtn.title = "Crawl laeuft - erst stoppen";
setTimeout(function() {
closeBtn.style.color = "#999";
closeBtn.title = "";
}, 1e3);
}
return;
}
var modal = document.getElementById(prefix + "-dealfinder-modal");
var floatBtn = document.getElementById(prefix + "-dealfinder-btn");
if (modal) modal.removeAttribute("open");
if (floatBtn) floatBtn.style.display = "block";
document.documentElement.style.marginRight = "";
}
function createDealFinderButton(prefix, gradient) {
var buttonId = prefix + "-dealfinder-btn";
if (document.getElementById(buttonId)) return;
var button = document.createElement("button");
button.id = buttonId;
button.textContent = "Deal Finder";
button.style.cssText = [
"position: fixed; top: 140px; right: 0; z-index: 99999;",
"padding: 12px 16px; background: " + gradient + ";",
"color: white; border: none; border-radius: 8px 0 0 8px; cursor: pointer;",
"box-shadow: -3px 3px 12px rgba(0,0,0,0.25); font-size: 15px; font-weight: bold;",
"transition: padding-right 0.2s ease, box-shadow 0.2s ease;"
].join(" ");
button.addEventListener("mouseenter", function() {
button.style.paddingRight = "22px";
button.style.boxShadow = "-5px 4px 18px rgba(0,0,0,0.35)";
});
button.addEventListener("mouseleave", function() {
button.style.paddingRight = "16px";
button.style.boxShadow = "-3px 3px 12px rgba(0,0,0,0.25)";
});
document.body.appendChild(button);
return button;
}
function switchToResultsView(prefix, deals) {
if (!S.uiContent) return;
S.uiContent.innerHTML = renderResultsView(prefix, deals || []);
}
class AIProvider {
constructor(config) {
if (new.target === AIProvider) {
throw new TypeError("AIProvider is abstract β must extend it");
}
this.config = config;
}
getEndpoint() {
throw new Error("Not implemented: getEndpoint() must be overridden");
}
getAuthHeaders() {
throw new Error("Not implemented: getAuthHeaders() must be overridden");
}
buildRequest(prompt, options = {}) {
throw new Error("Not implemented: buildRequest() must be overridden");
}
parseResponse(response) {
throw new Error("Not implemented: parseResponse() must be overridden");
}
isRateLimitError(status) {
return status === 429 || status === 503;
}
isAuthError(status) {
return status === 401 || status === 403;
}
}
const registry = new Map();
function registerProvider(type, providerClass) {
if (!(providerClass.prototype instanceof AIProvider)) {
throw new TypeError("providerClass must extend AIProvider");
}
registry.set(type, providerClass);
}
function createProvider(type, config) {
const ProviderClass = registry.get(type);
if (!ProviderClass) {
throw new Error(`Unknown provider type: "${type}". Available: ${Array.from(registry.keys()).join(", ")}`);
}
return new ProviderClass(config);
}
const Logger$5 = createLogger("MDF API");
function getTimeoutForProvider(providerType) {
return providerType === PROVIDER_TYPES.GEMINI ? GEMINI_API_TIMEOUT : REQUEST_TIMEOUT;
}
function gmRequest(params, signal) {
return new Promise((resolve, reject) => {
if (signal == null ? void 0 : signal.aborted) {
return reject(new DOMException("Aborted", "AbortError"));
}
const details = {
...params,
onload(resp) {
cleanup();
resolve({ status: resp.status, responseText: resp.responseText, responseHeaders: resp.responseHeaders });
},
onerror(err) {
cleanup();
reject(new Error(`GM_xmlhttpRequest failed: ${(err == null ? void 0 : err.finalUrl) || (err == null ? void 0 : err.status) || "network error"}`));
},
ontimeout() {
cleanup();
reject(new Error(`GM_xmlhttpRequest timed out after ${params.timeout || REQUEST_TIMEOUT}ms`));
}
};
let handle = null;
const cleanup = () => {
if (signal) signal.removeEventListener("abort", abortHandler);
};
const abortHandler = () => {
cleanup();
try {
if (handle && handle.abort) handle.abort();
} catch (e) {
}
reject(new DOMException("Aborted", "AbortError"));
};
if (signal) {
signal.addEventListener("abort", abortHandler, { once: true });
}
try {
handle = GM_xmlhttpRequest(details);
} catch (err) {
reject(new Error(`GM_xmlhttpRequest threw: ${err.message}`));
}
});
}
function buildRequestBody(provider, prompt, userOptions = {}) {
return provider.buildRequest(prompt, userOptions);
}
function calculateBackoff(retryCount, baseDelay = RETRY_BASE_DELAY) {
const exponential = baseDelay * Math.pow(2, retryCount);
const capped = Math.min(exponential, MAX_RATE_LIMIT_DELAY);
const jitter = capped * JITTER_FACTOR * (Math.random() * 2 - 1);
return Math.round(capped + jitter);
}
function cleanAIJson(text) {
let cleaned = (text || "").trim();
if (!cleaned) return cleaned;
if (cleaned.startsWith("```")) {
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
cleaned = cleaned.trim();
}
const firstBrace = cleaned.indexOf("{");
if (firstBrace === -1) return cleaned;
let depth = 0, inString = false, escaped = false, matchEnd = -1;
for (let i = firstBrace; i < cleaned.length; i++) {
const ch = cleaned[i];
if (escaped) {
escaped = false;
continue;
}
if (ch === "\\") {
escaped = true;
continue;
}
if (ch === '"') {
inString = !inString;
continue;
}
if (inString) continue;
if (ch === "{") depth++;
else if (ch === "}") {
depth--;
if (depth === 0) {
matchEnd = i + 1;
break;
}
}
}
if (matchEnd > 0) cleaned = cleaned.substring(firstBrace, matchEnd);
cleaned = cleaned.replace(/,(\s*[}\]])/g, "$1");
return cleaned;
}
async function callAI(prompt, settings, options = {}) {
var _a, _b;
const provider = createProvider(settings.providerType, {
type: settings.providerType,
apiKey: settings.apiKey,
modelId: settings.modelId,
baseUrl: settings.baseUrl,
providerOptions: settings.providerOptions
});
const endpoint = provider.getEndpoint();
const headers = provider.getAuthHeaders();
const body = buildRequestBody(provider, prompt, options);
const timeout = getTimeoutForProvider(settings.providerType);
let lastError = null;
for (let attempt = 0; attempt <= RATE_LIMIT_MAX_RETRIES; attempt++) {
if ((_a = options.signal) == null ? void 0 : _a.aborted) {
throw new DOMException("Aborted", "AbortError");
}
try {
const resp = await gmRequest({
method: "POST",
url: endpoint,
headers,
data: JSON.stringify(body),
timeout
}, options.signal);
if (provider.isAuthError(resp.status)) {
throw new Error(`Auth error (${resp.status}): API key rejected for ${settings.providerType}`);
}
if (provider.isRateLimitError(resp.status)) {
lastError = new Error(`Rate limited (${resp.status})`);
if (attempt < RATE_LIMIT_MAX_RETRIES) {
const delay = calculateBackoff(attempt);
if (options.onRetry) {
options.onRetry(attempt + 1, lastError);
}
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw lastError;
}
if (resp.status < 200 || resp.status >= 300) {
throw new Error(`API returned status ${resp.status}: ${(_b = resp.responseText) == null ? void 0 : _b.slice(0, 200)}`);
}
let parsed;
try {
parsed = JSON.parse(resp.responseText);
} catch (parseErr) {
throw new Error(`Failed to parse API response as JSON: ${parseErr.message}`);
}
let text;
try {
text = provider.parseResponse(parsed);
} catch (parseErr) {
throw new Error(`Provider parse error: ${parseErr.message}`);
}
if (!text || text.trim().length === 0) {
throw new Error("Provider returned empty text content");
}
let result;
try {
result = JSON.parse(text);
} catch (parseErr) {
try {
const repaired = cleanAIJson(text);
result = JSON.parse(repaired);
} catch (repairErr) {
Logger$5.warn("Raw AI output (first 500 chars):", (text || "").substring(0, 500));
throw new Error(`Failed to parse provider output as JSON: ${parseErr.message} | Repair error: ${repairErr.message}`);
}
}
if (!result.topDeals || !Array.isArray(result.topDeals)) {
throw new Error("Provider response missing topDeals array");
}
return { topDeals: result.topDeals };
} catch (err) {
if (err.name === "AbortError") throw err;
lastError = err;
if (attempt < RATE_LIMIT_MAX_RETRIES) {
const message = err.message || "";
const isRetryable = /timed out|network error|failed to parse (?:api response|provider output)/i.test(message);
if (isRetryable) {
const delay = calculateBackoff(attempt);
if (options.onRetry) {
options.onRetry(attempt + 1, err);
}
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw lastError;
}
}
}
throw lastError || new Error(`callAI failed after ${RATE_LIMIT_MAX_RETRIES + 1} attempts`);
}
function getValidScore(score) {
const num = Number(score);
return Number.isFinite(num) ? num : null;
}
function isValidScore(score) {
return getValidScore(score) !== null;
}
function normalizeUrl(url) {
if (!url) return url;
return url.split("#")[0];
}
function deduplicateDeals(deals) {
const seen = new Map();
for (let i = 0; i < deals.length; i++) {
const d = deals[i];
const key = normalizeUrl(d.url) || d.url;
if (!seen.has(key)) seen.set(key, d);
}
return Array.from(seen.values());
}
function sortDealsByScore(deals) {
return deals.slice().sort(function(a, b) {
return (getValidScore(b.score) ?? 0) - (getValidScore(a.score) ?? 0);
});
}
function computePriceStats(adsData) {
const prices = [];
for (let ai = 0; ai < adsData.length; ai++) {
const ad = adsData[ai];
const match = (ad.price || "").replace(/\./g, "").replace(/,/g, ".").match(/(\d+(?:\.\d+)?)/);
if (match) {
const p = parseFloat(match[1]);
if (p > 0) prices.push(p);
}
}
if (prices.length === 0) return null;
const sorted = prices.slice().sort(function(a, b) {
return a - b;
});
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
let sum = 0;
for (let si = 0; si < prices.length; si++) sum += prices[si];
const mean = sum / prices.length;
return {
min: sorted[0],
max: sorted[sorted.length - 1],
mean: Math.round(mean),
median: Math.round(median),
count: prices.length
};
}
function updateProgress(prefix, text, percentage, type) {
type = type || "info";
const container = S.uiRoot.getElementById(prefix + "-progress-container");
const progressText = S.uiRoot.getElementById(prefix + "-progress-text");
const progressBar = S.uiRoot.getElementById(prefix + "-progress-bar");
const borderColor = type === "error" ? "#dc3545" : type === "warning" ? "#ffc107" : type === "success" ? "#28a745" : "#667eea";
const textColor = type === "error" ? "#dc3545" : type === "warning" ? "#ffc107" : type === "success" ? "#28a745" : "#333";
const barColor = type === "error" ? "#dc3545" : type === "warning" ? "#ffc107" : type === "success" ? "#28a745" : "#007bff";
if (container) {
container.style.display = "block";
container.style.borderLeftColor = borderColor;
}
if (progressText) {
progressText.textContent = text;
progressText.style.color = textColor;
}
if (progressBar) {
progressBar.style.width = percentage + "%";
progressBar.style.backgroundColor = barColor;
progressBar.style.transition = "width 0.3s ease, background-color 0.3s ease";
}
}
function showWarning(prefix, message, percentage) {
updateProgress(prefix, "Warnung: " + message, percentage || 0, "warning");
}
function resetUI(prefix) {
const startBtn = S.uiRoot.getElementById(prefix + "-start-btn");
const pauseBtn = S.uiRoot.getElementById(prefix + "-pause-btn");
const stopBtn = S.uiRoot.getElementById(prefix + "-stop-btn");
const apiKeyInput = S.uiRoot.getElementById(prefix + "-api-key");
const searchInput = S.uiRoot.getElementById(prefix + "-search-context");
const topXInput = S.uiRoot.getElementById(prefix + "-top-x");
const providerSelect = S.uiRoot.getElementById(prefix + "-provider-select");
const modelIdInput = S.uiRoot.getElementById(prefix + "-model-id");
const baseUrlInput = S.uiRoot.getElementById(prefix + "-base-url");
if (startBtn) startBtn.style.display = "block";
if (pauseBtn) pauseBtn.style.display = "none";
if (stopBtn) stopBtn.style.display = "none";
if (apiKeyInput) apiKeyInput.disabled = false;
if (searchInput) searchInput.disabled = false;
if (topXInput) topXInput.disabled = false;
if (providerSelect) providerSelect.disabled = false;
if (modelIdInput) modelIdInput.disabled = false;
if (baseUrlInput) baseUrlInput.disabled = false;
const liveRanking = S.uiRoot.getElementById(prefix + "-live-ranking");
if (liveRanking) liveRanking.style.display = "none";
const progressContainer = S.uiRoot.getElementById(prefix + "-progress-container");
if (progressContainer) progressContainer.style.display = "none";
setRunning(false);
}
function setUIRunningState(prefix) {
const startBtn = S.uiRoot.getElementById(prefix + "-start-btn");
const pauseBtn = S.uiRoot.getElementById(prefix + "-pause-btn");
const stopBtn = S.uiRoot.getElementById(prefix + "-stop-btn");
const apiKeyInput = S.uiRoot.getElementById(prefix + "-api-key");
const searchInput = S.uiRoot.getElementById(prefix + "-search-context");
const topXInput = S.uiRoot.getElementById(prefix + "-top-x");
const providerSelect = S.uiRoot.getElementById(prefix + "-provider-select");
const modelIdInput = S.uiRoot.getElementById(prefix + "-model-id");
const baseUrlInput = S.uiRoot.getElementById(prefix + "-base-url");
if (startBtn) startBtn.style.display = "none";
if (pauseBtn) pauseBtn.style.display = "block";
if (stopBtn) stopBtn.style.display = "block";
if (apiKeyInput) apiKeyInput.disabled = true;
if (searchInput) searchInput.disabled = true;
if (topXInput) topXInput.disabled = true;
if (providerSelect) providerSelect.disabled = true;
if (modelIdInput) modelIdInput.disabled = true;
if (baseUrlInput) baseUrlInput.disabled = true;
}
function updateLiveRanking(prefix, allTopDeals, cachedSettings) {
const container = S.uiRoot.getElementById(prefix + "-live-ranking");
const content = S.uiRoot.getElementById(prefix + "-live-ranking-content");
if (!container || !content) return;
if (!S.isRunning) {
container.style.display = "none";
return;
}
if (!allTopDeals || allTopDeals.length === 0) {
container.style.display = "none";
return;
}
let currentPage = 0;
for (let di = 0; di < allTopDeals.length; di++) {
if (allTopDeals[di].page > currentPage) currentPage = allTopDeals[di].page;
}
const pageDeals = [];
for (let di = 0; di < allTopDeals.length; di++) {
if (allTopDeals[di].page === currentPage) pageDeals.push(allTopDeals[di]);
}
if (pageDeals.length === 0) {
container.style.display = "none";
return;
}
const topX = cachedSettings && cachedSettings.topX || 3;
container.style.display = "block";
pageDeals.sort(function(a, b) {
return (b && b.score || 0) - (a && a.score || 0);
});
const topItems = pageDeals.slice(0, Math.min(3, topX));
content.innerHTML = topItems.map(function(deal, idx) {
const borderStyle = idx < topItems.length - 1 ? "border-bottom:1px solid #ffe082;" : "";
return [
'',
'
' + (idx + 1) + ". " + esc(deal.title) + "
",
'
' + esc(deal.price) + "
",
'
Seite ' + deal.page + "
",
"
"
].join("\n");
}).join("\n");
}
const Logger$4 = createLogger("MDF UI");
function attachSettingsListeners(prefix, callbacks) {
const startBtn = S.uiRoot.getElementById(prefix + "-start-btn");
const pauseBtn = S.uiRoot.getElementById(prefix + "-pause-btn");
const stopBtn = S.uiRoot.getElementById(prefix + "-stop-btn");
const closeBtn = S.uiRoot.getElementById(prefix + "-close-btn-x");
const showResultsBtn = S.uiRoot.getElementById(prefix + "-show-results-btn");
const apiKeyInput = S.uiRoot.getElementById(prefix + "-api-key");
const searchContextInput = S.uiRoot.getElementById(prefix + "-search-context");
const providerSelect = S.uiRoot.getElementById(prefix + "-provider-select");
const modelIdInput = S.uiRoot.getElementById(prefix + "-model-id");
const baseUrlInput = S.uiRoot.getElementById(prefix + "-base-url");
const presetContainer = S.uiRoot.getElementById(prefix + "-model-presets");
if (startBtn && callbacks.start) {
startBtn.addEventListener("click", function() {
callbacks.start()["catch"](function(error) {
Logger$4.error("Unhandled error in start:", error);
updateProgress(prefix, "Fehler: " + error.message, 0, "error");
resetUI(prefix);
});
});
}
if (pauseBtn && callbacks.pause) {
pauseBtn.addEventListener("click", callbacks.pause);
}
if (stopBtn && callbacks.stop) {
stopBtn.addEventListener("click", callbacks.stop);
}
if (closeBtn && callbacks.close) {
closeBtn.addEventListener("click", callbacks.close);
}
if (showResultsBtn && callbacks.showSavedResults) {
showResultsBtn.addEventListener("click", callbacks.showSavedResults);
}
if (apiKeyInput && callbacks.apiKeyChange) {
apiKeyInput.addEventListener("blur", function() {
callbacks.apiKeyChange(apiKeyInput.value.trim());
});
}
if (searchContextInput && callbacks.searchContextChange) {
searchContextInput.addEventListener("blur", function() {
callbacks.searchContextChange(searchContextInput.value.trim());
});
}
if (providerSelect && callbacks.providerChange) {
providerSelect.addEventListener("change", function() {
callbacks.providerChange(providerSelect.value)["catch"](function(err) {
Logger$4.error("Provider change error:", err);
});
});
}
if (modelIdInput && callbacks.modelIdChange) {
modelIdInput.addEventListener("blur", function() {
callbacks.modelIdChange(modelIdInput.value.trim());
});
}
if (baseUrlInput && callbacks.baseUrlChange) {
baseUrlInput.addEventListener("blur", function() {
callbacks.baseUrlChange(baseUrlInput.value.trim());
});
}
const portkeyConfigInput = S.uiRoot.getElementById(prefix + "-portkey-config");
if (portkeyConfigInput && callbacks.portkeyConfigChange) {
portkeyConfigInput.addEventListener("blur", function() {
callbacks.portkeyConfigChange(portkeyConfigInput.value.trim());
});
}
if (presetContainer && callbacks.modelPresetClick) {
presetContainer.addEventListener("click", function(e) {
const btn = e.target.closest("[data-model-id]");
if (btn) {
const modelId = btn.getAttribute("data-model-id");
const rawOpts = btn.getAttribute("data-options");
let options = null;
if (rawOpts) {
try {
options = JSON.parse(rawOpts);
} catch (e2) {
}
}
callbacks.modelPresetClick(modelId, options);
}
});
}
[startBtn, pauseBtn, stopBtn, showResultsBtn].forEach(function(btn) {
if (btn) {
btn.addEventListener("mouseenter", function() {
btn.style.opacity = "0.9";
});
btn.addEventListener("mouseleave", function() {
btn.style.opacity = "1";
});
}
});
}
function attachResultsListeners(prefix, callbacks) {
const closeBtn = S.uiRoot.getElementById(prefix + "-close-btn-x");
const backBtn = S.uiRoot.getElementById(prefix + "-back-to-settings");
const exportMdBtn = S.uiRoot.getElementById(prefix + "-export-markdown-btn");
const exportJsonBtn = S.uiRoot.getElementById(prefix + "-export-json-btn");
const exportCsvBtn = S.uiRoot.getElementById(prefix + "-export-csv-btn");
const clearBtn = S.uiRoot.getElementById(prefix + "-clear-results-btn");
if (closeBtn && callbacks.close) closeBtn.addEventListener("click", callbacks.close);
if (backBtn && callbacks.backToSettings) backBtn.addEventListener("click", callbacks.backToSettings);
if (exportMdBtn && callbacks.exportMarkdown) exportMdBtn.addEventListener("click", callbacks.exportMarkdown);
if (exportJsonBtn && callbacks.exportJSON) exportJsonBtn.addEventListener("click", callbacks.exportJSON);
if (exportCsvBtn && callbacks.exportCSV) exportCsvBtn.addEventListener("click", callbacks.exportCSV);
if (clearBtn && callbacks.clearResults) clearBtn.addEventListener("click", callbacks.clearResults);
[closeBtn, backBtn, exportMdBtn, exportJsonBtn, exportCsvBtn, clearBtn].forEach(function(btn) {
if (btn) {
btn.addEventListener("mouseenter", function() {
btn.style.opacity = "0.9";
});
btn.addEventListener("mouseleave", function() {
btn.style.opacity = "1";
});
}
});
}
function escapeTable(str) {
return (str || "").replace(/\|/g, "\\|");
}
function formatDate(isoString) {
try {
let d = new Date(isoString);
return d.toLocaleDateString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
} catch (e) {
return isoString;
}
}
function generateMarkdown(deals, pages, timestamp, siteName) {
let ts = timestamp || ( new Date()).toISOString();
let md = "# " + siteName + " β Top-Deals\n\n";
md += "- **Gefunden:** " + deals.length + " Top-Deals\n";
md += "- **Analysierte Seiten:** " + pages + "\n";
md += "- **Erstellt:** " + formatDate(ts) + "\n\n";
let stats = computePriceStats(deals);
if (stats) {
md += "### Preis-Statistik\n\n";
md += "| Kennzahl | Wert |\n";
md += "|----------|-----|\n";
md += "| Minimal | " + stats.min.toLocaleString("de-DE") + " EUR |\n";
md += "| Maximal | " + stats.max.toLocaleString("de-DE") + " EUR |\n";
md += "| Median | " + stats.median.toLocaleString("de-DE") + " EUR |\n";
md += "| Durchschnitt | " + stats.mean.toLocaleString("de-DE") + " EUR |\n\n";
}
md += "## Uebersicht\n\n";
md += "| # | Titel | Preis | Score | Seite |\n";
md += "|---|-------|-------|-------|------|\n";
for (let i = 0; i < deals.length; i++) {
let deal = deals[i];
let rank = i + 1;
let title = escapeTable(deal.title || "Unbekannt");
let price = escapeTable(deal.price || "-");
let score = deal.score !== void 0 && isValidScore(deal.score) ? deal.score + "/100" : "-";
let page = deal.page || "-";
md += "| " + rank + " | " + title + " | " + price + " | " + score + " | " + page + " |\n";
}
md += "\n";
md += "---\n\n## Details\n\n";
for (let j = 0; j < deals.length; j++) {
let d = deals[j];
let r = j + 1;
md += "### #" + r + " β " + (d.title || "Unbekannt") + "\n\n";
md += "- **Preis:** " + (d.price || "Unbekannt") + "\n";
if (d.score !== void 0 && isValidScore(d.score)) {
md += "- **Score:** " + d.score + "/100\n";
}
md += "- **Begruendung:** " + (d.reasoning || "Keine Begruendung") + "\n";
md += "- **Seite:** " + (d.page || "?") + "\n\n";
if (d.description) {
var safeDesc = d.description.replace(/`{3,}/g, function(m) {
return "\\`".repeat(m.length);
});
md += "**Beschreibung:**\n\n> " + safeDesc.replace(/\n/g, "\n> ") + "\n\n";
} else {
md += "*Keine Beschreibung geladen.*\n\n";
}
md += "[Anzeige oeffnen](" + d.url + ")\n\n";
if (j < deals.length - 1) {
md += "---\n\n";
}
}
return md;
}
function escapeCsv(val) {
if (val === null || val === void 0) return "";
let str = String(val);
str = str.replace(/[\r\n]+/g, " ");
return '"' + str.replace(/"/g, '""') + '"';
}
async function exportMarkdown(prefix) {
let results = await loadResults(prefix);
if (!results) {
createToast("Keine Results verfuegbar!", { type: "error" });
return;
}
let siteName = prefix === "wh" ? "WILLHABEN" : "KLEINANZEIGEN";
let md = generateMarkdown(results.deals, results.pages, results.timestamp, siteName);
try {
await navigator.clipboard.writeText(md);
var btn = S.uiRoot.getElementById(prefix + "-export-markdown-btn");
if (btn) {
let orig = btn.textContent;
btn.textContent = "Kopiert!";
setTimeout(function() {
btn.textContent = orig;
}, 2e3);
}
} catch (error) {
createToast("Fehler beim Kopieren. Bitte Fenster fokussieren und nochmal versuchen.", { type: "error", duration: 5e3 });
}
}
async function exportJSON(prefix) {
let results = await loadResults(prefix);
if (!results) {
createToast("Keine Results verfuegbar!", { type: "error" });
return;
}
let siteName = prefix === "wh" ? "WILLHABEN" : "KLEINANZEIGEN";
let deals = results.deals || [];
let metaJson = JSON.stringify({
site: siteName,
totalDeals: deals.length,
pages: results.pages || 0,
timestamp: results.timestamp || ( new Date()).toISOString()
});
let jsonOut = '{\n "metadata": ' + metaJson + ',\n "deals": [\n';
for (let i = 0; i < deals.length; i++) {
jsonOut += " " + JSON.stringify(deals[i]);
if (i < deals.length - 1) jsonOut += ",\n";
else jsonOut += "\n";
}
jsonOut += " ]\n}\n";
let blob = new Blob([jsonOut], { type: "application/json" });
let url = URL.createObjectURL(blob);
let a = GM_addElement("a", { href: url, download: "deals-" + Date.now() + ".json" });
a.click();
setTimeout(function() {
URL.revokeObjectURL(url);
a.remove();
}, 1e3);
}
async function exportCSV(prefix) {
let results = await loadResults(prefix);
if (!results) {
createToast("Keine Results verfuegbar!", { type: "error" });
return;
}
let deals = results.deals || [];
let header = ["Rang", "Titel", "Preis", "Score", "Begruendung", "Seite", "Beschreibung", "URL"];
let rows = deals.map(function(d, i) {
return [
i + 1,
escapeCsv(d.title),
escapeCsv(d.price),
d.score !== void 0 && Number.isFinite(Number(d.score)) ? d.score : "",
escapeCsv(d.reasoning || d.reason),
d.page || "",
escapeCsv(d.description),
escapeCsv(d.url)
].join(";");
});
let csv = "\uFEFF" + header.join(";") + "\r\n" + rows.join("\r\n");
let blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
let url = URL.createObjectURL(blob);
let a = GM_addElement("a", { href: url, download: "deals-" + Date.now() + ".csv" });
a.click();
setTimeout(function() {
URL.revokeObjectURL(url);
a.remove();
}, 1e3);
}
const Logger$3 = createLogger("MDF Crawler");
function escapeForPrompt(str) {
return (str || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "");
}
function gradualScroll(targetY, durationMs) {
return new Promise(function(resolve) {
var startY = window.scrollY;
var startTime = performance.now();
function step() {
var elapsed = performance.now() - startTime;
var progress = Math.min(elapsed / durationMs, 1);
var eased = 1 - Math.pow(1 - progress, 3);
window.scrollTo(0, startY + (targetY - startY) * eased);
if (progress < 1) {
requestAnimationFrame(step);
} else {
resolve();
}
}
requestAnimationFrame(step);
});
}
function buildAnalysisPrompt(adsData, searchContext, topX, siteName, maxDescLength) {
maxDescLength = maxDescLength || 400;
const stats = computePriceStats(adsData);
const statsSection = stats ? "\n\n## Price Distribution\n- Min: " + stats.min + " EUR\n- Max: " + stats.max + " EUR\n- Avg: " + stats.mean + " EUR\n- Median: " + stats.median + " EUR\n- Listings with price: " + stats.count : "";
let prompt = "You are a deal and price analysis expert.\n\nSEARCH CONTEXT: " + escapeForPrompt(searchContext) + "\n\nTASK:\nAnalyze the following " + siteName + " listings and find the " + topX + " BEST deals.\n\nCRITERIA for a good deal:\n- 35-90% below the usual new price\n- Guaranteed profit on resale possible\n- MUST BUY quality\n- Real added value for the buyer" + statsSection + "\n\nLISTINGS:\n";
for (let adi = 0; adi < adsData.length; adi++) {
const ad = adsData[adi];
prompt += "\nListing " + (adi + 1) + ":\nTitle: " + escapeForPrompt(ad.title) + "\nPrice: " + escapeForPrompt(ad.price) + "\nDescription: " + escapeForPrompt(ad.description).substring(0, maxDescLength) + "\nURL: " + escapeForPrompt(ad.url) + "\n---\n";
}
prompt += '\nRESPONSE FORMAT (JSON ONLY, NO EXTRA TEXT):\n{\n "topDeals": [\n {\n "title": "...",\n "price": "...",\n "description": "...",\n "url": "...",\n "reasoning": "Why is this a top deal? (1-2 sentences)",\n "score": 85\n }\n ]\n}\n\nSort the top ' + topX + " deals by quality (best first). Score is 0-100 (100 = absolute bargain).\nReturn ONLY valid JSON. No markdown, no code fences, no extra text.";
return prompt;
}
async function waitIfPaused() {
while (S.isPaused && !S.shouldStop) {
await new Promise(function(r) {
setTimeout(r, PAUSE_POLL_INTERVAL);
});
}
}
function deepFindDescription(obj, depth) {
depth = depth || 0;
if (!obj || typeof obj !== "object" || depth > 10) return null;
if (obj["@type"]) {
var typeStr = String(obj["@type"]).toLowerCase();
if (/product|offer|article|vehicle|residence|apartment|car/i.test(typeStr)) {
if (typeof obj.description === "string" && obj.description.trim().length > 20) return obj.description;
if (typeof obj.body === "string" && obj.body.trim().length > 20) return obj.body;
if (typeof obj.teaser === "string" && obj.teaser.trim().length > 20) return obj.teaser;
}
}
if (typeof obj.description === "string" && obj.description.trim().length > 20) return obj.description;
if (typeof obj.body === "string" && obj.body.trim().length > 20) return obj.body;
if (typeof obj.text === "string" && obj.text.trim().length > 100) return obj.text;
if (Array.isArray(obj)) {
for (var ai = 0; ai < obj.length; ai++) {
var result = deepFindDescription(obj[ai], depth + 1);
if (result) return result;
}
} else {
var keys = Object.keys(obj);
for (var ki = 0; ki < keys.length; ki++) {
var val = obj[keys[ki]];
if (val && typeof val === "object") {
var result = deepFindDescription(val, depth + 1);
if (result) return result;
}
}
}
return null;
}
function decodeEntities(str) {
return (str || "").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ");
}
function extractDescriptionFromHtml(html, descSelectors2) {
var candidates = [];
var push = function(via, text) {
if (text) {
var clean = decodeEntities(text).replace(/\s+/g, " ").trim();
if (clean.length >= 20) candidates.push({ via, text: clean });
}
};
var doc = null;
try {
doc = new DOMParser().parseFromString(html, "text/html");
} catch (e) {
}
try {
var nextMatch = html.match(/