Phase 1 - Analytics y Reportes: - PostMetrics and AnalyticsReport models for tracking engagement - Analytics service with dashboard stats, top posts, optimal times - 8 API endpoints at /api/analytics/* - Interactive dashboard with Chart.js charts - Celery tasks for metrics fetch (15min) and weekly reports Phase 2 - Integración Odoo: - Lead and OdooSyncLog models for CRM integration - Odoo fields added to Product and Service models - XML-RPC service for bidirectional sync - Lead management API at /api/leads/* - Leads dashboard template - Celery tasks for product/service sync and lead export Phase 3 - A/B Testing y Recycling: - ABTest, ABTestVariant, RecycledPost models - Statistical winner analysis using chi-square test - Content recycling with engagement-based scoring - APIs at /api/ab-tests/* and /api/recycling/* - Automated test evaluation and content recycling tasks Phase 4 - Thread Series y Templates: - ThreadSeries and ThreadPost models for multi-post threads - AI-powered thread generation - Enhanced ImageTemplate with HTML template support - APIs at /api/threads/* and /api/templates/* - Thread scheduling with reply chain support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
182 lines
4.8 KiB
Python
182 lines
4.8 KiB
Python
"""
|
|
API Routes for Content Recycling.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.database import get_db
|
|
from app.services.recycling_service import recycling_service
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class RecycleRequest(BaseModel):
|
|
"""Schema for recycling a post."""
|
|
content: Optional[str] = None
|
|
hashtags: Optional[List[str]] = None
|
|
image_url: Optional[str] = None
|
|
scheduled_for: Optional[datetime] = None
|
|
platforms: Optional[List[str]] = None
|
|
reason: str = "manual"
|
|
|
|
|
|
@router.get("/candidates")
|
|
async def get_recyclable_candidates(
|
|
platform: Optional[str] = None,
|
|
content_type: Optional[str] = None,
|
|
min_engagement_rate: float = Query(2.0, ge=0),
|
|
min_days: int = Query(30, ge=7, le=365),
|
|
limit: int = Query(20, ge=1, le=50),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Get posts that are good candidates for recycling.
|
|
|
|
- **platform**: Filter by platform
|
|
- **content_type**: Filter by content type
|
|
- **min_engagement_rate**: Minimum engagement rate (default 2.0%)
|
|
- **min_days**: Minimum days since original publish (default 30)
|
|
"""
|
|
candidates = await recycling_service.find_recyclable_posts(
|
|
platform=platform,
|
|
content_type=content_type,
|
|
min_engagement_rate=min_engagement_rate,
|
|
min_days_since_publish=min_days,
|
|
limit=limit
|
|
)
|
|
|
|
return {
|
|
"candidates": candidates,
|
|
"count": len(candidates),
|
|
"filters": {
|
|
"platform": platform,
|
|
"content_type": content_type,
|
|
"min_engagement_rate": min_engagement_rate,
|
|
"min_days": min_days
|
|
}
|
|
}
|
|
|
|
|
|
@router.post("/{post_id}")
|
|
async def recycle_post(
|
|
post_id: int,
|
|
recycle_data: RecycleRequest = None,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Recycle a post by creating a new version.
|
|
|
|
You can optionally modify:
|
|
- **content**: New content text
|
|
- **hashtags**: New hashtag list
|
|
- **image_url**: New image URL
|
|
- **scheduled_for**: When to publish (default: 1 hour from now)
|
|
- **platforms**: Override platforms
|
|
- **reason**: Reason for recycling (manual, high_performer, evergreen, seasonal)
|
|
"""
|
|
modifications = None
|
|
scheduled_for = None
|
|
platforms = None
|
|
reason = "manual"
|
|
|
|
if recycle_data:
|
|
modifications = {}
|
|
if recycle_data.content:
|
|
modifications["content"] = recycle_data.content
|
|
if recycle_data.hashtags:
|
|
modifications["hashtags"] = recycle_data.hashtags
|
|
if recycle_data.image_url:
|
|
modifications["image_url"] = recycle_data.image_url
|
|
|
|
scheduled_for = recycle_data.scheduled_for
|
|
platforms = recycle_data.platforms
|
|
reason = recycle_data.reason
|
|
|
|
result = await recycling_service.recycle_post(
|
|
post_id=post_id,
|
|
modifications=modifications if modifications else None,
|
|
scheduled_for=scheduled_for,
|
|
platforms=platforms,
|
|
reason=reason
|
|
)
|
|
|
|
if not result.get("success"):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=result.get("error", "Failed to recycle post")
|
|
)
|
|
|
|
return {
|
|
"message": "Post recycled successfully",
|
|
**result
|
|
}
|
|
|
|
|
|
@router.post("/auto")
|
|
async def auto_recycle_posts(
|
|
platform: Optional[str] = None,
|
|
count: int = Query(1, ge=1, le=5),
|
|
min_engagement_rate: float = Query(2.0, ge=0),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Automatically recycle top-performing posts.
|
|
|
|
- **platform**: Filter by platform
|
|
- **count**: Number of posts to recycle (max 5)
|
|
- **min_engagement_rate**: Minimum engagement rate threshold
|
|
"""
|
|
result = await recycling_service.auto_recycle(
|
|
platform=platform,
|
|
count=count,
|
|
min_engagement_rate=min_engagement_rate
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/history")
|
|
async def get_recycling_history(
|
|
original_post_id: Optional[int] = None,
|
|
limit: int = Query(50, ge=1, le=200),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Get recycling history.
|
|
|
|
- **original_post_id**: Filter by original post
|
|
"""
|
|
history = await recycling_service.get_recycling_history(
|
|
original_post_id=original_post_id,
|
|
limit=limit
|
|
)
|
|
|
|
return {
|
|
"history": history,
|
|
"count": len(history)
|
|
}
|
|
|
|
|
|
@router.post("/{post_id}/disable")
|
|
async def disable_recycling(
|
|
post_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Mark a post as not eligible for recycling.
|
|
"""
|
|
result = await recycling_service.mark_post_not_recyclable(post_id)
|
|
|
|
if not result.get("success"):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=result.get("error", "Failed to disable recycling")
|
|
)
|
|
|
|
return result
|