diff options
| author | Tim Keller <tjk@tjkeller.xyz> | 2026-05-16 22:04:14 -0500 |
|---|---|---|
| committer | Tim Keller <tjk@tjkeller.xyz> | 2026-05-16 22:04:14 -0500 |
| commit | 1316aa7ca5e1668bbb7967264540bff3c8dbef86 (patch) | |
| tree | cc6a4121c13d31a96060e3fa9103ed314578c8cc | |
| parent | 3bfeed5a50cd1bb405cbe145611410fa4f628413 (diff) | |
| download | embedtube-1316aa7ca5e1668bbb7967264540bff3c8dbef86.tar.xz embedtube-1316aa7ca5e1668bbb7967264540bff3c8dbef86.zip | |
new api implemented server-side
| -rw-r--r-- | api.go | 88 | ||||
| -rw-r--r-- | main.go | 11 | ||||
| -rw-r--r-- | official-api.go | 125 | ||||
| -rw-r--r-- | watch.go | 4 |
4 files changed, 177 insertions, 51 deletions
@@ -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)) } @@ -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 +} @@ -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) }) |
