kv-tube/backend/routes/api.go
2026-03-26 13:11:20 +07:00

446 lines
11 KiB
Go

package routes
import (
"log"
"net/http"
"os"
"strconv"
"strings"
"kvtube-go/services"
"github.com/gin-gonic/gin"
)
// getAllowedOrigins returns allowed CORS origins from environment variable or defaults
func getAllowedOrigins() []string {
originsEnv := os.Getenv("CORS_ALLOWED_ORIGINS")
if originsEnv == "" {
// Default: allow localhost for development
return []string{
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://localhost:5011",
"http://127.0.0.1:5011",
}
}
origins := strings.Split(originsEnv, ",")
for i := range origins {
origins[i] = strings.TrimSpace(origins[i])
}
return origins
}
// isAllowedOrigin checks if the given origin is in the allowed list
func isAllowedOrigin(origin string, allowedOrigins []string) bool {
for _, allowed := range allowedOrigins {
if allowed == "*" || allowed == origin {
return true
}
}
return false
}
func SetupRouter() *gin.Engine {
r := gin.Default()
// CORS middleware - restrict to specific origins from environment variable
allowedOrigins := getAllowedOrigins()
r.Use(func(c *gin.Context) {
origin := c.GetHeader("Origin")
if origin != "" && isAllowedOrigin(origin, allowedOrigins) {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
}
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
// API Routes - Using yt-dlp for video operations
api := r.Group("/api")
{
// Health check
api.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// Video endpoints
api.GET("/search", handleSearch)
api.GET("/trending", handleTrending)
api.GET("/video/:id", handleGetVideoInfo)
api.GET("/video/:id/qualities", handleGetQualities)
api.GET("/video/:id/related", handleRelatedVideos)
api.GET("/video/:id/comments", handleComments)
api.GET("/video/:id/download", handleDownload)
// Channel endpoints
api.GET("/channel/info", handleChannelInfo)
api.GET("/channel/videos", handleChannelVideos)
// History routes
api.POST("/history", handlePostHistory)
api.GET("/history", handleGetHistory)
api.GET("/suggestions", handleGetSuggestions)
// Subscription routes
api.POST("/subscribe", handleSubscribe)
api.DELETE("/subscribe", handleUnsubscribe)
api.GET("/subscribe", handleCheckSubscription)
api.GET("/subscriptions", handleGetSubscriptions)
}
return r
}
// Video search endpoint
func handleSearch(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Query parameter 'q' is required"})
return
}
limitStr := c.Query("limit")
limit := 20
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
limit = l
}
results, err := services.SearchVideos(query, limit)
if err != nil {
log.Printf("Search error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search videos"})
return
}
c.JSON(http.StatusOK, results)
}
// Trending videos endpoint
func handleTrending(c *gin.Context) {
limitStr := c.Query("limit")
limit := 20
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
limit = l
}
// Use popular music search as trending
results, err := services.SearchVideos("popular music trending", limit)
if err != nil {
log.Printf("Trending error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get trending videos"})
return
}
c.JSON(http.StatusOK, results)
}
// Get video info
func handleGetVideoInfo(c *gin.Context) {
videoID := c.Param("id")
if videoID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Video ID is required"})
return
}
video, err := services.GetVideoInfo(videoID)
if err != nil {
log.Printf("GetVideoInfo error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get video info"})
return
}
c.JSON(http.StatusOK, video)
}
// Get video qualities
func handleGetQualities(c *gin.Context) {
videoID := c.Param("id")
if videoID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Video ID is required"})
return
}
qualities, audioURL, err := services.GetVideoQualitiesWithAudio(videoID)
if err != nil {
log.Printf("GetQualities error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get video qualities"})
return
}
c.JSON(http.StatusOK, gin.H{
"qualities": qualities,
"audio_url": audioURL,
})
}
// Get related videos
func handleRelatedVideos(c *gin.Context) {
videoID := c.Param("id")
if videoID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Video ID is required"})
return
}
limitStr := c.Query("limit")
limit := 15
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 {
limit = l
}
// First get video info to get title and uploader
video, err := services.GetVideoInfo(videoID)
if err != nil {
log.Printf("GetVideoInfo for related error: %v", err)
// Fallback: search for similar content
results, err := services.SearchVideos("music", limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get related videos"})
return
}
c.JSON(http.StatusOK, results)
return
}
related, err := services.GetRelatedVideos(video.Title, video.Uploader, limit)
if err != nil {
log.Printf("GetRelatedVideos error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get related videos"})
return
}
c.JSON(http.StatusOK, related)
}
// Get video comments
func handleComments(c *gin.Context) {
videoID := c.Param("id")
if videoID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Video ID is required"})
return
}
limitStr := c.Query("limit")
limit := 20
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
comments, err := services.GetComments(videoID, limit)
if err != nil {
log.Printf("GetComments error: %v", err)
c.JSON(http.StatusOK, []interface{}{}) // Return empty array instead of error
return
}
c.JSON(http.StatusOK, comments)
}
// Get download URL
func handleDownload(c *gin.Context) {
videoID := c.Param("id")
if videoID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Video ID is required"})
return
}
formatID := c.Query("format")
downloadInfo, err := services.GetDownloadURL(videoID, formatID)
if err != nil {
log.Printf("GetDownloadURL error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get download URL"})
return
}
c.JSON(http.StatusOK, downloadInfo)
}
// Get channel info
func handleChannelInfo(c *gin.Context) {
channelID := c.Query("id")
if channelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Channel ID is required"})
return
}
channelInfo, err := services.GetChannelInfo(channelID)
if err != nil {
log.Printf("GetChannelInfo error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get channel info"})
return
}
c.JSON(http.StatusOK, channelInfo)
}
// Get channel videos
func handleChannelVideos(c *gin.Context) {
channelID := c.Query("id")
if channelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Channel ID is required"})
return
}
limitStr := c.Query("limit")
limit := 30
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
videos, err := services.GetChannelVideos(channelID, limit)
if err != nil {
log.Printf("GetChannelVideos error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get channel videos"})
return
}
c.JSON(http.StatusOK, videos)
}
// History handlers
func handlePostHistory(c *gin.Context) {
var body struct {
VideoID string `json:"video_id"`
Title string `json:"title"`
Thumbnail string `json:"thumbnail"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if body.VideoID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Video ID is required"})
return
}
err := services.AddToHistory(body.VideoID, body.Title, body.Thumbnail)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update history"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
func handleGetHistory(c *gin.Context) {
limitStr := c.Query("limit")
limit := 50
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
history, err := services.GetHistory(limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get history"})
return
}
// Make the API response shape match the VideoData shape the frontend expects
var results []services.VideoData
for _, h := range history {
results = append(results, services.VideoData{
ID: h.ID,
Title: h.Title,
Thumbnail: h.Thumbnail,
Uploader: "History",
})
}
c.JSON(http.StatusOK, results)
}
func handleGetSuggestions(c *gin.Context) {
limitStr := c.Query("limit")
limit := 20
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
suggestions, err := services.GetSuggestions(limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get suggestions"})
return
}
c.JSON(http.StatusOK, suggestions)
}
// Subscription handlers
func handleSubscribe(c *gin.Context) {
var body struct {
ChannelID string `json:"channel_id"`
ChannelName string `json:"channel_name"`
ChannelAvatar string `json:"channel_avatar"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if body.ChannelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Channel ID is required"})
return
}
err := services.SubscribeChannel(body.ChannelID, body.ChannelName, body.ChannelAvatar)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to subscribe"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "subscribed"})
}
func handleUnsubscribe(c *gin.Context) {
channelID := c.Query("channel_id")
if channelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Channel ID is required"})
return
}
err := services.UnsubscribeChannel(channelID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unsubscribe"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "unsubscribed"})
}
func handleCheckSubscription(c *gin.Context) {
channelID := c.Query("channel_id")
if channelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Channel ID is required"})
return
}
subscribed, err := services.IsSubscribed(channelID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check subscription"})
return
}
c.JSON(http.StatusOK, gin.H{"subscribed": subscribed})
}
func handleGetSubscriptions(c *gin.Context) {
subs, err := services.GetSubscriptions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get subscriptions"})
return
}
c.JSON(http.StatusOK, subs)
}
func logPrintf(format string, v ...interface{}) {
log.Printf(format, v...)
}