- 新規記事検出システム: 記事の重複チェックと新規記事のみ処理 - 記事単位ポッドキャスト: フィード統合から記事個別エピソードに変更 - 6時間間隔バッチ処理: 自動定期実行スケジュールの改善 - 完全UIリニューアル: ダッシュボード・フィード管理・エピソード管理の3画面構成 - アクセシビリティ強化: ARIA属性、キーボードナビ、高コントラスト対応 - データベース刷新: feeds/articles/episodes階層構造への移行 - 中央集権設定管理: services/config.ts による設定統一 - エラーハンドリング改善: 全モジュールでの堅牢なエラー処理 - TypeScript型安全性向上: null安全性とインターフェース改善 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
import { promises as fs } from "fs";
|
|
import { dirname } from "path";
|
|
import { Episode, fetchAllEpisodes } 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, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function createItemXml(episode: Episode): 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);
|
|
}
|
|
|
|
return `
|
|
<item>
|
|
<title><![CDATA[${escapeXml(episode.title)}]]></title>
|
|
<description><![CDATA[${escapeXml(episode.title)}]]></description>
|
|
<author>${escapeXml(config.podcast.author)}</author>
|
|
<category>${escapeXml(config.podcast.categories)}</category>
|
|
<language>${config.podcast.language}</language>
|
|
<ttl>${config.podcast.ttl}</ttl>
|
|
<enclosure url="${escapeXml(fileUrl)}" length="${fileSize}" type="audio/mpeg" />
|
|
<guid>${escapeXml(fileUrl)}</guid>
|
|
<pubDate>${pubDate}</pubDate>
|
|
</item>`;
|
|
}
|
|
|
|
export async function updatePodcastRSS(): Promise<void> {
|
|
try {
|
|
const episodes: Episode[] = await fetchAllEpisodes();
|
|
const lastBuildDate = new Date().toUTCString();
|
|
|
|
const itemsXml = episodes.map(createItemXml).join("\n");
|
|
const outputPath = path.join(config.paths.publicDir, "podcast.xml");
|
|
|
|
// Create RSS XML content
|
|
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>${escapeXml(config.podcast.title)}</title>
|
|
<link>${escapeXml(config.podcast.link)}</link>
|
|
<description><![CDATA[${escapeXml(config.podcast.description)}]]></description>
|
|
<language>${config.podcast.language}</language>
|
|
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
|
<ttl>${config.podcast.ttl}</ttl>
|
|
<author>${escapeXml(config.podcast.author)}</author>
|
|
<category>${escapeXml(config.podcast.categories)}</category>${itemsXml}
|
|
</channel>
|
|
</rss>`;
|
|
|
|
// Ensure directory exists
|
|
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
await fs.writeFile(outputPath, rssXml);
|
|
|
|
console.log(`RSS feed updated with ${episodes.length} episodes`);
|
|
} catch (error) {
|
|
console.error("Error updating podcast RSS:", error);
|
|
throw error;
|
|
}
|
|
}
|