- Fix text selection menu not showing: wire contentRef via inputRef on multiline TextField - Fix blog title not truncating: add min-w-0 for flex item overflow - Fix outline generation 500: escape curly braces in f-string prompt template - Fix content generation 'NoneType not callable': replace SessionLocal() with get_session_for_user(), add db param to MediumBlogGenerator, fix signature mismatch in database_task_manager - Fix writing assistant suggest 500: add auth + user_id to API endpoint and service, replace sync requests with httpx.AsyncClient - Fix hallucination detector 404: explicitly include router in main.py and app.py - Fix missing error_data in task failure responses - Hide CopilotKit web inspector button - Remove hardcoded fallback suggestions from SmartTypingAssist - Fix stale closure refs in SmartTypingAssist handleTypingChange - Add two-column editor layout, stats bar, section hover menu - Various subscription, billing, and research module improvements
119 lines
4.8 KiB
Python
119 lines
4.8 KiB
Python
"""
|
|
Podcast Trends Handler
|
|
|
|
Endpoints for fetching Google Trends data relevant to podcast topics.
|
|
"""
|
|
|
|
import asyncio
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from typing import Dict, Any, List, Optional
|
|
from pydantic import BaseModel, Field
|
|
from loguru import logger
|
|
|
|
from middleware.auth_middleware import get_current_user
|
|
|
|
router = APIRouter(prefix="/trends", tags=["Podcast Trends"])
|
|
|
|
# Module-level shared instance (singleton pattern)
|
|
_trends_service_instance = None
|
|
_trends_service_lock = None
|
|
|
|
|
|
def get_trends_service():
|
|
"""Get or create shared GoogleTrendsService instance."""
|
|
global _trends_service_instance, _trends_service_lock
|
|
if _trends_service_instance is None:
|
|
try:
|
|
from services.research.trends import GoogleTrendsService
|
|
_trends_service_instance = GoogleTrendsService()
|
|
_trends_service_lock = asyncio.Lock()
|
|
logger.info("[Podcast Trends] Created shared GoogleTrendsService instance")
|
|
except (ImportError, RuntimeError) as e:
|
|
logger.error(f"[Podcast Trends] Failed to create GoogleTrendsService: {e}")
|
|
raise
|
|
return _trends_service_instance
|
|
|
|
|
|
class PodcastTrendsRequest(BaseModel):
|
|
keywords: List[str] = Field(..., min_length=1, max_length=5, description="1-5 keywords to analyze")
|
|
timeframe: str = Field(default="today 12-m", description="Timeframe: 'today 3-m', 'today 12-m', 'today 5-y', 'all'")
|
|
geo: str = Field(default="US", description="Country code: 'US', 'GB', 'IN', etc.")
|
|
source: str = Field(default="web", description="Data source: 'web' (Google), 'podcast' (YouTube)")
|
|
|
|
|
|
class PodcastTrendsResponse(BaseModel):
|
|
success: bool
|
|
data: Optional[Dict[str, Any]] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
@router.post("", response_model=PodcastTrendsResponse)
|
|
async def get_podcast_trends(
|
|
request: PodcastTrendsRequest,
|
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
|
):
|
|
"""Fetch Google Trends data for podcast topic keywords."""
|
|
user_id = current_user.get("user_id") or current_user.get("id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User ID not found")
|
|
|
|
try:
|
|
service = get_trends_service()
|
|
except (ImportError, RuntimeError) as e:
|
|
logger.error(f"[Podcast Trends] GoogleTrendsService unavailable: {e}")
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Google Trends service is currently unavailable. Please try again later."
|
|
)
|
|
|
|
try:
|
|
# Map 'source' to 'gprop' - 'podcast' uses YouTube for video/podcast relevance
|
|
gprop_map = {"": "", "web": "", "podcast": "youtube", "news": "news", "images": "images", "shopping": "froogle"}
|
|
gprop = gprop_map.get(request.source, "")
|
|
|
|
result = await service.analyze_trends(
|
|
keywords=request.keywords,
|
|
timeframe=request.timeframe,
|
|
geo=request.geo,
|
|
gprop=gprop,
|
|
user_id=user_id,
|
|
)
|
|
|
|
has_error = result.get("error")
|
|
has_data = (
|
|
len(result.get("interest_over_time", [])) > 0
|
|
or len(result.get("interest_by_region", [])) > 0
|
|
or len(result.get("related_topics", {}).get("top", [])) > 0
|
|
or len(result.get("related_topics", {}).get("rising", [])) > 0
|
|
or len(result.get("related_queries", {}).get("top", [])) > 0
|
|
or len(result.get("related_queries", {}).get("rising", [])) > 0
|
|
)
|
|
|
|
# Return error if: has error OR no data (meaning blocked/empty)
|
|
if has_error and not has_data:
|
|
error_msg = result.get("error", "")
|
|
cooldown_active = result.get("cooldown_active", False)
|
|
logger.warning(f"[Trends] No data or error: {error_msg[:100]}")
|
|
# Provide helpful message during cooldown
|
|
if cooldown_active:
|
|
return PodcastTrendsResponse(
|
|
success=False,
|
|
data=result,
|
|
error="Google is rate limiting requests. Try using 'Get Trending Topics' instead, or wait 30 minutes."
|
|
)
|
|
return PodcastTrendsResponse(success=False, data=result, error=error_msg or "No trends data available. Google may be blocking requests.")
|
|
|
|
# Even if no error but empty data - return error
|
|
if not has_data:
|
|
logger.warning("[Trends] Empty data returned")
|
|
return PodcastTrendsResponse(success=False, data=result, error="No trends data available. Please try different keywords.")
|
|
|
|
return PodcastTrendsResponse(success=True, data=result)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error(f"[Podcast Trends] Error fetching trends for {request.keywords}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to fetch trends data: {str(e)}"
|
|
) |