mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fotbal-club/internal/models"
|
||||
"fotbal-club/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type YouTubeController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func NewYouTubeController(db *gorm.DB) *YouTubeController {
|
||||
return &YouTubeController{DB: db}
|
||||
}
|
||||
|
||||
type YouTubeVideo struct {
|
||||
VideoID string `json:"video_id"`
|
||||
Title string `json:"title"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
ViewsText string `json:"views_text,omitempty"`
|
||||
Views int64 `json:"views,omitempty"`
|
||||
PublishedText string `json:"published_text,omitempty"`
|
||||
PublishedDate string `json:"published_date,omitempty"` // YYYY-MM-DD
|
||||
}
|
||||
|
||||
type YouTubeChannelPayload struct {
|
||||
Channel string `json:"channel"`
|
||||
ChannelURL string `json:"channel_url"`
|
||||
SubscribersText string `json:"subscribers_text,omitempty"`
|
||||
Subscribers int64 `json:"subscribers,omitempty"`
|
||||
Videos []YouTubeVideo `json:"videos"`
|
||||
}
|
||||
|
||||
// GetYouTubeVideos returns cached YouTube channel videos
|
||||
func (yc *YouTubeController) GetYouTubeVideos(c *gin.Context) {
|
||||
// Try to load from cache first
|
||||
cacheFile := filepath.Join("cache", "prefetch", "youtube_channel.json")
|
||||
if data, err := os.ReadFile(cacheFile); err == nil {
|
||||
var payload YouTubeChannelPayload
|
||||
if err := json.Unmarshal(data, &payload); err == nil && len(payload.Videos) > 0 {
|
||||
c.JSON(http.StatusOK, payload)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If no cache, try to fetch and cache
|
||||
var settings models.Settings
|
||||
if err := yc.DB.First(&settings).Error; err == nil {
|
||||
youtubeURL := settings.YoutubeURL
|
||||
if youtubeURL == "" {
|
||||
c.JSON(http.StatusNoContent, gin.H{})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch and cache
|
||||
payload, err := yc.fetchYouTubeChannel(youtubeURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to fetch YouTube channel: %v", err)
|
||||
c.JSON(http.StatusNoContent, gin.H{})
|
||||
return
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
if err := yc.saveYouTubeCache(payload); err != nil {
|
||||
logger.Warn("Failed to save YouTube cache: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, payload)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNoContent, gin.H{})
|
||||
}
|
||||
|
||||
// fetchYouTubeChannel fetches videos from YouTube channel URL
|
||||
func (yc *YouTubeController) fetchYouTubeChannel(channelURL string) (*YouTubeChannelPayload, error) {
|
||||
// Parse channel URL to extract channel handle or ID
|
||||
channelHandle := extractYouTubeHandle(channelURL)
|
||||
if channelHandle == "" {
|
||||
return nil, fmt.Errorf("invalid YouTube URL")
|
||||
}
|
||||
|
||||
// Fetch the channel page HTML
|
||||
resp, err := http.Get(channelURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
html := string(body)
|
||||
|
||||
// Parse channel data from HTML
|
||||
payload := &YouTubeChannelPayload{
|
||||
Channel: channelHandle,
|
||||
ChannelURL: channelURL,
|
||||
Videos: []YouTubeVideo{},
|
||||
}
|
||||
|
||||
// Extract channel title
|
||||
if match := regexp.MustCompile(`<meta name="title" content="([^"]+)"`).FindStringSubmatch(html); len(match) > 1 {
|
||||
payload.Channel = match[1]
|
||||
}
|
||||
|
||||
// Extract videos from initial data
|
||||
videos := parseYouTubeVideosFromHTML(html)
|
||||
payload.Videos = videos
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// parseYouTubeVideosFromHTML extracts video data from YouTube HTML
|
||||
func parseYouTubeVideosFromHTML(html string) []YouTubeVideo {
|
||||
videos := []YouTubeVideo{}
|
||||
|
||||
// Find ytInitialData JSON
|
||||
re := regexp.MustCompile(`var ytInitialData = ({.+?});`)
|
||||
matches := re.FindStringSubmatch(html)
|
||||
if len(matches) < 2 {
|
||||
return videos
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(matches[1]), &data); err != nil {
|
||||
return videos
|
||||
}
|
||||
|
||||
// Navigate to video items (this is a simplified version - YouTube's structure is complex)
|
||||
// Try to find gridRenderer or richItemRenderer
|
||||
tabs, ok := data["contents"].(map[string]interface{})
|
||||
if !ok {
|
||||
return videos
|
||||
}
|
||||
|
||||
// Try multiple paths to find videos
|
||||
extractVideosRecursive(tabs, &videos, 0, 20) // limit recursion
|
||||
|
||||
return videos
|
||||
}
|
||||
|
||||
// extractVideosRecursive recursively searches for video data
|
||||
func extractVideosRecursive(obj interface{}, videos *[]YouTubeVideo, depth, maxDepth int) {
|
||||
if depth > maxDepth || len(*videos) >= 20 {
|
||||
return
|
||||
}
|
||||
|
||||
switch v := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
// Check if this is a video renderer
|
||||
if videoId, ok := v["videoId"].(string); ok {
|
||||
video := YouTubeVideo{
|
||||
VideoID: videoId,
|
||||
ThumbnailURL: fmt.Sprintf("https://i.ytimg.com/vi/%s/hqdefault.jpg", videoId),
|
||||
}
|
||||
|
||||
// Extract title
|
||||
if title, ok := v["title"].(map[string]interface{}); ok {
|
||||
if runs, ok := title["runs"].([]interface{}); ok && len(runs) > 0 {
|
||||
if run, ok := runs[0].(map[string]interface{}); ok {
|
||||
if text, ok := run["text"].(string); ok {
|
||||
video.Title = text
|
||||
}
|
||||
}
|
||||
} else if simpleText, ok := title["simpleText"].(string); ok {
|
||||
video.Title = simpleText
|
||||
}
|
||||
}
|
||||
|
||||
// Extract views
|
||||
if viewCountText, ok := v["viewCountText"].(map[string]interface{}); ok {
|
||||
if simpleText, ok := viewCountText["simpleText"].(string); ok {
|
||||
video.ViewsText = simpleText
|
||||
video.Views = parseViews(simpleText)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract published date
|
||||
if publishedTimeText, ok := v["publishedTimeText"].(map[string]interface{}); ok {
|
||||
if simpleText, ok := publishedTimeText["simpleText"].(string); ok {
|
||||
video.PublishedText = simpleText
|
||||
video.PublishedDate = estimateDate(simpleText)
|
||||
}
|
||||
}
|
||||
|
||||
if video.Title != "" {
|
||||
*videos = append(*videos, video)
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into nested objects
|
||||
for _, val := range v {
|
||||
extractVideosRecursive(val, videos, depth+1, maxDepth)
|
||||
}
|
||||
|
||||
case []interface{}:
|
||||
for _, item := range v {
|
||||
extractVideosRecursive(item, videos, depth+1, maxDepth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractYouTubeHandle extracts channel handle from URL
|
||||
func extractYouTubeHandle(url string) string {
|
||||
url = strings.TrimSpace(url)
|
||||
if strings.Contains(url, "/@") {
|
||||
parts := strings.Split(url, "/@")
|
||||
if len(parts) > 1 {
|
||||
return strings.Split(parts[1], "/")[0]
|
||||
}
|
||||
}
|
||||
if strings.Contains(url, "/channel/") {
|
||||
parts := strings.Split(url, "/channel/")
|
||||
if len(parts) > 1 {
|
||||
return strings.Split(parts[1], "/")[0]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseViews converts view count text to number
|
||||
func parseViews(text string) int64 {
|
||||
text = strings.ToLower(text)
|
||||
text = strings.ReplaceAll(text, " ", "")
|
||||
text = strings.ReplaceAll(text, ",", "")
|
||||
|
||||
var multiplier int64 = 1
|
||||
if strings.Contains(text, "k") {
|
||||
multiplier = 1000
|
||||
text = strings.ReplaceAll(text, "k", "")
|
||||
} else if strings.Contains(text, "m") {
|
||||
multiplier = 1000000
|
||||
text = strings.ReplaceAll(text, "m", "")
|
||||
} else if strings.Contains(text, "mil") {
|
||||
multiplier = 1000000
|
||||
text = strings.ReplaceAll(text, "mil", "")
|
||||
}
|
||||
|
||||
// Extract first number
|
||||
re := regexp.MustCompile(`[\d.]+`)
|
||||
match := re.FindString(text)
|
||||
if match == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
val, _ := strconv.ParseFloat(match, 64)
|
||||
return int64(val * float64(multiplier))
|
||||
}
|
||||
|
||||
// estimateDate converts relative time text to YYYY-MM-DD
|
||||
func estimateDate(text string) string {
|
||||
now := time.Now()
|
||||
text = strings.ToLower(text)
|
||||
|
||||
if strings.Contains(text, "hour") || strings.Contains(text, "hodina") {
|
||||
return now.Format("2006-01-02")
|
||||
}
|
||||
if strings.Contains(text, "day") || strings.Contains(text, "den") || strings.Contains(text, "dní") {
|
||||
re := regexp.MustCompile(`(\d+)`)
|
||||
match := re.FindString(text)
|
||||
if match != "" {
|
||||
days, _ := strconv.Atoi(match)
|
||||
return now.AddDate(0, 0, -days).Format("2006-01-02")
|
||||
}
|
||||
return now.AddDate(0, 0, -1).Format("2006-01-02")
|
||||
}
|
||||
if strings.Contains(text, "week") || strings.Contains(text, "týden") {
|
||||
re := regexp.MustCompile(`(\d+)`)
|
||||
match := re.FindString(text)
|
||||
if match != "" {
|
||||
weeks, _ := strconv.Atoi(match)
|
||||
return now.AddDate(0, 0, -weeks*7).Format("2006-01-02")
|
||||
}
|
||||
return now.AddDate(0, 0, -7).Format("2006-01-02")
|
||||
}
|
||||
if strings.Contains(text, "month") || strings.Contains(text, "měsíc") {
|
||||
re := regexp.MustCompile(`(\d+)`)
|
||||
match := re.FindString(text)
|
||||
if match != "" {
|
||||
months, _ := strconv.Atoi(match)
|
||||
return now.AddDate(0, -months, 0).Format("2006-01-02")
|
||||
}
|
||||
return now.AddDate(0, -1, 0).Format("2006-01-02")
|
||||
}
|
||||
if strings.Contains(text, "year") || strings.Contains(text, "rok") {
|
||||
re := regexp.MustCompile(`(\d+)`)
|
||||
match := re.FindString(text)
|
||||
if match != "" {
|
||||
years, _ := strconv.Atoi(match)
|
||||
return now.AddDate(-years, 0, 0).Format("2006-01-02")
|
||||
}
|
||||
return now.AddDate(-1, 0, 0).Format("2006-01-02")
|
||||
}
|
||||
|
||||
return now.Format("2006-01-02")
|
||||
}
|
||||
|
||||
// saveYouTubeCache saves YouTube data to cache file
|
||||
func (yc *YouTubeController) saveYouTubeCache(payload *YouTubeChannelPayload) error {
|
||||
cacheDir := filepath.Join("cache", "prefetch")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheFile := filepath.Join(cacheDir, "youtube_channel.json")
|
||||
data, err := json.MarshalIndent(payload, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(cacheFile, data, 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user