aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api.go88
-rw-r--r--main.go11
-rw-r--r--official-api.go125
-rw-r--r--watch.go4
4 files changed, 177 insertions, 51 deletions
diff --git a/api.go b/api.go
index fa91ea6..21a2b5f 100644
--- a/api.go
+++ b/api.go
@@ -1,58 +1,58 @@
package main
import (
- "fmt"
- "io"
"log"
+ "encoding/json"
"net/http"
+ "time"
)
-const youtubeAPI = "https://www.googleapis.com/youtube/v3/"
-
-/* function to make an api request */
-func apiRequest(w http.ResponseWriter, r *http.Request, endpoint string, videoIdParam string, part string) {
- videoID := r.URL.Query().Get("id")
- if videoID == "" {
- msg := "Missing ?id=VIDEO_ID parameter"
- http.Error(w, msg, http.StatusBadRequest)
- return
- }
+type VideoDetails struct {
+ Channel string `json:"channel"`
+ DatePublished time.Time `json:"datePublished"`
+ Description string `json:"description"`
+ NumComments int `json:"comments"`
+ NumLikes int `json:"likes"`
+ NumViews int `json:"views"`
+ Tags []string `json:"tags"`
+}
- if apiKey == "" {
- msg := "API_KEY environment variable not set"
- http.Error(w, msg, http.StatusInternalServerError)
- log.Println(msg)
- return
- }
+type Comment struct {
+ Author string `json:"a"`
+ Body string `json:"b"`
+ DatePublished time.Time `json:"p"`
+ DateUpdated time.Time `json:"u"`
+ Likes int `json:"l"`
+ Replies []*Comment `json:"r,omitempty"`
+}
- url := fmt.Sprintf("%s?part=%s&%s=%s&key=%s", youtubeAPI + endpoint, part, videoIdParam, videoID, apiKey)
- resp, err := http.Get(url)
- if err != nil {
- msg := "Failed to fetch video info: " + err.Error()
- http.Error(w, msg, http.StatusInternalServerError)
- log.Println(msg)
- return
- }
- defer resp.Body.Close()
+type APISource interface {
+ getDetails(id string) (VideoDetails, error)
+ getComments(id string) ([]Comment, error)
+}
- if resp.StatusCode != http.StatusOK {
- msg := "YouTube API error: " + resp.Status
- http.Error(w, msg, http.StatusBadGateway)
- log.Println(msg)
- return
+func apiDataHandler[T any](f func(id string) (T, error)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ videoID := r.URL.Query().Get("id")
+ if videoID == "" {
+ msg := "Missing ?id=VIDEO_ID parameter"
+ http.Error(w, msg, http.StatusBadRequest)
+ return
+ }
+
+ data, err := f(videoID)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ log.Println(err.Error())
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(data)
}
-
- w.Header().Set("Content-Type", "application/json")
- io.Copy(w, resp.Body)
}
-/* routes */
-func setupRoutesAPI() {
- http.HandleFunc("/api/details", func(w http.ResponseWriter, r *http.Request) {
- apiRequest(w, r, "videos", "id", "snippet,statistics,topicDetails")
- })
-
- http.HandleFunc("/api/comments", func(w http.ResponseWriter, r *http.Request) {
- apiRequest(w, r, "commentThreads", "videoId", "snippet,replies&maxResults=100")
- })
+func registerRoutesAPI(mux *http.ServeMux, api APISource) {
+ mux.HandleFunc("/api/details", apiDataHandler(api.getDetails))
+ mux.HandleFunc("/api/comments", apiDataHandler(api.getComments))
}
diff --git a/main.go b/main.go
index e85649d..2f39712 100644
--- a/main.go
+++ b/main.go
@@ -30,15 +30,16 @@ func main() {
apiKey = os.Getenv("API_KEY")
// setup routes
- setupRoutesAPI()
- setupRoutesWatch()
+ mux := http.NewServeMux()
+ registerRoutesAPI(mux, &APISourceOfficial{})
+ registerRoutesWatch(mux)
setupHome()
- http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
- http.HandleFunc("/", rootHandler)
+ mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
+ mux.HandleFunc("/", rootHandler)
// start http server
log.Println("Listening on http://localhost:8080")
- err := http.ListenAndServe(":8080", nil)
+ err := http.ListenAndServe(":8080", mux)
if err != nil {
log.Fatal(err)
}
diff --git a/official-api.go b/official-api.go
new file mode 100644
index 0000000..39db1b8
--- /dev/null
+++ b/official-api.go
@@ -0,0 +1,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
+}
diff --git a/watch.go b/watch.go
index 8352e62..81f0d86 100644
--- a/watch.go
+++ b/watch.go
@@ -26,8 +26,8 @@ func renderWatchTemplate(w http.ResponseWriter, id string) {
}
/* routes */
-func setupRoutesWatch() {
- http.HandleFunc("/watch", func(w http.ResponseWriter, r *http.Request) {
+func registerRoutesWatch(mux *http.ServeMux) {
+ mux.HandleFunc("/watch", func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("v")
renderWatchTemplate(w, id)
})