diff --git a/backend/cmd/clipper/main.go b/backend/cmd/clipper/main.go index 8b18f6e..0104a24 100644 --- a/backend/cmd/clipper/main.go +++ b/backend/cmd/clipper/main.go @@ -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)) } diff --git a/backend/go.mod b/backend/go.mod index d898c2c..20f2825 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 ) diff --git a/backend/go.sum b/backend/go.sum index 04dcd1e..f94e5af 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/media/media_set.go b/backend/media/media_set.go index 433e050..c343147 100644 --- a/backend/media/media_set.go +++ b/backend/media/media_set.go @@ -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) } diff --git a/backend/server/handlers.go b/backend/server/handlers.go new file mode 100644 index 0000000..5018723 --- /dev/null +++ b/backend/server/handlers.go @@ -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) +} diff --git a/backend/server/server.go b/backend/server/server.go new file mode 100644 index 0000000..0aa3084 --- /dev/null +++ b/backend/server/server.go @@ -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) +} diff --git a/backend/youtube/youtube.go b/backend/youtube/youtube.go index 6f45307..ad3303c 100644 --- a/backend/youtube/youtube.go +++ b/backend/youtube/youtube.go @@ -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) { diff --git a/frontend/src/Waveform.tsx b/frontend/src/Waveform.tsx index b48dd6a..bec2805 100644 --- a/frontend/src/Waveform.tsx +++ b/frontend/src/Waveform.tsx @@ -87,7 +87,7 @@ export const Waveform: React.FC = ({ 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 = ({ 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 = ({ 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); diff --git a/frontend/src/Waveform/Thumbnails.tsx b/frontend/src/Waveform/Thumbnails.tsx index f621d88..19ac80b 100644 --- a/frontend/src/Waveform/Thumbnails.tsx +++ b/frontend/src/Waveform/Thumbnails.tsx @@ -21,7 +21,7 @@ export const Thumbnails: React.FC = ({ 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 = ({ 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: