/**
* eBay Content Script
* Wird auf eBay-Seiten ausgeführt und extrahiert Verkäufer-/Shop-Daten aus dem DOM
*/
// Log dass Content Script geladen wurde
console.log("[EBAY-CONTENT] Content script loaded on:", window.location.href);
// Message Listener für Parsing-Anfragen
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Ping-Handler für Content Script Verfügbarkeitsprüfung
if (message.action === "PING") {
sendResponse({ ok: true, ready: true });
return true;
}
if (message.action === "PARSE_EBAY") {
// Wrapper um sicherzustellen, dass immer eine Antwort gesendet wird
try {
// Prüfe ob DOM bereit ist
if (document.readyState === 'loading') {
// DOM noch nicht bereit, warte kurz
document.addEventListener('DOMContentLoaded', () => {
try {
const parsedData = parseEbayPage();
sendResponse({ ok: true, data: parsedData });
} catch (error) {
console.error("[EBAY-CONTENT] Error parsing:", error);
sendResponse({
ok: true,
data: {
sellerId: "",
shopName: "",
market: extractMarketFromHostname(),
status: "unknown",
stats: {}
}
});
}
}, { once: true });
return true; // async response
}
// DOM ist bereit, parse sofort
const parsedData = parseEbayPage();
sendResponse({ ok: true, data: parsedData });
} catch (error) {
// Niemals unhandled throws - immer graceful response
console.error("[EBAY-CONTENT] Error in parse handler:", error);
sendResponse({
ok: true,
data: {
sellerId: "",
shopName: "",
market: extractMarketFromHostname(),
status: "unknown",
stats: {}
}
});
}
return true; // async response
}
if (message.action === "PARSE_ITEM_DETAIL") {
try {
const detail = parseItemDetailPage();
sendResponse({ ok: true, data: detail });
} catch (e) {
console.warn("[EBAY-CONTENT] PARSE_ITEM_DETAIL error:", e);
sendResponse({ ok: true, data: {} });
}
return true;
}
if (message.action === "PARSE_FEEDBACK") {
try {
const feedbackData = extractFeedbackData();
sendResponse({ ok: true, data: feedbackData });
} catch (error) {
console.error("[EBAY-CONTENT] PARSE_FEEDBACK error:", error);
sendResponse({ ok: false, error: error.message || "Failed to extract feedback data" });
}
return true;
}
if (message.action === "PARSE_ABOUT") {
try {
const aboutData = extractResponseTime();
sendResponse({ ok: true, data: aboutData });
} catch (error) {
console.error("[EBAY-CONTENT] PARSE_ABOUT error:", error);
sendResponse({ ok: false, error: error.message || "Failed to extract response time" });
}
return true;
}
if (message.action === "PARSE_STORE") {
try {
const storeData = extractFollowers();
sendResponse({ ok: true, data: storeData });
} catch (error) {
console.error("[EBAY-CONTENT] PARSE_STORE error:", error);
sendResponse({ ok: false, error: error.message || "Failed to extract followers" });
}
return true;
}
if (message.action === "FIND_STORE_LINK") {
try {
const storeUrl = findStoreLink();
sendResponse({ ok: true, data: { storeUrl } });
} catch (error) {
console.error("[EBAY-CONTENT] FIND_STORE_LINK error:", error);
sendResponse({ ok: false, error: error.message || "Failed to find store link" });
}
return true;
}
if (message.action === "PARSE_PRODUCT_LIST") {
// async function, need to return promise
parseProductList()
.then(result => {
// result hat bereits die Struktur { ok, items?, error?, meta }
if (result.ok) {
sendResponse({
ok: true,
items: result.items || [],
meta: result.meta || {}
});
} else {
sendResponse({
ok: false,
error: result.error || "Failed to parse product list",
meta: result.meta || {}
});
}
})
.catch(error => {
// Niemals unhandled throws - immer graceful response
console.error("Error parsing product list:", error);
const pageType = detectPageType();
sendResponse({
ok: false,
error: error.message || "Failed to parse product list",
meta: {
pageType,
finalUrl: window.location.href,
attempts: 1,
reason: "unhandled_error"
}
});
});
return true; // async response
}
});
/**
* Extrahiert Marktplatz aus hostname
* @returns {string} Market Code (z.B. "DE", "US", "UK")
*/
function extractMarketFromHostname() {
try {
const hostname = window.location.hostname.toLowerCase();
// eBay Domain-Patterns
if (hostname.includes('.de') || hostname.includes('ebay.de')) {
return 'DE';
}
if (hostname.includes('.com') && !hostname.includes('.uk')) {
return 'US';
}
if (hostname.includes('.uk') || hostname.includes('ebay.co.uk')) {
return 'UK';
}
if (hostname.includes('.fr') || hostname.includes('ebay.fr')) {
return 'FR';
}
if (hostname.includes('.it') || hostname.includes('ebay.it')) {
return 'IT';
}
if (hostname.includes('.es') || hostname.includes('ebay.es')) {
return 'ES';
}
if (hostname.includes('.nl') || hostname.includes('ebay.nl')) {
return 'NL';
}
if (hostname.includes('.at') || hostname.includes('ebay.at')) {
return 'AT';
}
if (hostname.includes('.ch') || hostname.includes('ebay.ch')) {
return 'CH';
}
// Fallback: erster Teil der Domain nach "ebay."
const match = hostname.match(/ebay\.([a-z]{2,3})/);
if (match && match[1]) {
return match[1].toUpperCase();
}
return 'US'; // Default
} catch (e) {
return 'US';
}
}
/**
* Extrahiert Seller ID aus URL oder DOM
* @returns {string} Seller ID
*/
function extractSellerId() {
try {
// Methode 1: URL-Patterns
const url = window.location.href;
const urlLower = url.toLowerCase();
// Pattern: /usr/username oder /str/storename
const usrMatch = url.match(/\/usr\/([^\/\?]+)/i);
if (usrMatch && usrMatch[1]) {
return usrMatch[1].trim();
}
const strMatch = url.match(/\/str\/([^\/\?]+)/i);
if (strMatch && strMatch[1]) {
return strMatch[1].trim();
}
// Pattern: _ssn=username (Seller Name Parameter in Suchergebnis-URLs)
const ssnMatch = url.match(/[?&]_ssn=([^&]+)/i);
if (ssnMatch && ssnMatch[1]) {
return decodeURIComponent(ssnMatch[1]).trim();
}
// Methode 2: DOM-Elemente suchen
// Suche nach verschiedenen Selektoren, die Seller-ID enthalten könnten
const possibleSelectors = [
'[data-testid*="seller"]',
'.seller-username',
'.member-info-username',
'[class*="seller-id"]',
'[class*="username"]',
'[id*="seller"]'
];
for (const selector of possibleSelectors) {
try {
const element = document.querySelector(selector);
if (element) {
const text = element.textContent?.trim();
if (text && text.length > 0 && text.length < 100) {
return text;
}
}
} catch (e) {
// Continue to next selector
}
}
// Methode 3: Meta-Tags
try {
const metaSeller = document.querySelector('meta[property*="seller"], meta[name*="seller"]');
if (metaSeller && metaSeller.content) {
return metaSeller.content.trim();
}
} catch (e) {
// Continue
}
return ""; // Nicht gefunden
} catch (e) {
return "";
}
}
/**
* Extrahiert Shop Name aus DOM
* @returns {string} Shop Name
*/
function extractShopName() {
try {
// Methode 1: Shop-Name aus Storefront-Link extrahieren
// Pattern: Shop Name
try {
const storefrontLinks = document.querySelectorAll('a[href*="/str/"]');
for (const link of storefrontLinks) {
// Prüfe ob Link data-track Attribut hat (typisch für Storefront-Links)
if (link.hasAttribute('data-track') || link.href.includes('/str/')) {
const linkText = link.textContent?.trim();
if (linkText && linkText.length > 0 && linkText.length < 200) {
// Prüfe dass es nicht nur ein URL-Pfad ist
if (!linkText.match(/^https?:\/\//) && !linkText.match(/^\/str\//)) {
return linkText;
}
}
}
}
} catch (e) {
// Continue to next method
}
// Methode 2: Spezifische Selektoren versuchen
const shopNameSelectors = [
'h1.shop-name',
'.store-name',
'[data-testid="store-name"]',
'.store-title',
'[class*="shop-name"]',
'[class*="store-title"]',
'h1[class*="store"]',
'h1[class*="shop"]'
];
for (const selector of shopNameSelectors) {
try {
const element = document.querySelector(selector);
if (element) {
const text = element.textContent?.trim();
if (text && text.length > 0) {
return text;
}
}
} catch (e) {
// Continue to next selector
}
}
// Methode 3: document.title parsen
try {
const title = document.title || "";
// Versuche Muster wie "Shop Name | eBay" zu extrahieren
const titleMatch = title.match(/^(.+?)\s*[\|\-]\s*eBay/i);
if (titleMatch && titleMatch[1]) {
return titleMatch[1].trim();
}
// Wenn Titel einfach genug ist, verwende ihn direkt
if (title && title.length < 100 && !title.toLowerCase().includes('ebay')) {
return title.trim();
}
} catch (e) {
// Continue
}
// Methode 4: h1 Tag als Fallback
try {
const h1 = document.querySelector('h1');
if (h1) {
const text = h1.textContent?.trim();
if (text && text.length > 0 && text.length < 200) {
return text;
}
}
} catch (e) {
// Continue
}
return ""; // Nicht gefunden
} catch (e) {
return "";
}
}
/**
* Extrahiert "Artikel verkauft" aus Storefront-Profilen
* @returns {number|null} Anzahl verkaufter Artikel oder null
*/
function extractItemsSold() {
try {
// Suche nach Container
const container = document.querySelector(".str-seller-card__store-stats-content");
if (!container) {
return null;
}
// Finde div children, die "Artikel verkauft" enthalten
const divs = container.querySelectorAll("div");
for (const div of divs) {
const divText = div.textContent || "";
if (divText.includes("Artikel verkauft")) {
// Suche nach span mit Klasse str-text-span BOLD (Klasse kann als "str-text-span BOLD" sein)
const span1 = div.querySelector("span.str-text-span.BOLD");
const span2 = div.querySelector("span.BOLD");
const span3 = div.querySelector('span[class*="BOLD"]');
const span = span1 || span2 || span3;
if (span) {
let valueText = span.textContent?.trim() || "";
// Normalisierung: Entferne Leerzeichen
valueText = valueText.replace(/\s/g, "");
// Ersetze Tausendertrenner (. und ,) durch leeren String
valueText = valueText.replace(/[.,]/g, "");
// Nur Digits behalten
valueText = valueText.replace(/\D/g, "");
// Parse zu Integer
if (valueText.length > 0) {
const parsedValue = parseInt(valueText, 10);
if (!isNaN(parsedValue) && parsedValue >= 0) {
return parsedValue;
}
}
}
// Fallback: Regex auf div-Text, falls Struktur abweicht
const regexMatch = divText.match(/([\d.,]+)\s*Artikel\s*verkauft/i);
if (regexMatch && regexMatch[1]) {
let valueText = regexMatch[1].trim();
valueText = valueText.replace(/\s/g, "").replace(/[.,]/g, "").replace(/\D/g, "");
if (valueText.length > 0) {
const parsedValue = parseInt(valueText, 10);
if (!isNaN(parsedValue) && parsedValue >= 0) {
return parsedValue;
}
}
}
}
}
return null;
} catch (e) {
// Graceful fallback bei Fehlern
return null;
}
}
/**
* Extrahiert Stats aus DOM (Feedback Score, Positive Rate, etc.)
* @returns {object} Stats Objekt
*/
function extractStats() {
const stats = {};
try {
const pageText = document.body?.textContent || "";
// Feedback Score
try {
const feedbackScorePatterns = [
/feedback\s*score[:\s]+(\d+)/i,
/(?:score|bewertung)[:\s]+(\d+)/i,
/(\d+)\s*feedback/i
];
for (const pattern of feedbackScorePatterns) {
const match = pageText.match(pattern);
if (match && match[1]) {
const score = parseInt(match[1], 10);
if (!isNaN(score) && score >= 0) {
stats.feedbackScore = score;
break;
}
}
}
} catch (e) {
// Continue
}
// Positive Rate (%)
try {
const positiveRatePatterns = [
/positive[:\s]+(\d+\.?\d*)\s*%/i,
/(\d+\.?\d*)\s*%\s*positive/i,
/(?:rate|quote)[:\s]+(\d+\.?\d*)\s*%/i
];
for (const pattern of positiveRatePatterns) {
const match = pageText.match(pattern);
if (match && match[1]) {
const rate = parseFloat(match[1]);
if (!isNaN(rate) && rate >= 0 && rate <= 100) {
stats.positiveRate = rate;
break;
}
}
}
} catch (e) {
// Continue
}
// Feedback Count
try {
const feedbackCountPatterns = [
/(\d+)\s*(?:bewertungen|ratings|feedbacks?)/i,
/(?:count|anzahl)[:\s]+(\d+)/i
];
for (const pattern of feedbackCountPatterns) {
const match = pageText.match(pattern);
if (match && match[1]) {
const count = parseInt(match[1], 10);
if (!isNaN(count) && count >= 0) {
stats.feedbackCount = count;
break;
}
}
}
} catch (e) {
// Continue
}
// Items for Sale
try {
const itemsPatterns = [
/items?\s*(?:for\s*)?sale[:\s]+(\d+)/i,
/(\d+)\s*artikel/i,
/(\d+)\s*(?:items?|artikel)\s*(?:for\s*)?sale/i
];
for (const pattern of itemsPatterns) {
const match = pageText.match(pattern);
if (match && match[1]) {
const items = parseInt(match[1], 10);
if (!isNaN(items) && items >= 0) {
stats.itemsForSale = items;
break;
}
}
}
} catch (e) {
// Continue
}
// Member Since
try {
const memberSincePatterns = [
/member\s*since[:\s]+(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/i,
/mitglied\s*seit[:\s]+(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/i,
/(?:since|seit)[:\s]+(\d{4})/i
];
for (const pattern of memberSincePatterns) {
const match = pageText.match(pattern);
if (match && match[1]) {
stats.memberSince = match[1].trim();
break;
}
}
} catch (e) {
// Continue
}
// Items Sold (Artikel verkauft)
try {
const itemsSold = extractItemsSold();
if (itemsSold !== null) {
stats.itemsSold = itemsSold;
}
} catch (e) {
// Continue
}
} catch (e) {
// Return empty stats object
}
return stats;
}
/**
* Haupt-Parsing-Funktion
* @returns {object} Parsed eBay Account Data
*/
function parseEbayPage() {
try {
const sellerId = extractSellerId();
const shopName = extractShopName();
const market = extractMarketFromHostname();
const stats = extractStats();
// Status bestimmen: "active" wenn wir mindestens Shop Name oder Seller ID haben
const status = (sellerId || shopName) ? "active" : "unknown";
return {
sellerId: sellerId || "",
shopName: shopName || "",
market: market,
status: status,
stats: stats
};
} catch (error) {
// Graceful fallback
return {
sellerId: "",
shopName: "",
market: extractMarketFromHostname(),
status: "unknown",
stats: {}
};
}
}
/**
* Erkennt den Seitentyp basierend auf URL und DOM
* @returns {string} Page Type: "storefront" | "seller_profile" | "feedback" | "search_results" | "unknown"
*/
function detectPageType() {
try {
const pathname = window.location.pathname.toLowerCase();
const search = window.location.search.toLowerCase();
// Storefront
if (pathname.includes("/str/")) {
return "storefront";
}
// Seller Profile
if (pathname.includes("/usr/")) {
return "seller_profile";
}
// Feedback
if (search.includes("_tab=feedback") || pathname.includes("feedback")) {
return "feedback";
}
// Search Results
if (document.querySelector("ul.srp-results")) {
return "search_results";
}
return "unknown";
} catch (e) {
return "unknown";
}
}
/**
* Wartet auf Item-Links, bis minLinks gefunden wurden oder Timeout
* @param {number} maxMs - Maximale Wartezeit in ms (default: 4000)
* @param {number} intervalMs - Intervall zwischen Checks in ms (default: 250)
* @param {number} minLinks - Minimale Anzahl Links (default: 5)
* @returns {Promise} Array von Link-Elementen (kann 0 sein)
*/
async function waitForItemLinks(maxMs = 4000, intervalMs = 250, minLinks = 5) {
const startTime = Date.now();
while (Date.now() - startTime < maxMs) {
const links = findItemLinks();
if (links.length >= minLinks) {
return links;
}
// Warte auf nächsten Check
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
// Timeout: gib gefundene Links zurück (kann 0 sein)
return findItemLinks();
}
/**
* Findet Navigations-Link zu Items-Seite (Angebote/Artikel)
* @returns {string|null} Absolute URL oder null
*/
function findItemsNavigationLink() {
try {
const allLinks = Array.from(document.querySelectorAll("a"));
const candidates = [];
for (const link of allLinks) {
const href = link.href || link.getAttribute("href");
const text = (link.textContent || "").toLowerCase().trim();
if (!href) continue;
const hrefLower = href.toLowerCase();
// Prüfe Anchor-Text
const textMatches =
text.includes("artikel") ||
text.includes("angebote") ||
text.includes("items") ||
text.includes("items for sale") ||
text.includes("shop") ||
text.includes("shop anzeigen");
// Prüfe href-Patterns
const hrefMatches =
hrefLower.includes("/sch/") ||
hrefLower.includes("/s/i.html") ||
hrefLower.includes("/str/") ||
hrefLower.includes("?_tab=selling");
if (textMatches || hrefMatches) {
// Berechne Priorität
let priority = 0;
if (hrefLower.includes("/sch/")) priority = 1;
else if (hrefLower.includes("/str/")) priority = 2;
else if (hrefLower.includes("/s/i.html")) priority = 3;
else if (text.includes("items")) priority = 4;
else priority = 5;
candidates.push({ link, href, priority });
}
}
if (candidates.length === 0) {
return null;
}
// Sortiere nach Priorität (niedrigste Zahl = höchste Priorität)
candidates.sort((a, b) => a.priority - b.priority);
const bestCandidate = candidates[0].href;
const currentUrl = window.location.href;
// Prüfe ob URL sich unterscheidet
if (bestCandidate === currentUrl) {
return null; // Gleiche URL, keine Navigation nötig
}
// Erstelle absolute URL falls nötig
try {
const url = new URL(bestCandidate, window.location.origin);
return url.href;
} catch (e) {
return null;
}
} catch (e) {
return null;
}
}
/**
* Parst Produktliste von eBay Storefront oder Seller Listings
* @returns {Promise<{ok: boolean, items?: Array, error?: string, meta: object}>}
*/
async function parseProductList() {
let attempts = 1;
let pageType = detectPageType();
let finalUrl = window.location.href;
let reason = null;
try {
// Schritt 1: Warte auf Item-Links (Polling)
let links = await waitForItemLinks(4000, 250, 5);
// Schritt 2: Wenn keine Links gefunden, versuche Auto-Navigation (max. 1 Retry)
if (links.length === 0 && attempts === 1) {
const itemsLink = findItemsNavigationLink();
if (itemsLink && itemsLink !== window.location.href) {
// Navigiere zur Items-Seite
window.location.href = itemsLink;
// Warte auf DOM-Ready nach Navigation
await new Promise(resolve => setTimeout(resolve, 1200));
// Warte erneut auf Item-Links (längeres Timeout nach Navigation)
links = await waitForItemLinks(5000, 250, 5);
attempts = 2;
pageType = detectPageType();
finalUrl = window.location.href;
reason = "navigated_to_items_page";
// Wenn immer noch keine Links, gib Fehler zurück
if (links.length === 0) {
return {
ok: false,
error: "no_items_found",
meta: {
pageType,
finalUrl,
attempts,
reason: "no_items_found_after_navigation"
}
};
}
} else {
// Kein Navigations-Link gefunden
return {
ok: false,
error: "no_items_found",
meta: {
pageType,
finalUrl,
attempts,
reason: links.length === 0 ? "no_items_links_on_page" : "timed_out_waiting_for_items"
}
};
}
}
// Schritt 3: Parse Items
if (links.length === 0) {
return {
ok: false,
error: "no_items_found",
meta: {
pageType,
finalUrl,
attempts,
reason: reason || "no_items_links_on_page"
}
};
}
// Parse each item
const parsedItems = [];
const seenIds = new Set();
for (const itemLink of links) {
try {
const item = parseItemFromLink(itemLink);
// Deduplicate by platformProductId
if (item.platformProductId && !seenIds.has(item.platformProductId)) {
seenIds.add(item.platformProductId);
parsedItems.push(item);
}
} catch (e) {
// Continue with next item if one fails
console.warn("Failed to parse item:", e);
}
}
return {
ok: true,
items: parsedItems,
meta: {
pageType,
finalUrl,
attempts,
reason: reason || (parsedItems.length > 0 ? "items_found" : "parsed_zero_items")
}
};
} catch (error) {
// Graceful error handling
return {
ok: false,
error: error.message || "Failed to parse product list",
meta: {
pageType,
finalUrl,
attempts,
reason: "parse_error"
}
};
}
}
/**
* Parst Produktdetail-Seite (/itm/...) für title, price, currency, category, condition,
* quantityAvailable, quantitySold, watchCount, inCartsCount.
*/
function parseItemDetailPage() {
const out = {
title: "",
price: null,
currency: null,
category: null,
condition: null,
quantityAvailable: null,
quantitySold: null,
watchCount: null,
inCartsCount: null
};
try {
const titleEl = document.querySelector("[data-testid=\"x-item-title\"] h1 .ux-textspans") ||
document.querySelector("h1.x-item-title__mainTitle .ux-textspans");
if (titleEl) {
out.title = (titleEl.textContent || "").trim().replace(/\s+/g, " ");
}
const priceEl = document.querySelector("[data-testid=\"x-price-primary\"] .ux-textspans");
if (priceEl) {
const priceText = (priceEl.textContent || "").trim();
const parsed = parsePrice(priceText);
out.price = parsed.price;
out.currency = parsed.currency;
}
const breadcrumbs = document.querySelectorAll("a.seo-breadcrumb-text span");
if (breadcrumbs.length > 0) {
const parts = [];
breadcrumbs.forEach((span) => {
const t = (span.textContent || "").trim();
if (t) parts.push(t);
});
out.category = parts.join(" > ");
}
const condEl = document.querySelector("dd.ux-labels-values__values .ux-textspans");
if (condEl) {
const raw = (condEl.textContent || "").trim();
const colonIdx = raw.indexOf(":");
out.condition = colonIdx >= 0 ? raw.slice(0, colonIdx).trim() : raw.slice(0, 80);
}
// x-quantity__availability: "9 verfügbar", "8 verkauft"
const availEl = document.querySelector(".x-quantity__availability");
if (availEl) {
const text = (availEl.textContent || "").trim();
const verfMatch = text.match(/(\d+)\s*verfügbar/i);
if (verfMatch && verfMatch[1]) {
const n = parseInt(verfMatch[1], 10);
if (!isNaN(n) && n >= 0) out.quantityAvailable = n;
}
const verkMatch = text.match(/(\d+)\s*verkauft/i);
if (verkMatch && verkMatch[1]) {
const n = parseInt(verkMatch[1], 10);
if (!isNaN(n) && n >= 0) out.quantitySold = n;
}
}
// x-watch-heart-btn: .x-watch-heart-btn-text "14" oder aria-label "14 Beobachter"
const watchBtn = document.querySelector("button.x-watch-heart-btn");
if (watchBtn) {
const textEl = watchBtn.querySelector(".x-watch-heart-btn-text");
if (textEl) {
const n = parseInt((textEl.textContent || "").trim().replace(/\D/g, ""), 10);
if (!isNaN(n) && n >= 0) out.watchCount = n;
}
if (out.watchCount == null) {
const label = (watchBtn.getAttribute("aria-label") || "").trim();
const m = label.match(/(\d+)\s*Beobachter/i);
if (m && m[1]) {
const n = parseInt(m[1], 10);
if (!isNaN(n) && n >= 0) out.watchCount = n;
}
}
}
// [data-testid="x-ebay-signal"]: "In 8 Warenkörben"
const signalEl = document.querySelector("[data-testid=\"x-ebay-signal\"]");
if (signalEl) {
const text = (signalEl.textContent || "").trim();
const m = text.match(/in\s*(\d+)\s*warenkörben/i);
if (m && m[1]) {
const n = parseInt(m[1], 10);
if (!isNaN(n) && n >= 0) out.inCartsCount = n;
}
}
} catch (e) {
console.warn("[EBAY-CONTENT] parseItemDetailPage error:", e);
}
return out;
}
/**
* Findet Item-Links auf der Seite
* @returns {Array} Array von Link-Elementen
*/
function findItemLinks() {
const links = [];
try {
// Multiple selector fallbacks for item cards
const selectors = [
'a[href*="/itm/"]', // Direct item links
'.s-item a[href*="/itm/"]', // Search result items
'.srp-results .s-item a', // Search results
'.ebay-item-card a[href*="/itm/"]', // Item cards
'[class*="item-card"] a[href*="/itm/"]', // Generic item cards
];
for (const selector of selectors) {
try {
const found = document.querySelectorAll(selector);
if (found.length > 0) {
// Filter out duplicates by href
const hrefs = new Set();
for (const link of found) {
const href = link.href || link.getAttribute('href');
if (href && href.includes('/itm/') && !hrefs.has(href)) {
hrefs.add(href);
links.push(link);
}
}
if (links.length > 0) break; // Found items, stop trying other selectors
}
} catch (e) {
// Continue to next selector
}
}
} catch (e) {
// Return empty array on error
}
return links;
}
/**
* Parst ein einzelnes Item aus einem Link-Element
* @param {Element} itemLink - Link-Element zu einem eBay-Item
* @returns {object} Parsed Item-Data
*/
function parseItemFromLink(itemLink) {
const item = {
platformProductId: null,
title: "",
price: null,
currency: null,
url: "",
status: "active",
category: null,
condition: null
};
try {
// URL (absolute)
const href = itemLink.href || itemLink.getAttribute('href');
if (href) {
item.url = href.startsWith('http') ? href : new URL(href, window.location.origin).href;
// Extract platformProductId from URL: /itm/ or ?item=
const itmMatch = item.url.match(/\/itm\/(\d+)/);
if (itmMatch && itmMatch[1]) {
item.platformProductId = itmMatch[1];
} else {
const itemParamMatch = item.url.match(/[?&]item=(\d+)/);
if (itemParamMatch && itemParamMatch[1]) {
item.platformProductId = itemParamMatch[1];
}
}
}
// Fallback: data attributes if present
if (!item.platformProductId) {
const dataItemId = itemLink.getAttribute('data-item-id') ||
itemLink.closest('[data-item-id]')?.getAttribute('data-item-id');
if (dataItemId) {
item.platformProductId = dataItemId;
}
}
// Title: from link text or title element
try {
const titleElement = itemLink.querySelector('.s-item__title') ||
itemLink.querySelector('[class*="title"]') ||
itemLink.closest('.s-item')?.querySelector('.s-item__title');
if (titleElement) {
item.title = titleElement.textContent?.trim() || "";
} else {
// Fallback: link text itself
item.title = itemLink.textContent?.trim() || "";
}
// Normalize title (remove extra whitespace)
item.title = item.title.replace(/\s+/g, ' ').trim();
} catch (e) {
// Continue without title
}
// Price + Currency
try {
const itemCard = itemLink.closest('.s-item') || itemLink.closest('[class*="item-card"]');
if (itemCard) {
const priceElement = itemCard.querySelector('.s-item__price') ||
itemCard.querySelector('[class*="price"]') ||
itemCard.querySelector('.BOLD');
if (priceElement) {
const priceText = priceElement.textContent?.trim() || "";
const parsed = parsePrice(priceText);
item.price = parsed.price;
item.currency = parsed.currency;
}
}
} catch (e) {
// Continue without price
}
// Category (optional)
try {
const categoryElement = itemLink.closest('.s-item')?.querySelector('[class*="category"]');
if (categoryElement) {
item.category = categoryElement.textContent?.trim() || null;
}
} catch (e) {
// Continue without category
}
// Condition (optional)
try {
const conditionElement = itemLink.closest('.s-item')?.querySelector('[class*="condition"]');
if (conditionElement) {
item.condition = conditionElement.textContent?.trim() || null;
}
} catch (e) {
// Continue without condition
}
// Status: default "active" (storefront shows active listings)
// Could check for "ended" indicators, but MVP keeps it simple
item.status = "active";
} catch (e) {
// Continue with partial data
console.warn("Error parsing item:", e);
}
return item;
}
/**
* Extrahiert Feedback-Daten von ?_tab=feedback Seite
* Hybrid-Parsing-Strategie: Label-basiert → Selectors → Regex
* @returns {object} { feedbackTotal, feedback12mPositive, feedback12mNeutral, feedback12mNegative }
*/
function extractFeedbackData() {
const result = {
feedbackTotal: null,
feedback12mPositive: null,
feedback12mNeutral: null,
feedback12mNegative: null
};
try {
const bodyText = document.body?.innerText || "";
// Strategy 1: Label-basiert - Suche "Verkäuferbewertungen" / "Gesamtbewertungen"
const totalMatch = bodyText.match(/(?:Verkäuferbewertungen|Gesamtbewertungen)[\s\S]*?\(([\d.,]+)\)/i);
if (totalMatch && totalMatch[1]) {
result.feedbackTotal = parseNumberWithSeparators(totalMatch[1]);
}
// Strategy 2: Label-basiert - Suche "positive Bewertungen der letzten 12 Monate"
const positiveMatch = bodyText.match(/(\d+[\d.,]*)\s*(?:positive|positive\s*Bewertungen)\s*der\s*letzten\s*12\s*Monate/i);
if (positiveMatch && positiveMatch[1]) {
result.feedback12mPositive = parseNumberWithSeparators(positiveMatch[1]);
}
// Strategy 3: Selector-Fallback - Bekannte Klassen
try {
const titleEl = document.querySelector('h2.fdbk-detail-list__title, h2[class*="fdbk-detail-list__title"]');
if (titleEl) {
const titleText = titleEl.textContent || "";
const match = titleText.match(/\(([\d.,]+)\)/);
if (match && match[1] && !result.feedbackTotal) {
result.feedbackTotal = parseNumberWithSeparators(match[1]);
}
}
const ratingDetails = document.querySelector('.fdbk-overall-rating__details, [class*="fdbk-overall-rating__details"]');
if (ratingDetails) {
const links = ratingDetails.querySelectorAll('a');
for (const link of links) {
const linkText = link.textContent || "";
const numberMatch = linkText.match(/(\d+[\d.,]*)/);
if (numberMatch && numberMatch[1]) {
const num = parseNumberWithSeparators(numberMatch[1]);
if (linkText.toLowerCase().includes('positiv') && !result.feedback12mPositive) {
result.feedback12mPositive = num;
} else if (linkText.toLowerCase().includes('neutral') && !result.feedback12mNeutral) {
result.feedback12mNeutral = num;
} else if (linkText.toLowerCase().includes('negativ') && !result.feedback12mNegative) {
result.feedback12mNegative = num;
}
}
}
}
} catch (e) {
// Continue to regex fallback
}
// Strategy 4: Regex-Fallback auf bodyText
if (!result.feedback12mPositive) {
const posMatch = bodyText.match(/(\d+[\d.,]*)\s*-\s*positive\s*Bewertungen\s*der\s*letzten\s*12\s*Monate/i);
if (posMatch && posMatch[1]) {
result.feedback12mPositive = parseNumberWithSeparators(posMatch[1]);
}
}
if (!result.feedback12mNeutral) {
const neuMatch = bodyText.match(/(\d+[\d.,]*)\s*-\s*neutrale\s*Bewertungen\s*der\s*letzten\s*12\s*Monate/i);
if (neuMatch && neuMatch[1]) {
result.feedback12mNeutral = parseNumberWithSeparators(neuMatch[1]);
}
}
if (!result.feedback12mNegative) {
const negMatch = bodyText.match(/(\d+[\d.,]*)\s*-\s*negative\s*Bewertungen\s*der\s*letzten\s*12\s*Monate/i);
if (negMatch && negMatch[1]) {
result.feedback12mNegative = parseNumberWithSeparators(negMatch[1]);
}
}
} catch (e) {
console.warn("[EBAY-CONTENT] Error extracting feedback data:", e);
}
return result;
}
/**
* Extrahiert Response Time von ?_tab=about Seite
* Hybrid-Parsing-Strategie: Label-basiert → Selectors → Regex
* @returns {object} { responseTimeHours }
*/
function extractResponseTime() {
const result = {
responseTimeHours: null
};
try {
const bodyText = document.body?.innerText || "";
// Strategy 1: Label-basiert - Suche "Antwortzeit:"
const labelMatch = bodyText.match(/Antwortzeit[:\s]+(?:innerhalb\s+)?(\d+)\s*(?:Stunde|Stunden)/i);
if (labelMatch && labelMatch[1]) {
result.responseTimeHours = parseInt(labelMatch[1], 10);
if (!isNaN(result.responseTimeHours) && result.responseTimeHours > 0) {
return result;
}
}
// Strategy 2: Selector-Fallback
try {
const spans = document.querySelectorAll('span.str-text-span, span[class*="str-text-span"]');
for (const span of spans) {
const spanText = span.textContent || "";
if (spanText.includes('Antwortzeit')) {
const nextSpan = span.nextElementSibling;
if (nextSpan) {
const timeText = nextSpan.textContent || "";
const match = timeText.match(/(?:innerhalb\s+)?(\d+)\s*(?:Stunde|Stunden)/i);
if (match && match[1]) {
const hours = parseInt(match[1], 10);
if (!isNaN(hours) && hours > 0) {
result.responseTimeHours = hours;
return result;
}
}
}
}
}
} catch (e) {
// Continue to regex fallback
}
// Strategy 3: Regex-Fallback
const regexMatch = bodyText.match(/innerhalb\s+(\d+)\s*(?:Stunde|Stunden)/i);
if (regexMatch && regexMatch[1]) {
const hours = parseInt(regexMatch[1], 10);
if (!isNaN(hours) && hours > 0) {
result.responseTimeHours = hours;
}
}
} catch (e) {
console.warn("[EBAY-CONTENT] Error extracting response time:", e);
}
return result;
}
/**
* Extrahiert Follower von Store-Seite
* Hybrid-Parsing-Strategie: Label-basiert → Selectors → Regex
* @returns {object} { followers }
*/
function extractFollowers() {
const result = {
followers: null
};
try {
const bodyText = document.body?.innerText || "";
// Strategy 1: Label-basiert - Suche "Follower"
const followerMatch = bodyText.match(/(\d+[\d.,]*)\s*Follower/i);
if (followerMatch && followerMatch[1]) {
result.followers = parseNumberWithSeparators(followerMatch[1]);
if (result.followers !== null) {
return result;
}
}
// Strategy 2: Selector-Fallback
try {
const statsContainer = document.querySelector('.str-seller-card__store-stats-content, [class*="str-seller-card__store-stats-content"]');
if (statsContainer) {
const divs = statsContainer.querySelectorAll('div');
for (const div of divs) {
const divText = div.textContent || "";
if (divText.includes('Follower')) {
const boldSpan = div.querySelector('span.BOLD, span[class*="BOLD"], .str-text-span.BOLD');
if (boldSpan) {
const followerText = boldSpan.textContent || "";
result.followers = parseNumberWithSeparators(followerText);
if (result.followers !== null) {
return result;
}
}
// Fallback: regex on div text
const match = divText.match(/(\d+[\d.,]*)\s*Follower/i);
if (match && match[1]) {
result.followers = parseNumberWithSeparators(match[1]);
if (result.followers !== null) {
return result;
}
}
}
}
}
} catch (e) {
// Continue to regex fallback
}
// Strategy 3: Regex-Fallback auf bodyText (wenn noch nicht gefunden)
if (result.followers === null) {
const regexMatch = bodyText.match(/(\d+[\d.,]*)\s*Follower/i);
if (regexMatch && regexMatch[1]) {
result.followers = parseNumberWithSeparators(regexMatch[1]);
}
}
} catch (e) {
console.warn("[EBAY-CONTENT] Error extracting followers:", e);
}
return result;
}
/**
* Findet Store-URL auf der aktuellen Seite
* Sucht nach Links mit Text "Store" / "Shop" oder href enthält "/str/"
* @returns {string|null} Store URL oder null
*/
function findStoreLink() {
try {
// Strategy 1: Suche Links mit Text "Store" / "Shop"
const allLinks = document.querySelectorAll('a[href]');
for (const link of allLinks) {
const linkText = (link.textContent || "").toLowerCase().trim();
const href = link.href || link.getAttribute('href') || "";
if (href.includes('/str/')) {
// Absoluter oder relativer URL
try {
const url = new URL(href, window.location.origin);
if (url.hostname.includes('ebay.')) {
return url.href;
}
} catch (e) {
// Invalid URL, continue
}
}
// Prüfe Link-Text
if ((linkText.includes('store') || linkText.includes('shop')) && href.includes('/str/')) {
try {
const url = new URL(href, window.location.origin);
if (url.hostname.includes('ebay.')) {
return url.href;
}
} catch (e) {
// Invalid URL, continue
}
}
}
// Strategy 2: Suche nach href mit /str/ Pattern
for (const link of allLinks) {
const href = link.href || link.getAttribute('href') || "";
if (href.includes('/str/')) {
try {
const url = new URL(href, window.location.origin);
if (url.hostname.includes('ebay.')) {
return url.href;
}
} catch (e) {
// Invalid URL, continue
}
}
}
return null;
} catch (e) {
console.warn("[EBAY-CONTENT] Error finding store link:", e);
return null;
}
}
/**
* Parst eine Zahl mit Tausendertrennern und "Mio." Unterstützung
* @param {string} text - Zahl-String z.B. "884.318" oder "2,4 Mio."
* @returns {number|null} Geparste Zahl oder null
*/
function parseNumberWithSeparators(text) {
if (!text || typeof text !== 'string') return null;
try {
// Entferne Leerzeichen
let normalized = text.trim().replace(/\s/g, '');
// Prüfe auf "Mio." / "Millionen"
const mioMatch = normalized.match(/([\d,]+)\s*[,.]?\s*(?:Mio|Millionen)/i);
if (mioMatch && mioMatch[1]) {
const numStr = mioMatch[1].replace(/,/g, '.');
const num = parseFloat(numStr);
if (!isNaN(num)) {
return Math.round(num * 1000000);
}
}
// Entferne Tausendertrenner (Punkte und Kommas)
normalized = normalized.replace(/\./g, ''); // Entferne Punkte (Tausendertrenner)
normalized = normalized.replace(/,/g, '.'); // Ersetze Kommas durch Punkte (Dezimaltrenner)
// Nur Digits und einen Dezimalpunkt behalten
normalized = normalized.replace(/[^\d.]/g, '');
// Parse zu Integer (für Follower/Feedback sind nur ganze Zahlen relevant)
const num = parseInt(normalized, 10);
if (!isNaN(num) && num >= 0) {
return num;
}
return null;
} catch (e) {
return null;
}
}
/**
* Parst Preis-String in Zahl und Währung
* @param {string} priceText - Preis-String z.B. "EUR 12,99" oder "$15.50"
* @returns {object} { price: number|null, currency: string|null }
*/
function parsePrice(priceText) {
const result = { price: null, currency: null };
try {
if (!priceText) return result;
// Extract currency symbols/codes
const currencyMatch = priceText.match(/(EUR|USD|GBP|€|\$|£)/i);
if (currencyMatch) {
const currencyCode = currencyMatch[1].toUpperCase();
if (currencyCode === '€') result.currency = 'EUR';
else if (currencyCode === '$') result.currency = 'USD';
else if (currencyCode === '£') result.currency = 'GBP';
else result.currency = currencyCode;
}
// Extract numeric value (handle both comma and dot as decimal separator)
const normalized = priceText
.replace(/[^\d,.-]/g, '') // Remove non-numeric except , . -
.replace(/\./g, '') // Remove thousand separators (assume dot)
.replace(',', '.'); // Replace comma with dot for decimal
const price = parseFloat(normalized);
if (!isNaN(price) && price >= 0) {
result.price = price;
}
} catch (e) {
// Return null values on error
}
return result;
}