fix: KöPi - Bilder als Base64, Adresse statisch, Duplikate zusammengefasst
This commit is contained in:
@@ -16,11 +16,28 @@ const MG_SETTINGS = encodeURIComponent(JSON.stringify({
|
|||||||
|
|
||||||
// ── Gewünschte Händler ────────────────────────────────────────────────────────
|
// ── Gewünschte Händler ────────────────────────────────────────────────────────
|
||||||
const TARGET_RETAILERS = ['edeka','e center','rewe','netto','kaufland','penny','trinkgut'];
|
const TARGET_RETAILERS = ['edeka','e center','rewe','netto','kaufland','penny','trinkgut'];
|
||||||
|
const RETAILER_ADDRESSES = {
|
||||||
|
'edeka': 'Düsseldorfer Landstr. 361',
|
||||||
|
'e center': 'Düsseldorfer Landstr. 361',
|
||||||
|
'rewe': 'Mündelheimer Str. 132 / Keniastr. 39',
|
||||||
|
'netto': 'Im Bonnefeld 19-21',
|
||||||
|
'kaufland': 'Auf der Höhe 20',
|
||||||
|
'penny': 'Sittardsberger Allee 10',
|
||||||
|
'trinkgut': 'Keniastraße 39',
|
||||||
|
};
|
||||||
function isTargetRetailer(name) {
|
function isTargetRetailer(name) {
|
||||||
if (!name) return false;
|
if (!name) return false;
|
||||||
const n = name.toLowerCase();
|
const n = name.toLowerCase();
|
||||||
return TARGET_RETAILERS.some(r => n.includes(r));
|
return TARGET_RETAILERS.some(r => n.includes(r));
|
||||||
}
|
}
|
||||||
|
function getAddress(retailer) {
|
||||||
|
if (!retailer) return '';
|
||||||
|
const n = retailer.toLowerCase();
|
||||||
|
for (const [key, addr] of Object.entries(RETAILER_ADDRESSES)) {
|
||||||
|
if (n.includes(key)) return addr;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Angebote via Puppeteer von marktguru ─────────────────────────────────────
|
// ── Angebote via Puppeteer von marktguru ─────────────────────────────────────
|
||||||
async function scrapeMarktguru() {
|
async function scrapeMarktguru() {
|
||||||
@@ -67,13 +84,28 @@ async function scrapeMarktguru() {
|
|||||||
const description = descEl?.textContent?.trim() || '';
|
const description = descEl?.textContent?.trim() || '';
|
||||||
const img = card.querySelector('img.offer-list-item-img');
|
const img = card.querySelector('img.offer-list-item-img');
|
||||||
const badge = card.querySelector('.badge, .discount-badge')?.textContent?.trim() || '';
|
const badge = card.querySelector('.badge, .discount-badge')?.textContent?.trim() || '';
|
||||||
const address = card.querySelector('.retailer-address, .address, dd.address, [class*="address"]')?.textContent?.trim() || '';
|
const imgSrc = img?.src || null;
|
||||||
// Bild: medium → large für bessere Qualität
|
return { name, brand, price, oldPrice, retailer, dateRange, validity, description, badge, address: '', image: imgSrc };
|
||||||
const imgSrc = img?.src?.replace('/medium.webp', '/large.webp') || img?.src || null;
|
|
||||||
return { name, brand, price, oldPrice, retailer, dateRange, validity, description, badge, address, image: imgSrc };
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bilder als Base64 laden (cdn.marktguru.de blockt externe Requests)
|
||||||
|
for (const offer of offers) {
|
||||||
|
if (!offer.image) continue;
|
||||||
|
try {
|
||||||
|
const imgData = await page.evaluate(async (url) => {
|
||||||
|
const resp = await fetch(url);
|
||||||
|
const blob = await resp.blob();
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => resolve(reader.result);
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}, offer.image);
|
||||||
|
offer.image = imgData; // base64 data URL
|
||||||
|
} catch { offer.image = null; }
|
||||||
|
}
|
||||||
|
|
||||||
// Filtern auf Ziel-Händler + nur König Pilsener
|
// Filtern auf Ziel-Händler + nur König Pilsener
|
||||||
const filtered = offers.filter(o => {
|
const filtered = offers.filter(o => {
|
||||||
const isKP = (o.name + ' ' + o.brand + ' ' + o.description).toLowerCase();
|
const isKP = (o.name + ' ' + o.brand + ' ' + o.description).toLowerCase();
|
||||||
@@ -81,13 +113,23 @@ async function scrapeMarktguru() {
|
|||||||
return hasKP && isTargetRetailer(o.retailer);
|
return hasKP && isTargetRetailer(o.retailer);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deduplizieren
|
// Deduplizieren: gleicher Händler + gleicher Preis → zusammenfassen
|
||||||
const seen = new Set();
|
const merged = new Map();
|
||||||
const unique = filtered.filter(o => {
|
for (const o of filtered) {
|
||||||
const k = `${o.retailer}|${o.price}|${o.description?.slice(0,30)}`;
|
const key = `${o.retailer}|${o.price}`;
|
||||||
if (seen.has(k)) return false;
|
if (merged.has(key)) {
|
||||||
seen.add(k); return true;
|
// Beschreibungen zusammenführen falls unterschiedlich
|
||||||
});
|
const existing = merged.get(key);
|
||||||
|
if (o.description && o.description !== existing.description) {
|
||||||
|
existing.description = existing.description
|
||||||
|
? `${existing.description} / ${o.description}`
|
||||||
|
: o.description;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged.set(key, { ...o });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unique = [...merged.values()].map(o => ({ ...o, address: getAddress(o.retailer) }));
|
||||||
|
|
||||||
const result = { offers: unique, total: unique.length, scrapedAt: new Date().toISOString(), source: 'marktguru.de' };
|
const result = { offers: unique, total: unique.length, scrapedAt: new Date().toISOString(), source: 'marktguru.de' };
|
||||||
cacheSet(cacheKey, result);
|
cacheSet(cacheKey, result);
|
||||||
@@ -170,15 +212,15 @@ async function scrapeProspekte() {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bild-Proxy ────────────────────────────────────────────────────────────────
|
// ── Bild-Proxy (für kaufda Bilder) ───────────────────────────────────────────
|
||||||
router.get('/img', authenticate, (req, res) => {
|
router.get('/img', authenticate, (req, res) => {
|
||||||
const url = req.query.url;
|
const url = req.query.url;
|
||||||
if (!url || !url.startsWith('https://')) return res.status(400).end();
|
if (!url || !url.startsWith('https://')) return res.status(400).end();
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
const allowed = ['content-media.bonial.biz','publisher-media.bonial.biz','cdn.marktguru.de','www.marktguru.de'];
|
const allowed = ['content-media.bonial.biz','publisher-media.bonial.biz'];
|
||||||
if (!allowed.some(h => parsed.hostname === h)) return res.status(403).end();
|
if (!allowed.some(h => parsed.hostname === h)) return res.status(403).end();
|
||||||
const r = https.get(url, { headers: {'User-Agent':'Mozilla/5.0','Referer':'https://www.marktguru.de/','Origin':'https://www.marktguru.de'} }, imgRes => {
|
const r = https.get(url, { headers: {'User-Agent':'Mozilla/5.0'} }, imgRes => {
|
||||||
res.setHeader('Content-Type', imgRes.headers['content-type'] || 'image/jpeg');
|
res.setHeader('Content-Type', imgRes.headers['content-type'] || 'image/jpeg');
|
||||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
imgRes.pipe(res);
|
imgRes.pipe(res);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function OfferCard({ offer }) {
|
|||||||
{offer.image && (
|
{offer.image && (
|
||||||
<div style={{ height:160, background:'#f5f5f5', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, overflow:'hidden', position:'relative' }}>
|
<div style={{ height:160, background:'#f5f5f5', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, overflow:'hidden', position:'relative' }}>
|
||||||
<img
|
<img
|
||||||
src={`/api/tools/koepi/img?url=${encodeURIComponent(offer.image)}`}
|
src={offer.image}
|
||||||
alt="König Pilsener"
|
alt="König Pilsener"
|
||||||
style={{ maxHeight:'100%', maxWidth:'100%', objectFit:'contain' }}
|
style={{ maxHeight:'100%', maxWidth:'100%', objectFit:'contain' }}
|
||||||
onError={e => { e.target.parentElement.style.display='none'; }}
|
onError={e => { e.target.parentElement.style.display='none'; }}
|
||||||
|
|||||||
Reference in New Issue
Block a user