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...) }