Update Extension files and add deploy script

This commit is contained in:
2026-01-18 17:31:18 +01:00
parent ed9a75a1dc
commit 86d2191a25
4 changed files with 664 additions and 16 deletions

View File

@@ -26,17 +26,39 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
}
if (message.action === "PARSE_PRODUCT_LIST") {
try {
const items = parseProductList();
sendResponse({ ok: true, data: { items } });
} catch (error) {
// Niemals unhandled throws - immer graceful response
console.error("Error parsing product list:", error);
sendResponse({
ok: false,
error: error.message || "Failed to 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
}
});
@@ -471,10 +493,259 @@ function parseEbayPage() {
}
/**
* Parst Produktliste von eBay Storefront oder Seller Listings
* @returns {Array} Array von Produkt-Items
* Erkennt den Seitentyp basierend auf URL und DOM
* @returns {string} Page Type: "storefront" | "seller_profile" | "feedback" | "search_results" | "unknown"
*/
function parseProductList() {
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>} 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 max 60 items (first page)
const result = parsedItems.slice(0, 60);
return {
ok: true,
items: result,
meta: {
pageType,
finalUrl,
attempts,
reason: reason || (result.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"
}
};
}
}
try {
const url = window.location.href;
const urlLower = url.toLowerCase();