Spaces:
Running
Running
Upload 11 files
Browse files- data/news_sentiment.json +0 -0
- data/sentiment_trends.json +20 -20
- index.js +16 -4
- package-lock.json +84 -0
- package.json +1 -0
- public/app.js +10 -3
data/news_sentiment.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/sentiment_trends.json
CHANGED
|
@@ -1,24 +1,4 @@
|
|
| 1 |
[
|
| 2 |
-
{
|
| 3 |
-
"time": "2026-02-17T21:55:04.627Z",
|
| 4 |
-
"score": 2.178794178794179
|
| 5 |
-
},
|
| 6 |
-
{
|
| 7 |
-
"time": "2026-02-17T22:00:04.836Z",
|
| 8 |
-
"score": 2.1621052631578945
|
| 9 |
-
},
|
| 10 |
-
{
|
| 11 |
-
"time": "2026-02-17T22:05:04.721Z",
|
| 12 |
-
"score": 2.1461377870563676
|
| 13 |
-
},
|
| 14 |
-
{
|
| 15 |
-
"time": "2026-02-17T22:10:04.944Z",
|
| 16 |
-
"score": 2.1596638655462184
|
| 17 |
-
},
|
| 18 |
-
{
|
| 19 |
-
"time": "2026-02-17T22:15:06.180Z",
|
| 20 |
-
"score": 2.214723926380368
|
| 21 |
-
},
|
| 22 |
{
|
| 23 |
"time": "2026-02-17T22:20:04.427Z",
|
| 24 |
"score": 2.2378947368421054
|
|
@@ -198,5 +178,25 @@
|
|
| 198 |
{
|
| 199 |
"time": "2026-03-20T19:02:02.894Z",
|
| 200 |
"score": 1.3021523178807948
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
]
|
|
|
|
| 1 |
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
{
|
| 3 |
"time": "2026-02-17T22:20:04.427Z",
|
| 4 |
"score": 2.2378947368421054
|
|
|
|
| 178 |
{
|
| 179 |
"time": "2026-03-20T19:02:02.894Z",
|
| 180 |
"score": 1.3021523178807948
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"time": "2026-03-22T18:49:43.688Z",
|
| 184 |
+
"score": 0.5806772908366534
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"time": "2026-03-22T18:50:00.414Z",
|
| 188 |
+
"score": 0.5806772908366534
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"time": "2026-03-22T18:51:47.906Z",
|
| 192 |
+
"score": 0.5150300601202404
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"time": "2026-03-22T18:52:00.825Z",
|
| 196 |
+
"score": 0.5150300601202404
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"time": "2026-03-22T18:54:01.373Z",
|
| 200 |
+
"score": 0.4885799404170804
|
| 201 |
}
|
| 202 |
]
|
index.js
CHANGED
|
@@ -6,14 +6,16 @@ const cron = require('node-cron');
|
|
| 6 |
const cors = require('cors');
|
| 7 |
const fs = require('fs');
|
| 8 |
const path = require('path');
|
|
|
|
| 9 |
|
| 10 |
const app = express();
|
| 11 |
const parser = new Parser();
|
| 12 |
const sentiment = new Sentiment();
|
| 13 |
const PORT = 3001;
|
| 14 |
|
|
|
|
| 15 |
app.use(cors());
|
| 16 |
-
app.use(express.static('public'));
|
| 17 |
|
| 18 |
const DATA_FILE = path.join(__dirname, 'data', 'news_sentiment.json');
|
| 19 |
const TREND_FILE = path.join(__dirname, 'data', 'sentiment_trends.json');
|
|
@@ -117,19 +119,20 @@ async function fetchAndAnalyzeNews() {
|
|
| 117 |
const now = new Date();
|
| 118 |
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
| 119 |
|
| 120 |
-
|
| 121 |
try {
|
| 122 |
const response = await axios.get(url, { headers: BROWSER_HEADERS, timeout: 10000 });
|
| 123 |
const feed = await parser.parseString(response.data);
|
| 124 |
const sourceName = getSourceName(url, feed.title);
|
| 125 |
feedStatus[url] = { status: 'Success', displayName: sourceName, lastUpdate: new Date().toISOString() };
|
| 126 |
|
|
|
|
| 127 |
feed.items.forEach(item => {
|
| 128 |
const pubDate = new Date(item.pubDate);
|
| 129 |
if (pubDate >= twentyFourHoursAgo) {
|
| 130 |
const fullText = `${item.title} ${item.contentSnippet || ''}`;
|
| 131 |
const analysis = sentiment.analyze(fullText);
|
| 132 |
-
|
| 133 |
id: item.guid || item.link,
|
| 134 |
title: item.title,
|
| 135 |
link: item.link,
|
|
@@ -142,10 +145,19 @@ async function fetchAndAnalyzeNews() {
|
|
| 142 |
});
|
| 143 |
}
|
| 144 |
});
|
|
|
|
| 145 |
} catch (e) {
|
| 146 |
feedStatus[url] = { status: 'Failed', displayName: url.split('/')[2], lastUpdate: new Date().toISOString() };
|
|
|
|
| 147 |
}
|
| 148 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
const uniqueNews = Array.from(new Map(allEntries.map(item => [item.link, item])).values());
|
| 151 |
uniqueNews.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
|
|
|
|
| 6 |
const cors = require('cors');
|
| 7 |
const fs = require('fs');
|
| 8 |
const path = require('path');
|
| 9 |
+
const compression = require('compression');
|
| 10 |
|
| 11 |
const app = express();
|
| 12 |
const parser = new Parser();
|
| 13 |
const sentiment = new Sentiment();
|
| 14 |
const PORT = 3001;
|
| 15 |
|
| 16 |
+
app.use(compression());
|
| 17 |
app.use(cors());
|
| 18 |
+
app.use(express.static('public', { maxAge: '1d' }));
|
| 19 |
|
| 20 |
const DATA_FILE = path.join(__dirname, 'data', 'news_sentiment.json');
|
| 21 |
const TREND_FILE = path.join(__dirname, 'data', 'sentiment_trends.json');
|
|
|
|
| 119 |
const now = new Date();
|
| 120 |
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
| 121 |
|
| 122 |
+
const fetchPromises = RSS_FEEDS.map(async (url) => {
|
| 123 |
try {
|
| 124 |
const response = await axios.get(url, { headers: BROWSER_HEADERS, timeout: 10000 });
|
| 125 |
const feed = await parser.parseString(response.data);
|
| 126 |
const sourceName = getSourceName(url, feed.title);
|
| 127 |
feedStatus[url] = { status: 'Success', displayName: sourceName, lastUpdate: new Date().toISOString() };
|
| 128 |
|
| 129 |
+
const items = [];
|
| 130 |
feed.items.forEach(item => {
|
| 131 |
const pubDate = new Date(item.pubDate);
|
| 132 |
if (pubDate >= twentyFourHoursAgo) {
|
| 133 |
const fullText = `${item.title} ${item.contentSnippet || ''}`;
|
| 134 |
const analysis = sentiment.analyze(fullText);
|
| 135 |
+
items.push({
|
| 136 |
id: item.guid || item.link,
|
| 137 |
title: item.title,
|
| 138 |
link: item.link,
|
|
|
|
| 145 |
});
|
| 146 |
}
|
| 147 |
});
|
| 148 |
+
return items;
|
| 149 |
} catch (e) {
|
| 150 |
feedStatus[url] = { status: 'Failed', displayName: url.split('/')[2], lastUpdate: new Date().toISOString() };
|
| 151 |
+
return [];
|
| 152 |
}
|
| 153 |
+
});
|
| 154 |
+
|
| 155 |
+
const results = await Promise.allSettled(fetchPromises);
|
| 156 |
+
results.forEach(result => {
|
| 157 |
+
if (result.status === 'fulfilled') {
|
| 158 |
+
allEntries.push(...result.value);
|
| 159 |
+
}
|
| 160 |
+
});
|
| 161 |
|
| 162 |
const uniqueNews = Array.from(new Map(allEntries.map(item => [item.link, item])).values());
|
| 163 |
uniqueNews.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
|
package-lock.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
"license": "ISC",
|
| 11 |
"dependencies": {
|
| 12 |
"axios": "^1.13.5",
|
|
|
|
| 13 |
"cors": "^2.8.6",
|
| 14 |
"express": "^5.2.1",
|
| 15 |
"node-cron": "^4.2.1",
|
|
@@ -121,6 +122,60 @@
|
|
| 121 |
"node": ">= 0.8"
|
| 122 |
}
|
| 123 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
"node_modules/content-disposition": {
|
| 125 |
"version": "1.0.1",
|
| 126 |
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
|
@@ -716,6 +771,15 @@
|
|
| 716 |
"node": ">= 0.8"
|
| 717 |
}
|
| 718 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
"node_modules/once": {
|
| 720 |
"version": "1.4.0",
|
| 721 |
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
@@ -828,6 +892,26 @@
|
|
| 828 |
"xml2js": "^0.5.0"
|
| 829 |
}
|
| 830 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
"node_modules/safer-buffer": {
|
| 832 |
"version": "2.1.2",
|
| 833 |
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
|
|
| 10 |
"license": "ISC",
|
| 11 |
"dependencies": {
|
| 12 |
"axios": "^1.13.5",
|
| 13 |
+
"compression": "^1.8.1",
|
| 14 |
"cors": "^2.8.6",
|
| 15 |
"express": "^5.2.1",
|
| 16 |
"node-cron": "^4.2.1",
|
|
|
|
| 122 |
"node": ">= 0.8"
|
| 123 |
}
|
| 124 |
},
|
| 125 |
+
"node_modules/compressible": {
|
| 126 |
+
"version": "2.0.18",
|
| 127 |
+
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
| 128 |
+
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
| 129 |
+
"license": "MIT",
|
| 130 |
+
"dependencies": {
|
| 131 |
+
"mime-db": ">= 1.43.0 < 2"
|
| 132 |
+
},
|
| 133 |
+
"engines": {
|
| 134 |
+
"node": ">= 0.6"
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"node_modules/compression": {
|
| 138 |
+
"version": "1.8.1",
|
| 139 |
+
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
| 140 |
+
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
| 141 |
+
"license": "MIT",
|
| 142 |
+
"dependencies": {
|
| 143 |
+
"bytes": "3.1.2",
|
| 144 |
+
"compressible": "~2.0.18",
|
| 145 |
+
"debug": "2.6.9",
|
| 146 |
+
"negotiator": "~0.6.4",
|
| 147 |
+
"on-headers": "~1.1.0",
|
| 148 |
+
"safe-buffer": "5.2.1",
|
| 149 |
+
"vary": "~1.1.2"
|
| 150 |
+
},
|
| 151 |
+
"engines": {
|
| 152 |
+
"node": ">= 0.8.0"
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"node_modules/compression/node_modules/debug": {
|
| 156 |
+
"version": "2.6.9",
|
| 157 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
| 158 |
+
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
| 159 |
+
"license": "MIT",
|
| 160 |
+
"dependencies": {
|
| 161 |
+
"ms": "2.0.0"
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
"node_modules/compression/node_modules/ms": {
|
| 165 |
+
"version": "2.0.0",
|
| 166 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
| 167 |
+
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
| 168 |
+
"license": "MIT"
|
| 169 |
+
},
|
| 170 |
+
"node_modules/compression/node_modules/negotiator": {
|
| 171 |
+
"version": "0.6.4",
|
| 172 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
| 173 |
+
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
| 174 |
+
"license": "MIT",
|
| 175 |
+
"engines": {
|
| 176 |
+
"node": ">= 0.6"
|
| 177 |
+
}
|
| 178 |
+
},
|
| 179 |
"node_modules/content-disposition": {
|
| 180 |
"version": "1.0.1",
|
| 181 |
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
|
|
|
| 771 |
"node": ">= 0.8"
|
| 772 |
}
|
| 773 |
},
|
| 774 |
+
"node_modules/on-headers": {
|
| 775 |
+
"version": "1.1.0",
|
| 776 |
+
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
| 777 |
+
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
| 778 |
+
"license": "MIT",
|
| 779 |
+
"engines": {
|
| 780 |
+
"node": ">= 0.8"
|
| 781 |
+
}
|
| 782 |
+
},
|
| 783 |
"node_modules/once": {
|
| 784 |
"version": "1.4.0",
|
| 785 |
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
|
|
| 892 |
"xml2js": "^0.5.0"
|
| 893 |
}
|
| 894 |
},
|
| 895 |
+
"node_modules/safe-buffer": {
|
| 896 |
+
"version": "5.2.1",
|
| 897 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 898 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 899 |
+
"funding": [
|
| 900 |
+
{
|
| 901 |
+
"type": "github",
|
| 902 |
+
"url": "https://github.com/sponsors/feross"
|
| 903 |
+
},
|
| 904 |
+
{
|
| 905 |
+
"type": "patreon",
|
| 906 |
+
"url": "https://www.patreon.com/feross"
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"type": "consulting",
|
| 910 |
+
"url": "https://feross.org/support"
|
| 911 |
+
}
|
| 912 |
+
],
|
| 913 |
+
"license": "MIT"
|
| 914 |
+
},
|
| 915 |
"node_modules/safer-buffer": {
|
| 916 |
"version": "2.1.2",
|
| 917 |
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
| 12 |
"type": "commonjs",
|
| 13 |
"dependencies": {
|
| 14 |
"axios": "^1.13.5",
|
|
|
|
| 15 |
"cors": "^2.8.6",
|
| 16 |
"express": "^5.2.1",
|
| 17 |
"node-cron": "^4.2.1",
|
|
|
|
| 12 |
"type": "commonjs",
|
| 13 |
"dependencies": {
|
| 14 |
"axios": "^1.13.5",
|
| 15 |
+
"compression": "^1.8.1",
|
| 16 |
"cors": "^2.8.6",
|
| 17 |
"express": "^5.2.1",
|
| 18 |
"node-cron": "^4.2.1",
|
public/app.js
CHANGED
|
@@ -60,7 +60,10 @@ function renderFeed() {
|
|
| 60 |
return;
|
| 61 |
}
|
| 62 |
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
const timeAgo = getTimeAgo(new Date(item.pubDate));
|
| 65 |
const highlightedTitle = highlightImpact(item.title);
|
| 66 |
return `
|
|
@@ -124,9 +127,13 @@ function filterBySentiment(sentiment) {
|
|
| 124 |
renderFeed();
|
| 125 |
}
|
| 126 |
|
|
|
|
| 127 |
function handleSearch(val) {
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
|
|
|
|
| 60 |
return;
|
| 61 |
}
|
| 62 |
|
| 63 |
+
const maxItems = 100;
|
| 64 |
+
const itemsToRender = filtered.slice(0, maxItems);
|
| 65 |
+
|
| 66 |
+
feed.innerHTML = itemsToRender.map((item, index) => {
|
| 67 |
const timeAgo = getTimeAgo(new Date(item.pubDate));
|
| 68 |
const highlightedTitle = highlightImpact(item.title);
|
| 69 |
return `
|
|
|
|
| 127 |
renderFeed();
|
| 128 |
}
|
| 129 |
|
| 130 |
+
let searchTimeout;
|
| 131 |
function handleSearch(val) {
|
| 132 |
+
clearTimeout(searchTimeout);
|
| 133 |
+
searchTimeout = setTimeout(() => {
|
| 134 |
+
searchQuery = val;
|
| 135 |
+
renderFeed();
|
| 136 |
+
}, 300);
|
| 137 |
}
|
| 138 |
|
| 139 |
|