import { createHash } from 'node:crypto'; import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import sharp from 'sharp'; import type { Config } from '../lib/config.ts'; // Фото для страницы клиента (логика из бота list-am-bot, image/crop.py): фото скачивается, // сверху срезается полоса в процентах от высоты, результат отдаётся в JPEG и кладётся на диск, // чтобы второй раз не скачивать и не обрезать. export type ImageFetcher = (url: string) => Promise; export const fetchImage: ImageFetcher = async (url) => { const res = await fetch(url, { headers: { referer: 'https://www.list.am/', 'user-agent': 'Mozilla/5.0 (compatible; LeadRadar/1.0)' }, signal: AbortSignal.timeout(20_000), }); if (!res.ok) throw new Error(`Фото не скачалось: ${res.status}`); const data = Buffer.from(await res.arrayBuffer()); if (data.length > 15 * 1024 * 1024) throw new Error('Фото слишком большое'); return data; }; /** Срезать сверху `percent` % высоты и вернуть JPEG. */ export async function cropTop(data: Buffer, percent: number): Promise { const image = sharp(data).rotate(); const { width, height } = await image.metadata(); const cut = width && height ? Math.floor((height * percent) / 100) : 0; const cropped = cut > 0 && cut < height! ? image.extract({ left: 0, top: cut, width: width!, height: height! - cut }) : image; return cropped.jpeg({ quality: 88, mozjpeg: true }).toBuffer(); } /** Обработанное фото: с диска, если уже готово, иначе скачать, обрезать и сохранить. */ export async function croppedPhoto(config: Config, url: string, fetcher: ImageFetcher = fetchImage): Promise { const name = `${createHash('sha256').update(`${url}|${config.cropTopPercent}`).digest('hex').slice(0, 32)}.jpg`; const dir = join(config.mediaDir, name.slice(0, 2)); const file = join(dir, name); const cached = await readFile(file).catch(() => null); if (cached) return cached; const result = await cropTop(await fetcher(url), config.cropTopPercent); await mkdir(dir, { recursive: true }); // Пишем во временный файл и переименовываем: параллельный запрос не увидит половину файла. const tmp = `${file}.${process.pid}.${Date.now()}.tmp`; await writeFile(tmp, result); await rename(tmp, file); return result; }