diff --git a/Makefile b/Makefile
index 397ebd3f43fff5b73d65830d2e445cfc8c60fb53..b18993b3a1d074320216b62d9f8344ba839d51fa 100644
--- a/Makefile
+++ b/Makefile
@@ -21,6 +21,7 @@ run: sass
$(GO_RUN) $(SERVER) \
--log-level error \
--aes-key=6368616e676520746869732070617373 \
+ --cache-path=${HOME}/.thumb \
--root=${HOME}
sass:
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 702ca6e7756dc49c9e23d3a6483a0caada72042b..1bd445c277b1dafd0150aedc42880d55d3e008ca 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -33,6 +33,7 @@ dbType = flag.String("db-type", "sqlite", "Database to be used. Choose either mysql, psql or sqlite")
dbCon = flag.String("db-con", "main.db", "Database string connection for given database type. Ref: https://gorm.io/docs/connecting_to_the_database.html")
logLevel = flag.String("log-level", "error", "Log level: Choose either trace, debug, info, warning, error, fatal or panic")
schedulerCount = flag.Uint("scheduler-count", 10, "How many workers are created to process media files")
+ cachePath = flag.String("cache-path", "", "Folder to store thumbnail image")
// TODO: this will later be replaced by user specific root folder
root = flag.String("root", "", "root folder for the whole application. All the workers will use it as working directory")
@@ -112,8 +113,9 @@ }
// processors
var (
- fileScanner = worker.NewFileScanner(*root, mediaRepository)
- exifScanner = worker.NewEXIFScanner(mediaRepository)
+ fileScanner = worker.NewFileScanner(*root, mediaRepository)
+ exifScanner = worker.NewEXIFScanner(mediaRepository)
+ thumbnailScanner = worker.NewThumbnailScanner(*cachePath, mediaRepository)
)
// worker
@@ -129,12 +131,18 @@ exifScanner,
scheduler,
logrus.WithField("context", "exif scanner"),
)
+ thumbnailWorker = worker.NewWorkerFromBatchProcessor[*repository.Media](
+ thumbnailScanner,
+ scheduler,
+ logrus.WithField("context", "thumbnail scanner"),
+ )
)
pool := worker.NewWorkerPool()
pool.AddWorker("http server", serverWorker)
pool.AddWorker("exif scanner", exifWorker)
pool.AddWorker("file scanner", fileWorker)
+ pool.AddWorker("thumbnail scanner", thumbnailWorker)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
diff --git a/go.mod b/go.mod
index 519070cc76069ed724e608d7b5492c233dcdc9d7..a473a48a10f27c01829261445a200578185a04dc 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.19
require (
github.com/barasher/go-exiftool v1.10.0
+ github.com/disintegration/imaging v1.6.2
github.com/fasthttp/router v1.4.19
github.com/google/go-cmp v0.5.9
github.com/sirupsen/logrus v1.9.2
@@ -29,9 +30,10 @@ github.com/klauspost/compress v1.16.5 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
+ golang.org/x/image v0.8.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
- golang.org/x/text v0.9.0 // indirect
+ golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.9.3 // indirect
gorm.io/datatypes v1.2.0 // indirect
gorm.io/hints v1.1.2 // indirect
diff --git a/go.sum b/go.sum
index b90e74709efdc06de5807a334e87b48862661cfc..8694f645250aa9eae9b6d9d74ca7452bc73887f2 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -48,18 +50,51 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
+golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/pkg/database/repository/media.go b/pkg/database/repository/media.go
index 2e94ff3f45dd4fa6188e3ab389f68bed6e62b925..6ab4ee673adc2f12d4227113c9ef4822b5d34d0b 100644
--- a/pkg/database/repository/media.go
+++ b/pkg/database/repository/media.go
@@ -34,6 +34,10 @@ GPSLatitude *float64
GPSLongitude *float64
}
+ MediaThumbnail struct {
+ Path string
+ }
+
Pagination struct {
Page int
Size int
@@ -52,10 +56,15 @@ Exists(context.Context, string) (bool, error)
List(context.Context, *Pagination) ([]*Media, error)
Get(context.Context, string) (*Media, error)
GetPath(context.Context, string) (string, error)
+ GetThumbnailPath(context.Context, string) (string, error)
- GetEmptyEXIF(context.Context, *Pagination) ([]*Media, error)
+ ListEmptyEXIF(context.Context, *Pagination) ([]*Media, error)
GetEXIF(context.Context, uint) (*MediaEXIF, error)
CreateEXIF(context.Context, uint, *MediaEXIF) error
+
+ ListEmptyThumbnail(context.Context, *Pagination) ([]*Media, error)
+ GetThumbnail(context.Context, uint) (*MediaThumbnail, error)
+ CreateThumbnail(context.Context, uint, *MediaThumbnail) error
}
)
diff --git a/pkg/database/sql/media.go b/pkg/database/sql/media.go
index 27f8cf0f642ad82c9b6bec7480d0d31ada616ab8..b8203f382fb83a92aa9d8447afda5918188166af 100644
--- a/pkg/database/sql/media.go
+++ b/pkg/database/sql/media.go
@@ -41,6 +41,13 @@ GPSLatitude *float64
GPSLongitude *float64
}
+ MediaThumbnail struct {
+ gorm.Model
+ Path string
+ MediaID uint
+ Media Media
+ }
+
MediaRepository struct {
db *gorm.DB
}
@@ -76,6 +83,12 @@ Orientation: m.Orientation,
ExposureProgram: m.ExposureProgram,
GPSLatitude: m.GPSLatitude,
GPSLongitude: m.GPSLongitude,
+ }
+}
+
+func (m *MediaThumbnail) ToModel() *repository.MediaThumbnail {
+ return &repository.MediaThumbnail{
+ Path: m.Path,
}
}
@@ -173,6 +186,24 @@
return path, nil
}
+func (self *MediaRepository) GetThumbnailPath(ctx context.Context, pathHash string) (string, error) {
+ var path string
+ result := self.db.
+ WithContext(ctx).
+ Model(&Media{}).
+ Select("media_thumbnails.path").
+ Joins("left join media_thumbnails on media.id = media_thumbnails.media_id").
+ Where("media.path_hash = ?", pathHash).
+ Limit(1).
+ Find(&path)
+
+ if result.Error != nil {
+ return "", result.Error
+ }
+
+ return path, nil
+}
+
func (m *MediaRepository) GetEXIF(ctx context.Context, mediaID uint) (*repository.MediaEXIF, error) {
exif := &MediaEXIF{}
result := m.db.
@@ -220,7 +251,7 @@
return nil
}
-func (r *MediaRepository) GetEmptyEXIF(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
+func (r *MediaRepository) ListEmptyEXIF(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
medias := make([]*Media, 0)
result := r.db.
WithContext(ctx).
@@ -242,3 +273,58 @@ })
return m, nil
}
+
+func (r *MediaRepository) ListEmptyThumbnail(ctx context.Context, pagination *repository.Pagination) ([]*repository.Media, error) {
+ medias := make([]*Media, 0)
+ result := r.db.
+ WithContext(ctx).
+ Model(&Media{}).
+ Joins("left join media_thumbnails on media.id = media_thumbnails.media_id").
+ Where("media_thumbnails.media_id IS NULL").
+ Offset(pagination.Page * pagination.Size).
+ Limit(pagination.Size).
+ Order("media.created_at DESC").
+ Find(&medias)
+
+ if result.Error != nil {
+ return nil, result.Error
+ }
+
+ m := list.Map(medias, func(s *Media) *repository.Media {
+ return s.ToModel()
+ })
+
+ return m, nil
+}
+
+func (m *MediaRepository) GetThumbnail(ctx context.Context, mediaID uint) (*repository.MediaThumbnail, error) {
+ thumbnail := &MediaThumbnail{}
+ result := m.db.
+ WithContext(ctx).
+ Model(&Media{}).
+ Where("media_id = ?", mediaID).
+ Limit(1).
+ Take(m)
+
+ if result.Error != nil {
+ return nil, result.Error
+ }
+
+ return thumbnail.ToModel(), nil
+}
+
+func (m *MediaRepository) CreateThumbnail(ctx context.Context, mediaID uint, thumbnail *repository.MediaThumbnail) error {
+ media := &MediaThumbnail{
+ MediaID: mediaID,
+ Path: thumbnail.Path,
+ }
+
+ result := m.db.
+ WithContext(ctx).
+ Create(media)
+ if result.Error != nil {
+ return result.Error
+ }
+
+ return nil
+}
diff --git a/pkg/database/sql/migration.go b/pkg/database/sql/migration.go
index 019eb91371ca24ec8f1b55fa1e6b80730c716ac0..076bf69da597bead7bd132bbad5473ad73c6be4e 100644
--- a/pkg/database/sql/migration.go
+++ b/pkg/database/sql/migration.go
@@ -8,6 +8,7 @@ &User{},
&Settings{},
&Media{},
&MediaEXIF{},
+ &MediaThumbnail{},
} {
if err := db.AutoMigrate(m); err != nil {
return err
diff --git a/pkg/fileop/file.go b/pkg/fileop/file.go
new file mode 100644
index 0000000000000000000000000000000000000000..07c08e5d57eff695d8658ae7626adfb132e7f4d3
--- /dev/null
+++ b/pkg/fileop/file.go
@@ -0,0 +1,17 @@
+package fileop
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "strings"
+)
+
+func GetHashFromPath(path string) string {
+ hash := md5.Sum([]byte(path))
+ return hex.EncodeToString(hash[:])
+}
+
+func IsMimeTypeSupported(mimetype string) bool {
+ return strings.HasPrefix(mimetype, "video") &&
+ strings.HasPrefix(mimetype, "image")
+}
diff --git a/pkg/fileop/thumbnail.go b/pkg/fileop/thumbnail.go
new file mode 100644
index 0000000000000000000000000000000000000000..32f6064dc93654e14514875551c2c452fbe1d773
--- /dev/null
+++ b/pkg/fileop/thumbnail.go
@@ -0,0 +1,60 @@
+package fileop
+
+import (
+ "image"
+ "image/jpeg"
+ "os"
+ "os/exec"
+
+ "github.com/disintegration/imaging"
+)
+
+func EncodeImageThumbnail(inputPath string, outputPath string, width, height int) error {
+ inputImage, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
+ if err != nil {
+ return err
+ }
+
+ thumbImage := imaging.Fit(inputImage, width, height, imaging.Lanczos)
+ if err = encodeImageJPEG(thumbImage, outputPath, 60); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func encodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error {
+ photo_file, err := os.Create(outputPath)
+ if err != nil {
+ return err
+ }
+ defer photo_file.Close()
+
+ err = jpeg.Encode(photo_file, image, &jpeg.Options{Quality: jpegQuality})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func EncodeVideoThumbnail(inputPath string, outputPath string, width, height int) error {
+ args := []string{
+ "-i",
+ inputPath,
+ "-vframes", "1", // output one frame
+ "-an", // disable audio
+ "-vf", "scale='min(1024,iw)':'min(1024,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
+ "-vf", "select=gte(n\\,100)",
+ outputPath,
+ }
+
+ cmd := exec.Command("ffmpeg", args...)
+
+ if err := cmd.Run(); err != nil {
+ return err
+ }
+
+ return nil
+
+}
diff --git a/pkg/view/media.go b/pkg/view/media.go
index ce9e27278588eb37a883deda70eaa4b6aa4edc9c..0b588f4f4c52b7aff77b48979490d43e703b95f8 100644
--- a/pkg/view/media.go
+++ b/pkg/view/media.go
@@ -93,9 +93,23 @@ fasthttp.ServeFileUncompressed(ctx, media.Path)
return nil
}
+func (self *MediaView) GetThumbnail(ctx *fasthttp.RequestCtx) error {
+ pathHash := string(ctx.FormValue("path_hash"))
+
+ path, err := self.mediaRepository.GetThumbnailPath(ctx, pathHash)
+ if err != nil {
+ return self.GetImage(ctx)
+ }
+
+ ctx.Request.Header.SetContentType("image/jpeg")
+ fasthttp.ServeFileUncompressed(ctx, path)
+ return nil
+}
+
func (self *MediaView) SetMyselfIn(r *ext.Router) {
r.GET("/media", self.Index)
r.POST("/media", self.Index)
r.GET("/media/image", self.GetImage)
+ r.GET("/media/thumbnail", self.GetThumbnail)
}
diff --git a/pkg/worker/exif_scanner.go b/pkg/worker/exif_scanner.go
index 97790a083761672e3d03680d8e32be2eec902368..5ea18104d9be7dec5448e46321bc603d8d142e94 100644
--- a/pkg/worker/exif_scanner.go
+++ b/pkg/worker/exif_scanner.go
@@ -23,15 +23,10 @@ }
}
func (e *EXIFScanner) Query(ctx context.Context) ([]*repository.Media, error) {
- medias, err := e.repository.GetEmptyEXIF(ctx, &repository.Pagination{
+ return e.repository.ListEmptyEXIF(ctx, &repository.Pagination{
Page: 0,
Size: 100,
})
- if err != nil {
- return nil, err
- }
-
- return medias, nil
}
func (e *EXIFScanner) Process(ctx context.Context, m *repository.Media) error {
diff --git a/pkg/worker/file_scanner.go b/pkg/worker/file_scanner.go
index aa79035b77c5fb9724a149bbb0e29450583f0baa..b4f907ab682473e1082d0694b690c5a25a75c3a2 100644
--- a/pkg/worker/file_scanner.go
+++ b/pkg/worker/file_scanner.go
@@ -2,14 +2,12 @@ package worker
import (
"context"
- "crypto/md5"
- "encoding/hex"
"io/fs"
"mime"
"path/filepath"
- "strings"
"git.sr.ht/~gabrielgio/img/pkg/database/repository"
+ "git.sr.ht/~gabrielgio/img/pkg/fileop"
)
type (
@@ -59,18 +57,17 @@ return c, nil
}
func (f *FileScanner) Process(ctx context.Context, path string) error {
- m := mime.TypeByExtension(filepath.Ext(path))
- if !strings.HasPrefix(m, "video") && !strings.HasPrefix(m, "image") {
+ mimetype := mime.TypeByExtension(filepath.Ext(path))
+ supported := fileop.IsMimeTypeSupported(mimetype)
+ if !supported {
return nil
}
- hash := md5.Sum([]byte(path))
- str := hex.EncodeToString(hash[:])
- name := filepath.Base(path)
+ hash := fileop.GetHashFromPath(path)
- exists, errResp := f.repository.Exists(ctx, str)
- if errResp != nil {
- return errResp
+ exists, err := f.repository.Exists(ctx, hash)
+ if err != nil {
+ return err
}
if exists {
@@ -78,9 +75,9 @@ return nil
}
return f.repository.Create(ctx, &repository.CreateMedia{
- Name: name,
+ Name: filepath.Base(path),
Path: path,
- PathHash: str,
- MIMEType: m,
+ PathHash: hash,
+ MIMEType: mimetype,
})
}
diff --git a/pkg/worker/thumbnail_scanner.go b/pkg/worker/thumbnail_scanner.go
new file mode 100644
index 0000000000000000000000000000000000000000..cc201b830ff9d562380baa44cf80a2926cad9619
--- /dev/null
+++ b/pkg/worker/thumbnail_scanner.go
@@ -0,0 +1,62 @@
+package worker
+
+import (
+ "context"
+ "math"
+ "os"
+ "path"
+
+ "git.sr.ht/~gabrielgio/img/pkg/database/repository"
+ "git.sr.ht/~gabrielgio/img/pkg/fileop"
+)
+
+type (
+ ThumbnailScanner struct {
+ repository repository.MediaRepository
+ cachePath string
+ }
+)
+
+var _ BatchProcessor[*repository.Media] = &EXIFScanner{}
+
+func NewThumbnailScanner(cachePath string, repository repository.MediaRepository) *ThumbnailScanner {
+ return &ThumbnailScanner{
+ repository: repository,
+ cachePath: cachePath,
+ }
+}
+
+func (t *ThumbnailScanner) Query(ctx context.Context) ([]*repository.Media, error) {
+ return t.repository.ListEmptyThumbnail(ctx, &repository.Pagination{
+ Page: 0,
+ Size: 100,
+ })
+}
+
+func (t *ThumbnailScanner) Process(ctx context.Context, media *repository.Media) error {
+ split := media.PathHash[:2]
+ filename := media.PathHash[2:]
+ folder := path.Join(t.cachePath, split)
+ output := path.Join(folder, filename+".jpeg")
+
+ err := os.MkdirAll(folder, os.ModePerm)
+ if err != nil {
+ return err
+ }
+
+ if media.IsVideo() {
+ err := fileop.EncodeVideoThumbnail(media.Path, output, 1080, 1080)
+ if err != nil {
+ return err
+ }
+ } else {
+ err := fileop.EncodeImageThumbnail(media.Path, output, 1080, math.MaxInt)
+ if err != nil {
+ return err
+ }
+ }
+
+ return t.repository.CreateThumbnail(ctx, media.ID, &repository.MediaThumbnail{
+ Path: output,
+ })
+}
diff --git a/templates/media.html b/templates/media.html
index 6302a573451a953b2a02275611a63377930d0db3..188d5b4c2be930218a1ad0f7b5527c53052fc3db 100644
--- a/templates/media.html
+++ b/templates/media.html
@@ -5,13 +5,13 @@ <div class="columns is-multiline">
{{range .Data.Medias}}
<div class="card">
<div class="card-image">
- {{ if .IsVideo }}
- <video controls muted="true" preload="metadata">
- <source src="/media/image?path_hash={{.PathHash}}" type="{{.MIMEType}}">
- </video>
- {{ else }}
+ {{ if .IsVideo }}
+ <video controls muted="true" poster="/media/thumbnail?path_hash={{.PathHash}}" preload="none">
+ <source src="/media/image?path_hash={{.PathHash}}" type="{{.MIMEType}}">
+ </video>
+ {{ else }}
<figure class="image is-fit">
- <img src="/media/image?path_hash={{.PathHash}}">
+ <img src="/media/thumbnail?path_hash={{.PathHash}}">
</figure>
{{ end }}
</div>