diff --git a/README.md b/README.md index fa7b826..9d8bc89 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build Status](https://travis-ci.org/Arimeka/mediaprobe.svg?branch=master)](https://travis-ci.org/Arimeka/mediaprobe) [![Coverage Status](https://coveralls.io/repos/github/Arimeka/mediaprobe/badge.svg?branch=master)](https://coveralls.io/github/Arimeka/mediaprobe?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/Arimeka/mediaprobe)](https://goreportcard.com/report/github.com/Arimeka/mediaprobe) -[![GoDoc](https://godoc.org/github.com/Arimeka/mediaprobe?status.svg)](https://godoc.org/github.com/Arimeka/mediaprobe) +[![GoDoc](https://godoc.org/github.com/Arimeka/mediaprobe?status.svg)](https://pkg.go.dev/github.com/Arimeka/mediaprobe) # mediaprobe diff --git a/benchmark_test.go b/benchmark_test.go index 598bcea..1524c54 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -13,20 +13,20 @@ const ( func BenchmarkParse(b *testing.B) { for i := 0; i < b.N; i++ { - mediaprobe.Parse(benchmarkValidVideo) + _, _ = mediaprobe.Parse(benchmarkValidVideo) } } func BenchmarkNew(b *testing.B) { for i := 0; i < b.N; i++ { - mediaprobe.New(benchmarkValidVideo) + _, _ = mediaprobe.New(benchmarkValidVideo) } } func BenchmarkInfo_CalculateMime(b *testing.B) { info, _ := mediaprobe.New(benchmarkValidVideo) for i := 0; i < b.N; i++ { - info.CalculateMime() + _ = info.CalculateMime() } } @@ -34,7 +34,7 @@ func BenchmarkInfo_FFProbe(b *testing.B) { info, _ := mediaprobe.New(benchmarkValidVideo) b.ResetTimer() for i := 0; i < b.N; i++ { - info.FFProbe() + _ = info.FFProbe() } } @@ -42,6 +42,6 @@ func BenchmarkInfo_ParseImage(b *testing.B) { info, _ := mediaprobe.New(benchmarkValidImage) b.ResetTimer() for i := 0; i < b.N; i++ { - info.FFProbe() + _ = info.FFProbe() } } diff --git a/ffprobe_test.go b/ffprobe_test.go index a48d9e8..4deda0a 100644 --- a/ffprobe_test.go +++ b/ffprobe_test.go @@ -15,10 +15,14 @@ var bitrateFiles = map[string]int64{ } func TestInfo_FFProbe(t *testing.T) { + for filename, expectedBitrate := range bitrateFiles { - info, _ := mediaprobe.New(filename) + info, err := mediaprobe.New(filename) + if err != nil { + t.Fatalf("Filename: %s. Unexpected error %v", filename, err) + } - err := info.FFProbe() + err = info.FFProbe() if err != nil { if filename != "./fixtures/corrupted.mp4" { t.Errorf("Filename: %s. Unexpected error %v", filename, err) @@ -34,4 +38,29 @@ func TestInfo_FFProbe(t *testing.T) { t.Errorf("Filename: %s. Not expected video bitrate. Expected %d; got %d", filename, expectedBitrate, bitrate) } } + + t.Run("remote_file", ffprobeRemoteFile) +} + +func ffprobeRemoteFile(t *testing.T) { + handler := &Handler{ + Filename: "./fixtures/video.mp4", + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + err = info.FFProbe() + if err != nil { + t.Errorf("Unexpected error %v", err) + } + + bitrate := info.BitRate + if bitrate != 551193 { + t.Errorf("Not expected video bitrate. Expected %d; got %d", 551193, bitrate) + } } diff --git a/http.go b/http.go new file mode 100644 index 0000000..fb796aa --- /dev/null +++ b/http.go @@ -0,0 +1,31 @@ +package mediaprobe + +import ( + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +var httpClient = &http.Client{ + Timeout: 5 * time.Second, +} + +func getRemoteFile(uri *url.URL) (io.ReadCloser, error) { + req := &http.Request{ + Method: http.MethodGet, + URL: uri, + } + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + + return resp.Body, nil +} diff --git a/http_test.go b/http_test.go new file mode 100644 index 0000000..a5c312c --- /dev/null +++ b/http_test.go @@ -0,0 +1,59 @@ +package mediaprobe_test + +import ( + "net/http" + "net/http/httptest" + "sync" + "sync/atomic" + "time" +) + +type Server struct { + srv *httptest.Server +} + +func (s *Server) Stop() { + s.srv.Close() +} + +func (s *Server) Endpoint() string { + return s.srv.URL +} + +func ServeHttp(handler *Handler) *Server { + httptest.NewServer(handler) + + srv := httptest.NewServer(handler) + time.Sleep(100 * time.Millisecond) + + return &Server{srv: srv} +} + +type Handler struct { + Status int + Filename string + + FailOnAttempt uint64 + Counter uint64 + mu sync.RWMutex +} + +func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if h.Status != 0 { + rw.WriteHeader(h.Status) + return + } + if h.FailOnAttempt > 0 { + h.mu.RLock() + counter := h.Counter + h.mu.RUnlock() + if counter >= h.FailOnAttempt { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + atomic.AddUint64(&h.Counter, 1) + } + + http.ServeFile(rw, req, h.Filename) +} diff --git a/image.go b/image.go index 1938e3e..7c7e5e2 100644 --- a/image.go +++ b/image.go @@ -4,7 +4,6 @@ import ( "image" "io" "os" - // Expanding image package _ "golang.org/x/image/bmp" _ "golang.org/x/image/tiff" @@ -18,17 +17,46 @@ import ( // ParseImage used for retrieve image data func (info *Info) ParseImage() error { - file, err := os.Open(info.filename) + if info.data == nil { + file, err := os.Open(info.filename) + if err != nil { + return err + } + defer file.Close() + if err := info.decodeImage(file); err != nil { + return err + } + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + info.decodeExif(file) + + return nil + } + + body, err := getRemoteFile(info.uri) + if err != nil { + return err + } + err = info.decodeImage(body) + body.Close() if err != nil { return err } - defer file.Close() - img, _, err := image.DecodeConfig(file) + body, err = getRemoteFile(info.uri) if err != nil { return err } - _, err = file.Seek(0, io.SeekStart) + info.decodeExif(body) + body.Close() + + return nil +} + +func (info *Info) decodeImage(reader io.Reader) error { + img, _, err := image.DecodeConfig(reader) if err != nil { return err } @@ -36,15 +64,18 @@ func (info *Info) ParseImage() error { info.Width = img.Width info.Height = img.Height - if x, err := exif.Decode(file); err == nil { + return nil +} + +func (info *Info) decodeExif(reader io.Reader) { + if x, err := exif.Decode(reader); err == nil { if orientationTag, err := x.Get(exif.Orientation); err == nil { switch orientationTag.String() { case "5", "6", "7", "8": - info.Width = img.Height - info.Height = img.Width + w, h := info.Width, info.Height + info.Width = h + info.Height = w } } } - - return nil } diff --git a/image_test.go b/image_test.go index da24b78..24d330b 100644 --- a/image_test.go +++ b/image_test.go @@ -1,6 +1,7 @@ package mediaprobe_test import ( + "net/http" "testing" "github.com/Arimeka/mediaprobe" @@ -14,7 +15,21 @@ const ( testImagePNGWithoutExifImage = "./fixtures/without-exif.png" ) -func TestInfo_ParseImageNotFound(t *testing.T) { +func TestInfo_ParseImage(t *testing.T) { + t.Run("not_found", parseImageNotFound) + t.Run("not_found_remote", parseImageNotFoundRemote) + t.Run("conn_error_remote", parseImageConnectionErrorRemote) + t.Run("server_error_remote", parseImageServerErrorsRemote) + t.Run("valid", parseImageValid) + t.Run("valid_remote", parseImageValidRemote) + t.Run("invalid", parseImageInvalid) + t.Run("invalid_remote", parseImageInvalidRemote) + t.Run("with_orientation", parseImageWithOrientation) + t.Run("jpeg_without_exif", parseImageJPEGWithoutExif) + t.Run("png_without_exif", parseImagePNGWithoutExif) +} + +func parseImageNotFound(t *testing.T) { info := &mediaprobe.Info{} err := info.ParseImage() if err == nil { @@ -22,14 +37,107 @@ func TestInfo_ParseImageNotFound(t *testing.T) { } } -func TestInfo_ParseImage(t *testing.T) { - info, _ := mediaprobe.New(testImageInvalidImage) - err := info.ParseImage() +func parseImageNotFoundRemote(t *testing.T) { + handler := &Handler{ + Filename: testImageValidImage, + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + handler.Status = http.StatusNotFound + err = info.ParseImage() if err == nil { - t.Errorf("Filename: %s. Expected to return error but return nil", testImageInvalidImage) + t.Errorf("Expected to return error found but return nil") + } +} + +func parseImageConnectionErrorRemote(t *testing.T) { + handler := &Handler{ + Filename: testImageValidImage, + } + srv := ServeHttp(handler) + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + srv.Stop() + err = info.ParseImage() + if err == nil { + t.Errorf("Expected to return error found but return nil") + } +} + +func parseImageServerErrorsRemote(t *testing.T) { + handler := &Handler{ + Filename: testImageValidImage, + FailOnAttempt: 2, + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + err = info.ParseImage() + if err == nil { + t.Errorf("Expected to return error found but return nil") + } + + handler = &Handler{ + Filename: testImageValidImage, + FailOnAttempt: 3, + } + srv = ServeHttp(handler) + defer srv.Stop() + + info, err = mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + err = info.ParseImage() + if err == nil { + t.Errorf("Expected to return error found but return nil") + } +} + +func parseImageValid(t *testing.T) { + info, err := mediaprobe.New(testImageValidImage) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + err = info.ParseImage() + if err != nil { + t.Errorf("Filename: %s. Unexpected error %v", testImageValidImage, err) + } + + width := info.Width + if width != 290 { + t.Errorf("Filename: %s. Not expected width. Expected %d; got %d", testImageValidImage, 290, width) + } +} + +func parseImageValidRemote(t *testing.T) { + handler := &Handler{ + Filename: testImageValidImage, + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) } - info, _ = mediaprobe.New(testImageValidImage) err = info.ParseImage() if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testImageValidImage, err) @@ -41,7 +149,35 @@ func TestInfo_ParseImage(t *testing.T) { } } -func TestInfo_ParseImageWithOrientation(t *testing.T) { +func parseImageInvalid(t *testing.T) { + info, err := mediaprobe.New(testImageInvalidImage) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + err = info.ParseImage() + if err == nil { + t.Errorf("Filename: %s. Expected to return error but return nil", testImageInvalidImage) + } +} + +func parseImageInvalidRemote(t *testing.T) { + handler := &Handler{ + Filename: testImageInvalidImage, + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + err = info.ParseImage() + if err == nil { + t.Errorf("Expected to return error but return nil") + } +} + +func parseImageWithOrientation(t *testing.T) { info, _ := mediaprobe.New(testImageWithExifOrientation) err := info.ParseImage() if err != nil { @@ -54,7 +190,7 @@ func TestInfo_ParseImageWithOrientation(t *testing.T) { } } -func TestInfo_ParseImageJPEGWithoutExif(t *testing.T) { +func parseImageJPEGWithoutExif(t *testing.T) { info, _ := mediaprobe.New(testImageJPEGWithoutExifImage) err := info.ParseImage() if err != nil { @@ -67,7 +203,7 @@ func TestInfo_ParseImageJPEGWithoutExif(t *testing.T) { } } -func TestInfo_ParseImagePNGWithoutExif(t *testing.T) { +func parseImagePNGWithoutExif(t *testing.T) { info, _ := mediaprobe.New(testImagePNGWithoutExifImage) err := info.ParseImage() if err != nil { diff --git a/info.go b/info.go index 9d00b4d..177719b 100644 --- a/info.go +++ b/info.go @@ -1,12 +1,26 @@ package mediaprobe import ( + "fmt" + "net/http" + "net/url" "os" "time" ) -// New initialized Info using magic bytes for calculating media type +// New initialized Info and calculate file size. +// +// Accepted filename as local path or absolute url. func New(filename string) (*Info, error) { + uri, err := url.Parse(filename) + if err != nil || !uri.IsAbs() { + return openFile(filename) + } + + return openURL(filename, uri) +} + +func openFile(filename string) (*Info, error) { file, err := os.Open(filename) if err != nil { return nil, err @@ -27,9 +41,48 @@ func New(filename string) (*Info, error) { return info, nil } +func openURL(filename string, uri *url.URL) (*Info, error) { + headReq := &http.Request{ + Method: http.MethodHead, + URL: uri, + } + head, err := httpClient.Do(headReq) + if err != nil { + return nil, err + } + if head.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code %d", head.StatusCode) + } + head.Body.Close() + + body, err := getRemoteFile(uri) + if err != nil { + return nil, err + } + defer body.Close() + + info := &Info{ + data: make([]byte, 1024), + filename: filename, + uri: uri, + + Name: filename, + Size: head.ContentLength, + } + + _, err = body.Read(info.data) + if err != nil { + return nil, err + } + + return info, nil +} + // Info contains parsed information type Info struct { filename string + uri *url.URL + data []byte Name string MediaType string diff --git a/info_test.go b/info_test.go index f3c1603..57b8a2b 100644 --- a/info_test.go +++ b/info_test.go @@ -1,14 +1,84 @@ package mediaprobe_test import ( + "net/http" "testing" "github.com/Arimeka/mediaprobe" ) -func TestNewNotFound(t *testing.T) { +func TestNew(t *testing.T) { + t.Run("not_found", newNotFound) + t.Run("not_found_remote", newNotFoundRemote) + t.Run("conn_error_remote", newConnErrorRemote) + t.Run("server_error_remote", newServerErrorRemote) + t.Run("local", newLocalFile) + t.Run("remote", newRemoteFile) +} + +func newNotFound(t *testing.T) { _, err := mediaprobe.New("") if err == nil { t.Errorf("Expected to return error found but return nil") } } + +func newNotFoundRemote(t *testing.T) { + handler := &Handler{ + Status: http.StatusNotFound, + } + srv := ServeHttp(handler) + defer srv.Stop() + + _, err := mediaprobe.New(srv.Endpoint()) + if err == nil { + t.Errorf("Expected to return error found but return nil") + } +} + +func newConnErrorRemote(t *testing.T) { + _, err := mediaprobe.New("http://localhost:9091/not-exist") + if err == nil { + t.Errorf("Expected to return error found but return nil") + } +} + +func newServerErrorRemote(t *testing.T) { + handler := &Handler{ + Filename: "./fixtures/video.mp4", + FailOnAttempt: 1, + } + srv := ServeHttp(handler) + defer srv.Stop() + + _, err := mediaprobe.New(srv.Endpoint()) + if err == nil { + t.Errorf("Expected to return error found but return nil") + } +} + +func newLocalFile(t *testing.T) { + info, err := mediaprobe.New("./fixtures/video.mp4") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if info.Size != 383631 { + t.Errorf("Unexpected size. Expected %d, got %d", 383631, info.Size) + } +} + +func newRemoteFile(t *testing.T) { + handler := &Handler{ + Filename: "./fixtures/video.mp4", + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if info.Size != 383631 { + t.Errorf("Unexpected size. Expected %d, got %d", 383631, info.Size) + } +} diff --git a/mime.go b/mime.go index cf4d768..4cca09b 100644 --- a/mime.go +++ b/mime.go @@ -8,13 +8,18 @@ import ( // CalculateMime calculates mime type by magic numbers // Function uses libmagic bindings using github.com/rakyll/magicmime package. -func (info *Info) CalculateMime() error { +func (info *Info) CalculateMime() (err error) { if err := magicmime.Open(magicmime.MAGIC_MIME_TYPE | magicmime.MAGIC_SYMLINK | magicmime.MAGIC_ERROR); err != nil { return err } defer magicmime.Close() - media, err := magicmime.TypeByFile(info.filename) + var media string + if info.data != nil { + media, err = magicmime.TypeByBuffer(info.data) + } else { + media, err = magicmime.TypeByFile(info.filename) + } if err != nil { return err } diff --git a/mime_test.go b/mime_test.go index 4c329d8..2a82aad 100644 --- a/mime_test.go +++ b/mime_test.go @@ -6,10 +6,50 @@ import ( "github.com/Arimeka/mediaprobe" ) -func TestInfo_CalculateMimeNotFound(t *testing.T) { +func TestInfo_CalculateMime(t *testing.T) { + t.Run("not_found", calculateMimeNotFound) + t.Run("local", calculateMimeLocal) + t.Run("remote", calculateMimeRemote) +} + +func calculateMimeNotFound(t *testing.T) { info := &mediaprobe.Info{} err := info.CalculateMime() if err == nil { t.Errorf("Expected to return error found but return nil") } } + +func calculateMimeLocal(t *testing.T) { + info, err := mediaprobe.New("./fixtures/not-an-image.jpeg") + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + err = info.CalculateMime() + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if info.MediaType != "video" { + t.Errorf("Unexpected media type. Expected %s, got %s", "video", info.MediaType) + } +} + +func calculateMimeRemote(t *testing.T) { + handler := &Handler{ + Filename: "./fixtures/not-an-image.jpeg", + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.New(srv.Endpoint()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + err = info.CalculateMime() + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if info.MediaType != "video" { + t.Errorf("Unexpected media type. Expected %s, got %s", "video", info.MediaType) + } +} diff --git a/probe.go b/probe.go index 9935e02..d81ea67 100644 --- a/probe.go +++ b/probe.go @@ -9,6 +9,8 @@ import ( // Parse file media data // It determines the file type by magic bytes, // and parses the media data of the video or image. +// +// Accepted filename as local path or absolute url. func Parse(filename string) (Info, error) { info, err := New(filename) if err != nil { diff --git a/probe_test.go b/probe_test.go index f09f6a5..259dfb3 100644 --- a/probe_test.go +++ b/probe_test.go @@ -7,22 +7,34 @@ import ( ) const ( - testProbeValidImage = "./fixtures/image.jpeg" - testProbeInvalidImage = "./fixtures/not-an-image.jpeg" - testProbeValidVideo = "./fixtures/video.mp4" - testProbeInvalidVideo = "./fixtures/not-a-video.mp4" - testProbeValidAudio = "./fixtures/audio.mp3" - testProbeCorruptedFile = "./fixtures/corrupted.mp4" + testProbeValidRemoteImage = "http://localhost:9097/image.jpeg" + testProbeValidImage = "./fixtures/image.jpeg" + testProbeInvalidImage = "./fixtures/not-an-image.jpeg" + testProbeValidVideo = "./fixtures/video.mp4" + testProbeInvalidVideo = "./fixtures/not-a-video.mp4" + testProbeValidAudio = "./fixtures/audio.mp3" + testProbeCorruptedFile = "./fixtures/corrupted.mp4" ) -func TestParseNotFound(t *testing.T) { +func TestParse(t *testing.T) { + t.Run("not_found", parseNotFound) + t.Run("valid_remote_image", parseValidRemoteImage) + t.Run("valid_image", parseValidImage) + t.Run("invalid_image", parseInvalidImage) + t.Run("valid_video", parseValidVideo) + t.Run("invalid_video", parseInvalidVideo) + t.Run("valid_audio", parseValidAudio) + t.Run("corrupted_file", parseCorruptedFile) +} + +func parseNotFound(t *testing.T) { _, err := mediaprobe.Parse("") if err == nil { t.Errorf("Expected to return error found but return nil") } } -func TestParseValidImage(t *testing.T) { +func parseValidImage(t *testing.T) { info, err := mediaprobe.Parse(testProbeValidImage) if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testProbeValidImage, err) @@ -33,7 +45,24 @@ func TestParseValidImage(t *testing.T) { } } -func TestParseInvalidImage(t *testing.T) { +func parseValidRemoteImage(t *testing.T) { + handler := &Handler{ + Filename: "./fixtures/image.jpeg", + } + srv := ServeHttp(handler) + defer srv.Stop() + + info, err := mediaprobe.Parse(srv.Endpoint()) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + width := info.Width + if width != 290 { + t.Errorf("Filename: %s. Not expected width. Expected %d; got %d", testProbeValidRemoteImage, 290, width) + } +} + +func parseInvalidImage(t *testing.T) { info, err := mediaprobe.Parse(testProbeInvalidImage) if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testProbeInvalidImage, err) @@ -44,7 +73,7 @@ func TestParseInvalidImage(t *testing.T) { } } -func TestParseValidVideo(t *testing.T) { +func parseValidVideo(t *testing.T) { info, err := mediaprobe.Parse(testProbeValidVideo) if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testProbeValidVideo, err) @@ -55,7 +84,7 @@ func TestParseValidVideo(t *testing.T) { } } -func TestParseInvalidVideo(t *testing.T) { +func parseInvalidVideo(t *testing.T) { info, err := mediaprobe.Parse(testProbeInvalidVideo) if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testProbeInvalidVideo, err) @@ -66,7 +95,7 @@ func TestParseInvalidVideo(t *testing.T) { } } -func TestParseValidAudio(t *testing.T) { +func parseValidAudio(t *testing.T) { info, err := mediaprobe.Parse(testProbeValidAudio) if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testProbeValidAudio, err) @@ -77,7 +106,7 @@ func TestParseValidAudio(t *testing.T) { } } -func TestParseCorruptedFile(t *testing.T) { +func parseCorruptedFile(t *testing.T) { info, err := mediaprobe.Parse(testProbeCorruptedFile) if err != nil { t.Errorf("Filename: %s. Unexpected error %v", testProbeCorruptedFile, err)