folx-tv/server/horoscope-generator.ts
Folx Ops 365da96f5b Horoskop in kozmicni dogodki: dnevno AI generiranje + scheduler
- Nov dnevni scheduler (server/scheduler.ts): vsak dan ob zagonu in vsakih 6h
  preveri/generira horoskope in kozmicne dogodke (prej samo enkrat ob zagonu)
- Kozmicni dogodki so zdaj AI-generirani in dnevni (nova tabela cosmic_events
  + /api/cosmic-events), namesto hardcoded fiksnih datumov iz feb/mar 2026
- Naslovni horoskop widget bere pravi AI horoskop za danes (prej staticni tekst)
- Frontend: staleTime 30min + refetchOnWindowFocus za dnevno osvezevanje
2026-06-07 15:05:20 +00:00

212 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import OpenAI from "openai";
import { db } from "./db";
import { dailyHoroscopes, cosmicEvents } from "@shared/schema";
import { eq, and } from "drizzle-orm";
const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
});
const SIGN_NAMES = [
"Widder", "Stier", "Zwillinge", "Krebs", "Löwe", "Jungfrau",
"Waage", "Skorpion", "Schütze", "Steinbock", "Wassermann", "Fische"
];
function getTodayStr(): string {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
export async function getHoroscopesForToday(): Promise<any[]> {
const today = getTodayStr();
const existing = await db.select().from(dailyHoroscopes).where(eq(dailyHoroscopes.dateStr, today));
if (existing.length === 12) return existing;
return [];
}
export async function generateDailyHoroscopes(): Promise<void> {
const today = getTodayStr();
const existing = await db.select().from(dailyHoroscopes).where(eq(dailyHoroscopes.dateStr, today));
if (existing.length >= 12) {
console.log(`Horoscopes for ${today} already exist.`);
return;
}
console.log(`Generating horoscopes for ${today}...`);
for (let i = 0; i < SIGN_NAMES.length; i++) {
const signName = SIGN_NAMES[i];
const alreadyExists = existing.find(h => h.signIndex === i);
if (alreadyExists) continue;
try {
const response = await openai.chat.completions.create({
model: "gpt-5-mini",
messages: [
{
role: "system",
content: `Du bist ein erfahrener Astrologe, der tägliche Horoskope für eine deutschsprachige Volksmusik- und Schlager-Nachrichtenwebsite schreibt. Dein Stil ist warm, ermutigend und poetisch. Du beziehst manchmal Musik, Natur und alpine Kultur in deine Texte ein. Schreibe immer auf Deutsch. Das heutige Datum ist ${today}.`
},
{
role: "user",
content: `Erstelle ein ausführliches Tageshoroskop für das Sternzeichen ${signName} für heute (${today}).
Antworte NUR mit einem JSON-Objekt in diesem exakten Format (kein Markdown, keine Erklärung):
{
"general": "Ausführlicher allgemeiner Tagestext, mindestens 4-5 Sätze über die allgemeine Energie, Stimmung und Möglichkeiten des Tages.",
"love": "Ausführlicher Text über Liebe und Partnerschaft, mindestens 3-4 Sätze mit konkreten Ratschlägen für Singles und Paare.",
"career": "Ausführlicher Text über Beruf und Finanzen, mindestens 3-4 Sätze mit konkreten Tipps.",
"health": "Ausführlicher Text über Gesundheit und Wohlbefinden, mindestens 3-4 Sätze.",
"tip": "Ein konkreter, umsetzbarer Tipp des Tages in 1-2 Sätzen.",
"weekly": "Ausführliche Wochenvorschau, mindestens 4-5 Sätze mit Hinweisen für jeden Wochentag.",
"monthly": "Ausführliche Monatsvorschau, mindestens 4-5 Sätze über die wichtigsten Themen des Monats."
}`
}
],
max_completion_tokens: 2000,
});
const content = response.choices[0]?.message?.content || "";
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
console.error(`Failed to parse horoscope for ${signName}`);
continue;
}
const parsed = JSON.parse(jsonMatch[0]);
await db.insert(dailyHoroscopes).values({
signIndex: i,
signName,
dateStr: today,
general: parsed.general || "",
love: parsed.love || "",
career: parsed.career || "",
health: parsed.health || "",
tip: parsed.tip || "",
weekly: parsed.weekly || "",
monthly: parsed.monthly || "",
});
console.log(`Generated horoscope for ${signName}`);
} catch (err: any) {
console.error(`Error generating horoscope for ${signName}:`, err.message);
}
}
console.log(`Horoscope generation complete for ${today}.`);
}
export async function getOrGenerateHoroscope(signIndex: number): Promise<any | null> {
const today = getTodayStr();
const [existing] = await db.select().from(dailyHoroscopes)
.where(and(eq(dailyHoroscopes.dateStr, today), eq(dailyHoroscopes.signIndex, signIndex)));
if (existing) return existing;
return null;
}
const VALID_ICONS = ["mercury", "venus", "moon", "newmoon", "sun", "saturn", "jupiter", "mars"];
const VALID_TYPES = ["retrograde", "moon", "transit", "season"];
export async function getCosmicEventsForToday(): Promise<any[]> {
const today = getTodayStr();
const rows = await db.select().from(cosmicEvents)
.where(eq(cosmicEvents.dateStr, today));
return rows
.sort((a, b) => a.position - b.position)
.map((r) => ({
title: r.title,
description: r.description,
dateRange: r.dateRange,
icon: r.icon,
type: r.type,
affectedSigns: JSON.parse(r.affectedSigns || "[]"),
}));
}
export async function generateCosmicEvents(): Promise<void> {
const today = getTodayStr();
const existing = await db.select().from(cosmicEvents)
.where(eq(cosmicEvents.dateStr, today));
if (existing.length >= 6) {
console.log(`Cosmic events for ${today} already exist.`);
return;
}
console.log(`Generating cosmic events for ${today}...`);
const d = new Date();
const monthName = d.toLocaleDateString("de-DE", { month: "long", year: "numeric" });
try {
const response = await openai.chat.completions.create({
model: "gpt-5-mini",
messages: [
{
role: "system",
content: `Du bist ein erfahrener Astrologe, der die aktuellen kosmischen Ereignisse (Planetenkonstellationen, Mondphasen, Retrograden, Tierkreis-Saison) für eine deutschsprachige Volksmusik- und Schlager-Website beschreibt. Du kennst die realen astronomischen Daten. Heute ist der ${today} (${monthName}). Beschreibe ausschließlich Ereignisse, die JETZT, rund um dieses Datum tatsächlich aktiv oder relevant sind. Verwende realistische, dem aktuellen Datum entsprechende Datumsangaben. Schreibe warm und zugänglich auf Deutsch.`,
},
{
role: "user",
content: `Erstelle die 6 wichtigsten aktuellen kosmischen Ereignisse für heute (${today}). Dazu sollten gehören: die aktuelle Tierkreis-Saison (Sonne im Zeichen), die aktuelle Mondphase, eventuelle Retrograden, sowie relevante Planetentransite (Venus, Mars, Saturn, Jupiter).
Antworte NUR mit einem JSON-Array von genau 6 Objekten (kein Markdown, keine Erklärung) in diesem exakten Format:
[
{
"title": "Kurzer Titel, z.B. 'Vollmond im Skorpion' oder 'Zwillinge-Saison'",
"description": "3-4 Sätze über die Bedeutung dieses Ereignisses und seinen Einfluss.",
"dateRange": "Realistische Datumsangabe passend zum heutigen Datum, z.B. '21. Mai 20. Juni 2026' oder '11. Juni 2026'",
"icon": "EINER VON: mercury, venus, moon, newmoon, sun, saturn, jupiter, mars",
"type": "EINER VON: retrograde, moon, transit, season",
"affectedSigns": ["3-4 betroffene Sternzeichen auf Deutsch, z.B. Widder, Stier, Zwillinge, Krebs, Löwe, Jungfrau, Waage, Skorpion, Schütze, Steinbock, Wassermann, Fische"]
}
]`,
},
],
max_completion_tokens: 2500,
});
const content = response.choices[0]?.message?.content || "";
const jsonMatch = content.match(/\[[\s\S]*\]/);
if (!jsonMatch) {
console.error("Failed to parse cosmic events");
return;
}
const parsed = JSON.parse(jsonMatch[0]);
if (!Array.isArray(parsed) || parsed.length === 0) {
console.error("Cosmic events response is not a valid array");
return;
}
for (let i = 0; i < parsed.length; i++) {
const ev = parsed[i];
const icon = VALID_ICONS.includes(ev.icon) ? ev.icon : "moon";
const type = VALID_TYPES.includes(ev.type) ? ev.type : "transit";
const signs = Array.isArray(ev.affectedSigns) ? ev.affectedSigns : [];
await db.insert(cosmicEvents).values({
dateStr: today,
position: i,
title: String(ev.title || "").slice(0, 120),
description: String(ev.description || ""),
dateRange: String(ev.dateRange || "").slice(0, 120),
icon,
type,
affectedSigns: JSON.stringify(signs),
});
}
console.log(`Generated ${parsed.length} cosmic events for ${today}.`);
} catch (err: any) {
console.error("Error generating cosmic events:", err.message);
}
}