mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-07-28 23:58:38 +00:00
Use libmagic to identify files type. (#1124)
* Use libmagic to identify file types. * Fix mime array for database querying. * go mod tidy * Fix typo * Another fix for the scheduled image build workflow for tag case (#1137) * Try to fix the incorrect tag processing * more debug code * try with context=git * hopefully, final debug * the final commit with no debug code --------- Co-authored-by: Konstantin Koval * Add benchmark. * rollback workflows. * Fix AI comments. * Build image. * Fix AI comments. * Fix AI comments. * Rollback build workflow. --------- Co-authored-by: Kostiantyn <32730812+kkovaletp@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
module github.com/photoview/photoview/api
|
||||
|
||||
go 1.22.5
|
||||
|
||||
toolchain go1.23.1
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.57
|
||||
@@ -14,7 +12,7 @@ require (
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/hosom/gomagic v0.0.0-20160718182707-cbc00aac97a4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
||||
@@ -42,10 +42,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hosom/gomagic v0.0.0-20160718182707-cbc00aac97a4 h1:HWFAOHTnol5SkBTgR+A52tWHyH0SscmDjPJWOVWlwys=
|
||||
github.com/hosom/gomagic v0.0.0-20160718182707-cbc00aac97a4/go.mod h1:2Oan8DZ6RtFP1c8gJGsw1sshEGsuCRzHFCkssb1PPyA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
|
||||
@@ -2,6 +2,7 @@ package media_encoding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"os"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
|
||||
@@ -73,25 +73,28 @@ type EncodeMediaData struct {
|
||||
Media *models.Media
|
||||
CounterpartPath *string
|
||||
_photoImage image.Image
|
||||
_contentType *media_type.MediaType
|
||||
_contentType media_type.MediaType
|
||||
_videoMetadata *ffprobe.ProbeData
|
||||
}
|
||||
|
||||
func NewEncodeMediaData(media *models.Media) EncodeMediaData {
|
||||
fileType := media_type.GetMediaType(media.Path)
|
||||
|
||||
return EncodeMediaData{
|
||||
Media: media,
|
||||
Media: media,
|
||||
_contentType: fileType,
|
||||
}
|
||||
}
|
||||
|
||||
// ContentType reads the image to determine its content type
|
||||
func (img *EncodeMediaData) ContentType() (*media_type.MediaType, error) {
|
||||
if img._contentType != nil {
|
||||
func (img *EncodeMediaData) ContentType() (media_type.MediaType, error) {
|
||||
if img._contentType != media_type.TypeUnknown {
|
||||
return img._contentType, nil
|
||||
}
|
||||
|
||||
imgType, err := media_type.GetMediaType(img.Media.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
imgType := media_type.GetMediaType(img.Media.Path)
|
||||
if imgType == media_type.TypeUnknown {
|
||||
return imgType, fmt.Errorf("unknown type of %q", img.Media.Path)
|
||||
}
|
||||
|
||||
img._contentType = imgType
|
||||
@@ -109,78 +112,21 @@ func (img *EncodeMediaData) EncodeHighRes(outputPath string) error {
|
||||
}
|
||||
|
||||
// Use magick if there is no counterpart JPEG file to use instead
|
||||
if contentType.IsRaw() && img.CounterpartPath == nil {
|
||||
if executable_worker.Magick.IsInstalled() {
|
||||
err := executable_worker.Magick.EncodeJpeg(img.Media.Path, outputPath, 70)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("could not convert photo as no RAW converter was found")
|
||||
}
|
||||
} else {
|
||||
image, err := img.photoImage()
|
||||
if err != nil {
|
||||
return err
|
||||
if contentType.IsImage() && !contentType.IsWebCompatible() {
|
||||
imgPath := img.Media.Path
|
||||
if img.CounterpartPath != nil {
|
||||
imgPath = *img.CounterpartPath
|
||||
}
|
||||
|
||||
encodeImageJPEG(image, outputPath, 70)
|
||||
err := executable_worker.Magick.EncodeJpeg(imgPath, outputPath, 70)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert RAW photo %q to JPEG: %w", imgPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// photoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
|
||||
func (img *EncodeMediaData) photoImage() (image.Image, error) {
|
||||
if img._photoImage != nil {
|
||||
return img._photoImage, nil
|
||||
}
|
||||
|
||||
var photoPath string
|
||||
if img.CounterpartPath != nil {
|
||||
photoPath = *img.CounterpartPath
|
||||
} else {
|
||||
photoPath = img.Media.Path
|
||||
}
|
||||
|
||||
photoImg, err := img.decodeImage(photoPath)
|
||||
if err != nil {
|
||||
return nil, utils.HandleError("image decoding", err)
|
||||
}
|
||||
|
||||
img._photoImage = photoImg
|
||||
return img._photoImage, nil
|
||||
}
|
||||
|
||||
func (img *EncodeMediaData) decodeImage(imagePath string) (image.Image, error) {
|
||||
file, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to open file to decode image (%s)", imagePath)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
mediaType, err := img.ContentType()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get media content type needed to decode it (%s)", imagePath)
|
||||
}
|
||||
|
||||
var decodedImage image.Image
|
||||
|
||||
if *mediaType == media_type.TypeHeic {
|
||||
decodedImage, _, err = image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to decode HEIF image (%s)", imagePath)
|
||||
}
|
||||
} else {
|
||||
decodedImage, err = imaging.Decode(file, imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to decode image (%s)", imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
return decodedImage, nil
|
||||
}
|
||||
|
||||
func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.ProbeData, error) {
|
||||
|
||||
if enc._videoMetadata != nil {
|
||||
|
||||
45
api/scanner/media_type/counterpart.go
Normal file
45
api/scanner/media_type/counterpart.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package media_type
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FindWebCounterpart returns the filename if the file `imagePath` has a counterpart file competible with the browser.
|
||||
func FindWebCounterpart(imagePath string) (string, bool) {
|
||||
return findCounterpart(imagePath, func(filename string) bool {
|
||||
mt := GetMediaType(filename)
|
||||
return mt.IsImage() && mt.IsWebCompatible()
|
||||
})
|
||||
}
|
||||
|
||||
// FindRawCounterpart returns the filename if the file `imagePath` has a counterpart file which needs to be processed before showing in the browser.
|
||||
func FindRawCounterpart(imagePath string) (string, bool) {
|
||||
return findCounterpart(imagePath, func(filename string) bool {
|
||||
mt := GetMediaType(filename)
|
||||
return mt.IsImage() && !mt.IsWebCompatible()
|
||||
})
|
||||
}
|
||||
|
||||
func findCounterpart(filename string, acceptFn func(filepath string) bool) (string, bool) {
|
||||
ext := path.Ext(filename)
|
||||
filenamePattern := strings.TrimSuffix(filename, ext) + ".*"
|
||||
|
||||
filenames, err := filepath.Glob(filenamePattern)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for _, f := range filenames {
|
||||
if f == filename {
|
||||
continue
|
||||
}
|
||||
|
||||
if acceptFn(f) {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
66
api/scanner/media_type/counterpart_test.go
Normal file
66
api/scanner/media_type/counterpart_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package media_type
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
)
|
||||
|
||||
func TestFindWebCounterpart(t *testing.T) {
|
||||
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
wantFile string
|
||||
wantOk bool
|
||||
}{
|
||||
{"raw_with_jpg.tiff", "raw_with_jpg.jpg", true},
|
||||
{"raw_with_file.tiff", "", false},
|
||||
{"standalone_raw.tiff", "", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
input := filepath.Join(mediaPath, tc.input)
|
||||
if _, err := os.Stat(input); err != nil {
|
||||
t.Fatalf("input %q doesn't exist: %v", input, err)
|
||||
}
|
||||
|
||||
file, ok := FindWebCounterpart(input)
|
||||
got := strings.TrimLeft(strings.TrimPrefix(file, mediaPath), "/")
|
||||
|
||||
if got != tc.wantFile || ok != tc.wantOk {
|
||||
t.Errorf("FindWebCounterpart(%q) = (%q, %v), want: (%q, %v)", tc.input, got, ok, tc.wantFile, tc.wantOk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRawCounterpart(t *testing.T) {
|
||||
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
wantFile string
|
||||
wantOk bool
|
||||
}{
|
||||
{"raw_with_jpg.jpg", "raw_with_jpg.tiff", true},
|
||||
{"jpg_with_file.jpg", "", false},
|
||||
{"standalone_jpg.jpg", "", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
input := filepath.Join(mediaPath, tc.input)
|
||||
if _, err := os.Stat(input); err != nil {
|
||||
t.Fatalf("input %q doesn't exist: %v", input, err)
|
||||
}
|
||||
|
||||
file, ok := FindRawCounterpart(input)
|
||||
got := strings.TrimLeft(strings.TrimPrefix(file, mediaPath), "/")
|
||||
|
||||
if got != tc.wantFile || ok != tc.wantOk {
|
||||
t.Errorf("FindRawCounterpart(%q) = (%q, %v), want: (%q, %v)", tc.input, got, ok, tc.wantFile, tc.wantOk)
|
||||
}
|
||||
}
|
||||
}
|
||||
78
api/scanner/media_type/magic.go
Normal file
78
api/scanner/media_type/magic.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package media_type
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
magic "github.com/hosom/gomagic"
|
||||
"github.com/photoview/photoview/api/log"
|
||||
)
|
||||
|
||||
var libmagic struct {
|
||||
mu sync.Mutex
|
||||
libmagic *libMagic
|
||||
err error
|
||||
}
|
||||
|
||||
func init() {
|
||||
libmagic.libmagic, libmagic.err = newLibMagic()
|
||||
if libmagic.err != nil {
|
||||
libmagic.libmagic = nil
|
||||
log.Error("Init libmagic error.", "error", libmagic.err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetMediaType returns a media type of file `f`.
|
||||
// This function is thread-safe.
|
||||
func GetMediaType(f string) MediaType {
|
||||
libmagic.mu.Lock()
|
||||
defer libmagic.mu.Unlock()
|
||||
|
||||
if libmagic.err != nil {
|
||||
log.Warn("GetMediaType() error.", "error", libmagic.err, "file", f)
|
||||
return TypeUnknown
|
||||
}
|
||||
|
||||
mime, err := libmagic.libmagic.Type(f)
|
||||
if err != nil {
|
||||
log.Warn("GetMediaType() error.", "error", err, "file", f)
|
||||
return TypeUnknown
|
||||
}
|
||||
|
||||
mime = strings.SplitN(mime, ";", 2)[0]
|
||||
|
||||
return mediaType(mime)
|
||||
}
|
||||
|
||||
// libMagic parses the magic code in a file and returns the media type.
|
||||
type libMagic struct {
|
||||
magic *magic.Magic
|
||||
}
|
||||
|
||||
// newLibMagic creates an instance of Magic.
|
||||
func newLibMagic() (*libMagic, error) {
|
||||
// MAGIC_SYMLINK: Follow symlinks
|
||||
// MAGIC_MIME: Return MIME type string
|
||||
// MAGIC_ERROR: Handle errors in magic database
|
||||
// MAGIC_NO_CHECK_COMPRESS: Don't check for compressed files
|
||||
// MAGIC_NO_CHECK_ENCODING: Don't check for text encodings
|
||||
m, err := magic.Open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME | magic.MAGIC_ERROR | magic.MAGIC_NO_CHECK_COMPRESS | magic.MAGIC_NO_CHECK_ENCODING)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &libMagic{
|
||||
magic: m,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the library.
|
||||
func (m *libMagic) Close() error {
|
||||
return m.magic.Close()
|
||||
}
|
||||
|
||||
// Type returns the media type of the given file `f`. It returns TypeUnknown if the file is not a image nor a video.
|
||||
// This function is not thread-safe.
|
||||
func (m *libMagic) Type(f string) (string, error) {
|
||||
return m.magic.File(f)
|
||||
}
|
||||
146
api/scanner/media_type/magic_test.go
Normal file
146
api/scanner/media_type/magic_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package media_type
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
magic "github.com/hosom/gomagic"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
)
|
||||
|
||||
func TestMagic(t *testing.T) {
|
||||
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||
|
||||
tests := []struct {
|
||||
filepath string
|
||||
filetype MediaType
|
||||
}{
|
||||
{"file.pdf", mediaType("application/pdf")},
|
||||
|
||||
{"bmp.bmp", TypeBMP},
|
||||
{"gif.gif", TypeGIF},
|
||||
{"jpeg.jpg", TypeJPEG},
|
||||
{"png.png", TypePNG},
|
||||
{"webp.webp", TypeWebP},
|
||||
|
||||
{"heif.heif", mediaType("image/heic")},
|
||||
{"jpeg2000.jp2", mediaType("image/jp2")},
|
||||
{"tiff.tiff", mediaType("image/tiff")},
|
||||
|
||||
{"mp4.mp4", TypeMP4},
|
||||
{"ogg.ogg", TypeOGG},
|
||||
{"mpeg.mpg", TypeMPEG},
|
||||
{"webm.webm", TypeWEBM},
|
||||
|
||||
{"avi.avi", mediaType("video/x-msvideo")},
|
||||
{"mkv.mkv", mediaType("video/x-matroska")},
|
||||
{"quicktime.mov", mediaType("video/quicktime")},
|
||||
{"wmv.wmv", mediaType("video/x-ms-asf")},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
for _, tc := range tests {
|
||||
wg.Add(1)
|
||||
input, want := tc.filepath, tc.filetype
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
path := filepath.Join(mediaPath, input)
|
||||
|
||||
got := GetMediaType(path)
|
||||
if got != want {
|
||||
t.Errorf("magic.Type(%q) = %v, want: %v", path, got, want)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMagicNoInit(t *testing.T) {
|
||||
org := libmagic.libmagic
|
||||
libmagic.libmagic = nil
|
||||
libmagic.err = errors.New("error")
|
||||
t.Cleanup(func() {
|
||||
libmagic.libmagic = org
|
||||
libmagic.err = nil
|
||||
})
|
||||
|
||||
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||
file := filepath.Join(mediaPath, "file.pdf")
|
||||
|
||||
got := GetMediaType(file)
|
||||
if want := TypeUnknown; got != want {
|
||||
t.Errorf("GetMediaType() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func getMediaFiles() []string {
|
||||
mediaPath := test_utils.PathFromAPIRoot("./scanner/test_media/real_media")
|
||||
var files []string
|
||||
for _, f := range []string{
|
||||
"file.pdf",
|
||||
|
||||
"bmp.bmp",
|
||||
"gif.gif",
|
||||
"jpeg.jpg",
|
||||
"png.png",
|
||||
"webp.webp",
|
||||
|
||||
"heif.heif",
|
||||
"jpeg2000.jp2",
|
||||
"tiff.tiff",
|
||||
|
||||
"mp4.mp4",
|
||||
"ogg.ogg",
|
||||
"mpeg.mpg",
|
||||
"webm.webm",
|
||||
|
||||
"avi.avi",
|
||||
"mkv.mkv",
|
||||
"quicktime.mov",
|
||||
"wmv.wmv",
|
||||
} {
|
||||
files = append(files, filepath.Join(mediaPath, f))
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func BenchmarkMagicAll(b *testing.B) {
|
||||
files := getMediaFiles()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
m, err := magic.Open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME | magic.MAGIC_ERROR | magic.MAGIC_NO_CHECK_COMPRESS | magic.MAGIC_NO_CHECK_ENCODING)
|
||||
if err != nil {
|
||||
b.Fatalf("magic.Open() error: %v", err)
|
||||
}
|
||||
|
||||
_, _ = m.File(files[i%len(files)])
|
||||
m.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMagicType(b *testing.B) {
|
||||
files := getMediaFiles()
|
||||
|
||||
m, err := magic.Open(magic.MAGIC_SYMLINK | magic.MAGIC_MIME | magic.MAGIC_ERROR | magic.MAGIC_NO_CHECK_COMPRESS | magic.MAGIC_NO_CHECK_ENCODING)
|
||||
if err != nil {
|
||||
b.Fatalf("magic.Open() error: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
mu.Lock()
|
||||
_, _ = m.File(files[i%len(files)])
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -1,347 +1,121 @@
|
||||
package media_type
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/h2non/filetype"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
||||
"github.com/pkg/errors"
|
||||
"unique"
|
||||
)
|
||||
|
||||
type MediaType string
|
||||
type MediaType unique.Handle[string]
|
||||
|
||||
const (
|
||||
TypeJpeg MediaType = "image/jpeg"
|
||||
TypePng MediaType = "image/png"
|
||||
TypeTiff MediaType = "image/tiff"
|
||||
TypeWebp MediaType = "image/webp"
|
||||
TypeBmp MediaType = "image/bmp"
|
||||
TypeHeic MediaType = "image/heic"
|
||||
TypeGif MediaType = "image/gif"
|
||||
func mediaType(mime string) MediaType {
|
||||
return MediaType(unique.Make(mime))
|
||||
}
|
||||
|
||||
// Raw formats
|
||||
TypeDNG MediaType = "image/x-adobe-dng"
|
||||
TypeARW MediaType = "image/x-sony-arw"
|
||||
TypeSR2 MediaType = "image/x-sony-sr2"
|
||||
TypeSRF MediaType = "image/x-sony-srf"
|
||||
TypeCR2 MediaType = "image/x-canon-cr2"
|
||||
TypeCR3 MediaType = "image/x-canon-cr3"
|
||||
TypeCRW MediaType = "image/x-canon-crw"
|
||||
TypeERF MediaType = "image/x-epson-erf"
|
||||
TypeDCS MediaType = "image/x-kodak-dcs"
|
||||
TypeDRF MediaType = "image/x-kodak-drf"
|
||||
TypeDCR MediaType = "image/x-kodak-dcr"
|
||||
TypeK25 MediaType = "image/x-kodak-k25"
|
||||
TypeKDC MediaType = "image/x-kodak-kdc"
|
||||
TypeMRW MediaType = "image/x-minolta-mrw"
|
||||
TypeMDC MediaType = "image/x-minolta-mdc"
|
||||
TypeNEF MediaType = "image/x-nikon-nef"
|
||||
TypeNRW MediaType = "image/x-nikon-nrw"
|
||||
TypeORF MediaType = "image/x-olympus-orf"
|
||||
TypePEF MediaType = "image/x-pentax-pef"
|
||||
TypeRAF MediaType = "image/x-fuji-raf"
|
||||
TypeRAW MediaType = "image/x-panasonic-raw"
|
||||
TypeRW2 MediaType = "image/x-panasonic-rw2"
|
||||
TypeGPR MediaType = "image/x-gopro-gpr"
|
||||
Type3FR MediaType = "image/x-hasselblad-3fr"
|
||||
TypeFFF MediaType = "image/x-hasselblad-fff"
|
||||
TypeMEF MediaType = "image/x-mamiya-mef"
|
||||
TypeCap MediaType = "image/x-phaseone-cap"
|
||||
TypeIIQ MediaType = "image/x-phaseone-iiq"
|
||||
TypeMOS MediaType = "image/x-leaf-mos"
|
||||
TypeRWL MediaType = "image/x-leica-rwl"
|
||||
TypeSRW MediaType = "image/x-samsung-srw"
|
||||
var (
|
||||
TypeUnknown MediaType
|
||||
|
||||
// Video formats
|
||||
TypeMP4 MediaType = "video/mp4"
|
||||
TypeMPEG MediaType = "video/mpeg"
|
||||
Type3GP MediaType = "video/3gpp"
|
||||
Type3G2 MediaType = "video/3gpp2"
|
||||
TypeOGG MediaType = "video/ogg"
|
||||
TypeWMV MediaType = "video/x-ms-wmv"
|
||||
TypeAVI MediaType = "video/x-msvideo"
|
||||
TypeWEBM MediaType = "video/webm"
|
||||
TypeMOV MediaType = "video/quicktime"
|
||||
TypeTS MediaType = "video/mp2t"
|
||||
TypeMTS MediaType = "video/MP2T"
|
||||
TypeMKV MediaType = "video/x-matroska"
|
||||
TypeImage = mediaType("image/")
|
||||
TypeVideo = mediaType("video/")
|
||||
|
||||
// Web Image formats
|
||||
TypeJPEG = mediaType("image/jpeg")
|
||||
TypePNG = mediaType("image/png")
|
||||
TypeWebP = mediaType("image/webp")
|
||||
TypeBMP = mediaType("image/bmp")
|
||||
TypeGIF = mediaType("image/gif")
|
||||
|
||||
// Web Video formats
|
||||
TypeMP4 = mediaType("video/mp4")
|
||||
TypeMPEG = mediaType("video/mpeg")
|
||||
TypeOGG = mediaType("video/ogg")
|
||||
TypeWEBM = mediaType("video/webm")
|
||||
)
|
||||
|
||||
var SupportedMimetypes = [...]MediaType{
|
||||
TypeJpeg,
|
||||
TypePng,
|
||||
TypeTiff,
|
||||
TypeWebp,
|
||||
TypeBmp,
|
||||
TypeHeic,
|
||||
TypeGif,
|
||||
}
|
||||
var webImageMimetypes = arrayToSet([]MediaType{
|
||||
TypeJPEG,
|
||||
TypePNG,
|
||||
TypeWebP,
|
||||
TypeBMP,
|
||||
TypeGIF,
|
||||
})
|
||||
|
||||
var WebMimetypes = [...]MediaType{
|
||||
TypeJpeg,
|
||||
TypePng,
|
||||
TypeWebp,
|
||||
TypeBmp,
|
||||
TypeGif,
|
||||
}
|
||||
|
||||
var RawMimeTypes = [...]MediaType{
|
||||
TypeDNG,
|
||||
TypeARW,
|
||||
TypeSR2,
|
||||
TypeSRF,
|
||||
TypeCR2,
|
||||
TypeCR3,
|
||||
TypeCRW,
|
||||
TypeERF,
|
||||
TypeDCS,
|
||||
TypeDRF,
|
||||
TypeDCR,
|
||||
TypeK25,
|
||||
TypeKDC,
|
||||
TypeMRW,
|
||||
TypeMDC,
|
||||
TypeNEF,
|
||||
TypeNRW,
|
||||
TypeORF,
|
||||
TypePEF,
|
||||
TypeRAF,
|
||||
TypeRAW,
|
||||
TypeRW2,
|
||||
TypeGPR,
|
||||
Type3FR,
|
||||
TypeFFF,
|
||||
TypeMEF,
|
||||
TypeCap,
|
||||
TypeIIQ,
|
||||
TypeMOS,
|
||||
TypeRWL,
|
||||
TypeSRW,
|
||||
}
|
||||
|
||||
var VideoMimetypes = [...]MediaType{
|
||||
TypeMP4,
|
||||
TypeMPEG,
|
||||
Type3GP,
|
||||
Type3G2,
|
||||
TypeOGG,
|
||||
TypeWMV,
|
||||
TypeAVI,
|
||||
TypeWEBM,
|
||||
TypeMOV,
|
||||
TypeTS,
|
||||
TypeMTS,
|
||||
TypeMKV,
|
||||
}
|
||||
|
||||
// WebVideoMimetypes are video types that can be played directly in the browser without transcoding
|
||||
var WebVideoMimetypes = [...]MediaType{
|
||||
var webVideoMimetypes = arrayToSet([]MediaType{
|
||||
TypeMP4,
|
||||
TypeMPEG,
|
||||
TypeWEBM,
|
||||
TypeOGG,
|
||||
})
|
||||
|
||||
// Legacy function. Should be removed.
|
||||
var WebMimetypes = []string{
|
||||
TypeJPEG.String(),
|
||||
TypePNG.String(),
|
||||
TypeWebP.String(),
|
||||
TypeBMP.String(),
|
||||
TypeGIF.String(),
|
||||
|
||||
TypeMP4.String(),
|
||||
TypeMPEG.String(),
|
||||
TypeWEBM.String(),
|
||||
TypeOGG.String(),
|
||||
}
|
||||
|
||||
var fileExtensions = map[string]MediaType{
|
||||
".jpg": TypeJpeg,
|
||||
".jpeg": TypeJpeg,
|
||||
".png": TypePng,
|
||||
".tif": TypeTiff,
|
||||
".tiff": TypeTiff,
|
||||
".bmp": TypeBmp,
|
||||
".heic": TypeHeic,
|
||||
".gif": TypeGif,
|
||||
|
||||
// RAW formats
|
||||
".dng": TypeDNG,
|
||||
".arw": TypeARW,
|
||||
".sr2": TypeSR2,
|
||||
".srf": TypeSRF,
|
||||
".srw": TypeSRW,
|
||||
".cr2": TypeCR2,
|
||||
".cr3": TypeCR3,
|
||||
".crw": TypeCRW,
|
||||
".erf": TypeERF,
|
||||
".dcr": TypeDCR,
|
||||
".k25": TypeK25,
|
||||
".kdc": TypeKDC,
|
||||
".mrw": TypeMRW,
|
||||
".nef": TypeNEF,
|
||||
".nrw": TypeNRW,
|
||||
".mdc": TypeMDC,
|
||||
".mef": TypeMEF,
|
||||
".orf": TypeORF,
|
||||
".pef": TypePEF,
|
||||
".raf": TypeRAF,
|
||||
".raw": TypeRAW,
|
||||
".rw2": TypeRW2,
|
||||
".dcs": TypeDCS,
|
||||
".drf": TypeDRF,
|
||||
".gpr": TypeGPR,
|
||||
".3fr": Type3FR,
|
||||
".fff": TypeFFF,
|
||||
".cap": TypeCap,
|
||||
".iiq": TypeIIQ,
|
||||
".mos": TypeMOS,
|
||||
".rwl": TypeRWL,
|
||||
|
||||
// Video formats
|
||||
".mp4": TypeMP4,
|
||||
".m4v": TypeMP4,
|
||||
".mpeg": TypeMPEG,
|
||||
".3gp": Type3GP,
|
||||
".3g2": Type3G2,
|
||||
".ogv": TypeOGG,
|
||||
".wmv": TypeWMV,
|
||||
".avi": TypeAVI,
|
||||
".webm": TypeWEBM,
|
||||
".mov": TypeMOV,
|
||||
".qt": TypeMOV,
|
||||
".ts": TypeTS,
|
||||
".m2ts": TypeMTS,
|
||||
".mts": TypeMTS,
|
||||
".mkv": TypeMKV,
|
||||
func arrayToSet[T comparable](array []T) map[T]struct{} {
|
||||
ret := make(map[T]struct{})
|
||||
for _, item := range array {
|
||||
ret[item] = struct{}{}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (imgType *MediaType) IsRaw() bool {
|
||||
for _, raw_mime := range RawMimeTypes {
|
||||
if raw_mime == *imgType {
|
||||
return true
|
||||
}
|
||||
// IsWebCompatible returns true if the media type is compatible with the browser.
|
||||
func (t MediaType) IsWebCompatible() bool {
|
||||
if t == TypeUnknown {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (imgType *MediaType) IsWebCompatible() bool {
|
||||
for _, web_mime := range WebMimetypes {
|
||||
if web_mime == *imgType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, web_mime := range WebVideoMimetypes {
|
||||
if web_mime == *imgType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (imgType *MediaType) IsVideo() bool {
|
||||
for _, video_mime := range VideoMimetypes {
|
||||
if video_mime == *imgType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (imgType *MediaType) IsBasicTypeSupported() bool {
|
||||
for _, img_mime := range SupportedMimetypes {
|
||||
if img_mime == *imgType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSupported determines if the given type can be processed
|
||||
func (imgType *MediaType) IsSupported() bool {
|
||||
if imgType.IsBasicTypeSupported() {
|
||||
if _, ok := webImageMimetypes[t]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if executable_worker.Magick.IsInstalled() && imgType.IsRaw() {
|
||||
return true
|
||||
}
|
||||
|
||||
if executable_worker.Ffmpeg.IsInstalled() && imgType.IsVideo() {
|
||||
if _, ok := webVideoMimetypes[t]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetExtensionMediaType(ext string) (MediaType, bool) {
|
||||
result, found := fileExtensions[strings.ToLower(ext)]
|
||||
return result, found
|
||||
// IsImage returns true if the media type is image type.
|
||||
func (t MediaType) IsImage() bool {
|
||||
if t == TypeUnknown {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasPrefix(t.String(), TypeImage.String())
|
||||
}
|
||||
|
||||
func GetMediaType(path string) (*MediaType, error) {
|
||||
ext := filepath.Ext(path)
|
||||
|
||||
fileExtType, found := GetExtensionMediaType(ext)
|
||||
|
||||
if found {
|
||||
if fileExtType.IsSupported() {
|
||||
return &fileExtType, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
// IsVideo returns true if the media type is video type.
|
||||
func (t MediaType) IsVideo() bool {
|
||||
if t == TypeUnknown {
|
||||
return false
|
||||
}
|
||||
|
||||
// If extension was not recognized try to read file header
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not open file to determine content-type %s", path)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
head := make([]byte, 261)
|
||||
if _, err := file.Read(head); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, errors.Wrapf(err, "could not read file to determine content-type: %s", path)
|
||||
}
|
||||
|
||||
_imgType, err := filetype.Image(head)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
imgType := MediaType(_imgType.MIME.Value)
|
||||
if imgType.IsSupported() {
|
||||
return &imgType, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return strings.HasPrefix(t.String(), TypeVideo.String())
|
||||
}
|
||||
|
||||
func (mediaType MediaType) FileExtensions() []string {
|
||||
var extensions []string
|
||||
|
||||
for ext, extType := range fileExtensions {
|
||||
if extType == mediaType {
|
||||
extensions = append(extensions, ext)
|
||||
extensions = append(extensions, strings.ToUpper(ext))
|
||||
}
|
||||
// IsSupported returns true if the media type can be processed.
|
||||
func (t MediaType) IsSupported() bool {
|
||||
if t == TypeUnknown {
|
||||
return false
|
||||
}
|
||||
|
||||
return extensions
|
||||
return t.IsImage() || t.IsVideo()
|
||||
}
|
||||
|
||||
func RawCounterpart(imagePath string) *string {
|
||||
pathWithoutExt := strings.TrimSuffix(imagePath, path.Ext(imagePath))
|
||||
|
||||
for _, rawType := range RawMimeTypes {
|
||||
for _, ext := range rawType.FileExtensions() {
|
||||
testPath := pathWithoutExt + ext
|
||||
if scanner_utils.FileExists(testPath) {
|
||||
return &testPath
|
||||
}
|
||||
}
|
||||
func (t MediaType) String() string {
|
||||
if t == TypeUnknown {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
return nil
|
||||
return unique.Handle[string](t).Value()
|
||||
}
|
||||
|
||||
@@ -1,42 +1,85 @@
|
||||
package media_type_test
|
||||
package media_type
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(test_utils.UnitTestRun(m))
|
||||
type boolImage bool
|
||||
|
||||
const isImage boolImage = true
|
||||
|
||||
type boolVideo bool
|
||||
|
||||
const isVideo boolVideo = true
|
||||
|
||||
type boolWebCompatible bool
|
||||
|
||||
const isWebCompatible boolWebCompatible = true
|
||||
|
||||
type boolSupport bool
|
||||
|
||||
const isSupport boolSupport = true
|
||||
|
||||
func TestMediaTypeNoDeps(t *testing.T) {
|
||||
tests := []struct {
|
||||
mtype MediaType
|
||||
wantIsImage boolImage
|
||||
wantIsVideo boolVideo
|
||||
wantIsWebCompatible boolWebCompatible
|
||||
wantIsSupport boolSupport
|
||||
}{
|
||||
// Unknown and unsupported types
|
||||
{TypeUnknown, !isImage, !isVideo, !isWebCompatible, !isSupport},
|
||||
{mediaType("application/pdf"), !isImage, !isVideo, !isWebCompatible, !isSupport},
|
||||
|
||||
// Raw media types
|
||||
{mediaType("image/some-raw-type"), isImage, !isVideo, !isWebCompatible, isSupport},
|
||||
{mediaType("video/some-video-type"), !isImage, isVideo, !isWebCompatible, isSupport},
|
||||
|
||||
// Generic types
|
||||
{TypeImage, isImage, !isVideo, !isWebCompatible, isSupport},
|
||||
{TypeVideo, !isImage, isVideo, !isWebCompatible, isSupport},
|
||||
|
||||
// Web-compatible image types
|
||||
{TypeJPEG, isImage, !isVideo, isWebCompatible, isSupport},
|
||||
{TypePNG, isImage, !isVideo, isWebCompatible, isSupport},
|
||||
{TypeWebP, isImage, !isVideo, isWebCompatible, isSupport},
|
||||
{TypeBMP, isImage, !isVideo, isWebCompatible, isSupport},
|
||||
{TypeGIF, isImage, !isVideo, isWebCompatible, isSupport},
|
||||
|
||||
// Web-compatible video types
|
||||
{TypeMP4, !isImage, isVideo, isWebCompatible, isSupport},
|
||||
{TypeMPEG, !isImage, isVideo, isWebCompatible, isSupport},
|
||||
{TypeOGG, !isImage, isVideo, isWebCompatible, isSupport},
|
||||
{TypeWEBM, !isImage, isVideo, isWebCompatible, isSupport},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
gotImage := tc.mtype.IsImage()
|
||||
if got, want := gotImage, bool(tc.wantIsImage); got != want {
|
||||
t.Errorf("MediaType(%q).IsImage() = %v, want: %v", tc.mtype, got, want)
|
||||
}
|
||||
|
||||
gotVideo := tc.mtype.IsVideo()
|
||||
if got, want := gotVideo, bool(tc.wantIsVideo); got != want {
|
||||
t.Errorf("MediaType(%q).IsVideo() = %v, want: %v", tc.mtype, got, want)
|
||||
}
|
||||
|
||||
gotWebCompatible := tc.mtype.IsWebCompatible()
|
||||
if got, want := gotWebCompatible, bool(tc.wantIsWebCompatible); got != want {
|
||||
t.Errorf("MediaType(%q).IsWebCompatible() = %v, want: %v", tc.mtype, got, want)
|
||||
}
|
||||
|
||||
gotSupport := tc.mtype.IsSupported()
|
||||
if got, want := gotSupport, bool(tc.wantIsSupport); got != want {
|
||||
t.Errorf("MediaType(%q).IsSupported() = %v, want: %v", tc.mtype, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaTypeIsFunctions(t *testing.T) {
|
||||
rawType := media_type.TypeCR2
|
||||
pngType := media_type.TypePng
|
||||
mp4Type := media_type.TypeMP4
|
||||
|
||||
assert.True(t, rawType.IsRaw())
|
||||
assert.False(t, pngType.IsRaw())
|
||||
|
||||
assert.True(t, pngType.IsWebCompatible())
|
||||
assert.False(t, rawType.IsWebCompatible())
|
||||
|
||||
assert.True(t, mp4Type.IsVideo())
|
||||
assert.False(t, pngType.IsVideo())
|
||||
|
||||
assert.True(t, pngType.IsBasicTypeSupported())
|
||||
}
|
||||
|
||||
func TestMediaTypeFromExtension(t *testing.T) {
|
||||
pngType, found := media_type.GetExtensionMediaType(".PNG")
|
||||
|
||||
assert.True(t, found)
|
||||
assert.Equal(t, media_type.TypePng, pngType)
|
||||
}
|
||||
|
||||
func TestMediaTypeGetExtensions(t *testing.T) {
|
||||
assert.ElementsMatch(t, []string{".jpg", ".JPG", ".jpeg", ".JPEG"}, media_type.TypeJpeg.FileExtensions())
|
||||
func TestMediaTypeUnknown(t *testing.T) {
|
||||
var got MediaType
|
||||
if want := TypeUnknown; got != want {
|
||||
t.Errorf("MediaType zero value should be TypeUnknown, which is not")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package scanner_cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type AlbumScannerCache struct {
|
||||
@@ -69,24 +69,21 @@ func (c *AlbumScannerCache) AlbumContainsPhotos(path string) *bool {
|
||||
// (c.photo_types)[path] = content_type
|
||||
// }
|
||||
|
||||
func (c *AlbumScannerCache) GetMediaType(path string) (*media_type.MediaType, error) {
|
||||
func (c *AlbumScannerCache) GetMediaType(path string) (media_type.MediaType, error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
result, found := c.photo_types[path]
|
||||
if found {
|
||||
// log.Printf("Image cache hit: %s\n", path)
|
||||
return &result, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
mediaType, err := media_type.GetMediaType(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "get media type (%s)", path)
|
||||
mediaType := media_type.GetMediaType(path)
|
||||
if mediaType == media_type.TypeUnknown {
|
||||
return mediaType, fmt.Errorf("unknown media type (%s)", path)
|
||||
}
|
||||
|
||||
if mediaType != nil {
|
||||
(c.photo_types)[path] = *mediaType
|
||||
}
|
||||
c.photo_types[path] = mediaType
|
||||
|
||||
return mediaType, nil
|
||||
}
|
||||
@@ -122,16 +119,16 @@ func (c *AlbumScannerCache) IsPathMedia(mediaPath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if mediaType != nil {
|
||||
// Make sure file isn't empty
|
||||
fileStats, err := os.Stat(mediaPath)
|
||||
if err != nil || fileStats.Size() == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
if !mediaType.IsSupported() {
|
||||
log.Printf("Unsupported media type %q for file: %s\n", mediaType, mediaPath)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Printf("File is not a supported media %s\n", mediaPath)
|
||||
return false
|
||||
// Make sure file isn't empty
|
||||
fileStats, err := os.Stat(mediaPath)
|
||||
if err != nil || fileStats.Size() == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
package processing_tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_type"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CounterpartFilesTask struct {
|
||||
@@ -19,14 +15,14 @@ type CounterpartFilesTask struct {
|
||||
}
|
||||
|
||||
func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo fs.FileInfo, mediaPath string) (skip bool, err error) {
|
||||
ext := filepath.Ext(mediaPath)
|
||||
fileExtType, found := media_type.GetExtensionMediaType(ext)
|
||||
if !found {
|
||||
fileType := media_type.GetMediaType(mediaPath)
|
||||
|
||||
if !fileType.IsSupported() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if utils.EnvDisableRawProcessing.GetBool() {
|
||||
if fileExtType.IsRaw() {
|
||||
if !fileType.IsWebCompatible() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -34,13 +30,11 @@ func (t CounterpartFilesTask) MediaFound(ctx scanner_task.TaskContext, fileInfo
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !fileExtType.IsBasicTypeSupported() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
rawPath := media_type.RawCounterpart(mediaPath)
|
||||
if rawPath != nil {
|
||||
return true, nil
|
||||
if fileType.IsWebCompatible() {
|
||||
_, existed := media_type.FindRawCounterpart(mediaPath)
|
||||
if existed {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
@@ -50,38 +44,19 @@ func (t CounterpartFilesTask) BeforeProcessMedia(ctx scanner_task.TaskContext, m
|
||||
|
||||
mediaType, err := ctx.GetCache().GetMediaType(mediaData.Media.Path)
|
||||
if err != nil {
|
||||
return ctx, errors.Wrap(err, "scan for counterpart file")
|
||||
return ctx, fmt.Errorf("scan for counterpart file error: %w", err)
|
||||
}
|
||||
|
||||
if !mediaType.IsRaw() {
|
||||
if mediaType.IsWebCompatible() {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
counterpartFile := scanForCompressedCounterpartFile(mediaData.Media.Path)
|
||||
if counterpartFile != nil {
|
||||
mediaData.CounterpartPath = counterpartFile
|
||||
counterpartFile, ok := media_type.FindWebCounterpart(mediaData.Media.Path)
|
||||
if !ok {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
mediaData.CounterpartPath = &counterpartFile
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func scanForCompressedCounterpartFile(imagePath string) *string {
|
||||
ext := filepath.Ext(imagePath)
|
||||
fileExtType, found := media_type.GetExtensionMediaType(ext)
|
||||
|
||||
if found {
|
||||
if fileExtType.IsBasicTypeSupported() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
pathWithoutExt := strings.TrimSuffix(imagePath, path.Ext(imagePath))
|
||||
for _, ext := range media_type.TypeJpeg.FileExtensions() {
|
||||
testPath := pathWithoutExt + ext
|
||||
if scanner_utils.FileExists(testPath) {
|
||||
return &testPath
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
)
|
||||
|
||||
func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
||||
mediaPath := test_utils.PathFromAPIRoot("scanner/test_media/real_media")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
@@ -21,56 +23,54 @@ func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "StandaloneProcessRaw",
|
||||
file: "standalone.jpg",
|
||||
file: "standalone_jpg.jpg",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "StandaloneNotProcessRaw",
|
||||
file: "standalone.jpg",
|
||||
file: "standalone_jpg.jpg",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "RawJpegProcessRaw",
|
||||
file: "fujifilm_raw.jpg",
|
||||
file: "raw_with_jpg.jpg",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "RawJpegNotProcessRaw",
|
||||
file: "fujifilm_raw.jpg",
|
||||
file: "raw_with_jpg.jpg",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "RawProcessRaw",
|
||||
file: "fujifilm_raw.raf",
|
||||
file: "raw_with_jpg.tiff",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: false,
|
||||
},
|
||||
{
|
||||
name: "RawNotProcessRaw",
|
||||
file: "fujifilm_raw.raf",
|
||||
file: "raw_with_jpg.tiff",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "UnknownProcessRaw",
|
||||
file: "file.unknown",
|
||||
file: "file.pdf",
|
||||
disableRawProcessing: false,
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "UnknownNotProcessRaw",
|
||||
file: "file.unknown",
|
||||
file: "file.pdf",
|
||||
disableRawProcessing: true,
|
||||
wantSkip: true,
|
||||
},
|
||||
}
|
||||
|
||||
mediaPath := test_utils.PathFromAPIRoot("scanner/test_media/fake_media")
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
done := test_utils.SetEnv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing))
|
||||
|
||||
@@ -52,7 +52,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
|
||||
videoType, err := mediaData.ContentType()
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "error getting video content type")
|
||||
return []*models.MediaURL{}, fmt.Errorf("getting video content type error: %w", err)
|
||||
}
|
||||
|
||||
if videoOriginalURL == nil && videoType.IsWebCompatible() {
|
||||
@@ -75,7 +75,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
Width: webMetadata.Width,
|
||||
Height: webMetadata.Height,
|
||||
Purpose: models.MediaOriginal,
|
||||
ContentType: string(*videoType),
|
||||
ContentType: videoType.String(),
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *media_en
|
||||
Width: photoDimensions.Width,
|
||||
Height: photoDimensions.Height,
|
||||
Purpose: models.MediaOriginal,
|
||||
ContentType: string(*contentType),
|
||||
ContentType: contentType.String(),
|
||||
FileSize: fileStats.Size(),
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func (t SidecarTask) AfterMediaFound(ctx scanner_task.TaskContext, media *models
|
||||
return errors.Wrap(err, "scan for sidecar file")
|
||||
}
|
||||
|
||||
if !mediaType.IsRaw() {
|
||||
if mediaType.IsWebCompatible() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (t SidecarTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *media
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "sidecar task, process media")
|
||||
}
|
||||
|
||||
if !mediaType.IsRaw() {
|
||||
if mediaType.IsWebCompatible() {
|
||||
return []*models.MediaURL{}, nil
|
||||
}
|
||||
|
||||
|
||||
5
scripts/benchmark_api.sh
Executable file
5
scripts/benchmark_api.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
cd "$(dirname $0)/../api"
|
||||
go test ./... -bench . -benchmem
|
||||
@@ -11,5 +11,8 @@ apt-get install -y libdav1d-dev:${DEB_HOST_ARCH} libde265-dev:${DEB_HOST_ARCH} l
|
||||
apt-get install -y \
|
||||
libdlib-dev:${DEB_HOST_ARCH} libblas-dev:${DEB_HOST_ARCH} libatlas-base-dev:${DEB_HOST_ARCH} liblapack-dev:${DEB_HOST_ARCH} libjpeg62-turbo-dev:${DEB_HOST_ARCH}
|
||||
|
||||
# Install gomagic dependencies
|
||||
apt-get install -y libmagic-dev:${DEB_HOST_ARCH}
|
||||
|
||||
# Install tools for development
|
||||
apt-get install -y reflex sqlite3
|
||||
|
||||
@@ -12,3 +12,5 @@ apt-get install -y libjpeg62-turbo liblcms2-2 zlib1g libgomp1
|
||||
apt-get install -y libjxl0.7 liblcms2-2 liblqr-1-0 libdjvulibre21 libjpeg62-turbo libopenjp2-7 libopenexr-3-1-30 libpng16-16 libtiff6 libwebpmux3 libwebpdemux2 libwebp7 libxml2 zlib1g liblzma5 libbz2-1.0 libgomp1
|
||||
# go-face dependencies
|
||||
apt-get install -y libdlib19.1 libblas3 liblapack3 libjpeg62-turbo
|
||||
# libmagic dependencies
|
||||
apt-get install -y libmagic1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
cd $(dirname $0)/../api
|
||||
cd "$(dirname $0)/../api"
|
||||
go test ./... -v -database -filesystem -p 1 -coverprofile=coverage.txt -covermode=atomic 2>&1 | tee >(go-junit-report >test-api-coverage-report.xml)
|
||||
|
||||
Reference in New Issue
Block a user