Files
social-media-automation/app/api/routes/recycling.py
Consultoría AS ecc2ca73ea feat: Add Analytics, Odoo Integration, A/B Testing, and Content features
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>
2026-01-28 03:10:42 +00:00

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