/** * Podcast RSS Feed Generation Utilities * Generates RSS 2.0 with iTunes extensions for podcast distribution */ import { createClient } from "@supabase/supabase-js"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; const supabase = createClient(supabaseUrl, supabaseAnonKey); export interface PodcastEpisode { id: string; title: string; description: string; content: string; date: string; timestamp: number; audioUrl?: string; audioDuration?: number; tags?: string[]; } export interface PodcastConfig { title: string; description: string; author: string; email: string; category: string; language: string; websiteUrl: string; imageUrl: string; explicit: boolean; } // Default podcast configuration export const DEFAULT_CONFIG: PodcastConfig = { title: "OpenClaw Daily Digest", description: "Daily curated tech news covering AI coding assistants, iOS development, and digital entrepreneurship. AI-powered summaries delivered as audio.", author: "OpenClaw", email: "podcast@openclaw.ai", category: "Technology", language: "en-US", websiteUrl: "https://blog-backup-two.vercel.app", imageUrl: "https://blog-backup-two.vercel.app/podcast-cover.png", explicit: false, }; /** * Parse title from markdown content */ export function extractTitle(content: string): string { const lines = content.split("\n"); const titleLine = lines.find((l) => l.startsWith("# ") || l.startsWith("## ")); return titleLine?.replace(/#{1,2}\s/, "").trim() || "Daily Digest"; } /** * Extract plain text excerpt from markdown */ export function extractExcerpt(content: string, maxLength: number = 300): string { const plainText = content .replace(/#{1,6}\s/g, "") .replace(/(\*\*|__|\*|_)/g, "") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") .replace(/```[\s\S]*?```/g, "") .replace(/`([^`]+)`/g, "$1") .replace(/<[^>]+>/g, "") .replace(/\n+/g, " ") .trim(); if (plainText.length <= maxLength) return plainText; return plainText.substring(0, maxLength).trim() + "..."; } /** * Format duration in seconds to HH:MM:SS or MM:SS */ export function formatDuration(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; } return `${minutes}:${secs.toString().padStart(2, "0")}`; } /** * Format date to RFC 2822 format for RSS */ export function formatRFC2822(date: Date | string | number): string { const d = new Date(date); return d.toUTCString(); } /** * Escape XML special characters */ export function escapeXml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * Generate unique GUID for episode */ export function generateGuid(episodeId: string): string { return `openclaw-digest-${episodeId}`; } /** * Fetch episodes from database */ export async function fetchEpisodes(limit: number = 50): Promise { const { data, error } = await supabase .from("blog_messages") .select("id, date, content, timestamp, audio_url, audio_duration, tags") .not("audio_url", "is", null) // Only episodes with audio .order("timestamp", { ascending: false }) .limit(limit); if (error) { console.error("Error fetching episodes:", error); throw error; } return (data || []).map(item => ({ id: item.id, title: extractTitle(item.content), description: extractExcerpt(item.content), content: item.content, date: item.date, timestamp: item.timestamp, audioUrl: item.audio_url, audioDuration: item.audio_duration || 300, // Default 5 min if not set tags: item.tags || [], })); } /** * Generate RSS feed XML */ export function generateRSS(episodes: PodcastEpisode[], config: PodcastConfig = DEFAULT_CONFIG): string { const now = new Date(); const lastBuildDate = episodes.length > 0 ? new Date(episodes[0].timestamp) : now; const itemsXml = episodes.map(episode => { const guid = generateGuid(episode.id); const pubDate = formatRFC2822(episode.timestamp); const duration = formatDuration(episode.audioDuration || 300); const enclosureUrl = escapeXml(episode.audioUrl || ""); const title = escapeXml(episode.title); const description = escapeXml(episode.description); const keywords = episode.tags?.join(", ") || "technology, ai, programming"; return ` ${title} ${description} ${pubDate} ${guid} ${escapeXml(`${config.websiteUrl}/?post=${episode.id}`)} ${title} ${escapeXml(config.author)} ${description} ${duration} ${config.explicit ? "yes" : "no"} ${escapeXml(keywords)} `; }).join("\n"); return ` ${escapeXml(config.title)} ${escapeXml(config.websiteUrl)} ${escapeXml(config.description)} ${config.language} ${formatRFC2822(lastBuildDate)} ${escapeXml(config.author)} ${escapeXml(config.description)} ${config.explicit ? "yes" : "no"} ${escapeXml(config.author)} ${escapeXml(config.email)} ${itemsXml} `; }