| import os |
| import logging |
| from contextlib import asynccontextmanager |
| from pathlib import Path |
|
|
| from fastapi import FastAPI |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.responses import FileResponse |
| from apscheduler.schedulers.background import BackgroundScheduler |
| from apscheduler.triggers.cron import CronTrigger |
| from pytz import timezone |
|
|
| from app.config import settings |
| from app.database import engine, Base, SessionLocal |
| from app.api.videos import router as videos_router |
| from app.api.analytics import router as analytics_router |
| from app.api.tasks import router as tasks_router |
| from app.api.ai import router as ai_router |
| from app.scheduler.jobs import run_scheduled_crawl |
|
|
| logger = logging.getLogger(__name__) |
|
|
| Base.metadata.create_all(bind=engine) |
|
|
| scheduler = BackgroundScheduler() |
| US_EASTERN = timezone("America/New_York") |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| _auto_seed_if_empty() |
|
|
| scheduler.add_job( |
| run_scheduled_crawl, |
| CronTrigger(hour=0, minute=0, timezone=US_EASTERN), |
| id="scheduled_crawl", |
| replace_existing=True, |
| ) |
| scheduler.start() |
| yield |
| scheduler.shutdown() |
|
|
|
|
| def _auto_seed_if_empty(): |
| from app.models.video import Video |
| db = SessionLocal() |
| try: |
| count = db.query(Video).count() |
| if count == 0: |
| youtube_count = 0 |
| tiktok_count = 0 |
| |
| if settings.YOUTUBE_API_ENABLED: |
| try: |
| from app.crawlers.youtube import YouTubeCrawler |
| |
| crawler = YouTubeCrawler() |
| videos = crawler.get_trending_videos(region_code="US", max_results=50) |
| if videos: |
| saved = crawler.save_videos_to_db(videos, db) |
| youtube_count = len(saved) |
| logger.info(f"使用 YouTube API 获取了 {youtube_count} 条真实数据") |
| except Exception as e: |
| logger.warning(f"YouTube API 获取失败: {e}") |
| |
| try: |
| from app.crawlers.tiktok import TikTokCrawler |
| |
| crawler = TikTokCrawler(tikhub_api_key=settings.TIKHUB_API_KEY, use_free_first=True) |
| videos = crawler.get_trending_videos(count=50) |
| if videos: |
| saved = crawler.save_videos_to_db(videos, db) |
| tiktok_count = len(saved) |
| logger.info(f"获取了 {tiktok_count} 条 TikTok 数据") |
| except Exception as e: |
| logger.warning(f"TikTok 获取失败: {e}") |
| |
| if youtube_count > 0 or tiktok_count > 0: |
| from app.analyzers.viral import ViralAnalyzer |
| from app.analyzers.revenue import RevenueAnalyzer |
| ViralAnalyzer(db).calculate_viral_scores() |
| RevenueAnalyzer(db).batch_estimate_revenue() |
| logger.info(f"真实数据总计: YouTube {youtube_count} 条, TikTok {tiktok_count} 条") |
| else: |
| from app.demo.seed import generate_demo_data |
| from app.analyzers.viral import ViralAnalyzer |
| from app.analyzers.revenue import RevenueAnalyzer |
| n = generate_demo_data(db) |
| if n > 0: |
| ViralAnalyzer(db).calculate_viral_scores() |
| RevenueAnalyzer(db).batch_estimate_revenue() |
| logger.info(f"使用演示数据: {n} 条") |
| except Exception: |
| pass |
| finally: |
| db.close() |
|
|
|
|
| app = FastAPI( |
| title=settings.PROJECT_NAME, |
| version=settings.VERSION, |
| lifespan=lifespan, |
| ) |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=settings.CORS_ORIGINS + ["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| app.include_router(videos_router, prefix=settings.API_PREFIX) |
| app.include_router(analytics_router, prefix=settings.API_PREFIX) |
| app.include_router(tasks_router, prefix=settings.API_PREFIX) |
| app.include_router(ai_router, prefix=settings.API_PREFIX) |
|
|
| STATIC_DIR = Path(__file__).parent / "static" |
| if STATIC_DIR.exists(): |
| app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets") |
|
|
|
|
| @app.get("/") |
| def root(): |
| if STATIC_DIR.exists() and (STATIC_DIR / "index.html").exists(): |
| return FileResponse(STATIC_DIR / "index.html") |
| return { |
| "name": settings.PROJECT_NAME, |
| "version": settings.VERSION, |
| "docs": "/docs", |
| } |
|
|
|
|
| @app.get("/health") |
| def health_check(): |
| return {"status": "healthy", "timestamp": __import__("datetime").datetime.now().isoformat()} |
|
|
|
|
| @app.get("/{path:path}") |
| async def serve_spa(path: str): |
| file_path = STATIC_DIR / path |
| if file_path.exists() and file_path.is_file(): |
| return FileResponse(file_path) |
| if STATIC_DIR.exists() and (STATIC_DIR / "index.html").exists(): |
| return FileResponse(STATIC_DIR / "index.html") |
| return {"error": "Not found"} |
|
|