Files
yourpart3/backend/services/newsService.js

124 lines
4.1 KiB
JavaScript

/**
* Proxy für newsdata.io API.
* Endpoint: https://newsdata.io/api/1/news?apikey=...&language=...&category=...
* Pagination: counter = wievieltes Widget dieser Art (0 = erste Seite, 1 = zweite, …), damit News nicht doppelt gezeigt werden.
*/
const NEWS_BASE = 'https://newsdata.io/api/1/news';
// Cache für News-Ergebnisse (pro Sprache/Kategorie)
const newsCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 Minuten Cache
/**
* @param {object} options
* @param {number} options.counter - 0 = erste Seite, 1 = zweite, … (für Pagination/nextPage)
* @param {string} [options.language] - z. B. de, en
* @param {string} [options.category] - z. B. top, technology
* @returns {Promise<{ results: Array, nextPage: string|null }>}
*/
async function fetchNewsPage({ language = 'de', category = 'top', nextPageToken = null }) {
const apiKey = process.env.NEWSDATA_IO_API_KEY;
if (!apiKey || !apiKey.trim()) {
throw new Error('NEWSDATA_IO_API_KEY is not set in .env');
}
const params = new URLSearchParams();
params.set('apikey', apiKey.trim());
params.set('language', String(language));
params.set('category', String(category));
if (nextPageToken) params.set('page', nextPageToken);
const url = `${NEWS_BASE}?${params.toString()}`;
const res = await fetch(url);
if (!res.ok) {
const text = await res.text();
throw new Error(`newsdata.io: ${res.status} ${text.slice(0, 200)}`);
}
const data = await res.json();
return {
results: data.results ?? [],
nextPage: data.nextPage ?? null
};
}
/**
* Holt gecachte Artikel oder lädt sie von der API
*/
async function getCachedNews({ language = 'de', category = 'top', minArticles = 10 }) {
const cacheKey = `${language}:${category}`;
const cached = newsCache.get(cacheKey);
// Cache gültig?
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
// Wenn wir mehr Artikel brauchen, erweitern
if (cached.articles.length >= minArticles) {
return cached.articles;
}
}
// Neue Daten laden
const collected = cached?.articles || [];
let nextPageToken = cached?.nextPage || null;
while (collected.length < minArticles) {
try {
const page = await fetchNewsPage({
language,
category,
nextPageToken: nextPageToken || undefined
});
const items = page.results ?? [];
// Duplikate vermeiden (nach title)
const existingTitles = new Set(collected.map(a => a.title));
for (const item of items) {
if (!existingTitles.has(item.title)) {
collected.push(item);
existingTitles.add(item.title);
}
}
nextPageToken = page.nextPage ?? null;
if (items.length === 0 || !nextPageToken) break;
} catch (error) {
console.error('News fetch error:', error);
break;
}
}
// Cache aktualisieren
newsCache.set(cacheKey, {
articles: collected,
nextPage: nextPageToken,
timestamp: Date.now()
});
return collected;
}
/**
* Liefert den N-ten Artikel (counter = 0, 1, 2, …) für das N-te News-Widget.
* Nutzt Cache, damit mehrere Widgets unterschiedliche Artikel bekommen.
*
* @param {object} options
* @param {number} options.counter - Index des Artikels (0 = erster, 1 = zweiter, …)
* @param {string} [options.language]
* @param {string} [options.category]
* @returns {Promise<{ results: Array, nextPage: string|null }>}
*/
async function getNews({ counter = 0, language = 'de', category = 'top' }) {
const neededIndex = Math.max(0, counter);
// Mindestens so viele Artikel laden wie benötigt
const articles = await getCachedNews({
language,
category,
minArticles: neededIndex + 1
});
const single = articles[neededIndex] ? [articles[neededIndex]] : [];
return { results: single, nextPage: null };
}
export default { getNews };