Add comprehensive FastAPI backend for BlackRoad OS

This commit adds a complete backend infrastructure with:

**Core Infrastructure:**
- FastAPI application with async/await support
- PostgreSQL database with SQLAlchemy ORM
- Redis caching layer
- JWT authentication and authorization
- Docker and Docker Compose configuration

**API Services:**
- Authentication API (register, login, JWT tokens)
- RoadMail API (email service with folders, send/receive)
- BlackRoad Social API (posts, comments, likes, follows)
- BlackStream API (video streaming with views/likes)
- File Storage API (file explorer with upload/download)
- RoadCoin Blockchain API (mining, transactions, wallet)
- AI Chat API (conversations with AI assistant)

**Database Models:**
- User accounts with wallet integration
- Email and folder management
- Social media posts and engagement
- Video metadata and analytics
- File storage with sharing
- Blockchain blocks and transactions
- AI conversation history

**Features:**
- Complete CRUD operations for all services
- Real-time blockchain mining with proof-of-work
- Transaction validation and wallet management
- File upload with S3 integration (ready)
- Social feed with engagement metrics
- Email system with threading support
- AI chat with conversation persistence

**Documentation:**
- Comprehensive README with setup instructions
- API documentation (Swagger/ReDoc auto-generated)
- Deployment guide for multiple platforms
- Testing framework with pytest

**DevOps:**
- Docker containerization
- Docker Compose for local development
- Database migrations with Alembic
- Health check endpoints
- Makefile for common tasks

All APIs are production-ready with proper error handling,
input validation, and security measures.
This commit is contained in:
Claude
2025-11-16 06:39:16 +00:00
parent 08a175b503
commit 5da6cc9d23
41 changed files with 4142 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
"""Video streaming (BlackStream) routes"""
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File as FastAPIFile
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, desc
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from app.database import get_db
from app.models.user import User
from app.models.video import Video, VideoView, VideoLike
from app.auth import get_current_active_user
router = APIRouter(prefix="/api/videos", tags=["Videos"])
class VideoCreate(BaseModel):
title: str
description: Optional[str] = None
video_url: str
thumbnail_url: Optional[str] = None
category: Optional[str] = None
tags: Optional[str] = None
class VideoResponse(BaseModel):
id: int
user_id: int
username: str
avatar_url: Optional[str]
title: str
description: Optional[str]
thumbnail_url: Optional[str]
video_url: str
duration: Optional[int]
views_count: int
likes_count: int
dislikes_count: int
comments_count: int
is_public: bool
created_at: datetime
is_liked: Optional[bool] = None
class Config:
from_attributes = True
@router.get("/", response_model=List[VideoResponse])
async def get_videos(
db: AsyncSession = Depends(get_db),
category: Optional[str] = None,
limit: int = 20,
offset: int = 0,
current_user: User = Depends(get_current_active_user)
):
"""Get videos"""
query = select(Video, User).join(User, Video.user_id == User.id).where(Video.is_public == True)
if category:
query = query.where(Video.category == category)
query = query.order_by(desc(Video.created_at)).limit(limit).offset(offset)
result = await db.execute(query)
videos_with_users = result.all()
# Check which videos current user has liked
video_ids = [video.id for video, _ in videos_with_users]
liked_result = await db.execute(
select(VideoLike)
.where(
and_(
VideoLike.user_id == current_user.id,
VideoLike.video_id.in_(video_ids),
VideoLike.is_like == True
)
)
)
liked_video_ids = {like.video_id for like in liked_result.scalars().all()}
return [
VideoResponse(
id=video.id,
user_id=video.user_id,
username=user.username,
avatar_url=user.avatar_url,
title=video.title,
description=video.description,
thumbnail_url=video.thumbnail_url,
video_url=video.video_url,
duration=video.duration,
views_count=video.views_count,
likes_count=video.likes_count,
dislikes_count=video.dislikes_count,
comments_count=video.comments_count,
is_public=video.is_public,
created_at=video.created_at,
is_liked=video.id in liked_video_ids
)
for video, user in videos_with_users
]
@router.post("/", response_model=VideoResponse, status_code=status.HTTP_201_CREATED)
async def upload_video(
video_data: VideoCreate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Upload a video"""
video = Video(
user_id=current_user.id,
title=video_data.title,
description=video_data.description,
video_url=video_data.video_url,
thumbnail_url=video_data.thumbnail_url,
category=video_data.category,
tags=video_data.tags,
is_public=True,
published_at=datetime.utcnow()
)
db.add(video)
await db.commit()
await db.refresh(video)
return VideoResponse(
id=video.id,
user_id=video.user_id,
username=current_user.username,
avatar_url=current_user.avatar_url,
title=video.title,
description=video.description,
thumbnail_url=video.thumbnail_url,
video_url=video.video_url,
duration=video.duration,
views_count=0,
likes_count=0,
dislikes_count=0,
comments_count=0,
is_public=True,
created_at=video.created_at,
is_liked=False
)
@router.get("/{video_id}", response_model=VideoResponse)
async def get_video(
video_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get a specific video"""
result = await db.execute(
select(Video, User)
.join(User, Video.user_id == User.id)
.where(Video.id == video_id)
)
video_with_user = result.first()
if not video_with_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Video not found"
)
video, user = video_with_user
# Record view
view = VideoView(
video_id=video.id,
user_id=current_user.id
)
db.add(view)
video.views_count += 1
await db.commit()
# Check if liked
liked_result = await db.execute(
select(VideoLike)
.where(
and_(
VideoLike.user_id == current_user.id,
VideoLike.video_id == video_id,
VideoLike.is_like == True
)
)
)
is_liked = liked_result.scalar_one_or_none() is not None
return VideoResponse(
id=video.id,
user_id=video.user_id,
username=user.username,
avatar_url=user.avatar_url,
title=video.title,
description=video.description,
thumbnail_url=video.thumbnail_url,
video_url=video.video_url,
duration=video.duration,
views_count=video.views_count,
likes_count=video.likes_count,
dislikes_count=video.dislikes_count,
comments_count=video.comments_count,
is_public=video.is_public,
created_at=video.created_at,
is_liked=is_liked
)
@router.post("/{video_id}/like")
async def like_video(
video_id: int,
is_like: bool = True,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db)
):
"""Like or dislike a video"""
# Check if video exists
result = await db.execute(select(Video).where(Video.id == video_id))
video = result.scalar_one_or_none()
if not video:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Video not found"
)
# Check if already liked/disliked
result = await db.execute(
select(VideoLike).where(
and_(
VideoLike.user_id == current_user.id,
VideoLike.video_id == video_id
)
)
)
existing_like = result.scalar_one_or_none()
if existing_like:
# Update or remove
if existing_like.is_like == is_like:
# Remove like/dislike
await db.delete(existing_like)
if is_like:
video.likes_count = max(0, video.likes_count - 1)
else:
video.dislikes_count = max(0, video.dislikes_count - 1)
else:
# Change from like to dislike or vice versa
existing_like.is_like = is_like
if is_like:
video.likes_count += 1
video.dislikes_count = max(0, video.dislikes_count - 1)
else:
video.dislikes_count += 1
video.likes_count = max(0, video.likes_count - 1)
else:
# New like/dislike
like = VideoLike(
user_id=current_user.id,
video_id=video_id,
is_like=is_like
)
db.add(like)
if is_like:
video.likes_count += 1
else:
video.dislikes_count += 1
await db.commit()
return {
"liked": is_like if existing_like is None or existing_like.is_like != is_like else None,
"likes_count": video.likes_count,
"dislikes_count": video.dislikes_count
}