Lead Radar: исходный код
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
9749da5ae7
201 files changed
+97685
No files matched your search
@@ -0,0 +1,47 @@
|
||||
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<Buffer>;
|
||||
|
||||
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<Buffer> {
|
||||
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<Buffer> {
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user