1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
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
}
|