This commit is contained in:
+1
-1
@@ -129,4 +129,4 @@ dist
|
|||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ const symbols = {
|
|||||||
BPIDEST2040: {
|
BPIDEST2040: {
|
||||||
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2040",
|
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2040",
|
||||||
name: "BPI Destino PPR 2040",
|
name: "BPI Destino PPR 2040",
|
||||||
|
type: "bpi",
|
||||||
},
|
},
|
||||||
BPIDEST2050: {
|
BPIDEST2050: {
|
||||||
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2050",
|
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2050",
|
||||||
name: "BPI Destino PPR 2050",
|
name: "BPI Destino PPR 2050",
|
||||||
|
type: "bpi",
|
||||||
|
},
|
||||||
|
IE00BK5BQT80: {
|
||||||
|
url: "https://www.deco.proteste.pt/investe/investimentos/fundos/vanguard-ftse-all-world-ucits-etf-usd-acc",
|
||||||
|
name: "Vanguard FTSE All-World UCITS ETF USD Acc",
|
||||||
|
type: "deco",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,53 +45,99 @@ async function getLatestPrice(symbol) {
|
|||||||
// Visit the page and wait until network connections are completed
|
// Visit the page and wait until network connections are completed
|
||||||
await page.goto(config.url, { waitUntil: "networkidle2" });
|
await page.goto(config.url, { waitUntil: "networkidle2" });
|
||||||
|
|
||||||
// Interact with the DOM to retrieve the price and date
|
switch (config.type) {
|
||||||
const [price, date] = await page.evaluate(() => {
|
case "bpi":
|
||||||
const spanTags = document.getElementsByTagName("span");
|
return await scrapeBPI(page, symbol, config);
|
||||||
const priceSearchText = "ÚLTIMA COTAÇÃO:";
|
case "deco":
|
||||||
const dateSearchText = "DATA COTAÇÃO:";
|
return await scrapeDeco(page, symbol, config);
|
||||||
let priceElementFound;
|
default:
|
||||||
let dateElementFound;
|
throw new Error(`Unsupported type: ${config.type}`);
|
||||||
|
}
|
||||||
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 (!priceElementFound || !dateElementFound) {
|
|
||||||
throw new Error("Could not find price or date elements");
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
priceElementFound.nextSibling.innerHTML,
|
|
||||||
dateElementFound.nextSibling.innerHTML,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketPrice = parseFloat(
|
|
||||||
price.replace("€", "").trim().replace(",", "."),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
symbol,
|
|
||||||
name: config.name,
|
|
||||||
currency: "EUR",
|
|
||||||
price: marketPrice,
|
|
||||||
date: date.trim(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
} finally {
|
} finally {
|
||||||
// Always close the browser
|
// Always close the browser
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function scrapeDeco(page, symbol, config) {
|
||||||
|
// Interact with the DOM to retrieve the price and date
|
||||||
|
const [price, date] = await page.evaluate(() => {
|
||||||
|
const priceElement = document.querySelector(
|
||||||
|
".product-points-data__current-value",
|
||||||
|
);
|
||||||
|
const dateElement = document.querySelector(
|
||||||
|
".product-points-data__current-value + span",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!priceElement || !dateElement) {
|
||||||
|
throw new Error("Could not find price or date elements");
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = priceElement.textContent
|
||||||
|
.replace("€", "")
|
||||||
|
.replace("EUR", "")
|
||||||
|
.replace(",", ".")
|
||||||
|
.trim();
|
||||||
|
const date = dateElement.textContent.trim();
|
||||||
|
|
||||||
|
return [price, date];
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketPrice = parseFloat(price);
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
name: config.name,
|
||||||
|
currency: "EUR",
|
||||||
|
price: marketPrice,
|
||||||
|
date,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeBPI(page, symbol, config) {
|
||||||
|
// 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];
|
||||||
|
if (priceElementFound && dateElementFound) break;
|
||||||
|
}
|
||||||
|
if (spanTags[i].textContent.trim() == dateSearchText) {
|
||||||
|
dateElementFound = spanTags[i];
|
||||||
|
if (priceElementFound && dateElementFound) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priceElementFound || !dateElementFound) {
|
||||||
|
throw new Error("Could not find price or date elements");
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
priceElementFound.nextSibling.innerHTML,
|
||||||
|
dateElementFound.nextSibling.innerHTML,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketPrice = parseFloat(
|
||||||
|
price.replace("€", "").trim().replace(",", "."),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
name: config.name,
|
||||||
|
currency: "EUR",
|
||||||
|
price: marketPrice,
|
||||||
|
date: date.trim(),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// API endpoint to get latest price for a symbol
|
// API endpoint to get latest price for a symbol
|
||||||
app.get("/price/:symbol", async (req, res) => {
|
app.get("/price/:symbol", async (req, res) => {
|
||||||
const { symbol } = req.params;
|
const { symbol } = req.params;
|
||||||
@@ -92,7 +145,7 @@ app.get("/price/:symbol", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
console.log(`Fetching price for symbol: ${symbol}`);
|
console.log(`Fetching price for symbol: ${symbol}`);
|
||||||
const priceData = await getLatestPrice(symbol);
|
const priceData = await getLatestPrice(symbol);
|
||||||
console.log(`Success: ${symbol} - €${priceData.price}`);
|
console.log(`Success: ${symbol} - ${priceData.price}`);
|
||||||
res.json(priceData);
|
res.json(priceData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching price for ${symbol}:`, error.message);
|
console.error(`Error fetching price for ${symbol}:`, error.message);
|
||||||
|
|||||||
Generated
-78
@@ -9,7 +9,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"puppeteer": "^23.5.3"
|
"puppeteer": "^23.5.3"
|
||||||
@@ -432,19 +431,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/axios": {
|
|
||||||
"version": "1.7.7",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"follow-redirects": "^1.15.6",
|
|
||||||
"form-data": "^4.0.0",
|
|
||||||
"proxy-from-env": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
@@ -676,16 +662,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
|
||||||
"version": "1.0.8",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"delayed-stream": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -809,13 +785,6 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -1299,36 +1268,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
|
||||||
"version": "1.15.9",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"debug": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/form-data": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"asynckit": "^0.4.0",
|
|
||||||
"combined-stream": "^1.0.8",
|
|
||||||
"mime-types": "^2.1.12"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1808,23 +1747,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user