v5: Mollie (EU) statt Stripe, Sitemap + llms-full.txt, /prognose-Methodik, ausgebauter Datenschutz/Transparenz, Scraper-Adapter toom/bauhaus/hornbach + compose/cron-ready, checkedAt-Stempel im Monitor
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
// Bauhaus-Adapter (Muster wie OBI: Produktseite → Preis via JSON-LD + Markt-Status-Heuristik).
|
||||
// TODO: products{} mit echten Produkt-URLs/Artikelnummern füllen; Per-Markt-Endpoint ergänzen.
|
||||
const UA = 'KuehlfinderBot/0.1 (+https://kuehlfinder.de/bot)';
|
||||
export const meta = { id: 'bauhaus', name: 'Bauhaus', kind: 'baumarkt' };
|
||||
export const products = { /* 'portasplit': 'https://www....' */ };
|
||||
function jsonld(html){const o=[];const re=/<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi;let m;while((m=re.exec(html))){try{o.push(JSON.parse(m[1]));}catch{}}return o;}
|
||||
function offer(ld){for(const n of (Array.isArray(ld)?ld:[ld])){const g=n['@graph']||[n];for(const x of g){if(x&&(x['@type']==='Product'||x.offers)){const of=Array.isArray(x.offers)?x.offers[0]:x.offers;if(of)return{price:Number(of.price??of.lowPrice),availability:String(of.availability||'')};}}}return null;}
|
||||
export async function fetchProduct(productId,{marketId}={}){
|
||||
const url=products[productId]; if(!url) return null;
|
||||
const res=await fetch(url,{headers:{'User-Agent':UA,'Accept-Language':'de-DE'}}); if(!res.ok) throw new Error('Bauhaus '+res.status);
|
||||
const html=await res.text(); const o=jsonld(html).map(offer).find(Boolean);
|
||||
const inStore=/vorrätig|verfügbar/i.test(html)&&!/nicht (vorrätig|verfügbar)/i.test(html);
|
||||
const price=o?.price ?? Number((html.match(/(\d{2,4})[,.](\d{2})\s*€/)||[])[0]?.replace(/[^\d,]/g,'').replace(',','.'));
|
||||
return { productId, source:'bauhaus', marketId:marketId??null, price: price||null,
|
||||
availability: inStore?'in_stock':(o&&/InStock/i.test(o.availability)?'online_available':'out_of_stock'),
|
||||
checkedAt:new Date().toISOString(), url };
|
||||
}
|
||||
export async function fetchStores(){ return []; }
|
||||
@@ -0,0 +1,18 @@
|
||||
// Hornbach-Adapter (Muster wie OBI: Produktseite → Preis via JSON-LD + Markt-Status-Heuristik).
|
||||
// TODO: products{} mit echten Produkt-URLs/Artikelnummern füllen; Per-Markt-Endpoint ergänzen.
|
||||
const UA = 'KuehlfinderBot/0.1 (+https://kuehlfinder.de/bot)';
|
||||
export const meta = { id: 'hornbach', name: 'Hornbach', kind: 'baumarkt' };
|
||||
export const products = { /* 'portasplit': 'https://www....' */ };
|
||||
function jsonld(html){const o=[];const re=/<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi;let m;while((m=re.exec(html))){try{o.push(JSON.parse(m[1]));}catch{}}return o;}
|
||||
function offer(ld){for(const n of (Array.isArray(ld)?ld:[ld])){const g=n['@graph']||[n];for(const x of g){if(x&&(x['@type']==='Product'||x.offers)){const of=Array.isArray(x.offers)?x.offers[0]:x.offers;if(of)return{price:Number(of.price??of.lowPrice),availability:String(of.availability||'')};}}}return null;}
|
||||
export async function fetchProduct(productId,{marketId}={}){
|
||||
const url=products[productId]; if(!url) return null;
|
||||
const res=await fetch(url,{headers:{'User-Agent':UA,'Accept-Language':'de-DE'}}); if(!res.ok) throw new Error('Hornbach '+res.status);
|
||||
const html=await res.text(); const o=jsonld(html).map(offer).find(Boolean);
|
||||
const inStore=/vorrätig|verfügbar/i.test(html)&&!/nicht (vorrätig|verfügbar)/i.test(html);
|
||||
const price=o?.price ?? Number((html.match(/(\d{2,4})[,.](\d{2})\s*€/)||[])[0]?.replace(/[^\d,]/g,'').replace(',','.'));
|
||||
return { productId, source:'hornbach', marketId:marketId??null, price: price||null,
|
||||
availability: inStore?'in_stock':(o&&/InStock/i.test(o.availability)?'online_available':'out_of_stock'),
|
||||
checkedAt:new Date().toISOString(), url };
|
||||
}
|
||||
export async function fetchStores(){ return []; }
|
||||
@@ -0,0 +1,18 @@
|
||||
// Toom-Adapter (Muster wie OBI: Produktseite → Preis via JSON-LD + Markt-Status-Heuristik).
|
||||
// TODO: products{} mit echten Produkt-URLs/Artikelnummern füllen; Per-Markt-Endpoint ergänzen.
|
||||
const UA = 'KuehlfinderBot/0.1 (+https://kuehlfinder.de/bot)';
|
||||
export const meta = { id: 'toom', name: 'Toom', kind: 'baumarkt' };
|
||||
export const products = { /* 'portasplit': 'https://www....' */ };
|
||||
function jsonld(html){const o=[];const re=/<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi;let m;while((m=re.exec(html))){try{o.push(JSON.parse(m[1]));}catch{}}return o;}
|
||||
function offer(ld){for(const n of (Array.isArray(ld)?ld:[ld])){const g=n['@graph']||[n];for(const x of g){if(x&&(x['@type']==='Product'||x.offers)){const of=Array.isArray(x.offers)?x.offers[0]:x.offers;if(of)return{price:Number(of.price??of.lowPrice),availability:String(of.availability||'')};}}}return null;}
|
||||
export async function fetchProduct(productId,{marketId}={}){
|
||||
const url=products[productId]; if(!url) return null;
|
||||
const res=await fetch(url,{headers:{'User-Agent':UA,'Accept-Language':'de-DE'}}); if(!res.ok) throw new Error('Toom '+res.status);
|
||||
const html=await res.text(); const o=jsonld(html).map(offer).find(Boolean);
|
||||
const inStore=/vorrätig|verfügbar/i.test(html)&&!/nicht (vorrätig|verfügbar)/i.test(html);
|
||||
const price=o?.price ?? Number((html.match(/(\d{2,4})[,.](\d{2})\s*€/)||[])[0]?.replace(/[^\d,]/g,'').replace(',','.'));
|
||||
return { productId, source:'toom', marketId:marketId??null, price: price||null,
|
||||
availability: inStore?'in_stock':(o&&/InStock/i.test(o.availability)?'online_available':'out_of_stock'),
|
||||
checkedAt:new Date().toISOString(), url };
|
||||
}
|
||||
export async function fetchStores(){ return []; }
|
||||
Reference in New Issue
Block a user