import { promises as fs } from "fs"; import { dirname } from "path"; import { fetchEpisodesWithFeedInfo } from "./database.js"; import path from "node:path"; import fsSync from "node:fs"; import { config } from "./config.js"; function escapeXml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function createItemXml(episode: any): string { const fileUrl = `${config.podcast.baseUrl}/podcast_audio/${path.basename(episode.audioPath)}`; const pubDate = new Date(episode.createdAt).toUTCString(); let fileSize = 0; try { const audioPath = path.join( config.paths.podcastAudioDir, episode.audioPath, ); if (fsSync.existsSync(audioPath)) { fileSize = fsSync.statSync(audioPath).size; } } catch (error) { console.warn(`Could not get file size for ${episode.audioPath}:`, error); } // Build enhanced description with feed and article info let description = episode.title; if (episode.feedTitle || episode.articleTitle || episode.articleLink) { description += "\n\n"; if (episode.feedTitle) { description += `フィード: ${episode.feedTitle}\n`; } if (episode.articleTitle && episode.articleTitle !== episode.title) { description += `元記事: ${episode.articleTitle}\n`; } if (episode.articlePubDate) { description += `記事公開日: ${new Date(episode.articlePubDate).toLocaleString("ja-JP")}\n`; } if (episode.articleLink) { description += `元記事URL: ${episode.articleLink}`; } } return ` <![CDATA[${escapeXml(episode.title)}]]> ${escapeXml(config.podcast.author)} ${escapeXml(config.podcast.categories)} ${config.podcast.language} ${config.podcast.ttl} ${escapeXml(fileUrl)} ${pubDate} ${episode.articleLink ? `${escapeXml(episode.articleLink)}` : ""} `; } export async function updatePodcastRSS(): Promise { try { // Use episodes with feed info for enhanced descriptions const episodesWithFeedInfo = await fetchEpisodesWithFeedInfo(); // Filter episodes to only include those with valid audio files const validEpisodes = episodesWithFeedInfo.filter((episode) => { try { const audioPath = path.join( config.paths.podcastAudioDir, episode.audioPath, ); return fsSync.existsSync(audioPath); } catch (error) { console.warn(`Audio file not found for episode: ${episode.title}`); return false; } }); console.log( `Found ${episodesWithFeedInfo.length} episodes, ${validEpisodes.length} with valid audio files`, ); const lastBuildDate = new Date().toUTCString(); const itemsXml = validEpisodes.map(createItemXml).join("\n"); const outputPath = path.join(config.paths.publicDir, "podcast.xml"); // Create RSS XML content const rssXml = ` ${escapeXml(config.podcast.title)} ${escapeXml(config.podcast.link)} ${config.podcast.language} ${lastBuildDate} ${config.podcast.ttl} ${escapeXml(config.podcast.author)} ${escapeXml(config.podcast.categories)}${itemsXml} `; // Ensure directory exists await fs.mkdir(dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, rssXml); console.log( `RSS feed updated with ${validEpisodes.length} episodes (audio files verified)`, ); } catch (error) { console.error("Error updating podcast RSS:", error); throw error; } }