package main import ( "encoding/json" "fmt" "net/http" "strconv" "time" ) /* function to make an api request */ const youtubeAPI = "https://www.googleapis.com/youtube/v3/" func apiRequest[T any](videoId string, endpoint string, videoIdParam string, part string) (T, error) { var zero T if apiKey == "" { return zero, fmt.Errorf("API_KEY environment variable not set") } url := fmt.Sprintf("%s?part=%s&%s=%s&key=%s", youtubeAPI + endpoint, part, videoIdParam, videoId, apiKey) resp, err := http.Get(url) if err != nil { return zero, fmt.Errorf("Failed to fetch video info: " + err.Error()) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return zero, fmt.Errorf("YouTube API error: " + resp.Status) } var data T err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { return zero, fmt.Errorf("Failed to decode YouTube API response: " + err.Error()) } return data, nil } /* youtube api types */ type ytVideoDetails struct { Items []struct { Snippet struct { ChannelTitle string `json:"channelTitle"` ChannelId string `json:"channelId"` Description string `json:"description"` PublishedAt time.Time `json:"publishedAt"` Tags []string `json:"tags"` } `json:"snippet"` Statistics struct { CommentCount string `json:"commentCount"` LikeCount string `json:"likeCount"` ViewCount string `json:"viewCount"` } `json:"statistics"` } `json:"items"` } type ytComment struct { Snippet struct { AuthorDisplayName string `json:"authorDisplayName"` TextDisplay string `json:"textDisplay"` PublishedAt time.Time `json:"publishedAt"` UpdatedAt time.Time `json:"updatedAt"` LikeCount int `json:"likeCount"` } `json:"snippet"` } type ytComments struct { Items []struct { Snippet struct { TopLevelComment ytComment `json:"topLevelComment"` } `json:"snippet"` Replies struct { Comments []ytComment `json:"comments,omitempty"` } `json:"replies"` } `json:"items"` } /* api source interface definition */ type APISourceOfficial struct {} func (a *APISourceOfficial) getDetails(videoId string) (VideoDetails, error) { var zero VideoDetails d, err := apiRequest[ytVideoDetails](videoId, "videos", "id", "snippet,statistics,topicDetails") if err != nil { return zero, err } if len(d.Items) == 0 { return zero, fmt.Errorf("YouTube API returned no video details") } n := d.Items[0].Snippet t := d.Items[0].Statistics return VideoDetails{ Author: n.ChannelTitle, Channel: n.ChannelId, Published: n.PublishedAt, Description: n.Description, Comments: AtoiOrZero(t.CommentCount), Likes: AtoiOrZero(t.LikeCount), Views: AtoiOrZero(t.ViewCount), Tags: n.Tags, }, err } func genComment(c ytComment, r []ytComment) Comment { n := c.Snippet var replies []*Comment for _, rc := range r { temp := genComment(rc, []ytComment{}) // NOTE temp variable required here replies = append(replies, &temp) // cannot reference return value of genComment w/o temp var } return Comment{ Author: n.AuthorDisplayName, Body: n.TextDisplay, Published: n.PublishedAt, Updated: n.UpdatedAt, Likes: n.LikeCount, Replies: replies, } } func (a *APISourceOfficial) getComments(videoId string) ([]Comment, error) { const maxResults = 100 d, err := apiRequest[ytComments](videoId, "commentThreads", "videoId", "snippet,replies&maxResults=" + strconv.Itoa(maxResults)) // TODO configure max results var comments []Comment for _, c := range d.Items { tlc := c.Snippet.TopLevelComment r := c.Replies.Comments comments = append(comments, genComment(tlc, r)) } return comments, err }