package main import ( "context" "fmt" "log" "os" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gooseek/backend/internal/llm" "github.com/gooseek/backend/internal/podcast" "github.com/gooseek/backend/pkg/config" ) type PodcastStore struct { podcasts map[string]*podcast.Podcast } func NewPodcastStore() *PodcastStore { return &PodcastStore{ podcasts: make(map[string]*podcast.Podcast), } } func (s *PodcastStore) Save(p *podcast.Podcast) { s.podcasts[p.ID] = p } func (s *PodcastStore) Get(id string) *podcast.Podcast { return s.podcasts[id] } func (s *PodcastStore) List(limit, offset int) []*podcast.Podcast { result := make([]*podcast.Podcast, 0) i := 0 for _, p := range s.podcasts { if i >= offset && len(result) < limit { result = append(result, p) } i++ } return result } func (s *PodcastStore) GetLatest() *podcast.Podcast { var latest *podcast.Podcast for _, p := range s.podcasts { if latest == nil || p.GeneratedAt.After(latest.GeneratedAt) { latest = p } } return latest } func (s *PodcastStore) GetByDate(date time.Time) *podcast.Podcast { dateStr := date.Format("2006-01-02") for _, p := range s.podcasts { if p.Date.Format("2006-01-02") == dateStr { return p } } return nil } func main() { cfg, err := config.Load() if err != nil { log.Fatal("Failed to load config:", err) } var llmClient llm.Client if cfg.OpenAIAPIKey != "" { client, err := llm.NewOpenAIClient(llm.ProviderConfig{ ProviderID: "openai", APIKey: cfg.OpenAIAPIKey, ModelKey: "gpt-4o-mini", }) if err != nil { log.Fatal("Failed to create OpenAI client:", err) } llmClient = client } else if cfg.AnthropicAPIKey != "" { client, err := llm.NewAnthropicClient(llm.ProviderConfig{ ProviderID: "anthropic", APIKey: cfg.AnthropicAPIKey, ModelKey: "claude-3-5-sonnet-20241022", }) if err != nil { log.Fatal("Failed to create Anthropic client:", err) } llmClient = client } var ttsClient podcast.TTSClient elevenLabsKey := os.Getenv("ELEVENLABS_API_KEY") if elevenLabsKey != "" { ttsClient = podcast.NewElevenLabsTTS(elevenLabsKey) } else { ttsClient = &podcast.DummyTTS{} } generator := podcast.NewPodcastGenerator(llmClient, ttsClient, podcast.GeneratorConfig{ DefaultDuration: 300, MaxDuration: 1800, OutputDir: "/data/podcasts", }) store := NewPodcastStore() app := fiber.New(fiber.Config{ BodyLimit: 50 * 1024 * 1024, ReadTimeout: 120 * time.Second, WriteTimeout: 120 * time.Second, }) app.Use(logger.New()) app.Use(cors.New()) app.Get("/health", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ok"}) }) app.Post("/api/v1/podcast/generate", func(c *fiber.Ctx) error { var req struct { Type string `json:"type"` Topics []string `json:"topics"` NewsItems []podcast.NewsItem `json:"newsItems"` Duration int `json:"duration"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } podcastType := podcast.PodcastDaily switch req.Type { case "weekly": podcastType = podcast.PodcastWeekly case "topic_deep": podcastType = podcast.PodcastTopicDeep case "breaking": podcastType = podcast.PodcastBreaking } ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() p, err := generator.GenerateDailyPodcast(ctx, podcast.GenerateOptions{ Type: podcastType, Topics: req.Topics, NewsItems: req.NewsItems, Duration: req.Duration, Locale: req.Locale, IncludeIntro: true, IncludeOutro: true, }) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } store.Save(p) return c.JSON(p) }) app.Post("/api/v1/podcast/generate-daily", func(c *fiber.Ctx) error { var req struct { NewsItems []podcast.NewsItem `json:"newsItems"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } if req.Locale == "" { req.Locale = "ru" } ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() p, err := generator.GenerateDailyPodcast(ctx, podcast.GenerateOptions{ Type: podcast.PodcastDaily, NewsItems: req.NewsItems, Date: time.Now(), Duration: 300, Locale: req.Locale, IncludeIntro: true, IncludeOutro: true, }) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } store.Save(p) return c.JSON(p) }) app.Post("/api/v1/podcast/generate-weekly", func(c *fiber.Ctx) error { var req struct { NewsItems []podcast.NewsItem `json:"newsItems"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) defer cancel() p, err := generator.GenerateWeeklySummary(ctx, req.NewsItems, req.Locale) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } store.Save(p) return c.JSON(p) }) app.Post("/api/v1/podcast/generate-topic", func(c *fiber.Ctx) error { var req struct { Topic string `json:"topic"` Articles []podcast.NewsItem `json:"articles"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) defer cancel() p, err := generator.GenerateTopicDeepDive(ctx, req.Topic, req.Articles, req.Locale) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } store.Save(p) return c.JSON(p) }) app.Post("/api/v1/podcast/:id/audio", func(c *fiber.Ctx) error { id := c.Params("id") p := store.Get(id) if p == nil { return c.Status(404).JSON(fiber.Map{"error": "Podcast not found"}) } ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) defer cancel() audioData, err := generator.GenerateAudio(ctx, p) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } c.Set("Content-Type", "audio/mpeg") c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.mp3\"", p.ID)) return c.Send(audioData) }) app.Get("/api/v1/podcasts", func(c *fiber.Ctx) error { limit := c.QueryInt("limit", 20) offset := c.QueryInt("offset", 0) podcasts := store.List(limit, offset) summaries := make([]map[string]interface{}, 0) for _, p := range podcasts { summaries = append(summaries, map[string]interface{}{ "id": p.ID, "title": p.Title, "description": p.Description, "type": p.Type, "date": p.Date, "duration": p.Duration, "status": p.Status, "topics": p.Topics, "audioUrl": p.AudioURL, "generatedAt": p.GeneratedAt, }) } return c.JSON(fiber.Map{"podcasts": summaries, "count": len(summaries)}) }) app.Get("/api/v1/podcasts/latest", func(c *fiber.Ctx) error { p := store.GetLatest() if p == nil { return c.Status(404).JSON(fiber.Map{"error": "No podcasts found"}) } return c.JSON(p) }) app.Get("/api/v1/podcasts/today", func(c *fiber.Ctx) error { p := store.GetByDate(time.Now()) if p == nil { return c.Status(404).JSON(fiber.Map{"error": "No podcast for today"}) } return c.JSON(p) }) app.Get("/api/v1/podcasts/:id", func(c *fiber.Ctx) error { id := c.Params("id") p := store.Get(id) if p == nil { return c.Status(404).JSON(fiber.Map{"error": "Podcast not found"}) } return c.JSON(p) }) app.Get("/api/v1/podcasts/:id/transcript", func(c *fiber.Ctx) error { id := c.Params("id") p := store.Get(id) if p == nil { return c.Status(404).JSON(fiber.Map{"error": "Podcast not found"}) } return c.JSON(fiber.Map{ "id": p.ID, "title": p.Title, "transcript": p.Transcript, "segments": p.Segments, }) }) app.Get("/api/v1/podcasts/:id/segments", func(c *fiber.Ctx) error { id := c.Params("id") p := store.Get(id) if p == nil { return c.Status(404).JSON(fiber.Map{"error": "Podcast not found"}) } return c.JSON(fiber.Map{ "segments": p.Segments, }) }) app.Post("/api/v1/podcast/:id/publish", func(c *fiber.Ctx) error { id := c.Params("id") p := store.Get(id) if p == nil { return c.Status(404).JSON(fiber.Map{"error": "Podcast not found"}) } if p.Status != podcast.StatusReady { return c.Status(400).JSON(fiber.Map{"error": "Podcast audio not ready"}) } now := time.Now() p.Status = podcast.StatusPublished p.PublishedAt = &now return c.JSON(fiber.Map{"success": true, "publishedAt": now}) }) app.Get("/api/v1/podcast/rss", func(c *fiber.Ctx) error { podcasts := store.List(50, 0) rss := generateRSSFeed(podcasts, c.BaseURL()) c.Set("Content-Type", "application/rss+xml") return c.SendString(rss) }) port := getEnvInt("PODCAST_SVC_PORT", 3032) log.Printf("podcast-svc listening on :%d", port) log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) } func generateRSSFeed(podcasts []*podcast.Podcast, baseURL string) string { items := "" for _, p := range podcasts { if p.Status == podcast.StatusPublished && p.AudioURL != "" { pubDate := "" if p.PublishedAt != nil { pubDate = p.PublishedAt.Format(time.RFC1123Z) } items += fmt.Sprintf(` %s %s %s %d `, p.Title, p.Description, pubDate, p.AudioURL, p.ID, p.Duration) } } return fmt.Sprintf(` GooSeek Daily %s Ежедневный подкаст с главными новостями от GooSeek ru GooSeek %s `, baseURL, baseURL, items) } func getEnvInt(key string, defaultValue int) int { if val := os.Getenv(key); val != "" { var result int if _, err := fmt.Sscanf(val, "%d", &result); err == nil { return result } } return defaultValue }