385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
import CONSTANTS from "./constants.js";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { updateStatusWork } from "./apis/bid.js";
|
|
import _ from "lodash";
|
|
|
|
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
|
|
|
|
export const takeSnapshot = async (
|
|
page,
|
|
item,
|
|
imageName,
|
|
type = CONSTANTS.TYPE_IMAGE.ERRORS
|
|
) => {
|
|
if (!page || page.isClosed()) return;
|
|
|
|
try {
|
|
const baseDir = path.join(
|
|
CONSTANTS.ERROR_IMAGES_PATH,
|
|
item.type,
|
|
String(item.id)
|
|
); // Thư mục theo lot_id
|
|
const typeDir = path.join(baseDir, type); // Thư mục con theo type
|
|
|
|
// Tạo tên file, nếu type === 'work' thì không có timestamp
|
|
const fileName =
|
|
type === CONSTANTS.TYPE_IMAGE.WORK
|
|
? `${imageName}.png`
|
|
: `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
|
|
|
|
const filePath = path.join(typeDir, fileName);
|
|
|
|
// Kiểm tra và tạo thư mục nếu chưa tồn tại
|
|
if (!fs.existsSync(typeDir)) {
|
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
console.log(`📂 Save at folder: ${typeDir}`);
|
|
}
|
|
|
|
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
|
// Kiểm tra có thể điều hướng trang không
|
|
const isPageResponsive = await page.evaluate(
|
|
() => document.readyState === "complete"
|
|
);
|
|
if (!isPageResponsive) {
|
|
console.log("🚫 Page is unresponsive, skipping snapshot.");
|
|
return;
|
|
}
|
|
|
|
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
|
|
await page
|
|
.waitForSelector("body", { visible: true, timeout: 15000 })
|
|
.catch(() => {
|
|
console.log("⚠️ Body selector not found, skipping snapshot.");
|
|
return;
|
|
});
|
|
|
|
// Chụp ảnh màn hình và lưu vào filePath
|
|
await page.screenshot({ path: filePath });
|
|
|
|
console.log(`📸 Image saved at: ${filePath}`);
|
|
|
|
// Nếu type === 'work', gửi ảnh lên API
|
|
if (type === CONSTANTS.TYPE_IMAGE.WORK) {
|
|
await updateStatusWork(item, filePath);
|
|
}
|
|
} catch (error) {
|
|
console.log("Error when snapshot: " + error.message);
|
|
}
|
|
};
|
|
|
|
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
export const safeClosePageReal = async (page) => {
|
|
if (!page) return;
|
|
|
|
try {
|
|
if (page.isClosed()) {
|
|
console.log(`✅ Page already closed: ${page.url()}`);
|
|
return;
|
|
}
|
|
|
|
page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close
|
|
await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn
|
|
console.log(`✅ Successfully closed page: ${page.url()}`);
|
|
} catch (err) {
|
|
console.warn(
|
|
`⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}`
|
|
);
|
|
}
|
|
};
|
|
|
|
export async function safeClosePage(item) {
|
|
try {
|
|
const page = item.page_context;
|
|
|
|
if (page && !page.isClosed() && page.close) {
|
|
await safeClosePageReal(page);
|
|
}
|
|
|
|
if (item?.page_context) {
|
|
item.page_context = undefined;
|
|
}
|
|
|
|
if (item?.browser_context) {
|
|
try {
|
|
await item.browser_context.close();
|
|
} catch (ctxErr) {
|
|
console.warn(
|
|
`⚠️ Failed to close browser context for item ${item.id}: ${ctxErr.message}`
|
|
);
|
|
}
|
|
item.browser_context = undefined;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`⚠️ Can't close item ${item?.id}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
export function isTimeReached(targetTime) {
|
|
if (!targetTime) return false;
|
|
|
|
const targetDate = new Date(targetTime);
|
|
const now = new Date();
|
|
|
|
return now >= targetDate;
|
|
}
|
|
|
|
export function extractNumber(str) {
|
|
const match = str.match(/\d+(\.\d+)?/);
|
|
return match ? parseFloat(match[0]) : null;
|
|
}
|
|
|
|
export const sanitizeFileName = (url) => {
|
|
return url.replace(/[:\/]/g, "_");
|
|
};
|
|
|
|
export const getPathProfile = (origin_url) => {
|
|
return path.join(
|
|
CONSTANTS.PROFILE_PATH,
|
|
sanitizeFileName(origin_url) + ".json"
|
|
);
|
|
};
|
|
|
|
export const getPathLocalData = (origin_url) => {
|
|
return path.join(
|
|
CONSTANTS.LOCAL_DATA_PATH,
|
|
sanitizeFileName(origin_url) + ".json"
|
|
);
|
|
};
|
|
|
|
export function removeFalsyValues(obj, excludeKeys = []) {
|
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
if (value || excludeKeys.includes(key)) {
|
|
acc[key] = value;
|
|
}
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
export const enableAutoBidMessage = (data) => {
|
|
return `
|
|
<b>⭉ Activate Auto Bid</b><br>
|
|
📌 Product: <b>${data.name}</b><br>
|
|
🔗 Link: <a href="${data.url}">Click here</a><br>
|
|
💰 Max Price: <b>$${data.max_price}</b><br>
|
|
🌐 Platform: <a href="${data.web_bid.origin_url}">Langtons</a>
|
|
`;
|
|
};
|
|
|
|
export function convertAETtoUTC(dateString) {
|
|
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
|
|
const monthMap = {
|
|
Jan: 0,
|
|
Feb: 1,
|
|
Mar: 2,
|
|
Apr: 3,
|
|
May: 4,
|
|
Jun: 5,
|
|
Jul: 6,
|
|
Aug: 7,
|
|
Sep: 8,
|
|
Oct: 9,
|
|
Nov: 10,
|
|
Dec: 11,
|
|
};
|
|
|
|
// Tách chuỗi đầu vào
|
|
const parts = dateString.match(
|
|
/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/
|
|
);
|
|
if (!parts) {
|
|
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
|
|
}
|
|
|
|
const [, , day, month, year, hour, period] = parts;
|
|
|
|
// Chuyển đổi giờ sang định dạng 24h
|
|
let hours = parseInt(hour, 10);
|
|
if (period === "PM" && hours !== 12) hours += 12;
|
|
if (period === "AM" && hours === 12) hours = 0;
|
|
|
|
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
|
|
const date = new Date(
|
|
Date.UTC(
|
|
parseInt(year, 10),
|
|
monthMap[month],
|
|
parseInt(day, 10),
|
|
hours,
|
|
0,
|
|
0
|
|
)
|
|
);
|
|
|
|
// Hàm kiểm tra DST cho AET
|
|
function isDST(date) {
|
|
const year = date.getUTCFullYear();
|
|
const month = date.getUTCMonth();
|
|
const day = date.getUTCDate();
|
|
|
|
// DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
|
|
const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
|
|
dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
|
|
const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
|
|
|
|
// DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
|
|
const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
|
|
dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
|
|
const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
|
|
|
|
const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
|
|
return currentTime >= dstStartTime && currentTime < dstEndTime;
|
|
}
|
|
|
|
// Xác định offset dựa trên DST
|
|
const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
|
|
|
|
// Điều chỉnh thời gian về UTC
|
|
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
|
|
|
|
// Trả về chuỗi UTC
|
|
return utcDate.toUTCString();
|
|
}
|
|
|
|
export function extractPriceNumber(priceString) {
|
|
const cleaned = priceString.replace(/[^\d.]/g, "");
|
|
return parseFloat(cleaned);
|
|
}
|
|
|
|
export function findEarlyLoginTime(webBid) {
|
|
const now = new Date();
|
|
|
|
// Bước 1: Lọc ra những bid có close_time hợp lệ
|
|
const validChildren = webBid.children.filter((child) => child.close_time);
|
|
|
|
if (validChildren.length === 0) return null;
|
|
|
|
// Bước 2: Tìm bid có close_time gần hiện tại nhất
|
|
const closestBid = validChildren.reduce((closest, current) => {
|
|
const closestDiff = Math.abs(
|
|
new Date(closest.close_time).getTime() - now.getTime()
|
|
);
|
|
const currentDiff = Math.abs(
|
|
new Date(current.close_time).getTime() - now.getTime()
|
|
);
|
|
return currentDiff < closestDiff ? current : closest;
|
|
});
|
|
|
|
if (!closestBid.close_time) return null;
|
|
|
|
// Bước 3: Tính toán thời gian login sớm
|
|
const closeTime = new Date(closestBid.close_time);
|
|
closeTime.setSeconds(
|
|
closeTime.getSeconds() - (webBid.early_login_seconds || 0)
|
|
);
|
|
|
|
return closeTime.toISOString();
|
|
}
|
|
|
|
export function subtractMinutes(time, minutes) {
|
|
const date = new Date(time);
|
|
date.setMinutes(date.getMinutes() - minutes);
|
|
return date.toUTCString();
|
|
}
|
|
|
|
export function subtractSeconds(time, seconds) {
|
|
const date = new Date(time);
|
|
date.setSeconds(date.getSeconds() - seconds);
|
|
return date.toUTCString();
|
|
}
|
|
|
|
export async function isPageAvailable(page) {
|
|
if (!page || page.isClosed()) return false;
|
|
|
|
try {
|
|
await Promise.race([
|
|
page.title(), // hoặc page.url(), evaluate, vv
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error("Timeout")), 1000)
|
|
),
|
|
]);
|
|
return true;
|
|
} catch (err) {
|
|
console.warn(`⚠️ Page not available: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Lấy product có close time gần với hiện tại nhất
|
|
export function findNearestClosingChild(webBid) {
|
|
const now = Date.now();
|
|
|
|
const validChildren = webBid.children.filter((child) => {
|
|
return (
|
|
child.close_time &&
|
|
!isNaN(new Date(child.close_time).getTime()) &&
|
|
typeof child.getEarlyTrackingSeconds === "function"
|
|
);
|
|
});
|
|
|
|
if (validChildren.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// 🎯 Tìm con gần nhất với hiện tại, ưu tiên early_tracking lớn nhất
|
|
const nearestChild = _.minBy(validChildren, (child) => {
|
|
const closeTime = new Date(child.close_time).getTime();
|
|
const timeDiff = Math.abs(closeTime - now);
|
|
const earlyTracking = child.getEarlyTrackingSeconds() || 0;
|
|
|
|
// ❗ Giảm ảnh hưởng của earlyTracking bằng cách trừ vào timeDiff
|
|
// => càng nhiều earlyTracking thì càng tốt (timeDiff - earlyTracking * trọng số)
|
|
return timeDiff - earlyTracking * 1000; // dùng 1000 để ưu tiên rõ rệt
|
|
});
|
|
|
|
return nearestChild || null;
|
|
}
|
|
|
|
export function extractModelId(url) {
|
|
try {
|
|
switch (extractDomain(url)) {
|
|
case "https://www.grays.com": {
|
|
const match = url.match(/\/lot\/([\d-]+)\//);
|
|
return match ? match[1] : null;
|
|
}
|
|
case "https://www.langtons.com.au": {
|
|
const match = url.match(/auc-var-\d+/);
|
|
return match[0];
|
|
}
|
|
case "https://www.lawsons.com.au": {
|
|
const match = url.split("_");
|
|
return match ? match[1] : null;
|
|
}
|
|
case "https://www.pickles.com.au": {
|
|
const model = url.split("/").pop();
|
|
return model ? model : null;
|
|
}
|
|
case "https://www.allbids.com.au": {
|
|
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
|
return match ? match[1] : null;
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function extractDomain(url) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
return parsedUrl.origin;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function randomDelayWithMeta(min = 1000, max = 10000) {
|
|
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
return {
|
|
delay,
|
|
wait: new Promise((resolve) => setTimeout(resolve, delay)),
|
|
};
|
|
}
|