feat: 記事ごとのポッドキャスト生成と新規記事検出システム、モダンUIの実装
- 新規記事検出システム: 記事の重複チェックと新規記事のみ処理 - 記事単位ポッドキャスト: フィード統合から記事個別エピソードに変更 - 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>
This commit is contained in:
117
services/config.ts
Normal file
117
services/config.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import path from "path";
|
||||
|
||||
interface Config {
|
||||
// OpenAI Configuration
|
||||
openai: {
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
modelName: string;
|
||||
};
|
||||
|
||||
// VOICEVOX Configuration
|
||||
voicevox: {
|
||||
host: string;
|
||||
styleId: number;
|
||||
};
|
||||
|
||||
// Podcast Configuration
|
||||
podcast: {
|
||||
title: string;
|
||||
link: string;
|
||||
description: string;
|
||||
language: string;
|
||||
author: string;
|
||||
categories: string;
|
||||
ttl: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
// File paths
|
||||
paths: {
|
||||
projectRoot: string;
|
||||
dataDir: string;
|
||||
dbPath: string;
|
||||
publicDir: string;
|
||||
podcastAudioDir: string;
|
||||
frontendBuildDir: string;
|
||||
feedUrlsFile: string;
|
||||
};
|
||||
}
|
||||
|
||||
function getRequiredEnv(key: string): string {
|
||||
const value = import.meta.env[key];
|
||||
if (!value) {
|
||||
throw new Error(`Required environment variable ${key} is not set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getOptionalEnv(key: string, defaultValue: string): string {
|
||||
return import.meta.env[key] ?? defaultValue;
|
||||
}
|
||||
|
||||
function createConfig(): Config {
|
||||
const projectRoot = import.meta.dirname ? path.dirname(import.meta.dirname) : process.cwd();
|
||||
const dataDir = path.join(projectRoot, "data");
|
||||
const publicDir = path.join(projectRoot, "public");
|
||||
|
||||
return {
|
||||
openai: {
|
||||
apiKey: getRequiredEnv("OPENAI_API_KEY"),
|
||||
endpoint: getOptionalEnv("OPENAI_API_ENDPOINT", "https://api.openai.com/v1"),
|
||||
modelName: getOptionalEnv("OPENAI_MODEL_NAME", "gpt-4o-mini"),
|
||||
},
|
||||
|
||||
voicevox: {
|
||||
host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"),
|
||||
styleId: parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
|
||||
},
|
||||
|
||||
podcast: {
|
||||
title: getOptionalEnv("PODCAST_TITLE", "自動生成ポッドキャスト"),
|
||||
link: getOptionalEnv("PODCAST_LINK", "https://your-domain.com/podcast"),
|
||||
description: getOptionalEnv("PODCAST_DESCRIPTION", "RSSフィードから自動生成された音声ポッドキャスト"),
|
||||
language: getOptionalEnv("PODCAST_LANGUAGE", "ja"),
|
||||
author: getOptionalEnv("PODCAST_AUTHOR", "管理者"),
|
||||
categories: getOptionalEnv("PODCAST_CATEGORIES", "Technology"),
|
||||
ttl: getOptionalEnv("PODCAST_TTL", "60"),
|
||||
baseUrl: getOptionalEnv("PODCAST_BASE_URL", "https://your-domain.com"),
|
||||
},
|
||||
|
||||
paths: {
|
||||
projectRoot,
|
||||
dataDir,
|
||||
dbPath: path.join(dataDir, "podcast.db"),
|
||||
publicDir,
|
||||
podcastAudioDir: path.join(publicDir, "podcast_audio"),
|
||||
frontendBuildDir: path.join(projectRoot, "frontend", "dist"),
|
||||
feedUrlsFile: path.join(projectRoot, getOptionalEnv("FEED_URLS_FILE", "feed_urls.txt")),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const config = createConfig();
|
||||
|
||||
export function validateConfig(): void {
|
||||
// Validate required configuration
|
||||
if (!config.openai.apiKey) {
|
||||
throw new Error("OPENAI_API_KEY is required");
|
||||
}
|
||||
|
||||
if (isNaN(config.voicevox.styleId)) {
|
||||
throw new Error("VOICEVOX_STYLE_ID must be a valid number");
|
||||
}
|
||||
|
||||
// Validate URLs
|
||||
try {
|
||||
new URL(config.voicevox.host);
|
||||
} catch {
|
||||
throw new Error("VOICEVOX_HOST must be a valid URL");
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(config.openai.endpoint);
|
||||
} catch {
|
||||
throw new Error("OPENAI_API_ENDPOINT must be a valid URL");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user