Migrate HTTP handlers to Echo

This commit is contained in:
Rob Watson 2021-09-25 19:00:19 +02:00
parent 658ffa4344
commit 53bd2681af
9 changed files with 193 additions and 190 deletions

View File

@ -1,191 +1,22 @@
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
"git.netflux.io/rob/clipper/media"
"git.netflux.io/rob/clipper/youtube"
youtubev2 "github.com/kkdai/youtube/v2"
"git.netflux.io/rob/clipper/server"
)
const (
ContentTypeApplicationJSON = "application/json"
DefaultHTTPBindAddr = "0.0.0.0:8888"
DefaultTimeout = 30 * time.Second
DefaultHTTPBindAddr = "0.0.0.0:8888"
DefaultTimeout = 30 * time.Second
)
func handleRequest(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("method not allowed"))
return
}
w.Header().Add("Content-Type", ContentTypeApplicationJSON)
w.Header().Add("Access-Control-Allow-Origin", "*")
if strings.HasPrefix(r.URL.Path, "/api/media_sets") {
videoID := r.URL.Query().Get("video_id")
if videoID == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "no video ID provided"}`))
return
}
mediaSet := new(media.MediaSet)
mediaSet.ID = videoID
if mediaSet.Exists() {
// just load the metadata.json file:
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not fetch media"}`))
return
}
} else {
// download everything from YouTube:
var err error
var youtubeClient youtubev2.Client
downloader := youtube.NewDownloader(&youtubeClient)
log.Printf("background context = %p, req context = %p", context.Background(), r.Context())
mediaSet, err = downloader.Download(r.Context(), videoID)
if err != nil {
log.Printf("error downloading MediaSet: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not fetch media"}`))
return
}
}
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(mediaSet); err != nil {
log.Printf("error encoding MediaSet: %v", err)
}
return
}
if strings.HasPrefix(r.URL.Path, "/api/audio") {
log.Printf("got headers for audio request: %+v", r.Header)
videoID := r.URL.Query().Get("video_id")
mediaSet := media.MediaSet{ID: videoID}
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not fetch media"}`))
return
}
// TODO: ensure content-type matches the actual downloaded format.
w.Header().Set("Content-Type", "audio/webm")
http.ServeFile(w, r, mediaSet.EncodedAudioPath())
return
}
if strings.HasPrefix(r.URL.Path, "/api/video") {
videoID := r.URL.Query().Get("video_id")
mediaSet := media.MediaSet{ID: videoID}
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not fetch media"}`))
return
}
http.ServeFile(w, r, mediaSet.VideoPath())
return
}
if strings.HasPrefix(r.URL.Path, "/api/thumbnails") {
videoID := r.URL.Query().Get("video_id")
mediaSet := media.MediaSet{ID: videoID}
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not fetch media"}`))
return
}
w.Header().Set("Content-Type", "image/jpeg")
http.ServeFile(w, r, mediaSet.ThumbnailPath())
return
}
if strings.HasPrefix(r.URL.Path, "/api/peaks") {
videoID := r.URL.Query().Get("video_id")
if videoID == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "no video ID provided"}`))
return
}
start, err := strconv.ParseInt(r.URL.Query().Get("start"), 0, 64)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "invalid start parameter provided"}`))
return
}
end, err := strconv.ParseInt(r.URL.Query().Get("end"), 0, 64)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "invalid end parameter provided"}`))
return
}
numBins, err := strconv.Atoi(r.URL.Query().Get("bins"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "invalid bins parameter provided"}`))
return
}
mediaSet := media.MediaSet{ID: videoID}
if err = mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not fetch media"}`))
return
}
peaks, err := mediaSet.Peaks(start, end, numBins)
if err != nil {
log.Printf("error generating peaks: %v", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error": "could not generate peaks"}`))
}
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(peaks)
if err != nil {
log.Printf("error encoding peaks: %v", err)
}
return
}
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("page not found"))
}
func main() {
srv := http.Server{
ReadTimeout: DefaultTimeout,
WriteTimeout: DefaultTimeout,
Addr: DefaultHTTPBindAddr,
Handler: http.HandlerFunc(handleRequest),
serverOptions := server.Options{
BindAddr: DefaultHTTPBindAddr,
Timeout: DefaultTimeout,
}
log.Fatal(srv.ListenAndServe())
log.Fatal(server.Start(serverOptions))
}

View File

@ -4,13 +4,24 @@ go 1.17
require (
github.com/kkdai/youtube/v2 v2.7.4
github.com/labstack/echo/v4 v4.6.0
github.com/stretchr/testify v1.7.0
)
require (
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/labstack/gommon v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

View File

@ -101,6 +101,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -212,10 +214,22 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.6.0 h1:vsYEeeYy077cB5yMpgI+ubA7iVRZEtrzHhcvRhd27gA=
github.com/labstack/echo/v4 v4.6.0/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -290,6 +304,11 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vbauerster/mpb/v5 v5.4.0/go.mod h1:fi4wVo7BVQ22QcvFObm+VwliQXlV1eBT8JDaKXR4JGI=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -322,6 +341,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -395,8 +416,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8=
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -427,6 +449,7 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -434,11 +457,13 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -466,7 +491,11 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -476,9 +505,13 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -30,15 +30,18 @@ type Video struct {
// MediaSet represents the media and metadata associated with a single media
// resource (for example, a YouTube video).
type MediaSet struct {
Audio Audio `json:"audio"`
Video Video `json:"video"`
ID string `json:"id"`
Source string `json:"source"`
Audio Audio `json:"audio"`
Video Video `json:"video"`
ID string `json:"id"`
exists bool
}
// New builds a new MediaSet with the given ID.
func NewMediaSet(id string) *MediaSet {
return &MediaSet{ID: id}
}
// TODO: pass io.Readers/Writers instead of strings.
func (m *MediaSet) RawAudioPath() string { return fmt.Sprintf("cache/%s.raw", m.ID) }
func (m *MediaSet) EncodedAudioPath() string { return fmt.Sprintf("cache/%s.m4a", m.ID) }

View File

@ -0,0 +1,95 @@
package server
import (
"encoding/json"
"log"
"net/http"
"strconv"
"git.netflux.io/rob/clipper/media"
"git.netflux.io/rob/clipper/youtube"
youtubev2 "github.com/kkdai/youtube/v2"
"github.com/labstack/echo/v4"
)
// getMediaSet is a handler that responds with a MediaSet.
func getMediaSet(c echo.Context) error {
videoID := c.Param("id")
mediaSet := media.NewMediaSet(videoID)
if mediaSet.Exists() {
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch media set")
}
return c.JSON(http.StatusOK, mediaSet)
}
var youtubeClient youtubev2.Client
downloader := youtube.NewDownloader(&youtubeClient)
mediaSet, err := downloader.Download(c.Request().Context(), videoID)
if err != nil {
log.Printf("error downloading MediaSet: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch media set")
}
return c.JSON(http.StatusOK, mediaSet)
}
// getThumbnails is a handler that responds with a MediaSet thumbnail grid.
func getThumbnails(c echo.Context) error {
videoID := c.Param("id")
mediaSet := media.NewMediaSet(videoID)
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "could not load media set")
}
return c.File(mediaSet.ThumbnailPath())
}
// getVideo is a handler that responds with the video file for a MediaSet
func getVideo(c echo.Context) error {
videoID := c.Param("id")
mediaSet := media.NewMediaSet(videoID)
if err := mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "could not load media set")
}
return c.File(mediaSet.VideoPath())
}
// getPeaks is a handler that returns a two-dimensional array of peaks, with
// the number of bins matching the provided parameter.
func getPeaks(c echo.Context) error {
videoID := c.Param("id")
start, err := strconv.ParseInt(c.QueryParam("start"), 0, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid start parameter provided")
}
end, err := strconv.ParseInt(c.QueryParam("end"), 0, 64)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid end parameter provided")
}
numBins, err := strconv.Atoi(c.QueryParam("bins"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid bins parameter provided")
}
mediaSet := media.NewMediaSet(videoID)
if err = mediaSet.Load(); err != nil {
log.Printf("error loading MediaSet: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "could not load media set")
}
peaks, err := mediaSet.Peaks(start, end, numBins)
if err != nil {
log.Printf("error generating peaks: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, "could not generate peaks")
}
return json.NewEncoder(c.Response()).Encode(peaks)
}

31
backend/server/server.go Normal file
View File

@ -0,0 +1,31 @@
package server
import (
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Options struct {
BindAddr string
Timeout time.Duration
}
func Start(opts Options) error {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{Timeout: opts.Timeout}))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
}))
e.GET("/api/media_sets/:id", getMediaSet)
e.GET("/api/media_sets/:id/thumbnails", getThumbnails)
e.GET("/api/media_sets/:id/video", getVideo)
e.GET("/api/media_sets/:id/peaks", getPeaks)
return e.Start(opts.BindAddr)
}

View File

@ -71,7 +71,7 @@ func (d *Downloader) Download(ctx context.Context, videoID string) (*media.Media
return nil, fmt.Errorf("error fetching video: %v", err)
}
mediaSet := media.MediaSet{ID: videoID, Source: "youtube"}
mediaSet := media.NewMediaSet(videoID)
audioResultChan := make(chan audioResult, 1)
videoResultChan := make(chan videoResult, 1)
@ -123,7 +123,7 @@ func (d *Downloader) Download(ctx context.Context, videoID string) (*media.Media
log.Println("finished downloading mediaset")
return &mediaSet, nil
return mediaSet, nil
}
func (d *Downloader) downloadAudio(ctx context.Context, video *youtubev2.Video, outPath, rawOutPath string) (*media.Audio, error) {

View File

@ -87,7 +87,7 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
console.log('fetching media...');
const resp = await fetch(
`http://localhost:8888/api/media_sets?video_id=${videoID}`
`http://localhost:8888/api/media_sets/${videoID}`
);
const respBody = await resp.json();
@ -129,7 +129,7 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
return;
}
const url = `http://localhost:8888/api/video?video_id=${videoID}`;
const url = `http://localhost:8888/api/media_sets/${videoID}/video`;
video.src = url;
video.muted = false;
video.volume = 1;
@ -149,7 +149,7 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
}
const resp = await fetch(
`http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${CanvasLogicalWidth}`
`http://localhost:8888/api/media_sets/${videoID}/peaks?start=${zoomSettings.startFrame}&end=${endFrame}&bins=${CanvasLogicalWidth}`
);
const peaks = await resp.json();
setWaveformPeaks(peaks);

View File

@ -21,7 +21,7 @@ export const Thumbnails: React.FC<Props> = ({ mediaSet, style }: Props) => {
useEffect(() => {
if (mediaSet == null) return;
image.src = `http://localhost:8888/api/thumbnails?video_id=${mediaSet.id}`;
image.src = `http://localhost:8888/api/media_sets/${mediaSet.id}/thumbnails`;
image.onload = () => {
setState(State.Ready);
};
@ -47,7 +47,6 @@ export const Thumbnails: React.FC<Props> = ({ mediaSet, style }: Props) => {
const tw = mediaSet.video.thumbnailWidth;
const th = mediaSet.video.thumbnailHeight;
const iw = image.width;
const ih = image.height;
const { width: pw, height: ph } = canvas.getBoundingClientRect();
// set canvas logical width to suit the aspect ratio: