feat: Integrate account_metrics collection with monthly refresh calendar
- Add account_metrics collection schema script - Implement accountMetricsService with upsertAccountMetric, fetchAccountMetricsForMonth, calculateSalesBucket - Extend accountsService with getLastSuccessfulAccountMetric - Update AccountsPage to track daily metrics and display in calendar - Calculate sales difference from last successful refresh - Display refresh status and sales buckets in monthly calendar view - Remove account_refresh_events dependency (use account_metrics only)
This commit is contained in:
@@ -3,8 +3,10 @@ const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren
|
||||
|
||||
const PARSE_TIMEOUT_MS = 15000; // 15 seconds
|
||||
const SCAN_TIMEOUT_MS = 45000; // 45 seconds (listing with _ipg=240 can be slow)
|
||||
const ACCOUNT_EXTENDED_TIMEOUT_MS = 15000; // 15 seconds per sub-scan
|
||||
const activeParseRequests = new Map(); // Map<tabId, { timeout, originalSender, resolve }>
|
||||
const activeScanRequests = new Map(); // Map<tabId, { timeout, sendResponse }>;
|
||||
const activeExtendedParseRequests = new Map(); // Map<requestId, { sendResponse, results }>
|
||||
|
||||
/** Aktueller Scan-Fortschritt für GET_SCAN_PROGRESS (Polling durch Web-App) */
|
||||
let currentScanProgress = null;
|
||||
@@ -68,6 +70,11 @@ chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => {
|
||||
return true; // async
|
||||
}
|
||||
|
||||
if (msg?.action === "PARSE_ACCOUNT_EXTENDED" && msg.url) {
|
||||
handleParseAccountExtendedRequest(msg.url, sendResponse);
|
||||
return true; // async
|
||||
}
|
||||
|
||||
if (msg?.action === "GET_SCAN_PROGRESS") {
|
||||
sendResponse(currentScanProgress ?? { percent: 0, phase: "idle", total: 0, current: 0, complete: false });
|
||||
return false;
|
||||
@@ -676,6 +683,284 @@ async function cleanupScanRequest(tabId, data, error) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles extended account parsing request
|
||||
* Visits multiple URLs (base, feedback, about, store) and collects partial results
|
||||
*/
|
||||
async function handleParseAccountExtendedRequest(url, sendResponse) {
|
||||
try {
|
||||
// Validate URL
|
||||
if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) {
|
||||
sendResponse({ ok: false, error: "Invalid eBay URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[BACKGROUND] Starting extended account parse for:", url);
|
||||
|
||||
const requestId = `extended_${Date.now()}_${Math.random()}`;
|
||||
const results = {
|
||||
base: { ok: false, data: null, error: null },
|
||||
feedback: { ok: false, data: null, error: null },
|
||||
about: { ok: false, data: null, error: null },
|
||||
store: { ok: false, data: null, error: null }
|
||||
};
|
||||
|
||||
activeExtendedParseRequests.set(requestId, {
|
||||
sendResponse: sendResponse,
|
||||
results: results
|
||||
});
|
||||
|
||||
// Step 1: Parse base URL (like normal parse)
|
||||
try {
|
||||
const baseData = await parseSingleTab(url, "PARSE_EBAY", ACCOUNT_EXTENDED_TIMEOUT_MS);
|
||||
results.base = { ok: true, data: baseData, error: null };
|
||||
} catch (error) {
|
||||
console.warn("[BACKGROUND] Base parse failed:", error);
|
||||
results.base = { ok: false, data: null, error: error.message || "Base parse failed" };
|
||||
}
|
||||
|
||||
// Step 2: Parse feedback tab
|
||||
try {
|
||||
const feedbackUrl = appendQueryParam(url, "_tab=feedback");
|
||||
const feedbackData = await parseSingleTab(feedbackUrl, "PARSE_FEEDBACK", ACCOUNT_EXTENDED_TIMEOUT_MS);
|
||||
results.feedback = { ok: true, data: feedbackData, error: null };
|
||||
} catch (error) {
|
||||
console.warn("[BACKGROUND] Feedback parse failed:", error);
|
||||
results.feedback = { ok: false, data: null, error: error.message || "Feedback parse failed" };
|
||||
}
|
||||
|
||||
// Step 3: Parse about tab
|
||||
try {
|
||||
const aboutUrl = appendQueryParam(url, "_tab=about");
|
||||
const aboutData = await parseSingleTab(aboutUrl, "PARSE_ABOUT", ACCOUNT_EXTENDED_TIMEOUT_MS);
|
||||
results.about = { ok: true, data: aboutData, error: null };
|
||||
} catch (error) {
|
||||
console.warn("[BACKGROUND] About parse failed:", error);
|
||||
results.about = { ok: false, data: null, error: error.message || "About parse failed" };
|
||||
}
|
||||
|
||||
// Step 4: Find and parse store URL
|
||||
try {
|
||||
let storeUrl = null;
|
||||
|
||||
// First, try to find store link on base page
|
||||
if (results.base.ok) {
|
||||
try {
|
||||
// Re-open base page tab to search for store link
|
||||
const baseTab = await chrome.tabs.create({
|
||||
url: url,
|
||||
active: false
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const checkLoaded = (tabId, changeInfo) => {
|
||||
if (tabId === baseTab.id && changeInfo.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(checkLoaded);
|
||||
setTimeout(resolve, 2000);
|
||||
}
|
||||
};
|
||||
chrome.tabs.onUpdated.addListener(checkLoaded);
|
||||
setTimeout(resolve, 5000); // Max wait
|
||||
});
|
||||
|
||||
const injected = await ensureContentScriptInjected(baseTab.id);
|
||||
if (injected) {
|
||||
try {
|
||||
const linkResponse = await chrome.tabs.sendMessage(baseTab.id, { action: "FIND_STORE_LINK" });
|
||||
if (linkResponse && linkResponse.ok && linkResponse.data && linkResponse.data.storeUrl) {
|
||||
storeUrl = linkResponse.data.storeUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[BACKGROUND] Could not find store link via content script:", e);
|
||||
}
|
||||
}
|
||||
|
||||
await chrome.tabs.remove(baseTab.id);
|
||||
} catch (e) {
|
||||
console.warn("[BACKGROUND] Error searching for store link:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: heuristical derivation
|
||||
if (!storeUrl) {
|
||||
const sellerId = results.base.data?.sellerId || null;
|
||||
storeUrl = deriveStoreUrl(url, sellerId);
|
||||
}
|
||||
|
||||
if (storeUrl) {
|
||||
const storeData = await parseSingleTab(storeUrl, "PARSE_STORE", ACCOUNT_EXTENDED_TIMEOUT_MS);
|
||||
results.store = { ok: true, data: storeData, error: null };
|
||||
} else {
|
||||
results.store = { ok: false, data: null, error: "Could not determine store URL" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[BACKGROUND] Store parse failed:", error);
|
||||
results.store = { ok: false, data: null, error: error.message || "Store parse failed" };
|
||||
}
|
||||
|
||||
// Combine all results
|
||||
const combinedData = {
|
||||
// Base data (always present if base.ok)
|
||||
sellerId: results.base.data?.sellerId || "",
|
||||
shopName: results.base.data?.shopName || "",
|
||||
market: results.base.data?.market || "US",
|
||||
status: results.base.data?.status || "unknown",
|
||||
stats: results.base.data?.stats || {},
|
||||
// Extended data (can be null)
|
||||
responseTimeHours: results.about.data?.responseTimeHours || null,
|
||||
followers: results.store.data?.followers || null,
|
||||
feedbackTotal: results.feedback.data?.feedbackTotal || null,
|
||||
feedback12mPositive: results.feedback.data?.feedback12mPositive || null,
|
||||
feedback12mNeutral: results.feedback.data?.feedback12mNeutral || null,
|
||||
feedback12mNegative: results.feedback.data?.feedback12mNegative || null,
|
||||
// Partial results status
|
||||
partialResults: {
|
||||
base: { ok: results.base.ok, error: results.base.error },
|
||||
feedback: { ok: results.feedback.ok, error: results.feedback.error },
|
||||
about: { ok: results.about.ok, error: results.about.error },
|
||||
store: { ok: results.store.ok, error: results.store.error }
|
||||
}
|
||||
};
|
||||
|
||||
activeExtendedParseRequests.delete(requestId);
|
||||
sendResponse({ ok: true, data: combinedData });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in handleParseAccountExtendedRequest:", error);
|
||||
sendResponse({ ok: false, error: error.message || "Unknown error" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single tab with given action and timeout
|
||||
* @param {string} url - URL to parse
|
||||
* @param {string} action - Action to send to content script (PARSE_EBAY, PARSE_FEEDBACK, etc.)
|
||||
* @param {number} timeoutMs - Timeout in milliseconds
|
||||
* @returns {Promise<object>} Parsed data
|
||||
*/
|
||||
async function parseSingleTab(url, action, timeoutMs) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let tabId = null;
|
||||
let timeoutId = null;
|
||||
|
||||
try {
|
||||
// Create hidden tab
|
||||
const tab = await chrome.tabs.create({
|
||||
url: url,
|
||||
active: false
|
||||
});
|
||||
tabId = tab.id;
|
||||
|
||||
// Set up timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanupSingleTab(tabId);
|
||||
reject(new Error("timeout"));
|
||||
}, timeoutMs);
|
||||
|
||||
// Wait for tab to load
|
||||
const checkTabLoaded = (updatedTabId, changeInfo) => {
|
||||
if (updatedTabId !== tabId) return;
|
||||
if (changeInfo.status === 'complete') {
|
||||
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const injected = await ensureContentScriptInjected(tabId);
|
||||
if (!injected) {
|
||||
cleanupSingleTab(tabId);
|
||||
reject(new Error("Could not inject content script"));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await chrome.tabs.sendMessage(tabId, { action: action });
|
||||
cleanupSingleTab(tabId);
|
||||
|
||||
if (response && response.ok && response.data) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(new Error(response?.error || "Parsing failed"));
|
||||
}
|
||||
} catch (err) {
|
||||
cleanupSingleTab(tabId);
|
||||
reject(err);
|
||||
}
|
||||
}, 2000); // 2 second delay for DOM ready
|
||||
}
|
||||
};
|
||||
|
||||
chrome.tabs.onUpdated.addListener(checkTabLoaded);
|
||||
} catch (error) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (tabId) cleanupSingleTab(tabId);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up a single tab
|
||||
*/
|
||||
async function cleanupSingleTab(tabId) {
|
||||
try {
|
||||
await chrome.tabs.remove(tabId);
|
||||
} catch (err) {
|
||||
// Tab might already be closed
|
||||
console.warn("[BACKGROUND] Could not close tab:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a query parameter to a URL
|
||||
* @param {string} url - Base URL
|
||||
* @param {string} param - Query parameter (e.g. "_tab=feedback")
|
||||
* @returns {string} URL with appended parameter
|
||||
*/
|
||||
function appendQueryParam(url, param) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
urlObj.search += (urlObj.search ? '&' : '?') + param;
|
||||
return urlObj.href;
|
||||
} catch (e) {
|
||||
// Fallback: simple string append
|
||||
return url + (url.includes('?') ? '&' : '?') + param;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives store URL from account URL
|
||||
* @param {string} accountUrl - Account URL
|
||||
* @param {string|null} sellerId - Seller ID if available
|
||||
* @returns {string|null} Store URL or null
|
||||
*/
|
||||
function deriveStoreUrl(accountUrl, sellerId) {
|
||||
try {
|
||||
const urlObj = new URL(accountUrl);
|
||||
const pathname = urlObj.pathname;
|
||||
|
||||
// Try to extract seller ID from URL if not provided
|
||||
let id = sellerId;
|
||||
if (!id) {
|
||||
const usrMatch = pathname.match(/\/usr\/([^\/\?]+)/);
|
||||
if (usrMatch && usrMatch[1]) {
|
||||
id = usrMatch[1];
|
||||
} else {
|
||||
const strMatch = pathname.match(/\/str\/([^\/\?]+)/);
|
||||
if (strMatch && strMatch[1]) {
|
||||
id = strMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (id) {
|
||||
return `${urlObj.protocol}//${urlObj.hostname}/str/${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.warn("[BACKGROUND] Failed to derive store URL:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJwt() {
|
||||
const data = await chrome.storage.local.get(STORAGE_KEY);
|
||||
return data[STORAGE_KEY] || "";
|
||||
|
||||
@@ -72,6 +72,50 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
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()
|
||||
@@ -1051,6 +1095,325 @@ function parseItemFromLink(itemLink) {
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user