mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 21:19:18 +00:00
Use magick generating thumbnails.
Fix #1204 * Add bad medias to scanner_test. * Fix the script. * Add GenerateThumbnail function. * Add identify dimension * Use new API. Cover json condition. * Use magick to generate thumbnails. * Update go mod. * Remove dependency of executable_worker -> test_utils * Reduce dependency of magick. * Use magick to get dimension. * Remove almost all code around `image` package. * Fix thumbnailing with multi-frame gif. * Remove the struct in wrapper. * Fix typo. * Build images. * Update logs * Add jpeg decoder. * Remove code dependency. * Add log to confirm scan in parallel. * Update log * Add reformat image perf tests. * Rollback the build workflow.
This commit is contained in:
@@ -7,7 +7,6 @@ require (
|
||||
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e
|
||||
github.com/barasher/go-exiftool v1.10.0
|
||||
github.com/buckket/go-blurhash v1.1.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/go-sql-driver/mysql v1.9.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
@@ -19,12 +18,10 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/strukturag/libheif v1.18.2
|
||||
github.com/vektah/gqlparser/v2 v2.5.27
|
||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
|
||||
github.com/xor-gate/goexif2 v1.1.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/image v0.27.0
|
||||
golang.org/x/text v0.25.0
|
||||
gopkg.in/vansante/go-ffprobe.v2 v2.2.1
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
|
||||
@@ -25,8 +25,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
@@ -93,8 +91,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/strukturag/libheif v1.18.2 h1:CrlRS7Kwl2odl4DYM/m6ay/HPXEcmQPK1xF8FxAqP7k=
|
||||
github.com/strukturag/libheif v1.18.2/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
@@ -107,9 +103,6 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
@@ -118,7 +111,6 @@ golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
|
||||
@@ -5,12 +5,10 @@ import (
|
||||
"database/sql/driver"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/photoview/photoview/api/database/drivers"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
@@ -75,26 +73,12 @@ func (fd FaceDescriptor) Value() (driver.Value, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// FaceRectangle stores a relative rectangle of a face in an image.
|
||||
type FaceRectangle struct {
|
||||
MinX, MaxX float64
|
||||
MinY, MaxY float64
|
||||
}
|
||||
|
||||
// ToDBFaceRectangle converts a pixel absolute rectangle to a relative FaceRectangle to be saved in the database
|
||||
func ToDBFaceRectangle(imgRec image.Rectangle, imagePath string) (*FaceRectangle, error) {
|
||||
size, err := media_utils.GetPhotoDimensions(imagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FaceRectangle{
|
||||
MinX: float64(imgRec.Min.X) / float64(size.Width),
|
||||
MaxX: float64(imgRec.Max.X) / float64(size.Width),
|
||||
MinY: float64(imgRec.Min.Y) / float64(size.Height),
|
||||
MaxY: float64(imgRec.Max.Y) / float64(size.Height),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GormDataType datatype used in database
|
||||
func (fr FaceRectangle) GormDataType() string {
|
||||
return "VARCHAR(64)"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -55,8 +54,7 @@ func TestMediaURLCachePath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMediaURLGetURL(t *testing.T) {
|
||||
done := test_utils.SetEnv(string(utils.EnvAPIEndpoint), "")
|
||||
defer done()
|
||||
t.Setenv(string(utils.EnvAPIEndpoint), "")
|
||||
|
||||
photo := models.MediaURL{
|
||||
MediaName: "photo.jpg",
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/Kagami/go-face"
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
@@ -136,7 +137,7 @@ func (fd *faceDetector) classifyFace(db *gorm.DB, face *face.Face, media *models
|
||||
|
||||
match := fd.classifyDescriptor(face.Descriptor)
|
||||
|
||||
faceRect, err := models.ToDBFaceRectangle(face.Rectangle, imagePath)
|
||||
dimension, err := media_encoding.GetPhotoDimensions(imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -144,7 +145,13 @@ func (fd *faceDetector) classifyFace(db *gorm.DB, face *face.Face, media *models
|
||||
imageFace := models.ImageFace{
|
||||
MediaID: media.ID,
|
||||
Descriptor: models.FaceDescriptor(face.Descriptor),
|
||||
Rectangle: *faceRect,
|
||||
Rectangle: models.FaceRectangle{
|
||||
// Converts a pixel absolute rectangle to a relative FaceRectangle.
|
||||
MinX: float64(face.Rectangle.Min.X) / float64(dimension.Width),
|
||||
MaxX: float64(face.Rectangle.Max.X) / float64(dimension.Width),
|
||||
MinY: float64(face.Rectangle.Min.Y) / float64(dimension.Height),
|
||||
MaxY: float64(face.Rectangle.Max.Y) / float64(dimension.Height),
|
||||
},
|
||||
}
|
||||
|
||||
var faceGroup models.FaceGroup
|
||||
|
||||
@@ -4,59 +4,44 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"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/pkg/errors"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
|
||||
_ "github.com/strukturag/libheif/go/heif"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (*media_utils.PhotoDimensions, error) {
|
||||
|
||||
var siteInfo models.SiteInfo
|
||||
if err := db.First(&siteInfo).Error; err != nil {
|
||||
return nil, err
|
||||
// Dimension presents the Dimension of a image.
|
||||
type Dimension struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
inputImage, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
|
||||
// GetPhotoDimensions returns the dimension of the image `imagePath`.
|
||||
func GetPhotoDimensions(imagePath string) (Dimension, error) {
|
||||
w, h, err := executable_worker.Magick.IdentifyDimension(imagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Dimension{}, fmt.Errorf("identify dimension %q error: %w", imagePath, err)
|
||||
}
|
||||
|
||||
dimensions := media_utils.PhotoDimensionsFromRect(inputImage.Bounds())
|
||||
dimensions = dimensions.ThumbnailScale()
|
||||
|
||||
thumbImage := imaging.Resize(inputImage, dimensions.Width, dimensions.Height, imaging.NearestNeighbor)
|
||||
if err = encodeImageJPEG(thumbImage, outputPath, 60); err != nil {
|
||||
return nil, err
|
||||
return Dimension{
|
||||
Width: w,
|
||||
Height: h,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &dimensions, nil
|
||||
// EncodeThumbnail encodes a thumbnail of `inputPath`, and store it as `outputPath`.
|
||||
// It returns the dimension of the thumbnail. The thumbnail will be not bigger than 1024x1024.
|
||||
func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (Dimension, error) {
|
||||
if err := executable_worker.Magick.GenerateThumbnail(inputPath, outputPath, 1024, 1024); err != nil {
|
||||
return Dimension{}, fmt.Errorf("can't generate thumbnail of file %q: %w", inputPath, err)
|
||||
}
|
||||
|
||||
func encodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error {
|
||||
photo_file, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not create file: %s", outputPath)
|
||||
}
|
||||
defer photo_file.Close()
|
||||
|
||||
err = jpeg.Encode(photo_file, image, &jpeg.Options{Quality: jpegQuality})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return GetPhotoDimensions(outputPath)
|
||||
}
|
||||
|
||||
// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated
|
||||
|
||||
@@ -1,13 +1,37 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
)
|
||||
|
||||
const testdataBinPath = "./test_data/mock_bin"
|
||||
|
||||
func init() {
|
||||
// Avoid panic with providing flags in `test_utils/integration_setup.go`.
|
||||
flag.CommandLine.Init("executable_worker", flag.ContinueOnError)
|
||||
}
|
||||
|
||||
// SetPathWithCurrent sets PATH env to `paths` in the directory of testing files. The PATH will restore to the previous value when the test is done.
|
||||
func SetPathWithCurrent(t *testing.T, paths ...string) {
|
||||
_, file, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
t.Log("Can't get the test file. Ignore setting PATH.")
|
||||
return
|
||||
}
|
||||
|
||||
base := filepath.Dir(file)
|
||||
|
||||
for i, path := range paths {
|
||||
paths[i] = filepath.Join(base, path)
|
||||
}
|
||||
|
||||
t.Setenv("PATH", strings.Join(paths, ":"))
|
||||
}
|
||||
|
||||
func TestInitFfprobePath(t *testing.T) {
|
||||
t.Run("PathFail", func(t *testing.T) {
|
||||
err := SetFfprobePath()
|
||||
@@ -17,11 +41,8 @@ func TestInitFfprobePath(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("VersionFail", func(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
|
||||
doneEnv := test_utils.SetEnv("FAIL_WITH", "expect failure")
|
||||
defer doneEnv()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := SetFfprobePath()
|
||||
if err == nil {
|
||||
@@ -30,8 +51,7 @@ func TestInitFfprobePath(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Succeed", func(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
|
||||
err := SetFfprobePath()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,14 +5,12 @@ import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"gopkg.in/vansante/go-ffprobe.v2"
|
||||
)
|
||||
|
||||
func TestFfmpegNotExist(t *testing.T) {
|
||||
done := test_utils.SetPathWithCurrent()
|
||||
defer done()
|
||||
SetPathWithCurrent(t, "")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
@@ -34,11 +32,8 @@ func TestFfmpegNotExist(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFfmpegVersionFail(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
|
||||
doneEnv := test_utils.SetEnv("FAIL_WITH", "expect failure")
|
||||
defer doneEnv()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
@@ -60,11 +55,8 @@ func TestFfmpegVersionFail(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFfmpegIgnore(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
|
||||
doneEnv := test_utils.SetEnv("PHOTOVIEW_DISABLE_VIDEO_ENCODING", "true")
|
||||
defer doneEnv()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("PHOTOVIEW_DISABLE_VIDEO_ENCODING", "true")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
@@ -86,8 +78,7 @@ func TestFfmpegIgnore(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFfmpeg(t *testing.T) {
|
||||
done := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer done()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
@@ -96,8 +87,7 @@ func TestFfmpeg(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("EncodeMp4Failed", func(t *testing.T) {
|
||||
doneEnv := test_utils.SetEnv("FAIL_WITH", "expect failure")
|
||||
defer doneEnv()
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err == nil {
|
||||
@@ -121,8 +111,7 @@ func TestFfmpeg(t *testing.T) {
|
||||
},
|
||||
}
|
||||
t.Run("EncodeVideoThumbnailMp4Failed", func(t *testing.T) {
|
||||
doneEnv := test_utils.SetEnv("FAIL_WITH", "expect failure")
|
||||
defer doneEnv()
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeVideoThumbnail("input", "output", probeData)
|
||||
if err == nil {
|
||||
@@ -142,16 +131,12 @@ func TestFfmpeg(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFfmpegWithHWAcc(t *testing.T) {
|
||||
doneCodec := test_utils.SetEnv(utils.EnvVideoHardwareAcceleration.GetName(), "qsv")
|
||||
defer doneCodec()
|
||||
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "qsv")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
doneEnv := test_utils.SetEnv("FAIL_WITH", "expect failure")
|
||||
defer doneEnv()
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err == nil {
|
||||
@@ -162,17 +147,13 @@ func TestFfmpegWithHWAcc(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFfmpegWithCustomCOdec(t *testing.T) {
|
||||
doneCodec := test_utils.SetEnv(utils.EnvVideoHardwareAcceleration.GetName(), "_custom")
|
||||
defer doneCodec()
|
||||
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
func TestFfmpegWithCustomCodec(t *testing.T) {
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv(utils.EnvVideoHardwareAcceleration.GetName(), "_custom")
|
||||
|
||||
Ffmpeg = newFfmpegCli()
|
||||
|
||||
doneEnv := test_utils.SetEnv("FAIL_WITH", "expect failure")
|
||||
defer doneEnv()
|
||||
t.Setenv("FAIL_WITH", "expect failure")
|
||||
|
||||
err := Ffmpeg.EncodeMp4("input", "output")
|
||||
if err == nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package executable_worker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -69,3 +71,63 @@ func (cli *MagickCli) EncodeJpeg(inputPath string, outputPath string, jpegQualit
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *MagickCli) GenerateThumbnail(inputPath string, outputPath string, width, height int) error {
|
||||
if cli.err != nil {
|
||||
return fmt.Errorf("generate thumbnail %q error: magick: %w", inputPath, cli.err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
inputPath + "[0]", // If there are multiple frames (like gif), only thumbnail the first frame.
|
||||
"-thumbnail",
|
||||
fmt.Sprintf("%dx%d", width, height),
|
||||
outputPath,
|
||||
}
|
||||
|
||||
cmd := exec.Command(cli.path, args...)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("generate thumbnail with \"%s %v\" error: %w", cli.path, args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *MagickCli) IdentifyDimension(inputPath string) (width, height int, err error) {
|
||||
if cli.err != nil {
|
||||
err = fmt.Errorf("identify dimension %q error: magick: %w", inputPath, cli.err)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"identify",
|
||||
"-format",
|
||||
`{"height":%H, "width":%W}`,
|
||||
inputPath,
|
||||
}
|
||||
|
||||
cmd := exec.Command(cli.path, args...)
|
||||
|
||||
var output bytes.Buffer
|
||||
cmd.Stdout = &output
|
||||
|
||||
if e := cmd.Run(); e != nil {
|
||||
err = fmt.Errorf("identify dimension with \"%s %v\" error: %w", cli.path, args, e)
|
||||
return
|
||||
}
|
||||
|
||||
ret := struct {
|
||||
Width *int
|
||||
Height *int
|
||||
}{
|
||||
Width: &width,
|
||||
Height: &height,
|
||||
}
|
||||
|
||||
if e := json.NewDecoder(&output).Decode(&ret); e != nil {
|
||||
err = fmt.Errorf("identify dimension with \"%s %v\" error: %w", cli.path, args, e)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,13 +4,10 @@ import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
)
|
||||
|
||||
func TestMagickCliNotExist(t *testing.T) {
|
||||
done := test_utils.SetPathWithCurrent()
|
||||
defer done()
|
||||
SetPathWithCurrent(t, "")
|
||||
|
||||
Magick = newMagickCli()
|
||||
|
||||
@@ -25,14 +22,22 @@ func TestMagickCliNotExist(t *testing.T) {
|
||||
if got, want := Magick.EncodeJpeg("input", "output", 70), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.EncodeJpeg() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := Magick.GenerateThumbnail("input", "output", 100, 100), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.GenerateThumbnail() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
{
|
||||
_, _, got := Magick.IdentifyDimension("input")
|
||||
if want := ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.IdentifyDimension() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMagickCliIgnore(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
|
||||
doneDisableRaw := test_utils.SetEnv("PHOTOVIEW_DISABLE_RAW_PROCESSING", "true")
|
||||
defer doneDisableRaw()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("PHOTOVIEW_DISABLE_RAW_PROCESSING", "true")
|
||||
|
||||
Magick = newMagickCli()
|
||||
|
||||
@@ -47,14 +52,22 @@ func TestMagickCliIgnore(t *testing.T) {
|
||||
if got, want := Magick.EncodeJpeg("input", "output", 70), ErrDisabledFunction; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.EncodeJpeg() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := Magick.GenerateThumbnail("input", "output", 100, 100), ErrDisabledFunction; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.GenerateThumbnail() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
{
|
||||
_, _, got := Magick.IdentifyDimension("input")
|
||||
if want := ErrDisabledFunction; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.IdentifyDimension() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMagickCliVersionFail(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
|
||||
done := test_utils.SetEnv("FAIL_WITH", "failure")
|
||||
defer done()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
t.Setenv("FAIL_WITH", "failure")
|
||||
|
||||
Magick = newMagickCli()
|
||||
|
||||
@@ -69,11 +82,21 @@ func TestMagickCliVersionFail(t *testing.T) {
|
||||
if got, want := Magick.EncodeJpeg("input", "output", 70), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.EncodeJpeg() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
if got, want := Magick.GenerateThumbnail("input", "output", 100, 100), ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.GenerateThumbnail() = %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
{
|
||||
_, _, got := Magick.IdentifyDimension("input")
|
||||
if want := ErrNoDependency; !errors.Is(got, want) {
|
||||
t.Errorf("Magick.IdentifyDimension() = %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMagickCliFail(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
|
||||
Magick = newMagickCli()
|
||||
|
||||
@@ -81,8 +104,7 @@ func TestMagickCliFail(t *testing.T) {
|
||||
t.Fatal("MagickCli should be installed")
|
||||
}
|
||||
|
||||
done := test_utils.SetEnv("FAIL_WITH", "failure")
|
||||
defer done()
|
||||
t.Setenv("FAIL_WITH", "failure")
|
||||
|
||||
err := Magick.EncodeJpeg("input", "output", 70)
|
||||
if err == nil {
|
||||
@@ -92,11 +114,25 @@ func TestMagickCliFail(t *testing.T) {
|
||||
if got, want := err.Error(), `^encoding image with ".*/test_data/mock_bin/magick \[input -auto-orient -quality 70 output\]" error: .*$`; !regexp.MustCompile(want).MatchString(got) {
|
||||
t.Errorf(`MagickCli.EncodeJpeg(...) = %q, should be matched with reg pattern %q`, got, want)
|
||||
}
|
||||
|
||||
err = Magick.GenerateThumbnail("input", "output", 100, 100)
|
||||
if err == nil {
|
||||
t.Fatalf(`MagickCli.GenerateThumbnail(...) = nil, should be an error.`)
|
||||
}
|
||||
if got, want := err.Error(), `^generate thumbnail with ".*/test_data/mock_bin/magick \[input\[0\] -thumbnail 100x100 output\]" error: .*$`; !regexp.MustCompile(want).MatchString(got) {
|
||||
t.Errorf(`MagickCli.GenerateThumbnail(...) = %q, should be matched with reg pattern %q`, got, want)
|
||||
}
|
||||
|
||||
{
|
||||
_, _, got := Magick.IdentifyDimension("input")
|
||||
if want := `^identify dimension with ".*/test_data/mock_bin/magick \[identify -format {"height":\%H, "width":\%W} input\]" error: .*$`; !regexp.MustCompile(want).MatchString(got.Error()) {
|
||||
t.Errorf("Magick.IdentifyDimension() = %v, should be matched with reg pattern %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMagickCliSucceed(t *testing.T) {
|
||||
donePath := test_utils.SetPathWithCurrent(testdataBinPath)
|
||||
defer donePath()
|
||||
SetPathWithCurrent(t, testdataBinPath)
|
||||
|
||||
Magick = newMagickCli()
|
||||
|
||||
@@ -104,10 +140,40 @@ func TestMagickCliSucceed(t *testing.T) {
|
||||
t.Fatal("MagickCli should be installed")
|
||||
}
|
||||
|
||||
t.Run("Succeeded", func(t *testing.T) {
|
||||
t.Run("EncodeJpeg", func(t *testing.T) {
|
||||
err := Magick.EncodeJpeg("input", "output", 70)
|
||||
if err != nil {
|
||||
t.Fatalf("MagickCli.EncodeJpeg(...) = %v, should be nil.", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GenerateThumbnail", func(t *testing.T) {
|
||||
err := Magick.GenerateThumbnail("input", "output", 100, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("MagickCli.GenerateThumbnail(...) = %v, should be nil.", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IdentifyDimension", func(t *testing.T) {
|
||||
w, h, err := Magick.IdentifyDimension("input")
|
||||
if err != nil {
|
||||
t.Fatalf("MagickCli.IdentifyDimension(...) = %v, should be nil.", err)
|
||||
}
|
||||
|
||||
if got, want := w, 1000; got != want {
|
||||
t.Errorf("got = %d, want = %d", got, want)
|
||||
}
|
||||
if got, want := h, 800; got != want {
|
||||
t.Errorf("got = %d, want = %d", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IdentifyDimensionInvalidJSON", func(t *testing.T) {
|
||||
t.Setenv("INVALID_OUTPUT", `{"width":1000,`)
|
||||
|
||||
_, _, err := Magick.IdentifyDimension("input")
|
||||
if want := `unexpected EOF$`; !regexp.MustCompile(want).MatchString(err.Error()) {
|
||||
t.Errorf("MagickCli.IdentifyDimension() = error(%v), which should match with regexp %q", err, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
set -eu
|
||||
|
||||
: ${FAIL_WITH=""}
|
||||
|
||||
if [ "$1" = "-version" ]
|
||||
then
|
||||
echo magick: version fake
|
||||
fi
|
||||
: ${INVALID_OUTPUT=""}
|
||||
|
||||
if [ "${FAIL_WITH}" != "" ]
|
||||
then
|
||||
@@ -14,4 +10,26 @@ then
|
||||
exit -1
|
||||
fi
|
||||
|
||||
if [ "$#" = "0" ] # no args
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
"-version")
|
||||
echo magick: version fake
|
||||
exit 0
|
||||
;;
|
||||
"identify")
|
||||
if [ "${INVALID_OUTPUT}" != "" ]
|
||||
then
|
||||
echo -n ${INVALID_OUTPUT}
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -n '{"height":800, "width":1000}'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo $@
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package media_utils
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
)
|
||||
|
||||
type PhotoDimensions struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func GetPhotoDimensions(imagePath string) (*PhotoDimensions, error) {
|
||||
photoFile, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer photoFile.Close()
|
||||
|
||||
config, _, err := image.DecodeConfig(photoFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PhotoDimensions{
|
||||
Width: config.Width,
|
||||
Height: config.Height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PhotoDimensionsFromRect(rect image.Rectangle) PhotoDimensions {
|
||||
return PhotoDimensions{
|
||||
Width: rect.Bounds().Max.X,
|
||||
Height: rect.Bounds().Max.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (dimensions *PhotoDimensions) ThumbnailScale() PhotoDimensions {
|
||||
aspect := float64(dimensions.Width) / float64(dimensions.Height)
|
||||
|
||||
var width, height int
|
||||
|
||||
if aspect > 1 {
|
||||
width = 1024
|
||||
height = int(1024 / aspect)
|
||||
} else {
|
||||
width = int(1024 * aspect)
|
||||
height = 1024
|
||||
}
|
||||
|
||||
if width > dimensions.Width {
|
||||
width = dimensions.Width
|
||||
height = dimensions.Height
|
||||
}
|
||||
|
||||
return PhotoDimensions{
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
}
|
||||
59
api/scanner/perf_test.go
Normal file
59
api/scanner/perf_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/executable_worker"
|
||||
)
|
||||
|
||||
func BenchmarkStdlib(b *testing.B) {
|
||||
dir := b.TempDir()
|
||||
|
||||
for b.Loop() {
|
||||
func() {
|
||||
input, err := os.Open("./test_media/real_media/png.png")
|
||||
if err != nil {
|
||||
b.Fatal("open error:", err)
|
||||
}
|
||||
defer input.Close()
|
||||
|
||||
img, _, err := image.Decode(input)
|
||||
if err != nil {
|
||||
b.Fatal("decode error:", err)
|
||||
}
|
||||
|
||||
outfile := filepath.Join(dir, "test.jpg")
|
||||
defer os.Remove(outfile)
|
||||
|
||||
output, err := os.Create(outfile)
|
||||
if err != nil {
|
||||
b.Fatal("create error:", err)
|
||||
}
|
||||
defer output.Close()
|
||||
|
||||
if err := jpeg.Encode(output, img, &jpeg.Options{Quality: 70}); err != nil {
|
||||
b.Fatal("encode error:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMagickCLI(b *testing.B) {
|
||||
dir := b.TempDir()
|
||||
|
||||
for b.Loop() {
|
||||
func() {
|
||||
output := filepath.Join(dir, "test.jpg")
|
||||
defer os.Remove(output)
|
||||
|
||||
if err := executable_worker.Magick.EncodeJpeg("./test_media/real_media/png.png", output, 70); err != nil {
|
||||
b.Fatal("encode jpeg error:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -136,18 +136,20 @@ func (queue *ScannerQueue) CloseBackgroundWorker() {
|
||||
func (queue *ScannerQueue) processQueue(notifyThrottle *utils.Throttle) {
|
||||
log.Println("Queue waiting for lock")
|
||||
queue.mutex.Lock()
|
||||
log.Printf("Queue running: in_progress: %d, max_tasks: %d, queue_len: %d\n", len(queue.in_progress), queue.settings.max_concurrent_tasks, len(queue.up_next))
|
||||
maxJobs := queue.settings.max_concurrent_tasks
|
||||
log.Printf("Queue running: in_progress: %d, max_tasks: %d, queue_len: %d\n", len(queue.in_progress), maxJobs, len(queue.up_next))
|
||||
|
||||
for len(queue.in_progress) < queue.settings.max_concurrent_tasks && len(queue.up_next) > 0 {
|
||||
for len(queue.in_progress) < maxJobs && len(queue.up_next) > 0 {
|
||||
log.Println("Queue starting job")
|
||||
nextJob := queue.up_next[0]
|
||||
queue.up_next = queue.up_next[1:]
|
||||
queue.in_progress = append(queue.in_progress, nextJob)
|
||||
jobNum := len(queue.in_progress)
|
||||
|
||||
go func() {
|
||||
log.Println("Starting job")
|
||||
log.Printf("Starting job %d/%d\n", jobNum, maxJobs)
|
||||
nextJob.Run(queue.db)
|
||||
log.Println("Job finished")
|
||||
log.Printf("Finished job %d/%d\n", jobNum, maxJobs)
|
||||
|
||||
// Delete finished job from queue
|
||||
queue.mutex.Lock()
|
||||
|
||||
@@ -73,8 +73,7 @@ func TestCounterpartFilesTaskMediaFound(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
done := test_utils.SetEnv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing))
|
||||
defer done()
|
||||
t.Setenv(string(utils.EnvDisableRawProcessing), fmt.Sprintf("%v", tc.disableRawProcessing))
|
||||
|
||||
ctx := scanner_task.NewTaskContext(context.Background(), nil, nil, nil)
|
||||
|
||||
|
||||
@@ -8,17 +8,8 @@ import (
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
||||
"github.com/photoview/photoview/api/scanner/scanner_task"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
// Image decoders
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type ProcessPhotoTask struct {
|
||||
@@ -91,12 +82,11 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
}
|
||||
}
|
||||
|
||||
var photoDimensions *media_utils.PhotoDimensions
|
||||
// Save original photo to database
|
||||
if origURL == nil {
|
||||
|
||||
// Make sure photo dimensions is set
|
||||
photoDimensions, err = media_utils.GetPhotoDimensions(baseImagePath)
|
||||
photoDimensions, err := media_encoding.GetPhotoDimensions(baseImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, err
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"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/scanner_task"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
@@ -144,7 +143,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title)
|
||||
}
|
||||
|
||||
thumbDimensions, err := media_utils.GetPhotoDimensions(thumbImagePath)
|
||||
thumbDimensions, err := media_encoding.GetPhotoDimensions(thumbImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "get dimensions of video thumbnail image")
|
||||
}
|
||||
@@ -182,7 +181,7 @@ func (t ProcessVideoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
|
||||
return []*models.MediaURL{}, errors.Wrapf(err, "failed to generate thumbnail for video (%s)", video.Title)
|
||||
}
|
||||
|
||||
thumbDimensions, err := media_utils.GetPhotoDimensions(thumbImagePath)
|
||||
thumbDimensions, err := media_encoding.GetPhotoDimensions(thumbImagePath)
|
||||
if err != nil {
|
||||
return []*models.MediaURL{}, errors.Wrap(err, "get dimensions of video thumbnail image")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -18,7 +17,7 @@ func generateSaveHighResJPEG(tx *gorm.DB, media *models.Media, imageData *media_
|
||||
return nil, errors.Wrap(err, "creating high-res cached image")
|
||||
}
|
||||
|
||||
photoDimensions, err := media_utils.GetPhotoDimensions(imagePath)
|
||||
photoDimensions, err := media_encoding.GetPhotoDimensions(imagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding"
|
||||
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
||||
"github.com/photoview/photoview/api/utils"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
@@ -51,7 +50,7 @@ func generateUniqueMediaName(mediaPath string) string {
|
||||
return mediaName
|
||||
}
|
||||
|
||||
func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *media_encoding.EncodeMediaData, photoDimensions *media_utils.PhotoDimensions) (*models.MediaURL, error) {
|
||||
func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *media_encoding.EncodeMediaData, photoDimensions media_encoding.Dimension) (*models.MediaURL, error) {
|
||||
originalImageName := generateUniqueMediaName(photo.Path)
|
||||
|
||||
contentType, err := imageData.ContentType()
|
||||
|
||||
@@ -38,7 +38,6 @@ func TestFullScan(t *testing.T) {
|
||||
|
||||
wantNoImages := []string{
|
||||
"avi.avi",
|
||||
"bmp.bmp",
|
||||
"mkv.mkv",
|
||||
"mp4.mp4",
|
||||
"mpeg.mpg",
|
||||
@@ -47,12 +46,9 @@ func TestFullScan(t *testing.T) {
|
||||
"webm.webm",
|
||||
"wmv.wmv",
|
||||
}
|
||||
wantImages := []string{
|
||||
"gif.gif",
|
||||
"webp.webp",
|
||||
}
|
||||
wantThumbnailsImages := []string{
|
||||
"buttercup_close_summer_yellow.jpg",
|
||||
"gif.gif",
|
||||
"lilac_lilac_bush_lilac.jpg",
|
||||
"mount_merapi_volcano_indonesia.jpg",
|
||||
"boy1.jpg",
|
||||
@@ -62,10 +58,14 @@ func TestFullScan(t *testing.T) {
|
||||
"girl_blond2.jpg",
|
||||
"girl_blond3.jpg",
|
||||
|
||||
"bmp.bmp",
|
||||
"jpeg.jpg",
|
||||
"jpg_with_file.jpg",
|
||||
"webp.webp",
|
||||
"png.png",
|
||||
"standalone_jpg.jpg",
|
||||
|
||||
"recoverable_bad_rst_marker.jpg",
|
||||
}
|
||||
wantHighresImages := []string{
|
||||
"heif.heif",
|
||||
@@ -109,7 +109,7 @@ func TestFullScan(t *testing.T) {
|
||||
t.Fatal("get all media error:", err)
|
||||
}
|
||||
|
||||
want := slices.Clone(wantImages)
|
||||
var want []string
|
||||
want = append(want, wantNoImages...)
|
||||
want = append(want, wantThumbnailsImages...)
|
||||
want = append(want, wantHighresImages...)
|
||||
@@ -132,7 +132,7 @@ func TestFullScan(t *testing.T) {
|
||||
t.Fatal("get all media url error:", err)
|
||||
}
|
||||
|
||||
want := slices.Clone(wantImages)
|
||||
var want []string
|
||||
|
||||
wantThumbs := slices.Clone(wantThumbnailsImages)
|
||||
want = append(want, wantThumbnailsImages...)
|
||||
|
||||
BIN
api/scanner/test_media/bad_media/recoverable_bad_rst_marker.jpg
Normal file
BIN
api/scanner/test_media/bad_media/recoverable_bad_rst_marker.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 14 B After Width: | Height: | Size: 368 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 B After Width: | Height: | Size: 30 KiB |
@@ -3,6 +3,7 @@ package scanner
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
@@ -40,7 +41,7 @@ func GenerateBlurhashes(db *gorm.DB) error {
|
||||
|
||||
hashStr, err := GenerateBlurhashFromThumbnail(thumbnail)
|
||||
if err != nil {
|
||||
log.Printf("failed to generate blurhash for media (%d): %v", row.ID, err)
|
||||
log.Printf("failed to generate blurhash: %v", err)
|
||||
processErrors = append(processErrors, err)
|
||||
continue
|
||||
}
|
||||
@@ -72,26 +73,26 @@ func GenerateBlurhashes(db *gorm.DB) error {
|
||||
func GenerateBlurhashFromThumbnail(thumbnail *models.MediaURL) (string, error) {
|
||||
thumbnail_path, err := thumbnail.CachedPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("get path of media id=%d error: %w", thumbnail.MediaID, err)
|
||||
}
|
||||
|
||||
imageFile, err := os.Open(thumbnail_path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("open %s error: %w", thumbnail_path, err)
|
||||
}
|
||||
|
||||
imageData, _, err := image.Decode(imageFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("decode %q error: %w", thumbnail_path, err)
|
||||
}
|
||||
|
||||
hashStr, err := blurhash.Encode(4, 3, imageData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("encode blurhash of %q error: %w", thumbnail_path, err)
|
||||
}
|
||||
|
||||
// if err := db.Model(&models.Media{}).Where("id = ?", thumbnail.MediaID).Update("blurhash", hashStr).Error; err != nil {
|
||||
// return "", err
|
||||
// return "", fmt.Errorf("update blurhash of media id=%d error: %w", thumbnail.MediaID, err)
|
||||
// }
|
||||
|
||||
return hashStr, nil
|
||||
|
||||
@@ -1,42 +1,11 @@
|
||||
package test_utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SetPathWithCurrent(paths ...string) func() {
|
||||
_, file, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
return func() {
|
||||
// Return an empty function in case of error
|
||||
}
|
||||
}
|
||||
|
||||
base := filepath.Dir(file)
|
||||
|
||||
for i, path := range paths {
|
||||
paths[i] = filepath.Join(base, path)
|
||||
}
|
||||
|
||||
originalPath := os.Getenv("PATH")
|
||||
os.Setenv("PATH", strings.Join(paths, ":"))
|
||||
|
||||
return func() {
|
||||
os.Setenv("PATH", originalPath)
|
||||
}
|
||||
}
|
||||
|
||||
func SetEnv(key, value string) func() {
|
||||
org := os.Getenv(key)
|
||||
os.Setenv(key, value)
|
||||
return func() {
|
||||
os.Setenv(key, org)
|
||||
}
|
||||
}
|
||||
|
||||
// PathFromAPIRoot returns the real path in the API project root.
|
||||
func PathFromAPIRoot(rootRelatedPath string) string {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package test_utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -11,16 +10,3 @@ func TestPathFromAPIRoot(t *testing.T) {
|
||||
t.Fatalf(`PathFromAPIRoot("./server.go") = %q, want a suffix: %q`, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPathWithCurrent(t *testing.T) {
|
||||
pathDone := SetEnv("PATH", "")
|
||||
defer pathDone()
|
||||
|
||||
testDone := SetPathWithCurrent("./test")
|
||||
defer testDone()
|
||||
|
||||
path := os.Getenv("PATH")
|
||||
if got, want := path, "api/test_utils/test"; !strings.HasSuffix(got, want) {
|
||||
t.Errorf("path = %q, want a suffix: %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ set -eu
|
||||
: ${DEB_HOST_ARCH=`dpkg --print-architecture`}
|
||||
echo Arch: ${DEB_HOST_ARCH}
|
||||
|
||||
# Install go-heif dependencies
|
||||
apt-get install -y libdav1d-dev:${DEB_HOST_ARCH} libde265-dev:${DEB_HOST_ARCH} libjpeg62-turbo-dev:${DEB_HOST_ARCH} libopenh264-dev:${DEB_HOST_ARCH} libpng-dev:${DEB_HOST_ARCH} libnuma-dev:${DEB_HOST_ARCH} zlib1g-dev:${DEB_HOST_ARCH}
|
||||
|
||||
# Install go-face dependencies
|
||||
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}
|
||||
|
||||
@@ -31,7 +31,7 @@ GOOS="$TARGETOS"
|
||||
GOARCH="$TARGETARCH"
|
||||
|
||||
GOARM="7"
|
||||
if [ "$TARGETARCH" = "arm" && ! -z "$TARGETVARIANT" ]; then
|
||||
if [ "$TARGETARCH" = "arm" ] && [ ! -z "$TARGETVARIANT" ]; then
|
||||
case "$TARGETVARIANT" in
|
||||
"v5")
|
||||
export GOARM="5"
|
||||
|
||||
Reference in New Issue
Block a user