diff options
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | cmd/server/main.go | 12 | ||||
-rw-r--r-- | go.mod | 4 | ||||
-rw-r--r-- | go.sum | 39 | ||||
-rw-r--r-- | pkg/database/repository/media.go | 11 | ||||
-rw-r--r-- | pkg/database/sql/media.go | 88 | ||||
-rw-r--r-- | pkg/database/sql/migration.go | 1 | ||||
-rw-r--r-- | pkg/fileop/file.go | 17 | ||||
-rw-r--r-- | pkg/fileop/thumbnail.go | 60 | ||||
-rw-r--r-- | pkg/view/media.go | 14 | ||||
-rw-r--r-- | pkg/worker/exif_scanner.go | 7 | ||||
-rw-r--r-- | pkg/worker/file_scanner.go | 25 | ||||
-rw-r--r-- | pkg/worker/thumbnail_scanner.go | 62 | ||||
-rw-r--r-- | templates/media.html | 12 |
14 files changed, 320 insertions, 33 deletions
@@ -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 702ca6e..1bd445c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -33,6 +33,7 @@ func main() { 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 @@ func main() { // 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 @@ func main() { 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() @@ -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 @@ require ( 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 @@ -5,6 +5,8 @@ github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9 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/b9FG4Oi9Mr3q7XYx6Kllzaw 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 2e94ff3..6ab4ee6 100644 --- a/pkg/database/repository/media.go +++ b/pkg/database/repository/media.go @@ -34,6 +34,10 @@ type ( GPSLongitude *float64 } + MediaThumbnail struct { + Path string + } + Pagination struct { Page int Size int @@ -52,10 +56,15 @@ type ( 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 27f8cf0..b8203f3 100644 --- a/pkg/database/sql/media.go +++ b/pkg/database/sql/media.go @@ -41,6 +41,13 @@ type ( GPSLongitude *float64 } + MediaThumbnail struct { + gorm.Model + Path string + MediaID uint + Media Media + } + MediaRepository struct { db *gorm.DB } @@ -79,6 +86,12 @@ func (m *MediaEXIF) ToModel() *repository.MediaEXIF { } } +func (m *MediaThumbnail) ToModel() *repository.MediaThumbnail { + return &repository.MediaThumbnail{ + Path: m.Path, + } +} + func NewMediaRepository(db *gorm.DB) *MediaRepository { return &MediaRepository{ db: db, @@ -173,6 +186,24 @@ func (self *MediaRepository) GetPath(ctx context.Context, pathHash string) (stri 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 @@ func (s *MediaRepository) CreateEXIF(ctx context.Context, id uint, info *reposit 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 @@ func (r *MediaRepository) GetEmptyEXIF(ctx context.Context, pagination *reposito 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 019eb91..076bf69 100644 --- a/pkg/database/sql/migration.go +++ b/pkg/database/sql/migration.go @@ -8,6 +8,7 @@ func Migrate(db *gorm.DB) error { &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 0000000..07c08e5 --- /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 0000000..32f6064 --- /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 ce9e272..0b588f4 100644 --- a/pkg/view/media.go +++ b/pkg/view/media.go @@ -93,9 +93,23 @@ func (self *MediaView) GetImage(ctx *fasthttp.RequestCtx) error { 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 97790a0..5ea1810 100644 --- a/pkg/worker/exif_scanner.go +++ b/pkg/worker/exif_scanner.go @@ -23,15 +23,10 @@ func NewEXIFScanner(repository repository.MediaRepository) *EXIFScanner { } 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 aa79035..b4f907a 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 @@ func (f *FileScanner) Query(ctx context.Context) (<-chan string, error) { } 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 @@ func (f *FileScanner) Process(ctx context.Context, path string) error { } 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 0000000..cc201b8 --- /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 6302a57..188d5b4 100644 --- a/templates/media.html +++ b/templates/media.html @@ -5,13 +5,13 @@ {{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> |