package scraper import ( "context" "fmt" "time" // "log" "html" "strings" "ytgo/config" "google.golang.org/api/option" "google.golang.org/api/youtube/v3" ) type Video struct { Title string URL string Channel string Duration string Views string Thumbnail string UploadDate string } func formatViews(count uint64) string { switch { case count >= 1000000000: return fmt.Sprintf("%.1fB views", float64(count)/1000000000) case count >= 1000000: return fmt.Sprintf("%.1fM views", float64(count)/1000000) case count >= 1000: return fmt.Sprintf("%.1fK views", float64(count)/1000) default: return fmt.Sprintf("%d views", count) } } func formatDuration(duration string) string { // Remove PT from the start duration = strings.TrimPrefix(duration, "PT") var result strings.Builder // Handle hours if i := strings.Index(duration, "H"); i != -1 { result.WriteString(duration[:i]) result.WriteString(":") duration = duration[i+1:] } // Handle minutes if i := strings.Index(duration, "M"); i != -1 { minutes := duration[:i] if len(minutes) == 1 { result.WriteString("0") } result.WriteString(minutes) result.WriteString(":") duration = duration[i+1:] } else if result.Len() > 0 { result.WriteString("00:") } // Handle seconds if i := strings.Index(duration, "S"); i != -1 { seconds := duration[:i] if len(seconds) == 1 { result.WriteString("0") } result.WriteString(seconds) } else { result.WriteString("00") } return result.String() } func formatUploadDate(uploadDate string) string { t, err := time.Parse(time.RFC3339, uploadDate) if err != nil { return uploadDate } now := time.Now() diff := now.Sub(t) days := int(diff.Hours() / 24) formattedDate := t.Format("02-01-2006") // If video is less than 30 days old, add "X days ago" if days < 30 { var timeAgo string switch { case days == 0: hours := int(diff.Hours()) if hours == 0 { timeAgo = "just now" } else { timeAgo = fmt.Sprintf("%dh ago", hours) } case days == 1: timeAgo = "1 day ago" default: timeAgo = fmt.Sprintf("%d days ago", days) } return fmt.Sprintf("%s (%s)", formattedDate, timeAgo) } return formattedDate } func FetchVideos(query string) ([]Video, error) { cfg, err := config.LoadConfig() if err != nil { return nil, fmt.Errorf("error loading config: %w", err) } ctx := context.Background() youtubeService, err := youtube.NewService(ctx, option.WithAPIKey(cfg.APIKey)) if err != nil { return nil, fmt.Errorf("error creating YouTube client: %w", err) } // Make the search request call := youtubeService.Search.List([]string{"snippet"}). Q(query). MaxResults(50). Type("video"). VideoDuration("any") response, err := call.Do() if err != nil { return nil, fmt.Errorf("error making search request: %w", err) } var videos []Video for _, item := range response.Items { video := Video{ Title: item.Snippet.Title, URL: fmt.Sprintf("https://www.youtube.com/watch?v=%s", item.Id.VideoId), Channel: item.Snippet.ChannelTitle, Thumbnail: item.Snippet.Thumbnails.Default.Url, UploadDate: item.Snippet.PublishedAt, } videos = append(videos, video) } // Get additional video details (duration, views) in a single request videoIds := make([]string, len(response.Items)) for i, item := range response.Items { videoIds[i] = item.Id.VideoId } // Get video statistics statsCall := youtubeService.Videos.List([]string{"contentDetails", "statistics"}). Id(videoIds...) statsResponse, err := statsCall.Do() if err != nil { return nil, fmt.Errorf("error fetching video details: %w", err) } // Update videos with additional information for i, stat := range statsResponse.Items { if i < len(videos) { videos[i].Duration = formatDuration(stat.ContentDetails.Duration) videos[i].Views = formatViews(stat.Statistics.ViewCount) videos[i].Title = html.UnescapeString(videos[i].Title) videos[i].UploadDate = formatUploadDate(videos[i].UploadDate) } } return videos, nil }