Compare commits

...

12 Commits

Author SHA1 Message Date
Norbert Maciaszek
bcb7ae01b3 fix: install npm Dockerfile 2025-11-27 23:14:13 +01:00
Norbert Maciaszek
11f120ce2f feat: Introduce Dockerization and shift product data sourcing from external API to local files. 2025-11-27 23:02:01 +01:00
Norbert Maciaszek
f09b58fb63 Integrate Playwright for improved web scraping by launching a headless browser, enhancing product data retrieval with better error handling and dynamic content loading. Update logging for price saving confirmation. 2025-11-20 20:33:04 +01:00
Norbert Maciaszek
a25ab727b9 Add console log for first run completion in price comparison function to enhance debugging and monitoring. 2025-11-20 20:02:18 +01:00
Norbert Maciaszek
bf60325fac Add initial price notifications for products and adjust cron schedule to run every 15 minutes. Implement first run logic to send initial prices. 2025-11-20 19:54:29 +01:00
Norbert Maciaszek
63bae4f805 Refactor product ID retrieval to dynamically fetch from API instead of hardcoding values, enhancing flexibility for future updates. 2025-11-16 19:22:16 +01:00
Norbert Maciaszek
72ba9aae20 Refactor price monitoring to work exclusively with ceneo.pl, removing unused selectors and browser automation. Update product comparison logic to match by name and include shop information in notifications. 2025-11-16 18:40:36 +01:00
Norbert Maciaszek
1f41be62f1 Update cron schedule to run at the top of the hour and set timezone to Europe/Warsaw 2025-11-15 22:16:27 +01:00
Norbert Maciaszek
a64edaf22f Initialize productsPrice.json if it doesn't exist and update log message to Polish 2025-11-15 20:36:05 +01:00
Norbert Maciaszek
1559e4ccc9 Add logging for price checking and execute scheduled task immediately 2025-11-15 20:13:38 +01:00
Norbert Maciaszek
b62389085f Remove headless option from Chromium launch in init function 2025-11-15 20:10:16 +01:00
Norbert Maciaszek
beb1cf7bdc Add sendMessage function for Discord notifications 2025-11-15 20:06:29 +01:00
3 changed files with 250 additions and 86 deletions

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:22-slim
WORKDIR /app
COPY package*.json .
RUN npm install
RUN npx playwright install-deps
RUN npx playwright install chromium
COPY . .
CMD ["node", "index.js"]

167
index.js
View File

@@ -1,33 +1,32 @@
const cheerio = require("cheerio"); const cheerio = require("cheerio");
const cron = require("node-cron"); const cron = require("node-cron");
const axios = require("axios"); const axios = require("axios");
const { chromium } = require("playwright");
const fs = require("fs"); const fs = require("fs");
const { chromium } = require("playwright");
const discordWebhook = const discordWebhook =
"https://discord.com/api/webhooks/1439286509390921749/t2Hb8XloF6zhDRYD1yh_QlkHHa9eHUyXvd9TxZRHwqR_b_OxxbnwDgsm4em8TwA9NQIa"; "https://discord.com/api/webhooks/1439286509390921749/t2Hb8XloF6zhDRYD1yh_QlkHHa9eHUyXvd9TxZRHwqR_b_OxxbnwDgsm4em8TwA9NQIa";
const priceSelectors = { function sendMessage(message) {
miodowamydlarnia: ".projector_prices__price", axios.post(discordWebhook, {
greentouch: ".main-price", content: message,
amazon: "#corePrice_feature_div .a-price .a-offscreen", });
soxo: "#projector_price_value span", }
empik: '[data-ta-section="priceMainContainer"] [data-ta="price"]',
notino: "#pd-price",
};
const excludePage = ["allegro", "homla.com.pl", "home-you.com"];
let isFirstRun = true;
async function compareAndSave(productsPrice) { async function compareAndSave(productsPrice) {
const productsPriceJson = if (!fs.existsSync("productsPrice.json")) {
fs.readFileSync("productsPrice.json", "utf8") || "[]"; fs.writeFileSync("productsPrice.json", "[]");
}
const productsPriceJson = fs.readFileSync("productsPrice.json", "utf8");
const oldProductsPrice = JSON.parse(productsPriceJson); const oldProductsPrice = JSON.parse(productsPriceJson);
const diffProducts = []; const diffProducts = [];
for (const product of productsPrice) { for (const product of productsPrice) {
const oldProduct = oldProductsPrice.find( const oldProduct = oldProductsPrice.find(
(oldProduct) => oldProduct.link === product.link (oldProduct) => oldProduct.name === product.name
); );
if (oldProduct && oldProduct.price !== product.price) { if (oldProduct && oldProduct.price !== product.price) {
@@ -36,20 +35,22 @@ async function compareAndSave(productsPrice) {
newPrice: product.price, newPrice: product.price,
oldPrice: oldProduct.price, oldPrice: oldProduct.price,
link: product.link, link: product.link,
shop: product.shop,
}); });
} }
} }
for (const product of diffProducts) { for (const product of diffProducts) {
await axios.post(discordWebhook, { sendMessage(
content: `Zmiana ceny **${product.name}**:\nCena: ${product.oldPrice} -> ${product.newPrice}\nLink: ${product.link}`, `Zmiana ceny **${product.name}**: ${product.oldPrice} -> ${product.newPrice}\nLink: ${product.link}`
}); );
} }
if (diffProducts.length === 0) { if (isFirstRun) {
await axios.post(discordWebhook, { for (const product of productsPrice) {
content: "Brak zmian w cenach", sendMessage(`Początkowa cena **${product.name}**: ${product.price}`);
}); }
isFirstRun = false;
} }
fs.writeFileSync( fs.writeFileSync(
@@ -59,84 +60,80 @@ async function compareAndSave(productsPrice) {
} }
async function getProducts() { async function getProducts() {
const products = await axios const products = fs.readFileSync("input.txt", "utf8");
.get( const productsIds = products.split("\n");
"https://db.maciaszek.ovh/api/collections/gifts_items/records?fields=title,link"
)
.then((response) => response.data.items);
return products return productsIds;
.filter((product) => product.link !== "")
.filter(
(product) => !excludePage.some((page) => product.link.includes(page))
);
} }
async function init() { async function init() {
const productsWithLinks = await getProducts(); const browser = await chromium.launch({ headless: true });
const productsWithBrowser = [];
const productsPrice = [];
const selectors = Object.keys(priceSelectors);
for (const product of productsWithLinks) {
if (product.link === "") continue;
try {
const { data } = await axios.get(product.link, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
},
});
const $ = cheerio.load(data);
const selector = selectors.find((selector) =>
product.link.includes(selector)
);
const price = $(priceSelectors[selector]).text();
productsPrice.push({
name: product.title,
price: price,
link: product.link,
});
} catch {
productsWithBrowser.push(product);
}
}
if (productsWithBrowser.length > 0) {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ const context = await browser.newContext({
userAgent: userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
}); });
const page = await context.newPage(); const page = await context.newPage();
for (const product of productsWithBrowser) { const productsIds = await getProducts();
await page.goto(product.link); const products = [];
const selector = selectors.find((selector) =>
product.link.includes(selector)
);
const element = await page.$(priceSelectors[selector]);
if (!element) continue; for (const productId of productsIds) {
try {
const price = await element.textContent(); await page.goto(`https://www.ceneo.pl/${productId}`, {
productsPrice.push({ waitUntil: "networkidle",
name: product.title, timeout: 30000,
price: price,
link: product.link,
}); });
// Wait for bot detection to pass and content to load
await page
.waitForSelector(".product-offer__container, .product-top", {
timeout: 10000,
})
.catch(() => {
console.log(`Timeout waiting for content on ${productId}`);
});
const html = await page.content();
const $ = cheerio.load(html);
const items = $(".product-offer__container").first();
for (const item of items) {
let name = $(item).data("productname");
let price = $(item).data("price");
let link = `https://www.ceneo.pl/${$(item).data("click-url")}`;
const shop = $(item).data("shopurl") || "ceneo.pl";
if (!name || !price) {
name = $(item).find(".short-name__txt").text();
price = $(item).find(".price").text();
link = `https://www.ceneo.pl/${productId}`;
}
if (!name || !price || !link) {
continue;
}
products.push({ name, price, link, shop });
}
} catch (error) {
console.error(`Error fetching product ${productId}:`, error.message);
}
} }
await browser.close(); await browser.close();
await compareAndSave(products);
console.log("Aktualne ceny zapisane w productsPrice.json");
} }
await compareAndSave(productsPrice); sendMessage("Startuję monitoring cen");
const task = cron.schedule("*/15 7-23 * * *", init, {
console.log("Done! Check productsPrice.json"); timezone: "Europe/Warsaw",
}
cron.schedule("* */3 * * *", async () => {
await init();
}); });
cron.schedule("0 7 * * *", () => {
const date = new Date().toLocaleDateString("pl-PL");
sendMessage(`Zaczynamy monitoring ${date}`);
});
task.execute();

153
index.js.old Normal file
View File

@@ -0,0 +1,153 @@
const cheerio = require("cheerio");
const cron = require("node-cron");
const axios = require("axios");
const { chromium } = require("playwright");
const fs = require("fs");
const discordWebhook =
"https://discord.com/api/webhooks/1439286509390921749/t2Hb8XloF6zhDRYD1yh_QlkHHa9eHUyXvd9TxZRHwqR_b_OxxbnwDgsm4em8TwA9NQIa";
const priceSelectors = {
miodowamydlarnia: ".projector_prices__price",
greentouch: ".main-price",
amazon: "#corePrice_feature_div .a-price .a-offscreen",
soxo: "#projector_price_value span",
empik: '[data-ta-section="priceMainContainer"] [data-ta="price"]',
notino: "#pd-price",
};
const excludePage = ["allegro", "homla.com.pl", "home-you.com"];
function sendMessage(message) {
axios.post(discordWebhook, {
content: message,
});
}
async function compareAndSave(productsPrice) {
if (!fs.existsSync("productsPrice.json")) {
fs.writeFileSync("productsPrice.json", "[]");
}
const productsPriceJson = fs.readFileSync("productsPrice.json", "utf8");
const oldProductsPrice = JSON.parse(productsPriceJson);
const diffProducts = [];
for (const product of productsPrice) {
const oldProduct = oldProductsPrice.find(
(oldProduct) => oldProduct.link === product.link
);
if (oldProduct && oldProduct.price !== product.price) {
diffProducts.push({
name: product.name,
newPrice: product.price,
oldPrice: oldProduct.price,
link: product.link,
});
}
}
for (const product of diffProducts) {
sendMessage(
`Zmiana ceny **${product.name}**:\nCena: ${product.oldPrice} -> ${product.newPrice}\nLink: ${product.link}`
);
}
if (diffProducts.length === 0) {
sendMessage("Brak zmian w cenach");
}
fs.writeFileSync(
"productsPrice.json",
JSON.stringify(productsPrice, null, 2)
);
}
async function getProducts() {
const products = await axios
.get(
"https://db.maciaszek.ovh/api/collections/gifts_items/records?fields=title,link"
)
.then((response) => response.data.items);
return products
.filter((product) => product.link !== "")
.filter(
(product) => !excludePage.some((page) => product.link.includes(page))
);
}
async function init() {
const productsWithLinks = await getProducts();
const productsWithBrowser = [];
const productsPrice = [];
const selectors = Object.keys(priceSelectors);
console.log("Zaczynam sprawdzać ceny");
for (const product of productsWithLinks) {
if (product.link === "") continue;
try {
const { data } = await axios.get(product.link, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
},
});
const $ = cheerio.load(data);
const selector = selectors.find((selector) =>
product.link.includes(selector)
);
const price = $(priceSelectors[selector]).text();
productsPrice.push({
name: product.title,
price: price,
link: product.link,
});
} catch {
productsWithBrowser.push(product);
}
}
if (productsWithBrowser.length > 0) {
const browser = await chromium.launch();
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36",
});
const page = await context.newPage();
for (const product of productsWithBrowser) {
await page.goto(product.link);
const selector = selectors.find((selector) =>
product.link.includes(selector)
);
const element = await page.$(priceSelectors[selector]);
if (!element) continue;
const price = await element.textContent();
productsPrice.push({
name: product.title,
price: price,
link: product.link,
});
}
await browser.close();
}
await compareAndSave(productsPrice);
console.log("Sprawdzone! Aktualne ceny zapisane w productsPrice.json");
}
sendMessage("Zaczynam monitoring cen");
const task = cron.schedule("0 6,9,12,15,18,21 * * *", init, {
timezone: "Europe/Warsaw",
});
task.execute();