feat: Initial move to docker image
Build and publish / build (push) Failing after 2m37s

This commit is contained in:
Lino Silva
2026-04-10 17:24:44 +01:00
parent be8ff08f94
commit fe4c54ebcb
10 changed files with 3153 additions and 112 deletions
+102 -77
View File
@@ -1,103 +1,128 @@
import puppeteer from "puppeteer";
import process from "node:process";
import axios from "axios";
import express from "express";
import "dotenv/config";
const contas = {
bpi2040: {
const app = express();
const PORT = process.env.PORT || 3000;
// Symbol configuration
const symbols = {
BPIDEST2040: {
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2040",
ghostfolioName: "GF_BPI_Destino_PPR_2040",
name: "BPI Destino PPR 2040",
},
bpi2050: {
BPIDEST2050: {
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2050",
ghostfolioName: "GF_BPI_Destino_PPR_2050",
name: "BPI Destino PPR 2050",
},
};
async function parseLogRocketBlogHome(conta) {
// Function to scrape price for a given symbol
async function getLatestPrice(symbol) {
const config = symbols[symbol];
if (!config) {
throw new Error(`Invalid symbol: ${symbol}`);
}
// Launch the browser
const browser = await puppeteer.launch({
args: ["--no-sandbox"],
timeout: 10000,
});
// Open a new tab
const page = await browser.newPage();
try {
// Open a new tab
const page = await browser.newPage();
// Visit the page and wait until network connections are completed
await page.goto(conta.url, { waitUntil: "networkidle2" });
// Visit the page and wait until network connections are completed
await page.goto(config.url, { waitUntil: "networkidle2" });
// Interact with the DOM to retrieve the titles
const [price, date] = await page.evaluate(() => {
const spanTags = document.getElementsByTagName("span");
const priceSearchText = "ÚLTIMA COTAÇÃO:";
const dateSearchText = "DATA COTAÇÃO:";
let priceElementFound;
let dateElementFound;
// Interact with the DOM to retrieve the price and date
const [price, date] = await page.evaluate(() => {
const spanTags = document.getElementsByTagName("span");
const priceSearchText = "ÚLTIMA COTAÇÃO:";
const dateSearchText = "DATA COTAÇÃO:";
let priceElementFound;
let dateElementFound;
for (let i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent.trim() == priceSearchText) {
priceElementFound = spanTags[i];
console.log(`Found ${priceSearchText}.`);
if (priceElementFound && dateElementFound) break;
for (let i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent.trim() == priceSearchText) {
priceElementFound = spanTags[i];
if (priceElementFound && dateElementFound) break;
}
if (spanTags[i].textContent.trim() == dateSearchText) {
dateElementFound = spanTags[i];
if (priceElementFound && dateElementFound) break;
}
}
if (spanTags[i].textContent.trim() == dateSearchText) {
dateElementFound = spanTags[i];
console.log(`Found ${dateSearchText}.`);
if (priceElementFound && dateElementFound) break;
if (!priceElementFound || !dateElementFound) {
throw new Error("Could not find price or date elements");
}
}
return [
priceElementFound.nextSibling.innerHTML,
dateElementFound.nextSibling.innerHTML,
];
});
return [
priceElementFound.nextSibling.innerHTML,
dateElementFound.nextSibling.innerHTML,
];
});
// Don't forget to close the browser instance to clean up the memory
await browser.close();
const marketPrice = parseFloat(
price.replace("€", "").trim().replace(",", "."),
);
const marketPrice = parseFloat(
price.replace("€", "").trim().replace(",", "."),
);
// Print the results
console.log(decodeURIComponent(conta.ghostfolioName));
console.log(`Current price: € ${marketPrice}`);
console.log(`Date: ${date}`);
const bearerTokenResponse = await axios.post(
`${process.env.GHOSTFOLIO_HOST}/api/v1/auth/anonymous`,
{
accessToken: process.env.GHOSTFOLIO_SECURITY_TOKEN,
},
);
const response = await axios
.post(
`${process.env.GHOSTFOLIO_HOST}/api/v1/market-data/MANUAL/${conta.ghostfolioName}`,
{
marketData: [
{
date,
marketPrice,
},
],
},
{
headers: {
Authorization: `Bearer ${bearerTokenResponse.data.authToken}`,
},
},
)
.catch(console.error);
console.log(response.status);
return {
symbol,
name: config.name,
price: marketPrice,
date: date.trim(),
timestamp: new Date().toISOString(),
};
} finally {
// Always close the browser
await browser.close();
}
}
await Promise.all([
parseLogRocketBlogHome(contas.bpi2040),
parseLogRocketBlogHome(contas.bpi2050),
]);
// API endpoint to get latest price for a symbol
app.get("/price/:symbol", async (req, res) => {
const { symbol } = req.params;
process.exit(0);
try {
console.log(`Fetching price for symbol: ${symbol}`);
const priceData = await getLatestPrice(symbol);
console.log(`Success: ${symbol} - €${priceData.price}`);
res.json(priceData);
} catch (error) {
console.error(`Error fetching price for ${symbol}:`, error.message);
res.status(400).json({
error: error.message,
symbol,
});
}
});
// Health check endpoint
app.get("/health", (req, res) => {
res.json({ status: "ok", availableSymbols: Object.keys(symbols) });
});
// Root endpoint with usage instructions
app.get("/", (req, res) => {
res.json({
message: "BPI Stock Price Scraper API",
endpoints: {
health: "/health",
price: "/price/:symbol",
},
availableSymbols: Object.keys(symbols),
example: `/price/BPIDEST2040`,
});
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Available symbols: ${Object.keys(symbols).join(", ")}`);
console.log(`Example: http://localhost:${PORT}/price/BPIDEST2040`);
});