aboutsummaryrefslogtreecommitdiff
path: root/official-api.go
blob: f44a4b963abb81b3bf138bbbdc8bec696bcdf2d7 (plain)
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
}