aboutsummaryrefslogtreecommitdiff
path: root/official-api.go
blob: 39db1b83036b3208aa9f31d5e0444197bac8e7f6 (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
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"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 {
			Channel        string     `json:"channel"`
			Description    string     `json:"description"`
			PublishedAt    time.Time  `json:"publishedAt"`
			Tags           []string   `json:"tags"`
		} `json:"snippet"`
		Statistics struct {
			CommentCount int  `json:"commentCount"`
			LikeCount    int  `json:"likeCount"`
			ViewCount    int  `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) {
	d, err := apiRequest[ytVideoDetails](videoId, "videos", "id", "snippet,statistics,topicDetails")

	n := d.Items[0].Snippet
	t := d.Items[0].Statistics
	return VideoDetails{
		Channel:        n.Channel,
		DatePublished:  n.PublishedAt,
		Description:    n.Description,
		NumComments:    t.CommentCount,
		NumLikes:       t.LikeCount,
		NumViews:       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,
		DatePublished:  n.PublishedAt,
		DateUpdated:    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=" + fmt.Sprint(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
}