| 83 |
83 |
|
| 84 |
84 |
/* --------------------------------------------------------------- fetch */ |
| 85 |
85 |
|
|
86 |
+/** En-têtes ASCII seulement : un octet accentué dans le User-Agent fait répondre |
|
87 |
+ * HTTP 400 aux CDN stricts (Facebook Marketplace `*.fbcdn.net`, 2026-09-08). */ |
|
88 |
+const UA_BOT = "Vrai-Prix/1.0 (building analysis; +https://www.vrai-prix.com)"; |
|
89 |
+const UA_BROWSER = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; |
|
90 |
+const ACCEPT_IMG = "image/avif,image/webp,image/apng,image/*,*/*;q=0.8"; |
|
91 |
+ |
|
92 |
+/** Referer plausible pour les CDN qui l'exigent (photos servies depuis un domaine distinct du site). */ |
|
93 |
+export function refererFor(url: string): string { |
|
94 |
+ try { |
|
95 |
+ const h = new URL(url).hostname; |
|
96 |
+ if (/\.fbcdn\.net$|facebook\.com$/.test(h)) return "https://www.facebook.com/"; |
|
97 |
+ if (/ebayimg\.com$|kijiji/.test(h)) return "https://www.kijiji.ca/"; |
|
98 |
+ if (/centris\.ca$/.test(h)) return "https://www.centris.ca/"; |
|
99 |
+ if (/duproprio\.com$/.test(h)) return "https://duproprio.com/"; |
|
100 |
+ const parts = h.split("."); |
|
101 |
+ return `https://www.${parts.slice(-2).join(".")}/`; |
|
102 |
+ } catch { |
|
103 |
+ return "https://www.vrai-prix.com/"; |
|
104 |
+ } |
|
105 |
+} |
|
106 |
+ |
| 86 |
107 |
export async function fetchImage(url: string, timeoutMs = FETCH_TIMEOUT_MS): Promise<{ data: Buffer; mediaType: ImageMediaType }> { |
| 87 |
108 |
const ctrl = new AbortController(); |
| 88 |
109 |
const t = setTimeout(() => ctrl.abort(), timeoutMs); |
| 89 |
110 |
try { |
| 90 |
|
− const res = await fetch(url, { signal: ctrl.signal, headers: { "User-Agent": "Vrai-Prix/1.0 (analyse du bâtiment; +https://www.vrai-prix.com)", Accept: "image/*" }, redirect: "follow" }); |
| 91 |
|
− if (!res.ok) throw new Error(`HTTP ${res.status}`); |
|
111 |
+ // 1er essai : identité déclarée (bot) ; 2e essai (4xx) : en-têtes de navigateur + Referer. |
|
112 |
+ let res = await fetch(url, { signal: ctrl.signal, headers: { "User-Agent": UA_BOT, Accept: ACCEPT_IMG }, redirect: "follow" }); |
|
113 |
+ if (!res.ok && res.status >= 400 && res.status < 500) { |
|
114 |
+ res = await fetch(url, { |
|
115 |
+ signal: ctrl.signal, |
|
116 |
+ redirect: "follow", |
|
117 |
+ headers: { "User-Agent": UA_BROWSER, Accept: ACCEPT_IMG, "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8", Referer: refererFor(url), "Sec-Fetch-Dest": "image", "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Site": "cross-site" }, |
|
118 |
+ }); |
|
119 |
+ } |
|
120 |
+ if (!res.ok) throw new Error(`HTTP ${res.status} (${new URL(url).hostname})`); |
| 92 |
121 |
const len = Number(res.headers.get("content-length") ?? 0); |
| 93 |
122 |
if (len > MAX_IMAGE_BYTES) throw new Error(`trop volumineuse (${len} octets)`); |
| 94 |
123 |
const ab = await res.arrayBuffer(); |
| 95 |
124 |
|