Add blurhash generation
This commit is contained in:
parent
8868b4a584
commit
b4ad1c4f88
|
@ -7,6 +7,7 @@ require (
|
||||||
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e
|
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e
|
||||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||||
github.com/barasher/go-exiftool v1.7.0
|
github.com/barasher/go-exiftool v1.7.0
|
||||||
|
github.com/buckket/go-blurhash v1.1.0 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
|
|
|
@ -15,6 +15,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/barasher/go-exiftool v1.7.0 h1:EOGb5D6TpWXmqsnEjJ0ai6+tIW2gZFwIoS9O/33Nixs=
|
github.com/barasher/go-exiftool v1.7.0 h1:EOGb5D6TpWXmqsnEjJ0ai6+tIW2gZFwIoS9O/33Nixs=
|
||||||
github.com/barasher/go-exiftool v1.7.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
github.com/barasher/go-exiftool v1.7.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
|
||||||
|
github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do=
|
||||||
|
github.com/buckket/go-blurhash v1.1.0/go.mod h1:aT2iqo5W9vu9GpyoLErKfTHwgODsZp3bQfXjXJUxNb8=
|
||||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
|
|
@ -29,6 +29,7 @@ type Media struct {
|
||||||
SideCarPath *string
|
SideCarPath *string
|
||||||
SideCarHash *string `gorm:"unique"`
|
SideCarHash *string `gorm:"unique"`
|
||||||
Faces []*ImageFace `gorm:"constraint:OnDelete:CASCADE;"`
|
Faces []*ImageFace `gorm:"constraint:OnDelete:CASCADE;"`
|
||||||
|
Blurhash *string `gorm:""`
|
||||||
|
|
||||||
// Only used internally
|
// Only used internally
|
||||||
CounterpartPath *string `gorm:"-"`
|
CounterpartPath *string `gorm:"-"`
|
||||||
|
@ -49,6 +50,21 @@ func (m *Media) Date() time.Time {
|
||||||
return m.DateShot
|
return m.DateShot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Media) Thumbnail() (*MediaURL, error) {
|
||||||
|
if len(m.MediaURL) == 0 {
|
||||||
|
return nil, errors.New("media.MediaURL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, url := range m.MediaURL {
|
||||||
|
if url.Purpose == PhotoThumbnail || url.Purpose == VideoThumbnail {
|
||||||
|
url.Media = m
|
||||||
|
return &url, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
type MediaType string
|
type MediaType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
"github.com/photoview/photoview/api/graphql/notification"
|
"github.com/photoview/photoview/api/graphql/notification"
|
||||||
"github.com/photoview/photoview/api/scanner/scanner_cache"
|
"github.com/photoview/photoview/api/scanner/scanner_cache"
|
||||||
|
"github.com/photoview/photoview/api/scanner/scanner_utils"
|
||||||
"github.com/photoview/photoview/api/utils"
|
"github.com/photoview/photoview/api/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
@ -156,8 +157,20 @@ func (queue *ScannerQueue) processQueue(notifyThrottle *utils.Throttle) {
|
||||||
notification.BroadcastNotification(&models.Notification{
|
notification.BroadcastNotification(&models.Notification{
|
||||||
Key: "global-scanner-progress",
|
Key: "global-scanner-progress",
|
||||||
Type: models.NotificationTypeMessage,
|
Type: models.NotificationTypeMessage,
|
||||||
Header: fmt.Sprintf("Scanner complete"),
|
Header: "Generating blurhashes",
|
||||||
Content: fmt.Sprintf("All jobs have been scanned"),
|
Content: "Generating blurhashes for newly scanned media",
|
||||||
|
Positive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := GenerateBlurhashes(queue.db); err != nil {
|
||||||
|
scanner_utils.ScannerError("Failed to generate blurhashes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.BroadcastNotification(&models.Notification{
|
||||||
|
Key: "global-scanner-progress",
|
||||||
|
Type: models.NotificationTypeMessage,
|
||||||
|
Header: "Scanner complete",
|
||||||
|
Content: "All jobs have been scanned",
|
||||||
Positive: true,
|
Positive: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -165,7 +178,7 @@ func (queue *ScannerQueue) processQueue(notifyThrottle *utils.Throttle) {
|
||||||
notification.BroadcastNotification(&models.Notification{
|
notification.BroadcastNotification(&models.Notification{
|
||||||
Key: "global-scanner-progress",
|
Key: "global-scanner-progress",
|
||||||
Type: models.NotificationTypeMessage,
|
Type: models.NotificationTypeMessage,
|
||||||
Header: fmt.Sprintf("Scanning media"),
|
Header: "Scanning media",
|
||||||
Content: fmt.Sprintf("%d jobs in progress\n%d jobs waiting", in_progress_length, up_next_length),
|
Content: fmt.Sprintf("%d jobs in progress\n%d jobs waiting", in_progress_length, up_next_length),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -191,7 +204,9 @@ func AddAllToQueue() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
AddUserToQueue(user)
|
if err := AddUserToQueue(user); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to add user for scanning (%d)", user.ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
package scanner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/buckket/go-blurhash"
|
||||||
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateBlurhashes queries the database for media that are missing a blurhash and computes one for them.
|
||||||
|
// This function blocks until all hashes have been computed
|
||||||
|
func GenerateBlurhashes(db *gorm.DB) error {
|
||||||
|
var results []*models.Media
|
||||||
|
|
||||||
|
processErrors := make([]error, 0)
|
||||||
|
|
||||||
|
query := db.Model(&models.Media{}).
|
||||||
|
Preload("MediaURL").
|
||||||
|
Joins("INNER JOIN media_urls ON media.id = media_urls.media_id").
|
||||||
|
Where("blurhash IS NULL").
|
||||||
|
Where("media_urls.purpose = 'thumbnail' OR media_urls.purpose = 'video-thumbnail'")
|
||||||
|
|
||||||
|
err := query.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
|
||||||
|
log.Printf("generating %d blurhashes", len(results))
|
||||||
|
|
||||||
|
hashes := make([]*string, len(results))
|
||||||
|
|
||||||
|
for i, row := range results {
|
||||||
|
|
||||||
|
thumbnail, err := row.Thumbnail()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to get thumbnail for media to generate blurhash (%d): %v", row.ID, err)
|
||||||
|
processErrors = append(processErrors, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hashStr, err := GenerateBlurhashFromThumbnail(thumbnail)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to generate blurhash for media (%d): %v", row.ID, err)
|
||||||
|
processErrors = append(processErrors, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes[i] = &hashStr
|
||||||
|
results[i].Blurhash = &hashStr
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Save(results)
|
||||||
|
// if err := db.Update("blurhash", hashes).Error; err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(processErrors) == 0 {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to generate %d blurhashes", len(processErrors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateBlurhashFromThumbnail generates a blurhash for a single media and stores it in the database
|
||||||
|
func GenerateBlurhashFromThumbnail(thumbnail *models.MediaURL) (string, error) {
|
||||||
|
thumbnail_path, err := thumbnail.CachedPath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile, err := os.Open(thumbnail_path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageData, _, err := image.Decode(imageFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashStr, err := blurhash.Encode(4, 3, imageData)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if err := db.Model(&models.Media{}).Where("id = ?", thumbnail.MediaID).Update("blurhash", hashStr).Error; err != nil {
|
||||||
|
// return "", err
|
||||||
|
// }
|
||||||
|
|
||||||
|
return hashStr, nil
|
||||||
|
}
|
Loading…
Reference in New Issue