1003 lines
33 KiB
JavaScript
1003 lines
33 KiB
JavaScript
const STORAGE_KEY = "auth_jwt";
|
|
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;
|
|
|
|
// Messages from content script (der von der Web-App kommt)
|
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
// Auth messages vom Content Script
|
|
if (msg?.type === "AUTH_JWT" && msg.jwt) {
|
|
chrome.storage.local.set({ [STORAGE_KEY]: msg.jwt }).then(() => {
|
|
sendResponse({ ok: true });
|
|
});
|
|
return true; // async
|
|
}
|
|
|
|
if (msg?.type === "AUTH_CLEARED") {
|
|
chrome.storage.local.remove(STORAGE_KEY).then(() => {
|
|
sendResponse({ ok: true });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API calls vom Popup
|
|
if (msg?.type === "GET_JWT") {
|
|
getJwt().then(jwt => {
|
|
sendResponse({ jwt });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (msg?.type === "CALL_API" && msg.path) {
|
|
callProtectedApi(msg.path, msg.payload).then(data => {
|
|
sendResponse({ ok: true, data });
|
|
}).catch(err => {
|
|
sendResponse({ ok: false, error: err.message });
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// eBay Parsing Request (from Web App via content script or directly)
|
|
if (msg?.action === "PARSE_URL" && msg.url) {
|
|
handleParseRequest(msg.url, sendResponse);
|
|
return true; // async
|
|
}
|
|
|
|
// eBay Parsing Response (from eBay content script)
|
|
if (msg?.action === "PARSE_COMPLETE") {
|
|
const tabId = sender?.tab?.id;
|
|
handleParseComplete(tabId, msg.data);
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// Handle messages from external web apps (externally_connectable)
|
|
chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => {
|
|
if (msg?.action === "PARSE_URL" && msg.url) {
|
|
handleParseRequest(msg.url, sendResponse);
|
|
return true; // async
|
|
}
|
|
|
|
if (msg?.action === "SCAN_PRODUCTS" && msg.url && msg.accountId) {
|
|
handleScanProductsRequest(msg.url, msg.accountId, 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;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Handles eBay URL parsing request
|
|
* Creates a hidden tab, waits for load, sends parse message to content script
|
|
*/
|
|
async function handleParseRequest(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] Creating tab for parse request:", url);
|
|
|
|
// Create hidden tab
|
|
const tab = await chrome.tabs.create({
|
|
url: url,
|
|
active: false
|
|
});
|
|
if (!tab?.id) {
|
|
sendResponse({ ok: false, error: "Tab could not be created" });
|
|
return;
|
|
}
|
|
const tabId = tab.id;
|
|
console.log("[BACKGROUND] Tab created:", tabId);
|
|
|
|
// Set up timeout
|
|
const timeoutId = setTimeout(() => {
|
|
cleanupParseRequest(tabId, null, { ok: false, error: "timeout" });
|
|
}, PARSE_TIMEOUT_MS);
|
|
|
|
// Store request info
|
|
activeParseRequests.set(tabId, {
|
|
timeout: timeoutId,
|
|
sendResponse: sendResponse
|
|
});
|
|
|
|
// Wait for tab to load, then send parse message
|
|
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
|
|
if (updatedTabId !== tabId) return;
|
|
|
|
// Tab is fully loaded
|
|
if (changeInfo.status === 'complete' && updatedTab.url) {
|
|
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
|
console.log("[BACKGROUND] Tab loaded, sending parse message:", updatedTab.url);
|
|
|
|
// Wait longer for content script to auto-load (if it does)
|
|
// Content scripts from manifest.json might need more time in hidden tabs
|
|
setTimeout(() => {
|
|
sendParseMessageWithRetry(tabId, 0);
|
|
}, 2000); // 2 seconds delay - give content script time to auto-load
|
|
}
|
|
};
|
|
|
|
chrome.tabs.onUpdated.addListener(checkTabLoaded);
|
|
|
|
} catch (error) {
|
|
console.error("Error in handleParseRequest:", error);
|
|
sendResponse({ ok: false, error: error.message || "Unknown error" });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a URL is an eBay URL
|
|
* @param {string} url - URL to check
|
|
* @returns {boolean} - true if URL is an eBay URL
|
|
*/
|
|
function isEbayUrl(url) {
|
|
if (!url) return false;
|
|
const urlLower = url.toLowerCase();
|
|
return urlLower.includes('ebay.');
|
|
}
|
|
|
|
/**
|
|
* Injects the eBay content script manually if it's not already loaded
|
|
* @param {number} tabId - Tab ID
|
|
* @returns {Promise<boolean>} - true if injection successful or already loaded
|
|
*/
|
|
async function ensureContentScriptInjected(tabId) {
|
|
try {
|
|
// First, check if tab URL is an eBay URL
|
|
const tab = await chrome.tabs.get(tabId);
|
|
|
|
if (!isEbayUrl(tab.url)) {
|
|
console.log("[BACKGROUND] Tab is not an eBay URL:", tab.url);
|
|
return false;
|
|
}
|
|
|
|
// Check if content script is already loaded by sending a ping
|
|
try {
|
|
await chrome.tabs.sendMessage(tabId, { action: "PING" });
|
|
console.log("[BACKGROUND] Content script already loaded");
|
|
|
|
return true;
|
|
} catch (pingError) {
|
|
// Content script not loaded, try to inject it manually
|
|
console.log("[BACKGROUND] Content script not found, injecting manually...");
|
|
|
|
try {
|
|
// Get tab info to check frames
|
|
const tabInfo = await chrome.tabs.get(tabId);
|
|
// Inject full ebay-content-script.js so PING, PARSE_EBAY and PARSE_PRODUCT_LIST are all handled.
|
|
// The previous inline injection only handled PING+PARSE_EBAY; SCAN sends PARSE_PRODUCT_LIST.
|
|
try {
|
|
await chrome.scripting.executeScript({
|
|
target: { tabId: tabId, frameIds: [0] },
|
|
files: ["ebay-content-script.js"]
|
|
});
|
|
console.log("[BACKGROUND] ebay-content-script.js injected successfully");
|
|
} catch (injectError) {
|
|
console.error("[BACKGROUND] Failed to inject ebay-content-script:", injectError);
|
|
throw injectError;
|
|
}
|
|
|
|
// Also try injecting a simple test script to verify injection works
|
|
try {
|
|
await chrome.scripting.executeScript({
|
|
target: { tabId: tabId, frameIds: [0] },
|
|
func: () => {
|
|
console.log("[TEST-INJECTION] Test script executed in main frame");
|
|
}
|
|
});
|
|
|
|
} catch (testError) {
|
|
}
|
|
|
|
// Wait longer for the script to fully initialize and register message listeners
|
|
// Retry PING to verify the script is ready
|
|
// Also try sending to frameId 0 explicitly
|
|
for (let pingAttempt = 0; pingAttempt < 10; pingAttempt++) {
|
|
await new Promise(resolve => setTimeout(resolve, 300)); // 300ms between pings
|
|
|
|
try {
|
|
// Try sending to main frame explicitly
|
|
try {
|
|
await chrome.tabs.sendMessage(tabId, { action: "PING" }, { frameId: 0 });
|
|
} catch (frameErr) {
|
|
// Fallback: try without frameId
|
|
await chrome.tabs.sendMessage(tabId, { action: "PING" });
|
|
}
|
|
|
|
console.log("[BACKGROUND] Content script ready after injection");
|
|
return true;
|
|
} catch (pingErr) {
|
|
// Continue to next attempt
|
|
}
|
|
}
|
|
|
|
// Even if PING failed, return true - the script might still work
|
|
console.warn("[BACKGROUND] Content script injected but PING not responding");
|
|
return true;
|
|
} catch (injectError) {
|
|
console.error("[BACKGROUND] Failed to inject content script:", injectError);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("[BACKGROUND] Error checking/injecting content script:", error);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends parse message to content script with retry mechanism
|
|
* @param {number} tabId - Tab ID
|
|
* @param {number} attempt - Current attempt number (0-based)
|
|
*/
|
|
async function sendParseMessageWithRetry(tabId, attempt) {
|
|
const maxAttempts = 3;
|
|
const retryDelay = 500; // 500ms between retries
|
|
|
|
try {
|
|
console.log(`[BACKGROUND] Sending parse message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId);
|
|
|
|
// On first attempt, ensure content script is injected
|
|
if (attempt === 0) {
|
|
const injected = await ensureContentScriptInjected(tabId);
|
|
|
|
if (!injected) {
|
|
throw new Error("Could not inject content script");
|
|
}
|
|
}
|
|
|
|
// Check if content script is injected by trying to send a ping
|
|
// If this fails, the content script might not be loaded yet
|
|
// Try sending to main frame explicitly first
|
|
let response;
|
|
try {
|
|
response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" }, { frameId: 0 });
|
|
} catch (frameErr) {
|
|
// Fallback: try without frameId
|
|
response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" });
|
|
}
|
|
|
|
console.log("[BACKGROUND] Parse response received:", response);
|
|
|
|
if (response && response.ok && response.data) {
|
|
handleParseComplete(tabId, response.data);
|
|
} else {
|
|
cleanupParseRequest(tabId, null, {
|
|
ok: false,
|
|
error: response?.error || "Parsing failed"
|
|
});
|
|
}
|
|
} catch (err) {
|
|
// Check if error is due to content script not being ready
|
|
const runtimeError = chrome.runtime.lastError?.message || "";
|
|
const isContentScriptError =
|
|
err.message?.includes("Could not establish connection") ||
|
|
err.message?.includes("Receiving end does not exist") ||
|
|
err.message?.includes("Could not inject content script") ||
|
|
runtimeError.includes("Receiving end does not exist") ||
|
|
runtimeError.includes("Could not establish connection");
|
|
|
|
console.log(`[BACKGROUND] Error sending parse message (attempt ${attempt + 1}):`, {
|
|
error: err.message,
|
|
runtimeError: runtimeError,
|
|
isContentScriptError: isContentScriptError
|
|
});
|
|
|
|
if (isContentScriptError && attempt < maxAttempts) {
|
|
// Content script not ready yet, retry after delay
|
|
console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`);
|
|
setTimeout(() => {
|
|
sendParseMessageWithRetry(tabId, attempt + 1);
|
|
}, retryDelay);
|
|
} else {
|
|
// Max retries reached or different error
|
|
const errorMessage = isContentScriptError
|
|
? "Content script konnte nicht geladen werden. Bitte Extension neu laden."
|
|
: (err.message || runtimeError || "Content script error");
|
|
cleanupParseRequest(tabId, null, {
|
|
ok: false,
|
|
error: errorMessage
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles parse complete response from content script
|
|
*/
|
|
function handleParseComplete(tabId, data) {
|
|
cleanupParseRequest(tabId, data, null);
|
|
}
|
|
|
|
/**
|
|
* Cleans up parse request: closes tab, clears timeout, sends response
|
|
*/
|
|
async function cleanupParseRequest(tabId, data, error) {
|
|
if (tabId == null) return;
|
|
const request = activeParseRequests.get(tabId);
|
|
if (!request) return;
|
|
|
|
// Clear timeout
|
|
if (request.timeout) {
|
|
clearTimeout(request.timeout);
|
|
}
|
|
|
|
// Remove from active requests
|
|
activeParseRequests.delete(tabId);
|
|
|
|
// Close tab
|
|
try {
|
|
await chrome.tabs.remove(tabId);
|
|
} catch (err) {
|
|
// Tab might already be closed
|
|
console.warn("Could not close tab:", err);
|
|
}
|
|
|
|
// Send response
|
|
if (request.sendResponse) {
|
|
if (error) {
|
|
request.sendResponse(error);
|
|
} else if (data) {
|
|
request.sendResponse({ ok: true, data: data });
|
|
} else {
|
|
request.sendResponse({ ok: false, error: "Unknown error" });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transform account URL (e.g. /str/topmons) to listing URL with 240 items per page.
|
|
* Example: https://www.ebay.de/str/topmons?... → https://www.ebay.de/sch/i.html?_ssn=topmons&store_name=topmons&_ipg=240
|
|
*/
|
|
function transformAccountUrlTo240ItemsUrl(accountUrl) {
|
|
try {
|
|
const url = new URL(accountUrl);
|
|
const pathname = url.pathname;
|
|
|
|
const strMatch = pathname.match(/\/str\/([^\/\?]+)/);
|
|
if (!strMatch || !strMatch[1]) {
|
|
throw new Error("Could not extract account name from URL (expected /str/{name})");
|
|
}
|
|
|
|
const accountName = strMatch[1];
|
|
const baseUrl = `${url.protocol}//${url.hostname}`;
|
|
const listingUrl = `${baseUrl}/sch/i.html?_ssn=${encodeURIComponent(accountName)}&store_name=${encodeURIComponent(accountName)}&_ipg=240`;
|
|
return listingUrl;
|
|
} catch (e) {
|
|
throw new Error(`Failed to transform account URL: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles eBay product scan request
|
|
* Transforms account URL to listing URL with _ipg=240, creates hidden tab, parses, then loads each item in separate tab.
|
|
*/
|
|
async function handleScanProductsRequest(url, accountId, sendResponse) {
|
|
try {
|
|
// Validate URL
|
|
if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) {
|
|
sendResponse({ ok: false, error: "Invalid eBay URL" });
|
|
return;
|
|
}
|
|
|
|
let targetUrl = url;
|
|
try {
|
|
targetUrl = transformAccountUrlTo240ItemsUrl(url);
|
|
console.log("[BACKGROUND] Scan using listing URL (240 items):", targetUrl);
|
|
} catch (transformErr) {
|
|
console.warn("[BACKGROUND] URL transform failed, using original URL:", transformErr.message);
|
|
// Fallback: use original URL (e.g. already /sch/ or non-/str/)
|
|
}
|
|
|
|
// Create hidden tab
|
|
const tab = await chrome.tabs.create({
|
|
url: targetUrl,
|
|
active: false
|
|
});
|
|
if (!tab?.id) {
|
|
sendResponse({ ok: false, error: "Tab could not be created" });
|
|
return;
|
|
}
|
|
const tabId = tab.id;
|
|
|
|
// Set up timeout
|
|
const timeoutId = setTimeout(() => {
|
|
cleanupScanRequest(tabId, null, { ok: false, error: "timeout" });
|
|
}, SCAN_TIMEOUT_MS);
|
|
|
|
// Store request info
|
|
activeScanRequests.set(tabId, {
|
|
timeout: timeoutId,
|
|
sendResponse: sendResponse
|
|
});
|
|
|
|
currentScanProgress = { percent: 0, phase: "listing", total: 0, current: 0, complete: false };
|
|
|
|
// Wait for tab to load, then send parse message
|
|
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
|
|
if (updatedTabId !== tabId) return;
|
|
// Tab is fully loaded
|
|
if (changeInfo.status === 'complete' && updatedTab.url) {
|
|
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
|
// Small delay to ensure DOM is ready
|
|
setTimeout(() => {
|
|
sendScanMessageWithRetry(tabId, 0);
|
|
}, 1000); // 1 second delay for DOM ready
|
|
}
|
|
};
|
|
|
|
chrome.tabs.onUpdated.addListener(checkTabLoaded);
|
|
|
|
} catch (error) {
|
|
console.error("Error in handleScanProductsRequest:", error);
|
|
sendResponse({ ok: false, error: error.message || "Unknown error" });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends scan message to content script with retry mechanism
|
|
* @param {number} tabId - Tab ID
|
|
* @param {number} attempt - Current attempt number (0-based)
|
|
*/
|
|
async function sendScanMessageWithRetry(tabId, attempt) {
|
|
const maxAttempts = 3;
|
|
const retryDelay = 500; // 500ms between retries
|
|
|
|
try {
|
|
console.log(`[BACKGROUND] Sending scan message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId);
|
|
|
|
// On first attempt, ensure content script is injected
|
|
if (attempt === 0) {
|
|
const injected = await ensureContentScriptInjected(tabId);
|
|
if (!injected) {
|
|
throw new Error("Could not inject content script");
|
|
}
|
|
}
|
|
|
|
const response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" });
|
|
|
|
if (response && response.ok) {
|
|
// Prüfe ob items vorhanden und nicht leer
|
|
const items = response.items || response.data?.items || [];
|
|
const meta = response.meta || response.data?.meta || {};
|
|
|
|
// Log meta für Debugging
|
|
console.log("[SCAN meta]", meta);
|
|
|
|
if (items.length === 0) {
|
|
// Leere items: behandele als Fehler mit meta
|
|
cleanupScanRequest(tabId, null, {
|
|
ok: false,
|
|
error: "empty_items",
|
|
meta: meta
|
|
});
|
|
} else {
|
|
if (currentScanProgress) {
|
|
currentScanProgress.phase = "details";
|
|
currentScanProgress.total = items.length;
|
|
currentScanProgress.percent = 10;
|
|
}
|
|
await handleScanComplete(tabId, { items, meta });
|
|
}
|
|
} else {
|
|
// Fehler: sende error + meta
|
|
const meta = response?.meta || {};
|
|
console.log("[SCAN meta]", meta);
|
|
cleanupScanRequest(tabId, null, {
|
|
ok: false,
|
|
error: response?.error || "Parsing failed",
|
|
meta: meta
|
|
});
|
|
}
|
|
} catch (err) {
|
|
// Check if error is due to content script not being ready
|
|
const runtimeError = chrome.runtime.lastError?.message || "";
|
|
const isContentScriptError =
|
|
err.message?.includes("Could not establish connection") ||
|
|
err.message?.includes("Receiving end does not exist") ||
|
|
err.message?.includes("Could not inject content script") ||
|
|
runtimeError.includes("Receiving end does not exist") ||
|
|
runtimeError.includes("Could not establish connection");
|
|
|
|
console.log(`[BACKGROUND] Error sending scan message (attempt ${attempt + 1}):`, {
|
|
error: err.message,
|
|
runtimeError: runtimeError,
|
|
isContentScriptError: isContentScriptError
|
|
});
|
|
|
|
if (isContentScriptError && attempt < maxAttempts) {
|
|
// Content script not ready yet, retry after delay
|
|
console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`);
|
|
setTimeout(() => {
|
|
sendScanMessageWithRetry(tabId, attempt + 1);
|
|
}, retryDelay);
|
|
} else {
|
|
// Max retries reached or different error
|
|
const errorMessage = isContentScriptError
|
|
? "Content script konnte nicht geladen werden. Bitte Extension neu laden."
|
|
: (err.message || runtimeError || "Content script error");
|
|
cleanupScanRequest(tabId, null, {
|
|
ok: false,
|
|
error: errorMessage,
|
|
meta: {
|
|
pageType: "unknown",
|
|
finalUrl: "",
|
|
attempts: attempt + 1,
|
|
reason: "content_script_error"
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const ITEM_TAB_LOAD_TIMEOUT_MS = 3000;
|
|
|
|
/**
|
|
* Opens each item URL in a separate background tab, waits for load, parses detail page
|
|
* (title, price, currency, category, condition), merges into item, then closes tab.
|
|
*/
|
|
async function loadAndParseEachItemTab(items) {
|
|
if (!Array.isArray(items) || items.length === 0) return;
|
|
|
|
const total = items.length;
|
|
for (let i = 0; i < total; i++) {
|
|
const item = items[i];
|
|
if (!item || !item.url) continue;
|
|
|
|
try {
|
|
const tab = await chrome.tabs.create({
|
|
url: item.url,
|
|
active: false
|
|
});
|
|
if (!tab?.id) {
|
|
console.warn(`[BACKGROUND] Tab could not be created for item ${i + 1}, skipping`);
|
|
continue;
|
|
}
|
|
await new Promise((resolve) => {
|
|
const listener = (tabId, changeInfo) => {
|
|
if (tabId === tab.id && changeInfo.status === "complete") {
|
|
chrome.tabs.onUpdated.removeListener(listener);
|
|
resolve();
|
|
}
|
|
};
|
|
chrome.tabs.onUpdated.addListener(listener);
|
|
|
|
setTimeout(() => {
|
|
chrome.tabs.onUpdated.removeListener(listener);
|
|
resolve();
|
|
}, ITEM_TAB_LOAD_TIMEOUT_MS);
|
|
});
|
|
|
|
const injected = await ensureContentScriptInjected(tab.id);
|
|
if (!injected) {
|
|
console.warn(`[BACKGROUND] Content script not ready in item tab ${i + 1}, skipping detail parse`);
|
|
}
|
|
|
|
try {
|
|
const response = injected
|
|
? await chrome.tabs.sendMessage(tab.id, { action: "PARSE_ITEM_DETAIL" })
|
|
: null;
|
|
if (response?.ok && response?.data) {
|
|
const d = response.data;
|
|
if (d.title != null && d.title !== "") item.title = d.title;
|
|
if (d.price != null) item.price = d.price;
|
|
if (d.currency != null && d.currency !== "") item.currency = d.currency;
|
|
if (d.category != null && d.category !== "") item.category = d.category;
|
|
if (d.condition != null && d.condition !== "") item.condition = d.condition;
|
|
if (d.quantityAvailable != null) item.quantityAvailable = d.quantityAvailable;
|
|
if (d.quantitySold != null) item.quantitySold = d.quantitySold;
|
|
if (d.watchCount != null) item.watchCount = d.watchCount;
|
|
if (d.inCartsCount != null) item.inCartsCount = d.inCartsCount;
|
|
}
|
|
} catch (parseErr) {
|
|
console.warn(`[BACKGROUND] PARSE_ITEM_DETAIL failed for ${item.url}:`, parseErr);
|
|
}
|
|
|
|
if (currentScanProgress && total > 0) {
|
|
currentScanProgress.current = i + 1;
|
|
currentScanProgress.percent = Math.min(99, 10 + Math.round((90 * (i + 1)) / total));
|
|
}
|
|
|
|
try {
|
|
await chrome.tabs.remove(tab.id);
|
|
} catch (removeErr) {
|
|
console.warn(`[BACKGROUND] Could not close item tab:`, removeErr);
|
|
}
|
|
} catch (e) {
|
|
console.warn(`[BACKGROUND] Failed to load/parse item tab ${i + 1}/${total}:`, item.url, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles scan complete response from content script
|
|
*/
|
|
async function handleScanComplete(tabId, data) {
|
|
const request = activeScanRequests.get(tabId);
|
|
if (request && request.timeout) {
|
|
clearTimeout(request.timeout);
|
|
request.timeout = null;
|
|
}
|
|
|
|
const items = data?.items || [];
|
|
if (items.length > 0) {
|
|
await loadAndParseEachItemTab(items);
|
|
}
|
|
|
|
if (currentScanProgress) {
|
|
currentScanProgress.percent = 100;
|
|
currentScanProgress.complete = true;
|
|
currentScanProgress.current = items.length;
|
|
}
|
|
|
|
await cleanupScanRequest(tabId, data, null);
|
|
}
|
|
|
|
/**
|
|
* Cleans up scan request: closes tab, clears timeout, sends response
|
|
*/
|
|
async function cleanupScanRequest(tabId, data, error) {
|
|
if (tabId == null) return;
|
|
const request = activeScanRequests.get(tabId);
|
|
if (!request) return;
|
|
|
|
// Clear timeout
|
|
if (request.timeout) {
|
|
clearTimeout(request.timeout);
|
|
}
|
|
|
|
// Remove from active requests
|
|
activeScanRequests.delete(tabId);
|
|
|
|
currentScanProgress = null;
|
|
|
|
// Close tab (always, even on error)
|
|
try {
|
|
await chrome.tabs.remove(tabId);
|
|
} catch (err) {
|
|
// Tab might already be closed
|
|
console.warn("Could not close tab:", err);
|
|
}
|
|
|
|
// Send response
|
|
if (request.sendResponse) {
|
|
if (error) {
|
|
// error kann bereits meta enthalten
|
|
request.sendResponse(error);
|
|
} else if (data) {
|
|
// data kann items + meta enthalten
|
|
request.sendResponse({ ok: true, data: data });
|
|
} else {
|
|
request.sendResponse({ ok: false, error: "Unknown 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
|
|
});
|
|
if (!baseTab?.id) {
|
|
throw new Error("Tab could not be created");
|
|
}
|
|
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
|
|
});
|
|
if (!tab?.id) {
|
|
reject(new Error("Tab could not be created"));
|
|
return;
|
|
}
|
|
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] || "";
|
|
}
|
|
|
|
export async function callProtectedApi(path, payload) {
|
|
const jwt = await getJwt();
|
|
if (!jwt) throw new Error("Not authed");
|
|
|
|
const res = await fetch(`${BACKEND_URL}${path}`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"authorization": `Bearer ${jwt}`
|
|
},
|
|
body: JSON.stringify(payload || {})
|
|
});
|
|
|
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
return await res.json();
|
|
}
|